/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "IndexedDatabaseManager.h"

#include "chrome/common/ipc_channel.h" // for IPC::Channel::kMaximumMessageSize
#include "nsIConsoleService.h"
#include "nsIDOMWindow.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIScriptError.h"
#include "nsIScriptGlobalObject.h"

#include "jsapi.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/CondVar.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/dom/DOMError.h"
#include "mozilla/dom/ErrorEvent.h"
#include "mozilla/dom/ErrorEventBinding.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "nsContentUtils.h"
#include "nsGlobalWindow.h"
#include "nsThreadUtils.h"
#include "mozilla/Logging.h"

#include "FileInfo.h"
#include "FileManager.h"
#include "IDBEvents.h"
#include "IDBFactory.h"
#include "IDBKeyRange.h"
#include "IDBRequest.h"
#include "ProfilerHelpers.h"
#include "ScriptErrorHelper.h"
#include "WorkerScope.h"
#include "WorkerPrivate.h"

// Bindings for ResolveConstructors
#include "mozilla/dom/IDBCursorBinding.h"
#include "mozilla/dom/IDBDatabaseBinding.h"
#include "mozilla/dom/IDBFactoryBinding.h"
#include "mozilla/dom/IDBIndexBinding.h"
#include "mozilla/dom/IDBKeyRangeBinding.h"
#include "mozilla/dom/IDBMutableFileBinding.h"
#include "mozilla/dom/IDBObjectStoreBinding.h"
#include "mozilla/dom/IDBOpenDBRequestBinding.h"
#include "mozilla/dom/IDBRequestBinding.h"
#include "mozilla/dom/IDBTransactionBinding.h"
#include "mozilla/dom/IDBVersionChangeEventBinding.h"

#include "nsCharSeparatedTokenizer.h"
#include "unicode/locid.h"

#define IDB_STR "indexedDB"

namespace mozilla {
namespace dom {
namespace indexedDB {

using namespace mozilla::dom::quota;
using namespace mozilla::dom::workers;
using namespace mozilla::ipc;

class FileManagerInfo
{
public:
  already_AddRefed<FileManager>
  GetFileManager(PersistenceType aPersistenceType,
                 const nsAString& aName) const;

  void
  AddFileManager(FileManager* aFileManager);

  bool
  HasFileManagers() const
  {
    AssertIsOnIOThread();

    return !mPersistentStorageFileManagers.IsEmpty() ||
           !mTemporaryStorageFileManagers.IsEmpty() ||
           !mDefaultStorageFileManagers.IsEmpty();
  }

  void
  InvalidateAllFileManagers() const;

  void
  InvalidateAndRemoveFileManagers(PersistenceType aPersistenceType);

  void
  InvalidateAndRemoveFileManager(PersistenceType aPersistenceType,
                                 const nsAString& aName);

private:
  nsTArray<RefPtr<FileManager> >&
  GetArray(PersistenceType aPersistenceType);

  const nsTArray<RefPtr<FileManager> >&
  GetImmutableArray(PersistenceType aPersistenceType) const
  {
    return const_cast<FileManagerInfo*>(this)->GetArray(aPersistenceType);
  }

  nsTArray<RefPtr<FileManager> > mPersistentStorageFileManagers;
  nsTArray<RefPtr<FileManager> > mTemporaryStorageFileManagers;
  nsTArray<RefPtr<FileManager> > mDefaultStorageFileManagers;
};

} // namespace indexedDB

using namespace mozilla::dom::indexedDB;

namespace {

NS_DEFINE_IID(kIDBRequestIID, PRIVATE_IDBREQUEST_IID);

const uint32_t kDeleteTimeoutMs = 1000;

// The threshold we use for structured clone data storing.
// Anything smaller than the threshold is compressed and stored in the database.
// Anything larger is compressed and stored outside the database.
const int32_t kDefaultDataThresholdBytes = 1024 * 1024; // 1MB

// The maximal size of a serialized object to be transfered through IPC.
const int32_t kDefaultMaxSerializedMsgSize = IPC::Channel::kMaximumMessageSize;

#define IDB_PREF_BRANCH_ROOT "dom.indexedDB."

const char kTestingPref[] = IDB_PREF_BRANCH_ROOT "testing";
const char kPrefExperimental[] = IDB_PREF_BRANCH_ROOT "experimental";
const char kPrefFileHandle[] = "dom.fileHandle.enabled";
const char kDataThresholdPref[] = IDB_PREF_BRANCH_ROOT "dataThreshold";
const char kPrefMaxSerilizedMsgSize[] = IDB_PREF_BRANCH_ROOT "maxSerializedMsgSize";

#define IDB_PREF_LOGGING_BRANCH_ROOT IDB_PREF_BRANCH_ROOT "logging."

const char kPrefLoggingEnabled[] = IDB_PREF_LOGGING_BRANCH_ROOT "enabled";
const char kPrefLoggingDetails[] = IDB_PREF_LOGGING_BRANCH_ROOT "details";

#if defined(DEBUG)
const char kPrefLoggingProfiler[] =
  IDB_PREF_LOGGING_BRANCH_ROOT "profiler-marks";
#endif

#undef IDB_PREF_LOGGING_BRANCH_ROOT
#undef IDB_PREF_BRANCH_ROOT

StaticRefPtr<IndexedDatabaseManager> gDBManager;

Atomic<bool> gInitialized(false);
Atomic<bool> gClosed(false);
Atomic<bool> gTestingMode(false);
Atomic<bool> gExperimentalFeaturesEnabled(false);
Atomic<bool> gFileHandleEnabled(false);
Atomic<int32_t> gDataThresholdBytes(0);
Atomic<int32_t> gMaxSerializedMsgSize(0);

class DeleteFilesRunnable final
  : public nsIRunnable
  , public OpenDirectoryListener
{
  typedef mozilla::dom::quota::DirectoryLock DirectoryLock;

  enum State
  {
    // Just created on the main thread. Next step is State_DirectoryOpenPending.
    State_Initial,

    // Waiting for directory open allowed on the main thread. The next step is
    // State_DatabaseWorkOpen.
    State_DirectoryOpenPending,

    // Waiting to do/doing work on the QuotaManager IO thread. The next step is
    // State_UnblockingOpen.
    State_DatabaseWorkOpen,

    // Notifying the QuotaManager that it can proceed to the next operation on
    // the main thread. Next step is State_Completed.
    State_UnblockingOpen,

    // All done.
    State_Completed
  };

  nsCOMPtr<nsIEventTarget> mBackgroundThread;

  RefPtr<FileManager> mFileManager;
  nsTArray<int64_t> mFileIds;

  RefPtr<DirectoryLock> mDirectoryLock;

  nsCOMPtr<nsIFile> mDirectory;
  nsCOMPtr<nsIFile> mJournalDirectory;

  State mState;

public:
  DeleteFilesRunnable(nsIEventTarget* aBackgroundThread,
                      FileManager* aFileManager,
                      nsTArray<int64_t>& aFileIds);

  void
  Dispatch();

  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIRUNNABLE

  virtual void
  DirectoryLockAcquired(DirectoryLock* aLock) override;

  virtual void
  DirectoryLockFailed() override;

private:
  ~DeleteFilesRunnable() {}

  nsresult
  Open();

  nsresult
  DeleteFile(int64_t aFileId);

  nsresult
  DoDatabaseWork();

  void
  Finish();

  void
  UnblockOpen();
};

void
AtomicBoolPrefChangedCallback(const char* aPrefName, void* aClosure)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aClosure);

  *static_cast<Atomic<bool>*>(aClosure) = Preferences::GetBool(aPrefName);
}

void
DataThresholdPrefChangedCallback(const char* aPrefName, void* aClosure)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!strcmp(aPrefName, kDataThresholdPref));
  MOZ_ASSERT(!aClosure);

  int32_t dataThresholdBytes =
    Preferences::GetInt(aPrefName, kDefaultDataThresholdBytes);

  // The magic -1 is for use only by tests that depend on stable blob file id's.
  if (dataThresholdBytes == -1) {
    dataThresholdBytes = INT32_MAX;
  }

  gDataThresholdBytes = dataThresholdBytes;
}

void
MaxSerializedMsgSizePrefChangeCallback(const char* aPrefName, void* aClosure)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!strcmp(aPrefName, kPrefMaxSerilizedMsgSize));
  MOZ_ASSERT(!aClosure);

  gMaxSerializedMsgSize =
    Preferences::GetInt(aPrefName, kDefaultMaxSerializedMsgSize);
  MOZ_ASSERT(gMaxSerializedMsgSize > 0);
}

} // namespace

IndexedDatabaseManager::IndexedDatabaseManager()
  : mFileMutex("IndexedDatabaseManager.mFileMutex")
  , mBackgroundActor(nullptr)
{
  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
}

IndexedDatabaseManager::~IndexedDatabaseManager()
{
  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");

  if (mBackgroundActor) {
    mBackgroundActor->SendDeleteMeInternal();
    MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!");
  }
}

bool IndexedDatabaseManager::sIsMainProcess = false;
bool IndexedDatabaseManager::sFullSynchronousMode = false;

mozilla::LazyLogModule IndexedDatabaseManager::sLoggingModule("IndexedDB");

Atomic<IndexedDatabaseManager::LoggingMode>
  IndexedDatabaseManager::sLoggingMode(
    IndexedDatabaseManager::Logging_Disabled);

// static
IndexedDatabaseManager*
IndexedDatabaseManager::GetOrCreate()
{
  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");

  if (IsClosed()) {
    NS_ERROR("Calling GetOrCreate() after shutdown!");
    return nullptr;
  }

  if (!gDBManager) {
    sIsMainProcess = XRE_IsParentProcess();

    RefPtr<IndexedDatabaseManager> instance(new IndexedDatabaseManager());

    nsresult rv = instance->Init();
    NS_ENSURE_SUCCESS(rv, nullptr);

    if (gInitialized.exchange(true)) {
      NS_ERROR("Initialized more than once?!");
    }

    gDBManager = instance;

    ClearOnShutdown(&gDBManager);
  }

  return gDBManager;
}

// static
IndexedDatabaseManager*
IndexedDatabaseManager::Get()
{
  // Does not return an owning reference.
  return gDBManager;
}

nsresult
IndexedDatabaseManager::Init()
{
  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");

  // During Init() we can't yet call IsMainProcess(), just check sIsMainProcess
  // directly.
  if (sIsMainProcess) {
    mDeleteTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
    NS_ENSURE_STATE(mDeleteTimer);

    if (QuotaManager* quotaManager = QuotaManager::Get()) {
      NoteLiveQuotaManager(quotaManager);
    }
  }

  Preferences::RegisterCallbackAndCall(AtomicBoolPrefChangedCallback,
                                       kTestingPref,
                                       &gTestingMode);
  Preferences::RegisterCallbackAndCall(AtomicBoolPrefChangedCallback,
                                       kPrefExperimental,
                                       &gExperimentalFeaturesEnabled);
  Preferences::RegisterCallbackAndCall(AtomicBoolPrefChangedCallback,
                                       kPrefFileHandle,
                                       &gFileHandleEnabled);

  // By default IndexedDB uses SQLite with PRAGMA synchronous = NORMAL. This
  // guarantees (unlike synchronous = OFF) atomicity and consistency, but not
  // necessarily durability in situations such as power loss. This preference
  // allows enabling PRAGMA synchronous = FULL on SQLite, which does guarantee
  // durability, but with an extra fsync() and the corresponding performance
  // hit.
  sFullSynchronousMode = Preferences::GetBool("dom.indexedDB.fullSynchronous");

  Preferences::RegisterCallback(LoggingModePrefChangedCallback,
                                kPrefLoggingDetails);

  Preferences::RegisterCallbackAndCall(LoggingModePrefChangedCallback,
                                       kPrefLoggingEnabled);

  Preferences::RegisterCallbackAndCall(DataThresholdPrefChangedCallback,
                                       kDataThresholdPref);

  Preferences::RegisterCallbackAndCall(MaxSerializedMsgSizePrefChangeCallback,
                                       kPrefMaxSerilizedMsgSize);

  const nsAdoptingCString& acceptLang =
    Preferences::GetLocalizedCString("intl.accept_languages");

  // Split values on commas.
  nsCCharSeparatedTokenizer langTokenizer(acceptLang, ',');
  while (langTokenizer.hasMoreTokens()) {
    nsAutoCString lang(langTokenizer.nextToken());
    icu::Locale locale = icu::Locale::createCanonical(lang.get());
    if (!locale.isBogus()) {
      // icu::Locale::getBaseName is always ASCII as per BCP 47
      mLocale.AssignASCII(locale.getBaseName());
      break;
    }
  }

  if (mLocale.IsEmpty()) {
    mLocale.AssignLiteral("en_US");
  }

  return NS_OK;
}

void
IndexedDatabaseManager::Destroy()
{
  // Setting the closed flag prevents the service from being recreated.
  // Don't set it though if there's no real instance created.
  if (gInitialized && gClosed.exchange(true)) {
    NS_ERROR("Shutdown more than once?!");
  }

  if (sIsMainProcess && mDeleteTimer) {
    if (NS_FAILED(mDeleteTimer->Cancel())) {
      NS_WARNING("Failed to cancel timer!");
    }

    mDeleteTimer = nullptr;
  }

  Preferences::UnregisterCallback(AtomicBoolPrefChangedCallback,
                                  kTestingPref,
                                  &gTestingMode);
  Preferences::UnregisterCallback(AtomicBoolPrefChangedCallback,
                                  kPrefExperimental,
                                  &gExperimentalFeaturesEnabled);
  Preferences::UnregisterCallback(AtomicBoolPrefChangedCallback,
                                  kPrefFileHandle,
                                  &gFileHandleEnabled);

  Preferences::UnregisterCallback(LoggingModePrefChangedCallback,
                                  kPrefLoggingDetails);
  Preferences::UnregisterCallback(LoggingModePrefChangedCallback,
                                  kPrefLoggingEnabled);

  Preferences::UnregisterCallback(DataThresholdPrefChangedCallback,
                                  kDataThresholdPref);

  Preferences::UnregisterCallback(MaxSerializedMsgSizePrefChangeCallback,
                                  kPrefMaxSerilizedMsgSize);

  delete this;
}

// static
nsresult
IndexedDatabaseManager::CommonPostHandleEvent(EventChainPostVisitor& aVisitor,
                                              IDBFactory* aFactory)
{
  MOZ_ASSERT(aVisitor.mDOMEvent);
  MOZ_ASSERT(aFactory);

  if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
    return NS_OK;
  }

  Event* internalEvent = aVisitor.mDOMEvent->InternalDOMEvent();
  MOZ_ASSERT(internalEvent);

  if (!internalEvent->IsTrusted()) {
    return NS_OK;
  }

  nsString type;
  MOZ_ALWAYS_SUCCEEDS(internalEvent->GetType(type));

  MOZ_ASSERT(nsDependentString(kErrorEventType).EqualsLiteral("error"));
  if (!type.EqualsLiteral("error")) {
    return NS_OK;
  }

  nsCOMPtr<EventTarget> eventTarget = internalEvent->GetTarget();
  MOZ_ASSERT(eventTarget);

  // Only mess with events that were originally targeted to an IDBRequest.
  RefPtr<IDBRequest> request;
  if (NS_FAILED(eventTarget->QueryInterface(kIDBRequestIID,
                                            getter_AddRefs(request))) ||
      !request) {
    return NS_OK;
  }

  RefPtr<DOMError> error = request->GetErrorAfterResult();

  nsString errorName;
  if (error) {
    error->GetName(errorName);
  }

  RootedDictionary<ErrorEventInit> init(RootingCx());
  request->GetCallerLocation(init.mFilename, &init.mLineno, &init.mColno);

  init.mMessage = errorName;
  init.mCancelable = true;
  init.mBubbles = true;

  nsEventStatus status = nsEventStatus_eIgnore;

  if (NS_IsMainThread()) {
    nsCOMPtr<nsIDOMWindow> window = do_QueryInterface(eventTarget->GetOwnerGlobal());
    if (window) {
      nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(window);
      MOZ_ASSERT(sgo);

      if (NS_WARN_IF(NS_FAILED(sgo->HandleScriptError(init, &status)))) {
        status = nsEventStatus_eIgnore;
      }
    } else {
      // We don't fire error events at any global for non-window JS on the main
      // thread.
    }
  } else {
    // Not on the main thread, must be in a worker.
    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
    MOZ_ASSERT(workerPrivate);

    RefPtr<WorkerGlobalScope> globalScope = workerPrivate->GlobalScope();
    MOZ_ASSERT(globalScope);

    RefPtr<ErrorEvent> errorEvent =
      ErrorEvent::Constructor(globalScope,
                              nsDependentString(kErrorEventType),
                              init);
    MOZ_ASSERT(errorEvent);

    errorEvent->SetTrusted(true);

    auto* target = static_cast<EventTarget*>(globalScope.get());

    if (NS_WARN_IF(NS_FAILED(
      EventDispatcher::DispatchDOMEvent(target,
                                        /* aWidgetEvent */ nullptr,
                                        errorEvent,
                                        /* aPresContext */ nullptr,
                                        &status)))) {
      status = nsEventStatus_eIgnore;
    }
  }

  if (status == nsEventStatus_eConsumeNoDefault) {
    return NS_OK;
  }

  // Log the error to the error console.
  ScriptErrorHelper::Dump(errorName,
                          init.mFilename,
                          init.mLineno,
                          init.mColno,
                          nsIScriptError::errorFlag,
                          aFactory->IsChrome(),
                          aFactory->InnerWindowID());

  return NS_OK;
}

// static
bool
IndexedDatabaseManager::ResolveSandboxBinding(JSContext* aCx)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(js::GetObjectClass(JS::CurrentGlobalOrNull(aCx))->flags &
             JSCLASS_DOM_GLOBAL,
             "Passed object is not a global object!");

  // We need to ensure that the manager has been created already here so that we
  // load preferences that may control which properties are exposed.
  if (NS_WARN_IF(!GetOrCreate())) {
    return false;
  }

  if (!IDBCursorBinding::GetConstructorObject(aCx) ||
      !IDBCursorWithValueBinding::GetConstructorObject(aCx) ||
      !IDBDatabaseBinding::GetConstructorObject(aCx) ||
      !IDBFactoryBinding::GetConstructorObject(aCx) ||
      !IDBIndexBinding::GetConstructorObject(aCx) ||
      !IDBKeyRangeBinding::GetConstructorObject(aCx) ||
      !IDBLocaleAwareKeyRangeBinding::GetConstructorObject(aCx) ||
      !IDBMutableFileBinding::GetConstructorObject(aCx) ||
      !IDBObjectStoreBinding::GetConstructorObject(aCx) ||
      !IDBOpenDBRequestBinding::GetConstructorObject(aCx) ||
      !IDBRequestBinding::GetConstructorObject(aCx) ||
      !IDBTransactionBinding::GetConstructorObject(aCx) ||
      !IDBVersionChangeEventBinding::GetConstructorObject(aCx))
  {
    return false;
  }

  return true;
}

// static
bool
IndexedDatabaseManager::DefineIndexedDB(JSContext* aCx,
                                        JS::Handle<JSObject*> aGlobal)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(js::GetObjectClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL,
             "Passed object is not a global object!");

  RefPtr<IDBFactory> factory;
  if (NS_FAILED(IDBFactory::CreateForMainThreadJS(aCx,
                                                  aGlobal,
                                                  getter_AddRefs(factory)))) {
    return false;
  }

  MOZ_ASSERT(factory, "This should never fail for chrome!");

  JS::Rooted<JS::Value> indexedDB(aCx);
  js::AssertSameCompartment(aCx, aGlobal);
  if (!GetOrCreateDOMReflector(aCx, factory, &indexedDB)) {
    return false;
  }

  return JS_DefineProperty(aCx, aGlobal, IDB_STR, indexedDB, JSPROP_ENUMERATE);
}

// static
bool
IndexedDatabaseManager::IsClosed()
{
  return gClosed;
}

#ifdef DEBUG
// static
bool
IndexedDatabaseManager::IsMainProcess()
{
  NS_ASSERTION(gDBManager,
               "IsMainProcess() called before indexedDB has been initialized!");
  NS_ASSERTION((XRE_IsParentProcess()) ==
               sIsMainProcess, "XRE_GetProcessType changed its tune!");
  return sIsMainProcess;
}

// static
IndexedDatabaseManager::LoggingMode
IndexedDatabaseManager::GetLoggingMode()
{
  MOZ_ASSERT(gDBManager,
             "GetLoggingMode called before IndexedDatabaseManager has been "
             "initialized!");

  return sLoggingMode;
}

// static
mozilla::LogModule*
IndexedDatabaseManager::GetLoggingModule()
{
  MOZ_ASSERT(gDBManager,
             "GetLoggingModule called before IndexedDatabaseManager has been "
             "initialized!");

  return sLoggingModule;
}

#endif // DEBUG

// static
bool
IndexedDatabaseManager::InTestingMode()
{
  MOZ_ASSERT(gDBManager,
             "InTestingMode() called before indexedDB has been initialized!");

  return gTestingMode;
}

// static
bool
IndexedDatabaseManager::FullSynchronous()
{
  MOZ_ASSERT(gDBManager,
             "FullSynchronous() called before indexedDB has been initialized!");

  return sFullSynchronousMode;
}

// static
bool
IndexedDatabaseManager::ExperimentalFeaturesEnabled()
{
  if (NS_IsMainThread()) {
    if (NS_WARN_IF(!GetOrCreate())) {
      return false;
    }
  } else {
    MOZ_ASSERT(Get(),
               "ExperimentalFeaturesEnabled() called off the main thread "
               "before indexedDB has been initialized!");
  }

  return gExperimentalFeaturesEnabled;
}

// static
bool
IndexedDatabaseManager::ExperimentalFeaturesEnabled(JSContext* aCx, JSObject* aGlobal)
{
  // If, in the child process, properties of the global object are enumerated
  // before the chrome registry (and thus the value of |intl.accept_languages|)
  // is ready, calling IndexedDatabaseManager::Init will permanently break
  // that preference. We can retrieve gExperimentalFeaturesEnabled without
  // actually going through IndexedDatabaseManager.
  // See Bug 1198093 comment 14 for detailed explanation.
  if (IsNonExposedGlobal(aCx, js::GetGlobalForObjectCrossCompartment(aGlobal),
                         GlobalNames::BackstagePass)) {
    MOZ_ASSERT(NS_IsMainThread());
    static bool featureRetrieved = false;
    if (!featureRetrieved) {
      gExperimentalFeaturesEnabled = Preferences::GetBool(kPrefExperimental);
      featureRetrieved = true;
    }
    return gExperimentalFeaturesEnabled;
  }

  return ExperimentalFeaturesEnabled();
}

// static
bool
IndexedDatabaseManager::IsFileHandleEnabled()
{
  MOZ_ASSERT(gDBManager,
             "IsFileHandleEnabled() called before indexedDB has been "
             "initialized!");

  return gFileHandleEnabled;
}

// static
uint32_t
IndexedDatabaseManager::DataThreshold()
{
  MOZ_ASSERT(gDBManager,
             "DataThreshold() called before indexedDB has been initialized!");

  return gDataThresholdBytes;
}

// static
uint32_t
IndexedDatabaseManager::MaxSerializedMsgSize()
{
  MOZ_ASSERT(gDBManager,
             "MaxSerializedMsgSize() called before indexedDB has been initialized!");
  MOZ_ASSERT(gMaxSerializedMsgSize > 0);

  return gMaxSerializedMsgSize;
}

void
IndexedDatabaseManager::ClearBackgroundActor()
{
  MOZ_ASSERT(NS_IsMainThread());

  mBackgroundActor = nullptr;
}

void
IndexedDatabaseManager::NoteLiveQuotaManager(QuotaManager* aQuotaManager)
{
  MOZ_ASSERT(IsMainProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aQuotaManager);

  mBackgroundThread = aQuotaManager->OwningThread();
}

void
IndexedDatabaseManager::NoteShuttingDownQuotaManager()
{
  MOZ_ASSERT(IsMainProcess());
  MOZ_ASSERT(NS_IsMainThread());

  MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel());

  mBackgroundThread = nullptr;
}

already_AddRefed<FileManager>
IndexedDatabaseManager::GetFileManager(PersistenceType aPersistenceType,
                                       const nsACString& aOrigin,
                                       const nsAString& aDatabaseName)
{
  AssertIsOnIOThread();

  FileManagerInfo* info;
  if (!mFileManagerInfos.Get(aOrigin, &info)) {
    return nullptr;
  }

  RefPtr<FileManager> fileManager =
    info->GetFileManager(aPersistenceType, aDatabaseName);

  return fileManager.forget();
}

void
IndexedDatabaseManager::AddFileManager(FileManager* aFileManager)
{
  AssertIsOnIOThread();
  NS_ASSERTION(aFileManager, "Null file manager!");

  FileManagerInfo* info;
  if (!mFileManagerInfos.Get(aFileManager->Origin(), &info)) {
    info = new FileManagerInfo();
    mFileManagerInfos.Put(aFileManager->Origin(), info);
  }

  info->AddFileManager(aFileManager);
}

void
IndexedDatabaseManager::InvalidateAllFileManagers()
{
  AssertIsOnIOThread();

  for (auto iter = mFileManagerInfos.ConstIter(); !iter.Done(); iter.Next()) {
    auto value = iter.Data();
    MOZ_ASSERT(value);

    value->InvalidateAllFileManagers();
  }

  mFileManagerInfos.Clear();
}

void
IndexedDatabaseManager::InvalidateFileManagers(PersistenceType aPersistenceType,
                                               const nsACString& aOrigin)
{
  AssertIsOnIOThread();
  MOZ_ASSERT(!aOrigin.IsEmpty());

  FileManagerInfo* info;
  if (!mFileManagerInfos.Get(aOrigin, &info)) {
    return;
  }

  info->InvalidateAndRemoveFileManagers(aPersistenceType);

  if (!info->HasFileManagers()) {
    mFileManagerInfos.Remove(aOrigin);
  }
}

void
IndexedDatabaseManager::InvalidateFileManager(PersistenceType aPersistenceType,
                                              const nsACString& aOrigin,
                                              const nsAString& aDatabaseName)
{
  AssertIsOnIOThread();

  FileManagerInfo* info;
  if (!mFileManagerInfos.Get(aOrigin, &info)) {
    return;
  }

  info->InvalidateAndRemoveFileManager(aPersistenceType, aDatabaseName);

  if (!info->HasFileManagers()) {
    mFileManagerInfos.Remove(aOrigin);
  }
}

nsresult
IndexedDatabaseManager::AsyncDeleteFile(FileManager* aFileManager,
                                        int64_t aFileId)
{
  MOZ_ASSERT(IsMainProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aFileManager);
  MOZ_ASSERT(aFileId > 0);
  MOZ_ASSERT(mDeleteTimer);

  if (!mBackgroundThread) {
    return NS_OK;
  }

  nsresult rv = mDeleteTimer->Cancel();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mDeleteTimer->InitWithCallback(this, kDeleteTimeoutMs,
                                      nsITimer::TYPE_ONE_SHOT);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsTArray<int64_t>* array;
  if (!mPendingDeleteInfos.Get(aFileManager, &array)) {
    array = new nsTArray<int64_t>();
    mPendingDeleteInfos.Put(aFileManager, array);
  }

  array->AppendElement(aFileId);

  return NS_OK;
}

nsresult
IndexedDatabaseManager::BlockAndGetFileReferences(
                                               PersistenceType aPersistenceType,
                                               const nsACString& aOrigin,
                                               const nsAString& aDatabaseName,
                                               int64_t aFileId,
                                               int32_t* aRefCnt,
                                               int32_t* aDBRefCnt,
                                               int32_t* aSliceRefCnt,
                                               bool* aResult)
{
  MOZ_ASSERT(NS_IsMainThread());

  if (NS_WARN_IF(!InTestingMode())) {
    return NS_ERROR_UNEXPECTED;
  }

  if (!mBackgroundActor) {
    PBackgroundChild* bgActor = BackgroundChild::GetForCurrentThread();
    if (NS_WARN_IF(!bgActor)) {
      return NS_ERROR_FAILURE;
    }

    BackgroundUtilsChild* actor = new BackgroundUtilsChild(this);

    mBackgroundActor =
      static_cast<BackgroundUtilsChild*>(
        bgActor->SendPBackgroundIndexedDBUtilsConstructor(actor));
  }

  if (NS_WARN_IF(!mBackgroundActor)) {
    return NS_ERROR_FAILURE;
  }

  if (!mBackgroundActor->SendGetFileReferences(aPersistenceType,
                                               nsCString(aOrigin),
                                               nsString(aDatabaseName),
                                               aFileId,
                                               aRefCnt,
                                               aDBRefCnt,
                                               aSliceRefCnt,
                                               aResult)) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult
IndexedDatabaseManager::FlushPendingFileDeletions()
{
  MOZ_ASSERT(NS_IsMainThread());

  if (NS_WARN_IF(!InTestingMode())) {
    return NS_ERROR_UNEXPECTED;
  }

  if (IsMainProcess()) {
    nsresult rv = mDeleteTimer->Cancel();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = Notify(mDeleteTimer);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  } else {
    PBackgroundChild* bgActor = BackgroundChild::GetForCurrentThread();
    if (NS_WARN_IF(!bgActor)) {
      return NS_ERROR_FAILURE;
    }

    if (!bgActor->SendFlushPendingFileDeletions()) {
      return NS_ERROR_FAILURE;
    }
  }

  return NS_OK;
}

// static
void
IndexedDatabaseManager::LoggingModePrefChangedCallback(
                                                    const char* /* aPrefName */,
                                                    void* /* aClosure */)
{
  MOZ_ASSERT(NS_IsMainThread());

  if (!Preferences::GetBool(kPrefLoggingEnabled)) {
    sLoggingMode = Logging_Disabled;
    return;
  }

  bool useProfiler =
#if defined(DEBUG)
    Preferences::GetBool(kPrefLoggingProfiler);
  if (useProfiler) {
    NS_WARNING("IndexedDB cannot create profiler marks because this build does "
               "not have profiler extensions enabled!");
    useProfiler = false;
  }
#else
    false;
#endif

  const bool logDetails = Preferences::GetBool(kPrefLoggingDetails);

  if (useProfiler) {
    sLoggingMode = logDetails ?
                   Logging_DetailedProfilerMarks :
                   Logging_ConciseProfilerMarks;
  } else {
    sLoggingMode = logDetails ? Logging_Detailed : Logging_Concise;
  }
}

// static
const nsCString&
IndexedDatabaseManager::GetLocale()
{
  IndexedDatabaseManager* idbManager = Get();
  MOZ_ASSERT(idbManager, "IDBManager is not ready!");

  return idbManager->mLocale;
}

NS_IMPL_ADDREF(IndexedDatabaseManager)
NS_IMPL_RELEASE_WITH_DESTROY(IndexedDatabaseManager, Destroy())
NS_IMPL_QUERY_INTERFACE(IndexedDatabaseManager, nsITimerCallback)

NS_IMETHODIMP
IndexedDatabaseManager::Notify(nsITimer* aTimer)
{
  MOZ_ASSERT(IsMainProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mBackgroundThread);

  for (auto iter = mPendingDeleteInfos.ConstIter(); !iter.Done(); iter.Next()) {
    auto key = iter.Key();
    auto value = iter.Data();
    MOZ_ASSERT(!value->IsEmpty());

    RefPtr<DeleteFilesRunnable> runnable =
      new DeleteFilesRunnable(mBackgroundThread, key, *value);

    MOZ_ASSERT(value->IsEmpty());

    runnable->Dispatch();
  }

  mPendingDeleteInfos.Clear();

  return NS_OK;
}

already_AddRefed<FileManager>
FileManagerInfo::GetFileManager(PersistenceType aPersistenceType,
                                const nsAString& aName) const
{
  AssertIsOnIOThread();

  const nsTArray<RefPtr<FileManager> >& managers =
    GetImmutableArray(aPersistenceType);

  for (uint32_t i = 0; i < managers.Length(); i++) {
    const RefPtr<FileManager>& fileManager = managers[i];

    if (fileManager->DatabaseName() == aName) {
      RefPtr<FileManager> result = fileManager;
      return result.forget();
    }
  }

  return nullptr;
}

void
FileManagerInfo::AddFileManager(FileManager* aFileManager)
{
  AssertIsOnIOThread();

  nsTArray<RefPtr<FileManager> >& managers = GetArray(aFileManager->Type());

  NS_ASSERTION(!managers.Contains(aFileManager), "Adding more than once?!");

  managers.AppendElement(aFileManager);
}

void
FileManagerInfo::InvalidateAllFileManagers() const
{
  AssertIsOnIOThread();

  uint32_t i;

  for (i = 0; i < mPersistentStorageFileManagers.Length(); i++) {
    mPersistentStorageFileManagers[i]->Invalidate();
  }

  for (i = 0; i < mTemporaryStorageFileManagers.Length(); i++) {
    mTemporaryStorageFileManagers[i]->Invalidate();
  }

  for (i = 0; i < mDefaultStorageFileManagers.Length(); i++) {
    mDefaultStorageFileManagers[i]->Invalidate();
  }
}

void
FileManagerInfo::InvalidateAndRemoveFileManagers(
                                               PersistenceType aPersistenceType)
{
  AssertIsOnIOThread();

  nsTArray<RefPtr<FileManager > >& managers = GetArray(aPersistenceType);

  for (uint32_t i = 0; i < managers.Length(); i++) {
    managers[i]->Invalidate();
  }

  managers.Clear();
}

void
FileManagerInfo::InvalidateAndRemoveFileManager(
                                               PersistenceType aPersistenceType,
                                               const nsAString& aName)
{
  AssertIsOnIOThread();

  nsTArray<RefPtr<FileManager > >& managers = GetArray(aPersistenceType);

  for (uint32_t i = 0; i < managers.Length(); i++) {
    RefPtr<FileManager>& fileManager = managers[i];
    if (fileManager->DatabaseName() == aName) {
      fileManager->Invalidate();
      managers.RemoveElementAt(i);
      return;
    }
  }
}

nsTArray<RefPtr<FileManager> >&
FileManagerInfo::GetArray(PersistenceType aPersistenceType)
{
  switch (aPersistenceType) {
    case PERSISTENCE_TYPE_PERSISTENT:
      return mPersistentStorageFileManagers;
    case PERSISTENCE_TYPE_TEMPORARY:
      return mTemporaryStorageFileManagers;
    case PERSISTENCE_TYPE_DEFAULT:
      return mDefaultStorageFileManagers;

    case PERSISTENCE_TYPE_INVALID:
    default:
      MOZ_CRASH("Bad storage type value!");
  }
}

DeleteFilesRunnable::DeleteFilesRunnable(nsIEventTarget* aBackgroundThread,
                                         FileManager* aFileManager,
                                         nsTArray<int64_t>& aFileIds)
  : mBackgroundThread(aBackgroundThread)
  , mFileManager(aFileManager)
  , mState(State_Initial)
{
  mFileIds.SwapElements(aFileIds);
}

void
DeleteFilesRunnable::Dispatch()
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mState == State_Initial);

  MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch(this, NS_DISPATCH_NORMAL));
}

NS_IMPL_ISUPPORTS(DeleteFilesRunnable, nsIRunnable)

NS_IMETHODIMP
DeleteFilesRunnable::Run()
{
  nsresult rv;

  switch (mState) {
    case State_Initial:
      rv = Open();
      break;

    case State_DatabaseWorkOpen:
      rv = DoDatabaseWork();
      break;

    case State_UnblockingOpen:
      UnblockOpen();
      return NS_OK;

    case State_DirectoryOpenPending:
    default:
      MOZ_CRASH("Should never get here!");
  }

  if (NS_WARN_IF(NS_FAILED(rv)) && mState != State_UnblockingOpen) {
    Finish();
  }

  return NS_OK;
}

void
DeleteFilesRunnable::DirectoryLockAcquired(DirectoryLock* aLock)
{
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(mState == State_DirectoryOpenPending);
  MOZ_ASSERT(!mDirectoryLock);

  mDirectoryLock = aLock;

  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  // Must set this before dispatching otherwise we will race with the IO thread
  mState = State_DatabaseWorkOpen;

  nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Finish();
    return;
  }
}

void
DeleteFilesRunnable::DirectoryLockFailed()
{
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(mState == State_DirectoryOpenPending);
  MOZ_ASSERT(!mDirectoryLock);

  Finish();
}

nsresult
DeleteFilesRunnable::Open()
{
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(mState == State_Initial);

  QuotaManager* quotaManager = QuotaManager::Get();
  if (NS_WARN_IF(!quotaManager)) {
    return NS_ERROR_FAILURE;
  }

  mState = State_DirectoryOpenPending;

  quotaManager->OpenDirectory(mFileManager->Type(),
                              mFileManager->Group(),
                              mFileManager->Origin(),
                              Client::IDB,
                              /* aExclusive */ false,
                              this);

  return NS_OK;
}

nsresult
DeleteFilesRunnable::DeleteFile(int64_t aFileId)
{
  MOZ_ASSERT(mDirectory);
  MOZ_ASSERT(mJournalDirectory);

  nsCOMPtr<nsIFile> file = mFileManager->GetFileForId(mDirectory, aFileId);
  NS_ENSURE_TRUE(file, NS_ERROR_FAILURE);

  nsresult rv;
  int64_t fileSize;

  if (mFileManager->EnforcingQuota()) {
    rv = file->GetFileSize(&fileSize);
    NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
  }

  rv = file->Remove(false);
  NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);

  if (mFileManager->EnforcingQuota()) {
    QuotaManager* quotaManager = QuotaManager::Get();
    NS_ASSERTION(quotaManager, "Shouldn't be null!");

    quotaManager->DecreaseUsageForOrigin(mFileManager->Type(),
                                         mFileManager->Group(),
                                         mFileManager->Origin(), fileSize);
  }

  file = mFileManager->GetFileForId(mJournalDirectory, aFileId);
  NS_ENSURE_TRUE(file, NS_ERROR_FAILURE);

  rv = file->Remove(false);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult
DeleteFilesRunnable::DoDatabaseWork()
{
  AssertIsOnIOThread();
  MOZ_ASSERT(mState == State_DatabaseWorkOpen);

  if (!mFileManager->Invalidated()) {
    mDirectory = mFileManager->GetDirectory();
    if (NS_WARN_IF(!mDirectory)) {
      return NS_ERROR_FAILURE;
    }

    mJournalDirectory = mFileManager->GetJournalDirectory();
    if (NS_WARN_IF(!mJournalDirectory)) {
      return NS_ERROR_FAILURE;
    }

    for (int64_t fileId : mFileIds) {
      if (NS_FAILED(DeleteFile(fileId))) {
        NS_WARNING("Failed to delete file!");
      }
    }
  }

  Finish();

  return NS_OK;
}

void
DeleteFilesRunnable::Finish()
{
  // Must set mState before dispatching otherwise we will race with the main
  // thread.
  mState = State_UnblockingOpen;

  MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch(this, NS_DISPATCH_NORMAL));
}

void
DeleteFilesRunnable::UnblockOpen()
{
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(mState == State_UnblockingOpen);

  mDirectoryLock = nullptr;

  mState = State_Completed;
}

} // namespace dom
} // namespace mozilla
