/* -*- 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_SharedSubResourceCache_h__
#define mozilla_SharedSubResourceCache_h__

// A cache that allows us to share subresources across documents. In order to
// use it you need to provide some types, mainly:
//
// * Loader, which implements LoaderPrincipal() and allows you to key per
//   principal. The idea is that this would be the
//   {CSS,Script,Image}Loader object.
//
// * Key (self explanatory). We might want to introduce a common key to
//   share the cache partitioning logic.
//
// * Value, which represents the final cached value. This is expected to
//   be a StyleSheet / Stencil / imgRequestProxy.
//
// * LoadingValue, which must inherit from
//   SharedSubResourceCacheLoadingValueBase (which contains the linked
//   list and the state that the cache manages). It also must provide a
//   ValueForCache() and ExpirationTime() members. For style, this is the
//   SheetLoadData.

#include "mozilla/PrincipalHashKey.h"
#include "mozilla/RefPtr.h"
#include "mozilla/WeakPtr.h"
#include "nsTHashMap.h"
#include "nsIMemoryReporter.h"
#include "nsRefPtrHashtable.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/dom/CacheExpirationTime.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/Document.h"
#include "nsContentUtils.h"
#include "nsHttpResponseHead.h"
#include "nsISupportsImpl.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/CacheablePerformanceTimingData.h"

namespace mozilla {

// A struct to hold the network-related metadata associated with the cache.
//
// When inserting a cache, the consumer should create this from the request and
// make it available via
// SharedSubResourceCacheLoadingValueBase::GetNetworkMetadata.
//
// When using a cache, the consumer can retrieve this from
// SharedSubResourceCache::Result::mNetworkMetadata and use it for notifying
// the observers once the necessary data becomes ready.
// This struct is ref-counted in order to allow this usage.
class SubResourceNetworkMetadataHolder {
 public:
  SubResourceNetworkMetadataHolder() = delete;

  explicit SubResourceNetworkMetadataHolder(nsIRequest* aRequest);

  const dom::CacheablePerformanceTimingData* GetPerfData() const {
    return mPerfData.ptrOr(nullptr);
  }

  const net::nsHttpResponseHead* GetResponseHead() const {
    return mResponseHead.get();
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SubResourceNetworkMetadataHolder)

 private:
  ~SubResourceNetworkMetadataHolder() = default;

  mozilla::Maybe<dom::CacheablePerformanceTimingData> mPerfData;
  mozilla::UniquePtr<net::nsHttpResponseHead> mResponseHead;
};

enum class CachedSubResourceState {
  Miss,
  Loading,
  Pending,
  Complete,
};

template <typename Derived>
struct SharedSubResourceCacheLoadingValueBase {
  // Whether we're in the "loading" hash table.
  RefPtr<Derived> mNext;

  virtual bool IsLoading() const = 0;
  virtual bool IsCancelled() const = 0;
  virtual bool IsSyncLoad() const = 0;

  virtual SubResourceNetworkMetadataHolder* GetNetworkMetadata() const = 0;

  virtual void StartLoading() = 0;
  virtual void SetLoadCompleted() = 0;
  virtual void OnCoalescedTo(const Derived& aExistingLoad) = 0;
  virtual void Cancel() = 0;

  // Return the next sub-resource which has the same key.
  Derived* GetNextSubResource() { return mNext; }

  ~SharedSubResourceCacheLoadingValueBase() {
    // Do this iteratively to avoid blowing up the stack.
    RefPtr<Derived> next = std::move(mNext);
    while (next) {
      next = std::move(next->mNext);
    }
  }
};

namespace SharedSubResourceCacheUtils {

void AddPerformanceEntryForCache(
    const nsString& aEntryName, const nsString& aInitiatorType,
    const SubResourceNetworkMetadataHolder* aNetworkMetadata,
    TimeStamp aStartTime, TimeStamp aEndTime, dom::Document* aDocument);

}  // namespace SharedSubResourceCacheUtils

template <typename Traits, typename Derived>
class SharedSubResourceCache {
 private:
  using Loader = typename Traits::Loader;
  using Key = typename Traits::Key;
  using Value = typename Traits::Value;
  using LoadingValue = typename Traits::LoadingValue;
  static Key KeyFromLoadingValue(const LoadingValue& aValue) {
    return Traits::KeyFromLoadingValue(aValue);
  }

  const Derived& AsDerived() const {
    return *static_cast<const Derived*>(this);
  }
  Derived& AsDerived() { return *static_cast<Derived*>(this); }

 public:
  SharedSubResourceCache(const SharedSubResourceCache&) = delete;
  SharedSubResourceCache(SharedSubResourceCache&&) = delete;
  SharedSubResourceCache() = default;

  static Derived* Get() {
    static_assert(
        std::is_base_of_v<SharedSubResourceCacheLoadingValueBase<LoadingValue>,
                          LoadingValue>);

    if (sSingleton) {
      return sSingleton.get();
    }
    MOZ_DIAGNOSTIC_ASSERT(!sSingleton);
    sSingleton = new Derived();
    sSingleton->Init();
    return sSingleton.get();
  }

  static void DeleteSingleton() { sSingleton = nullptr; }

 protected:
  struct CompleteSubResource {
    RefPtr<Value> mResource;
    RefPtr<SubResourceNetworkMetadataHolder> mNetworkMetadata;
    CacheExpirationTime mExpirationTime = CacheExpirationTime::Never();
    bool mWasSyncLoad = false;

    explicit CompleteSubResource(LoadingValue& aValue)
        : mResource(aValue.ValueForCache()),
          mNetworkMetadata(aValue.GetNetworkMetadata()),
          mExpirationTime(aValue.ExpirationTime()),
          mWasSyncLoad(aValue.IsSyncLoad()) {}

    inline bool Expired() const;
  };

 public:
  struct Result {
    Value* mCompleteValue = nullptr;
    RefPtr<SubResourceNetworkMetadataHolder> mNetworkMetadata;

    LoadingValue* mLoadingOrPendingValue = nullptr;
    CachedSubResourceState mState = CachedSubResourceState::Miss;

    constexpr Result() = default;

    explicit constexpr Result(const CompleteSubResource& aCompleteSubResource)
        : mCompleteValue(aCompleteSubResource.mResource.get()),
          mNetworkMetadata(aCompleteSubResource.mNetworkMetadata),
          mLoadingOrPendingValue(nullptr),
          mState(CachedSubResourceState::Complete) {}

    constexpr Result(LoadingValue* aLoadingOrPendingValue,
                     CachedSubResourceState aState)
        : mLoadingOrPendingValue(aLoadingOrPendingValue), mState(aState) {}
  };

  Result Lookup(Loader&, const Key&, bool aSyncLoad);

  // Tries to coalesce with an already existing load. The sheet state must be
  // the one that Lookup returned, if it returned a sheet.
  //
  // TODO(emilio): Maybe try to merge this with the lookup? Most consumers could
  // have a data there already.
  [[nodiscard]] bool CoalesceLoad(const Key&, LoadingValue& aNewLoad,
                                  CachedSubResourceState aExistingLoadState);

  size_t SizeOfIncludingThis(MallocSizeOf) const;

  // Puts the load into the "loading" set.
  void LoadStarted(const Key&, LoadingValue&);

  // Removes the load from the "loading" set if there.
  void LoadCompleted(LoadingValue&);

  // Inserts a value into the cache.
  void Insert(LoadingValue&);

  // Puts a load into the "pending" set.
  void DeferLoad(const Key&, LoadingValue&);

  template <typename Callback>
  void StartPendingLoadsForLoader(Loader&, const Callback& aShouldStartLoad);
  void CancelLoadsForLoader(Loader&);

  // Register a loader into the cache. This has the effect of keeping alive all
  // subresources for the origin of the loader's document until UnregisterLoader
  // is called.
  void RegisterLoader(Loader&);

  // Unregister a loader from the cache.
  //
  // If this is the loader for the last document of a given origin, then all the
  // subresources for that document will be removed from the cache. This needs
  // to be called when the document goes away, or when its principal changes.
  void UnregisterLoader(Loader&);

  void ClearInProcess(const Maybe<nsCOMPtr<nsIPrincipal>>& aPrincipal,
                      const Maybe<nsCString>& aSchemelessSite,
                      const Maybe<OriginAttributesPattern>& aPattern);

 protected:
  void CancelPendingLoadsForLoader(Loader&);

  void WillStartPendingLoad(LoadingValue&);

  nsTHashMap<Key, CompleteSubResource> mComplete;
  nsRefPtrHashtable<Key, LoadingValue> mPending;
  // The SheetLoadData pointers in mLoadingDatas below are weak references that
  // get cleaned up when StreamLoader::OnStopRequest gets called.
  //
  // Note that we hold on to all sheet loads, even if in the end they happen not
  // to be cacheable.
  nsTHashMap<Key, WeakPtr<LoadingValue>> mLoading;

  // An origin-to-number-of-registered-documents count, in order to manage cache
  // eviction as described in RegisterLoader / UnregisterLoader.
  nsTHashMap<PrincipalHashKey, uint32_t> mLoaderPrincipalRefCnt;

 protected:
  // Lazily created in the first Get() call.
  // The singleton should be deleted by DeleteSingleton() during shutdown.
  inline static MOZ_GLOBINIT StaticRefPtr<Derived> sSingleton;
};

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::ClearInProcess(
    const Maybe<nsCOMPtr<nsIPrincipal>>& aPrincipal,
    const Maybe<nsCString>& aSchemelessSite,
    const Maybe<OriginAttributesPattern>& aPattern) {
  MOZ_ASSERT(aSchemelessSite.isSome() == aPattern.isSome(),
             "Must pass both site and OA pattern.");

  if (!aPrincipal && !aSchemelessSite) {
    mComplete.Clear();
    return;
  }

  for (auto iter = mComplete.Iter(); !iter.Done(); iter.Next()) {
    const bool shouldRemove = [&] {
      if (aPrincipal && iter.Key().Principal()->Equals(aPrincipal.ref())) {
        return true;
      }
      if (!aSchemelessSite) {
        return false;
      }
      // Clear by site.
      nsIPrincipal* partitionPrincipal = iter.Key().PartitionPrincipal();

      // Clear entries with site. This includes entries which are partitioned
      // under other top level sites (= have a partitionKey set).
      nsAutoCString principalBaseDomain;
      nsresult rv = partitionPrincipal->GetBaseDomain(principalBaseDomain);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return false;
      }
      if (principalBaseDomain.Equals(aSchemelessSite.ref()) &&
          aPattern.ref().Matches(partitionPrincipal->OriginAttributesRef())) {
        return true;
      }

      // Clear entries partitioned under aSchemelessSite. We need to add the
      // partition key filter to aPattern so that we include any OA filtering
      // specified by the caller. For example the caller may pass aPattern = {
      // privateBrowsingId: 1 } which means we may only clear partitioned
      // private browsing data.
      OriginAttributesPattern patternWithPartitionKey(aPattern.ref());
      patternWithPartitionKey.mPartitionKeyPattern.Construct();
      patternWithPartitionKey.mPartitionKeyPattern.Value()
          .mBaseDomain.Construct(NS_ConvertUTF8toUTF16(aSchemelessSite.ref()));

      return patternWithPartitionKey.Matches(
          partitionPrincipal->OriginAttributesRef());
    }();

    if (shouldRemove) {
      iter.Remove();
    }
  }
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::RegisterLoader(Loader& aLoader) {
  mLoaderPrincipalRefCnt.LookupOrInsert(aLoader.LoaderPrincipal(), 0) += 1;
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::UnregisterLoader(
    Loader& aLoader) {
  nsIPrincipal* prin = aLoader.LoaderPrincipal();
  auto lookup = mLoaderPrincipalRefCnt.Lookup(prin);
  MOZ_RELEASE_ASSERT(lookup);
  MOZ_RELEASE_ASSERT(lookup.Data());
  if (!--lookup.Data()) {
    lookup.Remove();
    // TODO(emilio): Do this off a timer or something maybe.
    for (auto iter = mComplete.Iter(); !iter.Done(); iter.Next()) {
      if (iter.Key().LoaderPrincipal()->Equals(prin)) {
        iter.Remove();
      }
    }
  }
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::CancelPendingLoadsForLoader(
    Loader& aLoader) {
  AutoTArray<RefPtr<LoadingValue>, 10> arr;

  for (auto iter = mPending.Iter(); !iter.Done(); iter.Next()) {
    RefPtr<LoadingValue>& first = iter.Data();
    LoadingValue* prev = nullptr;
    LoadingValue* current = iter.Data();
    do {
      if (&current->Loader() != &aLoader) {
        prev = current;
        current = current->mNext;
        continue;
      }
      // Detach the load from the list, mark it as cancelled, and then below
      // call SheetComplete on it.
      RefPtr<LoadingValue> strong =
          prev ? std::move(prev->mNext) : std::move(first);
      MOZ_ASSERT(strong == current);
      if (prev) {
        prev->mNext = std::move(strong->mNext);
        current = prev->mNext;
      } else {
        first = std::move(strong->mNext);
        current = first;
      }
      arr.AppendElement(std::move(strong));
    } while (current);

    if (!first) {
      iter.Remove();
    }
  }

  for (auto& loading : arr) {
    loading->DidCancelLoad();
  }
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::WillStartPendingLoad(
    LoadingValue& aData) {
  LoadingValue* curr = &aData;
  do {
    curr->Loader().WillStartPendingLoad();
  } while ((curr = curr->mNext));
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::CancelLoadsForLoader(
    Loader& aLoader) {
  CancelPendingLoadsForLoader(aLoader);

  // We can't stop in-progress loads because some other loader may care about
  // them.
  for (LoadingValue* data : mLoading.Values()) {
    MOZ_DIAGNOSTIC_ASSERT(data,
                          "We weren't properly notified and the load was "
                          "incorrectly dropped on the floor");
    for (; data; data = data->mNext) {
      if (&data->Loader() == &aLoader) {
        data->Cancel();
        MOZ_ASSERT(data->IsCancelled());
      }
    }
  }
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::DeferLoad(const Key& aKey,
                                                        LoadingValue& aValue) {
  MOZ_ASSERT(KeyFromLoadingValue(aValue).KeyEquals(aKey));
  MOZ_DIAGNOSTIC_ASSERT(!aValue.mNext, "Should only defer loads once");

  mPending.InsertOrUpdate(aKey, RefPtr{&aValue});
}

template <typename Traits, typename Derived>
template <typename Callback>
void SharedSubResourceCache<Traits, Derived>::StartPendingLoadsForLoader(
    Loader& aLoader, const Callback& aShouldStartLoad) {
  AutoTArray<RefPtr<LoadingValue>, 10> arr;

  for (auto iter = mPending.Iter(); !iter.Done(); iter.Next()) {
    bool startIt = false;
    {
      LoadingValue* data = iter.Data();
      do {
        if (&data->Loader() == &aLoader) {
          if (aShouldStartLoad(*data)) {
            startIt = true;
            break;
          }
        }
      } while ((data = data->mNext));
    }
    if (startIt) {
      arr.AppendElement(std::move(iter.Data()));
      iter.Remove();
    }
  }
  for (auto& data : arr) {
    WillStartPendingLoad(*data);
    data->StartPendingLoad();
  }
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::Insert(LoadingValue& aValue) {
  auto key = KeyFromLoadingValue(aValue);
#ifdef DEBUG
  // We only expect a complete entry to be overriding when:
  //  * It's expired.
  //  * We're explicitly bypassing the cache.
  //  * Our entry is a sync load that was completed after aValue started loading
  //    async.
  for (const auto& entry : mComplete) {
    if (key.KeyEquals(entry.GetKey())) {
      MOZ_ASSERT(entry.GetData().Expired() ||
                     aValue.Loader().ShouldBypassCache() ||
                     (entry.GetData().mWasSyncLoad && !aValue.IsSyncLoad()),
                 "Overriding existing complete entry?");
    }
  }
#endif

  mComplete.InsertOrUpdate(key, CompleteSubResource(aValue));
}

template <typename Traits, typename Derived>
bool SharedSubResourceCache<Traits, Derived>::CoalesceLoad(
    const Key& aKey, LoadingValue& aNewLoad,
    CachedSubResourceState aExistingLoadState) {
  MOZ_ASSERT(KeyFromLoadingValue(aNewLoad).KeyEquals(aKey));
  // TODO(emilio): If aExistingLoadState is inconvenient, we could get rid of it
  // by paying two hash lookups...
  LoadingValue* existingLoad = nullptr;
  if (aExistingLoadState == CachedSubResourceState::Loading) {
    existingLoad = mLoading.Get(aKey);
    MOZ_ASSERT(existingLoad, "Caller lied about the state");
  } else if (aExistingLoadState == CachedSubResourceState::Pending) {
    existingLoad = mPending.GetWeak(aKey);
    MOZ_ASSERT(existingLoad, "Caller lied about the state");
  }

  if (!existingLoad) {
    return false;
  }

  if (aExistingLoadState == CachedSubResourceState::Pending &&
      !aNewLoad.ShouldDefer()) {
    // Kick the load off; someone cares about it right away
    RefPtr<LoadingValue> removedLoad;
    mPending.Remove(aKey, getter_AddRefs(removedLoad));
    MOZ_ASSERT(removedLoad == existingLoad, "Bad loading table");

    WillStartPendingLoad(*removedLoad);

    // We insert to the front instead of the back, to keep the invariant that
    // the front sheet always is the one that triggers the load.
    aNewLoad.mNext = std::move(removedLoad);
    return false;
  }

  LoadingValue* data = existingLoad;
  while (data->mNext) {
    data = data->mNext;
  }
  data->mNext = &aNewLoad;

  aNewLoad.OnCoalescedTo(*existingLoad);
  return true;
}

template <typename Traits, typename Derived>
auto SharedSubResourceCache<Traits, Derived>::Lookup(Loader& aLoader,
                                                     const Key& aKey,
                                                     bool aSyncLoad) -> Result {
  // Now complete sheets.
  if (auto lookup = mComplete.Lookup(aKey)) {
    const CompleteSubResource& completeSubResource = lookup.Data();
    if ((!aLoader.ShouldBypassCache() && !completeSubResource.Expired()) ||
        aLoader.HasLoaded(aKey)) {
      return Result(completeSubResource);
    }
  }

  if (aSyncLoad) {
    return Result();
  }

  if (LoadingValue* data = mLoading.Get(aKey)) {
    return Result(data, CachedSubResourceState::Loading);
  }

  if (LoadingValue* data = mPending.GetWeak(aKey)) {
    return Result(data, CachedSubResourceState::Pending);
  }

  return {};
}

template <typename Traits, typename Derived>
size_t SharedSubResourceCache<Traits, Derived>::SizeOfIncludingThis(
    MallocSizeOf aMallocSizeOf) const {
  size_t n = aMallocSizeOf(&AsDerived());

  n += mComplete.ShallowSizeOfExcludingThis(aMallocSizeOf);
  for (const auto& data : mComplete.Values()) {
    n += data.mResource->SizeOfIncludingThis(aMallocSizeOf);
  }

  return n;
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::LoadStarted(
    const Key& aKey, LoadingValue& aValue) {
  MOZ_DIAGNOSTIC_ASSERT(!aValue.IsLoading(), "Already loading? How?");
  MOZ_DIAGNOSTIC_ASSERT(KeyFromLoadingValue(aValue).KeyEquals(aKey));
  MOZ_DIAGNOSTIC_ASSERT(!mLoading.Contains(aKey), "Load not coalesced?");
  aValue.StartLoading();
  MOZ_ASSERT(aValue.IsLoading(), "Check that StartLoading is effectful.");
  mLoading.InsertOrUpdate(aKey, &aValue);
}

template <typename Traits, typename Derived>
bool SharedSubResourceCache<Traits, Derived>::CompleteSubResource::Expired()
    const {
  return mExpirationTime.IsExpired();
}

template <typename Traits, typename Derived>
void SharedSubResourceCache<Traits, Derived>::LoadCompleted(
    LoadingValue& aValue) {
  if (!aValue.IsLoading()) {
    return;
  }
  auto key = KeyFromLoadingValue(aValue);
  Maybe<LoadingValue*> value = mLoading.Extract(key);
  MOZ_DIAGNOSTIC_ASSERT(value);
  MOZ_DIAGNOSTIC_ASSERT(value.value() == &aValue);
  Unused << value;
  aValue.SetLoadCompleted();
  MOZ_ASSERT(!aValue.IsLoading(), "Check that SetLoadCompleted is effectful.");
}

}  // namespace mozilla

#endif
