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

#include "content/browser/renderer_host/render_widget_host_view_ios_uiview.h"

#include "base/apple/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "components/input/native_web_keyboard_event.h"
#include "components/input/web_input_event_builders_ios.h"
#include "components/strings/grit/components_strings.h"
#include "content/browser/renderer_host/ios_extended_text_input_traits.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/base/l10n/l10n_util_mac.h"

static void* kObservingContext = &kObservingContext;

namespace {
NSString* const kPreviousAccessoryImageName = @"chevron.up";
NSString* const kNextAccessoryImageName = @"chevron.down";
NSString* const kDoneAccessoryImageName = @"checkmark";
}  // namespace

#pragma mark - BETextPosition
@interface BETextPosition : UITextPosition {
  CGRect rect_;
}
- (instancetype)initWithRect:(CGRect)rect;

@end

@implementation BETextPosition
- (instancetype)initWithRect:(CGRect)rect {
  rect_ = rect;
  return [self init];
}
- (CGRect)rect {
  return rect_;
}
@end

#pragma mark - BETextRange
@interface BETextRange : UITextRange {
  CGRect start_;
  CGRect end_;
}
- (instancetype)initWithRegion:
    (const content::TextInputManager::SelectionRegion*)region;
@end

@implementation BETextRange

- (instancetype)initWithRegion:
    (const content::TextInputManager::SelectionRegion*)region {
  start_ = CGRectMake(region->anchor.edge_start_rounded().x(),
                      region->anchor.edge_start_rounded().y(), 1,
                      region->anchor.GetHeight());

  end_ = CGRectMake(region->focus.edge_start_rounded().x(),
                    region->focus.edge_start_rounded().y(), 1,
                    region->focus.GetHeight());
  return [self init];
}

- (BOOL)isEmpty {
  return CGRectEqualToRect(start_, end_);
}

- (UITextPosition*)start {
  return [[BETextPosition alloc] initWithRect:end_];
}
- (UITextPosition*)end {
  return [[BETextPosition alloc] initWithRect:end_];
}
@end

#pragma mark - BETextSelectionHandles
@interface BETextSelectionHandles : UITextSelectionRect
- (instancetype)initWithCGRect:(CGRect)rect atStart:(BOOL)start;
@end
@implementation BETextSelectionHandles {
  CGRect rect_;
  BOOL start_;
}
- (instancetype)initWithCGRect:(CGRect)rect atStart:(BOOL)start {
  rect_ = rect;
  start_ = start;
  return [self init];
}
- (NSWritingDirection)writingDirection {
  return NSWritingDirectionLeftToRight;
}
- (CGRect)rect {
  return rect_;
}
- (BOOL)containsStart {
  return start_;
}
- (BOOL)containsEnd {
  return !start_;
}
@end

#pragma mark - BETextSelectionRect
@interface BETextSelectionRect : UITextSelectionRect {
  CGRect rect_;
}
- (instancetype)initWithCGRect:(CGRect)rect;
@end

@implementation BETextSelectionRect
- (instancetype)initWithCGRect:(CGRect)rect {
  rect_ = rect;
  return [self init];
}
- (CGRect)rect {
  return rect_;
}
@end

@implementation RenderWidgetUIView
@synthesize tokenizer;

- (instancetype)initWithWidget:
    (base::WeakPtr<content::RenderWidgetHostViewIOS>)view {
  self = [self init];
  if (self) {
    _view = view;
    _extendedTextInputTraits = [[IOSExtendedTextInputTraits alloc] init];
    text_interaction_ = [[BETextInteraction alloc] init];
    [self addInteraction:text_interaction_];
    self.multipleTouchEnabled = YES;
    self.autoresizingMask =
        UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self initializeInputAccessory];
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(keyboardWillChangeFrame:)
               name:UIKeyboardWillChangeFrameNotification
             object:nil];
  }
  return self;
}

- (void)layoutSubviews {
  CHECK(_view);
  [super layoutSubviews];
  _view->UpdateScreenInfo();

  // TODO(dtapuska): This isn't correct, we need to figure out when the window
  // gains/loses focus.
  _view->SetActive(true);
}

- (UIView*)inputAccessoryView {
  return _inputAccessoryContainerView;
}

- (void)initializeInputAccessory {
  _previousAccessoryButton = [[UIBarButtonItem alloc]
      initWithImage:[UIImage systemImageNamed:kPreviousAccessoryImageName]
              style:UIBarButtonItemStylePlain
             target:self
             action:@selector(handlePreviousAccessoryAction)];
  _previousAccessoryButton.accessibilityLabel =
      l10n_util::GetNSString(IDS_ACCNAME_PREVIOUS);
  _nextAccessoryButton = [[UIBarButtonItem alloc]
      initWithImage:[UIImage systemImageNamed:kNextAccessoryImageName]
              style:UIBarButtonItemStylePlain
             target:self
             action:@selector(handleNextAccessoryAction)];
  _nextAccessoryButton.accessibilityLabel =
      l10n_util::GetNSString(IDS_ACCNAME_NEXT);

  if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
    UIBarButtonItemGroup* navigationGroup =
        [[UIBarButtonItemGroup alloc] initWithBarButtonItems:@[
          _previousAccessoryButton, _nextAccessoryButton
        ]
                                          representativeItem:nil];
    self.inputAssistantItem.trailingBarButtonGroups = @[ navigationGroup ];
    return;
  }

  UIToolbar* toolbar = [[UIToolbar alloc] init];
  [toolbar sizeToFit];

  CGSize toolbarSize = toolbar.frame.size;

  _inputAccessoryContainerView = [[UIView alloc]
      initWithFrame:CGRectMake(0, 0, toolbarSize.width,
                               toolbarSize.height +
                                   kInputAccessoryToolbarBottomMargin)];
  toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  [_inputAccessoryContainerView addSubview:toolbar];

  UIBarButtonItem* flexSpace = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];
  UIBarButtonItem* doneButton = [[UIBarButtonItem alloc]
      initWithImage:[UIImage systemImageNamed:kDoneAccessoryImageName]
              style:UIBarButtonItemStylePlain
             target:self
             action:@selector(hideKeyboard)];
  doneButton.accessibilityLabel = l10n_util::GetNSString(IDS_DONE);

  toolbar.items = @[
    _previousAccessoryButton, _nextAccessoryButton, flexSpace, doneButton
  ];
}

- (ui::CALayerFrameSink*)frameSink {
  return _view.get();
}

- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (BOOL)becomeFirstResponder {
  CHECK(_view);
  BOOL result = [super becomeFirstResponder];
  if (result || _view->CanBecomeFirstResponderForTesting()) {
    _view->OnFirstResponderChanged();
  }
  return result;
}

- (BOOL)resignFirstResponder {
  BOOL result = [super resignFirstResponder];
  if (_view && (result || _view->CanResignFirstResponderForTesting())) {
    _view->OnFirstResponderChanged();
  }
  return result;
}

- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  CHECK(_view);
  for (UITouch* touch in touches) {
    blink::WebTouchEvent webTouchEvent = input::WebTouchEventBuilder::Build(
        blink::WebInputEvent::Type::kTouchStart, touch, event, self,
        _viewOffsetDuringTouchSequence);
    if (!_viewOffsetDuringTouchSequence) {
      _viewOffsetDuringTouchSequence =
          webTouchEvent.touches[0].PositionInWidget() -
          webTouchEvent.touches[0].PositionInScreen();
    }
    _view->OnTouchEvent(std::move(webTouchEvent));
  }
}

- (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  CHECK(_view);
  for (UITouch* touch in touches) {
    _view->OnTouchEvent(input::WebTouchEventBuilder::Build(
        blink::WebInputEvent::Type::kTouchEnd, touch, event, self,
        _viewOffsetDuringTouchSequence));
  }
  if (event.allTouches.count == 1) {
    _viewOffsetDuringTouchSequence.reset();
  }
}

- (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  CHECK(_view);
  for (UITouch* touch in touches) {
    _view->OnTouchEvent(input::WebTouchEventBuilder::Build(
        blink::WebInputEvent::Type::kTouchMove, touch, event, self,
        _viewOffsetDuringTouchSequence));
  }
}

- (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
  CHECK(_view);
  for (UITouch* touch in touches) {
    _view->OnTouchEvent(input::WebTouchEventBuilder::Build(
        blink::WebInputEvent::Type::kTouchCancel, touch, event, self,
        _viewOffsetDuringTouchSequence));
  }
  _viewOffsetDuringTouchSequence.reset();
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  CHECK(_view);
  if (context == kObservingContext) {
    _view->ContentInsetChanged();
  } else {
    [super observeValueForKeyPath:keyPath
                         ofObject:object
                           change:change
                          context:context];
  }
}

- (void)removeView {
  [[NSNotificationCenter defaultCenter]
      removeObserver:self
                name:UIKeyboardWillChangeFrameNotification
              object:nil];
  UIScrollView* view = (UIScrollView*)[self superview];
  [view removeObserver:self
            forKeyPath:NSStringFromSelector(@selector(contentInset))];
  [self removeFromSuperview];
}

- (CGFloat)keyboardHeight {
  return _keyboardHeight;
}

- (void)keyboardWillChangeFrame:(NSNotification*)notification {
  CHECK(_view);
  UIWindow* window = self.window;
  if (!window) {
    _keyboardHeight = 0;
    _view->OnKeyboardVisibilityChanged();
    return;
  }
  NSDictionary* userInfo = notification.userInfo;
  CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];

  id<UICoordinateSpace> fromCoordinateSpace =
      ((UIScreen*)notification.object).coordinateSpace;

  CGRect keyboardFrameInWindow =
      [fromCoordinateSpace convertRect:keyboardFrame
                     toCoordinateSpace:window.coordinateSpace];

  CGFloat visibleHeight =
      CGRectIntersection(keyboardFrameInWindow, window.bounds).size.height;
  CGFloat accessoryHeight = _inputAccessoryContainerView.frame.size.height;
  _keyboardHeight = std::max(visibleHeight - accessoryHeight, 0.0);
  _view->OnKeyboardVisibilityChanged();
}

- (BETextInteraction*)textInteraction {
  return text_interaction_;
}

- (void)updateView:(UIScrollView*)view {
  [view addSubview:self];
  view.scrollEnabled = NO;
  // Remove all existing gestureRecognizers since the header might be reused.
  for (UIGestureRecognizer* recognizer in view.gestureRecognizers) {
    [view removeGestureRecognizer:recognizer];
  }
  [view addObserver:self
         forKeyPath:NSStringFromSelector(@selector(contentInset))
            options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
            context:kObservingContext];
}

- (BOOL)isEditable {
  return _isEditable;
}

- (BOOL)setIsEditable:(BOOL)isEditable {
  if (isEditable == _isEditable) {
    return NO;
  }

  _isEditable = isEditable;
  return YES;
}

- (BOOL)automaticallyPresentEditMenu {
  // Needs an edit menu implementation.
  return NO;
}

- (BOOL)isReplaceAllowed {
  // Needs an implementatino to check if the focused field allows replacements,
  // e.g. password fields do not.
  return NO;
}

- (BOOL)isSelectionAtDocumentStart {
  // Unclear what this does if true.
  return NO;
}

- (NSAttributedString*)attributedMarkedText {
  NSString* text = [self markedText];
  if (!text) {
    return nil;
  }
  return [[NSAttributedString alloc] initWithString:text];
}

- (CGRect)textFirstRect {
  // The bounds of the first line of either marked text or insertion point.
  return CGRectNull;
}

- (CGRect)textLastRect {
  // The bounds of the last line of either marked text or insertion point.
  return CGRectNull;
}

- (CGRect)unobscuredContentRect {
  // Similar to selectionClipRect, this needs to be larger or selection handles
  // will appear in the wrong place when zoomed out of view. This needs a proper
  // implementation showing the real rect of the view transformed from the
  // [view bounds]
  return CGRectMake(-1000, -1000, 10000, 10000);
}

- (UIView*)unscaledView {
  // View representing the web content that is agnostic of zoom state, so
  // returning self is a simple hack and wrong.
  return self;
}

- (id<BETextInputDelegate>)asyncInputDelegate {
  return be_text_input_delegate_;
}

- (id<UITextInputDelegate>)inputDelegate {
  return nil;
}

- (void)setInputDelegate:(id<UITextInputDelegate>)inputDelegate {
}

- (void)setAsyncInputDelegate:(id<BETextInputDelegate>)delegate {
  be_text_input_delegate_ = delegate;
}

- (UIView*)textInputView {
  return self;
}

- (BOOL)hasMarkedText {
  return _markedText.length() > 0;
}

- (NSString*)markedText {
  if (![self hasMarkedText]) {
    return nil;
  }
  return base::SysUTF16ToNSString(_markedText);
}

- (NSString*)selectedText {
  auto* selection = [self textSelection];
  if (!selection || !selection->selected_text().length()) {
    return nil;
  }

  return base::SysUTF16ToNSString(selection->selected_text());
}

- (void)unmarkText {
  if (![self hasMarkedText]) {
    return;
  }

  CHECK(_view);
  _view->ImeFinishComposingText(false);
  _markedText.clear();
}

- (CGRect)selectionClipRect {
  auto rect = [self textControlBounds];
  if (!rect) {
    return CGRectNull;
  }
  // Need to get a more realistic rect here. If this clip is too small,
  // selection handles won't draw correctly.
  return CGRectMake(rect->x(), rect->y(), rect->width(), rect->height());
}

- (id<BEExtendedTextInputTraits>)extendedTextInputTraits {
  return _extendedTextInputTraits;
}

- (void)handleEditCommands:(const std::vector<std::string>&)commands {
  CHECK(_view);
  // If there's a pending key down event, forward it along with the edit
  // commands to the renderer. This allows the renderer to associate the
  // commands with the keyboard event that triggered them.
  if (auto event = std::exchange(_currentKeyDownEvent, std::nullopt)) {
    std::vector<blink::mojom::EditCommandPtr> editCommands;
    editCommands.reserve(commands.size());
    for (const auto& command : commands) {
      editCommands.push_back(blink::mojom::EditCommand::New(command, ""));
    }
    _view->ForwardKeyboardEventWithCommands(*event, std::move(editCommands));
    return;
  }
  // No pending key event - execute the edit commands directly. This handles
  // cases where commands are triggered by non-keyboard input.
  for (const auto& command : commands) {
    _view->ExecuteEditCommand(command);
  }
}

- (std::string)moveSelectionCommand:(UITextLayoutDirection)direction {
  switch (direction) {
    case UITextLayoutDirectionLeft:
      return "moveLeft";
    case UITextLayoutDirectionRight:
      return "moveRight";
    case UITextLayoutDirectionUp:
      return "moveUp";
    case UITextLayoutDirectionDown:
      return "moveDown";
  }
  NOTREACHED() << "Unknown Text Layout Direction";
}

- (void)moveInLayoutDirection:(UITextLayoutDirection)direction {
  [self handleEditCommands:{[self moveSelectionCommand:direction]}];
}

- (std::string)extendSelectionCommand:(UITextLayoutDirection)direction {
  switch (direction) {
    case UITextLayoutDirectionLeft:
      return "moveLeftAndModifySelection";
    case UITextLayoutDirectionRight:
      return "moveRightAndModifySelection";
    case UITextLayoutDirectionUp:
      return "moveUpAndModifySelection";
    case UITextLayoutDirectionDown:
      return "moveDownAndModifySelection";
  }
  NOTREACHED() << "Unknown Text Layout Direction";
}

- (void)extendInLayoutDirection:(UITextLayoutDirection)direction {
  [self handleEditCommands:{[self extendSelectionCommand:direction]}];
}

- (std::vector<std::string>)
    moveSelectionCommands:(UITextStorageDirection)direction
            byGranularity:(UITextGranularity)granularity {
  if (granularity == UITextGranularityCharacter) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveForward"}
               : std::vector<std::string>{"moveBackward"};
  }
  if (granularity == UITextGranularityWord) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveWordForward"}
               : std::vector<std::string>{"moveWordBackward"};
  }
  if (granularity == UITextGranularitySentence) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveToEndOfSentence"}
               : std::vector<std::string>{"moveToBeginningOfSentence"};
  }
  if (granularity == UITextGranularityParagraph) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveForward", "moveToEndOfParagraph"}
               : std::vector<std::string>{"moveBackward",
                                          "moveToBeginningOfParagraph"};
  }
  if (granularity == UITextGranularityLine) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveToEndOfLine"}
               : std::vector<std::string>{"moveToBeginningOfLine"};
  }
  return direction == UITextStorageDirectionForward
             ? std::vector<std::string>{"moveToEndOfDocument"}
             : std::vector<std::string>{"moveToBeginningOfDocument"};
}

- (void)moveInStorageDirection:(UITextStorageDirection)direction
                 byGranularity:(UITextGranularity)granularity {
  [self handleEditCommands:[self moveSelectionCommands:direction
                                         byGranularity:granularity]];
}

- (std::vector<std::string>)
    extendSelectionCommands:(UITextStorageDirection)direction
              byGranularity:(UITextGranularity)granularity {
  if (granularity == UITextGranularityCharacter) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveBackwardAndModifySelection"}
               : std::vector<std::string>{"moveForwardAndModifySelection"};
  }
  if (granularity == UITextGranularityWord) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveWordForwardAndModifySelection"}
               : std::vector<std::string>{"moveWordBackwardAndModifySelection"};
  }
  if (granularity == UITextGranularitySentence) {
    return direction == UITextStorageDirectionForward
               ? std::vector<
                     std::string>{"moveToEndOfSentenceAndModifySelection"}
               : std::vector<std::string>{
                     "moveToBeginningOfSentenceAndModifySelection"};
  }
  if (granularity == UITextGranularityParagraph) {
    return direction == UITextStorageDirectionForward
               ? std::vector<
                     std::string>{"moveForwardAndModifySelection",
                                  "moveToEndOfParagraphAndModifySelection"}
               : std::vector<std::string>{
                     "moveBackwardAndModifySelection",
                     "moveToBeginningOfParagraphAndModifySelection"};
  }
  if (granularity == UITextGranularityLine) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"moveToEndOfLineAndModifySelection"}
               : std::vector<std::string>{
                     "moveToBeginningOfLineAndModifySelection"};
  }
  return direction == UITextStorageDirectionForward
             ? std::vector<std::string>{"moveToEndOfDocumentAndModifySelection"}
             : std::vector<std::string>{
                   "moveToBeginningOfDocumentAndModifySelection"};
}

- (void)extendInStorageDirection:(UITextStorageDirection)direction
                   byGranularity:(UITextGranularity)granularity {
  [self handleEditCommands:[self extendSelectionCommands:direction
                                           byGranularity:granularity]];
}

- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender {
  return YES;
}

- (BOOL)shouldInsertCharacter:(const blink::WebKeyboardEvent&)webKeyboardEvent {
  size_t textLength =
      std::char_traits<char16_t>::length(webKeyboardEvent.text.data());

  // For inputting emojis (multiple characters)
  if (textLength > 1) {
    return YES;
  }

  if (textLength == 0) {
    return NO;
  }

  // Check the first character if text is available
  char16_t ch = webKeyboardEvent.text[0];
  if (ch < ' ') {
    return NO;
  }

  // Check for ASCII control characters with modifiers
  if (ch < 0x80) {
    int modifiers = webKeyboardEvent.GetModifiers();
    if ((modifiers & blink::WebInputEvent::kControlKey) ||
        (modifiers & blink::WebInputEvent::kMetaKey)) {
      return NO;
    }
  }

  return YES;
}

- (void)handleKeyEntry:(BEKeyEntry*)entry
    withCompletionHandler:
        (void (^)(BEKeyEntry* theEvent, BOOL wasHandled))completionHandler {
  CHECK(_view);

  input::NativeWebKeyboardEvent nativeEvent(
      (base::apple::OwnedBEKeyEntry(entry)));
  if (entry.state != BEKeyPressState::BEKeyPressStateDown) {
    _currentKeyDownEvent.reset();
    _view->SendKeyEvent(nativeEvent);
    completionHandler(entry, YES);
    return;
  }

  _currentKeyDownEvent = nativeEvent;
  BEKeyEntryContext* contextForKeyDown =
      [[BEKeyEntryContext alloc] initWithKeyEntry:entry];
  [contextForKeyDown setDocumentEditable:[self isEditable]];
  // To trigger key commands correctly, e.g. trigger
  // `transposeCharactersAroundSelection` on Ctrl+T, we need to set
  // `shouldInsertCharacter` to NO when users are not inputting characters.
  // Otherwise, the key commands will not be triggered.
  [contextForKeyDown
      setShouldInsertCharacter:[self shouldInsertCharacter:nativeEvent]];

  BOOL handled = [[self asyncInputDelegate]
      shouldDeferEventHandlingToSystemForTextInput:self
                                           context:contextForKeyDown];
  if (!handled) {
    // The system did not handle the event (e.g., the user pressed Enter).
    auto event = std::exchange(_currentKeyDownEvent, std::nullopt);
    // Reset to kKeyDown so Blink dispatches both keydown and keypress events.
    event->SetType(blink::WebInputEvent::Type::kKeyDown);
    _view->SendKeyEvent(*event);
  }
  completionHandler(entry, YES);
}

- (void)shiftKeyStateChangedFromState:(BEKeyModifierFlags)oldState
                              toState:(BEKeyModifierFlags)newState {
}

- (std::vector<std::string>)
    deleteSelectionCommands:(UITextStorageDirection)direction
              toGranularity:(UITextGranularity)granularity {
  if (granularity == UITextGranularityCharacter) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"deleteForward"}
               : std::vector<std::string>{"deleteBackward"};
  }
  if (granularity == UITextGranularityWord) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"deleteWordForward"}
               : std::vector<std::string>{"deleteWordBackward"};
  }
  if (granularity == UITextGranularitySentence) {
    return {direction == UITextStorageDirectionForward
                ? "moveToEndOfSentenceAndModifySelection"
                : "moveToBeginningOfSentenceAndModifySelection",
            "deleteBackward"};
  }
  if (granularity == UITextGranularityParagraph) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"deleteToEndOfParagraph"}
               : std::vector<std::string>{"deleteToBeginningOfParagraph"};
  }
  if (granularity == UITextGranularityLine) {
    return direction == UITextStorageDirectionForward
               ? std::vector<std::string>{"deleteToEndOfLine"}
               : std::vector<std::string>{"deleteToBeginningOfLine"};
  }
  return {direction == UITextStorageDirectionForward
              ? "moveToEndOfDocumentAndModifySelection"
              : "moveToBeginningOfDocumentAndModifySelection",
          "deleteBackward"};
}

- (void)deleteInDirection:(UITextStorageDirection)direction
            toGranularity:(UITextGranularity)granularity {
  [self handleEditCommands:[self deleteSelectionCommands:direction
                                           toGranularity:granularity]];
}

- (void)transposeCharactersAroundSelection {
  [self handleEditCommands:{"transpose"}];
}

- (BOOL)replaceText:(NSString*)originalText
           withText:(NSString*)replacementText {
  if (replacementText == originalText) {
    return NO;
  }

  // If we call ExtendSelectionAndReplace with an empty replacementText,
  // textarea will be broken, users cannot focus and input in textarea.
  // TODO(crbug.com/428561251): Call ExtendSelectionAndReplace with an empty
  // replacementText will make textarea broken
  if (!replacementText.length) {
    _view->ExtendSelectionAndDelete(originalText.length, 0);
  } else {
    _view->ExtendSelectionAndReplace(originalText.length, 0,
                                     base::SysNSStringToUTF16(replacementText));
  }
  return YES;
}

- (void)replaceText:(NSString*)originalText
             withText:(NSString*)replacementText
              options:(BETextReplacementOptions)options
    completionHandler:
        (void (^)(NSArray<UITextSelectionRect*>* rects))completionHandler {
  if (![self replaceText:originalText withText:replacementText]) {
    completionHandler(@[]);
    return;
  }

  // TODO: bug 388320178 - still don't know what to do with this.
  completionHandler(@[]);
}

- (void)requestTextContextForAutocorrectionWithCompletionHandler:
    (void (^)(BETextDocumentContext* context))completionHandler {
  completionHandler(nil);
}

- (void)requestTextRectsForString:(NSString*)input
            withCompletionHandler:
                (void (^)(NSArray<UITextSelectionRect*>* rects))
                    completionHandler {
  auto* state = [self editState];
  if (!state || !state->selection.is_empty()) {
    completionHandler(@[]);
    return;
  }

  NSRange range =
      [[self editText] rangeOfString:input
                             options:NSLiteralSearch
                               range:NSMakeRange(0, state->selection.start())];
  if (range.location == NSNotFound) {
    completionHandler(@[]);
    return;
  }

  _view->RectForEditFieldChars(
      gfx::Range(range),
      base::BindOnce(
          [](void (^completionHandler)(NSArray<UITextSelectionRect*>* rects),
             const gfx::Rect& rect) {
            if (rect.IsEmpty()) {
              completionHandler(@[]);
              return;
            }
            completionHandler(@[ [[BETextSelectionRect alloc]
                initWithCGRect:rect.ToCGRect()] ]);
          },
          completionHandler));
}

- (void)requestPreferredArrowDirectionForEditMenuWithCompletionHandler:
    (void (^)(UIEditMenuArrowDirection))completionHandler {
  completionHandler(UIEditMenuArrowDirectionAutomatic);
}

- (void)systemWillPresentEditMenuWithAnimator:
    (id<UIEditMenuInteractionAnimating>)animator
    API_UNAVAILABLE(watchos, tvos) {
}

- (void)systemWillDismissEditMenuWithAnimator:
    (id<UIEditMenuInteractionAnimating>)animator
    API_UNAVAILABLE(watchos, tvos) {
}

- (nullable NSDictionary<NSAttributedStringKey, id>*)
    textStylingAtPosition:(UITextPosition*)position
              inDirection:(UITextStorageDirection)direction {
  return nil;
}

- (void)replaceSelectedText:(NSString*)text
                   withText:(NSString*)replacementText {
}

- (void)updateCurrentSelectionTo:(CGPoint)point
                     fromGesture:(BEGestureType)gestureType
                         inState:(UIGestureRecognizerState)state {
  if (!_view) {
    return;
  }
  _view->host()->delegate()->MoveRangeSelectionExtent(
      gfx::Point(point.x, point.y));
}

- (void)setSelectionFromPoint:(CGPoint)from
                      toPoint:(CGPoint)to
                      gesture:(BEGestureType)gesture
                        state:(UIGestureRecognizerState)state
    NS_SWIFT_NAME(setSelection(from:to:gesture:state:)) {
}

- (void)adjustSelectionBoundaryToPoint:(CGPoint)point
                            touchPhase:(BESelectionTouchPhase)touch
                           baseIsStart:(BOOL)boundaryIsStart
                                 flags:(BESelectionFlags)flags {
  auto* region = [self selectionRegion];
  if (!region || !region->focus.HasHandle()) {
    return;
  }

  // A simple naive implementation that updates the selection range based on
  // a combination of boundaryIsStart (to know which handle was grabbed) and
  // SelectionRegion data. In the future this could be simplified with more
  // data, such as document position of selection to know which should be
  // start and end.
  CGPoint start, end;
  if (region->focus.type() == gfx::SelectionBound::RIGHT) {
    start = CGPointMake(region->focus.edge_start_rounded().x(),
                        region->focus.edge_start_rounded().y());
    end = CGPointMake(region->anchor.edge_start_rounded().x(),
                      region->anchor.edge_start_rounded().y());
  } else {
    end = CGPointMake(region->focus.edge_start_rounded().x(),
                      region->focus.edge_start_rounded().y());
    start = CGPointMake(region->anchor.edge_start_rounded().x(),
                        region->anchor.edge_start_rounded().y());
  }

  if (boundaryIsStart) {
    end = point;
  } else {
    start = point;
  }

  // This should look at document position instead, but for a naive
  // implementation works well enough.
  if (end.x < start.x && end.y < start.y) {
    flags = BESelectionFlipped;
    CGPoint flip = start;
    start = end;
    end = flip;
  }

  _view->host()->delegate()->SelectRange(gfx::Point(start.x, start.y),
                                         gfx::Point(end.x, end.y));

  // Tells the system the selection adjustment has been handled for the given
  // `point` and touch.
  [text_interaction_ selectionBoundaryAdjustedToPoint:point
                                           touchPhase:touch
                                                flags:flags];
}

- (BOOL)textInteractionGesture:(BEGestureType)gestureType
            shouldBeginAtPoint:(CGPoint)point {
  // Check if point is really selectable here.
  return NO;
}

- (void)selectWordForReplacement {
}

- (void)updateSelectionWithExtentPoint:(CGPoint)point
                              boundary:(UITextGranularity)granularity
                     completionHandler:(void (^)(BOOL selectionEndIsMoving))
                                           completionHandler {
  if (!_view) {
    completionHandler(false);
    return;
  }
  _view->host()->delegate()->MoveRangeSelectionExtent(
      gfx::Point(point.x, point.y));
  completionHandler(true);
}

- (void)selectTextInGranularity:(UITextGranularity)granularity
                        atPoint:(CGPoint)point
              completionHandler:(void (^)(void))completionHandler {
  if (!_view) {
    completionHandler();
    return;
  }
  _view->host()->delegate()->MoveCaret(gfx::Point(point.x, point.y));
  _view->host()->delegate()->SelectRange(gfx::Point(point.x, point.y),
                                         gfx::Point(point.x, point.y));
  _view->host()->delegate()->SelectRange(gfx::Point(point.x, point.y),
                                         gfx::Point(point.x, point.y));
  _view->host()->delegate()->SelectAroundCaret(
      blink::mojom::SelectionGranularity::kWord,
      /*should_show_handle=*/true,
      /*should_show_context_menu=*/false);
  completionHandler();
}

// To set caret when users long-press on spacebar and move.
- (void)selectPositionAtPoint:(CGPoint)point
            completionHandler:(void (^)(void))completionHandler {
  if (!_view) {
    completionHandler();
    return;
  }

  CGFloat x = point.x;
  CGFloat y = point.y;
  // Constrain point to bounds of focused element.
  auto textControlBounds = [self textControlBounds];
  if (textControlBounds.has_value()) {
    x = std::clamp<CGFloat>(x, textControlBounds->x(),
                            textControlBounds->right());
    y = std::clamp<CGFloat>(y, textControlBounds->y(),
                            textControlBounds->bottom());
  }
  _view->host()->delegate()->MoveCaret(gfx::ToRoundedPoint(gfx::PointF(x, y)));
  completionHandler();
}

- (void)selectPositionAtPoint:(CGPoint)point
           withContextRequest:(BETextDocumentRequest*)request
            completionHandler:
                (void (^)(BETextDocumentContext*))completionHandler {
}

- (void)adjustSelectionByRange:(BEDirectionalTextRange)range
             completionHandler:(void (^)(void))completionHandler {
}

- (void)moveByOffset:(NSInteger)offset {
}

- (void)moveSelectionAtBoundary:(UITextGranularity)granularity
             inStorageDirection:(UITextStorageDirection)direction
              completionHandler:(void (^)(void))completionHandler {
}

- (void)
    selectTextForEditMenuWithLocationInView:(CGPoint)locationInView
                          completionHandler:
                              (void (^)(BOOL shouldPresentMenu,
                                        NSString* _Nullable contextString,
                                        NSRange selectedRangeInContextString))
                                  completionHandler {
}

- (void)setAttributedMarkedText:(nullable NSAttributedString*)markedText
                  selectedRange:(NSRange)selectedRange {
  [self setMarkedText:markedText.string selectedRange:selectedRange];
}

- (BOOL)isPointNearMarkedText:(CGPoint)point {
  // This needs a real implementation.
  return YES;
}

- (void)requestDocumentContext:(BETextDocumentRequest*)request
             completionHandler:
                 (void (^)(BETextDocumentContext*))completionHandler {
  completionHandler(nil);
}

- (void)willInsertFinalDictationResult {
}

- (void)replaceDictatedText:(NSString*)oldText withText:(NSString*)newText {
  [self replaceText:oldText withText:newText];
}

- (void)didInsertFinalDictationResult {
}

- (nullable NSArray<BETextAlternatives*>*)alternativesForSelectedText {
  return nil;
}

- (void)addTextAlternatives:(BETextAlternatives*)alternatives {
}

- (void)insertTextAlternatives:(BETextAlternatives*)alternatives {
  auto text = alternatives.primaryString;
  [self insertText:text];
}

- (void)insertTextPlaceholderWithSize:(CGSize)size
                    completionHandler:
                        (void (^)(UITextPlaceholder*))completionHandler {
}

- (void)removeTextPlaceholder:(UITextPlaceholder*)placeholder
               willInsertText:(BOOL)willInsertText
            completionHandler:(void (^)(void))completionHandler {
}

- (void)insertTextSuggestion:(BETextSuggestion*)textSuggestion {
}

- (void)autoscrollToPoint:(CGPoint)point {
  _view->StartAutoscrollForSelectionToPoint(gfx::PointF(point.x, point.y));
}

- (void)cancelAutoscroll {
  _view->StopAutoscroll();
}

- (UITextRange*)markedTextRange {
  return nil;
}

- (NSDictionary*)markedTextStyle {
  return nil;
}

- (void)setMarkedTextStyle:(NSDictionary*)styleDictionary {
}

- (UITextPosition*)beginningOfDocument {
  return nil;
}

- (UITextPosition*)endOfDocument {
  return nil;
}

- (BOOL)hasText {
  const ui::mojom::TextInputState* state = [self editState];
  if (state && state->value.has_value()) {
    return state->value->size() > 0;
  } else {
    return NO;
  }
}

- (void)insertText:(NSString*)text {
  CHECK(_view);
  if (auto event = std::exchange(_currentKeyDownEvent, std::nullopt)) {
    // If this insert was triggered by a key down event, forward it to the
    // renderer as kKeyDown. This ensures both keydown and keypress events
    // are dispatched to JavaScript with the correct text.
    event->SetType(blink::WebInputEvent::Type::kKeyDown);
    _view->SendKeyEvent(*event);
    return;
  }
  if (text.length == 0) {
    return;
  }

  _markedText.clear();
  _view->ImeCommitText(base::SysNSStringToUTF16(text),
                       gfx::Range::InvalidRange(), 0);
}

- (void)deleteBackward {
  [self handleEditCommands:{"deleteBackward"}];
}

- (void)selectAll:(nullable id)sender {
  [self handleEditCommands:{"selectAll"}];
}

- (void)setSelectedTextRange:(UITextRange*)range {
}

- (UITextRange*)selectedTextRange {
  auto* region = [self selectionRegion];
  if (region) {
    return [[BETextRange alloc] initWithRegion:region];
  }

  return nil;
}
- (nullable NSString*)textInRange:(UITextRange*)range {
  return nil;
}

- (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
}

- (void)setMarkedText:(nullable NSString*)markedText
        selectedRange:(NSRange)selectedRange {
  _markedText = base::SysNSStringToUTF16(markedText);
  std::vector<ui::ImeTextSpan> imeTextSpans;
  if (_markedText.length() > 0) {
    ui::ImeTextSpan span;
    span.start_offset = 0;
    span.end_offset = _markedText.length();
    span.underline_style = ui::ImeTextSpan::UnderlineStyle::kSolid;
    imeTextSpans.push_back(span);
  }

  CHECK(_view);
  if (auto event = std::exchange(_currentKeyDownEvent, std::nullopt)) {
    // If an Input Method Editor is processing key input and the event is
    // keydown, keyCode should return 229, see:
    // https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
    event->windows_key_code = 0xE5;  // VKEY_PROCESSKEY
    _view->SendKeyEvent(*event);
  }
  _view->ImeSetComposition(_markedText, imeTextSpans,
                           gfx::Range::InvalidRange(), selectedRange.location,
                           selectedRange.location + selectedRange.length);
}

- (nullable UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
                                    toPosition:(UITextPosition*)toPosition {
  return nil;
}

- (nullable UITextPosition*)positionFromPosition:(UITextPosition*)position
                                          offset:(NSInteger)offset {
  return nil;
}

- (nullable UITextPosition*)positionFromPosition:(UITextPosition*)position
                                     inDirection:
                                         (UITextLayoutDirection)direction
                                          offset:(NSInteger)offset {
  return nil;
}

- (NSComparisonResult)comparePosition:(UITextPosition*)position
                           toPosition:(UITextPosition*)other {
  return NSOrderedSame;
}

- (NSInteger)offsetFromPosition:(UITextPosition*)from
                     toPosition:(UITextPosition*)toPosition {
  return 0;
}

- (nullable UITextPosition*)positionWithinRange:(UITextRange*)range
                            farthestInDirection:
                                (UITextLayoutDirection)direction {
  return nil;
}

- (nullable UITextRange*)
    characterRangeByExtendingPosition:(UITextPosition*)position
                          inDirection:(UITextLayoutDirection)direction {
  return nil;
}

- (NSWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
                                          inDirection:(UITextStorageDirection)
                                                          direction {
  return NSWritingDirectionNatural;
}

- (void)setBaseWritingDirection:(NSWritingDirection)writingDirection
                       forRange:(UITextRange*)range {
}

- (CGRect)caretRectForPosition:(UITextPosition*)position {
  BETextPosition* be_position = base::apple::ObjCCast<BETextPosition>(position);
  if (be_position) {
    return [be_position rect];
  }
  return CGRectNull;
}

- (NSArray<UITextSelectionRect*>*)selectionRectsForRange:(UITextRange*)range {
  auto* region = [self selectionRegion];
  // The following should instead use |range| rather than assuming
  // GetSelectionRegion. Consider this proof-of-concept only.
  if (!region || !region->focus.HasHandle() ||
      region->focus.type() == gfx::SelectionBound::CENTER) {
    return @[];
  }

  UITextSelectionRect* start = [[BETextSelectionHandles alloc]
      initWithCGRect:CGRectMake(region->focus.edge_start_rounded().x(),
                                region->focus.edge_start_rounded().y(), 1,
                                region->focus.GetHeight())
             atStart:region->focus.type() == gfx::SelectionBound::RIGHT];
  UITextSelectionRect* end = [[BETextSelectionHandles alloc]
      initWithCGRect:CGRectMake(region->anchor.edge_start_rounded().x(),
                                region->anchor.edge_start_rounded().y(), 1,
                                region->anchor.GetHeight())
             atStart:region->anchor.type() == gfx::SelectionBound::RIGHT];
  return @[ start, end ];
}

#pragma mark - Hit testing

- (nullable UITextPosition*)closestPositionToPoint:(CGPoint)point {
  return nil;
}

- (nullable UITextPosition*)closestPositionToPoint:(CGPoint)point
                                       withinRange:(UITextRange*)range {
  return nil;
}

- (nullable UITextRange*)characterRangeAtPoint:(CGPoint)point {
  return nil;
}

- (NSArray*)accessibilityElements {
  ui::BrowserAccessibilityManager* manager =
      _view->host()->GetRootBrowserAccessibilityManager();
  if (manager) {
    id root =
        manager->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get();
    if (root) {
      return @[ root ];
    }
  }
  return nil;
}

- (const std::optional<gfx::Rect>)textControlBounds {
  if (!_view || !_view->GetTextInputManager()) {
    return std::nullopt;
  }
  return _view->GetTextInputManager()->GetTextControlBounds();
}

- (const content::TextInputManager::SelectionRegion*)selectionRegion {
  if (!_view || !_view->GetTextInputManager()) {
    return nil;
  }
  return _view->GetTextInputManager()->GetSelectionRegion(_view.get());
}

- (const content::TextInputManager::TextSelection*)textSelection {
  if (!_view || !_view->GetTextInputManager()) {
    return nil;
  }
  return _view->GetTextInputManager()->GetTextSelection(_view.get());
}

- (const ui::mojom::TextInputState*)editState {
  if (!_view || !_view->GetTextInputManager()) {
    return nil;
  }
  return _view->GetTextInputManager()->GetTextInputState();
}

- (NSString*)editText {
  const ui::mojom::TextInputState* state = [self editState];
  if (state && state->value.has_value()) {
    const unichar* pchars = (const unichar*)state->value->c_str();
    NSString* result = [NSString stringWithCharacters:pchars
                                               length:state->value->size()];
    return result;
  } else {
    return @"";
  }
}

- (BOOL)isAccessibilityElement {
  return NO;
}

- (CGRect)firstRectForRange:(UITextRange*)range {
  return CGRectZero;
}

- (void)onUpdateTextInputState:(const ui::mojom::TextInputState&)state
                    withBounds:(CGRect)bounds {
  [_extendedTextInputTraits updateFromTextInputState:state];
  _previousAccessoryButton.enabled =
      (state.flags & ui::TEXT_INPUT_FLAG_HAVE_PREVIOUS_FOCUSABLE_ELEMENT) != 0;
  _nextAccessoryButton.enabled =
      (state.flags & ui::TEXT_INPUT_FLAG_HAVE_NEXT_FOCUSABLE_ELEMENT) != 0;

  bool editable = state.type != ui::TextInputType::TEXT_INPUT_TYPE_NONE &&
                  state.mode != ui::TextInputMode::TEXT_INPUT_MODE_NONE;
  [self setIsEditable:editable];

  // Check for the visibility request and policy if VK APIs are enabled.
  if (state.vk_policy == ui::mojom::VirtualKeyboardPolicy::MANUAL) {
    // policy is manual.
    if (state.last_vk_visibility_request ==
        ui::mojom::VirtualKeyboardVisibilityRequest::SHOW) {
      [self showKeyboard:(state.value && !state.value->empty())
              withBounds:bounds];
    } else if (state.last_vk_visibility_request ==
               ui::mojom::VirtualKeyboardVisibilityRequest::HIDE) {
      [self hideKeyboard];
    }
  } else if (state.always_hide_ime || !editable) {
    [self hideKeyboard];
  } else if (state.show_ime_if_needed) {
    [self showKeyboard:(state.value && !state.value->empty())
            withBounds:bounds];
  }
}

- (void)handlePreviousAccessoryAction {
  CHECK(_view);
  _view->AdvanceFocusForIME(blink::mojom::FocusType::kBackward);
}

- (void)handleNextAccessoryAction {
  CHECK(_view);
  _view->AdvanceFocusForIME(blink::mojom::FocusType::kForward);
}

- (void)showKeyboard:(bool)has_text withBounds:(CGRect)bounds {
  self.frame = bounds;
  [self becomeFirstResponder];
  [self reloadInputViews];
}

- (void)hideKeyboard {
  [self resignFirstResponder];
  [self reloadInputViews];
}

@end
