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

#include "include/mouse_interpreter.h"

#include <math.h>

#include "include/logging.h"
#include "include/macros.h"
#include "include/tracer.h"

namespace gestures {

/*
 * The number of subdivisions that `REL_WHEEL_HI_RES` and `REL_HWHEEL_HI_RES`
 * have relative to `REL_WHEEL` and `REL_HWHEEL` respectively.
 */
const static int REL_WHEEL_HI_RES_UNITS_PER_NOTCH = 120;

// Default value for mouse scroll sensitivity.
const static int kMouseScrollSensitivityDefaultValue = 3;

MouseInterpreter::MouseInterpreter(PropRegistry* prop_reg, Tracer* tracer)
    : Interpreter(nullptr, tracer, false),
      wheel_emulation_accu_x_(0.0),
      wheel_emulation_accu_y_(0.0),
      wheel_emulation_active_(false),
      reverse_scrolling_(prop_reg, "Mouse Reverse Scrolling", false),
      scroll_acceleration_(prop_reg, "Mouse Scroll Acceleration", true),
      scroll_sensitivity_(prop_reg,"Mouse Scroll Sensitivity",
        kMouseScrollSensitivityDefaultValue),
      hi_res_scrolling_(prop_reg, "Mouse High Resolution Scrolling", true),
      scroll_velocity_buffer_size_(prop_reg, "Scroll Wheel Velocity Buffer", 3),
      scroll_accel_curve_prop_(prop_reg, "Mouse Scroll Accel Curve",
          scroll_accel_curve_, sizeof(scroll_accel_curve_) / sizeof(double)),
      scroll_max_allowed_input_speed_(prop_reg,
                                      "Mouse Scroll Max Input Speed",
                                      177.0),
      force_scroll_wheel_emulation_(prop_reg,
                                     "Force Scroll Wheel Emulation",
                                     false),
      scroll_wheel_emulation_speed_(prop_reg,
                                    "Scroll Wheel Emulation Speed",
                                    100.0),
      scroll_wheel_emulation_thresh_(prop_reg,
                                    "Scroll Wheel Emulation Threshold",
                                    1.0),
      output_mouse_wheel_gestures_(prop_reg,
                                   "Output Mouse Wheel Gestures", false) {
  InitName();
  memset(&prev_state_, 0, sizeof(prev_state_));
  // Scroll acceleration curve coefficients. See the definition for more
  // details on how to generate them.
  scroll_accel_curve_[0] = 1.0374e+01;
  scroll_accel_curve_[1] = 4.1773e-01;
  scroll_accel_curve_[2] = 2.5737e-02;
  scroll_accel_curve_[3] = 8.0428e-05;
  scroll_accel_curve_[4] = -9.1149e-07;
  scroll_max_allowed_input_speed_.SetDelegate(this);
}

void MouseInterpreter::SyncInterpretImpl(HardwareState& hwstate,
                                         stime_t* timeout) {
  const char name[] = "MouseInterpreter::SyncInterpretImpl";
  LogHardwareStatePre(name, hwstate);

  if(!EmulateScrollWheel(hwstate)) {
    // Interpret mouse events in the order of pointer moves, scroll wheels and
    // button clicks.
    InterpretMouseMotionEvent(prev_state_, hwstate);
    // Note that unlike touchpad scrolls, we interpret and send separate events
    // for horizontal/vertical mouse wheel scrolls. This is partly to match what
    // the xf86-input-evdev driver does and is partly because not all code in
    // Chrome honors MouseWheelEvent that has both X and Y offsets.
    InterpretScrollWheelEvent(hwstate, true);
    InterpretScrollWheelEvent(hwstate, false);
    InterpretMouseButtonEvent(prev_state_, hwstate);
  }
  // Pass max_finger_cnt = 0 to DeepCopy() since we don't care fingers and
  // did not allocate any space for fingers.
  prev_state_.DeepCopy(hwstate, 0);

  LogHardwareStatePost(name, hwstate);
}

double MouseInterpreter::ComputeScrollAccelFactor(double input_speed) {
  double result = 0.0;
  double term = 1.0;
  double allowed_speed = fabs(input_speed);
  if (allowed_speed > scroll_max_allowed_input_speed_.val_)
    allowed_speed = scroll_max_allowed_input_speed_.val_;

  // Compute the scroll acceleration factor.
  for (size_t i = 0; i < arraysize(scroll_accel_curve_); i++) {
    result += term * scroll_accel_curve_[i];
    term *= allowed_speed;
  }
  return result;
}

bool MouseInterpreter::EmulateScrollWheel(const HardwareState& hwstate) {
  const char name[] = "MouseInterpreter::EmulateScrollWheel";

  if (!force_scroll_wheel_emulation_.val_ && hwprops_->has_wheel)
    return false;
  bool down = hwstate.buttons_down & GESTURES_BUTTON_MIDDLE ||
              (hwstate.buttons_down & GESTURES_BUTTON_LEFT &&
               hwstate.buttons_down & GESTURES_BUTTON_RIGHT);
  bool prev_down = prev_state_.buttons_down & GESTURES_BUTTON_MIDDLE ||
                   (prev_state_.buttons_down & GESTURES_BUTTON_LEFT &&
                    prev_state_.buttons_down & GESTURES_BUTTON_RIGHT);
  bool raising = down && !prev_down;
  bool falling = !down && prev_down;

  // Reset scroll emulation detection on button down.
  if (raising) {
    wheel_emulation_accu_x_ = 0;
    wheel_emulation_accu_y_ = 0;
    wheel_emulation_active_ = false;
  }

  // Send button event if button has been released without scrolling.
  if (falling && !wheel_emulation_active_) {
    auto button_change = Gesture(kGestureButtonsChange,
                           prev_state_.timestamp,
                           hwstate.timestamp,
                           prev_state_.buttons_down,
                           prev_state_.buttons_down,
                           false); // is_tap
    LogGestureProduce(name, button_change);
    ProduceGesture(button_change);
  }

  if (down) {
    // Detect scroll emulation
    if (!wheel_emulation_active_) {
      wheel_emulation_accu_x_ += hwstate.rel_x;
      wheel_emulation_accu_y_ += hwstate.rel_y;
      double dist_sq = wheel_emulation_accu_x_ * wheel_emulation_accu_x_ +
                       wheel_emulation_accu_y_ * wheel_emulation_accu_y_;
      double thresh_sq = scroll_wheel_emulation_thresh_.val_ *
                         scroll_wheel_emulation_thresh_.val_;
      if (dist_sq > thresh_sq) {
        // Lock into scroll emulation until button is released.
        wheel_emulation_active_ = true;
      }
    }

    // Transform motion into scrolling.
    if (wheel_emulation_active_) {
      double scroll_x = hwstate.rel_x * scroll_wheel_emulation_speed_.val_;
      double scroll_y = hwstate.rel_y * scroll_wheel_emulation_speed_.val_;

      auto scroll = Gesture(kGestureScroll, hwstate.timestamp,
                            hwstate.timestamp, scroll_x, scroll_y);
      LogGestureProduce(name, scroll);
      ProduceGesture(scroll);
    }
    return true;
  }

  return false;
}

void MouseInterpreter::InterpretScrollWheelEvent(const HardwareState& hwstate,
                                                 bool is_vertical) {
  const char name[] = "MouseInterpreter::InterpretScrollWheelEvent";

  const size_t max_buffer_size = scroll_velocity_buffer_size_.val_;
  const float scroll_wheel_event_time_delta_min = 0.008 * max_buffer_size;
  bool use_high_resolution =
      is_vertical && hwprops_->wheel_is_hi_res
      && hi_res_scrolling_.val_;
  // Vertical wheel or horizontal wheel.
  WheelRecord current_wheel;
  current_wheel.timestamp = hwstate.timestamp;
  int ticks;
  std::vector<WheelRecord>* last_wheels;
  if (is_vertical) {
    // Only vertical high-res scrolling is supported for now.
    if (use_high_resolution) {
      current_wheel.change = hwstate.rel_wheel_hi_res
          / REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
      ticks = hwstate.rel_wheel_hi_res;
    } else {
      current_wheel.change = hwstate.rel_wheel;
      ticks = hwstate.rel_wheel * REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
    }
    last_wheels = &last_vertical_wheels_;
  } else {
    last_wheels = &last_horizontal_wheels_;
    current_wheel.change = hwstate.rel_hwheel;
    ticks = hwstate.rel_hwheel * REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
  }

  // Check if the wheel is scrolled.
  if (current_wheel.change) {
    stime_t start_time, end_time = hwstate.timestamp;
    // Check if this scroll is in same direction as previous scroll event.
    if (!last_wheels->empty() &&
        ((current_wheel.change < 0 && last_wheels->back().change < 0) ||
         (current_wheel.change > 0 && last_wheels->back().change > 0))) {
      start_time = last_wheels->begin()->timestamp;
    } else {
      last_wheels->clear();
      start_time = end_time;
    }

    // We will only accelerate scrolls if we have filled our buffer of scroll
    // events all in the same direction. If the buffer is full, then calculate
    // scroll velocity using the average velocity of the entire buffer.
    float velocity;
    if (last_wheels->size() < max_buffer_size) {
      velocity = 0.0;
    } else {
      stime_t dt = end_time - last_wheels->back().timestamp;
      if (dt < scroll_wheel_event_time_delta_min) {
        // The first packets received after BT wakeup may be delayed, causing
        // the time delta between that and the subsequent packets to be
        // artificially very small.
        // Prevent small time deltas from triggering large amounts of
        // acceleration by enforcing a minimum time delta.
        dt = scroll_wheel_event_time_delta_min;
      }

      last_wheels->pop_back();
      float buffer_scroll_distance = current_wheel.change;
      for (auto wheel : *last_wheels) {
        buffer_scroll_distance += wheel.change;
      }

      velocity = buffer_scroll_distance / dt;
    }
    last_wheels->insert(last_wheels->begin(), current_wheel);

    // When scroll acceleration is off, the scroll factor does not relate to
    // scroll velocity. It's simply a constant multiplier to the wheel value.
    const double unaccel_scroll_factors[] = { 20.0, 36.0, 72.0, 112.0, 164.0 };

    float offset = current_wheel.change * (
      scroll_acceleration_.val_?
      ComputeScrollAccelFactor(velocity) :
      unaccel_scroll_factors[scroll_sensitivity_.val_ - 1]);

    if (is_vertical) {
      // For historical reasons the vertical wheel (REL_WHEEL) is inverted
      if (!reverse_scrolling_.val_) {
        offset = -offset;
        ticks = -ticks;
      }
      auto scroll_wheel = CreateWheelGesture(start_time, end_time,
                                             0, offset, 0, ticks);
      LogGestureProduce(name, scroll_wheel);
      ProduceGesture(scroll_wheel);
    } else {
      auto scroll_wheel = CreateWheelGesture(start_time, end_time,
                                             offset, 0, ticks, 0);
      LogGestureProduce(name, scroll_wheel);
      ProduceGesture(scroll_wheel);
    }
  }
}

Gesture MouseInterpreter::CreateWheelGesture(
    stime_t start_time, stime_t end_time, float dx, float dy,
    int tick_120ths_dx, int tick_120ths_dy) {
  if (output_mouse_wheel_gestures_.val_) {
    return Gesture(kGestureMouseWheel, start_time, end_time, dx, dy,
                   tick_120ths_dx, tick_120ths_dy);
  } else {
    return Gesture(kGestureScroll, start_time, end_time, dx, dy);
  }
}

void MouseInterpreter::InterpretMouseButtonEvent(
    const HardwareState& prev_state, const HardwareState& hwstate) {
  const char name[] = "MouseInterpreter::InterpretMouseButtonEvent";

  const unsigned buttons[] = {
    GESTURES_BUTTON_LEFT,
    GESTURES_BUTTON_MIDDLE,
    GESTURES_BUTTON_RIGHT,
    GESTURES_BUTTON_BACK,
    GESTURES_BUTTON_FORWARD,
    GESTURES_BUTTON_SIDE,
    GESTURES_BUTTON_EXTRA,
  };
  unsigned down = 0, up = 0;

  for (unsigned i = 0; i < arraysize(buttons); i++) {
    if (!(prev_state.buttons_down & buttons[i]) &&
        (hwstate.buttons_down & buttons[i]))
      down |= buttons[i];
    if ((prev_state.buttons_down & buttons[i]) &&
        !(hwstate.buttons_down & buttons[i]))
      up |= buttons[i];
  }

  if (down || up) {
    auto button_change = Gesture(kGestureButtonsChange,
                                 prev_state.timestamp,
                                 hwstate.timestamp,
                                 down,
                                 up,
                                 false); // is_tap
    LogGestureProduce(name, button_change);
    ProduceGesture(button_change);
  }
}

void MouseInterpreter::InterpretMouseMotionEvent(
    const HardwareState& prev_state,
    const HardwareState& hwstate) {
  const char name[] = "MouseInterpreter::InterpretMouseMotionEvent";

  if (hwstate.rel_x || hwstate.rel_y) {
    auto move = Gesture(kGestureMove,
                        prev_state.timestamp,
                        hwstate.timestamp,
                        hwstate.rel_x,
                        hwstate.rel_y);
    LogGestureProduce(name, move);
    ProduceGesture(move);
  }
}

}  // namespace gestures
