// 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/palm_classifying_filter_interpreter.h"

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

namespace gestures {

PalmClassifyingFilterInterpreter::PalmClassifyingFilterInterpreter(
    PropRegistry* prop_reg, Interpreter* next,
    Tracer* tracer)
    : FilterInterpreter(nullptr, next, tracer, false),
      palm_pressure_(prop_reg, "Palm Pressure", 200.0),
      palm_width_(prop_reg, "Palm Width", 21.2),
      multi_palm_width_(prop_reg, "Multiple Palm Width", 75.0),
      fat_finger_pressure_ratio_(prop_reg, "Fat Finger Pressure Ratio", 1.4),
      fat_finger_width_ratio_(prop_reg, "Fat Finger Width Ratio", 1.3),
      fat_finger_min_dist_(prop_reg, "Fat Finger Min Move Distance", 15.0),
      palm_edge_min_width_(prop_reg, "Tap Exclusion Border Width", 8.0),
      palm_edge_width_(prop_reg, "Palm Edge Zone Width", 14.0),
      palm_top_edge_min_width_(prop_reg, "Top Edge Tap Exclusion Border Width",
                               3.0),
      palm_edge_point_speed_(prop_reg, "Palm Edge Zone Min Point Speed", 100.0),
      palm_eval_timeout_(prop_reg, "Palm Eval Timeout", 0.1),
      palm_stationary_time_(prop_reg, "Palm Stationary Time", 2.0),
      palm_stationary_distance_(prop_reg, "Palm Stationary Distance", 4.0),
      palm_pointing_min_dist_(prop_reg,
                              "Palm Pointing Min Move Distance",
                              8.0),
      palm_pointing_max_reverse_dist_(prop_reg,
                                      "Palm Pointing Max Reverse Move Distance",
                                      0.3),
      palm_split_max_distance_(prop_reg, "Palm Split Maximum Distance", 4.0),
      filter_top_edge_(prop_reg, "Palm Filter Top Edge Enable", false)
{
  InitName();
  requires_metrics_ = true;
}

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

  FillOriginInfo(hwstate);
  FillMaxPressureWidthInfo(hwstate);
  UpdateDistanceInfo(hwstate);
  UpdatePalmState(hwstate);
  UpdatePalmFlags(hwstate);
  FillPrevInfo(hwstate);

  LogHardwareStatePost(name, hwstate);
  if (next_.get())
    next_->SyncInterpret(hwstate, timeout);
}

void PalmClassifyingFilterInterpreter::FillOriginInfo(
    const HardwareState& hwstate) {
  RemoveMissingIdsFromMap(&origin_timestamps_, hwstate);
  RemoveMissingIdsFromMap(&origin_fingerstates_, hwstate);
  for (size_t i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    if (MapContainsKey(origin_timestamps_, fs.tracking_id))
      continue;
    origin_timestamps_[fs.tracking_id] = hwstate.timestamp;
    origin_fingerstates_[fs.tracking_id] = fs;
  }
}

void PalmClassifyingFilterInterpreter::FillPrevInfo(
    const HardwareState& hwstate) {
  RemoveMissingIdsFromMap(&prev_fingerstates_, hwstate);
  prev_time_ = hwstate.timestamp;
  for (size_t i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    prev_fingerstates_[fs.tracking_id] = fs;
  }
}

void PalmClassifyingFilterInterpreter::FillMaxPressureWidthInfo(
    const HardwareState& hwstate) {
  RemoveMissingIdsFromMap(&max_pressure_, hwstate);
  RemoveMissingIdsFromMap(&max_width_, hwstate);
  for (size_t i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    int id = fs.tracking_id;
    if (MapContainsKey(max_pressure_, id)) {
      if (fs.pressure > max_pressure_[id])
        max_pressure_[id] = fs.pressure;
      if (fs.touch_major > max_width_[id])
        max_width_[id] = fs.touch_major;
    } else {
      max_pressure_[id] = fs.pressure;
      max_width_[id] = fs.touch_major;
    }
  }
}

void PalmClassifyingFilterInterpreter::UpdateDistanceInfo(
    const HardwareState& hwstate) {
  RemoveMissingIdsFromMap(&distance_positive_[0], hwstate);
  RemoveMissingIdsFromMap(&distance_positive_[1], hwstate);
  RemoveMissingIdsFromMap(&distance_negative_[0], hwstate);
  RemoveMissingIdsFromMap(&distance_negative_[1], hwstate);
  for (size_t i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    int id = fs.tracking_id;
    if (MapContainsKey(prev_fingerstates_, id)) {
      float delta[2];
      delta[0] = fs.position_x - prev_fingerstates_[id].position_x;
      delta[1] = fs.position_y - prev_fingerstates_[id].position_y;
      for (int i = 0; i < 2; i++) {
        if (delta[i] > 0)
          distance_positive_[i][id] += delta[i];
        else
          distance_negative_[i][id] -= delta[i];
      }
    } else {
      distance_positive_[0][id] = 0;
      distance_positive_[1][id] = 0;
      distance_negative_[0][id] = 0;
      distance_negative_[1][id] = 0;
    }
  }
}

bool PalmClassifyingFilterInterpreter::FingerNearOtherFinger(
    const HardwareState& hwstate,
    size_t finger_idx) {
  const FingerState& fs = hwstate.fingers[finger_idx];
  for (int i = 0; i < hwstate.finger_cnt; ++i) {
    const FingerState& other_fs = hwstate.fingers[i];
    if (other_fs.tracking_id == fs.tracking_id)
      continue;
    bool close_enough_together =
        metrics_->CloseEnoughToGesture(Vector2(fs), Vector2(other_fs)) &&
        !SetContainsValue(palm_, other_fs.tracking_id);
    bool too_close_together = DistSq(fs, other_fs) <
        palm_split_max_distance_.val_ * palm_split_max_distance_.val_;
    if (close_enough_together && !too_close_together) {
      was_near_other_fingers_.insert(fs.tracking_id);
      return true;
    }
  }
  return false;
}

bool PalmClassifyingFilterInterpreter::FingerInPalmEnvelope(
    const FingerState& fs) {
  float limit = palm_edge_min_width_.val_ +
      (fs.pressure / palm_pressure_.val_) *
      (palm_edge_width_.val_ - palm_edge_min_width_.val_);
  return fs.position_x < limit ||
      fs.position_x > (hwprops_->right - limit) ||
      (filter_top_edge_.val_ && fs.position_y < palm_top_edge_min_width_.val_);
}

bool PalmClassifyingFilterInterpreter::FingerInBottomArea(
    const FingerState& fs) {
  return fs.position_y > (hwprops_->bottom - palm_edge_min_width_.val_);
}

void PalmClassifyingFilterInterpreter::UpdatePalmState(
    const HardwareState& hwstate) {
  RemoveMissingIdsFromSet(&palm_, hwstate);
  RemoveMissingIdsFromSet(&large_palm_, hwstate);
  RemoveMissingIdsFromMap(&pointing_, hwstate);
  RemoveMissingIdsFromSet(&non_stationary_palm_, hwstate);
  RemoveMissingIdsFromSet(&fingers_not_in_edge_, hwstate);
  RemoveMissingIdsFromSet(&was_near_other_fingers_, hwstate);

  // Some finger(s) just leaves, skip this update for stability
  if (prev_fingerstates_.size() > hwstate.finger_cnt)
    return;

  for (short i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    if (!(FingerInPalmEnvelope(fs) || FingerInBottomArea(fs)))
      fingers_not_in_edge_.insert(fs.tracking_id);
    // Mark anything over the palm thresh as a palm
    if (fs.pressure >= palm_pressure_.val_ ||
        fs.touch_major >= multi_palm_width_.val_) {
      large_palm_.insert(fs.tracking_id);
      palm_.insert(fs.tracking_id);
      pointing_.erase(fs.tracking_id);
      continue;
    }
    // Mark externally reported palms
    if(fs.tool_type == FingerState::ToolType::kPalm){
      palm_.insert(fs.tracking_id);
      pointing_.erase(fs.tracking_id);
    }
  }

  if (hwstate.finger_cnt == 1 &&
      hwstate.fingers[0].touch_major >= palm_width_.val_) {
    large_palm_.insert(hwstate.fingers[0].tracking_id);
    palm_.insert(hwstate.fingers[0].tracking_id);
    pointing_.erase(hwstate.fingers[0].tracking_id);
  }

  const float kPalmStationaryDistSq =
      palm_stationary_distance_.val_ * palm_stationary_distance_.val_;
  const float kFatFingerMinDistSq =
      fat_finger_min_dist_.val_ * fat_finger_min_dist_.val_;
  const float kFatFingerMaxPressure =
      palm_pressure_.val_ * fat_finger_pressure_ratio_.val_;
  const float kFatFingerMaxWidth =
      palm_width_.val_ * fat_finger_width_ratio_.val_;

  for (short i = 0; i < hwstate.finger_cnt; i++) {
    const FingerState& fs = hwstate.fingers[i];
    bool prev_palm = SetContainsValue(palm_, fs.tracking_id);
    bool prev_pointing = MapContainsKey(pointing_, fs.tracking_id);

    if (prev_palm) {
      // If the finger's pressure & width are more like a fat finger
      // and it has moved a lot, it might be a fat finger and remove
      // it from palm.
      float dist_sq = DistSq(origin_fingerstates_[fs.tracking_id], fs);
      if (max_pressure_[fs.tracking_id] <= kFatFingerMaxPressure &&
          max_width_[fs.tracking_id] <= kFatFingerMaxWidth &&
          dist_sq > kFatFingerMinDistSq) {
        large_palm_.erase(fs.tracking_id);
        palm_.erase(fs.tracking_id);
      } else {
        // Lock onto palm
        continue;
      }
    }

    // If the finger is recently placed, remove it from pointing/fingers.
    // If it's still looking like pointing, it'll get readded.
    if (FingerAge(fs.tracking_id, hwstate.timestamp) <
        palm_eval_timeout_.val_) {
      pointing_.erase(fs.tracking_id);

      prev_pointing = false;
    }
    // If another finger is close by, let this be pointing
    bool near_finger = FingerNearOtherFinger(hwstate, i);
    bool on_edge = FingerInPalmEnvelope(fs) ||
        FingerInBottomArea(fs);
    if (!prev_pointing && (near_finger || !on_edge)) {
      unsigned reason = (near_finger ? kPointCloseToFinger : 0) |
          ((!on_edge) ? kPointNotInEdge : 0);
      pointing_[fs.tracking_id] = reason;
    }

    // Check if fingers that only move within palm envelope are pointing.
    int id = fs.tracking_id;
    float min_dist = palm_pointing_min_dist_.val_;
    float max_reverse_dist = palm_pointing_max_reverse_dist_.val_;

    // Ideally, we want to say that a finger is pointing if it moves only in
    // one direction significantly without zig-zag. But due to touch sensor's
    // inaccuratcy, we make the rule to be that a finger has to move in one
    // direction significantly with little move in the opposite direction.
    for (size_t j = 0; j < arraysize(distance_positive_); j++)
      if ((distance_positive_[j][id] >= min_dist &&
           distance_negative_[j][id] <= max_reverse_dist) ||
          (distance_positive_[j][id] <= max_reverse_dist &&
           distance_negative_[j][id] >= min_dist)) {
        pointing_[id] |= kPointMoving;
      }

    // However, if the contact has been stationary for a while since it
    // touched down, it is a palm. We track a potential palm closely for the
    // first amount of time to see if it fits this pattern.
    if (FingerAge(fs.tracking_id, prev_time_) >
        palm_stationary_time_.val_ ||
        SetContainsValue(non_stationary_palm_, fs.tracking_id)) {
      // Finger is too old to reconsider or is moving a lot
      continue;
    }
    if (DistSq(origin_fingerstates_[fs.tracking_id], fs) >
        kPalmStationaryDistSq || !(FingerInPalmEnvelope(fs) ||
                                   FingerInBottomArea(fs))) {
      // Finger moving a lot or not in palm envelope; not a stationary palm.
      non_stationary_palm_.insert(fs.tracking_id);
      continue;
    }
    if (FingerAge(fs.tracking_id, prev_time_) <=
        palm_stationary_time_.val_ &&
        FingerAge(fs.tracking_id, hwstate.timestamp) >
        palm_stationary_time_.val_ &&
        !SetContainsValue(non_stationary_palm_, fs.tracking_id) &&
        !FingerNearOtherFinger(hwstate, i)) {
      // Enough time has passed. Make this stationary contact a palm.
      palm_.insert(fs.tracking_id);
      pointing_.erase(fs.tracking_id);
    }
  }
}

void PalmClassifyingFilterInterpreter::UpdatePalmFlags(HardwareState& hwstate) {
  for (short i = 0; i < hwstate.finger_cnt; i++) {
    FingerState* fs = &hwstate.fingers[i];
    if (SetContainsValue(large_palm_, fs->tracking_id)) {
      fs->flags |= GESTURES_FINGER_LARGE_PALM;
    }
    if (SetContainsValue(palm_, fs->tracking_id)) {
      fs->flags |= GESTURES_FINGER_PALM;
    } else if (!MapContainsKey(pointing_, fs->tracking_id) &&
               !SetContainsValue(was_near_other_fingers_, fs->tracking_id)) {
      if (FingerInPalmEnvelope(*fs)) {
        fs->flags |= GESTURES_FINGER_PALM;
      } else if (FingerInBottomArea(*fs)) {
        fs->flags |= (GESTURES_FINGER_WARP_X | GESTURES_FINGER_WARP_Y);
      }
    } else if (MapContainsKey(pointing_, fs->tracking_id) &&
               FingerInPalmEnvelope(*fs)) {
      fs->flags |= GESTURES_FINGER_POSSIBLE_PALM;
      if (pointing_[fs->tracking_id] == kPointCloseToFinger &&
          !FingerNearOtherFinger(hwstate, i)) {
        // Finger was near another finger, but it's not anymore, and it was
        // only this other finger that caused it to point. Mark it w/ warp
        // until it moves sufficiently to have another reason to be
        // pointing.
        fs->flags |= (GESTURES_FINGER_WARP_X | GESTURES_FINGER_WARP_Y);
      }
    }
  }
}

stime_t PalmClassifyingFilterInterpreter::FingerAge(short finger_id,
                                                    stime_t now) const {
  if (!MapContainsKey(origin_timestamps_, finger_id)) {
    Err("Don't have record of finger age for finger %d", finger_id);
    return -1;
  }
  return now - origin_timestamps_.at(finger_id);
}

}  // namespace gestures
