// Copyright 2013 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/multitouch_mouse_interpreter.h"

#include <algorithm>

#include "include/tracer.h"
#include "include/util.h"

namespace gestures {

void Origin::PushGesture(const Gesture& result) {
  if (result.type == kGestureTypeButtonsChange) {
    if (result.details.buttons.up & GESTURES_BUTTON_LEFT)
      button_going_up_left_ = result.end_time;
    if (result.details.buttons.up & GESTURES_BUTTON_MIDDLE)
      button_going_up_middle_ = result.end_time;
    if (result.details.buttons.up & GESTURES_BUTTON_RIGHT)
      button_going_up_right_ = result.end_time;
  }
}

stime_t Origin::ButtonGoingUp(int button) const {
  if (button == GESTURES_BUTTON_LEFT)
    return button_going_up_left_;
  if (button == GESTURES_BUTTON_MIDDLE)
    return button_going_up_middle_;
  if (button == GESTURES_BUTTON_RIGHT)
    return button_going_up_right_;
  return 0;
}

MultitouchMouseInterpreter::MultitouchMouseInterpreter(
    PropRegistry* prop_reg,
    Tracer* tracer)
    : MouseInterpreter(prop_reg, tracer),
      state_buffer_(2),
      scroll_buffer_(15),
      prev_gesture_type_(kGestureTypeNull),
      current_gesture_type_(kGestureTypeNull),
      should_fling_(false),
      scroll_manager_(prop_reg),
      click_buffer_depth_(prop_reg, "Click Buffer Depth", 10),
      click_max_distance_(prop_reg, "Click Max Distance", 1.0),
      click_left_button_going_up_lead_time_(prop_reg,
          "Click Left Button Going Up Lead Time", 0.01),
      click_right_button_going_up_lead_time_(prop_reg,
          "Click Right Button Going Up Lead Time", 0.1),
      min_finger_move_distance_(prop_reg, "Minimum Mouse Finger Move Distance",
                                1.75),
      moving_min_rel_amount_(prop_reg, "Moving Min Rel Magnitude", 0.1) {
  InitName();
  memset(&prev_state_, 0, sizeof(prev_state_));
}

void MultitouchMouseInterpreter::ProduceGesture(const Gesture& gesture) {
  origin_.PushGesture(gesture);
  MouseInterpreter::ProduceGesture(gesture);
}

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

  if (!state_buffer_.Get(0).fingers) {
    Err("Must call SetHardwareProperties() before interpreting anything.");
    return;
  }

  // Should we remove all fingers from our structures, or just removed ones?
  if ((hwstate.rel_x * hwstate.rel_x + hwstate.rel_y * hwstate.rel_y) >
      moving_min_rel_amount_.val_ * moving_min_rel_amount_.val_) {
    start_position_.clear();
    moving_.clear();
    should_fling_ = false;
  } else {
    RemoveMissingIdsFromMap(&start_position_, hwstate);
    RemoveMissingIdsFromSet(&moving_, hwstate);
  }

  // Set start positions/moving
  for (size_t i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    if (MapContainsKey(start_position_, fs.tracking_id)) {
      // Is moving?
      if (!SetContainsValue(moving_, fs.tracking_id) &&  // not already moving &
          start_position_[fs.tracking_id].Sub(Vector2(fs)).MagSq() >=  // moving
          min_finger_move_distance_.val_ * min_finger_move_distance_.val_) {
        moving_.insert(fs.tracking_id);
      }
      continue;
    }
    start_position_[fs.tracking_id] = Vector2(fs);
  }

  // Mark all non-moving fingers as unable to cause scroll
  for (size_t i = 0; i < hwstate.finger_cnt; i++) {
    FingerState* fs = &hwstate.fingers[i];
    if (!SetContainsValue(moving_, fs->tracking_id))
      fs->flags |=
          GESTURES_FINGER_WARP_X_NON_MOVE | GESTURES_FINGER_WARP_Y_NON_MOVE;
  }

  // Record current HardwareState now.
  state_buffer_.PushState(hwstate);

  // TODO(clchiou): Remove palm and thumb.
  gs_fingers_.clear();
  size_t num_fingers = std::min(kMaxGesturingFingers,
                                (size_t)state_buffer_.Get(0).finger_cnt);
  const FingerState* fs = state_buffer_.Get(0).fingers;
  for (size_t i = 0; i < num_fingers; i++)
    gs_fingers_.insert(fs[i].tracking_id);

  InterpretScrollWheelEvent(hwstate, true);
  InterpretScrollWheelEvent(hwstate, false);
  InterpretMouseButtonEvent(prev_state_, state_buffer_.Get(0));
  InterpretMouseMotionEvent(prev_state_, state_buffer_.Get(0));

  bool should_interpret_multitouch = true;

  // Some mice (Logitech) will interleave finger data and rel data, which can
  // make finger tracking tricky. To avoid problems, if this current frame
  // was rel data, and the previous finger data exactly matches this finger
  // data, we remove the last hardware state from our buffer. This is okay
  // because we already processed the rel data.
  const HardwareState& prev_hs = state_buffer_.Get(1);
  const HardwareState& cur_hs = state_buffer_.Get(0);
  bool cur_has_rel = cur_hs.rel_x || cur_hs.rel_y ||
      cur_hs.rel_wheel || cur_hs.rel_hwheel;
  bool different_fingers = prev_hs.touch_cnt != cur_hs.touch_cnt ||
      prev_hs.finger_cnt != cur_hs.finger_cnt;
  if (!different_fingers && cur_has_rel) {
    // Compare actual fingers themselves
    for (size_t i = 0; i < cur_hs.finger_cnt; i++) {
      if (!cur_hs.fingers[i].NonFlagsEquals(prev_hs.fingers[i])) {
        different_fingers = true;
        break;
      }
    }
    if (!different_fingers) {
      state_buffer_.PopState();
      should_interpret_multitouch = false;
    }
  }

  if (should_interpret_multitouch)
    InterpretMultitouchEvent();

  // We don't keep finger data here, this is just for standard mouse:
  prev_state_ = hwstate;
  prev_state_.fingers = nullptr;
  prev_state_.finger_cnt = 0;

  prev_gs_fingers_ = gs_fingers_;
  prev_gesture_type_ = current_gesture_type_;

  LogHardwareStatePost(name, hwstate);
}

void MultitouchMouseInterpreter::Initialize(
    const HardwareProperties* hw_props,
    Metrics* metrics,
    MetricsProperties* mprops,
    GestureConsumer* consumer) {
  Interpreter::Initialize(hw_props, metrics, mprops, consumer);
  state_buffer_.Reset(hw_props->max_finger_cnt);
}

void MultitouchMouseInterpreter::InterpretMultitouchEvent() {
  const char name[] = "MultitouchMouseInterpreter::InterpretMultitouchEvent";

  Gesture result;

  // If a gesturing finger just left, do fling/lift
  if (should_fling_ && AnyGesturingFingerLeft(state_buffer_.Get(0),
                                              prev_gs_fingers_)) {
    current_gesture_type_ = kGestureTypeFling;
    scroll_manager_.FillResultFling(state_buffer_, scroll_buffer_, &result);
    if (result.type == kGestureTypeFling)
      result.details.fling.vx = 0.0;
    if (result.details.fling.vy == 0.0)
      result.type = kGestureTypeNull;
    should_fling_ = false;
  } else if (gs_fingers_.size() > 0) {
    // In general, finger movements are interpreted as scroll, but as
    // clicks and scrolls on multi-touch mice are both single-finger
    // gesture, we have to recognize and separate clicks from scrolls,
    // when a user is actually clicking.
    //
    // This is how we do for now: We look for characteristic patterns of
    // clicks, and if we find one, we hold off emitting scroll gesture for
    // a few time frames to prevent premature scrolls.
    //
    // The patterns we look for:
    // * Small finger movements when button is down
    // * Finger movements after button goes up

    bool update_scroll_buffer =
        scroll_manager_.FillResultScroll(state_buffer_,
                                      prev_gs_fingers_,
                                      gs_fingers_,
                                      prev_gesture_type_,
                                      prev_result_,
                                      &result,
                                      &scroll_buffer_);
    current_gesture_type_ = result.type;
    if (current_gesture_type_ == kGestureTypeScroll)
      should_fling_ = true;

    bool hold_off_scroll = false;
    const HardwareState& state = state_buffer_.Get(0);
    // Check small finger movements when button is down
    if (state.buttons_down) {
      float dist_sq, dt;
      scroll_buffer_.GetSpeedSq(click_buffer_depth_.val_, &dist_sq, &dt);
      if (dist_sq < click_max_distance_.val_ * click_max_distance_.val_)
        hold_off_scroll = true;
    }
    // Check button going up lead time
    stime_t now = state.timestamp;
    stime_t button_left_age =
        now - origin_.ButtonGoingUp(GESTURES_BUTTON_LEFT);
    stime_t button_right_age =
        now - origin_.ButtonGoingUp(GESTURES_BUTTON_RIGHT);
    hold_off_scroll = hold_off_scroll ||
        (button_left_age < click_left_button_going_up_lead_time_.val_) ||
        (button_right_age < click_right_button_going_up_lead_time_.val_);

    if (hold_off_scroll && result.type == kGestureTypeScroll) {
      current_gesture_type_ = kGestureTypeNull;
      result.type = kGestureTypeNull;
    }
    if (current_gesture_type_ == kGestureTypeScroll &&
        !update_scroll_buffer) {
      return;
    }
  }
  scroll_manager_.UpdateScrollEventBuffer(current_gesture_type_,
                                          &scroll_buffer_);
  if (result.type != kGestureTypeNull) {
    LogGestureProduce(name, result);
    ProduceGesture(result);
  }
  prev_result_ = result;
}

}  // namespace gestures
