// Copyright 2015 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/input/synthetic_gesture_target_mac.h"

#include "components/input/render_widget_host_input_event_router.h"
#import "content/app_shim_remote_cocoa/render_widget_host_view_cocoa.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/cocoa/cocoa_event_utils.h"
#include "ui/events/gesture_detection/gesture_configuration.h"

// Unlike some event APIs, Apple does not provide a way to programmatically
// build a zoom event. To work around this, we leverage ObjectiveC's flexible
// typing and build up an object with the right interface to provide a zoom
// event.
@interface SyntheticPinchEvent : NSObject

// Populated based on desired zoom level.
@property CGFloat magnification;
@property NSPoint locationInWindow;
@property NSEventType type;
@property NSTimeInterval timestamp;
@property NSEventPhase phase;

// Filled with default values.
@property(readonly) CGFloat deltaX;
@property(readonly) CGFloat deltaY;
@property(readonly) NSEventModifierFlags modifierFlags;

@end

@implementation SyntheticPinchEvent

@synthesize magnification = _magnification;
@synthesize locationInWindow = _locationInWindow;
@synthesize type = _type;
@synthesize timestamp = _timestamp;
@synthesize phase = _phase;
@synthesize deltaX = _deltaX;
@synthesize deltaY = _deltaY;
@synthesize modifierFlags = _modifierFlags;

- (instancetype)initWithMagnification:(float)magnification
                     locationInWindow:(NSPoint)location
                            timestamp:(NSTimeInterval)timestamp {
  if (self = [super init]) {
    _type = NSEventTypeMagnify;
    _phase = NSEventPhaseChanged;
    _magnification = magnification;
    _locationInWindow = location;
    _timestamp = timestamp;

    _deltaX = 0;
    _deltaY = 0;
    _modifierFlags = 0;
  }

  return self;
}

+ (instancetype)eventWithMagnification:(float)magnification
                      locationInWindow:(NSPoint)location
                             timestamp:(NSTimeInterval)timestamp
                                 phase:(NSEventPhase)phase {
  SyntheticPinchEvent* event =
      [[SyntheticPinchEvent alloc] initWithMagnification:magnification
                                        locationInWindow:location
                                               timestamp:timestamp];
  event.phase = phase;
  return event;
}

@end

using blink::WebInputEvent;
using blink::WebGestureEvent;

namespace content {

SyntheticGestureTargetMac::SyntheticGestureTargetMac(
    RenderWidgetHostImpl* host,
    RenderWidgetHostViewCocoa* cocoa_view)
    : SyntheticGestureTargetBase(host), cocoa_view_(cocoa_view) {}

void SyntheticGestureTargetMac::DispatchWebGestureEventToPlatform(
    const WebGestureEvent& web_gesture,
    const ui::LatencyInfo& latency_info) {
  // Create an autorelease pool so that we clean up any synthetic events we
  // generate.
  @autoreleasepool {
    NSPoint content_local = NSMakePoint(
        web_gesture.PositionInWidget().x(),
        cocoa_view_.frame.size.height - web_gesture.PositionInWidget().y());
    NSPoint location_in_window = [cocoa_view_ convertPoint:content_local
                                                    toView:nil];
    NSTimeInterval timestamp =
        ui::EventTimeStampToSeconds(web_gesture.TimeStamp());

    switch (web_gesture.GetType()) {
      case WebInputEvent::Type::kGesturePinchBegin: {
        id cocoa_event =
            [SyntheticPinchEvent eventWithMagnification:0.0f
                                       locationInWindow:location_in_window
                                              timestamp:timestamp
                                                  phase:NSEventPhaseBegan];
        [cocoa_view_ magnifyWithEvent:cocoa_event isSyntheticallyInjected:YES];
        return;
      }
      case WebInputEvent::Type::kGesturePinchEnd: {
        id cocoa_event =
            [SyntheticPinchEvent eventWithMagnification:0.0f
                                       locationInWindow:location_in_window
                                              timestamp:timestamp
                                                  phase:NSEventPhaseEnded];
        [cocoa_view_ magnifyWithEvent:cocoa_event isSyntheticallyInjected:YES];
        return;
      }
      case WebInputEvent::Type::kGesturePinchUpdate: {
        id cocoa_event = [SyntheticPinchEvent
            eventWithMagnification:web_gesture.data.pinch_update.scale - 1.0f
                  locationInWindow:location_in_window
                         timestamp:timestamp
                             phase:NSEventPhaseChanged];
        [cocoa_view_ magnifyWithEvent:cocoa_event isSyntheticallyInjected:YES];
        return;
      }
      default:
        NOTREACHED();
    }
  }
}

void SyntheticGestureTargetMac::DispatchWebTouchEventToPlatform(
    const blink::WebTouchEvent& web_touch,
    const ui::LatencyInfo& latency_info) {
  GetView()->InjectTouchEvent(web_touch, latency_info);
}

void SyntheticGestureTargetMac::DispatchWebMouseWheelEventToPlatform(
    const blink::WebMouseWheelEvent& web_wheel,
    const ui::LatencyInfo& latency_info) {
  blink::WebMouseWheelEvent wheel_event = web_wheel;
  wheel_event.wheel_ticks_x =
      web_wheel.delta_x / ui::kScrollbarPixelsPerCocoaTick;
  wheel_event.wheel_ticks_y =
      web_wheel.delta_y / ui::kScrollbarPixelsPerCocoaTick;

  // Manually route the WebMouseWheelEvent to any open popup window if the
  // mouse is currently over the pop window, because window-level event routing
  // on Mac happens at the OS API level which we cannot easily inject the
  // events into.
  if (GetView()->PopupChildHostView() &&
      PointIsWithinContents(GetView()->PopupChildHostView(),
                            web_wheel.PositionInWidget())) {
    GetView()->PopupChildHostView()->RouteOrProcessWheelEvent(wheel_event);
  } else {
    GetView()->RouteOrProcessWheelEvent(wheel_event);
  }
  if (web_wheel.phase == blink::WebMouseWheelEvent::kPhaseEnded) {
    // Send the pending wheel end event immediately. Otherwise, the
    // MouseWheelPhaseHandler will defer the end event in case of momentum
    // scrolling. We want the end event sent before resolving the completion
    // callback.
    GetView()->GetMouseWheelPhaseHandler()->DispatchPendingWheelEndEvent();
  }
}

void SyntheticGestureTargetMac::DispatchWebMouseEventToPlatform(
    const blink::WebMouseEvent& web_mouse,
    const ui::LatencyInfo& latency_info) {
  GetView()->RouteOrProcessMouseEvent(web_mouse);
}

content::mojom::GestureSourceType
SyntheticGestureTargetMac::GetDefaultSyntheticGestureSourceType() const {
  return content::mojom::GestureSourceType::kMouseInput;
}

float SyntheticGestureTargetMac::GetTouchSlopInDips() const {
  return ui::GestureConfiguration::GetInstance()
      ->max_touch_move_in_pixels_for_click();
}

float SyntheticGestureTargetMac::GetSpanSlopInDips() const {
  return ui::GestureConfiguration::GetInstance()->span_slop();
}

float SyntheticGestureTargetMac::GetMinScalingSpanInDips() const {
  return ui::GestureConfiguration::GetInstance()->min_scaling_span_in_pixels();
}

RenderWidgetHostViewMac* SyntheticGestureTargetMac::GetView() const {
  auto* view =
      static_cast<RenderWidgetHostViewMac*>(render_widget_host()->GetView());
  CHECK(view, base::NotFatalUntil::M152);
  return view;
}

bool SyntheticGestureTargetMac::PointIsWithinContents(
    RenderWidgetHostView* view,
    const gfx::PointF& point) {
  gfx::Rect bounds = view->GetViewBounds();
  gfx::Rect bounds_in_window =
      bounds - bounds.OffsetFromOrigin();  // Translate the bounds to (0,0).
  return bounds_in_window.Contains(point.x(), point.y());
}

}  // namespace content
