/* 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/. */

#include "mozilla/dom/BrowserSessionStore.h"
#include <cstdint>

#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticPtr.h"

#include "mozilla/dom/BrowserSessionStoreBinding.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/SessionStoreFormData.h"
#include "mozilla/dom/SessionStoreScrollData.h"
#include "mozilla/dom/WindowGlobalParent.h"

#include "nsTHashMap.h"
#include "nsHashtablesFwd.h"

#include "js/RootingAPI.h"

using namespace mozilla;
using namespace mozilla::dom;

static StaticAutoPtr<nsTHashMap<nsUint64HashKey, BrowserSessionStore*>>
    sSessionStore;

NS_IMPL_CYCLE_COLLECTION(BrowserSessionStore, mBrowsingContext, mFormData,
                         mScrollData)

/* static */
already_AddRefed<BrowserSessionStore> BrowserSessionStore::GetOrCreate(
    CanonicalBrowsingContext* aBrowsingContext) {
  if (!aBrowsingContext->IsTop()) {
    return nullptr;
  }

  if (!sSessionStore) {
    sSessionStore = new nsTHashMap<nsUint64HashKey, BrowserSessionStore*>();
    ClearOnShutdown(&sSessionStore);
  }

  return do_AddRef(sSessionStore->LookupOrInsertWith(
      aBrowsingContext->Id(),
      [&] { return new BrowserSessionStore(aBrowsingContext); }));
}

BrowserSessionStore::BrowserSessionStore(
    CanonicalBrowsingContext* aBrowsingContext)
    : mBrowsingContext(aBrowsingContext) {}

SessionStoreFormData* BrowserSessionStore::GetFormdata() { return mFormData; }

SessionStoreScrollData* BrowserSessionStore::GetScroll() { return mScrollData; }

static bool ShouldUpdateSessionStore(CanonicalBrowsingContext* aBrowsingContext,
                                     uint32_t aEpoch) {
  if (!aBrowsingContext) {
    return false;
  }

  if (aBrowsingContext->Top()->GetSessionStoreEpoch() != aEpoch) {
    return false;
  }

  if (aBrowsingContext->IsReplaced()) {
    return false;
  }

  if (aBrowsingContext->IsDynamic()) {
    return false;
  }

  return true;
}

// With GetOrCreate we can create either of the weak fields:
//   WeakPtr<SessionStoreFormData> mFormdata;
//   WeakPtr<SessionStoreScrollData> mScroll;
// in CanonicalBrowsingContext. If one already exists, then we return that.
template <typename T, WeakPtr<T>& (CanonicalBrowsingContext::*GetWeakRef)()>
static already_AddRefed<T> GetOrCreateEntry(
    CanonicalBrowsingContext* aBrowsingContext) {
  typename T::LocationType& location = (aBrowsingContext->*GetWeakRef)();
  RefPtr<T> entry = location.get();
  if (!entry) {
    entry = MakeRefPtr<T>();
    location = entry;
  }

  return entry.forget();
}

// With InsertEntry we can insert an entry in the session store data tree in
// either of the weak fields:
//   WeakPtr<SessionStoreFormData> mFormdata;
//   WeakPtr<SessionStoreScrollData> mScroll;
// in CanonicalBrowsingContext. If an entry is inserted where there is no parent
// entry, a spine of entries will be created until one is found, or we reach the
// top browsing context.
template <typename T>
void InsertEntry(BrowsingContext* aBrowsingContext, T* aParent, T* aUpdate) {
  int32_t offset = aBrowsingContext->ChildOffset();
  if (offset < 0) {
    return;
  }

  aParent->ClearCachedChildren();

  auto& children = aParent->Children();

  children.EnsureLengthAtLeast(offset + 1);
  if (children[offset] && !aBrowsingContext->Children().IsEmpty()) {
    children[offset]->ClearCachedChildren();
    aUpdate->ClearCachedChildren();
  }

  children[offset] = aUpdate;
}

// With RemoveEntry we can remove an entry in the session store data tree in
// either of the weak fields:
//   WeakPtr<SessionStoreFormData> mFormdata;
//   WeakPtr<SessionStoreScrollData> mScroll;
// in CanonicalBrowsingContext. If an entry is removed, where its parent doesn't
// contain data, we'll remove the parent and repeat until we either find an
// entry with data or reach the top browsing context.
template <typename T>
void RemoveEntry(BrowsingContext* aBrowsingContext, T* aParent) {
  int32_t offset = aBrowsingContext->ChildOffset();
  if (offset < 0) {
    return;
  }

  if (!aParent) {
    return;
  }

  aParent->ClearCachedChildren();

  auto& children = aParent->Children();
  size_t length = children.Length();
  if (children.Length() <= static_cast<size_t>(offset)) {
    // The children array doesn't extend to offset.
    return;
  }

  if (static_cast<size_t>(offset) < length - 1) {
    // offset is before the last item in the children array.
    children[offset] = nullptr;
    return;
  }

  // offset is the last item, find the first non-null item before it
  // and remove anything after that item.
  while (offset > 0 && !children[offset - 1]) {
    --offset;
  }

  children.TruncateLength(offset);
}

// With UpdateSessionStoreField we can update an entry in the session store
// data tree in either of the weak fields:
//   WeakPtr<SessionStoreFormData> mFormdata;
//   WeakPtr<SessionStoreScrollData> mScroll;
// in CanonicalBrowsingContext. UpdateSessionStoreField uses the above
// functions, `GetOrCreateEntry`, `InsertEntry` and `RemoveEntry` to operate on
// the weak fields. We return the top-level entry attached to the top browsing
// context through `aEntry`. If the entire browsing context tree contains no
// session store data this will be set to nullptr.
template <typename T, WeakPtr<T>& (CanonicalBrowsingContext::*GetWeakRef)()>
void UpdateSessionStoreField(CanonicalBrowsingContext* aBrowsingContext,
                             const typename T::CollectedType& aUpdate,
                             T** aEntry) {
  RefPtr<T> currentEntry;

  if (T::HasData(aUpdate)) {
    currentEntry = GetOrCreateEntry<T, GetWeakRef>(aBrowsingContext);
    currentEntry->Update(aUpdate);

    CanonicalBrowsingContext* currentBrowsingContext = aBrowsingContext;
    while (CanonicalBrowsingContext* parent =
               currentBrowsingContext->GetParent()) {
      WeakPtr<T>& parentEntry = (parent->*GetWeakRef)();
      if (parentEntry) {
        InsertEntry(aBrowsingContext, parentEntry.get(), currentEntry.get());
        break;
      }

      RefPtr<T> entry = GetOrCreateEntry<T, GetWeakRef>(parent);
      InsertEntry(currentBrowsingContext, entry.get(), currentEntry.get());

      currentEntry = entry;
      currentBrowsingContext = parent;
    }

    currentEntry = (aBrowsingContext->Top()->*GetWeakRef)().get();
  } else if ((currentEntry = (aBrowsingContext->*GetWeakRef)())) {
    currentEntry->Update(aUpdate);

    CanonicalBrowsingContext* currentBrowsingContext = aBrowsingContext;
    while (CanonicalBrowsingContext* parent =
               currentBrowsingContext->GetParent()) {
      if (!currentEntry || !currentEntry->IsEmpty()) {
        break;
      }

      T* parentEntry = (parent->*GetWeakRef)().get();
      RemoveEntry(currentBrowsingContext, parentEntry);

      currentEntry = parentEntry;
      currentBrowsingContext = parent;
    }

    if (currentEntry && currentEntry->IsEmpty()) {
      currentEntry = nullptr;
    } else {
      currentEntry = (aBrowsingContext->Top()->*GetWeakRef)().get();
    }
  } else {
    currentEntry = (aBrowsingContext->Top()->*GetWeakRef)().get();
  }

  *aEntry = currentEntry.forget().take();
}

void BrowserSessionStore::UpdateSessionStore(
    CanonicalBrowsingContext* aBrowsingContext,
    const Maybe<sessionstore::FormData>& aFormData,
    const Maybe<nsPoint>& aScrollPosition, uint32_t aEpoch) {
  if (!aFormData && !aScrollPosition) {
    return;
  }

  if (!ShouldUpdateSessionStore(aBrowsingContext, aEpoch)) {
    return;
  }

  if (aFormData) {
    UpdateSessionStoreField<
        SessionStoreFormData,
        &CanonicalBrowsingContext::GetSessionStoreFormDataRef>(
        aBrowsingContext, *aFormData, getter_AddRefs(mFormData));
  }

  if (aScrollPosition) {
    UpdateSessionStoreField<
        SessionStoreScrollData,
        &CanonicalBrowsingContext::GetSessionStoreScrollDataRef>(
        aBrowsingContext, *aScrollPosition, getter_AddRefs(mScrollData));
  }
}

void BrowserSessionStore::RemoveSessionStore(
    CanonicalBrowsingContext* aBrowsingContext) {
  if (!aBrowsingContext) {
    return;
  }

  CanonicalBrowsingContext* parentContext = aBrowsingContext->GetParent();

  if (parentContext) {
    RemoveEntry(aBrowsingContext,
                parentContext->GetSessionStoreFormDataRef().get());

    RemoveEntry(aBrowsingContext,
                parentContext->GetSessionStoreScrollDataRef().get());

    return;
  }

  if (aBrowsingContext->IsTop()) {
    mFormData = nullptr;
    mScrollData = nullptr;
  }
}

BrowserSessionStore::~BrowserSessionStore() {
  if (sSessionStore) {
    sSessionStore->Remove(mBrowsingContext->Id());
  }
}
