/* -*- 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_DataStorage_h
#define mozilla_DataStorage_h

#include "mozilla/Atomics.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/StaticPtr.h"
#include "nsCOMPtr.h"
#include "nsTHashMap.h"
#include "nsIDataStorage.h"
#include "nsIMemoryReporter.h"
#include "nsIObserver.h"
#include "nsITimer.h"
#include "nsRefPtrHashtable.h"
#include "nsString.h"

namespace mozilla {
class TaskQueue;

/**
 * DataStorage is a threadsafe, generic, narrow string-based hash map that
 * persists data on disk and additionally handles temporary and private data.
 * However, if used in a context where there is no profile directory, data
 * will not be persisted.
 *
 * Its lifecycle is as follows:
 * - Allocate with a filename (this is or will eventually be a file in the
 *   profile directory, if the profile exists).
 * - Call Init() from the main thread. This spins off an asynchronous read
 *   of the backing file.
 * - Eventually observers of the topic "data-storage-ready" will be notified
 *   with the backing filename as the data in the notification when this
 *   has completed.
 * - Should the profile directory not be available, (e.g. in xpcshell),
 *   DataStorage will not initially read any persistent data. The
 *   "data-storage-ready" event will still be emitted. This follows semantics
 *   similar to the permission manager and allows tests that test unrelated
 *   components to proceed without a profile.
 * - A timer periodically fires on a background thread that checks if any
 *   persistent data has changed, and if so writes all persistent data to the
 *   backing file. When this happens, observers will be notified with the
 *   topic "data-storage-written" and the backing filename as the data.
 *   It is possible to receive a "data-storage-written" event while there exist
 *   pending persistent data changes. However, those changes will eventually be
 *   written when the timer fires again, and eventually another
 *   "data-storage-written" event will be sent.
 * - When a DataStorage instance observes the topic "profile-before-change" in
 *   anticipation of shutdown, all persistent data for that DataStorage is
 *   written to the backing file (this blocks the main thread). In the process
 *   of doing this, the background serial event target responsible for these
 *   writes is then shut down to prevent further writes to that file (the
 *   background timer is also cancelled when this happens).
 *   If "profile-before-change" is not observed, this happens upon observing
 *   "xpcom-shutdown-threads".
 * - For testing purposes, the preference "test.datastorage.write_timer_ms" can
 *   be set to cause the asynchronous writing of data to happen more quickly.
 * - To prevent unbounded memory and disk use, the number of entries in each
 *   table is limited to 1024. Evictions are handled in by a modified LRU scheme
 *   (see implementation comments).
 * - NB: Instances of DataStorage have long lifetimes because they are strong
 *   observers of events and won't go away until the observer service does.
 *
 * For each key/value:
 * - The key must be a non-empty string containing no instances of '\t' or '\n'
 *   (this is a limitation of how the data is stored and will be addressed in
 *   the future).
 * - The key must have a length no more than 256.
 * - The value must not contain '\n' and must have a length no more than 1024.
 *   (the length limits are to prevent unbounded disk and memory usage)
 */

class DataStorageManager final : public nsIDataStorageManager {
 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIDATASTORAGEMANAGER

 private:
  ~DataStorageManager() = default;

  bool mAlternateServicesCreated = false;
  bool mClientAuthRememberListCreated = false;
  bool mSiteSecurityServiceStateCreated = false;
};

class DataStorageItem final : public nsIDataStorageItem {
 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIDATASTORAGEITEM

  DataStorageItem(const nsACString& aKey, const nsACString& aValue,
                  nsIDataStorage::DataType aType)
      : key(aKey), value(aValue), type(aType) {}

 private:
  ~DataStorageItem() = default;

  nsAutoCString key;
  nsAutoCString value;
  nsIDataStorage::DataType type;
};

class DataStorage final : public nsIDataStorage,
                          public nsIMemoryReporter,
                          public nsIObserver {
  MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)

 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIDATASTORAGE
  NS_DECL_NSIMEMORYREPORTER
  NS_DECL_NSIOBSERVER

  explicit DataStorage(const nsString& aFilename);
  // Initializes the DataStorage. Must be called before using.
  nsresult Init();

 private:
  ~DataStorage() = default;

  void ArmTimer(const MutexAutoLock& aProofOfLock);
  void ShutdownTimer();

  class Writer;
  class Reader;

  class Entry {
   public:
    Entry();
    bool UpdateScore();

    uint32_t mScore;
    int32_t mLastAccessed;  // the last accessed time in days since the epoch
    nsCString mValue;
  };

  // Utility class for scanning tables for an entry to evict.
  class KeyAndEntry {
   public:
    nsCString mKey;
    Entry mEntry;
  };

  typedef nsTHashMap<nsCStringHashKey, Entry> DataStorageTable;
  typedef nsRefPtrHashtable<nsStringHashKey, DataStorage> DataStorages;

  void WaitForReady();
  nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
  nsresult AsyncReadData(const MutexAutoLock& aProofOfLock);

  static nsresult ValidateKeyAndValue(const nsACString& aKey,
                                      const nsACString& aValue);
  static void TimerCallback(nsITimer* aTimer, void* aClosure);
  void NotifyObservers(const char* aTopic);

  bool GetInternal(const nsACString& aKey, Entry* aEntry,
                   nsIDataStorage::DataType aType,
                   const MutexAutoLock& aProofOfLock);
  nsresult PutInternal(const nsACString& aKey, Entry& aEntry,
                       nsIDataStorage::DataType aType,
                       const MutexAutoLock& aProofOfLock);
  void MaybeEvictOneEntry(nsIDataStorage::DataType aType,
                          const MutexAutoLock& aProofOfLock);
  DataStorageTable& GetTableForType(nsIDataStorage::DataType aType,
                                    const MutexAutoLock& aProofOfLock);

  void ReadAllFromTable(nsIDataStorage::DataType aType,
                        nsTArray<RefPtr<nsIDataStorageItem>>& aItems,
                        const MutexAutoLock& aProofOfLock);

  Mutex mMutex;  // This mutex protects access to the following members:
  DataStorageTable mPersistentDataTable MOZ_GUARDED_BY(mMutex);
  DataStorageTable mTemporaryDataTable MOZ_GUARDED_BY(mMutex);
  DataStorageTable mPrivateDataTable MOZ_GUARDED_BY(mMutex);
  nsCOMPtr<nsIFile> mBackingFile MOZ_GUARDED_BY(mMutex);
  bool mPendingWrite MOZ_GUARDED_BY(
      mMutex);  // true if a write is needed but hasn't been dispatched
  bool mTimerArmed MOZ_GUARDED_BY(mMutex);
  bool mShuttingDown MOZ_GUARDED_BY(mMutex);
  RefPtr<TaskQueue> mBackgroundTaskQueue MOZ_GUARDED_BY(mMutex);
  // (End list of members protected by mMutex)

  nsCOMPtr<nsITimer> mTimer;

  mozilla::Atomic<bool> mInitCalled;  // Indicates that Init() has been called.
  uint32_t mTimerDelayMS;

  Monitor mReadyMonitor;  // Do not acquire this at the same time as mMutex.
  bool mReady MOZ_GUARDED_BY(mReadyMonitor);  // Indicates that saved data has
                                              // been read and Get can proceed.

  const nsString mFilename;

  static StaticAutoPtr<DataStorages> sDataStorages;
};

}  // namespace mozilla

#endif  // mozilla_DataStorage_h
