// Copyright 2025 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_tvos_uiview.h"

#include "base/apple/owned_objc.h"
#include "base/strings/sys_string_conversions.h"
#include "components/input/native_web_keyboard_event.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/base/ime/mojom/ime_types.mojom-shared.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/keyboard_codes.h"

static void* kObservingContext = &kObservingContext;

namespace {

typedef NS_ENUM(NSInteger, RemoteButton) {
  kUp,
  kDown,
  kLeft,
  kRight,
  kMediaPlayPause,
  kSelect,
  kMenu,
  kNone
};

// The minimum velocity to generate left/right direction events from
// UIPanGestureRecognizer.
const CGFloat kMinVelocity = 100;

UIKeyboardType keyboardTypeForInputType(ui::TextInputType inputType) {
  // TODO(crbug.com/411452047): Implement textFieldShouldEndEditing to detect
  // invalid contents in the text field. When texts are inserted via a H/W
  // connected keyboard, it's still possible to insert invalid contents.
  switch (inputType) {
    case ui::TextInputType::TEXT_INPUT_TYPE_SEARCH:
      return UIKeyboardTypeWebSearch;
    case ui::TextInputType::TEXT_INPUT_TYPE_EMAIL:
      return UIKeyboardTypeEmailAddress;
    case ui::TextInputType::TEXT_INPUT_TYPE_NUMBER:
      return UIKeyboardTypeNumberPad;
    case ui::TextInputType::TEXT_INPUT_TYPE_TELEPHONE:
      return UIKeyboardTypePhonePad;
    case ui::TextInputType::TEXT_INPUT_TYPE_URL:
      return UIKeyboardTypeURL;
    default:
      return UIKeyboardTypeASCIICapable;
  }
}

RemoteButton remoteButtonFromPressType(UIPressType type) {
  RemoteButton button = kNone;
  switch (type) {
    case UIPressTypeUpArrow:
      button = kUp;
      break;
    case UIPressTypeDownArrow:
      button = kDown;
      break;
    case UIPressTypeLeftArrow:
      button = kLeft;
      break;
    case UIPressTypeRightArrow:
      button = kRight;
      break;
    case UIPressTypePlayPause:
      button = kMediaPlayPause;
      break;
    case UIPressTypeSelect:
      button = kSelect;
      break;
    case UIPressTypeMenu:
      button = kMenu;
      break;
    default:
      break;
  }
  return button;
}

}  // namespace

@implementation RenderWidgetUIView

#pragma mark - Public

- (instancetype)initWithWidget:
    (base::WeakPtr<content::RenderWidgetHostViewIOS>)view {
  self = [self init];
  if (self) {
    _view = view;
    self.autoresizingMask =
        UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    // tvOS supports multiple types of input events from the Remote, including
    // the clickpad (touch surface), the clickpad ring (directional control),
    // and various physical buttons.
    [self addSwipeAndPanGestureRecognizers];
  }
  return self;
}

// Contrary to iOS, on tvOS the on-screen keyboard takes the entire screen, so
// instead of showing it when an input element is focused, we require a tap to
// do it.
- (void)onUpdateTextInputState:(const ui::mojom::TextInputState&)state
                    withBounds:(CGRect)bounds {
  // 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];
    } else if (state.last_vk_visibility_request ==
               ui::mojom::VirtualKeyboardVisibilityRequest::HIDE) {
      [self hideAndDeleteKeyboard];
    }
  } else {
    bool hide = state.always_hide_ime ||
                state.mode == ui::TextInputMode::TEXT_INPUT_MODE_NONE ||
                state.type == ui::TextInputType::TEXT_INPUT_TYPE_NONE;
    if (hide) {
      [self hideAndDeleteKeyboard];
    } else if (state.show_ime_if_needed) {
      [self showKeyboard:state];
    }
  }
}

- (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];
}

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

#pragma mark - Private

- (void)addSwipeAndPanGestureRecognizers {
  // Create and add swipe gesture recognizers for all directions originating
  // from the clickpad buttons.
  [self addSwipeGestureRecognizerWithDirection:
            UISwipeGestureRecognizerDirectionUp];
  [self addSwipeGestureRecognizerWithDirection:
            UISwipeGestureRecognizerDirectionLeft];
  [self addSwipeGestureRecognizerWithDirection:
            UISwipeGestureRecognizerDirectionRight];
  [self addSwipeGestureRecognizerWithDirection:
            UISwipeGestureRecognizerDirectionDown];

  // Add a pan gesture recognizer to capture input from the clickpad ring,
  // which allows for continuous movement to the left or right.
  UIPanGestureRecognizer* panGesture =
      [[UIPanGestureRecognizer alloc] initWithTarget:self
                                              action:@selector(handlePan:)];
  panGesture.delegate = self;
  [self addGestureRecognizer:panGesture];

  // Only allow the pan gesture to activate if the swipe gesture fails to
  // recognize.
  for (UIGestureRecognizer* swipeGesture in self.gestureRecognizers) {
    if ([swipeGesture isKindOfClass:[UISwipeGestureRecognizer class]]) {
      [panGesture requireGestureRecognizerToFail:swipeGesture];
    }
  }
}

// Helper method to add swipe gestures for `direction`.
- (void)addSwipeGestureRecognizerWithDirection:
    (UISwipeGestureRecognizerDirection)direction {
  UISwipeGestureRecognizer* swipeGesture = [[UISwipeGestureRecognizer alloc]
      initWithTarget:self
              action:@selector(swipeGesture:)];
  swipeGesture.direction = direction;
  swipeGesture.delegate = self;
  [self addGestureRecognizer:swipeGesture];
}

- (void)swipeGesture:(UISwipeGestureRecognizer*)gestureRecognizer {
  if ([gestureRecognizer state] != UIGestureRecognizerStateEnded) {
    return;
  }

  RemoteButton button = kNone;
  switch (gestureRecognizer.direction) {
    case UISwipeGestureRecognizerDirectionLeft:
      button = kLeft;
      break;
    case UISwipeGestureRecognizerDirectionRight:
      button = kRight;
      break;
    case UISwipeGestureRecognizerDirectionUp:
      button = kUp;
      break;
    case UISwipeGestureRecognizerDirectionDown:
      button = kDown;
      break;
  }
  // Because a swipe is a discrete gesture, the system sends the associated
  // action message just once per gesture. So, kKeyDown and kKeyUp are sent to
  // blink in this method.
  [self sendKeyEventWithRemoteButton:button
                           eventType:blink::WebInputEvent::Type::kKeyDown];
  [self sendKeyEventWithRemoteButton:button
                           eventType:blink::WebInputEvent::Type::kKeyUp];
}

- (void)handlePan:(UIPanGestureRecognizer*)gesture {
  CGPoint velocity = [gesture velocityInView:self];

  // Detect left and right swipes with the velocity.
  if (gesture.state == UIGestureRecognizerStateEnded ||
      gesture.state == UIGestureRecognizerStateChanged) {
    // Use `kMinVelocity` to avoid excessive events.
    RemoteButton button = kNone;
    if (velocity.x > kMinVelocity) {
      button = kRight;
    } else if (velocity.x < -kMinVelocity) {
      button = kLeft;
    }
    [self sendKeyEventWithRemoteButton:button
                             eventType:blink::WebInputEvent::Type::kKeyDown];
    [self sendKeyEventWithRemoteButton:button
                             eventType:blink::WebInputEvent::Type::kKeyUp];
  }
}

// Handles keyboard-show logic for UIPressTypeSelect. Returns YES if the key
// event should be suppressed (keyboard was shown or will be shown).
- (BOOL)handleSelectPressWithType:(blink::WebInputEvent::Type)type {
  const ui::mojom::TextInputState* state = [self editState];
  if (type == blink::WebInputEvent::Type::kKeyDown) {
    if (state && state->mode != ui::TextInputMode::TEXT_INPUT_MODE_NONE &&
        state->type != ui::TextInputType::TEXT_INPUT_TYPE_NONE) {
      _selectWillShowKeyboard = YES;
      return YES;
    }
    // Reset `_selectWillShowKeyboard` to NO. If a previous press was
    // cancelled, the flag could remain YES.
    _selectWillShowKeyboard = NO;
  } else if (type == blink::WebInputEvent::Type::kKeyUp) {
    if (_selectWillShowKeyboard) {
      _selectWillShowKeyboard = NO;
      if (state) {
        [self showKeyboard:*state];
      }
      return YES;
    }
  }
  return NO;
}

// Returns the set of unhanlded UIPress events to propagate to `super`.
- (NSSet<UIPress*>*)handlePresses:(NSSet<UIPress*>*)presses
                         withType:(blink::WebInputEvent::Type)type {
  NSMutableSet<UIPress*>* unhandled = [NSMutableSet set];
  for (UIPress* press in presses) {
    RemoteButton button = remoteButtonFromPressType(press.type);
    if (button == kNone) {
      // Since UIPress has key information from the physical keyboard,
      // NativeWebKeyboardEvent is built with it in `sendKeyboardEvent`.
      // If `press` is not handled in `sendKeyboardEvent`, it's
      // added in `unhandled`.
      if (![self sendKeyboardEvent:press eventType:type]) {
        [unhandled addObject:press];
      }
      continue;
    }
    if (button == kSelect && [self handleSelectPressWithType:type]) {
      continue;
    }
    if (![self sendKeyEventWithRemoteButton:button eventType:type]) {
      [unhandled addObject:press];
      continue;
    }
    if (press.type == UIPressTypeMenu) {
      // Pass `UIPressTypeMenu` to the framework to manage app suspension.
      [unhandled addObject:press];
    }
  }
  return unhandled;
}

- (void)pressesBegan:(NSSet<UIPress*>*)presses
           withEvent:(UIPressesEvent*)event {
  NSSet<UIPress*>* unhandled =
      [self handlePresses:presses
                 withType:blink::WebInputEvent::Type::kKeyDown];
  if (unhandled.count > 0) {
    [super pressesBegan:unhandled withEvent:event];
  }
}

- (void)pressesEnded:(NSSet<UIPress*>*)presses
           withEvent:(UIPressesEvent*)event {
  NSSet<UIPress*>* unhandled =
      [self handlePresses:presses withType:blink::WebInputEvent::Type::kKeyUp];
  if (unhandled.count > 0) {
    [super pressesEnded:presses withEvent:event];
  }
}

// Helper method to send the keyboard event.
- (BOOL)sendKeyboardEvent:(UIPress*)press
                eventType:(blink::WebInputEvent::Type)type {
  input::NativeWebKeyboardEvent native_event =
      input::NativeWebKeyboardEvent(base::apple::OwnedUIPress(press));
  if (!blink::WebInputEvent::IsKeyboardEventType(native_event.GetType())) {
    return NO;
  }
  if (native_event.dom_code == static_cast<uint32_t>(ui::DomCode::NONE)) {
    return NO;
  }
  _view->SendKeyEvent(native_event);
  return YES;
}

// Helper method to generate WebKeyboardEvent with RemoteButton.
- (BOOL)sendKeyEventWithRemoteButton:(RemoteButton)remoteButton
                           eventType:(blink::WebInputEvent::Type)type {
  blink::WebKeyboardEvent event(type, blink::WebInputEvent::kNoModifiers,
                                ui::EventTimeForNow());

  switch (remoteButton) {
    case kLeft:
      event.native_key_code = UIKeyboardHIDUsageKeyboardLeftArrow;
      event.dom_code = static_cast<int>(ui::DomCode::ARROW_LEFT);
      event.dom_key = ui::DomKey::ARROW_LEFT;
      event.windows_key_code = ui::VKEY_LEFT;
      break;
    case kRight:
      event.native_key_code = UIKeyboardHIDUsageKeyboardRightArrow;
      event.dom_code = static_cast<int>(ui::DomCode::ARROW_RIGHT);
      event.dom_key = ui::DomKey::ARROW_RIGHT;
      event.windows_key_code = ui::VKEY_RIGHT;
      break;
    case kUp:
      event.native_key_code = UIKeyboardHIDUsageKeyboardUpArrow;
      event.dom_code = static_cast<int>(ui::DomCode::ARROW_UP);
      event.dom_key = ui::DomKey::ARROW_UP;
      event.windows_key_code = ui::VKEY_UP;
      break;
    case kDown:
      event.native_key_code = UIKeyboardHIDUsageKeyboardDownArrow;
      event.dom_code = static_cast<int>(ui::DomCode::ARROW_DOWN);
      event.dom_key = ui::DomKey::ARROW_DOWN;
      event.windows_key_code = ui::VKEY_DOWN;
      break;
    case kMediaPlayPause:
      event.native_key_code = UIKeyboardHIDUsageKeyboardPause;
      event.dom_code = static_cast<int>(ui::DomCode::MEDIA_PLAY_PAUSE);
      event.dom_key = ui::DomKey::MEDIA_PLAY_PAUSE;
      event.windows_key_code = ui::VKEY_MEDIA_PLAY_PAUSE;
      break;
    case kSelect:
      event.native_key_code = UIKeyboardHIDUsageKeyboardReturnOrEnter;
      event.dom_code = static_cast<int>(ui::DomCode::ENTER);
      event.dom_key = ui::DomKey::ENTER;
      event.windows_key_code = ui::VKEY_RETURN;
      // Copied from components/input/web_input_event_builders_mac.mm's
      // WebKeyboardEventBuilder::Build().
      // This is necessary due to way some HTML elements process keyboard
      // activation (e.g. blink::HTMLElement::HandleKeyboardActivation()).
      event.text[0] = '\r';
      event.unmodified_text[0] = '\r';
      break;
    case kMenu:
      // Refer to https://support.apple.com/en-us/102337.
      // The menu button works to return to the previous screen.
      event.native_key_code = UIKeyboardHIDUsageKeyboardEscape;
      event.dom_code = static_cast<int>(ui::DomCode::ESCAPE);
      event.dom_key = ui::DomKey::ESCAPE;
      event.windows_key_code = ui::VKEY_ESCAPE;
      break;
    case kNone:
      return NO;
  }

  _view->SendKeyEvent(
      input::NativeWebKeyboardEvent(event, _view->GetNativeView()));
  return YES;
}

- (void)showKeyboard:(const ui::mojom::TextInputState&)state {
  if (_textFieldForAllTextInput) {
    return;
  }

  _textFieldForAllTextInput = [[UITextField alloc] init];
  _textFieldForAllTextInput.delegate = self;
  _textFieldForAllTextInput.keyboardType = keyboardTypeForInputType(state.type);
  if (state.value.has_value()) {
    _textFieldForAllTextInput.text =
        base::SysUTF16ToNSString(state.value.value());
  }
  if (state.type == ui::TextInputType::TEXT_INPUT_TYPE_PASSWORD) {
    _textFieldForAllTextInput.secureTextEntry = YES;
    // state.value.value() contains "\u2022" characters instead of actual text,
    // so it makes more sense to just start with an empty field to avoid the
    // case of a bogus value being set and used if the user just presses "done"
    // on the on-screen keyboard.
    _textFieldForAllTextInput.text = nil;
  }

  [self addSubview:_textFieldForAllTextInput];
  [_textFieldForAllTextInput becomeFirstResponder];
}

- (void)hideAndDeleteKeyboard {
  [_textFieldForAllTextInput removeFromSuperview];
  _textFieldForAllTextInput = nil;
}

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

#pragma mark - CALayerFrameSinkProvider

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

#pragma mark - NSObject

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

- (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];
  }
}

#pragma mark - UIAccessibilityElement

- (BOOL)isAccessibilityElement {
  return NO;
}

- (CGRect)accessibilityFrame {
  return CGRectZero;
}

#pragma mark - UIResponder

- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (BOOL)isFirstResponder {
  return
      [super isFirstResponder] || [_textFieldForAllTextInput isFirstResponder];
}

- (BOOL)becomeFirstResponder {
  CHECK(_view);
  const BOOL result = [super becomeFirstResponder];
  const BOOL keyboard_is_hidden =
      !_textFieldForAllTextInput || [_textFieldForAllTextInput isHidden];
  if (keyboard_is_hidden &&
      (result || _view->CanBecomeFirstResponderForTesting())) {
    _view->OnFirstResponderChanged();
  }
  return result;
}

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

#pragma mark - UITextFieldDelegate

- (void)textFieldDidEndEditing:(UITextField*)textField
                        reason:(UITextFieldDidEndEditingReason)reason {
  CHECK_EQ(textField, _textFieldForAllTextInput);
  if (reason == UITextFieldDidEndEditingReasonCommitted) {
    const ui::mojom::TextInputState* state = [self editState];
    const gfx::Range range = state ? gfx::Range(0, state->selection.GetMax())
                                   : gfx::Range::InvalidRange();
    _view->ImeCommitText(base::SysNSStringToUTF16(textField.text), range, 0);
  }

  [self hideAndDeleteKeyboard];
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
        (UIGestureRecognizer*)otherGestureRecognizer {
  return YES;
}

#pragma mark - UIView

- (BOOL)canBecomeFocused {
  return YES;
}

- (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);
}

@end
