/* 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/. */

#ifndef DOM_QUOTA_CACHINGDATABASECONNECTION_H_
#define DOM_QUOTA_CACHINGDATABASECONNECTION_H_

#include "mozStorageHelper.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/NotNull.h"
#include "mozilla/dom/quota/Config.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
#include "nsCOMPtr.h"
#include "nsHashKeys.h"
#include "nsISupportsImpl.h"
#include "nsInterfaceHashtable.h"
#include "nsString.h"
#include "nscore.h"

namespace mozilla::dom::quota {

class CachingDatabaseConnection {
 public:
  class CachedStatement;

  // A stack-only RAII wrapper that resets its borrowed statement when the
  // wrapper goes out of scope. Note it's intentionally not declared MOZ_RAII,
  // because it actually is used as a temporary in simple cases like
  // `stmt.Borrow()->Execute()`. It also automatically exposes the current query
  // to ScopedLogExtraInfo as "query" in builds where this mechanism is active.
  class MOZ_STACK_CLASS BorrowedStatement : mozStorageStatementScoper {
   public:
    mozIStorageStatement& operator*() const;

    MOZ_NONNULL_RETURN mozIStorageStatement* operator->() const
        MOZ_NO_ADDREF_RELEASE_ON_RETURN;

    BorrowedStatement(BorrowedStatement&& aOther) = default;

    // No funny business allowed.
    BorrowedStatement& operator=(BorrowedStatement&&) = delete;
    BorrowedStatement(const BorrowedStatement&) = delete;
    BorrowedStatement& operator=(const BorrowedStatement&) = delete;

   private:
    friend class CachedStatement;

#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
    BorrowedStatement(NotNull<mozIStorageStatement*> aStatement,
                      const nsACString& aQuery)
        : mozStorageStatementScoper(aStatement),
          mExtraInfo{ScopedLogExtraInfo::kTagQueryTainted, aQuery} {}

    ScopedLogExtraInfo mExtraInfo;
#else
    MOZ_IMPLICIT BorrowedStatement(NotNull<mozIStorageStatement*> aStatement)
        : mozStorageStatementScoper(aStatement) {}
#endif
  };

  class LazyStatement;

  void AssertIsOnConnectionThread() const {
#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
    mOwningEventTarget->AssertOwnership(
        "CachingDatabaseConnection not thread-safe");
#endif
  }

  bool HasStorageConnection() const {
    return static_cast<bool>(mStorageConnection);
  }

  mozIStorageConnection& MutableStorageConnection() const {
    AssertIsOnConnectionThread();
    MOZ_ASSERT(mStorageConnection);

    return **mStorageConnection;
  }

  bool Closed() const { return mClosed; }

  Result<CachedStatement, nsresult> GetCachedStatement(
      const nsACString& aQuery);

  Result<BorrowedStatement, nsresult> BorrowCachedStatement(
      const nsACString& aQuery);

  template <typename BindFunctor>
  nsresult ExecuteCachedStatement(const nsACString& aQuery,
                                  BindFunctor&& aBindFunctor) {
    QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement(aQuery));
    QM_TRY(std::forward<BindFunctor>(aBindFunctor)(*stmt));
    QM_TRY(MOZ_TO_RESULT(stmt->Execute()));

    return NS_OK;
  }

  nsresult ExecuteCachedStatement(const nsACString& aQuery);

  template <typename BindFunctor>
  Result<Maybe<BorrowedStatement>, nsresult>
  BorrowAndExecuteSingleStepStatement(const nsACString& aQuery,
                                      BindFunctor&& aBindFunctor);

#ifdef DEBUG
  ~CachingDatabaseConnection() {
    MOZ_ASSERT(!mStorageConnection);
    MOZ_ASSERT(!mCachedStatements.Count());
  }
#endif

 protected:
  explicit CachingDatabaseConnection(
      MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection);

  CachingDatabaseConnection() = default;

  void LazyInit(
      MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection);

  void Close();

 private:
#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
  LazyInitializedOnce<const nsAutoOwningEventTarget> mOwningEventTarget;
#endif

  LazyInitializedOnceEarlyDestructible<
      const NotNull<nsCOMPtr<mozIStorageConnection>>>
      mStorageConnection;
  nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
      mCachedStatements;
  Atomic<bool> mClosed;
};

class CachingDatabaseConnection::CachedStatement final {
  friend class CachingDatabaseConnection;

  nsCOMPtr<mozIStorageStatement> mStatement;

#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
  nsCString mQuery;
#endif

#ifdef DEBUG
  CachingDatabaseConnection* mDEBUGConnection;
#endif

 public:
#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
  CachedStatement();
  ~CachedStatement();
#else
  CachedStatement() = default;
#endif

  void AssertIsOnConnectionThread() const;

  explicit operator bool() const;

  BorrowedStatement Borrow() const;

 private:
  // Only called by CachingDatabaseConnection.
  CachedStatement(CachingDatabaseConnection* aConnection,
                  nsCOMPtr<mozIStorageStatement> aStatement,
                  const nsACString& aQuery);

 public:
#if defined(NS_BUILD_REFCNT_LOGGING)
  CachedStatement(CachedStatement&& aOther)
      : mStatement(std::move(aOther.mStatement))
#  ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
        ,
        mQuery(std::move(aOther.mQuery))
#  endif
#  ifdef DEBUG
        ,
        mDEBUGConnection(aOther.mDEBUGConnection)
#  endif
  {
    MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement);
  }
#else
  CachedStatement(CachedStatement&&) = default;
#endif

  CachedStatement& operator=(CachedStatement&&) = default;

  // No funny business allowed.
  CachedStatement(const CachedStatement&) = delete;
  CachedStatement& operator=(const CachedStatement&) = delete;
};

class CachingDatabaseConnection::LazyStatement final {
 public:
  LazyStatement(CachingDatabaseConnection& aConnection,
                const nsACString& aQueryString)
      : mConnection{aConnection}, mQueryString{aQueryString} {}

  Result<CachingDatabaseConnection::BorrowedStatement, nsresult> Borrow();

  template <typename BindFunctor>
  Result<Maybe<CachingDatabaseConnection::BorrowedStatement>, nsresult>
  BorrowAndExecuteSingleStep(BindFunctor&& aBindFunctor) {
    QM_TRY_UNWRAP(auto borrowedStatement, Borrow());

    QM_TRY(std::forward<BindFunctor>(aBindFunctor)(*borrowedStatement));

    QM_TRY_INSPECT(
        const bool& hasResult,
        MOZ_TO_RESULT_INVOKE_MEMBER(&*borrowedStatement, ExecuteStep));

    return hasResult ? Some(std::move(borrowedStatement)) : Nothing{};
  }

 private:
  Result<Ok, nsresult> Initialize();

  CachingDatabaseConnection& mConnection;
  const nsCString mQueryString;
  CachingDatabaseConnection::CachedStatement mCachedStatement;
};

template <typename BindFunctor>
Result<Maybe<CachingDatabaseConnection::BorrowedStatement>, nsresult>
CachingDatabaseConnection::BorrowAndExecuteSingleStepStatement(
    const nsACString& aQuery, BindFunctor&& aBindFunctor) {
  return LazyStatement{*this, aQuery}.BorrowAndExecuteSingleStep(
      std::forward<BindFunctor>(aBindFunctor));
}

}  // namespace mozilla::dom::quota

#endif  // DOM_QUOTA_CACHINGDATABASECONNECTION_H_
