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

#include "third_party/blink/renderer/modules/gamepad/gamepad_comparisons.h"

#include "third_party/blink/renderer/modules/gamepad/gamepad.h"

namespace blink {

namespace {

// A button press must have a value at least this large to qualify as a user
// activation. The selected value should be greater than 0.5 so that axes
// incorrectly mapped as triggers do not generate activations in the idle
// position.
const double kButtonActivationThreshold = 0.9;

template <typename T>
auto AsSpan(const T& collection) {
  return collection.AsSpan();
}

template <size_t N>
Vector<int> GetSetBitIndexes(const std::bitset<N>& bitset) {
  Vector<int> set_bits;
  for (size_t i = 0; i < bitset.size(); ++i) {
    if (bitset.test(i)) {
      set_bits.push_back(static_cast<int>(i));
    }
  }
  return set_bits;
}

template <typename Collection,
          typename Pred = std::equal_to<typename Collection::ValueType>>
bool Compare(const Collection* old_array,
             const Collection* new_array,
             Pred pred = Pred{}) {
  if (old_array && new_array) {
    // Both arrays are non-null.
    return !std::ranges::equal(AsSpan(*old_array), AsSpan(*new_array), pred);
  } else if (old_array != new_array) {
    // Exactly one array is non-null.
    return true;
  }
  // Both arrays are null, or the arrays are identical.
  return false;
}

}  // namespace

// static
bool GamepadComparisons::HasUserActivation(
    const HeapVector<Member<Gamepad>>& gamepads) {
  // A button press counts as a user activation if the button's value is greater
  // than the activation threshold. A threshold is used so that analog buttons
  // or triggers do not generate an activation from a light touch.
  for (Gamepad* pad : gamepads) {
    if (pad) {
      for (auto button : pad->buttons()) {
        if (button->value() > kButtonActivationThreshold)
          return true;
      }
    }
  }
  return false;
}

// static
void GamepadComparisons::HasGamepadConnectionChanged(bool old_connected,
                                                     bool new_connected,
                                                     bool id_changed,
                                                     bool* gamepad_found,
                                                     bool* gamepad_lost) {
  if (gamepad_found)
    *gamepad_found = id_changed || (!old_connected && new_connected);
  if (gamepad_lost)
    *gamepad_lost = id_changed || (old_connected && !new_connected);
}

GamepadStateCompareResult::GamepadStateCompareResult(
    const HeapVector<Member<Gamepad>>& old_gamepads,
    const HeapVector<Member<Gamepad>>& new_gamepads,
    bool compare_all_axes,
    bool compare_all_buttons) {
  any_change_ = CompareGamepads(old_gamepads, new_gamepads, compare_all_axes,
                                compare_all_buttons);
}

bool GamepadStateCompareResult::IsDifferent() const {
  return any_change_;
}

bool GamepadStateCompareResult::IsGamepadConnected(size_t pad_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  return gamepad_connected_.test(pad_index);
}

bool GamepadStateCompareResult::IsGamepadDisconnected(size_t pad_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  return gamepad_disconnected_.test(pad_index);
}

bool GamepadStateCompareResult::HasGamepadInputChanged(size_t pad_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  return gamepad_input_changed_.test(pad_index);
}

bool GamepadStateCompareResult::IsAxisChanged(size_t pad_index,
                                              size_t axis_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  DCHECK_LT(axis_index, device::Gamepad::kAxesLengthCap);
  return axis_changed_[pad_index].test(axis_index);
}

bool GamepadStateCompareResult::IsButtonChanged(size_t pad_index,
                                                size_t button_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  DCHECK_LT(button_index, device::Gamepad::kButtonsLengthCap);
  return button_changed_[pad_index].test(button_index);
}

bool GamepadStateCompareResult::IsButtonDown(size_t pad_index,
                                             size_t button_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  DCHECK_LT(button_index, device::Gamepad::kButtonsLengthCap);
  return button_down_[pad_index].test(button_index);
}

bool GamepadStateCompareResult::IsButtonUp(size_t pad_index,
                                           size_t button_index) const {
  DCHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  DCHECK_LT(button_index, device::Gamepad::kButtonsLengthCap);
  return button_up_[pad_index].test(button_index);
}

bool GamepadStateCompareResult::IsTouchChanged(size_t pad_index,
                                               size_t touch_index) const {
  CHECK_LT(pad_index, device::Gamepads::kItemsLengthCap);
  CHECK_LT(touch_index, device::Gamepad::kTouchEventsLengthCap);
  return touch_changed_[pad_index].test(touch_index);
}

bool GamepadStateCompareResult::CompareGamepads(
    const HeapVector<Member<Gamepad>>& old_gamepads,
    const HeapVector<Member<Gamepad>>& new_gamepads,
    bool compare_all_axes,
    bool compare_all_buttons) {
  bool any_change = false;
  for (uint32_t i = 0; i < new_gamepads.size(); ++i) {
    Gamepad* old_gamepad = i < old_gamepads.size() ? old_gamepads[i] : nullptr;
    Gamepad* new_gamepad = new_gamepads[i];
    // Check whether the gamepad is newly connected or disconnected.
    bool newly_connected = false;
    bool newly_disconnected = false;
    bool old_connected = old_gamepad && old_gamepad->connected();
    bool new_connected = new_gamepad && new_gamepad->connected();
    if (old_gamepad && new_gamepad) {
      GamepadComparisons::HasGamepadConnectionChanged(
          old_connected, new_connected, old_gamepad->id() != new_gamepad->id(),
          &newly_connected, &newly_disconnected);
    } else {
      newly_connected = new_connected;
      newly_disconnected = old_connected;
    }

    bool any_axis_updated =
        CompareAxes(old_gamepad, new_gamepad, i, compare_all_axes);
    bool any_button_updated =
        CompareButtons(old_gamepad, new_gamepad, i, compare_all_buttons);
    bool any_touch_updated = CompareTouches(old_gamepad, new_gamepad, i);

    if (newly_connected)
      gamepad_connected_.set(i);
    if (newly_disconnected)
      gamepad_disconnected_.set(i);
    if ((!newly_connected && !newly_disconnected) &&
        (any_axis_updated || any_button_updated || any_touch_updated)) {
      gamepad_input_changed_.set(i);
    }
    if (newly_connected || newly_disconnected || any_axis_updated ||
        any_button_updated || any_touch_updated) {
      any_change = true;
    }
  }
  return any_change;
}

bool GamepadStateCompareResult::CompareAxes(Gamepad* old_gamepad,
                                            Gamepad* new_gamepad,
                                            size_t index,
                                            bool compare_all) {
  DCHECK_LT(index, device::Gamepads::kItemsLengthCap);
  if (!new_gamepad)
    return false;
  auto& changed_set = axis_changed_[index];
  const auto& new_axes = new_gamepad->axes();
  const auto* old_axes = old_gamepad ? &old_gamepad->axes() : nullptr;
  bool any_axis_changed = false;
  for (wtf_size_t i = 0; i < new_axes.size(); ++i) {
    double new_value = new_axes[i];
    if (old_axes && i < old_axes->size()) {
      double old_value = old_axes->at(i);
      if (old_value != new_value) {
        any_axis_changed = true;
        if (!compare_all)
          break;
        changed_set.set(i);
      }
    } else {
      if (new_value) {
        any_axis_changed = true;
        if (!compare_all)
          break;
        changed_set.set(i);
      }
    }
  }
  return any_axis_changed;
}

bool GamepadStateCompareResult::CompareButtons(Gamepad* old_gamepad,
                                               Gamepad* new_gamepad,
                                               size_t index,
                                               bool compare_all) {
  DCHECK_LT(index, device::Gamepads::kItemsLengthCap);
  if (!new_gamepad)
    return false;
  auto& changed_set = button_changed_[index];
  auto& down_set = button_down_[index];
  auto& up_set = button_up_[index];
  const auto& new_buttons = new_gamepad->buttons();
  const auto* old_buttons = old_gamepad ? &old_gamepad->buttons() : nullptr;
  bool any_button_changed = false;
  for (wtf_size_t i = 0; i < new_buttons.size(); ++i) {
    double new_value = new_buttons[i]->value();
    bool new_pressed = new_buttons[i]->pressed();
    if (old_buttons && i < old_buttons->size()) {
      double old_value = old_buttons->at(i)->value();
      bool old_pressed = old_buttons->at(i)->pressed();
      if (old_value != new_value) {
        any_button_changed = true;
        if (!compare_all)
          break;
        changed_set.set(i);
      }
      if (old_pressed != new_pressed) {
        any_button_changed = true;
        if (!compare_all)
          break;
        if (new_pressed)
          down_set.set(i);
        else
          up_set.set(i);
      }
    } else {
      if (new_value > 0.0) {
        any_button_changed = true;
        if (!compare_all)
          break;
        changed_set.set(i);
      }
      if (new_pressed) {
        any_button_changed = true;
        if (!compare_all)
          break;
        down_set.set(i);
      }
    }
  }
  return any_button_changed;
}

bool GamepadStateCompareResult::CompareTouches(Gamepad* old_gamepad,
                                               Gamepad* new_gamepad,
                                               size_t gamepad_index) {
  if (!new_gamepad) {
    return false;
  }

  const auto* new_touches = new_gamepad->touchEvents();
  const auto* old_touches = old_gamepad ? old_gamepad->touchEvents() : nullptr;

  // No touches on either gamepad: nothing changed.
  if (!new_touches && !old_touches) {
    return false;
  }

  // One has touches and the other does not: considered different.
  if (!new_touches != !old_touches) {
    return true;
  }

  // Different number of touches: considered different.
  if (new_touches->size() != old_touches->size()) {
    return true;
  }

  // Compare individual touches.
  for (wtf_size_t i = 0; i < new_touches->size(); ++i) {
    const auto& new_touch = new_touches->at(i);
    const auto& old_touch = old_touches->at(i);

    if (new_touch->touchId() != old_touch->touchId() ||
        new_touch->surfaceId() != old_touch->surfaceId() ||
        Compare(new_touch->surfaceDimensions().Get(),
                old_touch->surfaceDimensions().Get()) ||
        Compare(new_touch->position().Get(), old_touch->position().Get())) {
      touch_changed_[gamepad_index].set(i);
      return touch_changed_[gamepad_index].any();
    }
  }

  return false;
}

Vector<int> GamepadStateCompareResult::GetChangedAxes(size_t pad_index) const {
  return GetSetBitIndexes(axis_changed_[pad_index]);
}

Vector<int> GamepadStateCompareResult::GetChangedButtons(
    size_t pad_index) const {
  return GetSetBitIndexes(button_changed_[pad_index]);
}

Vector<int> GamepadStateCompareResult::GetButtonsPressed(
    size_t pad_index) const {
  return GetSetBitIndexes(button_down_[pad_index]);
}

Vector<int> GamepadStateCompareResult::GetButtonsReleased(
    size_t pad_index) const {
  return GetSetBitIndexes(button_up_[pad_index]);
}

Vector<int> GamepadStateCompareResult::GetChangedTouches(
    size_t pad_index) const {
  return GetSetBitIndexes(touch_changed_[pad_index]);
}

GamepadStateCompareResult GamepadComparisons::Compare(
    const HeapVector<Member<Gamepad>>& old_gamepads,
    const HeapVector<Member<Gamepad>>& new_gamepads,
    bool compare_all_axes,
    bool compare_all_buttons) {
  return GamepadStateCompareResult(old_gamepads, new_gamepads, compare_all_axes,
                                   compare_all_buttons);
}

}  // namespace blink
