// Copyright 2013 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 "device/gamepad/xbox_data_fetcher_mac.h"

#include <algorithm>
#include <cmath>
#include <limits>

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/usb/USB.h>

#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"

namespace device {

namespace {

void CopyToUString(UChar* dest, size_t dest_length, base::string16 src) {
  static_assert(sizeof(base::string16::value_type) == sizeof(UChar),
                "Mismatched string16/WebUChar size.");

  const size_t str_to_copy = std::min(src.size(), dest_length - 1);
  src.copy(dest, str_to_copy);
  std::fill(dest + str_to_copy, dest + dest_length, 0);
}

}  // namespace

XboxDataFetcher::PendingController::PendingController(
    XboxDataFetcher* fetcher,
    std::unique_ptr<XboxControllerMac> controller)
    : fetcher(fetcher), controller(std::move(controller)) {}

XboxDataFetcher::PendingController::~PendingController() = default;

XboxDataFetcher::XboxDataFetcher() = default;

XboxDataFetcher::~XboxDataFetcher() {
  while (!controllers_.empty()) {
    RemoveController(*controllers_.begin());
  }
  UnregisterFromNotifications();
}

GamepadSource XboxDataFetcher::source() {
  return Factory::static_source();
}

void XboxDataFetcher::GetGamepadData(bool devices_changed_hint) {
  // This just loops through all the connected pads and "pings" them to indicate
  // that they're still active.
  for (auto* controller : controllers_) {
    GetPadState(controller->location_id());
  }
}

void XboxDataFetcher::PlayEffect(
    int source_id,
    mojom::GamepadHapticEffectType type,
    mojom::GamepadEffectParametersPtr params,
    mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback) {
  XboxControllerMac* controller = ControllerForLocation(source_id);
  if (!controller) {
    std::move(callback).Run(
        mojom::GamepadHapticsResult::GamepadHapticsResultError);
    return;
  }

  controller->PlayEffect(type, std::move(params), std::move(callback));
}

void XboxDataFetcher::ResetVibration(
    int source_id,
    mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback) {
  XboxControllerMac* controller = ControllerForLocation(source_id);
  if (!controller) {
    std::move(callback).Run(
        mojom::GamepadHapticsResult::GamepadHapticsResultError);
    return;
  }

  controller->ResetVibration(std::move(callback));
}

void XboxDataFetcher::OnAddedToProvider() {
  RegisterForNotifications();
}

// static
void XboxDataFetcher::DeviceAdded(void* context, io_iterator_t iterator) {
  DCHECK(context);
  XboxDataFetcher* fetcher = static_cast<XboxDataFetcher*>(context);
  io_service_t ref;
  while ((ref = IOIteratorNext(iterator))) {
    base::mac::ScopedIOObject<io_service_t> scoped_ref(ref);
    fetcher->TryOpenDevice(ref);
  }
}

// static
void XboxDataFetcher::DeviceRemoved(void* context, io_iterator_t iterator) {
  DCHECK(context);
  XboxDataFetcher* fetcher = static_cast<XboxDataFetcher*>(context);
  io_service_t ref;
  while ((ref = IOIteratorNext(iterator))) {
    base::mac::ScopedIOObject<io_service_t> scoped_ref(ref);
    base::ScopedCFTypeRef<CFNumberRef> number(
        base::mac::CFCastStrict<CFNumberRef>(IORegistryEntryCreateCFProperty(
            ref, CFSTR(kUSBDevicePropertyLocationID), kCFAllocatorDefault,
            kNilOptions)));
    UInt32 location_id = 0;
    CFNumberGetValue(number, kCFNumberSInt32Type, &location_id);
    fetcher->RemoveControllerByLocationID(location_id);
  }
}

// static
void XboxDataFetcher::InterestCallback(void* context,
                                       io_service_t service,
                                       IOMessage message_type,
                                       void* message_argument) {
  if (message_type == kIOMessageServiceWasClosed) {
    PendingController* pending = static_cast<PendingController*>(context);
    pending->fetcher->PendingControllerBecameAvailable(service, pending);
  }
}

void XboxDataFetcher::PendingControllerBecameAvailable(
    io_service_t service,
    PendingController* pending) {
  // Destroying the PendingController object unregisters our interest
  // notification.
  for (auto it = pending_controllers_.begin(); it != pending_controllers_.end();
       ++it) {
    if (pending == it->get()) {
      pending_controllers_.erase(it);
      break;
    }
  }
  TryOpenDevice(service);
}

bool XboxDataFetcher::TryOpenDevice(io_service_t service) {
  auto pending = std::make_unique<PendingController>(
      this, std::make_unique<XboxControllerMac>(this));
  bool did_register_interest =
      RegisterForInterestNotifications(service, pending.get());

  auto* controller = pending->controller.get();
  XboxControllerMac::OpenDeviceResult result = controller->OpenDevice(service);
  if (result == XboxControllerMac::OpenDeviceResult::OPEN_SUCCEEDED) {
    AddController(pending->controller.release());
    return true;
  }

  if (did_register_interest &&
      result ==
          XboxControllerMac::OpenDeviceResult::OPEN_FAILED_EXCLUSIVE_ACCESS) {
    pending_controllers_.insert(std::move(pending));
  }
  return false;
}

bool XboxDataFetcher::RegisterForNotifications() {
  if (listening_)
    return true;
  if (port_ == nullptr)
    port_.reset(IONotificationPortCreate(kIOMasterPortDefault));
  if (!port_.is_valid())
    return false;
  source_ = IONotificationPortGetRunLoopSource(port_.get());
  if (!source_)
    return false;
  CFRunLoopAddSource(CFRunLoopGetCurrent(), source_, kCFRunLoopDefaultMode);

  listening_ = true;

  if (!RegisterForDeviceNotifications(
          XboxControllerMac::kVendorMicrosoft,
          XboxControllerMac::kProductXboxOneEliteController,
          &xbox_one_elite_device_added_iter_,
          &xbox_one_elite_device_removed_iter_))
    return false;

  if (!RegisterForDeviceNotifications(
          XboxControllerMac::kVendorMicrosoft,
          XboxControllerMac::kProductXboxOneController2013,
          &xbox_one_2013_device_added_iter_,
          &xbox_one_2013_device_removed_iter_))
    return false;

  if (!RegisterForDeviceNotifications(
          XboxControllerMac::kVendorMicrosoft,
          XboxControllerMac::kProductXboxOneController2015,
          &xbox_one_2015_device_added_iter_,
          &xbox_one_2015_device_removed_iter_))
    return false;

  if (!RegisterForDeviceNotifications(
          XboxControllerMac::kVendorMicrosoft,
          XboxControllerMac::kProductXboxOneSController,
          &xbox_one_s_device_added_iter_, &xbox_one_s_device_removed_iter_))
    return false;

  if (!RegisterForDeviceNotifications(
          XboxControllerMac::kVendorMicrosoft,
          XboxControllerMac::kProductXbox360Controller,
          &xbox_360_device_added_iter_, &xbox_360_device_removed_iter_))
    return false;

  return true;
}

bool XboxDataFetcher::RegisterForDeviceNotifications(
    int vendor_id,
    int product_id,
    base::mac::ScopedIOObject<io_iterator_t>* added_iter,
    base::mac::ScopedIOObject<io_iterator_t>* removed_iter) {
  base::ScopedCFTypeRef<CFNumberRef> vendor_cf(
      CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendor_id));
  base::ScopedCFTypeRef<CFNumberRef> product_cf(
      CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &product_id));
  base::ScopedCFTypeRef<CFMutableDictionaryRef> matching_dict(
      IOServiceMatching(kIOUSBDeviceClassName));
  if (!matching_dict)
    return false;
  CFDictionarySetValue(matching_dict, CFSTR(kUSBVendorID), vendor_cf);
  CFDictionarySetValue(matching_dict, CFSTR(kUSBProductID), product_cf);

  // IOServiceAddMatchingNotification() releases the dictionary when it's done.
  // Retain it before each call to IOServiceAddMatchingNotification to keep
  // things balanced.
  CFRetain(matching_dict);
  IOReturn ret;
  ret = IOServiceAddMatchingNotification(port_.get(), kIOFirstMatchNotification,
                                         matching_dict, DeviceAdded, this,
                                         added_iter->InitializeInto());
  if (ret != kIOReturnSuccess) {
    LOG(ERROR) << "Error listening for Xbox controller add events: " << ret;
    return false;
  }
  DeviceAdded(this, added_iter->get());

  CFRetain(matching_dict);
  ret = IOServiceAddMatchingNotification(port_.get(), kIOTerminatedNotification,
                                         matching_dict, DeviceRemoved, this,
                                         removed_iter->InitializeInto());
  if (ret != kIOReturnSuccess) {
    LOG(ERROR) << "Error listening for Xbox controller remove events: " << ret;
    return false;
  }
  DeviceRemoved(this, removed_iter->get());
  return true;
}

bool XboxDataFetcher::RegisterForInterestNotifications(
    io_service_t service,
    PendingController* pending) {
  if (port_ == nullptr)
    port_.reset(IONotificationPortCreate(kIOMasterPortDefault));
  if (!port_.is_valid())
    return false;

  kern_return_t kr = IOServiceAddInterestNotification(
      port_.get(), service, kIOGeneralInterest, InterestCallback, pending,
      pending->notify.InitializeInto());
  return kr == KERN_SUCCESS;
}

void XboxDataFetcher::UnregisterFromNotifications() {
  if (!listening_)
    return;
  listening_ = false;
  if (source_)
    CFRunLoopSourceInvalidate(source_);
  port_.reset();
  pending_controllers_.clear();
}

XboxControllerMac* XboxDataFetcher::ControllerForLocation(UInt32 location_id) {
  for (std::set<XboxControllerMac*>::iterator i = controllers_.begin();
       i != controllers_.end(); ++i) {
    if ((*i)->location_id() == location_id)
      return *i;
  }
  return NULL;
}

void XboxDataFetcher::AddController(XboxControllerMac* controller) {
  DCHECK(!ControllerForLocation(controller->location_id()))
      << "Controller with location ID " << controller->location_id()
      << " already exists in the set of controllers.";
  PadState* state = GetPadState(controller->location_id());
  if (!state) {
    delete controller;
    return;  // No available slot for this device
  }

  controllers_.insert(controller);

  controller->SetLEDPattern((XboxControllerMac::LEDPattern)(
      XboxControllerMac::LED_FLASH_TOP_LEFT + controller->location_id()));

  CopyToUString(state->data.id, arraysize(state->data.id),
                base::UTF8ToUTF16(controller->GetIdString()));
  CopyToUString(state->data.mapping, arraysize(state->data.mapping),
                base::UTF8ToUTF16("standard"));

  state->data.connected = true;
  state->data.axes_length = 4;
  state->data.buttons_length = 17;
  state->data.timestamp = CurrentTimeInMicroseconds();
  state->mapper = 0;
  state->axis_mask = 0;
  state->button_mask = 0;

  // Assume all Xbox gamepads support vibration effects.
  state->data.vibration_actuator.type = GamepadHapticActuatorType::kDualRumble;
  state->data.vibration_actuator.not_null = true;
}

void XboxDataFetcher::RemoveController(XboxControllerMac* controller) {
  controllers_.erase(controller);
  delete controller;
}

void XboxDataFetcher::RemoveControllerByLocationID(uint32_t location_id) {
  XboxControllerMac* controller = NULL;
  for (std::set<XboxControllerMac*>::iterator i = controllers_.begin();
       i != controllers_.end(); ++i) {
    if ((*i)->location_id() == location_id) {
      controller = *i;
      break;
    }
  }
  if (controller)
    RemoveController(controller);
}

void XboxDataFetcher::XboxControllerGotData(
    XboxControllerMac* controller,
    const XboxControllerMac::Data& data) {
  PadState* state = GetPadState(controller->location_id());
  if (!state)
    return;  // No available slot for this device

  Gamepad& pad = state->data;

  for (size_t i = 0; i < 6; i++) {
    pad.buttons[i].pressed = data.buttons[i];
    pad.buttons[i].value = data.buttons[i] ? 1.0f : 0.0f;
  }
  pad.buttons[6].pressed = data.triggers[0] > kDefaultButtonPressedThreshold;
  pad.buttons[6].value = data.triggers[0];
  pad.buttons[7].pressed = data.triggers[1] > kDefaultButtonPressedThreshold;
  pad.buttons[7].value = data.triggers[1];
  for (size_t i = 8; i < 16; i++) {
    pad.buttons[i].pressed = data.buttons[i - 2];
    pad.buttons[i].value = data.buttons[i - 2] ? 1.0f : 0.0f;
  }
  if (controller->GetControllerType() ==
      XboxControllerMac::XBOX_360_CONTROLLER) {
    pad.buttons[16].pressed = data.buttons[14];
    pad.buttons[16].value = data.buttons[14] ? 1.0f : 0.0f;
  }
  for (size_t i = 0; i < arraysize(data.axes); i++) {
    pad.axes[i] = data.axes[i];
  }

  pad.timestamp = CurrentTimeInMicroseconds();
}

void XboxDataFetcher::XboxControllerGotGuideData(XboxControllerMac* controller,
                                                 bool guide) {
  PadState* state = GetPadState(controller->location_id());
  if (!state)
    return;  // No available slot for this device

  Gamepad& pad = state->data;

  pad.buttons[16].pressed = guide;
  pad.buttons[16].value = guide ? 1.0f : 0.0f;

  pad.timestamp = CurrentTimeInMicroseconds();
}

void XboxDataFetcher::XboxControllerError(XboxControllerMac* controller) {
  RemoveController(controller);
}

}  // namespace device
