/* 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 "SDBConnection.h"

// Local includes
#include "ActorsChild.h"
#include "SDBRequest.h"
#include "SimpleDBCommon.h"

// Global includes
#include <stdint.h>

#include <utility>

#include "MainThreadUtils.h"
#include "js/ArrayBuffer.h"
#include "js/RootingAPI.h"
#include "js/TypeDecls.h"
#include "mozilla/Assertions.h"
#include "mozilla/Preferences.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/PBackgroundSDBConnection.h"
#include "mozilla/dom/TypedArray.h"
#include "mozilla/dom/quota/PrincipalUtils.h"
#include "mozilla/fallible.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsISDBCallbacks.h"
#include "nsISupportsUtils.h"
#include "nsStringFwd.h"
#include "nscore.h"

namespace mozilla::dom {

using namespace mozilla::ipc;

namespace {

template <typename BufferT>
nsresult AppendDataToString(const BufferT& aBuffer, nsCString& aData) {
  return aBuffer.ProcessData(
      [&aData](const mozilla::Span<const uint8_t>& aSrcData,
               JS::AutoCheckCannotGC&&) {
        if (aSrcData.LengthBytes() > INT32_MAX) {
          return NS_ERROR_ILLEGAL_VALUE;
        }
        if (NS_WARN_IF(!aData.Append(aSrcData, fallible))) {
          return NS_ERROR_OUT_OF_MEMORY;
        }
        return NS_OK;
      });
}

nsresult GetWriteData(JSContext* aCx, JS::Handle<JS::Value> aValue,
                      nsCString& aData) {
  MOZ_ASSERT(aData.IsEmpty());

  if (!aValue.isObject()) {
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  mozilla::dom::ArrayBufferView view;
  if (view.Init(&aValue.toObject())) {
    return AppendDataToString(view, aData);
  }

  mozilla::dom::ArrayBuffer arrayBuffer;
  if (arrayBuffer.Init(&aValue.toObject())) {
    return AppendDataToString(arrayBuffer, aData);
  }

  return NS_ERROR_NOT_IMPLEMENTED;
}

}  // namespace

SDBConnection::SDBConnection()
    : mBackgroundActor(nullptr),
      mPersistenceType(quota::PERSISTENCE_TYPE_INVALID),
      mRunningRequest(false),
      mOpen(false),
      mAllowedToClose(false) {
  AssertIsOnOwningThread();
}

SDBConnection::~SDBConnection() {
  AssertIsOnOwningThread();

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

// static
nsresult SDBConnection::Create(REFNSIID aIID, void** aResult) {
  MOZ_ASSERT(aResult);

  if (NS_WARN_IF(!Preferences::GetBool(kPrefSimpleDBEnabled, false))) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  RefPtr<SDBConnection> connection = new SDBConnection();

  nsresult rv = connection->QueryInterface(aIID, aResult);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

void SDBConnection::ClearBackgroundActor() {
  AssertIsOnOwningThread();

  mBackgroundActor = nullptr;
}

void SDBConnection::OnNewRequest() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mRunningRequest);

  mRunningRequest = true;
}

void SDBConnection::OnRequestFinished() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mRunningRequest);

  mRunningRequest = false;
}

void SDBConnection::OnOpen() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mOpen);

  mOpen = true;
}

void SDBConnection::OnClose(bool aAbnormal) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mOpen);

  mOpen = false;

  if (aAbnormal) {
    MOZ_ASSERT(mAllowedToClose);

    if (mCloseCallback) {
      mCloseCallback->OnClose(this);
    }
  }
}

bool SDBConnection::IsAllowedToClose() const {
  AssertIsOnOwningThread();

  return mAllowedToClose;
}

void SDBConnection::AllowToClose() {
  AssertIsOnOwningThread();

  mAllowedToClose = true;
}

nsresult SDBConnection::CheckState() {
  AssertIsOnOwningThread();

  if (mAllowedToClose) {
    return NS_ERROR_ABORT;
  }

  if (mRunningRequest) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  return NS_OK;
}

nsresult SDBConnection::EnsureBackgroundActor() {
  AssertIsOnOwningThread();

  if (mBackgroundActor) {
    return NS_OK;
  }

  PBackgroundChild* backgroundActor =
      BackgroundChild::GetOrCreateForCurrentThread();
  if (NS_WARN_IF(!backgroundActor)) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<SDBConnectionChild> actor = new SDBConnectionChild(this);

  mBackgroundActor = static_cast<SDBConnectionChild*>(
      backgroundActor->SendPBackgroundSDBConnectionConstructor(
          actor, mPersistenceType, *mPrincipalInfo));
  if (NS_WARN_IF(!mBackgroundActor)) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult SDBConnection::InitiateRequest(SDBRequest* aRequest,
                                        const SDBRequestParams& aParams) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aRequest);
  MOZ_ASSERT(mBackgroundActor);

  auto actor = new SDBRequestChild(aRequest);

  if (!mBackgroundActor->SendPBackgroundSDBRequestConstructor(actor, aParams)) {
    return NS_ERROR_FAILURE;
  }

  // Balanced in SDBRequestChild::Recv__delete__().
  OnNewRequest();

  return NS_OK;
}

NS_IMPL_ISUPPORTS(SDBConnection, nsISDBConnection)

NS_IMETHODIMP
SDBConnection::Init(nsIPrincipal* aPrincipal,
                    const nsACString& aPersistenceType) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aPrincipal);

  if (!BackgroundChild::ValidatePrincipal(aPrincipal, {})) {
    MOZ_ASSERT_UNREACHABLE(
        "Process is not allowed to access simpleDB for this principal");
    return NS_ERROR_INVALID_ARG;
  }

  UniquePtr<PrincipalInfo> principalInfo(new PrincipalInfo());
  nsresult rv = PrincipalToPrincipalInfo(aPrincipal, principalInfo.get());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (principalInfo->type() != PrincipalInfo::TContentPrincipalInfo &&
      principalInfo->type() != PrincipalInfo::TSystemPrincipalInfo) {
    NS_WARNING("Simpledb not allowed for this principal!");
    return NS_ERROR_INVALID_ARG;
  }

  if (NS_WARN_IF(!quota::IsPrincipalInfoValid(*principalInfo))) {
    return NS_ERROR_INVALID_ARG;
  }

  PersistenceType persistenceType;
  if (aPersistenceType.IsVoid()) {
    persistenceType = quota::PERSISTENCE_TYPE_DEFAULT;
  } else {
    const auto maybePersistenceType =
        quota::PersistenceTypeFromString(aPersistenceType, fallible);
    if (NS_WARN_IF(maybePersistenceType.isNothing())) {
      return NS_ERROR_INVALID_ARG;
    }

    persistenceType = maybePersistenceType.value();
  }

  mPrincipalInfo = std::move(principalInfo);
  mPersistenceType = persistenceType;

  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::Open(const nsAString& aName, nsISDBRequest** _retval) {
  AssertIsOnOwningThread();

  nsresult rv = CheckState();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (mOpen) {
    return NS_ERROR_ALREADY_INITIALIZED;
  }

  SDBRequestOpenParams params;
  params.name() = aName;

  RefPtr<SDBRequest> request = new SDBRequest(this);

  rv = EnsureBackgroundActor();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = InitiateRequest(request, params);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  request.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::Seek(uint64_t aOffset, nsISDBRequest** _retval) {
  AssertIsOnOwningThread();

  nsresult rv = CheckState();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (!mOpen) {
    return NS_BASE_STREAM_CLOSED;
  }

  SDBRequestSeekParams params;
  params.offset() = aOffset;

  RefPtr<SDBRequest> request = new SDBRequest(this);

  rv = InitiateRequest(request, params);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  request.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::Read(uint64_t aSize, nsISDBRequest** _retval) {
  AssertIsOnOwningThread();

  nsresult rv = CheckState();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (!mOpen) {
    return NS_BASE_STREAM_CLOSED;
  }

  SDBRequestReadParams params;
  params.size() = aSize;

  RefPtr<SDBRequest> request = new SDBRequest(this);

  rv = InitiateRequest(request, params);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  request.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::Write(JS::Handle<JS::Value> aValue, JSContext* aCx,
                     nsISDBRequest** _retval) {
  AssertIsOnOwningThread();

  nsresult rv = CheckState();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (!mOpen) {
    return NS_BASE_STREAM_CLOSED;
  }

  JS::Rooted<JS::Value> value(aCx, aValue);

  nsCString data;
  rv = GetWriteData(aCx, value, data);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  SDBRequestWriteParams params;
  params.data() = data;

  RefPtr<SDBRequest> request = new SDBRequest(this);

  rv = InitiateRequest(request, params);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  request.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::Close(nsISDBRequest** _retval) {
  AssertIsOnOwningThread();

  nsresult rv = CheckState();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (!mOpen) {
    return NS_BASE_STREAM_CLOSED;
  }

  SDBRequestCloseParams params;

  RefPtr<SDBRequest> request = new SDBRequest(this);

  rv = InitiateRequest(request, params);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  request.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::GetCloseCallback(nsISDBCloseCallback** aCloseCallback) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aCloseCallback);

  NS_IF_ADDREF(*aCloseCallback = mCloseCallback);
  return NS_OK;
}

NS_IMETHODIMP
SDBConnection::SetCloseCallback(nsISDBCloseCallback* aCloseCallback) {
  AssertIsOnOwningThread();

  mCloseCallback = aCloseCallback;
  return NS_OK;
}

}  // namespace mozilla::dom
