/*
 * Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "IDBDatabase.h"

#if ENABLE(INDEXED_DATABASE)

#include "DOMStringList.h"
#include "EventNames.h"
#include "EventQueue.h"
#include "IDBConnectionProxy.h"
#include "IDBConnectionToServer.h"
#include "IDBIndex.h"
#include "IDBObjectStore.h"
#include "IDBOpenDBRequest.h"
#include "IDBResultData.h"
#include "IDBTransaction.h"
#include "IDBVersionChangeEvent.h"
#include "Logging.h"
#include "ScriptExecutionContext.h"
#include <JavaScriptCore/HeapInlines.h>

namespace WebCore {

Ref<IDBDatabase> IDBDatabase::create(ScriptExecutionContext& context, IDBClient::IDBConnectionProxy& connectionProxy, const IDBResultData& resultData)
{
    return adoptRef(*new IDBDatabase(context, connectionProxy, resultData));
}

IDBDatabase::IDBDatabase(ScriptExecutionContext& context, IDBClient::IDBConnectionProxy& connectionProxy, const IDBResultData& resultData)
    : IDBActiveDOMObject(&context)
    , m_connectionProxy(connectionProxy)
    , m_info(resultData.databaseInfo())
    , m_databaseConnectionIdentifier(resultData.databaseConnectionIdentifier())
    , m_eventNames(eventNames())
{
    LOG(IndexedDB, "IDBDatabase::IDBDatabase - Creating database %s with version %" PRIu64 " connection %" PRIu64 " (%p)", m_info.name().utf8().data(), m_info.version(), m_databaseConnectionIdentifier, this);
    suspendIfNeeded();
    m_connectionProxy->registerDatabaseConnection(*this);
}

IDBDatabase::~IDBDatabase()
{
    ASSERT(&originThread() == &Thread::current());

    if (!m_closedInServer)
        m_connectionProxy->databaseConnectionClosed(*this);

    m_connectionProxy->unregisterDatabaseConnection(*this);
}

bool IDBDatabase::hasPendingActivity() const
{
    ASSERT(&originThread() == &Thread::current() || mayBeGCThread());

    if (m_closedInServer || isContextStopped())
        return false;

    if (!m_activeTransactions.isEmpty() || !m_committingTransactions.isEmpty() || !m_abortingTransactions.isEmpty())
        return true;

    return hasEventListeners(m_eventNames.abortEvent) || hasEventListeners(m_eventNames.errorEvent) || hasEventListeners(m_eventNames.versionchangeEvent);
}

const String IDBDatabase::name() const
{
    ASSERT(&originThread() == &Thread::current());
    return m_info.name();
}

uint64_t IDBDatabase::version() const
{
    ASSERT(&originThread() == &Thread::current());
    return m_info.version();
}

Ref<DOMStringList> IDBDatabase::objectStoreNames() const
{
    ASSERT(&originThread() == &Thread::current());

    auto objectStoreNames = DOMStringList::create();
    for (auto& name : m_info.objectStoreNames())
        objectStoreNames->append(name);
    objectStoreNames->sort();
    return objectStoreNames;
}

void IDBDatabase::renameObjectStore(IDBObjectStore& objectStore, const String& newName)
{
    ASSERT(&originThread() == &Thread::current());
    ASSERT(m_versionChangeTransaction);
    ASSERT(m_info.hasObjectStore(objectStore.info().name()));

    m_info.renameObjectStore(objectStore.info().identifier(), newName);

    m_versionChangeTransaction->renameObjectStore(objectStore, newName);
}

void IDBDatabase::renameIndex(IDBIndex& index, const String& newName)
{
    ASSERT(&originThread() == &Thread::current());
    ASSERT(m_versionChangeTransaction);
    ASSERT(m_info.hasObjectStore(index.objectStore().info().name()));
    ASSERT(m_info.infoForExistingObjectStore(index.objectStore().info().name())->hasIndex(index.info().name()));

    m_info.infoForExistingObjectStore(index.objectStore().info().name())->infoForExistingIndex(index.info().identifier())->rename(newName);

    m_versionChangeTransaction->renameIndex(index, newName);
}

ExceptionOr<Ref<IDBObjectStore>> IDBDatabase::createObjectStore(const String& name, ObjectStoreParameters&& parameters)
{
    LOG(IndexedDB, "IDBDatabase::createObjectStore - (%s %s)", m_info.name().utf8().data(), name.utf8().data());

    ASSERT(&originThread() == &Thread::current());
    ASSERT(!m_versionChangeTransaction || m_versionChangeTransaction->isVersionChange());

    if (!m_versionChangeTransaction)
        return Exception { InvalidStateError, "Failed to execute 'createObjectStore' on 'IDBDatabase': The database is not running a version change transaction."_s };

    if (!m_versionChangeTransaction->isActive())
        return Exception { TransactionInactiveError };

    if (m_info.hasObjectStore(name))
        return Exception { ConstraintError, "Failed to execute 'createObjectStore' on 'IDBDatabase': An object store with the specified name already exists."_s };

    auto& keyPath = parameters.keyPath;
    if (keyPath && !isIDBKeyPathValid(keyPath.value()))
        return Exception { SyntaxError, "Failed to execute 'createObjectStore' on 'IDBDatabase': The keyPath option is not a valid key path."_s };

    if (keyPath && parameters.autoIncrement && ((WTF::holds_alternative<String>(keyPath.value()) && WTF::get<String>(keyPath.value()).isEmpty()) || WTF::holds_alternative<Vector<String>>(keyPath.value())))
        return Exception { InvalidAccessError, "Failed to execute 'createObjectStore' on 'IDBDatabase': The autoIncrement option was set but the keyPath option was empty or an array."_s };

    // Install the new ObjectStore into the connection's metadata.
    auto info = m_info.createNewObjectStore(name, WTFMove(keyPath), parameters.autoIncrement);

    // Create the actual IDBObjectStore from the transaction, which also schedules the operation server side.
    return m_versionChangeTransaction->createObjectStore(info);
}

ExceptionOr<Ref<IDBTransaction>> IDBDatabase::transaction(StringOrVectorOfStrings&& storeNames, IDBTransactionMode mode)
{
    LOG(IndexedDB, "IDBDatabase::transaction");

    ASSERT(&originThread() == &Thread::current());

    if (m_closePending)
        return Exception { InvalidStateError, "Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing."_s };

    Vector<String> objectStores;
    if (WTF::holds_alternative<Vector<String>>(storeNames))
        objectStores = WTFMove(WTF::get<Vector<String>>(storeNames));
    else
        objectStores.append(WTFMove(WTF::get<String>(storeNames)));

    if (objectStores.isEmpty())
        return Exception { InvalidAccessError, "Failed to execute 'transaction' on 'IDBDatabase': The storeNames parameter was empty."_s };

    if (mode != IDBTransactionMode::Readonly && mode != IDBTransactionMode::Readwrite)
        return Exception { TypeError };

    if (m_versionChangeTransaction && !m_versionChangeTransaction->isFinishedOrFinishing())
        return Exception { InvalidStateError, "Failed to execute 'transaction' on 'IDBDatabase': A version change transaction is running."_s };

    // It is valid for javascript to pass in a list of object store names with the same name listed twice,
    // so we need to put them all in a set to get a unique list.
    HashSet<String> objectStoreSet;
    for (auto& objectStore : objectStores)
        objectStoreSet.add(objectStore);

    objectStores = copyToVector(objectStoreSet);

    for (auto& objectStoreName : objectStores) {
        if (m_info.hasObjectStore(objectStoreName))
            continue;
        return Exception { NotFoundError, "Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found."_s };
    }

    auto info = IDBTransactionInfo::clientTransaction(m_connectionProxy.get(), objectStores, mode);

    LOG(IndexedDBOperations, "IDB creating transaction: %s", info.loggingString().utf8().data());
    auto transaction = IDBTransaction::create(*this, info);

    LOG(IndexedDB, "IDBDatabase::transaction - Added active transaction %s", info.identifier().loggingString().utf8().data());

    m_activeTransactions.set(info.identifier(), transaction.ptr());

    return transaction;
}

ExceptionOr<void> IDBDatabase::deleteObjectStore(const String& objectStoreName)
{
    LOG(IndexedDB, "IDBDatabase::deleteObjectStore");

    ASSERT(&originThread() == &Thread::current());

    if (!m_versionChangeTransaction)
        return Exception { InvalidStateError, "Failed to execute 'deleteObjectStore' on 'IDBDatabase': The database is not running a version change transaction."_s };

    if (!m_versionChangeTransaction->isActive())
        return Exception { TransactionInactiveError };

    if (!m_info.hasObjectStore(objectStoreName))
        return Exception { NotFoundError, "Failed to execute 'deleteObjectStore' on 'IDBDatabase': The specified object store was not found."_s };

    m_info.deleteObjectStore(objectStoreName);
    m_versionChangeTransaction->deleteObjectStore(objectStoreName);

    return { };
}

void IDBDatabase::close()
{
    LOG(IndexedDB, "IDBDatabase::close - %" PRIu64, m_databaseConnectionIdentifier);

    ASSERT(&originThread() == &Thread::current());

    if (!m_closePending) {
        m_closePending = true;
        m_connectionProxy->databaseConnectionPendingClose(*this);
    }

    maybeCloseInServer();
}

void IDBDatabase::didCloseFromServer(const IDBError& error)
{
    LOG(IndexedDB, "IDBDatabase::didCloseFromServer - %" PRIu64, m_databaseConnectionIdentifier);

    connectionToServerLost(error);

    m_connectionProxy->confirmDidCloseFromServer(*this);
}

void IDBDatabase::connectionToServerLost(const IDBError& error)
{
    LOG(IndexedDB, "IDBDatabase::connectionToServerLost - %" PRIu64, m_databaseConnectionIdentifier);

    ASSERT(&originThread() == &Thread::current());

    m_closePending = true;
    m_closedInServer = true;

    auto activeTransactions = copyToVector(m_activeTransactions.values());
    for (auto& transaction : activeTransactions)
        transaction->connectionClosedFromServer(error);
    
    auto committingTransactions = copyToVector(m_committingTransactions.values());
    for (auto& transaction : committingTransactions)
        transaction->connectionClosedFromServer(error);

    auto errorEvent = Event::create(m_eventNames.errorEvent, Event::CanBubble::Yes, Event::IsCancelable::No);
    errorEvent->setTarget(this);

    if (auto* context = scriptExecutionContext())
        context->eventQueue().enqueueEvent(WTFMove(errorEvent));

    auto closeEvent = Event::create(m_eventNames.closeEvent, Event::CanBubble::Yes, Event::IsCancelable::No);
    closeEvent->setTarget(this);

    if (auto* context = scriptExecutionContext())
        context->eventQueue().enqueueEvent(WTFMove(closeEvent));
}

void IDBDatabase::maybeCloseInServer()
{
    LOG(IndexedDB, "IDBDatabase::maybeCloseInServer - %" PRIu64, m_databaseConnectionIdentifier);

    ASSERT(&originThread() == &Thread::current());

    if (m_closedInServer)
        return;

    // 3.3.9 Database closing steps
    // Wait for all transactions created using this connection to complete.
    // Once they are complete, this connection is closed.
    if (!m_activeTransactions.isEmpty() || !m_committingTransactions.isEmpty())
        return;

    m_closedInServer = true;
    m_connectionProxy->databaseConnectionClosed(*this);
}

const char* IDBDatabase::activeDOMObjectName() const
{
    ASSERT(&originThread() == &Thread::current());
    return "IDBDatabase";
}

bool IDBDatabase::canSuspendForDocumentSuspension() const
{
    ASSERT(&originThread() == &Thread::current());

    // FIXME: This value will sometimes be false when database operations are actually in progress.
    // Such database operations do not yet exist.
    return true;
}

void IDBDatabase::stop()
{
    LOG(IndexedDB, "IDBDatabase::stop - %" PRIu64, m_databaseConnectionIdentifier);

    ASSERT(&originThread() == &Thread::current());

    removeAllEventListeners();

    Vector<IDBResourceIdentifier> transactionIdentifiers;
    transactionIdentifiers.reserveInitialCapacity(m_activeTransactions.size());

    for (auto& id : m_activeTransactions.keys())
        transactionIdentifiers.uncheckedAppend(id);

    for (auto& id : transactionIdentifiers) {
        IDBTransaction* transaction = m_activeTransactions.get(id);
        if (transaction)
            transaction->stop();
    }

    close();
}

Ref<IDBTransaction> IDBDatabase::startVersionChangeTransaction(const IDBTransactionInfo& info, IDBOpenDBRequest& request)
{
    LOG(IndexedDB, "IDBDatabase::startVersionChangeTransaction %s", info.identifier().loggingString().utf8().data());

    ASSERT(&originThread() == &Thread::current());
    ASSERT(!m_versionChangeTransaction);
    ASSERT(info.mode() == IDBTransactionMode::Versionchange);
    ASSERT(!m_closePending);
    ASSERT(scriptExecutionContext());

    Ref<IDBTransaction> transaction = IDBTransaction::create(*this, info, request);
    m_versionChangeTransaction = &transaction.get();

    m_activeTransactions.set(transaction->info().identifier(), &transaction.get());

    return transaction;
}

void IDBDatabase::didStartTransaction(IDBTransaction& transaction)
{
    LOG(IndexedDB, "IDBDatabase::didStartTransaction %s", transaction.info().identifier().loggingString().utf8().data());
    ASSERT(!m_versionChangeTransaction);
    ASSERT(&originThread() == &Thread::current());

    // It is possible for the client to have aborted a transaction before the server replies back that it has started.
    if (m_abortingTransactions.contains(transaction.info().identifier()))
        return;

    m_activeTransactions.set(transaction.info().identifier(), &transaction);
}

void IDBDatabase::willCommitTransaction(IDBTransaction& transaction)
{
    LOG(IndexedDB, "IDBDatabase::willCommitTransaction %s", transaction.info().identifier().loggingString().utf8().data());

    ASSERT(&originThread() == &Thread::current());

    auto refTransaction = m_activeTransactions.take(transaction.info().identifier());
    ASSERT(refTransaction);
    m_committingTransactions.set(transaction.info().identifier(), WTFMove(refTransaction));
}

void IDBDatabase::didCommitTransaction(IDBTransaction& transaction)
{
    LOG(IndexedDB, "IDBDatabase::didCommitTransaction %s", transaction.info().identifier().loggingString().utf8().data());

    ASSERT(&originThread() == &Thread::current());

    if (m_versionChangeTransaction == &transaction)
        m_info.setVersion(transaction.info().newVersion());

    didCommitOrAbortTransaction(transaction);
}

void IDBDatabase::willAbortTransaction(IDBTransaction& transaction)
{
    LOG(IndexedDB, "IDBDatabase::willAbortTransaction %s", transaction.info().identifier().loggingString().utf8().data());

    ASSERT(&originThread() == &Thread::current());

    auto refTransaction = m_activeTransactions.take(transaction.info().identifier());
    if (!refTransaction)
        refTransaction = m_committingTransactions.take(transaction.info().identifier());

    ASSERT(refTransaction);
    m_abortingTransactions.set(transaction.info().identifier(), WTFMove(refTransaction));

    if (transaction.isVersionChange()) {
        ASSERT(transaction.originalDatabaseInfo());
        m_info = *transaction.originalDatabaseInfo();
        m_closePending = true;
    }
}

void IDBDatabase::didAbortTransaction(IDBTransaction& transaction)
{
    LOG(IndexedDB, "IDBDatabase::didAbortTransaction %s", transaction.info().identifier().loggingString().utf8().data());

    ASSERT(&originThread() == &Thread::current());

    if (transaction.isVersionChange()) {
        ASSERT(transaction.originalDatabaseInfo());
        ASSERT(m_info.version() == transaction.originalDatabaseInfo()->version());
        m_closePending = true;
        maybeCloseInServer();
    }

    didCommitOrAbortTransaction(transaction);
}

void IDBDatabase::didCommitOrAbortTransaction(IDBTransaction& transaction)
{
    LOG(IndexedDB, "IDBDatabase::didCommitOrAbortTransaction %s", transaction.info().identifier().loggingString().utf8().data());

    ASSERT(&originThread() == &Thread::current());

    if (m_versionChangeTransaction == &transaction)
        m_versionChangeTransaction = nullptr;

#ifndef NDBEBUG
    unsigned count = 0;
    if (m_activeTransactions.contains(transaction.info().identifier()))
        ++count;
    if (m_committingTransactions.contains(transaction.info().identifier()))
        ++count;
    if (m_abortingTransactions.contains(transaction.info().identifier()))
        ++count;

    ASSERT(count == 1);
#endif

    m_activeTransactions.remove(transaction.info().identifier());
    m_committingTransactions.remove(transaction.info().identifier());
    m_abortingTransactions.remove(transaction.info().identifier());

    if (m_closePending)
        maybeCloseInServer();
}

void IDBDatabase::fireVersionChangeEvent(const IDBResourceIdentifier& requestIdentifier, uint64_t requestedVersion)
{
    uint64_t currentVersion = m_info.version();
    LOG(IndexedDB, "IDBDatabase::fireVersionChangeEvent - current version %" PRIu64 ", requested version %" PRIu64 ", connection %" PRIu64 " (%p)", currentVersion, requestedVersion, m_databaseConnectionIdentifier, this);

    ASSERT(&originThread() == &Thread::current());

    if (!scriptExecutionContext() || m_closePending) {
        connectionProxy().didFireVersionChangeEvent(m_databaseConnectionIdentifier, requestIdentifier);
        return;
    }
    
    Ref<Event> event = IDBVersionChangeEvent::create(requestIdentifier, currentVersion, requestedVersion, m_eventNames.versionchangeEvent);
    event->setTarget(this);
    scriptExecutionContext()->eventQueue().enqueueEvent(WTFMove(event));
}

void IDBDatabase::dispatchEvent(Event& event)
{
    LOG(IndexedDB, "IDBDatabase::dispatchEvent (%" PRIu64 ") (%p)", m_databaseConnectionIdentifier, this);
    ASSERT(&originThread() == &Thread::current());

    auto protectedThis = makeRef(*this);

    EventTargetWithInlineData::dispatchEvent(event);

    if (event.isVersionChangeEvent() && event.type() == m_eventNames.versionchangeEvent)
        m_connectionProxy->didFireVersionChangeEvent(m_databaseConnectionIdentifier, downcast<IDBVersionChangeEvent>(event).requestIdentifier());
}

void IDBDatabase::didCreateIndexInfo(const IDBIndexInfo& info)
{
    ASSERT(&originThread() == &Thread::current());

    auto* objectStore = m_info.infoForExistingObjectStore(info.objectStoreIdentifier());
    ASSERT(objectStore);
    objectStore->addExistingIndex(info);
}

void IDBDatabase::didDeleteIndexInfo(const IDBIndexInfo& info)
{
    ASSERT(&originThread() == &Thread::current());

    auto* objectStore = m_info.infoForExistingObjectStore(info.objectStoreIdentifier());
    ASSERT(objectStore);
    objectStore->deleteIndex(info.name());
}

} // namespace WebCore

#endif // ENABLE(INDEXED_DATABASE)
