/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* 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 URLPreloader_h
#define URLPreloader_h

#include "mozilla/DataMutex.h"
#include "mozilla/FileLocation.h"
#include "mozilla/HashFunctions.h"
#include "mozilla/LinkedList.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Monitor.h"
#include "mozilla/Omnijar.h"
#include "mozilla/Range.h"
#include "mozilla/Vector.h"
#include "mozilla/Result.h"
#include "nsClassHashtable.h"
#include "nsHashKeys.h"
#include "nsIChromeRegistry.h"
#include "nsIFile.h"
#include "nsIURI.h"
#include "nsIMemoryReporter.h"
#include "nsIResProtocolHandler.h"
#include "nsIThread.h"
#include "nsReadableUtils.h"

class nsZipArchive;

namespace mozilla {
namespace loader {
class InputBuffer;
}

using namespace mozilla::loader;

class ScriptPreloader;

/**
 * A singleton class to manage loading local URLs during startup, recording
 * them, and pre-loading them during early startup in the next session. URLs
 * that are not already loaded (or already being pre-loaded) when required are
 * read synchronously from disk, and (if startup is not already complete)
 * added to the pre-load list for the next session.
 */
class URLPreloader final : public nsIMemoryReporter {
  MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)

  URLPreloader() = default;

 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIMEMORYREPORTER

  static URLPreloader& GetSingleton();

  // The type of read operation to perform.
  enum ReadType {
    // Read the file and then immediately forget its data.
    Forget,
    // Read the file and retain its data for the next caller.
    Retain,
  };

  // Helpers to read the contents of files or JAR archive entries with various
  // representations. If the preloader has not yet been initialized, or the
  // given location is not supported by the cache, the entries will be read
  // synchronously, and not stored in the cache.
  static Result<nsCString, nsresult> Read(FileLocation& location,
                                          ReadType readType = Forget);

  static Result<nsCString, nsresult> ReadURI(nsIURI* uri,
                                             ReadType readType = Forget);

  static Result<nsCString, nsresult> ReadFile(nsIFile* file,
                                              ReadType readType = Forget);

  static Result<nsCString, nsresult> ReadZip(nsZipArchive* archive,
                                             const nsACString& path,
                                             ReadType readType = Forget);

  void SetStartupFinished() { mStartupFinished = true; }

 private:
  struct CacheKey;

  Result<nsCString, nsresult> ReadInternal(const CacheKey& key,
                                           ReadType readType);

  Result<nsCString, nsresult> ReadURIInternal(nsIURI* uri, ReadType readType);

  Result<nsCString, nsresult> ReadFileInternal(nsIFile* file,
                                               ReadType readType);

  static Result<nsCString, nsresult> Read(const CacheKey& key,
                                          ReadType readType);

  static bool sInitialized;

  static mozilla::StaticRefPtr<URLPreloader> sSingleton;

 protected:
  friend class AddonManagerStartup;
  friend class ScriptPreloader;

  virtual ~URLPreloader();

  Result<Ok, nsresult> WriteCache();

  static URLPreloader& ReInitialize();

  // Clear leftover entries after the cache has been written.
  void Cleanup();

  // Begins reading files off-thread, and ensures that initialization has
  // completed before leaving the current scope. The caller *must* ensure that
  // no code on the main thread access Omnijar, either directly or indirectly,
  // for the lifetime of this guard object.
  struct MOZ_RAII AutoBeginReading final {
    AutoBeginReading() { GetSingleton().BeginBackgroundRead(); }

    ~AutoBeginReading() {
      auto& reader = GetSingleton();

      MonitorAutoLock mal(reader.mMonitor);

      while (!reader.mReaderInitialized && URLPreloader::sInitialized) {
        mal.Wait();
      }
    }
  };

 private:
  // Represents a key for an entry in the URI cache, based on its file or JAR
  // location.
  struct CacheKey {
    // The type of the entry. TypeAppJar and TypeGREJar entries are in the
    // app-specific or toolkit Omnijar files, and are handled specially.
    // TypeFile entries are plain files in the filesystem.
    enum EntryType : uint8_t {
      TypeAppJar,
      TypeGREJar,
      TypeFile,
    };

    CacheKey() = default;
    CacheKey(const CacheKey& other) = default;

    CacheKey(EntryType type, const nsACString& path)
        : mType(type), mPath(path) {}

    explicit CacheKey(nsIFile* file) : mType(TypeFile) {
      nsString path;
      MOZ_ALWAYS_SUCCEEDS(file->GetPath(path));
      MOZ_DIAGNOSTIC_ASSERT(path.Length() > 0);
      CopyUTF16toUTF8(path, mPath);
    }

    explicit inline CacheKey(InputBuffer& buffer);

    // Encodes or decodes the cache key for storage in a session cache file.
    template <typename Buffer>
    void Code(Buffer& buffer) {
      buffer.codeUint8(*reinterpret_cast<uint8_t*>(&mType));
      buffer.codeString(mPath);
      MOZ_DIAGNOSTIC_ASSERT(mPath.Length() > 0);
    }

    uint32_t Hash() const { return HashGeneric(mType, HashString(mPath)); }

    bool operator==(const CacheKey& other) const {
      return mType == other.mType && mPath == other.mPath;
    }

    // Returns the Omnijar type for this entry. This may *only* be called
    // for Omnijar entries.
    Omnijar::Type OmnijarType() {
      switch (mType) {
        case TypeAppJar:
          return Omnijar::APP;
        case TypeGREJar:
          return Omnijar::GRE;
        default:
          MOZ_CRASH("Unexpected entry type");
          return Omnijar::GRE;
      }
    }

    const char* TypeString() const {
      switch (mType) {
        case TypeAppJar:
          return "AppJar";
        case TypeGREJar:
          return "GREJar";
        case TypeFile:
          return "File";
      }
      MOZ_ASSERT_UNREACHABLE("no such type");
      return "";
    }

    already_AddRefed<nsZipArchive> Archive() {
      return Omnijar::GetReader(OmnijarType());
    }

    Result<FileLocation, nsresult> ToFileLocation();

    EntryType mType = TypeFile;

    // The path of the entry. For Type*Jar entries, this is the path within
    // the Omnijar archive. For TypeFile entries, this is the full path to
    // the file.
    nsCString mPath{};
  };

  // Represents an entry in the URI cache.
  struct URLEntry final : public CacheKey, public LinkedListElement<URLEntry> {
    MOZ_IMPLICIT URLEntry(const CacheKey& key)
        : CacheKey(key), mData(VoidCString()) {}

    explicit URLEntry(nsIFile* file) : CacheKey(file) {}

    // For use with nsTArray::Sort.
    //
    // Sorts entries by the time they were initially read during this
    // session.
    struct Comparator final {
      bool Equals(const URLEntry* a, const URLEntry* b) const {
        return a->mReadTime == b->mReadTime;
      }

      bool LessThan(const URLEntry* a, const URLEntry* b) const {
        return a->mReadTime < b->mReadTime;
      }
    };

    // Sets the first-used time of this file to the earlier of its current
    // first-use time or the given timestamp.
    void UpdateUsedTime(const TimeStamp& time = TimeStamp::Now()) {
      if (!mReadTime || time < mReadTime) {
        mReadTime = time;
      }
    }

    Result<nsCString, nsresult> Read();
    static Result<nsCString, nsresult> ReadLocation(FileLocation& location);

    size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const {
      return (mallocSizeOf(this) +
              mPath.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
              mData.SizeOfExcludingThisEvenIfShared(mallocSizeOf));
    }

    // Reads the contents of the file referenced by this entry, or wait for
    // an off-thread read operation to finish if it is currently pending,
    // and return the file's contents.
    Result<nsCString, nsresult> ReadOrWait(ReadType readType);

    nsCString mData;

    TimeStamp mReadTime{};

    nsresult mResultCode = NS_OK;
  };

  // Resolves the given URI to a CacheKey, if the URI is cacheable.
  Result<CacheKey, nsresult> ResolveURI(nsIURI* uri);

  static already_AddRefed<URLPreloader> Create(bool* aInitialized);

  Result<Ok, nsresult> InitInternal();

  // Returns a file pointer to the (possibly nonexistent) cache file with the
  // given suffix.
  Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix);
  // Finds the correct cache file to use for this session.
  Result<nsCOMPtr<nsIFile>, nsresult> FindCacheFile();

  Result<Ok, nsresult> ReadCache(LinkedList<URLEntry>& pendingURLs);

  void BackgroundReadFiles();
  void BeginBackgroundRead();

  using HashType = nsClassHashtable<nsGenericHashKey<CacheKey>, URLEntry>;

  size_t ShallowSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf);

  bool mStartupFinished = false;
  bool mReaderInitialized = false;

  // Only to be accessed from the cache write thread.
  bool mCacheWritten = false;

  // The prefix URLs for files in the GRE and App omni jar archives.
  nsCString mGREPrefix;
  nsCString mAppPrefix;

  nsCOMPtr<nsIResProtocolHandler> mResProto;
  nsCOMPtr<nsIChromeRegistry> mChromeReg;
  nsCOMPtr<nsIFile> mProfD;

  // Note: We use a RefPtr rather than an nsCOMPtr here because the
  // AssertNoQueryNeeded checks done by getter_AddRefs happen at a time that
  // violate data access invariants. It's wrapped in a mutex because
  // the reader thread needs to be able to null this out to terminate itself.
  DataMutex<RefPtr<nsIThread>> mReaderThread{"ReaderThread"};

  // A map of URL entries which have were either read this session, or read
  // from the last session's cache file.
  HashType mCachedURLs;

  Monitor mMonitor MOZ_UNANNOTATED{"[URLPreloader::mMutex]"};
};

}  // namespace mozilla

#endif  // URLPreloader_h