/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=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/. */

#ifndef mozilla_dom_idbtransaction_h__
#define mozilla_dom_idbtransaction_h__

#include "FlippedOnce.h"
#include "mozilla/Attributes.h"
#include "mozilla/dom/IDBTransactionBinding.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIRunnable.h"
#include "nsString.h"
#include "nsTArray.h"
#include "SafeRefPtr.h"

namespace mozilla {

class ErrorResult;
class EventChainPreVisitor;

namespace dom {

class DOMException;
class DOMStringList;
class IDBCursor;
class IDBDatabase;
class IDBObjectStore;
class IDBOpenDBRequest;
class IDBRequest;
class StrongWorkerRef;

namespace indexedDB {
class PBackgroundIDBCursorChild;
class BackgroundRequestChild;
class BackgroundTransactionChild;
class BackgroundVersionChangeTransactionChild;
class IndexMetadata;
class ObjectStoreSpec;
class OpenCursorParams;
class RequestParams;
}  // namespace indexedDB

class IDBTransaction final
    : public DOMEventTargetHelper,
      public nsIRunnable,
      public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
  friend class indexedDB::BackgroundRequestChild;

 public:
  enum struct Mode {
    ReadOnly = 0,
    ReadWrite,
    ReadWriteFlush,
    Cleanup,
    VersionChange,

    // Only needed for IPC serialization helper, should never be used in code.
    Invalid
  };

  enum struct ReadyState { Active, Inactive, Committing, Finished };

 private:
  // TODO: Only non-const because of Bug 1575173.
  RefPtr<IDBDatabase> mDatabase;
  RefPtr<DOMException> mError;
  const nsTArray<nsString> mObjectStoreNames;
  nsTArray<RefPtr<IDBObjectStore>> mObjectStores;
  nsTArray<RefPtr<IDBObjectStore>> mDeletedObjectStores;
  RefPtr<StrongWorkerRef> mWorkerRef;
  nsTArray<NotNull<IDBCursor*>> mCursors;

  // Tagged with mMode. If mMode is Mode::VersionChange then mBackgroundActor
  // will be a BackgroundVersionChangeTransactionChild. Otherwise it will be a
  // BackgroundTransactionChild.
  union {
    indexedDB::BackgroundTransactionChild* mNormalBackgroundActor;
    indexedDB::BackgroundVersionChangeTransactionChild*
        mVersionChangeBackgroundActor;
  } mBackgroundActor;

  const int64_t mLoggingSerialNumber;

  // Only used for Mode::VersionChange transactions.
  int64_t mNextObjectStoreId;
  int64_t mNextIndexId;

  nsresult mAbortCode;  ///< The result that caused the transaction to be
                        ///< aborted, or NS_OK if not aborted.
                        ///< NS_ERROR_DOM_INDEXEDDB_ABORT_ERR indicates that the
                        ///< user explicitly requested aborting. Should be
                        ///< renamed to mResult or so, because it is actually
                        ///< used to check if the transaction has been aborted.
  uint32_t mPendingRequestCount;  ///< Counted via OnNewRequest and
                                  ///< OnRequestFinished, so that the
                                  ///< transaction can auto-commit when the last
                                  ///< pending request finished.

  const nsString mFilename;
  const uint32_t mLineNo;
  const uint32_t mColumn;

  ReadyState mReadyState = ReadyState::Active;
  FlippedOnce<false> mStarted;
  const Mode mMode;

  bool mRegistered;  ///< Whether mDatabase->RegisterTransaction() has been
                     ///< called (which may not be the case if construction was
                     ///< incomplete).
  FlippedOnce<false> mAbortedByScript;
  bool mNotedActiveTransaction;
  FlippedOnce<false> mSentCommitOrAbort;

#ifdef DEBUG
  FlippedOnce<false> mFiredCompleteOrAbort;
  FlippedOnce<false> mWasExplicitlyCommitted;
#endif

 public:
  [[nodiscard]] static SafeRefPtr<IDBTransaction> CreateVersionChange(
      IDBDatabase* aDatabase,
      indexedDB::BackgroundVersionChangeTransactionChild* aActor,
      NotNull<IDBOpenDBRequest*> aOpenRequest, int64_t aNextObjectStoreId,
      int64_t aNextIndexId);

  [[nodiscard]] static SafeRefPtr<IDBTransaction> Create(
      JSContext* aCx, IDBDatabase* aDatabase,
      const nsTArray<nsString>& aObjectStoreNames, Mode aMode);

  static Maybe<IDBTransaction&> MaybeCurrent();

  void AssertIsOnOwningThread() const
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  void SetBackgroundActor(
      indexedDB::BackgroundTransactionChild* aBackgroundActor);

  void ClearBackgroundActor() {
    AssertIsOnOwningThread();

    if (mMode == Mode::VersionChange) {
      mBackgroundActor.mVersionChangeBackgroundActor = nullptr;
    } else {
      mBackgroundActor.mNormalBackgroundActor = nullptr;
    }

    // Note inactive transaction here if we didn't receive the Complete message
    // from the parent.
    MaybeNoteInactiveTransaction();
  }

  indexedDB::BackgroundRequestChild* StartRequest(
      MovingNotNull<RefPtr<mozilla::dom::IDBRequest>> aRequest,
      const indexedDB::RequestParams& aParams);

  void OpenCursor(indexedDB::PBackgroundIDBCursorChild& aBackgroundActor,
                  const indexedDB::OpenCursorParams& aParams);

  void RefreshSpec(bool aMayDelete);

  bool IsCommittingOrFinished() const {
    AssertIsOnOwningThread();

    return mReadyState == ReadyState::Committing ||
           mReadyState == ReadyState::Finished;
  }

  bool IsActive() const {
    AssertIsOnOwningThread();

    return mReadyState == ReadyState::Active;
  }

  bool IsInactive() const {
    AssertIsOnOwningThread();

    return mReadyState == ReadyState::Inactive;
  }

  bool IsFinished() const {
    AssertIsOnOwningThread();

    return mReadyState == ReadyState::Finished;
  }

  bool IsWriteAllowed() const {
    AssertIsOnOwningThread();
    return mMode == Mode::ReadWrite || mMode == Mode::ReadWriteFlush ||
           mMode == Mode::Cleanup || mMode == Mode::VersionChange;
  }

  bool IsAborted() const {
    AssertIsOnOwningThread();
    return NS_FAILED(mAbortCode);
  }

#ifdef DEBUG
  bool WasExplicitlyCommitted() const { return mWasExplicitlyCommitted; }
#endif

  void TransitionToActive() {
    MOZ_ASSERT(mReadyState == ReadyState::Inactive);
    mReadyState = ReadyState::Active;
  }

  void TransitionToInactive() {
    MOZ_ASSERT(mReadyState == ReadyState::Active);
    mReadyState = ReadyState::Inactive;
  }

  nsresult AbortCode() const {
    AssertIsOnOwningThread();
    return mAbortCode;
  }

  void GetCallerLocation(nsAString& aFilename, uint32_t* aLineNo,
                         uint32_t* aColumn) const;

  // 'Get' prefix is to avoid name collisions with the enum
  Mode GetMode() const {
    AssertIsOnOwningThread();
    return mMode;
  }

  uint32_t GetPendingRequestCount() const { return mPendingRequestCount; }

  IDBDatabase* Database() const {
    AssertIsOnOwningThread();
    return mDatabase;
  }

  // Only for use by ProfilerHelpers.h
  const nsTArray<nsString>& ObjectStoreNamesInternal() const {
    AssertIsOnOwningThread();
    return mObjectStoreNames;
  }

  [[nodiscard]] RefPtr<IDBObjectStore> CreateObjectStore(
      indexedDB::ObjectStoreSpec& aSpec);

  void DeleteObjectStore(int64_t aObjectStoreId);

  void RenameObjectStore(int64_t aObjectStoreId, const nsAString& aName) const;

  void CreateIndex(IDBObjectStore* aObjectStore,
                   const indexedDB::IndexMetadata& aMetadata) const;

  void DeleteIndex(IDBObjectStore* aObjectStore, int64_t aIndexId) const;

  void RenameIndex(IDBObjectStore* aObjectStore, int64_t aIndexId,
                   const nsAString& aName) const;

  void Abort(IDBRequest* aRequest);

  void Abort(nsresult aErrorCode);

  int64_t LoggingSerialNumber() const {
    AssertIsOnOwningThread();

    return mLoggingSerialNumber;
  }

  nsIGlobalObject* GetParentObject() const;

  void FireCompleteOrAbortEvents(nsresult aResult);

  // Only for Mode::VersionChange transactions.
  int64_t NextObjectStoreId();

  // Only for Mode::VersionChange transactions.
  int64_t NextIndexId();

  void InvalidateCursorCaches();
  void RegisterCursor(IDBCursor& aCursor);
  void UnregisterCursor(IDBCursor& aCursor);

  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_NSIRUNNABLE
  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBTransaction, DOMEventTargetHelper)

  void CommitIfNotStarted();

  // nsWrapperCache
  JSObject* WrapObject(JSContext* aCx,
                       JS::Handle<JSObject*> aGivenProto) override;

  // Methods bound via WebIDL.
  IDBDatabase* Db() const { return Database(); }

  IDBTransactionMode GetMode(ErrorResult& aRv) const;

  DOMException* GetError() const;

  [[nodiscard]] RefPtr<IDBObjectStore> ObjectStore(const nsAString& aName,
                                                   ErrorResult& aRv);

  void Commit(ErrorResult& aRv);

  void Abort(ErrorResult& aRv);

  IMPL_EVENT_HANDLER(abort)
  IMPL_EVENT_HANDLER(complete)
  IMPL_EVENT_HANDLER(error)

  [[nodiscard]] RefPtr<DOMStringList> ObjectStoreNames() const;

  // EventTarget
  void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;

 private:
  struct CreatedFromFactoryFunction {};

 public:
  IDBTransaction(IDBDatabase* aDatabase,
                 const nsTArray<nsString>& aObjectStoreNames, Mode aMode,
                 nsString aFilename, uint32_t aLineNo, uint32_t aColumn,
                 CreatedFromFactoryFunction aDummy);

 private:
  ~IDBTransaction();

  void AbortInternal(nsresult aAbortCode, RefPtr<DOMException> aError);

  void SendCommit(bool aAutoCommit);

  void SendAbort(nsresult aResultCode);

  void NoteActiveTransaction();

  void MaybeNoteInactiveTransaction();

  // TODO consider making private again, or move to the right place
 public:
  void OnNewRequest();

  void OnRequestFinished(bool aRequestCompletedSuccessfully);

 private:
  template <typename Func>
  auto DoWithTransactionChild(const Func& aFunc) const;

  bool HasTransactionChild() const;
};

inline bool ReferenceEquals(const Maybe<IDBTransaction&>& aLHS,
                            const Maybe<IDBTransaction&>& aRHS) {
  if (aLHS.isNothing() != aRHS.isNothing()) {
    return false;
  }
  return aLHS.isNothing() || &aLHS.ref() == &aRHS.ref();
}

}  // namespace dom
}  // namespace mozilla

#endif  // mozilla_dom_idbtransaction_h__
