// Copyright 2013 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "InputCommon/ControllerInterface/OSX/OSXJoystick.h"

#include <algorithm>
#include <sstream>

#include <Foundation/Foundation.h>
#include <IOKit/hid/IOHIDLib.h>

#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"

namespace ciface::OSX
{
void Joystick::AddElements(CFArrayRef elements, std::set<IOHIDElementCookie>& cookies)
{
  for (int i = 0; i < CFArrayGetCount(elements); i++)
  {
    IOHIDElementRef e = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i);

    const uint32_t type = IOHIDElementGetType(e);

    switch (type)
    {
    case kIOHIDElementTypeCollection:
      AddElements(IOHIDElementGetChildren(e), cookies);
      continue;
    case kIOHIDElementTypeOutput:
      continue;
    }

    IOHIDElementCookie cookie = IOHIDElementGetCookie(e);

    // Check for any existing elements with the same cookie
    if (cookies.count(cookie) > 0)
      continue;

    cookies.insert(cookie);

    const uint32_t usage = IOHIDElementGetUsage(e);

    switch (usage)
    {
    // Axis
    case kHIDUsage_GD_X:
    case kHIDUsage_GD_Y:
    case kHIDUsage_GD_Z:
    case kHIDUsage_GD_Rx:
    case kHIDUsage_GD_Ry:
    case kHIDUsage_GD_Rz:
    case kHIDUsage_GD_Slider:
    case kHIDUsage_GD_Dial:
    case kHIDUsage_GD_Wheel:
    case kHIDUsage_GD_Hatswitch:
    // Simulator
    case kHIDUsage_Sim_Accelerator:
    case kHIDUsage_Sim_Brake:
    case kHIDUsage_Sim_Rudder:
    case kHIDUsage_Sim_Throttle:
    {
      if (usage == kHIDUsage_GD_Hatswitch)
      {
        AddInput(new Hat(e, m_device, Hat::up));
        AddInput(new Hat(e, m_device, Hat::right));
        AddInput(new Hat(e, m_device, Hat::down));
        AddInput(new Hat(e, m_device, Hat::left));
      }
      else
      {
        AddAnalogInputs(new Axis(e, m_device, Axis::negative),
                        new Axis(e, m_device, Axis::positive));
      }
      break;
    }
    // Buttons
    case kHIDUsage_GD_DPadUp:
    case kHIDUsage_GD_DPadDown:
    case kHIDUsage_GD_DPadRight:
    case kHIDUsage_GD_DPadLeft:
    case kHIDUsage_GD_Start:
    case kHIDUsage_GD_Select:
    case kHIDUsage_GD_SystemMainMenu:
      AddInput(new Button(e, m_device));
      break;

    default:
      // Catch any easily identifiable axis and buttons that slipped through
      if (type == kIOHIDElementTypeInput_Button)
      {
        AddInput(new Button(e, m_device));
        break;
      }

      const uint32_t usage_page = IOHIDElementGetUsagePage(e);

      if (usage_page == kHIDPage_Button || usage_page == kHIDPage_Consumer)
      {
        AddInput(new Button(e, m_device));
        break;
      }

      if (type == kIOHIDElementTypeInput_Axis)
      {
        AddAnalogInputs(new Axis(e, m_device, Axis::negative),
                        new Axis(e, m_device, Axis::positive));
        break;
      }

      NOTICE_LOG_FMT(CONTROLLERINTERFACE,
                     "Unknown IOHIDElement, ignoring (Usage: {:x}, Type: {:x})", usage,
                     IOHIDElementGetType(e));

      break;
    }
  }
}
Joystick::Joystick(IOHIDDeviceRef device, std::string name)
    : m_device(device), m_device_name(name), m_ff_device(nullptr)
{
  CFArrayRef elements = IOHIDDeviceCopyMatchingElements(m_device, nullptr, kIOHIDOptionsTypeNone);
  std::set<IOHIDElementCookie> known_cookies;
  AddElements(elements, known_cookies);

  // Force Feedback
  FFCAPABILITIES ff_caps;
  if (SUCCEEDED(
          ForceFeedback::FFDeviceAdapter::Create(IOHIDDeviceGetService(m_device), &m_ff_device)) &&
      SUCCEEDED(FFDeviceGetForceFeedbackCapabilities(m_ff_device->m_device, &ff_caps)))
  {
    InitForceFeedback(m_ff_device, ff_caps.numFfAxes);
  }
}

Joystick::~Joystick()
{
  DeInitForceFeedback();

  if (m_ff_device)
    m_ff_device->Release();
}

std::string Joystick::GetName() const
{
  return m_device_name;
}

std::string Joystick::GetSource() const
{
  return "Input";
}

ControlState Joystick::Button::GetState() const
{
  IOHIDValueRef value;
  if (IOHIDDeviceGetValue(m_device, m_element, &value) == kIOReturnSuccess)
    return IOHIDValueGetIntegerValue(value);
  else
    return 0;
}

std::string Joystick::Button::GetName() const
{
  std::ostringstream s;
  s << IOHIDElementGetUsage(m_element);
  return std::string("Button ").append(StripWhitespace(s.str()));
}

Joystick::Axis::Axis(IOHIDElementRef element, IOHIDDeviceRef device, direction dir)
    : m_element(element), m_device(device), m_direction(dir)
{
  // Need to parse the element a bit first
  std::string description("unk");

  int const usage = IOHIDElementGetUsage(m_element);
  switch (usage)
  {
  case kHIDUsage_GD_X:
    description = "X";
    break;
  case kHIDUsage_GD_Y:
    description = "Y";
    break;
  case kHIDUsage_GD_Z:
    description = "Z";
    break;
  case kHIDUsage_GD_Rx:
    description = "Rx";
    break;
  case kHIDUsage_GD_Ry:
    description = "Ry";
    break;
  case kHIDUsage_GD_Rz:
    description = "Rz";
    break;
  case kHIDUsage_GD_Wheel:
    description = "Wheel";
    break;
  case kHIDUsage_Csmr_ACPan:
    description = "Pan";
    break;
  default:
  {
    IOHIDElementCookie elementCookie = IOHIDElementGetCookie(m_element);
    // This axis isn't a 'well-known' one so cook a descriptive and uniquely
    // identifiable name. macOS provides a 'cookie' for each element that
    // will persist between sessions and identify the same physical controller
    // element so we can use that as a component of the axis name
    std::ostringstream s;
    s << "CK-";
    s << elementCookie;
    description = StripWhitespace(s.str());
    break;
  }
  }

  m_name = std::string("Axis ") + description;
  m_name.append((m_direction == positive) ? "+" : "-");

  m_neutral = (IOHIDElementGetLogicalMax(m_element) + IOHIDElementGetLogicalMin(m_element)) / 2.;
  m_scale = 1 / fabs(IOHIDElementGetLogicalMax(m_element) - m_neutral);
}

ControlState Joystick::Axis::GetState() const
{
  IOHIDValueRef value;

  if (IOHIDDeviceGetValue(m_device, m_element, &value) == kIOReturnSuccess)
  {
    // IOHIDValueGetIntegerValue() crashes when trying
    // to convert unusually large element values.
    if (IOHIDValueGetLength(value) > 2)
      return 0;

    float position = IOHIDValueGetIntegerValue(value);

    if (m_direction == positive && position > m_neutral)
      return (position - m_neutral) * m_scale;
    if (m_direction == negative && position < m_neutral)
      return (m_neutral - position) * m_scale;
  }

  return 0;
}

std::string Joystick::Axis::GetName() const
{
  return m_name;
}

Joystick::Hat::Hat(IOHIDElementRef element, IOHIDDeviceRef device, direction dir)
    : m_element(element), m_device(device), m_direction(dir)
{
  switch (dir)
  {
  case up:
    m_name = "Up";
    break;
  case right:
    m_name = "Right";
    break;
  case down:
    m_name = "Down";
    break;
  case left:
    m_name = "Left";
    break;
  default:
    m_name = "unk";
  }
}

ControlState Joystick::Hat::GetState() const
{
  IOHIDValueRef value;

  if (IOHIDDeviceGetValue(m_device, m_element, &value) == kIOReturnSuccess)
  {
    int position = IOHIDValueGetIntegerValue(value);
    int min = IOHIDElementGetLogicalMin(m_element);
    int max = IOHIDElementGetLogicalMax(m_element);

    // if the position is outside the min or max, don't register it as a valid button press
    if (position < min || position > max)
    {
      return 0;
    }

    // normalize the position so that its lowest value is 0
    position -= min;

    switch (position)
    {
    case 0:
      if (m_direction == up)
        return 1;
      break;
    case 1:
      if (m_direction == up || m_direction == right)
        return 1;
      break;
    case 2:
      if (m_direction == right)
        return 1;
      break;
    case 3:
      if (m_direction == right || m_direction == down)
        return 1;
      break;
    case 4:
      if (m_direction == down)
        return 1;
      break;
    case 5:
      if (m_direction == down || m_direction == left)
        return 1;
      break;
    case 6:
      if (m_direction == left)
        return 1;
      break;
    case 7:
      if (m_direction == left || m_direction == up)
        return 1;
      break;
    };
  }

  return 0;
}

std::string Joystick::Hat::GetName() const
{
  return m_name;
}

bool Joystick::IsSameDevice(const IOHIDDeviceRef other_device) const
{
  return m_device == other_device;
}
}  // namespace ciface::OSX
