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

#include "MsaaAccessible.h"
#include "TextLeafRange.h"
#include "UiaTextRange.h"
#include "mozilla/a11y/States.h"

namespace mozilla::a11y {

// Helpers

static SAFEARRAY* TextLeafRangesToUiaRanges(
    const nsTArray<TextLeafRange>& aRanges) {
  // The documentation for GetSelection doesn't specify whether we should return
  // an empty array or null if there are no ranges to return. However,
  // GetVisibleRanges says that we should return an empty array, never null, so
  // that's what we do.
  // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextprovider-getvisibleranges
  SAFEARRAY* uiaRanges = SafeArrayCreateVector(VT_UNKNOWN, 0, aRanges.Length());
  LONG indices[1] = {0};
  for (const TextLeafRange& range : aRanges) {
    // SafeArrayPutElement calls AddRef on the element, so we use a raw
    // pointer here.
    UiaTextRange* uiaRange = new UiaTextRange(range);
    SafeArrayPutElement(uiaRanges, indices, uiaRange);
    ++indices[0];
  }
  return uiaRanges;
}

// IUnknown
IMPL_IUNKNOWN2(UiaText, ITextProvider2, ITextProvider)

// UiaText

UiaText::UiaText(MsaaAccessible* aMsaa) : mMsaa(aMsaa) {}

Accessible* UiaText::Acc() const { return mMsaa->Acc(); }

// ITextProvider methods

STDMETHODIMP
UiaText::GetSelection(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
  if (!aRetVal) {
    return E_INVALIDARG;
  }
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }
  AutoTArray<TextLeafRange, 1> ranges;
  TextLeafRange::GetSelection(acc, ranges);
  if (ranges.IsEmpty()) {
    // There is no selection. Check if there is a caret.
    if (TextLeafPoint caret = TextLeafPoint::GetCaret(acc)) {
      ranges.EmplaceBack(caret, caret);
    }
  }
  *aRetVal = TextLeafRangesToUiaRanges(ranges);
  return S_OK;
}

STDMETHODIMP
UiaText::GetVisibleRanges(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
  if (!aRetVal) {
    return E_INVALIDARG;
  }
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }
  TextLeafRange fullRange = TextLeafRange::FromAccessible(acc);
  // The most pragmatic way to determine visible text is to walk by line.
  // XXX TextLeafRange::VisibleLines doesn't correctly handle lines that are
  // scrolled out where the scroll container is a descendant of acc. See bug
  // 1945010.
  nsTArray<TextLeafRange> ranges = fullRange.VisibleLines(acc);
  *aRetVal = TextLeafRangesToUiaRanges(ranges);
  return S_OK;
}

STDMETHODIMP
UiaText::RangeFromChild(__RPC__in_opt IRawElementProviderSimple* aChildElement,
                        __RPC__deref_out_opt ITextRangeProvider** aRetVal) {
  if (!aChildElement || !aRetVal) {
    return E_INVALIDARG;
  }
  *aRetVal = nullptr;
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }
  Accessible* child = MsaaAccessible::GetAccessibleFrom(aChildElement);
  if (!child || !acc->IsAncestorOf(child)) {
    return E_INVALIDARG;
  }
  TextLeafRange range = TextLeafRange::FromAccessible(child);
  auto uiaRange = MakeRefPtr<UiaTextRange>(range);
  uiaRange.forget(aRetVal);
  return S_OK;
}

STDMETHODIMP
UiaText::RangeFromPoint(struct UiaPoint aPoint,
                        __RPC__deref_out_opt ITextRangeProvider** aRetVal) {
  if (!aRetVal) {
    return E_INVALIDARG;
  }
  *aRetVal = nullptr;
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }

  // Find the deepest accessible node at the given screen coordinates.
  Accessible* child = acc->ChildAtPoint(
      aPoint.x, aPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
  if (!child) {
    return E_INVALIDARG;
  }

  // Find the closest point within the entirety of the leaf where the screen
  // coordinates lie.
  TextLeafRange leafRange = TextLeafRange::FromAccessible(child);
  TextLeafPoint closestPoint =
      leafRange.TextLeafPointAtScreenPoint(aPoint.x, aPoint.y);
  TextLeafRange range{closestPoint, closestPoint};
  auto uiaRange = MakeRefPtr<UiaTextRange>(range);
  uiaRange.forget(aRetVal);
  return S_OK;
}

STDMETHODIMP
UiaText::get_DocumentRange(__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
  if (!aRetVal) {
    return E_INVALIDARG;
  }
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }
  // On the web, the "document range" could either span the entire document or
  // just a text input control, depending on the element on which the Text
  // pattern was queried. See:
  // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview#webpage-and-text-input-controls-in-edge
  TextLeafRange range = TextLeafRange::FromAccessible(acc);
  auto uiaRange = MakeRefPtr<UiaTextRange>(range);
  uiaRange.forget(aRetVal);
  return S_OK;
}

STDMETHODIMP
UiaText::get_SupportedTextSelection(
    __RPC__out enum SupportedTextSelection* aRetVal) {
  if (!aRetVal) {
    return E_INVALIDARG;
  }
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }
  if (!acc->IsHyperText()) {
    // Currently, the SELECTABLE_TEXT state is only exposed on HyperText
    // Accessibles.
    acc = acc->Parent();
  }
  if (acc && acc->State() & states::SELECTABLE_TEXT) {
    *aRetVal = SupportedTextSelection_Multiple;
  } else {
    *aRetVal = SupportedTextSelection_None;
  }
  return S_OK;
}

// ITextProvider2 methods

STDMETHODIMP
UiaText::RangeFromAnnotation(
    __RPC__in_opt IRawElementProviderSimple* aAnnotationElement,
    __RPC__deref_out_opt ITextRangeProvider** aRetVal) {
  return E_NOTIMPL;
}

STDMETHODIMP
UiaText::GetCaretRange(__RPC__out BOOL* aIsActive,
                       __RPC__deref_out_opt ITextRangeProvider** aRetVal) {
  if (!aRetVal) {
    return E_INVALIDARG;
  }
  *aRetVal = nullptr;
  if (!aIsActive) {
    return E_INVALIDARG;
  }
  *aIsActive = FALSE;
  Accessible* acc = Acc();
  if (!acc) {
    return CO_E_OBJNOTCONNECTED;
  }
  TextLeafPoint caret = TextLeafPoint::GetCaret(acc);
  if (!caret) {
    return S_OK;
  }
  *aIsActive = !!(acc->State() & states::FOCUSED);
  TextLeafRange range{caret, caret};
  auto uiaRange = MakeRefPtr<UiaTextRange>(range);
  uiaRange.forget(aRetVal);
  return S_OK;
}

}  // namespace mozilla::a11y
