/* -*- 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/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/StaticPtr.h"
#include "nsCOMPtr.h"
#include "nsDataHashtable.h"
#include "nsIObserver.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsRefPtrHashtable.h"
#include "nsString.h"

namespace mozilla {

namespace dom {
class DataStorageItem;
}

/**
 * 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.
 * - When any persistent data changes, a timer is initialized that will
 *   eventually asynchronously write 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 cause the
 *   timer to be reinitialized and another "data-storage-written" event will
 *   be sent.
 * - When DataStorage observes the topic "profile-before-change" in
 *   anticipation of shutdown, all persistent data is synchronously written to
 *   the backing file. The worker thread responsible for these writes is then
 *   disabled to prevent further writes to that file (the delayed-write timer
 *   is cancelled when this happens).
 * - 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)
 */

/**
 * Data that is DataStorage_Persistent is saved on disk. DataStorage_Temporary
 * and DataStorage_Private are not saved. DataStorage_Private is meant to
 * only be set and accessed from private contexts. It will be cleared upon
 * observing the event "last-pb-context-exited".
 */
enum DataStorageType {
  DataStorage_Persistent,
  DataStorage_Temporary,
  DataStorage_Private
};

class DataStorage : public nsIObserver
{
  typedef dom::DataStorageItem DataStorageItem;

public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIOBSERVER

  // If there is a profile directory, there is or will eventually be a file
  // by the name specified by aFilename there.
  static already_AddRefed<DataStorage> Get(const nsString& aFilename);
  static already_AddRefed<DataStorage> GetIfExists(const nsString& aFilename);

  // Initializes the DataStorage. Must be called before using.
  // aDataWillPersist returns whether or not data can be persistently saved.
  nsresult Init(/*out*/bool& aDataWillPersist);
  // Given a key and a type of data, returns a value. Returns an empty string if
  // the key is not present for that type of data. If Get is called before the
  // "data-storage-ready" event is observed, it will block. NB: It is not
  // currently possible to differentiate between missing data and data that is
  // the empty string.
  nsCString Get(const nsCString& aKey, DataStorageType aType);
  // Give a key, value, and type of data, adds an entry as appropriate.
  // Updates existing entries.
  nsresult Put(const nsCString& aKey, const nsCString& aValue,
               DataStorageType aType);
  // Given a key and type of data, removes an entry if present.
  void Remove(const nsCString& aKey, DataStorageType aType);
  // Removes all entries of all types of data.
  nsresult Clear();

  // Read all of the data items.
  void GetAll(InfallibleTArray<DataStorageItem>* aItems);

private:
  explicit DataStorage(const nsString& aFilename);
  virtual ~DataStorage();

  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 nsDataHashtable<nsCStringHashKey, Entry> DataStorageTable;
  typedef nsRefPtrHashtable<nsStringHashKey, DataStorage> DataStorages;

  void WaitForReady();
  nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
  nsresult AsyncReadData(bool& aHaveProfileDir,
                         const MutexAutoLock& aProofOfLock);
  nsresult AsyncSetTimer(const MutexAutoLock& aProofOfLock);
  nsresult DispatchShutdownTimer(const MutexAutoLock& aProofOfLock);

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

  bool GetInternal(const nsCString& aKey, Entry* aEntry, DataStorageType aType,
                   const MutexAutoLock& aProofOfLock);
  nsresult PutInternal(const nsCString& aKey, Entry& aEntry,
                       DataStorageType aType,
                       const MutexAutoLock& aProofOfLock);
  void MaybeEvictOneEntry(DataStorageType aType,
                          const MutexAutoLock& aProofOfLock);
  DataStorageTable& GetTableForType(DataStorageType aType,
                                    const MutexAutoLock& aProofOfLock);

  void ReadAllFromTable(DataStorageType aType,
                        InfallibleTArray<DataStorageItem>* aItems,
                        const MutexAutoLock& aProofOfLock);

  Mutex mMutex; // This mutex protects access to the following members:
  DataStorageTable  mPersistentDataTable;
  DataStorageTable  mTemporaryDataTable;
  DataStorageTable  mPrivateDataTable;
  nsCOMPtr<nsIThread> mWorkerThread;
  nsCOMPtr<nsIFile> mBackingFile;
  nsCOMPtr<nsITimer> mTimer; // All uses after init must be on the worker thread
  uint32_t mTimerDelay; // in milliseconds
  bool mPendingWrite; // true if a write is needed but hasn't been dispatched
  bool mShuttingDown;
  bool mInitCalled; // Indicates that Init() has been called.
  // (End list of members protected by mMutex)

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

  const nsString mFilename;

  static StaticAutoPtr<DataStorages> sDataStorages;
};

} // namespace mozilla

#endif // mozilla_DataStorage_h