/* clang-format off */
/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* clang-format on */
/* 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/. */

#import "GeckoTextMarker.h"

#import "MacUtils.h"

#include "AccAttributes.h"
#include "DocAccessible.h"
#include "DocAccessibleParent.h"
#include "nsCocoaUtils.h"
#include "HyperTextAccessible.h"
#include "States.h"
#include "nsAccUtils.h"

namespace mozilla {
namespace a11y {

struct TextMarkerData {
  TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
      : mDoc(aDoc), mID(aID), mOffset(aOffset) {}
  TextMarkerData() {}
  uintptr_t mDoc;
  uintptr_t mID;
  int32_t mOffset;
};

// GeckoTextMarker

GeckoTextMarker::GeckoTextMarker(Accessible* aAcc, int32_t aOffset) {
  HyperTextAccessibleBase* ht = aAcc->AsHyperTextBase();
  if (ht && aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT &&
      aOffset <= static_cast<int32_t>(ht->CharacterCount())) {
    mPoint = aAcc->AsHyperTextBase()->ToTextLeafPoint(aOffset);
  } else {
    mPoint = TextLeafPoint(aAcc, aOffset);
  }
}

GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker(
    Accessible* aDoc, AXTextMarkerRef aTextMarker) {
  MOZ_ASSERT(aDoc);
  if (!aTextMarker) {
    return GeckoTextMarker();
  }

  if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) {
    MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef");
    return GeckoTextMarker();
  }

  TextMarkerData markerData;
  memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker),
         sizeof(TextMarkerData));

  if (!utils::DocumentExists(aDoc, markerData.mDoc)) {
    return GeckoTextMarker();
  }

  Accessible* doc = reinterpret_cast<Accessible*>(markerData.mDoc);
  MOZ_ASSERT(doc->IsDoc());
  int32_t offset = markerData.mOffset;
  Accessible* acc = nullptr;
  if (doc->IsRemote()) {
    acc = doc->AsRemote()->AsDoc()->GetAccessible(markerData.mID);
  } else {
    acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
        reinterpret_cast<void*>(markerData.mID));
  }

  if (!acc) {
    return GeckoTextMarker();
  }

  return GeckoTextMarker(acc, offset);
}

GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
                                                 int32_t aIndex) {
  TextLeafRange range(
      TextLeafPoint(aRoot, 0),
      TextLeafPoint(aRoot, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
  int32_t index = aIndex;
  // Iterate through all segments until we exhausted the index sum
  // so we can find the segment the index lives in.
  for (TextLeafRange segment : range) {
    if (segment.Start().mAcc->IsMenuPopup() &&
        (segment.Start().mAcc->State() & states::COLLAPSED)) {
      // XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
      // them.
      continue;
    }

    if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
      // XXX: MacOS expects bullets to be in the range's text, but not in
      // the calculated length!
      continue;
    }

    index -= segment.End().mOffset - segment.Start().mOffset;
    if (index <= 0) {
      // The index is in the current segment.
      return GeckoTextMarker(segment.Start().mAcc,
                             segment.End().mOffset + index);
    }
  }

  return GeckoTextMarker();
}

AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
  if (!IsValid()) {
    return nil;
  }

  Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc);
  TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(),
                            mPoint.mOffset);
  AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
      kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData),
      sizeof(TextMarkerData));

  return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
}

bool GeckoTextMarker::Next() {
  TextLeafPoint next =
      mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
                          TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);

  if (next && next != mPoint) {
    mPoint = next;
    return true;
  }

  return false;
}

bool GeckoTextMarker::Previous() {
  TextLeafPoint prev =
      mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
                          TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
  if (prev && mPoint != prev) {
    mPoint = prev;
    return true;
  }

  return false;
}

/**
 * Return true if the given point is inside editable content.
 */
static bool IsPointInEditable(const TextLeafPoint& aPoint) {
  if (aPoint.mAcc) {
    if (aPoint.mAcc->State() & states::EDITABLE) {
      return true;
    }

    Accessible* parent = aPoint.mAcc->Parent();
    if (parent && (parent->State() & states::EDITABLE)) {
      return true;
    }
  }

  return false;
}

GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const {
  bool includeCurrentInStart = !mPoint.IsParagraphStart(true);
  if (includeCurrentInStart) {
    TextLeafPoint prevChar =
        mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
    if (!prevChar.IsSpace()) {
      includeCurrentInStart = false;
    }
  }

  TextLeafPoint start = mPoint.FindBoundary(
      nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
      includeCurrentInStart
          ? (TextLeafPoint::BoundaryFlags::eIncludeOrigin |
             TextLeafPoint::BoundaryFlags::eStopInEditable |
             TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)
          : (TextLeafPoint::BoundaryFlags::eStopInEditable |
             TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker));

  TextLeafPoint end;
  if (start == mPoint) {
    end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious,
                             TextLeafPoint::BoundaryFlags::eStopInEditable);
  }

  if (start != mPoint || end == start) {
    end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
                             TextLeafPoint::BoundaryFlags::eStopInEditable);
    if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) {
      start = end;
      end = mPoint;
    }
  }

  return GeckoTextMarkerRange(start < end ? start : end,
                              start < end ? end : start);
}

GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const {
  TextLeafPoint prevChar =
      mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
                          TextLeafPoint::BoundaryFlags::eStopInEditable);

  if (prevChar != mPoint && mPoint.IsParagraphStart(true)) {
    return GeckoTextMarkerRange(mPoint, mPoint);
  }

  TextLeafPoint end =
      mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
                          TextLeafPoint::BoundaryFlags::eStopInEditable);

  if (end == mPoint) {
    // No word to the right of this point.
    return GeckoTextMarkerRange(mPoint, mPoint);
  }

  TextLeafPoint start =
      end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
                       TextLeafPoint::BoundaryFlags::eStopInEditable);

  if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
                         TextLeafPoint::BoundaryFlags::eStopInEditable) <
      mPoint) {
    // Word end is inside of an input to the left of this.
    return GeckoTextMarkerRange(mPoint, mPoint);
  }

  if (mPoint < start) {
    end = start;
    start = mPoint;
  }

  return GeckoTextMarkerRange(start < end ? start : end,
                              start < end ? end : start);
}

GeckoTextMarkerRange GeckoTextMarker::LineRange() const {
  TextLeafPoint start = mPoint.FindBoundary(
      nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
      TextLeafPoint::BoundaryFlags::eStopInEditable |
          TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker |
          TextLeafPoint::BoundaryFlags::eIncludeOrigin);
  // If this is a blank line containing only a line feed, the start boundary
  // is the same as the end boundary. We do not want to walk to the end of the
  // next line.
  TextLeafPoint end =
      start.IsLineFeedChar()
          ? start
          : start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
                               TextLeafPoint::BoundaryFlags::eStopInEditable);

  return GeckoTextMarkerRange(start, end);
}

GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const {
  TextLeafPoint start = mPoint.FindBoundary(
      nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
      TextLeafPoint::BoundaryFlags::eStopInEditable |
          TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
  TextLeafPoint end =
      start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
                         TextLeafPoint::BoundaryFlags::eStopInEditable);

  return GeckoTextMarkerRange(start, end);
}

GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const {
  TextLeafPoint end =
      mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
                          TextLeafPoint::BoundaryFlags::eStopInEditable);
  TextLeafPoint start =
      end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
                       TextLeafPoint::BoundaryFlags::eStopInEditable);

  return GeckoTextMarkerRange(start, end);
}

GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const {
  // XXX: WebKit gets trapped in inputs. Maybe we shouldn't?
  TextLeafPoint end =
      mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext,
                          TextLeafPoint::BoundaryFlags::eStopInEditable);
  TextLeafPoint start =
      end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious,
                       TextLeafPoint::BoundaryFlags::eStopInEditable);

  return GeckoTextMarkerRange(start, end);
}

GeckoTextMarkerRange GeckoTextMarker::StyleRange() const {
  if (mPoint.mOffset == 0) {
    // If the marker is on the boundary between two leafs, MacOS expects the
    // previous leaf.
    TextLeafPoint prev = mPoint.FindBoundary(
        nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
        TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
    if (prev != mPoint) {
      return GeckoTextMarker(prev).StyleRange();
    }
  }

  TextLeafPoint start(mPoint.mAcc, 0);
  TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc));
  return GeckoTextMarkerRange(start, end);
}

Accessible* GeckoTextMarker::Leaf() {
  MOZ_ASSERT(mPoint.mAcc);
  Accessible* acc = mPoint.mAcc;
  if (mPoint.mOffset == 0) {
    // If the marker is on the boundary between two leafs, MacOS expects the
    // previous leaf.
    TextLeafPoint prev = mPoint.FindBoundary(
        nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
        TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
    acc = prev.mAcc;
  }

  Accessible* parent = acc->Parent();
  return parent && nsAccUtils::MustPrune(parent) ? parent : acc;
}

// GeckoTextMarkerRange

GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
  mRange = TextLeafRange(
      TextLeafPoint(aAccessible, 0),
      TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
}

GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
    Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
  if (!aTextMarkerRange ||
      CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
    return GeckoTextMarkerRange();
  }

  AXTextMarkerRef start_marker(
      AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
  AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));

  GeckoTextMarker start =
      GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker);
  GeckoTextMarker end =
      GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker);

  CFRelease(start_marker);
  CFRelease(end_marker);

  return GeckoTextMarkerRange(start, end);
}

AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
  if (!IsValid()) {
    return nil;
  }

  GeckoTextMarker start = GeckoTextMarker(mRange.Start());
  GeckoTextMarker end = GeckoTextMarker(mRange.End());

  AXTextMarkerRangeRef cf_text_marker_range =
      AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(),
                              end.CreateAXTextMarker());

  return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
      cf_text_marker_range)autorelease];
}

NSString* GeckoTextMarkerRange::Text() const {
  if (mRange.Start() == mRange.End()) {
    return @"";
  }

  if ((mRange.Start().mAcc == mRange.End().mAcc) &&
      (mRange.Start().mAcc->ChildCount() == 0) &&
      (mRange.Start().mAcc->State() & states::EDITABLE)) {
    return @"";
  }

  nsAutoString text;
  TextLeafPoint prev = mRange.Start().FindBoundary(
      nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
  TextLeafRange range =
      prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
          ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
          : mRange;

  for (TextLeafRange segment : range) {
    TextLeafPoint start = segment.Start();
    if (start.mAcc->IsMenuPopup() &&
        (start.mAcc->State() & states::COLLAPSED)) {
      // XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
      // them.
      continue;
    }
    if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) {
      continue;
    }

    start.mAcc->AppendTextTo(text, start.mOffset,
                             segment.End().mOffset - start.mOffset);
  }

  return nsCocoaUtils::ToNSString(text);
}

static void AppendTextToAttributedString(
    NSMutableAttributedString* aAttributedString, Accessible* aAccessible,
    const nsString& aString, AccAttributes* aAttributes) {
  NSAttributedString* substr = [[[NSAttributedString alloc]
      initWithString:nsCocoaUtils::ToNSString(aString)
          attributes:utils::StringAttributesFromAccAttributes(
                         aAttributes, aAccessible)] autorelease];

  [aAttributedString appendAttributedString:substr];
}

static RefPtr<AccAttributes> GetTextAttributes(TextLeafPoint aPoint) {
  RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
  if (!attrs) {
    // If we can't fetch text attributes for the given point, return null.
    // We avoid creating a new AccAttributes here because our AttributedText()
    // code below relies on this null return value to indicate we're dealing
    // with a non-text control.
    return nullptr;
  }
  // Mac expects some object properties to be exposed as text attributes. We
  // add these here rather than in utils::StringAttributesFromAccAttributes so
  // we can use AccAttributes::Equal to determine whether we need to start a new
  // run, rather than needing additional special case comparisons.
  for (Accessible* ancestor = aPoint.mAcc->Parent();
       ancestor && !ancestor->IsDoc(); ancestor = ancestor->Parent()) {
    if (ancestor->Role() == roles::MARK) {
      attrs->SetAttribute(nsGkAtoms::mark, true);
    }
  }
  return attrs;
}

NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
  NSMutableAttributedString* str =
      [[[NSMutableAttributedString alloc] init] autorelease];

  if (mRange.Start() == mRange.End()) {
    return str;
  }

  if ((mRange.Start().mAcc == mRange.End().mAcc) &&
      (mRange.Start().mAcc->ChildCount() == 0) &&
      (mRange.Start().mAcc->IsTextField())) {
    return str;
  }

  TextLeafPoint prev = mRange.Start().FindBoundary(
      nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
  TextLeafRange range =
      prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
          ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
          : mRange;

  nsAutoString text;
  TextLeafPoint start = range.Start();
  const TextLeafPoint stop = range.End();
  RefPtr<AccAttributes> currentRun = GetTextAttributes(start);
  Accessible* runAcc = start.mAcc;
  do {
    TextLeafPoint attributesNext = start.FindTextAttrsStart(eDirNext, false);
    if (stop < attributesNext) {
      attributesNext = stop;
    }
    if (start.mAcc->IsMenuPopup() &&
        (start.mAcc->State() & states::COLLAPSED)) {
      // XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
      // them.
      start = attributesNext;
      continue;
    }
    RefPtr<AccAttributes> attributes = GetTextAttributes(start);
    if (!currentRun || !attributes || !attributes->Equal(currentRun)) {
      // If currentRun is null this is a non-text control and we will
      // append a run with no text or attributes, just an AXAttachment
      // referencing this accessible.
      AppendTextToAttributedString(str, runAcc, text, currentRun);
      text.Truncate();
      currentRun = attributes;
      runAcc = start.mAcc;
    }
    for (TextLeafRange segment : TextLeafRange(start, attributesNext)) {
      TextLeafPoint segStart = segment.Start();
      segStart.mAcc->AppendTextTo(text, segStart.mOffset,
                                  segment.End().mOffset - segStart.mOffset);
    }
    start = attributesNext;
  } while (start != stop);

  if (!text.IsEmpty()) {
    AppendTextToAttributedString(str, runAcc, text, currentRun);
  }

  return str;
}

int32_t GeckoTextMarkerRange::Length() const {
  int32_t length = 0;
  for (TextLeafRange segment : mRange) {
    if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
      // XXX: MacOS expects bullets to be in the range's text, but not in
      // the calculated length!
      continue;
    }
    length += segment.End().mOffset - segment.Start().mOffset;
  }

  return length;
}

NSValue* GeckoTextMarkerRange::Bounds() const {
  LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect();

  NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
  CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
  NSRect r =
      NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
                 [mainView frame].size.height -
                     static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
                 static_cast<CGFloat>(rect.width) / scaleFactor,
                 static_cast<CGFloat>(rect.height) / scaleFactor);

  return [NSValue valueWithRect:r];
}

void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); }

}  // namespace a11y
}  // namespace mozilla
