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

#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/Maybe.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/NavigateEvent.h"
#include "mozilla/dom/NavigationBinding.h"
#include "nsHashtablesFwd.h"
#include "nsStringFwd.h"

class nsIDHashKey;

namespace mozilla::dom {

class FormData;
class NavigationActivation;
class NavigationDestination;
class NavigationHistoryEntry;
struct NavigationNavigateOptions;
struct NavigationOptions;
class NavigationTransition;
struct NavigationUpdateCurrentEntryOptions;
struct NavigationReloadOptions;
struct NavigationResult;
class PreviousSessionHistoryInfo;

class SessionHistoryInfo;

// https://html.spec.whatwg.org/#navigation-api-method-tracker
struct NavigationAPIMethodTracker final : public nsISupports {
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL
  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(NavigationAPIMethodTracker)

  NavigationAPIMethodTracker(Navigation* aNavigationObject,
                             const Maybe<nsID> aKey, const JS::Value& aInfo,
                             nsIStructuredCloneContainer* aSerializedState,
                             NavigationHistoryEntry* aCommittedToEntry,
                             Promise* aCommittedPromise,
                             Promise* aFinishedPromise, bool aPending = false);

  // Mark this tracker as no longer pending (promoted to ongoing).
  void MarkAsNotPending() { mPending = false; }

  void CleanUp();
  void NotifyAboutCommittedToEntry(NavigationHistoryEntry* aNHE);
  void ResolveFinishedPromise();
  void RejectFinishedPromise(JS::Handle<JS::Value> aException);
  void CreateResult(JSContext* aCx, NavigationResult& aResult);
  void SetSerializedState(nsIStructuredCloneContainer* aSerializedState) {
    mSerializedState = aSerializedState;
  }

  Promise* CommittedPromise() { return mCommittedPromise; }
  Promise* FinishedPromise() { return mFinishedPromise; }

  bool IsHandled() const;

  RefPtr<Navigation> mNavigationObject;
  Maybe<nsID> mKey;
  JS::Heap<JS::Value> mInfo;

 private:
  ~NavigationAPIMethodTracker();

  bool mPending;
  RefPtr<nsIStructuredCloneContainer> mSerializedState;
  RefPtr<NavigationHistoryEntry> mCommittedToEntry;
  RefPtr<Promise> mCommittedPromise;
  RefPtr<Promise> mFinishedPromise;
};

class Navigation final : public DOMEventTargetHelper {
 public:
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Navigation, DOMEventTargetHelper)

  explicit Navigation(nsPIDOMWindowInner* aWindow);

  bool IsNavigation() const override { return true; }

  NS_IMPL_FROMEVENTTARGET_HELPER(Navigation, IsNavigation())

  using EventTarget::EventListenerAdded;
  virtual void EventListenerAdded(nsAtom* aType) override;
  using EventTarget::EventListenerRemoved;
  virtual void EventListenerRemoved(nsAtom* aType) override;

  // Navigation.webidl
  void Entries(nsTArray<RefPtr<NavigationHistoryEntry>>& aResult) const;
  already_AddRefed<NavigationHistoryEntry> GetCurrentEntry() const;
  MOZ_CAN_RUN_SCRIPT
  void UpdateCurrentEntry(JSContext* aCx,
                          const NavigationUpdateCurrentEntryOptions& aOptions,
                          ErrorResult& aRv);
  NavigationTransition* GetTransition() const;
  NavigationActivation* GetActivation() const;

  bool CanGoBack() {
    return !HasEntriesAndEventsDisabled() && mCurrentEntryIndex &&
           *mCurrentEntryIndex != 0;
  }
  bool CanGoForward() {
    return !HasEntriesAndEventsDisabled() && mCurrentEntryIndex &&
           *mCurrentEntryIndex != mEntries.Length() - 1;
  }

  void Navigate(JSContext* aCx, const nsAString& aUrl,
                const NavigationNavigateOptions& aOptions,
                NavigationResult& aResult);

  MOZ_CAN_RUN_SCRIPT void Reload(JSContext* aCx,
                                 const NavigationReloadOptions& aOptions,
                                 NavigationResult& aResult);

  void TraverseTo(JSContext* aCx, const nsAString& aKey,
                  const NavigationOptions& aOptions, NavigationResult& aResult);
  void Back(JSContext* aCx, const NavigationOptions& aOptions,
            NavigationResult& aResult);
  void Forward(JSContext* aCx, const NavigationOptions& aOptions,
               NavigationResult& aResult);

  IMPL_EVENT_HANDLER(navigate);
  IMPL_EVENT_HANDLER(navigatesuccess);
  IMPL_EVENT_HANDLER(navigateerror);
  IMPL_EVENT_HANDLER(currententrychange);

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#initialize-the-navigation-api-entries-for-a-new-document
  void InitializeHistoryEntries(
      mozilla::Span<const SessionHistoryInfo> aNewSHInfos,
      const SessionHistoryInfo* aInitialSHInfo);

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-reactivation
  MOZ_CAN_RUN_SCRIPT
  void UpdateForReactivation(Span<const SessionHistoryInfo> aNewSHEs,
                             const SessionHistoryInfo* aReactivatedEntry);

  MOZ_CAN_RUN_SCRIPT
  void UpdateEntriesForSameDocumentNavigation(
      SessionHistoryInfo* aDestinationSHE, NavigationType aNavigationType,
      bool aFiredNavigateEvent = true);

  MOZ_CAN_RUN_SCRIPT void TruncateForwardEntries(uint32_t aNewLength);

  MOZ_CAN_RUN_SCRIPT
  void RunNavigateEventHandlerSteps(
      NavigateEvent* aNavigateEvent,
      NavigationAPIMethodTracker* aAPIMethodTracker);

  JSObject* WrapObject(JSContext* aCx,
                       JS::Handle<JSObject*> aGivenProto) override;

  // The Navigation API is only enabled if the dom.navigation.webidl.enabled
  // pref is set.
  static bool IsAPIEnabled(JSContext* /* unused */ = nullptr,
                           JSObject* /* unused */ = nullptr);

  enum class FinalStatus : uint8_t { Continue, Intercept, Prevent };

  // Wrapper algorithms for firing the navigate event.
  // https://html.spec.whatwg.org/#navigate-event-firing
  MOZ_CAN_RUN_SCRIPT bool FireTraverseNavigateEvent(
      JSContext* aCx, nsDocShellLoadState* aLoadState,
      Maybe<UserNavigationInvolvement> aUserInvolvement);

  MOZ_CAN_RUN_SCRIPT bool FirePushReplaceReloadNavigateEvent(
      JSContext* aCx, NavigationType aNavigationType, nsIURI* aDestinationURL,
      bool aIsSameDocument, Maybe<UserNavigationInvolvement> aUserInvolvement,
      Element* aSourceElement, FormData* aFormDataEntryList,
      nsIStructuredCloneContainer* aNavigationAPIState,
      nsIStructuredCloneContainer* aClassicHistoryAPIState,
      NavigationAPIMethodTracker* aApiMethodTrackerForNavigateOrReload =
          nullptr);

  MOZ_CAN_RUN_SCRIPT bool FireDownloadRequestNavigateEvent(
      JSContext* aCx, nsIURI* aDestinationURL,
      UserNavigationInvolvement aUserInvolvement, Element* aSourceElement,
      const nsAString& aFilename);

  bool FocusedChangedDuringOngoingNavigation() const;
  void SetFocusedChangedDuringOngoingNavigation(
      bool aFocusChangedDuringOngoingNavigation);

  bool HasOngoingNavigateEvent() const;

  MOZ_CAN_RUN_SCRIPT
  void InnerInformAboutAbortingNavigation(JSContext* aCx);

  MOZ_CAN_RUN_SCRIPT
  void AbortOngoingNavigation(
      JSContext* aCx, JS::Handle<JS::Value> aError = JS::UndefinedHandleValue);

  MOZ_CAN_RUN_SCRIPT
  void AbortNavigateEvent(JSContext* aCx, const NavigateEvent* aEvent,
                          JS::Handle<JS::Value> aReason);

  MOZ_CAN_RUN_SCRIPT
  void InformAboutChildNavigableDestruction(JSContext* aCx);

  void CreateNavigationActivationFrom(
      const Maybe<PreviousSessionHistoryInfo>& aPreviousEntryForActivation,
      Maybe<NavigationType> aNavigationType);

  void SetSerializedStateIntoOngoingAPIMethodTracker(
      nsIStructuredCloneContainer* aSerializedState);

 private:
  friend struct NavigationAPIMethodTracker;
  friend struct NavigationWaitForAllScope;
  using UpcomingTraverseAPIMethodTrackers =
      nsTHashMap<nsIDHashKey, RefPtr<NavigationAPIMethodTracker>>;

  ~Navigation() = default;

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#has-entries-and-events-disabled
  bool HasEntriesAndEventsDisabled() const;

  MOZ_CAN_RUN_SCRIPT
  nsresult FireEvent(const nsAString& aName);

  MOZ_CAN_RUN_SCRIPT
  nsresult FireErrorEvent(const nsAString& aName,
                          const ErrorEventInit& aEventInitDict);

  // https://html.spec.whatwg.org/#inner-navigate-event-firing-algorithm
  MOZ_CAN_RUN_SCRIPT bool InnerFireNavigateEvent(
      JSContext* aCx, NavigationType aNavigationType,
      NavigationDestination* aDestination,
      UserNavigationInvolvement aUserInvolvement, Element* aSourceElement,
      FormData* aFormDataEntryList,
      nsIStructuredCloneContainer* aClassicHistoryAPIState,
      const nsAString& aDownloadRequestFilename,
      NavigationAPIMethodTracker* aNavigationAPIMethodTracker = nullptr,
      nsDocShellLoadState* aLoadState = nullptr);

  NavigationHistoryEntry* FindNavigationHistoryEntry(
      const SessionHistoryInfo& aSessionHistoryInfo) const;

  Maybe<size_t> GetNavigationEntryIndex(
      const SessionHistoryInfo& aSessionHistoryInfo) const;

  RefPtr<NavigationAPIMethodTracker> SetUpNavigateReloadAPIMethodTracker(
      JS::Handle<JS::Value> aInfo,
      nsIStructuredCloneContainer* aSerializedState);

  RefPtr<NavigationAPIMethodTracker> AddUpcomingTraverseAPIMethodTracker(
      const nsID& aKey, JS::Handle<JS::Value> aInfo);

  void SetEarlyErrorResult(JSContext* aCx, NavigationResult& aResult,
                           ErrorResult&& aRv) const;

  void SetEarlyStateErrorResult(JSContext* aCx, NavigationResult& aResult,
                                const nsACString& aMessage) const;

  bool CheckIfDocumentIsFullyActiveAndMaybeSetEarlyErrorResult(
      JSContext* aCx, const Document* aDocument,
      NavigationResult& aResult) const;

  bool CheckDocumentUnloadCounterAndMaybeSetEarlyErrorResult(
      JSContext* aCx, const Document* aDocument,
      NavigationResult& aResult) const;

  already_AddRefed<nsIStructuredCloneContainer>
  CreateSerializedStateAndMaybeSetEarlyErrorResult(
      JSContext* aCx, const JS::Value& aState, NavigationResult& aResult) const;

  static void CleanUp(NavigationAPIMethodTracker* aNavigationAPIMethodTracker);

  void SetCurrentEntryIndex(const SessionHistoryInfo* aTargetInfo);

  Document* GetAssociatedDocument() const;

  // Update the state managing if we need to dispatch the traverse event or not.
  void UpdateNeedsTraverse();

  void LogHistory() const;

  void PerformNavigationTraversal(JSContext* aCx, const nsID& aKey,
                                  const NavigationOptions& aOptions,
                                  NavigationResult& aResult);

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-entry-list
  nsTArray<RefPtr<NavigationHistoryEntry>> mEntries;

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-current-entry
  Maybe<uint64_t> mCurrentEntryIndex;

  // https://html.spec.whatwg.org/#ongoing-navigation-tracking:navigateevent-2
  RefPtr<NavigateEvent> mOngoingNavigateEvent;

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#focus-changed-during-ongoing-navigation
  bool mFocusChangedDuringOngoingNavigation = false;

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#suppress-normal-scroll-restoration-during-ongoing-navigation
  bool mSuppressNormalScrollRestorationDuringOngoingNavigation = false;

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#ongoing-api-method-tracker
  RefPtr<NavigationAPIMethodTracker> mOngoingAPIMethodTracker;

  // https://html.spec.whatwg.org/multipage/nav-history-apis.html#upcoming-traverse-api-method-trackers
  UpcomingTraverseAPIMethodTrackers mUpcomingTraverseAPIMethodTrackers;

  // https://html.spec.whatwg.org/#concept-navigation-transition
  RefPtr<NavigationTransition> mTransition;

  // https://html.spec.whatwg.org/#navigation-activation
  RefPtr<NavigationActivation> mActivation;
};

inline Navigation* EventTarget::GetAsNavigation() {
  return IsNavigation() ? AsNavigation() : nullptr;
}
inline const Navigation* EventTarget::GetAsNavigation() const {
  return IsNavigation() ? AsNavigation() : nullptr;
}
inline Navigation* EventTarget::AsNavigation() {
  MOZ_DIAGNOSTIC_ASSERT(IsNavigation());
  return static_cast<Navigation*>(this);
}
inline const Navigation* EventTarget::AsNavigation() const {
  MOZ_DIAGNOSTIC_ASSERT(IsNavigation());
  return static_cast<const Navigation*>(this);
}

}  // namespace mozilla::dom

template <>
struct fmt::formatter<mozilla::dom::NavigationType, char>
    : public formatter<nsLiteralCString> {
  template <typename FmtContext>
  constexpr auto format(const mozilla::dom::NavigationType& aNavigationType,
                        FmtContext& aCtx) const {
    return formatter<nsLiteralCString>::format(
        mozilla::dom::GetEnumString(aNavigationType), aCtx);
  }
};

template <>
struct fmt::formatter<mozilla::dom::NavigationHistoryBehavior, char>
    : public formatter<nsLiteralCString> {
  template <typename FmtContext>
  constexpr auto format(
      const mozilla::dom::NavigationHistoryBehavior& aNavigationHistoryBehavior,
      FmtContext& aCtx) const {
    return formatter<nsLiteralCString>::format(
        mozilla::dom::GetEnumString(aNavigationHistoryBehavior), aCtx);
  }
};

#endif  // mozilla_dom_Navigation_h_
