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

#include "GroupInfo.h"
#include "GroupInfoPair.h"
#include "mozilla/dom/quota/AssertionsImpl.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/UsageInfo.h"

namespace mozilla::dom::quota {

// This constructor is called from the "QuotaManager IO" thread and so we
// can't check if the principal has a WebExtensionPolicy instance associated
// to it, and even besides that if the extension is currently disabled (and so
// no WebExtensionPolicy instance would actually exist) its stored data
// shouldn't be cleared until the extension is uninstalled and so here we
// resort to check the origin scheme instead to initialize mIsExtension.
OriginInfo::OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin,
                       const nsACString& aStorageOrigin, bool aIsPrivate,
                       const ClientUsageArray& aClientUsages, uint64_t aUsage,
                       int64_t aAccessTime, int32_t aMaintenanceDate,
                       bool aPersisted, bool aDirectoryExists)
    : mGroupInfo(aGroupInfo),
      mOrigin(aOrigin),
      mStorageOrigin(aStorageOrigin),
      mAccessTime(aAccessTime),
      mMaintenanceDate(aMaintenanceDate),
      mIsPrivate(aIsPrivate),
      mAccessed(false),
      mPersisted(aPersisted),
      mIsExtension(StringBeginsWith(aOrigin, "moz-extension://"_ns)),
      mDirectoryExists(aDirectoryExists),
      mClientUsages(aClientUsages),
      mUsage(aUsage) {
  MOZ_ASSERT(aGroupInfo);
  MOZ_ASSERT_IF(!aIsPrivate, aOrigin == aStorageOrigin);
  MOZ_ASSERT_IF(aIsPrivate, aOrigin != aStorageOrigin);
  MOZ_ASSERT(aClientUsages.Length() == Client::TypeMax());
  MOZ_ASSERT_IF(aPersisted,
                aGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT);

#ifdef DEBUG
  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  uint64_t usage = 0;
  for (Client::Type type : quotaManager->AllClientTypes()) {
    AssertNoOverflow(usage, aClientUsages[type].valueOr(0));
    usage += aClientUsages[type].valueOr(0);
  }
  MOZ_ASSERT(aUsage == usage);
#endif

  MOZ_COUNT_CTOR(OriginInfo);
}

OriginMetadata OriginInfo::FlattenToOriginMetadata() const {
  return {mGroupInfo->mGroupInfoPair->Suffix(),
          mGroupInfo->mGroupInfoPair->Group(),
          mOrigin,
          mStorageOrigin,
          mIsPrivate,
          mGroupInfo->mPersistenceType};
}

OriginStateMetadata OriginInfo::LockedFlattenToOriginStateMetadata() const {
  AssertCurrentThreadOwnsQuotaMutex();

  return {mAccessTime, mMaintenanceDate, mAccessed, mPersisted};
}

FullOriginMetadata OriginInfo::LockedFlattenToFullOriginMetadata() const {
  AssertCurrentThreadOwnsQuotaMutex();

  return {FlattenToOriginMetadata(), LockedFlattenToOriginStateMetadata(),
          mClientUsages, mUsage, kCurrentQuotaVersion};
}

nsresult OriginInfo::LockedBindToStatement(
    mozIStorageStatement* aStatement) const {
  AssertCurrentThreadOwnsQuotaMutex();
  MOZ_ASSERT(mGroupInfo);

  QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName(
      "repository_id"_ns, mGroupInfo->mPersistenceType)));

  QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName(
      "suffix"_ns, mGroupInfo->mGroupInfoPair->Suffix())));
  QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName(
      "group_"_ns, mGroupInfo->mGroupInfoPair->Group())));
  QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName("origin"_ns, mOrigin)));

  MOZ_ASSERT(!mIsPrivate);

  nsCString clientUsagesText;
  mClientUsages.Serialize(clientUsagesText);

  QM_TRY(MOZ_TO_RESULT(
      aStatement->BindUTF8StringByName("client_usages"_ns, clientUsagesText)));
  QM_TRY(MOZ_TO_RESULT(aStatement->BindInt64ByName("usage"_ns, mUsage)));
  QM_TRY(MOZ_TO_RESULT(
      aStatement->BindInt64ByName("last_access_time"_ns, mAccessTime)));
  QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName("last_maintenance_date"_ns,
                                                   mMaintenanceDate)));
  QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName("accessed"_ns, mAccessed)));
  QM_TRY(
      MOZ_TO_RESULT(aStatement->BindInt32ByName("persisted"_ns, mPersisted)));

  return NS_OK;
}

void OriginInfo::LockedDecreaseUsage(Client::Type aClientType, int64_t aSize) {
  AssertCurrentThreadOwnsQuotaMutex();

  MOZ_ASSERT(mClientUsages[aClientType].isSome());
  QM_ASSERT_NO_UNDERFLOW_2(
      mClientUsages[aClientType].value(), aSize,
      "mClientUsages["_ns + Client::TypeToText(aClientType) + "]"_ns);
  mClientUsages[aClientType] = Some(mClientUsages[aClientType].value() - aSize);

  QM_ASSERT_NO_UNDERFLOW(mUsage, aSize);
  mUsage -= aSize;

  if (!LockedPersisted()) {
    QM_ASSERT_NO_UNDERFLOW(mGroupInfo->mUsage, aSize);
    mGroupInfo->mUsage -= aSize;
  }

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

  QM_ASSERT_NO_UNDERFLOW(quotaManager->mTemporaryStorageUsage, aSize);
  quotaManager->mTemporaryStorageUsage -= aSize;
}

void OriginInfo::LockedResetUsageForClient(Client::Type aClientType) {
  AssertCurrentThreadOwnsQuotaMutex();

  uint64_t size = mClientUsages[aClientType].valueOr(0);

  mClientUsages[aClientType].reset();

  QM_ASSERT_NO_UNDERFLOW(mUsage, size);
  mUsage -= size;

  if (!LockedPersisted()) {
    QM_ASSERT_NO_UNDERFLOW(mGroupInfo->mUsage, size);
    mGroupInfo->mUsage -= size;
  }

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

  QM_ASSERT_NO_UNDERFLOW(quotaManager->mTemporaryStorageUsage, size);
  quotaManager->mTemporaryStorageUsage -= size;
}

UsageInfo OriginInfo::LockedGetUsageForClient(Client::Type aClientType) {
  AssertCurrentThreadOwnsQuotaMutex();

  // The current implementation of this method only supports DOMCACHE and LS,
  // which only use DatabaseUsage. If this assertion is lifted, the logic below
  // must be adapted.
  MOZ_ASSERT(aClientType == Client::Type::DOMCACHE ||
             aClientType == Client::Type::LS ||
             aClientType == Client::Type::FILESYSTEM);

  return UsageInfo{DatabaseUsageType{mClientUsages[aClientType]}};
}

void OriginInfo::LockedPersist() {
  AssertCurrentThreadOwnsQuotaMutex();
  MOZ_ASSERT(mGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT);
  MOZ_ASSERT(!mPersisted);

  mPersisted = true;

  // Remove Usage from GroupInfo
  QM_ASSERT_NO_UNDERFLOW(mGroupInfo->mUsage, mUsage);
  mGroupInfo->mUsage -= mUsage;
}

void OriginInfo::LockedTruncateUsages(Client::Type aClientType,
                                      uint64_t aDelta) {
  AssertCurrentThreadOwnsQuotaMutex();

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

  QM_ASSERT_NO_UNDERFLOW(quotaManager->mTemporaryStorageUsage, aDelta);
  quotaManager->mTemporaryStorageUsage -= aDelta;

  if (!LockedPersisted()) {
    QM_ASSERT_NO_UNDERFLOW(mGroupInfo->mUsage, aDelta);
    mGroupInfo->mUsage -= aDelta;
  }

  QM_ASSERT_NO_UNDERFLOW(mUsage, aDelta);
  mUsage -= aDelta;

  MOZ_ASSERT(mClientUsages[aClientType].isSome());
  QM_ASSERT_NO_UNDERFLOW_2(
      mClientUsages[aClientType].value(), aDelta,
      "mClientUsages["_ns + Client::TypeToText(aClientType) + "]"_ns);
  mClientUsages[aClientType] =
      Some(mClientUsages[aClientType].value() - aDelta);
};

Maybe<bool> OriginInfo::LockedUpdateUsages(Client::Type aClientType,
                                           uint64_t aDelta) {
  AssertCurrentThreadOwnsQuotaMutex();

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

  const auto& complementaryPersistenceTypes =
      ComplementaryPersistenceTypes(mGroupInfo->mPersistenceType);

  AssertNoOverflow(mClientUsages[aClientType].valueOr(0), aDelta);
  uint64_t newClientUsage = mClientUsages[aClientType].valueOr(0) + aDelta;

  AssertNoOverflow(mUsage, aDelta);
  uint64_t newUsage = mUsage + aDelta;

  // Temporary storage has no limit for origin usage (there's a group and the
  // global limit though).

  uint64_t newGroupUsage = mGroupInfo->mUsage;
  if (!LockedPersisted()) {
    AssertNoOverflow(mGroupInfo->mUsage, aDelta);
    newGroupUsage += aDelta;

    uint64_t groupUsage = mGroupInfo->mUsage;
    for (const auto& complementaryPersistenceType :
         complementaryPersistenceTypes) {
      const auto& complementaryGroupInfo =
          mGroupInfo->mGroupInfoPair->LockedGetGroupInfo(
              complementaryPersistenceType);

      if (complementaryGroupInfo) {
        AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage);
        groupUsage += complementaryGroupInfo->mUsage;
      }
    }

    // Temporary storage has a hard limit for group usage (20 % of the global
    // limit).
    AssertNoOverflow(groupUsage, aDelta);
    if (groupUsage + aDelta > quotaManager->GetGroupLimit()) {
      return Some(false);
    }
  }

  AssertNoOverflow(quotaManager->mTemporaryStorageUsage, aDelta);
  uint64_t newTemporaryStorageUsage =
      quotaManager->mTemporaryStorageUsage + aDelta;

  if (newTemporaryStorageUsage <= quotaManager->mTemporaryStorageLimit) {
    mClientUsages[aClientType] = Some(newClientUsage);

    mUsage = newUsage;
    if (!LockedPersisted()) {
      mGroupInfo->mUsage = newGroupUsage;
    }
    quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage;

    return Some(true);
  }

  return Nothing();
}

bool OriginInfo::LockedUpdateUsagesForEviction(Client::Type aClientType,
                                               uint64_t aDelta) {
  AssertCurrentThreadOwnsQuotaMutex();

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

  AssertNoOverflow(mUsage, aDelta);
  uint64_t newUsage = mUsage + aDelta;

  AssertNoOverflow(mClientUsages[aClientType].valueOr(0), aDelta);
  uint64_t newClientUsage = mClientUsages[aClientType].valueOr(0) + aDelta;

  AssertNoOverflow(quotaManager->mTemporaryStorageUsage, aDelta);
  uint64_t newTemporaryStorageUsage =
      quotaManager->mTemporaryStorageUsage + aDelta;

  uint64_t newGroupUsage = mGroupInfo->mUsage;
  if (!LockedPersisted()) {
    AssertNoOverflow(mGroupInfo->mUsage, aDelta);
    newGroupUsage += aDelta;

    uint64_t groupUsage = mGroupInfo->mUsage;

    const auto& complementaryPersistenceTypes =
        ComplementaryPersistenceTypes(mGroupInfo->mPersistenceType);
    for (const auto& complementaryPersistenceType :
         complementaryPersistenceTypes) {
      const auto& complementaryGroupInfo =
          mGroupInfo->mGroupInfoPair->LockedGetGroupInfo(
              complementaryPersistenceType);

      if (complementaryGroupInfo) {
        AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage);
        groupUsage += complementaryGroupInfo->mUsage;
      }
    }

    AssertNoOverflow(groupUsage, aDelta);
    if (groupUsage + aDelta > quotaManager->GetGroupLimit()) {
      // Unfortunately some other thread increased the group usage in the
      // meantime and we are not below the group limit anymore.
      return false;
    }
  }

  AssertNoOverflow(quotaManager->mTemporaryStorageUsage, aDelta);
  newTemporaryStorageUsage = quotaManager->mTemporaryStorageUsage + aDelta;

  NS_ASSERTION(newTemporaryStorageUsage <= quotaManager->mTemporaryStorageLimit,
               "How come?!");

  // Ok, we successfully freed enough space and the operation can continue
  // without throwing the quota error.
  mClientUsages[aClientType] = Some(newClientUsage);

  mUsage = newUsage;
  if (!LockedPersisted()) {
    MOZ_ASSERT(mGroupInfo);
    mGroupInfo->mUsage = newGroupUsage;
  }
  quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage;

  return true;
};

void OriginInfo::LockedDirectoryCreated() {
  AssertCurrentThreadOwnsQuotaMutex();
  MOZ_ASSERT(!mDirectoryExists);

  mDirectoryExists = true;
}

}  // namespace mozilla::dom::quota
