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

#include <stdint.h>

#include "AccAttributes.h"
#include "mozilla/FunctionRef.h"
#include "nsDirection.h"
#include "nsIAccessibleText.h"

namespace mozilla {
namespace dom {
class AbstractRange;
class Document;
}  // namespace dom

namespace a11y {
class Accessible;
class LocalAccessible;

/**
 * Represents a point within accessible text.
 * This is stored as a leaf Accessible and an offset into that Accessible.
 * For an empty Accessible, the offset will always be 0.
 * This will eventually replace TextPoint. Unlike TextPoint, this does not
 * use HyperTextAccessible offsets.
 */
class TextLeafPoint final {
 public:
  TextLeafPoint(Accessible* aAcc, int32_t aOffset);

  /**
   * Constructs an invalid TextPoint (mAcc is null).
   * A TextLeafPoint in this state will evaluate to false.
   * mAcc can be set later. Alternatively, this can be used to indicate an error
   * (e.g. if a requested point couldn't be found).
   */
  TextLeafPoint() : mAcc(nullptr), mOffset(0) {}

  /**
   * Construct a TextLeafPoint representing the caret.
   */
  static TextLeafPoint GetCaret(Accessible* aAcc);

  Accessible* mAcc;
  int32_t mOffset;

  /**
   * True if this point is the insertion point at the end of a line. This is the
   * point where the caret is positioned when pressing the end key, for example.
   * On the very last line, mOffset will be equal to the length of the text.
   * However, where text wraps across lines, this line end insertion point
   * doesn't have its own offset, so mOffset will be the offset for the first
   * character on the next line. This is where this flag becomes important.
   * Otherwise, for example, commanding a screen reader to read the current line
   * would read the next line instead of the current line in this case.
   */
  bool mIsEndOfLineInsertionPoint = false;

  bool operator==(const TextLeafPoint& aPoint) const {
    return mAcc == aPoint.mAcc && mOffset == aPoint.mOffset;
  }

  bool operator!=(const TextLeafPoint& aPoint) const {
    return !(*this == aPoint);
  }

  bool operator<(const TextLeafPoint& aPoint) const;

  bool operator<=(const TextLeafPoint& aPoint) const;

  /**
   * A valid TextLeafPoint evaluates to true. An invalid TextLeafPoint
   * evaluates to false.
   */
  explicit operator bool() const { return !!mAcc; }

  enum class BoundaryFlags : uint32_t {
    eDefaultBoundaryFlags = 0,
    // Return point unchanged if it is at the given boundary type.
    eIncludeOrigin = 1 << 0,
    // If current point is in editable, return point within same editable.
    eStopInEditable = 1 << 1,
    // Skip over list items in searches and don't consider them line or
    // paragraph starts.
    eIgnoreListItemMarker = 1 << 2,
  };

  /**
   * Find a boundary (word start, line start, etc.) in a specific direction.
   * If no boundary is found, the start/end of the document is returned
   * (depending on the direction).
   */
  TextLeafPoint FindBoundary(
      AccessibleTextBoundary aBoundaryType, nsDirection aDirection,
      BoundaryFlags aFlags = BoundaryFlags::eDefaultBoundaryFlags) const;

  /**
   * These two functions find a line start boundary within the same
   * LocalAccessible as this. That is, they do not cross Accessibles. If no
   * boundary is found, an invalid TextLeafPoint is returned.
   * These are used by FindBoundary. Most callers will want FindBoundary
   * instead.
   */
  TextLeafPoint FindPrevLineStartSameLocalAcc(bool aIncludeOrigin) const;
  TextLeafPoint FindNextLineStartSameLocalAcc(bool aIncludeOrigin) const;

  /**
   * These two functions find a word start boundary within the same
   * Accessible as this. That is, they do not cross Accessibles. If no
   * boundary is found, an invalid TextLeafPoint is returned.
   * These are used by FindBoundary. Most callers will want FindBoundary
   * instead.
   */
  TextLeafPoint FindPrevWordStartSameAcc(bool aIncludeOrigin) const;
  TextLeafPoint FindNextWordStartSameAcc(bool aIncludeOrigin) const;

  /**
   * Get the text attributes at this point.
   * If aIncludeDefaults is true, default attributes on the HyperTextAccessible
   * will be included.
   */
  already_AddRefed<AccAttributes> GetTextAttributes(
      bool aIncludeDefaults = true) const;

  /**
   * Get Get the text attributes at this point in a LocalAccessible.
   * This is used by GetTextAttributes. Most callers will want GetTextAttributes
   * instead.
   */
  already_AddRefed<AccAttributes> GetTextAttributesLocalAcc(
      bool aIncludeDefaults = true) const;

  /**
   * Get all the attributes that apply to offset ranges in a given text leaf
   * LocalAccessible. This should only be used when pushing the cache. Most
   * callers will want FindTextAttrsStart instead.
   */
  static nsTArray<TextOffsetAttribute> GetTextOffsetAttributes(
      LocalAccessible* aAcc);

  /**
   * Queue a cache update for text offset attributes in a given DOM range.
   */
  static void UpdateCachedTextOffsetAttributes(
      dom::Document* aDocument, const dom::AbstractRange& aRange);

  /**
   * Find the start of a run of text attributes in a specific direction.
   * A text attributes run is a span of text where the attributes are the same.
   * If no boundary is found, the function will walk out of the container and
   * into the next/previous leaf (if it exists) until it finds a start point.
   * If aIncludeOrigin is true and this is at a boundary, this will be
   * returned unchanged.
   */
  TextLeafPoint FindTextAttrsStart(nsDirection aDirection,
                                   bool aIncludeOrigin = false) const;

  /**
   * Returns a rect (in dev pixels) describing position and size of
   * the character at mOffset in mAcc. This rect is screen-relative.
   */
  LayoutDeviceIntRect CharBounds() const;

  /**
   * Returns true if the given point (in screen coords) is contained
   * in the char bounds of the current TextLeafPoint. Returns false otherwise.
   * If the current point is an empty container, we use the acc's bounds instead
   * of char bounds.
   */
  bool ContainsPoint(int32_t aX, int32_t aY);

  bool IsLineFeedChar() const { return GetChar() == '\n'; }

  bool IsSpace() const;

  bool IsParagraphStart(bool aIgnoreListItemMarker = false) const {
    return mOffset == 0 &&
           FindParagraphSameAcc(eDirPrevious, true, aIgnoreListItemMarker);
  }

  /**
   * Translate given TextLeafPoint into a DOM point.
   */
  MOZ_CAN_RUN_SCRIPT std::pair<RefPtr<nsIContent>, uint32_t> ToDOMPoint(
      bool aIncludeGenerated = true) const;

 private:
  /**
   * If this is the insertion point at the end of a line, return an adjusted
   * point such that word and line boundaries can be calculated correctly.
   */
  TextLeafPoint AdjustEndOfLine() const;

  bool IsEmptyLastLine() const;

  bool IsDocEdge(nsDirection aDirection) const;

  bool IsLeafAfterListItemMarker() const;

  char16_t GetChar() const;

  TextLeafPoint FindLineStartSameRemoteAcc(nsDirection aDirection,
                                           bool aIncludeOrigin) const;

  /**
   * Helper which just calls the appropriate function based on whether mAcc
   *is local or remote.
   */
  TextLeafPoint FindLineStartSameAcc(nsDirection aDirection,
                                     bool aIncludeOrigin,
                                     bool aIgnoreListItemMarker = false) const;

  TextLeafPoint FindLineEnd(nsDirection aDirection, BoundaryFlags aFlags) const;
  TextLeafPoint FindWordEnd(nsDirection aDirection, BoundaryFlags aFlags) const;

  TextLeafPoint FindParagraphSameAcc(nsDirection aDirection,
                                     bool aIncludeOrigin,
                                     bool aIgnoreListItemMarker = false) const;

  TextLeafPoint FindClusterSameAcc(nsDirection aDirection,
                                   bool aIncludeOrigin) const;

  void AddTextOffsetAttributes(AccAttributes* aAttrs) const;

  /**
   * Find a text offset attribute boundary in the same Accessible. This function
   * searches for either start or end points, since either means a change in
   * text attributes. This only considers attributes such as spelling errors
   * which are mapped to DOM selections. Most callers will want
   * FindTextAttrsStart instead.
   */
  TextLeafPoint FindTextOffsetAttributeSameAcc(nsDirection aDirection,
                                               bool aIncludeOrigin) const;

  // Return the point immediately succeeding or preceding this leaf depending
  // on given direction.
  TextLeafPoint NeighborLeafPoint(nsDirection aDirection, bool aIsEditable,
                                  bool aIgnoreListItemMarker) const;

  /**
   * This function assumes mAcc is a LocalAccessible.
   * It iterates the continuations of mAcc's primary frame until it locates
   * the continuation containing mOffset (a rendered offset). It then uses
   * GetScreenRectInAppUnits to compute screen coords for the frame, resizing
   * such that the resulting rect contains only one character.
   */
  LayoutDeviceIntRect ComputeBoundsFromFrame() const;

  LayoutDeviceIntRect InsertionPointBounds() const;

  friend class TextLeafRange;
};

MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(TextLeafPoint::BoundaryFlags)

/**
 * Represents a range of accessible text.
 * This will eventually replace TextRange.
 */
class TextLeafRange final {
 public:
  TextLeafRange(const TextLeafPoint& aStart, const TextLeafPoint& aEnd)
      : mStart(aStart), mEnd(aEnd) {}
  explicit TextLeafRange(const TextLeafPoint& aStart)
      : mStart(aStart), mEnd(aStart) {}
  explicit TextLeafRange() = default;

  // Create a TextLeafRange spanning the entire leaf.
  static TextLeafRange FromAccessible(Accessible* aAcc) {
    return {{aAcc, 0}, {aAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT}};
  }

  /**
   * A valid TextLeafRange evaluates to true. An invalid TextLeafRange
   * evaluates to false.
   */
  explicit operator bool() const { return !!mStart && !!mEnd; }

  bool operator!=(const TextLeafRange& aOther) const {
    return mEnd != aOther.mEnd || mStart != aOther.mStart;
  }

  bool operator==(const TextLeafRange& aOther) const {
    return mEnd == aOther.mEnd && mStart == aOther.mStart;
  }

  TextLeafPoint Start() const { return mStart; }
  void SetStart(const TextLeafPoint& aStart) { mStart = aStart; }
  TextLeafPoint End() const { return mEnd; }
  void SetEnd(const TextLeafPoint& aEnd) { mEnd = aEnd; }

  bool Crop(Accessible* aContainer);

  /**
   * Returns a union rect (in dev pixels) of all character bounds in this range.
   * This rect is screen-relative and exclusive of mEnd.
   */
  LayoutDeviceIntRect Bounds() const;

  /**
   * Returns an array of bounding rectangles, one for each visible text line in
   * this range. These rectangles are screen-relative and exclusive of mEnd.
   */
  nsTArray<LayoutDeviceIntRect> LineRects() const;

  /**
   * Returns a TextLeafPoint corresponding to the point in the TextLeafRange
   * containing the given screen point. The function returns a TextLeafPoint
   * constructed from mStart if it does not find a containing character.
   */
  TextLeafPoint TextLeafPointAtScreenPoint(int32_t aX, int32_t aY) const;

  /**
   * Get the ranges of text that are selected within this Accessible. The caret
   * is not included as a collapsed range.
   */
  static void GetSelection(Accessible* aAcc, nsTArray<TextLeafRange>& aRanges);

  static const int32_t kCreateNewSelectionRange = -1;
  static const int32_t kRemoveAllExistingSelectedRanges = -2;

  /**
   * Set range as DOM selection.
   * aSelectionNum is the selection index to use. If aSelectionNum is
   * out of bounds for current selection ranges, or is kCreateNewSelectionRange,
   * a new selection range is created. If aSelectionNum is
   * kRemoveAllExistingSelectedRanges, this will be set as the only range in the
   * selection; i.e. all existing ranges (if any) will be removed from the
   * selection first.
   * If aSetFocus is true, the element containing the start point will be
   * focused if appropriate. If aSetFocus is false, the focused element will
   * be left as is.
   */
  MOZ_CAN_RUN_SCRIPT bool SetSelection(int32_t aSelectionNum,
                                       bool aSetFocus = true) const;

  MOZ_CAN_RUN_SCRIPT void ScrollIntoView(uint32_t aScrollType) const;

  /**
   * Returns sub-ranges for all the lines in this range visible within the given
   * container Accessible.
   */
  nsTArray<TextLeafRange> VisibleLines(Accessible* aContainer) const;

  void GetFlattenedText(nsAString& aText) const;

 private:
  TextLeafPoint mStart;
  TextLeafPoint mEnd;

  /**
   * Walk all of the lines within the TextLeafRange. This function invokes the
   * given callback with the sub-range for each line and the line's bounding
   * rectangle. The bounds are inclusive of all characters in each line, except
   * that the first and last lines might be partial if the range begins or ends
   * in the middle of a line. They are exclusive of mEnd, since range ends are
   * always exclusive, so including mEnd would include the bounds for 1
   * character past the end of the range. Each rectangle is screen-relative. If
   * this range is collapsed, the callback is called with the insertion point
   * bounds. The function returns true if it walks any lines, and false if it
   * could not walk any lines, which could happen if the start and end points
   * are improperly positioned.
   */
  using LineRectCallback =
      FunctionRef<void(TextLeafRange, LayoutDeviceIntRect)>;
  bool WalkLineRects(LineRectCallback aCallback) const;

 public:
  /**
   * A TextLeafRange iterator will iterate through single leaf segments of the
   * given range.
   */

  class Iterator {
   public:
    Iterator(Iterator&& aOther)
        : mRange(aOther.mRange),
          mSegmentStart(aOther.mSegmentStart),
          mSegmentEnd(aOther.mSegmentEnd) {}

    Iterator() = delete;
    Iterator(const Iterator&) = delete;
    Iterator& operator=(const Iterator&) = delete;
    Iterator& operator=(const Iterator&&) = delete;

    static Iterator BeginIterator(const TextLeafRange& aRange);

    static Iterator EndIterator(const TextLeafRange& aRange);

    Iterator& operator++();

    bool operator!=(const Iterator& aOther) const {
      return mRange != aOther.mRange || mSegmentStart != aOther.mSegmentStart ||
             mSegmentEnd != aOther.mSegmentEnd;
    }

    TextLeafRange operator*() {
      return TextLeafRange(mSegmentStart, mSegmentEnd);
    }

   private:
    explicit Iterator(const TextLeafRange& aRange) : mRange(aRange) {}

    const TextLeafRange& mRange;
    TextLeafPoint mSegmentStart;
    TextLeafPoint mSegmentEnd;
  };

  Iterator begin() const { return Iterator::BeginIterator(*this); }
  Iterator end() const { return Iterator::EndIterator(*this); }
};

}  // namespace a11y
}  // namespace mozilla

#endif
