/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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 "LSDatabase.h"

// Local includes
#include "ActorsChild.h"
#include "LSObject.h"
#include "LSSnapshot.h"

// Global includes
#include <cstring>
#include <new>
#include <utility>
#include "MainThreadUtils.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/PBackgroundLSDatabase.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsTHashMap.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsHashKeys.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nscore.h"

namespace mozilla::dom {

namespace {

#define XPCOM_SHUTDOWN_OBSERVER_TOPIC "xpcom-shutdown"

using LSDatabaseHashtable = nsTHashMap<nsCStringHashKey, LSDatabase*>;

StaticAutoPtr<LSDatabaseHashtable> gLSDatabases;

}  // namespace

StaticRefPtr<LSDatabase::Observer> LSDatabase::sObserver;

class LSDatabase::Observer final : public nsIObserver {
  bool mInvalidated;

 public:
  Observer() : mInvalidated(false) { MOZ_ASSERT(NS_IsMainThread()); }

  void Invalidate() { mInvalidated = true; }

 private:
  ~Observer() { MOZ_ASSERT(NS_IsMainThread()); }

  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER
};

LSDatabase::LSDatabase(const nsACString& aOrigin)
    : mActor(nullptr),
      mSnapshot(nullptr),
      mOrigin(aOrigin),
      mAllowedToClose(false),
      mRequestedAllowToClose(false) {
  AssertIsOnOwningThread();

  if (!gLSDatabases) {
    gLSDatabases = new LSDatabaseHashtable();

    MOZ_ASSERT(!sObserver);

    sObserver = new Observer();

    nsCOMPtr<nsIObserverService> obsSvc =
        mozilla::services::GetObserverService();
    MOZ_ASSERT(obsSvc);

    MOZ_ALWAYS_SUCCEEDS(
        obsSvc->AddObserver(sObserver, XPCOM_SHUTDOWN_OBSERVER_TOPIC, false));
  }

  MOZ_ASSERT(!gLSDatabases->Contains(mOrigin));
  gLSDatabases->InsertOrUpdate(mOrigin, this);
}

LSDatabase::~LSDatabase() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mSnapshot);

  if (!mAllowedToClose) {
    AllowToClose();
  }

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

// static
LSDatabase* LSDatabase::Get(const nsACString& aOrigin) {
  return gLSDatabases ? gLSDatabases->Get(aOrigin) : nullptr;
}

void LSDatabase::SetActor(LSDatabaseChild* aActor) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aActor);
  MOZ_ASSERT(!mActor);

  mActor = aActor;
}

void LSDatabase::RequestAllowToClose() {
  AssertIsOnOwningThread();

  if (mRequestedAllowToClose) {
    return;
  }

  mRequestedAllowToClose = true;

  if (mSnapshot) {
    mSnapshot->MarkDirty();
  } else {
    AllowToClose();
  }
}

void LSDatabase::NoteFinishedSnapshot(LSSnapshot* aSnapshot) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aSnapshot == mSnapshot);

  mSnapshot = nullptr;

  if (mRequestedAllowToClose) {
    AllowToClose();
  }
}

// All these methods assert `!mAllowedToClose` because they shoudn't be called
// if the database is being closed. Callers should first check the state by
// calling `IsAlloweToClose` and eventually obtain a new database.

nsresult LSDatabase::GetLength(LSObject* aObject, uint32_t* aResult) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, VoidString());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->GetLength(aResult);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::GetKey(LSObject* aObject, uint32_t aIndex,
                            nsAString& aResult) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, VoidString());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->GetKey(aIndex, aResult);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::GetItem(LSObject* aObject, const nsAString& aKey,
                             nsAString& aResult) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, aKey);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->GetItem(aKey, aResult);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::GetKeys(LSObject* aObject, nsTArray<nsString>& aKeys) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, VoidString());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->GetKeys(aKeys);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::SetItem(LSObject* aObject, const nsAString& aKey,
                             const nsAString& aValue,
                             LSNotifyInfo& aNotifyInfo) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, aKey);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->SetItem(aKey, aValue, aNotifyInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::RemoveItem(LSObject* aObject, const nsAString& aKey,
                                LSNotifyInfo& aNotifyInfo) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, aKey);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->RemoveItem(aKey, aNotifyInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::Clear(LSObject* aObject, LSNotifyInfo& aNotifyInfo) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  nsresult rv = EnsureSnapshot(aObject, VoidString());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = mSnapshot->Clear(aNotifyInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::BeginExplicitSnapshot(LSObject* aObject) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);
  MOZ_ASSERT(!mSnapshot);

  nsresult rv = EnsureSnapshot(aObject, VoidString(), /* aExplicit */ true);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::CheckpointExplicitSnapshot() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);
  MOZ_ASSERT(mSnapshot);
  MOZ_ASSERT(mSnapshot->Explicit());

  nsresult rv = mSnapshot->ExplicitCheckpoint();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult LSDatabase::EndExplicitSnapshot() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);
  MOZ_ASSERT(mSnapshot);
  MOZ_ASSERT(mSnapshot->Explicit());

  nsresult rv = mSnapshot->ExplicitEnd();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

bool LSDatabase::HasSnapshot() const {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);

  return !!mSnapshot;
}

int64_t LSDatabase::GetSnapshotUsage() const {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(!mAllowedToClose);
  MOZ_ASSERT(mSnapshot);

  return mSnapshot->GetUsage();
}

nsresult LSDatabase::EnsureSnapshot(LSObject* aObject, const nsAString& aKey,
                                    bool aExplicit) {
  MOZ_ASSERT(aObject);
  MOZ_ASSERT(mActor);
  MOZ_ASSERT_IF(mSnapshot, !aExplicit);
  MOZ_ASSERT(!mAllowedToClose);

  if (mSnapshot) {
    return NS_OK;
  }

  RefPtr<LSSnapshot> snapshot = new LSSnapshot(this);

  LSSnapshotChild* actor = new LSSnapshotChild(snapshot);

  LSSnapshotInitInfo initInfo;
  bool ok = mActor->SendPBackgroundLSSnapshotConstructor(
      actor, aObject->DocumentURI(), nsString(aKey),
      /* increasePeakUsage */ true,
      /* minSize */ 0, &initInfo);
  if (NS_WARN_IF(!ok)) {
    return NS_ERROR_FAILURE;
  }

  snapshot->SetActor(actor);

  // This add refs snapshot.
  nsresult rv = snapshot->Init(aKey, initInfo, aExplicit);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // This is cleared in LSSnapshot::Run() before the snapshot is destroyed.
  mSnapshot = snapshot;

  return NS_OK;
}

void LSDatabase::AllowToClose() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mAllowedToClose);
  MOZ_ASSERT(!mSnapshot);

  mAllowedToClose = true;

  if (mActor) {
    mActor->SendAllowToClose();
  }

  MOZ_ASSERT(gLSDatabases);
  MOZ_ASSERT(gLSDatabases->Get(mOrigin));
  gLSDatabases->Remove(mOrigin);

  if (!gLSDatabases->Count()) {
    gLSDatabases = nullptr;

    MOZ_ASSERT(sObserver);

    nsCOMPtr<nsIObserverService> obsSvc =
        mozilla::services::GetObserverService();
    MOZ_ASSERT(obsSvc);

    MOZ_ALWAYS_SUCCEEDS(
        obsSvc->RemoveObserver(sObserver, XPCOM_SHUTDOWN_OBSERVER_TOPIC));

    // We also need to invalidate the observer because AllowToClose can be
    // triggered by an indirectly related observer, so the observer service
    // may still keep our observer alive and call Observe on it. This is
    // possible because observer service snapshots the observer list for given
    // subject before looping over the list.
    sObserver->Invalidate();

    sObserver = nullptr;
  }
}

NS_IMPL_ISUPPORTS(LSDatabase::Observer, nsIObserver)

NS_IMETHODIMP
LSDatabase::Observer::Observe(nsISupports* aSubject, const char* aTopic,
                              const char16_t* aData) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!strcmp(aTopic, XPCOM_SHUTDOWN_OBSERVER_TOPIC));

  if (mInvalidated) {
    return NS_OK;
  }

  MOZ_ASSERT(gLSDatabases);

  for (const RefPtr<LSDatabase>& database :
       ToTArray<nsTArray<RefPtr<LSDatabase>>>(gLSDatabases->Values())) {
    database->RequestAllowToClose();
  }

  return NS_OK;
}

}  // namespace mozilla::dom
