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

#include "nsPoint.h"
#include "mozilla/Saturate.h"
#include "mozilla/TimeStamp.h"

class nsFrameList;
class nsHTMLScrollFrame;
class nsIFrame;
class nsIScrollableFrame;

namespace mozilla::layout {

/**
 * A scroll anchor container finds a descendent element of a scrollable frame
 * to be an anchor node. After every reflow, the scroll anchor will apply
 * scroll adjustments to keep the anchor node in the same relative position.
 *
 * See: https://drafts.csswg.org/css-scroll-anchoring/
 */
class ScrollAnchorContainer final {
 public:
  explicit ScrollAnchorContainer(nsHTMLScrollFrame* aScrollFrame);
  ~ScrollAnchorContainer();

  /**
   * Returns the nearest scroll anchor container that could select aFrame as an
   * anchor node.
   */
  static ScrollAnchorContainer* FindFor(nsIFrame* aFrame);

  /**
   * Returns the frame that is the selected anchor node or null if no anchor
   * is selected.
   */
  nsIFrame* AnchorNode() const { return mAnchorNode; }

  // The owner of this scroll anchor container.
  nsHTMLScrollFrame* Frame() const;

  /**
   * Returns the frame that owns this scroll anchor container as a scrollable
   * frame. This is always non-null.
   */
  nsIScrollableFrame* ScrollableFrame() const;

  /**
   * Find a suitable anchor node among the descendants of the scrollable frame.
   * This should only be called after the scroll anchor has been invalidated.
   */
  void SelectAnchor();

  /**
   * Whether this scroll frame can maintain an anchor node at the moment.
   */
  bool CanMaintainAnchor() const;

  /**
   * Notify the scroll anchor container that its scroll frame has been
   * scrolled by a user and should invalidate itself.
   */
  void UserScrolled();

  /**
   * Notify the scroll anchor container that a reflow has happened and it
   * should query its anchor to see if a scroll adjustment needs to occur.
   */
  void ApplyAdjustments();

  /**
   * Notify the scroll anchor container that it should suppress any scroll
   * adjustment that may happen after the next layout flush.
   */
  void SuppressAdjustments();

  /**
   * Notify this scroll anchor container that its anchor node should be
   * invalidated, and recomputed at the next available opportunity if
   * ScheduleSelection is Yes.
   */
  enum class ScheduleSelection { No, Yes };
  void InvalidateAnchor(ScheduleSelection = ScheduleSelection::Yes);

  /**
   * Notify this scroll anchor container that it will be destroyed along with
   * its parent frame.
   */
  void Destroy();

 private:
  // Represents an assessment of a frame's suitability as a scroll anchor,
  // from the scroll-anchoring spec's "candidate examination algorithm":
  // https://drafts.csswg.org/css-scroll-anchoring-1/#candidate-examination
  enum class ExamineResult {
    // The frame is an excluded subtree or fully clipped and should be ignored.
    // This corresponds with step 1 in the algorithm.
    Exclude,
    // This frame is an anonymous or inline box and its descendants should be
    // searched to find an anchor node. If none are found, then continue
    // searching. This is implied by the prologue of the algorithm, and
    // should be made explicit in the spec [1].
    //
    // [1] https://github.com/w3c/csswg-drafts/issues/3489
    PassThrough,
    // The frame is partially visible and its descendants should be searched to
    // find an anchor node. If none are found then this frame should be
    // selected. This corresponds with step 3 in the algorithm.
    Traverse,
    // The frame is fully visible and should be selected as an anchor node. This
    // corresponds with step 2 in the algorithm.
    Accept,
  };

  ExamineResult ExamineAnchorCandidate(nsIFrame* aPrimaryFrame) const;

  // Search a frame's children to find an anchor node. Returns the frame for a
  // valid anchor node, if one was found in the frames descendants, or null
  // otherwise.
  nsIFrame* FindAnchorIn(nsIFrame* aFrame) const;

  // Search a child list to find an anchor node. Returns the frame for a valid
  // anchor node, if one was found in this child list, or null otherwise.
  nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const;

  // Notes that a given adjustment has happened, and maybe disables scroll
  // anchoring on this scroller altogether based on various prefs.
  void AdjustmentMade(nscoord aAdjustment);

  // The anchor node that we will scroll to keep in the same relative position
  // after reflows. This may be null if we were not able to select a valid
  // scroll anchor
  nsIFrame* mAnchorNode = nullptr;

  // The last offset of the scroll anchor node's scrollable overflow rect start
  // edge relative to the scroll-port start edge, in the block axis of the
  // scroll frame. This is used for calculating the distance to scroll to keep
  // the anchor node in the same relative position
  nscoord mLastAnchorOffset = 0;

  struct DisablingHeuristic {
    // The number of consecutive scroll anchoring adjustments that have happened
    // without a user scroll.
    SaturateUint32 mConsecutiveScrollAnchoringAdjustments{0};

    // The total length that has been adjusted by all the consecutive
    // adjustments referenced above. Note that this is a sum, so that
    // oscillating adjustments average towards zero.
    nscoord mConsecutiveScrollAnchoringAdjustmentLength{0};

    // The time we started checking for adjustments.
    TimeStamp mTimeStamp;

    // Returns whether anchoring should get disabled.
    bool AdjustmentMade(const ScrollAnchorContainer&, nscoord aAdjustment);
    void Reset();
  } mHeuristic;

  // True if we've been disabled by the heuristic controlled by
  // layout.css.scroll-anchoring.max-consecutive-adjustments and
  // layout.css.scroll-anchoring.min-adjustment-threshold.
  bool mDisabled : 1;

  // True if when we selected the current scroll anchor, there were unlaid out
  // children that could be better anchor nodes after layout.
  bool mAnchorMightBeSubOptimal : 1;
  // True if we should recalculate our anchor node at the next chance
  bool mAnchorNodeIsDirty : 1;
  // True if we are applying a scroll anchor adjustment
  bool mApplyingAnchorAdjustment : 1;
  // True if we should suppress anchor adjustments
  bool mSuppressAnchorAdjustment : 1;
};

}  // namespace mozilla::layout

#endif  // mozilla_layout_ScrollAnchorContainer_h_
