// Copyright 2021 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 "ui/events/ozone/evdev/numberpad_metrics.h"

#include "base/at_exit.h"
#include "base/logging.h"

#include <linux/input.h>

namespace ui {

namespace {

// Constants for identifying Elan Dynamic Numberpad
const int kDynamicNumberpadVendor = 0x04F3;
const int kDynamicNumberpadProduct = 0x31C6;

bool IsDeviceDynamicNumberpad(const InputDevice& input_device) {
  bool result = (input_device.vendor_id == kDynamicNumberpadVendor &&
                 input_device.product_id == kDynamicNumberpadProduct);
  return result;
}

bool IsNumpadEnterKey(unsigned int keycode) {
  return (keycode == KEY_KPENTER);
}

// Recognize the majority of keys (other than Enter) that could
// be found on a numberpad, and only a number pad; this does not
// cover additional keys that are often on external stand-alone
// numeric pads (backspace, tab, 'Calculator'), or cover every
// defined KEY_KP* symbol.
bool IsNumpadOtherKey(unsigned int keycode) {
  switch (keycode) {
    case KEY_KP0:
    case KEY_KP1:
    case KEY_KP2:
    case KEY_KP3:
    case KEY_KP4:
    case KEY_KP5:
    case KEY_KP6:
    case KEY_KP7:
    case KEY_KP8:
    case KEY_KP9:
    case KEY_KPMINUS:
    case KEY_KPPLUS:
    case KEY_KPDOT:
    case KEY_KPSLASH:
    case KEY_KPASTERISK:
    case KEY_KPEQUAL:
      return true;
    default:
      return false;
  }
}

bool IsNumpadKey(unsigned int keycode) {
  return (IsNumpadEnterKey(keycode) || IsNumpadOtherKey(keycode));
}

bool IsNumlockKey(unsigned int keycode) {
  return (keycode == KEY_NUMLOCK);
}

// The Elan Dynamic Numberpad includes a few extra keys,
// which we want to recognize; they include backspace, a
// non-kp equals key, and it can emit a '%' via Shift-5.
bool IsDynamicNumpadKey(unsigned int keycode) {
  if (IsNumpadKey(keycode) || IsNumpadEnterKey(keycode) ||
      IsNumpadOtherKey(keycode) || IsNumlockKey(keycode))
    return true;

  switch (keycode) {
    case KEY_5:
    case KEY_BACKSPACE:
    case KEY_EQUAL:
      return true;
    default:
      return false;
  }
}

bool IsInternalDevice(const InputDevice& input_device) {
  bool result = (input_device.type == INPUT_DEVICE_INTERNAL ||
                 input_device.type == INPUT_DEVICE_UNKNOWN);
  return result;
}

}  // namespace

// static
constexpr char NumberpadMetricsRecorder::kFeatureDynamicActivations[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureDynamicCancellations[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureDynamicEnterKeystrokes[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureDynamicNonEnterKeystrokes[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureInternalEnterKeystrokes[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureInternalNonEnterKeystrokes[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureExternalEnterKeystrokes[];
// static
constexpr char NumberpadMetricsRecorder::kFeatureExternalNonEnterKeystrokes[];

NumberpadMetricsDelegate::NumberpadMetricsDelegate(
    const std::string& feature_name)
    : metrics_(feature_name, this) {}

NumberpadMetricsDelegate::~NumberpadMetricsDelegate() = default;

bool NumberpadMetricsDelegate::IsEligible() const {
  return eligible_;
}

bool NumberpadMetricsDelegate::IsEnabled() const {
  return enabled_;
}

void NumberpadMetricsDelegate::SetState(bool now_eligible, bool now_enabled) {
  // enabled must only be set if eligible is set
  DCHECK(now_enabled ? now_eligible : true);
  eligible_ = now_eligible;
  enabled_ = now_enabled;
}

void NumberpadMetricsDelegate::RecordUsage(bool success) {
  // If we are out of sync for some reason, just ignore events to
  // preserve feature_usage_metric's assumptions.
  if (!eligible_ || !enabled_)
    return;
  metrics_.RecordUsage(success);
}

// static
NumberpadMetricsRecorder* NumberpadMetricsRecorder::instance_;

// static
NumberpadMetricsRecorder* NumberpadMetricsRecorder::GetInstance() {
  if (!instance_) {
    instance_ = new NumberpadMetricsRecorder();
    base::AtExitManager::RegisterTask(base::BindOnce(ClearInstance));
  }
  return instance_;
}

// static
void NumberpadMetricsRecorder::ClearInstance() {
  if (instance_) {
    delete instance_;
    instance_ = nullptr;
  }
}

void NumberpadMetricsRecorder::AddDevice(const ui::InputDevice& input_device) {
  if (IsInternalDevice(input_device)) {
    if (IsDeviceDynamicNumberpad(input_device)) {
      dynamic_numpad_present_++;
    } else {
      internal_numpad_present_++;
    }
  } else {
    external_numpad_present_++;
  }
  UpdateDeviceState();
}

void NumberpadMetricsRecorder::RemoveDevice(
    const ui::InputDevice& input_device) {
  if (IsInternalDevice(input_device)) {
    if (IsDeviceDynamicNumberpad(input_device)) {
      dynamic_numpad_present_--;
    } else {
      internal_numpad_present_--;
    }
  } else {
    external_numpad_present_--;
  }
  UpdateDeviceState();
}

void NumberpadMetricsRecorder::UpdateDeviceState() {
  if (dynamic_numpad_present_ > 1)
    LOG(ERROR) << "Unexpected multiple dynamic numberpads seen.";

  dynamic_activations_metrics_delegate_.SetState(dynamic_numpad_present_ > 0);
  dynamic_cancellations_metrics_delegate_.SetState(dynamic_numpad_present_ > 0);
  dynamic_enter_keystrokes_metrics_delegate_.SetState(dynamic_numpad_present_ >
                                                      0);
  dynamic_non_enter_keystrokes_metrics_delegate_.SetState(
      dynamic_numpad_present_ > 0);

  internal_enter_keystrokes_metrics_delegate_.SetState(
      internal_numpad_present_ > 0);
  internal_non_enter_keystrokes_metrics_delegate_.SetState(
      internal_numpad_present_ > 0);

  external_enter_keystrokes_metrics_delegate_.SetState(
      true, external_numpad_present_ > 0);
  external_non_enter_keystrokes_metrics_delegate_.SetState(
      true, external_numpad_present_ > 0);
}

NumberpadMetricsRecorder::NumberpadMetricsRecorder() {
  UpdateDeviceState();
}

NumberpadMetricsRecorder::~NumberpadMetricsRecorder() = default;

// Background: the dynamic numberpad lets us know when its state is changed
// via a numlock keypress, however we cannot retrieve the current state. On
// boot it is reasonable to assume it is off, however on chromium restart (for
// crash or any other reason) we don't know.
//
// Using the assumption that a numberpad key can only come through when it
// is turned on, we can fix the state at that point; prior to that, we can
// only guess. We are conservative and only push events that must be true,
// and accept we might lose an activation or cancellation in combination
// with a crash/restart.

void NumberpadMetricsRecorder::ToggleDynamicNumlockState() {
  if (dynamic_numlock_state_known_) {
    dynamic_numlock_state_ = !dynamic_numlock_state_;
    if (dynamic_numlock_state_) {
      // Activated numlock
      dynamic_activations_metrics_delegate_.RecordUsage(true);
    } else {
      // Deactivated numlock; if we have not used a numberpad key since it was
      // activated, consider it a cancelled use of the numberpad.
      if (!dynamic_numpad_used_)
        dynamic_cancellations_metrics_delegate_.RecordUsage(true);
    }
    dynamic_numpad_used_ = false;
  } else {
    // We have an unknown original state: after two flips, it must have been
    // turned on, after three flips (without seeing a numpad key) it must have
    // been turned back off without being used. (If it was used, we would no
    // longer be in this branch of code.)

    if (!any_dynamic_numlock_state_flips_) {
      // Discard first flip
      any_dynamic_numlock_state_flips_ = true;
    } else {
      dynamic_numlock_state_guess_ = !dynamic_numlock_state_guess_;
      if (dynamic_numlock_state_guess_) {
        // 2nd, 4th, etc. flip
        dynamic_activations_metrics_delegate_.RecordUsage(true);
      } else {
        // 3rd, 5th, etc. flip
        dynamic_cancellations_metrics_delegate_.RecordUsage(true);
      }
    }
  }
}

// Used when a key has been pressed on a dynamic numberpad,
// proving the numlock state is on.
void NumberpadMetricsRecorder::SetDynamicNumlockStateOn() {
  if (dynamic_numlock_state_known_) {
    // If we have known numlock state, then we should be accurately changing
    // the state every time a numlock key comes in; if we get a numberpad
    // key from the dynamic numberpad when we believe numlock is off, something
    // unexpected has happened; just log an error, fix our state, and move on.
    if (dynamic_numlock_state_ == false) {
      LOG(ERROR)
          << "Dynamic numberpad in an unexpected state; numlock is on now.";
      ToggleDynamicNumlockState();
    }
    return;
  }

  // Now we know the state, we can stop guessing; sync up our previous
  // guess with the known state.

  dynamic_numlock_state_known_ = true;
  dynamic_numlock_state_ = true;

  // If we're sure we're off by one, issue an additional metric.
  // This won't issue a metric if numberlock isn't pressed before the
  // first key, as it could be excess, and we're being conservative.
  if (any_dynamic_numlock_state_flips_ && !dynamic_numlock_state_guess_)
    dynamic_activations_metrics_delegate_.RecordUsage(true);
}

// This method is only intended to be called for input devices where
// HasNumberpad() is true. We do not have enough information
// (from the input_device) to actually check this.
void NumberpadMetricsRecorder::ProcessKey(unsigned int key,
                                          bool down,
                                          const InputDevice& input_device) {
  // Only process key presses, not releases
  if (!down)
    return;

  if (IsInternalDevice(input_device)) {
    if (IsDeviceDynamicNumberpad(input_device) && IsDynamicNumpadKey(key)) {
      // Logic specific to particular dynamic numberpad.
      if (IsNumlockKey(key)) {
        ToggleDynamicNumlockState();
      } else {
        // Saw a numpad key, therefore numlock must be enabled
        SetDynamicNumlockStateOn();
        dynamic_numpad_used_ = true;
        if (IsNumpadEnterKey(key)) {
          dynamic_enter_keystrokes_metrics_delegate_.RecordUsage(true);
        } else {
          dynamic_non_enter_keystrokes_metrics_delegate_.RecordUsage(true);
        }
      }
    } else if (IsNumpadKey(key)) {
      if (IsNumpadEnterKey(key)) {
        internal_enter_keystrokes_metrics_delegate_.RecordUsage(true);
      } else {
        internal_non_enter_keystrokes_metrics_delegate_.RecordUsage(true);
      }
    }
  } else {  // External device
    if (IsNumpadKey(key)) {
      if (IsNumpadEnterKey(key)) {
        external_enter_keystrokes_metrics_delegate_.RecordUsage(true);
      } else {
        external_non_enter_keystrokes_metrics_delegate_.RecordUsage(true);
      }
    }
  }
}

}  // namespace ui
