/* 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/SessionStoreChild.h"

#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Assertions.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/InProcessChild.h"
#include "mozilla/dom/InProcessParent.h"
#include "mozilla/dom/BrowserSessionStore.h"
#include "mozilla/dom/SessionStoreChangeListener.h"
#include "mozilla/dom/SessionStoreParent.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/ipc/Endpoint.h"
#include "nsCOMPtr.h"
#include "nsFrameLoader.h"

using namespace mozilla;
using namespace mozilla::dom;

class nsIDocShell;

static already_AddRefed<TabListener> CreateTabListener(nsIDocShell* aDocShell) {
  RefPtr<TabListener> tabListener =
      mozilla::MakeRefPtr<TabListener>(aDocShell, nullptr);
  nsresult rv = tabListener->Init();
  if (NS_FAILED(rv)) {
    return nullptr;
  }

  return tabListener.forget();
}

already_AddRefed<SessionStoreChild> SessionStoreChild::GetOrCreate(
    BrowsingContext* aBrowsingContext, Element* aOwnerElement) {
  RefPtr<TabListener> tabListener =
      CreateTabListener(aBrowsingContext->GetDocShell());
  if (!tabListener) {
    return nullptr;
  }

  RefPtr<SessionStoreChangeListener> sessionStoreChangeListener =
      SessionStoreChangeListener::Create(aBrowsingContext);
  if (!sessionStoreChangeListener) {
    return nullptr;
  }

  RefPtr<SessionStoreChild> sessionStoreChild =
      new SessionStoreChild(tabListener, sessionStoreChangeListener);

  sessionStoreChangeListener->SetActor(sessionStoreChild);

  if (XRE_IsParentProcess()) {
    MOZ_DIAGNOSTIC_ASSERT(aOwnerElement);
    InProcessChild* inProcessChild = InProcessChild::Singleton();
    InProcessParent* inProcessParent = InProcessParent::Singleton();
    if (!inProcessChild || !inProcessParent) {
      return nullptr;
    }

    RefPtr<BrowserSessionStore> sessionStore =
        BrowserSessionStore::GetOrCreate(aBrowsingContext->Canonical()->Top());
    if (!sessionStore) {
      return nullptr;
    }

    CanonicalBrowsingContext* browsingContext = aBrowsingContext->Canonical();
    RefPtr<SessionStoreParent> sessionStoreParent =
        new SessionStoreParent(browsingContext, sessionStore);
    ManagedEndpoint<PSessionStoreParent> endpoint =
        inProcessChild->OpenPSessionStoreEndpoint(sessionStoreChild);
    inProcessParent->BindPSessionStoreEndpoint(std::move(endpoint),
                                               sessionStoreParent);
  } else {
    MOZ_DIAGNOSTIC_ASSERT(!aOwnerElement);
    RefPtr<BrowserChild> browserChild =
        BrowserChild::GetFrom(aBrowsingContext->GetDOMWindow());

    MOZ_DIAGNOSTIC_ASSERT(browserChild);
    MOZ_DIAGNOSTIC_ASSERT(aBrowsingContext->IsInProcess());
    sessionStoreChild = static_cast<SessionStoreChild*>(
        browserChild->SendPSessionStoreConstructor(sessionStoreChild));
  }

  return sessionStoreChild.forget();
}

/* static */
SessionStoreChild* SessionStoreChild::From(WindowGlobalChild* aWindowChild) {
  if (!aWindowChild) {
    return nullptr;
  }

  // If `aWindowChild` is inprocess
  if (RefPtr<BrowserChild> browserChild = aWindowChild->GetBrowserChild()) {
    return browserChild->GetSessionStoreChild();
  }

  if (XRE_IsContentProcess()) {
    return nullptr;
  }

  WindowGlobalParent* windowParent = aWindowChild->WindowContext()->Canonical();
  if (!windowParent) {
    return nullptr;
  }

  RefPtr<nsFrameLoader> frameLoader = windowParent->GetRootFrameLoader();
  if (!frameLoader) {
    return nullptr;
  }

  return frameLoader->GetSessionStoreChild();
}

SessionStoreChild::SessionStoreChild(
    TabListener* aSessionStoreListener,
    SessionStoreChangeListener* aSessionStoreChangeListener)
    : mSessionStoreListener(aSessionStoreListener),
      mSessionStoreChangeListener(aSessionStoreChangeListener) {}

void SessionStoreChild::SetEpoch(uint32_t aEpoch) {
  if (mSessionStoreListener) {
    mSessionStoreListener->SetEpoch(aEpoch);
  }

  if (mSessionStoreChangeListener) {
    mSessionStoreChangeListener->SetEpoch(aEpoch);
  }
}

void SessionStoreChild::SetOwnerContent(Element* aElement) {
  if (mSessionStoreChangeListener) {
    mSessionStoreChangeListener->FlushSessionStore();
  }

  if (!aElement) {
    return;
  }

  if (mSessionStoreListener) {
    mSessionStoreListener->SetOwnerContent(aElement);
  }
}

void SessionStoreChild::Stop() {
  if (mSessionStoreListener) {
    mSessionStoreListener->RemoveListeners();
    mSessionStoreListener = nullptr;
  }

  if (mSessionStoreChangeListener) {
    mSessionStoreChangeListener->Stop();
  }
}

void SessionStoreChild::UpdateEventTargets() {
  if (mSessionStoreChangeListener) {
    mSessionStoreChangeListener->UpdateEventTargets();
  }
}

void SessionStoreChild::UpdateSessionStore(bool aSessionHistoryUpdate,
                                           const MaybeSessionStoreZoom& aZoom) {
  if (!mSessionStoreListener) {
    // This is the case when we're shutting down, and expect a final update.
    SessionStoreUpdate(Nothing(), Nothing(), Nothing(), aSessionHistoryUpdate,
                       0);
    return;
  }

  RefPtr<ContentSessionStore> store = mSessionStoreListener->GetSessionStore();

  Maybe<nsCString> docShellCaps;
  if (store->IsDocCapChanged()) {
    docShellCaps.emplace(store->GetDocShellCaps());
  }

  Maybe<bool> privatedMode;
  if (store->IsPrivateChanged()) {
    privatedMode.emplace(store->GetPrivateModeEnabled());
  }

  SessionStoreUpdate(
      docShellCaps, privatedMode, aZoom,
      store->GetAndClearSHistoryChanged() || aSessionHistoryUpdate,
      mSessionStoreListener->GetEpoch());
}

void SessionStoreChild::FlushSessionStore() {
  if (mSessionStoreChangeListener) {
    mSessionStoreChangeListener->FlushSessionStore();
  }
}

void SessionStoreChild::UpdateSHistoryChanges() {
  if (mSessionStoreListener) {
    mSessionStoreListener->UpdateSHistoryChanges();
  }
}

mozilla::ipc::IPCResult SessionStoreChild::RecvFlushTabState(
    FlushTabStateResolver&& aResolver) {
  if (mSessionStoreChangeListener) {
    mSessionStoreChangeListener->FlushSessionStore();
  }
  aResolver(true);

  return IPC_OK();
}

void SessionStoreChild::SessionStoreUpdate(
    const Maybe<nsCString>& aDocShellCaps, const Maybe<bool>& aPrivatedMode,
    const MaybeSessionStoreZoom& aZoom, const bool aNeedCollectSHistory,
    const uint32_t& aEpoch) {
  // Skipping an update here is acceptable, since it will only happen
  // during actor teardown, and we're most likely in a final flush
  // which expects that not all content processes manage to respond.
  if (XRE_IsContentProcess() && CanSend()) {
    (void)SendSessionStoreUpdate(aDocShellCaps, aPrivatedMode, aZoom,
                                 aNeedCollectSHistory, aEpoch);
  } else if (SessionStoreParent* sessionStoreParent =
                 static_cast<SessionStoreParent*>(
                     InProcessChild::ParentActorFor(this))) {
    sessionStoreParent->SessionStoreUpdate(aDocShellCaps, aPrivatedMode, aZoom,
                                           aNeedCollectSHistory, aEpoch);
  }
}

void SessionStoreChild::IncrementalSessionStoreUpdate(
    const MaybeDiscarded<BrowsingContext>& aBrowsingContext,
    const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition,
    uint32_t aEpoch) {
  // Skipping an update here is acceptable, since it will only happen
  // during actor teardown, and we're most likely in a final flush
  // which expects that not all content processes manage to respond.
  if (XRE_IsContentProcess() && CanSend()) {
    (void)SendIncrementalSessionStoreUpdate(aBrowsingContext, aFormData,
                                            aScrollPosition, aEpoch);
  } else if (SessionStoreParent* sessionStoreParent =
                 static_cast<SessionStoreParent*>(
                     InProcessChild::ParentActorFor(this))) {
    sessionStoreParent->IncrementalSessionStoreUpdate(
        aBrowsingContext, aFormData, aScrollPosition, aEpoch);
  }
}

void SessionStoreChild::ResetSessionStore(
    const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch) {
  if (XRE_IsContentProcess()) {
    (void)SendResetSessionStore(aBrowsingContext, aEpoch);
  } else if (SessionStoreParent* sessionStoreParent =
                 static_cast<SessionStoreParent*>(
                     InProcessChild::ParentActorFor(this))) {
    sessionStoreParent->ResetSessionStore(aBrowsingContext, aEpoch);
  }
}

NS_IMPL_CYCLE_COLLECTION_CLASS(SessionStoreChild)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SessionStoreChild)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSessionStoreListener)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSessionStoreChangeListener)
  tmp->UnlinkManager();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SessionStoreChild)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSessionStoreListener)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSessionStoreChangeListener)
  if (XRE_IsContentProcess()) {
    CycleCollectionNoteChild(cb, static_cast<BrowserChild*>(tmp->Manager()),
                             "Manager()");
  }
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
