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

#include "content/browser/devtools/protocol/input_handler.h"

#include <stddef.h>

#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "cc/output/compositor_frame_metadata.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/common/input/synthetic_pinch_gesture_params.h"
#include "content/common/input/synthetic_smooth_scroll_gesture_params.h"
#include "content/common/input/synthetic_tap_gesture_params.h"
#include "third_party/WebKit/public/platform/WebInputEvent.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/geometry/point.h"

namespace content {
namespace devtools {
namespace input {

namespace {

gfx::PointF CssPixelsToPointF(int x, int y, float page_scale_factor) {
  return gfx::PointF(x * page_scale_factor, y * page_scale_factor);
}

gfx::Vector2dF CssPixelsToVector2dF(int x, int y, float page_scale_factor) {
  return gfx::Vector2dF(x * page_scale_factor, y * page_scale_factor);
}

bool StringToGestureSourceType(const std::string& in,
                               SyntheticGestureParams::GestureSourceType& out) {
  if (in == kGestureSourceTypeDefault) {
    out = SyntheticGestureParams::GestureSourceType::DEFAULT_INPUT;
    return true;
  } else if (in == kGestureSourceTypeTouch) {
    out = SyntheticGestureParams::GestureSourceType::TOUCH_INPUT;
    return true;
  } else if (in == kGestureSourceTypeMouse) {
    out = SyntheticGestureParams::GestureSourceType::MOUSE_INPUT;
    return true;
  } else {
    return false;
  }
}

}

typedef DevToolsProtocolClient::Response Response;

namespace {

void SetEventModifiers(blink::WebInputEvent* event, const int* modifiers) {
  if (!modifiers)
    return;
  if (*modifiers & 1)
    event->modifiers |= blink::WebInputEvent::AltKey;
  if (*modifiers & 2)
    event->modifiers |= blink::WebInputEvent::ControlKey;
  if (*modifiers & 4)
    event->modifiers |= blink::WebInputEvent::MetaKey;
  if (*modifiers & 8)
    event->modifiers |= blink::WebInputEvent::ShiftKey;
}

void SetEventTimestamp(blink::WebInputEvent* event, const double* timestamp) {
  // Convert timestamp, in seconds since unix epoch, to an event timestamp
  // which is time ticks since platform start time.
  base::TimeTicks ticks = timestamp
                              ? base::TimeDelta::FromSecondsD(*timestamp) +
                                    base::TimeTicks::UnixEpoch()
                              : base::TimeTicks::Now();
  event->timeStampSeconds = (ticks - base::TimeTicks()).InSecondsF();
}

bool SetKeyboardEventText(blink::WebUChar* to, const std::string* from) {
  if (!from)
    return true;

  base::string16 text16 = base::UTF8ToUTF16(*from);
  if (text16.size() > blink::WebKeyboardEvent::textLengthCap)
    return false;

  for (size_t i = 0; i < text16.size(); ++i)
    to[i] = text16[i];
  return true;
}

bool SetMouseEventButton(blink::WebMouseEvent* event,
                         const std::string* button) {
  if (!button)
    return true;

  if (*button == dispatch_mouse_event::kButtonNone) {
    event->button = blink::WebMouseEvent::Button::NoButton;
  } else if (*button == dispatch_mouse_event::kButtonLeft) {
    event->button = blink::WebMouseEvent::Button::Left;
    event->modifiers |= blink::WebInputEvent::LeftButtonDown;
  } else if (*button == dispatch_mouse_event::kButtonMiddle) {
    event->button = blink::WebMouseEvent::Button::Middle;
    event->modifiers |= blink::WebInputEvent::MiddleButtonDown;
  } else if (*button == dispatch_mouse_event::kButtonRight) {
    event->button = blink::WebMouseEvent::Button::Right;
    event->modifiers |= blink::WebInputEvent::RightButtonDown;
  } else {
    return false;
  }
  return true;
}

bool SetMouseEventType(blink::WebMouseEvent* event, const std::string& type) {
  if (type == dispatch_mouse_event::kTypeMousePressed) {
    event->type = blink::WebInputEvent::MouseDown;
  } else if (type == dispatch_mouse_event::kTypeMouseReleased) {
    event->type = blink::WebInputEvent::MouseUp;
  } else if (type == dispatch_mouse_event::kTypeMouseMoved) {
    event->type = blink::WebInputEvent::MouseMove;
  } else {
    return false;
  }
  return true;
}

}  // namespace

InputHandler::InputHandler()
    : host_(NULL),
      page_scale_factor_(1.0),
      weak_factory_(this) {
}

InputHandler::~InputHandler() {
}

void InputHandler::SetRenderWidgetHost(RenderWidgetHostImpl* host) {
  host_ = host;
}

void InputHandler::SetClient(std::unique_ptr<Client> client) {
  client_.swap(client);
}

void InputHandler::OnSwapCompositorFrame(
    const cc::CompositorFrameMetadata& frame_metadata) {
  page_scale_factor_ = frame_metadata.page_scale_factor;
  scrollable_viewport_size_ = frame_metadata.scrollable_viewport_size;
}

Response InputHandler::DispatchKeyEvent(
    const std::string& type,
    const int* modifiers,
    const double* timestamp,
    const std::string* text,
    const std::string* unmodified_text,
    const std::string* key_identifier,
    const std::string* code,
    const std::string* key,
    const int* windows_virtual_key_code,
    const int* native_virtual_key_code,
    const bool* auto_repeat,
    const bool* is_keypad,
    const bool* is_system_key) {
  NativeWebKeyboardEvent event;
  event.skip_in_browser = true;

  if (type == dispatch_key_event::kTypeKeyDown) {
    event.type = blink::WebInputEvent::KeyDown;
  } else if (type == dispatch_key_event::kTypeKeyUp) {
    event.type = blink::WebInputEvent::KeyUp;
  } else if (type == dispatch_key_event::kTypeChar) {
    event.type = blink::WebInputEvent::Char;
  } else if (type == dispatch_key_event::kTypeRawKeyDown) {
    event.type = blink::WebInputEvent::RawKeyDown;
  } else {
    return Response::InvalidParams(
        base::StringPrintf("Unexpected event type '%s'", type.c_str()));
  }

  SetEventModifiers(&event, modifiers);
  SetEventTimestamp(&event, timestamp);
  if (!SetKeyboardEventText(event.text, text))
    return Response::InvalidParams("Invalid 'text' parameter");
  if (!SetKeyboardEventText(event.unmodifiedText, unmodified_text))
    return Response::InvalidParams("Invalid 'unmodifiedText' parameter");

  if (windows_virtual_key_code)
    event.windowsKeyCode = *windows_virtual_key_code;
  if (native_virtual_key_code)
    event.nativeKeyCode = *native_virtual_key_code;
  if (auto_repeat && *auto_repeat)
    event.modifiers |= blink::WebInputEvent::IsAutoRepeat;
  if (is_keypad && *is_keypad)
    event.modifiers |= blink::WebInputEvent::IsKeyPad;
  if (is_system_key)
    event.isSystemKey = *is_system_key;

  if (code) {
    event.domCode = static_cast<int>(
        ui::KeycodeConverter::CodeStringToDomCode(*code));
  }

  if (key) {
    event.domKey = static_cast<int>(
        ui::KeycodeConverter::KeyStringToDomKey(*key));
  }

  if (!host_)
    return Response::ServerError("Could not connect to view");

  host_->Focus();
  host_->ForwardKeyboardEvent(event);
  return Response::OK();
}

Response InputHandler::DispatchMouseEvent(
    const std::string& type,
    int x,
    int y,
    const int* modifiers,
    const double* timestamp,
    const std::string* button,
    const int* click_count) {
  blink::WebMouseEvent event;

  if (!SetMouseEventType(&event, type)) {
    return Response::InvalidParams(
        base::StringPrintf("Unexpected event type '%s'", type.c_str()));
  }
  SetEventModifiers(&event, modifiers);
  SetEventTimestamp(&event, timestamp);
  if (!SetMouseEventButton(&event, button))
    return Response::InvalidParams("Invalid mouse button");

  event.x = x * page_scale_factor_;
  event.y = y * page_scale_factor_;
  event.windowX = x * page_scale_factor_;
  event.windowY = y * page_scale_factor_;
  event.globalX = x * page_scale_factor_;
  event.globalY = y * page_scale_factor_;
  event.clickCount = click_count ? *click_count : 0;
  event.pointerType = blink::WebPointerProperties::PointerType::Mouse;

  if (!host_)
    return Response::ServerError("Could not connect to view");

  host_->Focus();
  host_->ForwardMouseEvent(event);
  return Response::OK();
}

Response InputHandler::EmulateTouchFromMouseEvent(const std::string& type,
                                                  int x,
                                                  int y,
                                                  double timestamp,
                                                  const std::string& button,
                                                  double* delta_x,
                                                  double* delta_y,
                                                  int* modifiers,
                                                  int* click_count) {
  blink::WebMouseWheelEvent wheel_event;
  blink::WebMouseEvent mouse_event;
  blink::WebMouseEvent* event = &mouse_event;

  if (type == emulate_touch_from_mouse_event::kTypeMouseWheel) {
    if (!delta_x || !delta_y) {
      return Response::InvalidParams(
          "'deltaX' and 'deltaY' are expected for mouseWheel event");
    }
    wheel_event.deltaX = static_cast<float>(*delta_x);
    wheel_event.deltaY = static_cast<float>(*delta_y);
    event = &wheel_event;
    event->type = blink::WebInputEvent::MouseWheel;
  } else if (!SetMouseEventType(event, type)) {
    return Response::InvalidParams(
        base::StringPrintf("Unexpected event type '%s'", type.c_str()));
  }

  SetEventModifiers(event, modifiers);
  SetEventTimestamp(event, &timestamp);
  if (!SetMouseEventButton(event, &button))
    return Response::InvalidParams("Invalid mouse button");

  event->x = x;
  event->y = y;
  event->windowX = x;
  event->windowY = y;
  event->globalX = x;
  event->globalY = y;
  event->clickCount = click_count ? *click_count : 0;
  event->pointerType = blink::WebPointerProperties::PointerType::Touch;

  if (!host_)
    return Response::ServerError("Could not connect to view");

  if (event->type == blink::WebInputEvent::MouseWheel)
    host_->ForwardWheelEvent(wheel_event);
  else
    host_->ForwardMouseEvent(mouse_event);
  return Response::OK();
}

Response InputHandler::SynthesizePinchGesture(
    DevToolsCommandId command_id,
    int x,
    int y,
    double scale_factor,
    const int* relative_speed,
    const std::string* gesture_source_type) {
  if (!host_)
    return Response::ServerError("Could not connect to view");

  SyntheticPinchGestureParams gesture_params;
  const int kDefaultRelativeSpeed = 800;

  gesture_params.scale_factor = scale_factor;
  gesture_params.anchor = CssPixelsToPointF(x, y, page_scale_factor_);
  gesture_params.relative_pointer_speed_in_pixels_s =
      relative_speed ? *relative_speed : kDefaultRelativeSpeed;

  if (!StringToGestureSourceType(
      gesture_source_type ? *gesture_source_type : kGestureSourceTypeDefault,
      gesture_params.gesture_source_type)) {
    return Response::InvalidParams("gestureSourceType");
  }

  host_->QueueSyntheticGesture(
      SyntheticGesture::Create(gesture_params),
      base::Bind(&InputHandler::SendSynthesizePinchGestureResponse,
                 weak_factory_.GetWeakPtr(), command_id));

  return Response::OK();
}

Response InputHandler::SynthesizeScrollGesture(
    DevToolsCommandId command_id,
    int x,
    int y,
    const int* x_distance,
    const int* y_distance,
    const int* x_overscroll,
    const int* y_overscroll,
    const bool* prevent_fling,
    const int* speed,
    const std::string* gesture_source_type,
    const int* repeat_count,
    const int* repeat_delay_ms,
    const std::string* interaction_marker_name) {
  if (!host_)
    return Response::ServerError("Could not connect to view");

  SyntheticSmoothScrollGestureParams gesture_params;
  const bool kDefaultPreventFling = true;
  const int kDefaultSpeed = 800;

  gesture_params.anchor = CssPixelsToPointF(x, y, page_scale_factor_);
  gesture_params.prevent_fling =
      prevent_fling ? *prevent_fling : kDefaultPreventFling;
  gesture_params.speed_in_pixels_s = speed ? *speed : kDefaultSpeed;

  if (x_distance || y_distance) {
    gesture_params.distances.push_back(
        CssPixelsToVector2dF(x_distance ? *x_distance : 0,
                             y_distance ? *y_distance : 0, page_scale_factor_));
  }

  if (x_overscroll || y_overscroll) {
    gesture_params.distances.push_back(CssPixelsToVector2dF(
        x_overscroll ? -*x_overscroll : 0, y_overscroll ? -*y_overscroll : 0,
        page_scale_factor_));
  }

  if (!StringToGestureSourceType(
      gesture_source_type ? *gesture_source_type : kGestureSourceTypeDefault,
      gesture_params.gesture_source_type)) {
    return Response::InvalidParams("gestureSourceType");
  }

  SynthesizeRepeatingScroll(
      gesture_params, repeat_count ? *repeat_count : 0,
      base::TimeDelta::FromMilliseconds(repeat_delay_ms ? *repeat_delay_ms
                                                        : 250),
      interaction_marker_name ? *interaction_marker_name : "", command_id);

  return Response::OK();
}

void InputHandler::SynthesizeRepeatingScroll(
    SyntheticSmoothScrollGestureParams gesture_params,
    int repeat_count,
    base::TimeDelta repeat_delay,
    std::string interaction_marker_name,
    DevToolsCommandId command_id) {
  if (!interaction_marker_name.empty()) {
    // TODO(alexclarke): Can we move this elsewhere? It doesn't really fit here.
    TRACE_EVENT_COPY_ASYNC_BEGIN0("benchmark", interaction_marker_name.c_str(),
                                  command_id.call_id);
  }

  host_->QueueSyntheticGesture(
      SyntheticGesture::Create(gesture_params),
      base::Bind(&InputHandler::OnScrollFinished, weak_factory_.GetWeakPtr(),
                 gesture_params, repeat_count, repeat_delay,
                 interaction_marker_name, command_id));
}

void InputHandler::OnScrollFinished(
    SyntheticSmoothScrollGestureParams gesture_params,
    int repeat_count,
    base::TimeDelta repeat_delay,
    std::string interaction_marker_name,
    DevToolsCommandId command_id,
    SyntheticGesture::Result result) {
  if (!interaction_marker_name.empty()) {
    TRACE_EVENT_COPY_ASYNC_END0("benchmark", interaction_marker_name.c_str(),
                                command_id.call_id);
  }

  if (repeat_count > 0) {
    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&InputHandler::SynthesizeRepeatingScroll,
                   weak_factory_.GetWeakPtr(), gesture_params, repeat_count - 1,
                   repeat_delay, interaction_marker_name, command_id),
        repeat_delay);
  } else {
    SendSynthesizeScrollGestureResponse(command_id, result);
  }
}

Response InputHandler::SynthesizeTapGesture(
    DevToolsCommandId command_id,
    int x,
    int y,
    const int* duration,
    const int* tap_count,
    const std::string* gesture_source_type) {
  if (!host_)
    return Response::ServerError("Could not connect to view");

  SyntheticTapGestureParams gesture_params;
  const int kDefaultDuration = 50;
  const int kDefaultTapCount = 1;

  gesture_params.position = CssPixelsToPointF(x, y, page_scale_factor_);
  gesture_params.duration_ms = duration ? *duration : kDefaultDuration;

  if (!StringToGestureSourceType(
      gesture_source_type ? *gesture_source_type : kGestureSourceTypeDefault,
      gesture_params.gesture_source_type)) {
    return Response::InvalidParams("gestureSourceType");
  }

  if (!tap_count)
    tap_count = &kDefaultTapCount;

  for (int i = 0; i < *tap_count; i++) {
    // If we're doing more than one tap, don't send the response to the client
    // until we've completed the last tap.
    bool is_last_tap = i == *tap_count - 1;
    host_->QueueSyntheticGesture(
        SyntheticGesture::Create(gesture_params),
        base::Bind(&InputHandler::SendSynthesizeTapGestureResponse,
                   weak_factory_.GetWeakPtr(), command_id, is_last_tap));
  }

  return Response::OK();
}

Response InputHandler::DispatchTouchEvent(
    const std::string& type,
    const std::vector<std::unique_ptr<base::DictionaryValue>>& touch_points,
    const int* modifiers,
    const double* timestamp) {
  return Response::FallThrough();
}

void InputHandler::SendSynthesizePinchGestureResponse(
    DevToolsCommandId command_id,
    SyntheticGesture::Result result) {
  if (result == SyntheticGesture::Result::GESTURE_FINISHED) {
    client_->SendSynthesizePinchGestureResponse(
        command_id, SynthesizePinchGestureResponse::Create());
  } else {
    client_->SendError(command_id,
                       Response::InternalError(base::StringPrintf(
                           "Synthetic pinch failed, result was %d", result)));
  }
}

void InputHandler::SendSynthesizeScrollGestureResponse(
    DevToolsCommandId command_id,
    SyntheticGesture::Result result) {
  if (result == SyntheticGesture::Result::GESTURE_FINISHED) {
    client_->SendSynthesizeScrollGestureResponse(
        command_id, SynthesizeScrollGestureResponse::Create());
  } else {
    client_->SendError(command_id,
                       Response::InternalError(base::StringPrintf(
                           "Synthetic scroll failed, result was %d", result)));
  }
}

void InputHandler::SendSynthesizeTapGestureResponse(
    DevToolsCommandId command_id,
    bool send_success,
    SyntheticGesture::Result result) {
  if (result == SyntheticGesture::Result::GESTURE_FINISHED) {
    if (send_success) {
      client_->SendSynthesizeTapGestureResponse(
          command_id, SynthesizeTapGestureResponse::Create());
    }
  } else {
    client_->SendError(command_id,
                       Response::InternalError(base::StringPrintf(
                           "Synthetic tap failed, result was %d", result)));
  }
}

}  // namespace input
}  // namespace devtools
}  // namespace content
