/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// mostly derived from the Allegro source code at:
// http://alleg.svn.sourceforge.net/viewvc/alleg/allegro/branches/4.9/src/macosx/hidjoy.m?revision=13760&view=markup

#include "mozilla/dom/GamepadPlatformService.h"
#include "mozilla/ArrayUtils.h"
#include "nsThreadUtils.h"
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/hid/IOHIDBase.h>
#include <IOKit/hid/IOHIDKeys.h>
#include <IOKit/hid/IOHIDManager.h>

#include <stdio.h>
#include <vector>

namespace {

using namespace mozilla;
using namespace mozilla::dom;
using std::vector;

struct Button {
  int id;
  bool analog;
  IOHIDElementRef element;
  CFIndex min;
  CFIndex max;

  Button(int aId, IOHIDElementRef aElement, CFIndex aMin, CFIndex aMax) :
    id(aId),
    analog((aMax - aMin) > 1),
    element(aElement),
    min(aMin),
    max(aMax) {}
};

struct Axis {
  int id;
  IOHIDElementRef element;
  uint32_t usagePage;
  uint32_t usage;
  CFIndex min;
  CFIndex max;
};

typedef bool dpad_buttons[4];

// These values can be found in the USB HID Usage Tables:
// http://www.usb.org/developers/hidpage
const unsigned kDesktopUsagePage = 0x01;
const unsigned kSimUsagePage = 0x02;
const unsigned kAcceleratorUsage = 0xC4;
const unsigned kBrakeUsage = 0xC5;
const unsigned kJoystickUsage = 0x04;
const unsigned kGamepadUsage = 0x05;
const unsigned kAxisUsageMin = 0x30;
const unsigned kAxisUsageMax = 0x35;
const unsigned kDpadUsage = 0x39;
const unsigned kButtonUsagePage = 0x09;
const unsigned kConsumerPage = 0x0C;
const unsigned kHomeUsage = 0x223;
const unsigned kBackUsage = 0x224;


class Gamepad {
 private:
  IOHIDDeviceRef mDevice;
  nsTArray<Button> buttons;
  nsTArray<Axis> axes;
  IOHIDElementRef mDpad;
  dpad_buttons mDpadState;

 public:
  Gamepad() : mDevice(nullptr), mDpad(nullptr), mSuperIndex(-1) {}
  bool operator==(IOHIDDeviceRef device) const { return mDevice == device; }
  bool empty() const { return mDevice == nullptr; }
  void clear()
  {
    mDevice = nullptr;
    buttons.Clear();
    axes.Clear();
    mDpad = nullptr;
    mSuperIndex = -1;
  }
  void init(IOHIDDeviceRef device);
  size_t numButtons() { return buttons.Length() + (mDpad ? 4 : 0); }
  size_t numAxes() { return axes.Length(); }

  // Index given by our superclass.
  uint32_t mSuperIndex;

  bool isDpad(IOHIDElementRef element) const
  {
    return element == mDpad;
  }

  const dpad_buttons& getDpadState() const
  {
    return mDpadState;
  }

  void setDpadState(const dpad_buttons& dpadState)
  {
    for (unsigned i = 0; i < ArrayLength(mDpadState); i++) {
      mDpadState[i] = dpadState[i];
    }
  }

  const Button* lookupButton(IOHIDElementRef element) const
  {
    for (unsigned i = 0; i < buttons.Length(); i++) {
      if (buttons[i].element == element)
        return &buttons[i];
    }
    return nullptr;
  }

  const Axis* lookupAxis(IOHIDElementRef element) const
  {
    for (unsigned i = 0; i < axes.Length(); i++) {
      if (axes[i].element == element)
        return &axes[i];
    }
    return nullptr;
  }
};

class AxisComparator {
public:
  bool Equals(const Axis& a1, const Axis& a2) const
  {
    return a1.usagePage == a2.usagePage && a1.usage == a2.usage;
  }
  bool LessThan(const Axis& a1, const Axis& a2) const
  {
    if (a1.usagePage == a2.usagePage) {
      return a1.usage < a2.usage;
    }
    return a1.usagePage < a2.usagePage;
  }
};

void Gamepad::init(IOHIDDeviceRef device)
{
  clear();
  mDevice = device;

  CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device,
                                                        nullptr,
                                                        kIOHIDOptionsTypeNone);
  CFIndex n = CFArrayGetCount(elements);
  for (CFIndex i = 0; i < n; i++) {
    IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements,
                                                                      i);
    uint32_t usagePage = IOHIDElementGetUsagePage(element);
    uint32_t usage = IOHIDElementGetUsage(element);

    if (usagePage == kDesktopUsagePage &&
        usage >= kAxisUsageMin &&
        usage <= kAxisUsageMax)
    {
      Axis axis = { int(axes.Length()),
                    element,
                    usagePage,
                    usage,
                    IOHIDElementGetLogicalMin(element),
                    IOHIDElementGetLogicalMax(element) };
      axes.AppendElement(axis);
    } else if (usagePage == kDesktopUsagePage && usage == kDpadUsage &&
               // Don't know how to handle d-pads that return weird values.
               IOHIDElementGetLogicalMax(element) - IOHIDElementGetLogicalMin(element) == 7) {
      mDpad = element;
    } else if ((usagePage == kSimUsagePage &&
                 (usage == kAcceleratorUsage ||
                  usage == kBrakeUsage)) ||
               (usagePage == kButtonUsagePage) ||
               (usagePage == kConsumerPage &&
                 (usage == kHomeUsage ||
                  usage == kBackUsage))) {
      Button button(int(buttons.Length()), element, IOHIDElementGetLogicalMin(element), IOHIDElementGetLogicalMax(element));
      buttons.AppendElement(button);
    } else {
      //TODO: handle other usage pages
    }
  }

  AxisComparator comparator;
  axes.Sort(comparator);
  for (unsigned i = 0; i < axes.Length(); i++) {
    axes[i].id = i;
  }
}

class DarwinGamepadService {
 private:
  IOHIDManagerRef mManager;
  vector<Gamepad> mGamepads;

  //Workaround to support running in background thread
  CFRunLoopRef mMonitorRunLoop;
  nsCOMPtr<nsIThread> mMonitorThread;

  static void DeviceAddedCallback(void* data, IOReturn result,
                                  void* sender, IOHIDDeviceRef device);
  static void DeviceRemovedCallback(void* data, IOReturn result,
                                    void* sender, IOHIDDeviceRef device);
  static void InputValueChangedCallback(void* data, IOReturn result,
                                        void* sender, IOHIDValueRef newValue);

  void DeviceAdded(IOHIDDeviceRef device);
  void DeviceRemoved(IOHIDDeviceRef device);
  void InputValueChanged(IOHIDValueRef value);
  void StartupInternal();

 public:
  DarwinGamepadService();
  ~DarwinGamepadService();
  void Startup();
  void Shutdown();
  friend class DarwinGamepadServiceStartupRunnable;
};

class DarwinGamepadServiceStartupRunnable final : public Runnable
{
 private:
  ~DarwinGamepadServiceStartupRunnable() {}
  // This Runnable schedules startup of DarwinGamepadService
  // in a new thread, pointer to DarwinGamepadService is only
  // used by this Runnable within its thread.
  DarwinGamepadService MOZ_NON_OWNING_REF *mService;
 public:
  explicit DarwinGamepadServiceStartupRunnable(DarwinGamepadService *service)
             : mService(service) {}
  NS_IMETHOD Run() override
  {
    MOZ_ASSERT(mService);
    mService->StartupInternal();
    return NS_OK;
  }
};

void
DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device)
{
  RefPtr<GamepadPlatformService> service =
    GamepadPlatformService::GetParentService();
  if (!service) {
    return;
  }

  size_t slot = size_t(-1);
  for (size_t i = 0; i < mGamepads.size(); i++) {
    if (mGamepads[i] == device)
      return;
    if (slot == size_t(-1) && mGamepads[i].empty())
      slot = i;
  }

  if (slot == size_t(-1)) {
    slot = mGamepads.size();
    mGamepads.push_back(Gamepad());
  }
  mGamepads[slot].init(device);

  // Gather some identifying information
  CFNumberRef vendorIdRef =
    (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
  CFNumberRef productIdRef =
    (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
  CFStringRef productRef =
    (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
  int vendorId, productId;
  CFNumberGetValue(vendorIdRef, kCFNumberIntType, &vendorId);
  CFNumberGetValue(productIdRef, kCFNumberIntType, &productId);
  char product_name[128];
  CFStringGetCString(productRef, product_name,
                     sizeof(product_name), kCFStringEncodingASCII);
  char buffer[256];
  sprintf(buffer, "%x-%x-%s", vendorId, productId, product_name);
  uint32_t index = service->AddGamepad(buffer,
                                       mozilla::dom::GamepadMappingType::_empty,
                                       (int)mGamepads[slot].numButtons(),
                                       (int)mGamepads[slot].numAxes());
  mGamepads[slot].mSuperIndex = index;
}

void
DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device)
{
  RefPtr<GamepadPlatformService> service =
    GamepadPlatformService::GetParentService();
  if (!service) {
    return;
  }
  for (size_t i = 0; i < mGamepads.size(); i++) {
    if (mGamepads[i] == device) {
      service->RemoveGamepad(mGamepads[i].mSuperIndex);
      mGamepads[i].clear();
      return;
    }
  }
}

/*
 * Given a value from a d-pad (POV hat in USB HID terminology),
 * represent it as 4 buttons, one for each cardinal direction.
 */
static void
UnpackDpad(int dpad_value, int min, int max, dpad_buttons& buttons)
{
  const unsigned kUp = 0;
  const unsigned kDown = 1;
  const unsigned kLeft = 2;
  const unsigned kRight = 3;

  // Different controllers have different ways of representing
  // "nothing is pressed", but they're all outside the range of values.
  if (dpad_value < min || dpad_value > max) {
    // Nothing is pressed.
    return;
  }

  // Normalize value to start at 0.
  int value = dpad_value - min;

  // Value will be in the range 0-7. The value represents the
  // position of the d-pad around a circle, with 0 being straight up,
  // 2 being right, 4 being straight down, and 6 being left.
  if (value < 2 || value > 6) {
    buttons[kUp] = true;
  }
  if (value > 2 && value < 6) {
    buttons[kDown] = true;
  }
  if (value > 4) {
    buttons[kLeft] = true;
  }
  if (value > 0 && value < 4) {
    buttons[kRight] = true;
  }
}

void
DarwinGamepadService::InputValueChanged(IOHIDValueRef value)
{
  RefPtr<GamepadPlatformService> service =
    GamepadPlatformService::GetParentService();
  if (!service) {
    return;
  }

  uint32_t value_length = IOHIDValueGetLength(value);
  if (value_length > 4) {
    // Workaround for bizarre issue with PS3 controllers that try to return
    // massive (30+ byte) values and crash IOHIDValueGetIntegerValue
    return;
  }
  IOHIDElementRef element = IOHIDValueGetElement(value);
  IOHIDDeviceRef device = IOHIDElementGetDevice(element);

  for (unsigned i = 0; i < mGamepads.size(); i++) {
    Gamepad &gamepad = mGamepads[i];
    if (gamepad == device) {
      if (gamepad.isDpad(element)) {
        const dpad_buttons& oldState = gamepad.getDpadState();
        dpad_buttons newState = { false, false, false, false };
        UnpackDpad(IOHIDValueGetIntegerValue(value),
                   IOHIDElementGetLogicalMin(element),
                   IOHIDElementGetLogicalMax(element),
                   newState);
        const int numButtons = gamepad.numButtons();
        for (unsigned b = 0; b < ArrayLength(newState); b++) {
          if (newState[b] != oldState[b]) {
            service->NewButtonEvent(gamepad.mSuperIndex,
                                    numButtons - 4 + b,
                                    newState[b]);
          }
        }
        gamepad.setDpadState(newState);
      } else if (const Axis* axis = gamepad.lookupAxis(element)) {
        double d = IOHIDValueGetIntegerValue(value);
        double v = 2.0f * (d - axis->min) /
          (double)(axis->max - axis->min) - 1.0f;
        service->NewAxisMoveEvent(gamepad.mSuperIndex, axis->id, v);
      } else if (const Button* button = gamepad.lookupButton(element)) {
        int iv = IOHIDValueGetIntegerValue(value);
        bool pressed = iv != 0;
        double v = 0;
        if (button->analog) {
          double dv = iv;
          v = (dv - button->min) / (double)(button->max - button->min);
        } else {
          v = pressed ? 1.0 : 0.0;
        }
        service->NewButtonEvent(gamepad.mSuperIndex, button->id, pressed, v);
      }
      return;
    }
  }
}

void
DarwinGamepadService::DeviceAddedCallback(void* data, IOReturn result,
                                         void* sender, IOHIDDeviceRef device)
{
  DarwinGamepadService* service = (DarwinGamepadService*)data;
  service->DeviceAdded(device);
}

void
DarwinGamepadService::DeviceRemovedCallback(void* data, IOReturn result,
                                           void* sender, IOHIDDeviceRef device)
{
  DarwinGamepadService* service = (DarwinGamepadService*)data;
  service->DeviceRemoved(device);
}

void
DarwinGamepadService::InputValueChangedCallback(void* data,
                                               IOReturn result,
                                               void* sender,
                                               IOHIDValueRef newValue)
{
  DarwinGamepadService* service = (DarwinGamepadService*)data;
  service->InputValueChanged(newValue);
}

static CFMutableDictionaryRef
MatchingDictionary(UInt32 inUsagePage, UInt32 inUsage)
{
  CFMutableDictionaryRef dict =
    CFDictionaryCreateMutable(kCFAllocatorDefault,
                              0,
                              &kCFTypeDictionaryKeyCallBacks,
                              &kCFTypeDictionaryValueCallBacks);
  if (!dict)
    return nullptr;
  CFNumberRef number = CFNumberCreate(kCFAllocatorDefault,
                                      kCFNumberIntType,
                                      &inUsagePage);
  if (!number) {
    CFRelease(dict);
    return nullptr;
  }
  CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsagePageKey), number);
  CFRelease(number);

  number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &inUsage);
  if (!number) {
    CFRelease(dict);
    return nullptr;
  }
  CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsageKey), number);
  CFRelease(number);

  return dict;
}

DarwinGamepadService::DarwinGamepadService() : mManager(nullptr) {}

DarwinGamepadService::~DarwinGamepadService()
{
  if (mManager != nullptr)
    CFRelease(mManager);
}

void
DarwinGamepadService::StartupInternal()
{
  if (mManager != nullptr)
    return;

  IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault,
                                               kIOHIDOptionsTypeNone);

  CFMutableDictionaryRef criteria_arr[2];
  criteria_arr[0] = MatchingDictionary(kDesktopUsagePage,
                                       kJoystickUsage);
  if (!criteria_arr[0]) {
    CFRelease(manager);
    return;
  }

  criteria_arr[1] = MatchingDictionary(kDesktopUsagePage,
                                       kGamepadUsage);
  if (!criteria_arr[1]) {
    CFRelease(criteria_arr[0]);
    CFRelease(manager);
    return;
  }

  CFArrayRef criteria =
    CFArrayCreate(kCFAllocatorDefault, (const void**)criteria_arr, 2, nullptr);
  if (!criteria) {
    CFRelease(criteria_arr[1]);
    CFRelease(criteria_arr[0]);
    CFRelease(manager);
    return;
  }

  IOHIDManagerSetDeviceMatchingMultiple(manager, criteria);
  CFRelease(criteria);
  CFRelease(criteria_arr[1]);
  CFRelease(criteria_arr[0]);

  IOHIDManagerRegisterDeviceMatchingCallback(manager,
                                             DeviceAddedCallback,
                                             this);
  IOHIDManagerRegisterDeviceRemovalCallback(manager,
                                            DeviceRemovedCallback,
                                            this);
  IOHIDManagerRegisterInputValueCallback(manager,
                                         InputValueChangedCallback,
                                         this);
  IOHIDManagerScheduleWithRunLoop(manager,
                                  CFRunLoopGetCurrent(),
                                  kCFRunLoopDefaultMode);
  IOReturn rv = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone);
  if (rv != kIOReturnSuccess) {
    CFRelease(manager);
    return;
  }

  mManager = manager;

  // We held the handle of the CFRunLoop to make sure we
  // can shut it down explicitly by CFRunLoopStop in another
  // thread.
  mMonitorRunLoop = CFRunLoopGetCurrent();

  // CFRunLoopRun() is a blocking message loop when it's called in
  // non-main thread so this thread cannot receive any other runnables
  // and nsITimer timeout events after it's called.
  CFRunLoopRun();
}

void DarwinGamepadService::Startup()
{
  Unused << NS_NewThread(getter_AddRefs(mMonitorThread),
                         new DarwinGamepadServiceStartupRunnable(this));
}

void DarwinGamepadService::Shutdown()
{
  IOHIDManagerRef manager = (IOHIDManagerRef)mManager;
  CFRunLoopStop(mMonitorRunLoop);
  if (manager) {
    IOHIDManagerClose(manager, 0);
    CFRelease(manager);
    mManager = nullptr;
  }
  mMonitorThread->Shutdown();
}

} // namespace

namespace mozilla {
namespace dom {

DarwinGamepadService* gService = nullptr;

void StartGamepadMonitoring()
{
  if (gService) {
    return;
  }

  gService = new DarwinGamepadService();
  gService->Startup();
}

void StopGamepadMonitoring()
{
  if (!gService) {
    return;
  }

  gService->Shutdown();
  delete gService;
  gService = nullptr;
}

} // namespace dom
} // namespace mozilla