// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/accessibility/ax_selection.h"

#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/focus_params.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/set_selection_options.h"
#include "third_party/blink/renderer/core/editing/text_affinity.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object-inl.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"

namespace blink {

namespace {

// TODO(nektar): Add Web tests for this event.
void ScheduleSelectEvent(TextControlElement& text_control) {
  Event* event = Event::CreateBubble(event_type_names::kSelect);
  event->SetTarget(&text_control);
  text_control.GetDocument().EnqueueAnimationFrameEvent(event);
}

// TODO(nektar): Add Web tests for this event.
DispatchEventResult DispatchSelectStart(Node* node) {
  if (!node)
    return DispatchEventResult::kNotCanceled;

  return node->DispatchEvent(
      *Event::CreateCancelableBubble(event_type_names::kSelectstart));
}

}  // namespace

//
// AXSelection::Builder
//

AXSelection::Builder& AXSelection::Builder::SetAnchor(
    const AXPosition& anchor) {
  DCHECK(anchor.IsValid());
  selection_.anchor_ = anchor;
  return *this;
}

AXSelection::Builder& AXSelection::Builder::SetAnchor(const Position& anchor) {
  const auto ax_anchor = AXPosition::FromPosition(anchor, ax_object_cache_);
  DCHECK(ax_anchor.IsValid());
  selection_.anchor_ = ax_anchor;
  return *this;
}

AXSelection::Builder& AXSelection::Builder::SetFocus(const AXPosition& focus) {
  DCHECK(focus.IsValid());
  selection_.focus_ = focus;
  return *this;
}

AXSelection::Builder& AXSelection::Builder::SetFocus(const Position& focus) {
  const auto ax_focus = AXPosition::FromPosition(focus, ax_object_cache_);
  DCHECK(ax_focus.IsValid());
  selection_.focus_ = ax_focus;
  return *this;
}

AXSelection::Builder& AXSelection::Builder::SetSelection(
    const SelectionInDomTree& selection) {
  if (selection.IsNone())
    return *this;

  selection_.anchor_ =
      AXPosition::FromPosition(selection.Anchor(), ax_object_cache_);
  selection_.focus_ =
      AXPosition::FromPosition(selection.Focus(), ax_object_cache_);
  return *this;
}

const AXSelection AXSelection::Builder::Build() {
  if (!selection_.Anchor().IsValid() || !selection_.Focus().IsValid()) {
    return {};
  }

  const Document* document =
      selection_.Anchor().ContainerObject()->GetDocument();
  DCHECK(document);
  DCHECK(document->IsActive());
  DCHECK(!document->NeedsLayoutTreeUpdate());
  // We don't support selections that span across documents.
  if (selection_.Focus().ContainerObject()->GetDocument() != document) {
    return {};
  }

#if DCHECK_IS_ON()
  selection_.dom_tree_version_ = document->DomTreeVersion();
  selection_.style_version_ = document->StyleVersion();
#endif
  return selection_;
}

//
// AXSelection
//

// static
void AXSelection::ClearCurrentSelection(Document& document) {
  LocalFrame* frame = document.GetFrame();
  if (!frame)
    return;

  FrameSelection& frame_selection = frame->Selection();
  if (!frame_selection.IsAvailable())
    return;

  frame_selection.Clear();
}

// static
AXSelection AXSelection::FromCurrentSelection(
    const Document& document,
    const AXObjectCacheImpl& ax_object_cache,
    const AXSelectionBehavior selection_behavior) {
  const LocalFrame* frame = document.GetFrame();
  if (!frame)
    return {};

  const FrameSelection& frame_selection = frame->Selection();
  if (!frame_selection.IsAvailable())
    return {};

  return FromSelection(frame_selection.GetSelectionInDomTree(), ax_object_cache,
                       selection_behavior);
}

// static
AXSelection AXSelection::FromCurrentSelection(
    const TextControlElement& text_control,
    const AXObjectCacheImpl& ax_object_cache) {
  const AXObject* ax_text_control = ax_object_cache.Get(&text_control);
  DCHECK(ax_text_control);

  // If the selection offsets are out of sync with the text control's value
  // length in the accessibility tree (which can happen if the shadow DOM is
  // mutated directly), return an invalid selection to avoid crashing
  // downstream.
  unsigned value_length = ax_text_control->GetValueForControl().length();
  if (text_control.selectionStart() > value_length ||
      text_control.selectionEnd() > value_length) {
    return {};
  }

  // We can't directly use "text_control.Selection()" because the selection it
  // returns is inside the shadow DOM and it's not anchored to the text field
  // itself.
  const TextAffinity focus_affinity = text_control.Selection().Affinity();
  const TextAffinity anchor_affinity =
      text_control.selectionStart() == text_control.selectionEnd()
          ? focus_affinity
          : TextAffinity::kDownstream;

  const bool is_backward = (text_control.selectionDirection() == "backward");
  const auto ax_anchor = AXPosition::CreatePositionInTextObject(
      *ax_text_control,
      static_cast<int>(is_backward ? text_control.selectionEnd()
                                   : text_control.selectionStart()),
      anchor_affinity);
  const auto ax_focus = AXPosition::CreatePositionInTextObject(
      *ax_text_control,
      static_cast<int>(is_backward ? text_control.selectionStart()
                                   : text_control.selectionEnd()),
      focus_affinity);

  if (!ax_anchor.IsValid() || !ax_focus.IsValid()) {
    return {};
  }

  AXSelection::Builder selection_builder(ax_object_cache);
  selection_builder.SetAnchor(ax_anchor).SetFocus(ax_focus);
  return selection_builder.Build();
}

// static
AXSelection AXSelection::FromSelection(
    const SelectionInDomTree& selection,
    const AXObjectCacheImpl& ax_object_cache,
    const AXSelectionBehavior selection_behavior) {
  if (selection.IsNone())
    return {};
  DCHECK(selection.AssertValid());

  const Position dom_anchor = selection.Anchor();
  const Position dom_focus = selection.Focus();
  const TextAffinity focus_affinity = selection.Affinity();
  const TextAffinity anchor_affinity =
      selection.IsCaret() ? focus_affinity : TextAffinity::kDownstream;

  AXPositionAdjustmentBehavior anchor_adjustment =
      AXPositionAdjustmentBehavior::kMoveRight;
  AXPositionAdjustmentBehavior focus_adjustment =
      AXPositionAdjustmentBehavior::kMoveRight;
  // If the selection is not collapsed, extend or shrink the DOM selection if
  // there is no equivalent selection in the accessibility tree, i.e. if the
  // corresponding endpoints are either ignored or unavailable in the
  // accessibility tree. If the selection is collapsed, move both endpoints to
  // the next valid position in the accessibility tree but do not extend or
  // shrink the selection, because this will result in a non-collapsed selection
  // in the accessibility tree.
  if (!selection.IsCaret()) {
    switch (selection_behavior) {
      case AXSelectionBehavior::kShrinkToValidRange:
        if (selection.IsAnchorFirst()) {
          anchor_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
          focus_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
        } else {
          anchor_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
          focus_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
        }
        break;
      case AXSelectionBehavior::kExtendToValidRange:
        if (selection.IsAnchorFirst()) {
          anchor_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
          focus_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
        } else {
          anchor_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
          focus_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
        }
        break;
    }
  }

  const auto ax_anchor = AXPosition::FromPosition(
      dom_anchor, ax_object_cache, anchor_affinity, anchor_adjustment);
  const auto ax_focus = AXPosition::FromPosition(
      dom_focus, ax_object_cache, focus_affinity, focus_adjustment);

  if (!ax_anchor.IsValid() || !ax_focus.IsValid()) {
    return {};
  }

  AXSelection::Builder selection_builder(ax_object_cache);
  selection_builder.SetAnchor(ax_anchor).SetFocus(ax_focus);
  return selection_builder.Build();
}

AXSelection::AXSelection() : anchor_(), focus_() {
#if DCHECK_IS_ON()
  dom_tree_version_ = 0;
  style_version_ = 0;
#endif
}

bool AXSelection::IsValid() const {
  if (!anchor_.IsValid() || !focus_.IsValid()) {
    return false;
  }

  // We don't support selections that span across documents.
  if (anchor_.ContainerObject()->GetDocument() !=
      focus_.ContainerObject()->GetDocument()) {
    return false;
  }

  //
  // The following code checks if a text position in a text control is valid.
  // Since the contents of a text control are implemented using user agent
  // shadow DOM, we want to prevent users from selecting across the shadow DOM
  // boundary.
  //
  // TODO(nektar): Generalize this logic to adjust user selection if it crosses
  // disallowed shadow DOM boundaries such as user agent shadow DOM, editing
  // boundaries, replaced elements, CSS user-select, etc.
  //

  if (anchor_.IsTextPosition() &&
      anchor_.ContainerObject()->IsAtomicTextField() &&
      !(anchor_.ContainerObject() == focus_.ContainerObject() &&
        focus_.IsTextPosition() &&
        focus_.ContainerObject()->IsAtomicTextField())) {
    return false;
  }

  if (focus_.IsTextPosition() &&
      focus_.ContainerObject()->IsAtomicTextField() &&
      !(anchor_.ContainerObject() == focus_.ContainerObject() &&
        anchor_.IsTextPosition() &&
        anchor_.ContainerObject()->IsAtomicTextField())) {
    return false;
  }

  DCHECK(!anchor_.ContainerObject()->GetDocument()->NeedsLayoutTreeUpdate());
#if DCHECK_IS_ON()
  DCHECK_EQ(anchor_.ContainerObject()->GetDocument()->DomTreeVersion(),
            dom_tree_version_);
  DCHECK_EQ(anchor_.ContainerObject()->GetDocument()->StyleVersion(),
            style_version_);
#endif  // DCHECK_IS_ON()
  return true;
}

const SelectionInDomTree AXSelection::AsSelection(
    const AXSelectionBehavior selection_behavior) const {
  if (!IsValid())
    return {};

  AXPositionAdjustmentBehavior anchor_adjustment =
      AXPositionAdjustmentBehavior::kMoveLeft;
  AXPositionAdjustmentBehavior focus_adjustment =
      AXPositionAdjustmentBehavior::kMoveLeft;
  switch (selection_behavior) {
    case AXSelectionBehavior::kShrinkToValidRange:
      if (anchor_ < focus_) {
        anchor_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
        focus_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
      } else if (anchor_ > focus_) {
        anchor_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
        focus_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
      }
      break;
    case AXSelectionBehavior::kExtendToValidRange:
      if (anchor_ < focus_) {
        anchor_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
        focus_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
      } else if (anchor_ > focus_) {
        anchor_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
        focus_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
      }
      break;
  }

  const auto dom_anchor = anchor_.ToPositionWithAffinity(anchor_adjustment);
  const auto dom_focus = focus_.ToPositionWithAffinity(focus_adjustment);
  SelectionInDomTree::Builder selection_builder;
  selection_builder.SetBaseAndExtent(dom_anchor.GetPosition(),
                                     dom_focus.GetPosition());
  if (focus_.IsTextPosition()) {
    selection_builder.SetAffinity(focus_.Affinity());
  }
  return selection_builder.Build();
}

void AXSelection::UpdateSelectionIfNecessary() {
  Document* document = anchor_.ContainerObject()->GetDocument();
  if (!document)
    return;

  LocalFrameView* view = document->View();
  if (!view || !view->LayoutPending())
    return;

  document->UpdateStyleAndLayout(DocumentUpdateReason::kSelection);
#if DCHECK_IS_ON()
  anchor_.dom_tree_version_ = focus_.dom_tree_version_ = dom_tree_version_ =
      document->DomTreeVersion();
  anchor_.style_version_ = focus_.style_version_ = style_version_ =
      document->StyleVersion();
#endif  // DCHECK_IS_ON()
}

bool AXSelection::Select(const AXSelectionBehavior selection_behavior) {
  if (!IsValid()) {
    // By the time the selection action gets here, content could have
    // changed from the content the action was initially prepared for.
    return false;
  }

  std::optional<AXSelection::TextControlSelection> text_control_selection =
      AsTextControlSelection();

  // We need to make sure we only go into here if we're dealing with a position
  // in the atomic text field. This is because the offsets are being assumed
  // to be on the atomic text field, and not on the descendant inline text
  // boxes.
  if (text_control_selection.has_value() &&
      *anchor_.ContainerObject() ==
          *anchor_.ContainerObject()->GetAtomicTextFieldAncestor() &&
      *focus_.ContainerObject() ==
          *focus_.ContainerObject()->GetAtomicTextFieldAncestor()) {
    DCHECK_LE(text_control_selection->start, text_control_selection->end);
    TextControlElement& text_control = ToTextControl(
        *anchor_.ContainerObject()->GetAtomicTextFieldAncestor()->GetNode());
    if (!text_control.SetSelectionRange(text_control_selection->start,
                                        text_control_selection->end,
                                        text_control_selection->direction)) {
      return false;
    }

    // TextControl::SetSelectionRange deliberately does not set focus. But if
    // we're updating the selection, the text control should be focused.
    ScheduleSelectEvent(text_control);
    text_control.Focus(FocusParams(FocusTrigger::kUserGesture));
    return true;
  }

  const SelectionInDomTree old_selection = AsSelection(selection_behavior);
  DCHECK(old_selection.AssertValid());
  Document* document = old_selection.Anchor().GetDocument();
  if (!document) {
    // By the time the selection action gets here, content could have
    // changed from the content the action was initially prepared for.
    return false;
  }

  LocalFrame* frame = document->GetFrame();
  if (!frame) {
    NOTREACHED();
  }

  FrameSelection& frame_selection = frame->Selection();
  if (!frame_selection.IsAvailable())
    return false;

  // See the following section in the Selection API Specification:
  // https://w3c.github.io/selection-api/#selectstart-event
  if (DispatchSelectStart(old_selection.Anchor().ComputeContainerNode()) !=
      DispatchEventResult::kNotCanceled) {
    return false;
  }

  UpdateSelectionIfNecessary();
  if (!IsValid())
    return false;

  // Dispatching the "selectstart" event could potentially change the document
  // associated with the current frame.
  if (!frame_selection.IsAvailable())
    return false;

  // Re-retrieve the SelectionInDomTree in case a DOM mutation took place.
  // That way it will also have the updated DOM tree and Style versions,
  // and the SelectionTemplate checks for each won't fail.
  const SelectionInDomTree selection = AsSelection(selection_behavior);

  SetSelectionOptions::Builder options_builder;
  options_builder.SetIsDirectional(true)
      .SetShouldCloseTyping(true)
      .SetShouldClearTypingStyle(true)
      .SetSetSelectionBy(SetSelectionBy::kUser);
  frame_selection.SetSelectionForAccessibility(selection,
                                               options_builder.Build());
  return true;
}

String AXSelection::ToString() const {
  return StrCat({IsValid() ? "" : "Invalid ", "AXSelection from ",
                 Anchor().ToString(), " to ", Focus().ToString()});
}

std::optional<AXSelection::TextControlSelection>
AXSelection::AsTextControlSelection() const {
  if (!IsValid() || !anchor_.IsTextPosition() || !focus_.IsTextPosition() ||
      anchor_.ContainerObject() != focus_.ContainerObject()) {
    return {};
  }

  const AXObject* text_control =
      anchor_.ContainerObject()->GetAtomicTextFieldAncestor();
  if (!text_control)
    return {};

  DCHECK(IsTextControl(text_control->GetNode()));

  if (anchor_ <= focus_) {
    return TextControlSelection(anchor_.TextOffset(), focus_.TextOffset(),
                                kSelectionHasForwardDirection);
  }
  return TextControlSelection(focus_.TextOffset(), anchor_.TextOffset(),
                              kSelectionHasBackwardDirection);
}

bool operator==(const AXSelection& a, const AXSelection& b) {
  DCHECK(a.IsValid() && b.IsValid());
  return a.Anchor() == b.Anchor() && a.Focus() == b.Focus();
}

std::ostream& operator<<(std::ostream& ostream, const AXSelection& selection) {
  return ostream << selection.ToString().Utf8();
}

}  // namespace blink
