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

#include "mozilla/ProfileBufferChunk.h"

#include <functional>
#include <vector>

namespace mozilla {

// A "Controlled" chunk manager will provide updates about chunks that it
// creates, releases, and destroys; and it can destroy released chunks as
// requested.
class ProfileBufferControlledChunkManager {
 public:
  using Length = ProfileBufferChunk::Length;

  virtual ~ProfileBufferControlledChunkManager() = default;

  // Minimum amount of chunk metadata to be transferred between processes.
  struct ChunkMetadata {
    // Timestamp when chunk was marked "done", which is used to:
    // - determine its age, so the oldest one will be destroyed first,
    // - uniquely identify this chunk in this process. (The parent process is
    //   responsible for associating this timestamp to its process id.)
    TimeStamp mDoneTimeStamp;
    // Size of this chunk's buffer.
    Length mBufferBytes;

    ChunkMetadata(TimeStamp aDoneTimeStamp, Length aBufferBytes)
        : mDoneTimeStamp(aDoneTimeStamp), mBufferBytes(aBufferBytes) {}
  };

  // Class collecting all information necessary to describe updates that
  // happened in a chunk manager.
  // An update can be folded into a previous update.
  class Update {
   public:
    // Construct a "not-an-Update" object, which should only be used after a
    // real update is folded into it.
    Update() = default;

    // Construct a "final" Update, which marks the end of all updates from a
    // chunk manager.
    explicit Update(decltype(nullptr)) : mUnreleasedBytes(FINAL) {}

    // Construct an Update from the given data and released chunks.
    // The chunk pointers may be null, and it doesn't matter if
    // `aNewlyReleasedChunks` is already linked to `aExistingReleasedChunks` or
    // not.
    Update(size_t aUnreleasedBytes, size_t aReleasedBytes,
           const ProfileBufferChunk* aExistingReleasedChunks,
           const ProfileBufferChunk* aNewlyReleasedChunks)
        : mUnreleasedBytes(aUnreleasedBytes),
          mReleasedBytes(aReleasedBytes),
          mOldestDoneTimeStamp(
              aExistingReleasedChunks
                  ? aExistingReleasedChunks->ChunkHeader().mDoneTimeStamp
                  : TimeStamp{}) {
      MOZ_RELEASE_ASSERT(
          !IsNotUpdate(),
          "Empty update should only be constructed with default constructor");
      MOZ_RELEASE_ASSERT(
          !IsFinal(),
          "Final update should only be constructed with nullptr constructor");
      for (const ProfileBufferChunk* chunk = aNewlyReleasedChunks; chunk;
           chunk = chunk->GetNext()) {
        mNewlyReleasedChunks.emplace_back(ChunkMetadata{
            chunk->ChunkHeader().mDoneTimeStamp, chunk->BufferBytes()});
      }
    }

    // Construct an Update from raw data.
    // This may be used to re-construct an Update that was previously
    // serialized.
    Update(size_t aUnreleasedBytes, size_t aReleasedBytes,
           TimeStamp aOldestDoneTimeStamp,
           std::vector<ChunkMetadata>&& aNewlyReleasedChunks)
        : mUnreleasedBytes(aUnreleasedBytes),
          mReleasedBytes(aReleasedBytes),
          mOldestDoneTimeStamp(aOldestDoneTimeStamp),
          mNewlyReleasedChunks(std::move(aNewlyReleasedChunks)) {}

    // Clear the Update completely and return it to a "not-an-Update" state.
    void Clear() {
      mUnreleasedBytes = NO_UPDATE;
      mReleasedBytes = 0;
      mOldestDoneTimeStamp = TimeStamp{};
      mNewlyReleasedChunks.clear();
    }

    bool IsNotUpdate() const { return mUnreleasedBytes == NO_UPDATE; }

    bool IsFinal() const { return mUnreleasedBytes == FINAL; }

    size_t UnreleasedBytes() const {
      MOZ_RELEASE_ASSERT(!IsNotUpdate(),
                         "Cannot access UnreleasedBytes from empty update");
      MOZ_RELEASE_ASSERT(!IsFinal(),
                         "Cannot access UnreleasedBytes from final update");
      return mUnreleasedBytes;
    }

    size_t ReleasedBytes() const {
      MOZ_RELEASE_ASSERT(!IsNotUpdate(),
                         "Cannot access ReleasedBytes from empty update");
      MOZ_RELEASE_ASSERT(!IsFinal(),
                         "Cannot access ReleasedBytes from final update");
      return mReleasedBytes;
    }

    TimeStamp OldestDoneTimeStamp() const {
      MOZ_RELEASE_ASSERT(!IsNotUpdate(),
                         "Cannot access OldestDoneTimeStamp from empty update");
      MOZ_RELEASE_ASSERT(!IsFinal(),
                         "Cannot access OldestDoneTimeStamp from final update");
      return mOldestDoneTimeStamp;
    }

    const std::vector<ChunkMetadata>& NewlyReleasedChunksRef() const {
      MOZ_RELEASE_ASSERT(
          !IsNotUpdate(),
          "Cannot access NewlyReleasedChunksRef from empty update");
      MOZ_RELEASE_ASSERT(
          !IsFinal(), "Cannot access NewlyReleasedChunksRef from final update");
      return mNewlyReleasedChunks;
    }

    // Fold a later update into this one.
    void Fold(Update&& aNewUpdate) {
      MOZ_ASSERT(
          !IsFinal() || aNewUpdate.IsFinal(),
          "There shouldn't be another non-final update after the final update");

      if (IsNotUpdate() || aNewUpdate.IsFinal()) {
        // We were empty, or the new update is the final update, we just switch
        // to that new update.
        *this = std::move(aNewUpdate);
        return;
      }

      mUnreleasedBytes = aNewUpdate.mUnreleasedBytes;
      mReleasedBytes = aNewUpdate.mReleasedBytes;
      if (!aNewUpdate.mOldestDoneTimeStamp.IsNull()) {
        MOZ_ASSERT(mOldestDoneTimeStamp.IsNull() ||
                   mOldestDoneTimeStamp <= aNewUpdate.mOldestDoneTimeStamp);
        mOldestDoneTimeStamp = aNewUpdate.mOldestDoneTimeStamp;
        auto it = mNewlyReleasedChunks.begin();
        while (it != mNewlyReleasedChunks.end() &&
               it->mDoneTimeStamp < mOldestDoneTimeStamp) {
          it = mNewlyReleasedChunks.erase(it);
        }
      }
      if (!aNewUpdate.mNewlyReleasedChunks.empty()) {
        mNewlyReleasedChunks.reserve(mNewlyReleasedChunks.size() +
                                     aNewUpdate.mNewlyReleasedChunks.size());
        mNewlyReleasedChunks.insert(mNewlyReleasedChunks.end(),
                                    aNewUpdate.mNewlyReleasedChunks.begin(),
                                    aNewUpdate.mNewlyReleasedChunks.end());
      }
    }

   private:
    static const size_t NO_UPDATE = size_t(-1);
    static const size_t FINAL = size_t(-2);

    size_t mUnreleasedBytes = NO_UPDATE;
    size_t mReleasedBytes = 0;
    TimeStamp mOldestDoneTimeStamp;
    std::vector<ChunkMetadata> mNewlyReleasedChunks;
  };

  using UpdateCallback = std::function<void(Update&&)>;

  // This *may* be set (or reset) by an object that needs to know about all
  // chunk updates that happen in this manager. The main use will be to
  // coordinate the global memory usage of Firefox.
  // If a non-empty callback is given, it will be immediately invoked with the
  // current state.
  // When the callback is about to be destroyed (by overwriting it here, or in
  // the class destructor), it will be invoked one last time with an empty
  // update.
  // Note that the callback (even the first current-state callback) will be
  // invoked from inside a locked scope, so it should *not* call other functions
  // of the chunk manager. A side benefit of this locking is that it guarantees
  // that no two invocations can overlap.
  virtual void SetUpdateCallback(UpdateCallback&& aUpdateCallback) = 0;

  // This is a request to destroy all chunks before the given timestamp.
  // This timestamp should be one that was given in a previous UpdateCallback
  // call. Obviously, only released chunks can be destroyed.
  virtual void DestroyChunksAtOrBefore(TimeStamp aDoneTimeStamp) = 0;
};

}  // namespace mozilla

#endif  // ProfileBufferControlledChunkManager_h
