// Copyright 2014 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/hid/hid_service_mac.h"

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/hid/IOHIDManager.h>

#include <set>
#include <string>
#include <vector>

#include "base/bind.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/message_loop/message_loop.h"
#include "base/message_loop/message_loop_proxy.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "device/hid/hid_connection_mac.h"

namespace device {

class HidServiceMac;

namespace {

typedef std::vector<IOHIDDeviceRef> HidDeviceList;

HidServiceMac* HidServiceFromContext(void* context) {
  return static_cast<HidServiceMac*>(context);
}

// Callback for CFSetApplyFunction as used by EnumerateHidDevices.
void HidEnumerationBackInserter(const void* value, void* context) {
  HidDeviceList* devices = static_cast<HidDeviceList*>(context);
  const IOHIDDeviceRef device =
      static_cast<IOHIDDeviceRef>(const_cast<void*>(value));
  devices->push_back(device);
}

void EnumerateHidDevices(IOHIDManagerRef hid_manager,
                         HidDeviceList* device_list) {
  DCHECK(device_list->size() == 0);
  // Note that our ownership of each copied device is implied.
  base::ScopedCFTypeRef<CFSetRef> devices(IOHIDManagerCopyDevices(hid_manager));
  if (devices)
    CFSetApplyFunction(devices, HidEnumerationBackInserter, device_list);
}

bool TryGetHidIntProperty(IOHIDDeviceRef device,
                          CFStringRef key,
                          int32_t* result) {
  CFNumberRef ref =
      base::mac::CFCast<CFNumberRef>(IOHIDDeviceGetProperty(device, key));
  return ref && CFNumberGetValue(ref, kCFNumberSInt32Type, result);
}

int32_t GetHidIntProperty(IOHIDDeviceRef device, CFStringRef key) {
  int32_t value;
  if (TryGetHidIntProperty(device, key, &value))
    return value;
  return 0;
}

bool TryGetHidStringProperty(IOHIDDeviceRef device,
                             CFStringRef key,
                             std::string* result) {
  CFStringRef ref =
      base::mac::CFCast<CFStringRef>(IOHIDDeviceGetProperty(device, key));
  if (!ref) {
    return false;
  }
  *result = base::SysCFStringRefToUTF8(ref);
  return true;
}

std::string GetHidStringProperty(IOHIDDeviceRef device, CFStringRef key) {
  std::string value;
  TryGetHidStringProperty(device, key, &value);
  return value;
}

void GetReportIds(IOHIDElementRef element, std::set<int>* reportIDs) {
  uint32_t reportID = IOHIDElementGetReportID(element);
  if (reportID) {
    reportIDs->insert(reportID);
  }

  CFArrayRef children = IOHIDElementGetChildren(element);
  if (!children) {
    return;
  }

  CFIndex childrenCount = CFArrayGetCount(children);
  for (CFIndex j = 0; j < childrenCount; ++j) {
    const IOHIDElementRef child = static_cast<IOHIDElementRef>(
        const_cast<void*>(CFArrayGetValueAtIndex(children, j)));
    GetReportIds(child, reportIDs);
  }
}

void GetCollectionInfos(IOHIDDeviceRef device,
                        bool* has_report_id,
                        std::vector<HidCollectionInfo>* top_level_collections) {
  STLClearObject(top_level_collections);
  CFMutableDictionaryRef collections_filter =
      CFDictionaryCreateMutable(kCFAllocatorDefault,
                                0,
                                &kCFTypeDictionaryKeyCallBacks,
                                &kCFTypeDictionaryValueCallBacks);
  const int kCollectionTypeValue = kIOHIDElementTypeCollection;
  CFNumberRef collection_type_id = CFNumberCreate(
      kCFAllocatorDefault, kCFNumberIntType, &kCollectionTypeValue);
  CFDictionarySetValue(
      collections_filter, CFSTR(kIOHIDElementTypeKey), collection_type_id);
  CFRelease(collection_type_id);
  CFArrayRef collections = IOHIDDeviceCopyMatchingElements(
      device, collections_filter, kIOHIDOptionsTypeNone);
  CFIndex collectionsCount = CFArrayGetCount(collections);
  *has_report_id = false;
  for (CFIndex i = 0; i < collectionsCount; i++) {
    const IOHIDElementRef collection = static_cast<IOHIDElementRef>(
        const_cast<void*>(CFArrayGetValueAtIndex(collections, i)));
    // Top-Level Collection has no parent
    if (IOHIDElementGetParent(collection) == 0) {
      HidCollectionInfo collection_info;
      HidUsageAndPage::Page page = static_cast<HidUsageAndPage::Page>(
          IOHIDElementGetUsagePage(collection));
      uint16_t usage = IOHIDElementGetUsage(collection);
      collection_info.usage = HidUsageAndPage(usage, page);
      // Explore children recursively and retrieve their report IDs
      GetReportIds(collection, &collection_info.report_ids);
      if (collection_info.report_ids.size() > 0) {
        *has_report_id = true;
      }
      top_level_collections->push_back(collection_info);
    }
  }
}

}  // namespace

HidServiceMac::HidServiceMac() {
  DCHECK(thread_checker_.CalledOnValidThread());
  message_loop_ = base::MessageLoopProxy::current();
  DCHECK(message_loop_);
  hid_manager_.reset(IOHIDManagerCreate(NULL, 0));
  if (!hid_manager_) {
    LOG(ERROR) << "Failed to initialize HidManager";
    return;
  }
  DCHECK(CFGetTypeID(hid_manager_) == IOHIDManagerGetTypeID());
  IOHIDManagerOpen(hid_manager_, kIOHIDOptionsTypeNone);
  IOHIDManagerSetDeviceMatching(hid_manager_, NULL);

  // Enumerate all the currently known devices.
  Enumerate();

  // Register for plug/unplug notifications.
  StartWatchingDevices();
}

HidServiceMac::~HidServiceMac() {
  StopWatchingDevices();
}

void HidServiceMac::StartWatchingDevices() {
  DCHECK(thread_checker_.CalledOnValidThread());
  IOHIDManagerRegisterDeviceMatchingCallback(
      hid_manager_, &AddDeviceCallback, this);
  IOHIDManagerRegisterDeviceRemovalCallback(
      hid_manager_, &RemoveDeviceCallback, this);
  IOHIDManagerScheduleWithRunLoop(
      hid_manager_, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
}

void HidServiceMac::StopWatchingDevices() {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!hid_manager_)
    return;
  IOHIDManagerUnscheduleFromRunLoop(
      hid_manager_, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
  IOHIDManagerClose(hid_manager_, kIOHIDOptionsTypeNone);
}

void HidServiceMac::AddDeviceCallback(void* context,
                                      IOReturn result,
                                      void* sender,
                                      IOHIDDeviceRef hid_device) {
  DCHECK(CFRunLoopGetMain() == CFRunLoopGetCurrent());
  // Claim ownership of the device.
  CFRetain(hid_device);
  HidServiceMac* service = HidServiceFromContext(context);
  service->message_loop_->PostTask(FROM_HERE,
                                   base::Bind(&HidServiceMac::PlatformAddDevice,
                                              base::Unretained(service),
                                              base::Unretained(hid_device)));
}

void HidServiceMac::RemoveDeviceCallback(void* context,
                                         IOReturn result,
                                         void* sender,
                                         IOHIDDeviceRef hid_device) {
  DCHECK(CFRunLoopGetMain() == CFRunLoopGetCurrent());
  HidServiceMac* service = HidServiceFromContext(context);
  service->message_loop_->PostTask(
      FROM_HERE,
      base::Bind(&HidServiceMac::PlatformRemoveDevice,
                 base::Unretained(service),
                 base::Unretained(hid_device)));
}

void HidServiceMac::Enumerate() {
  DCHECK(thread_checker_.CalledOnValidThread());
  HidDeviceList devices;
  EnumerateHidDevices(hid_manager_, &devices);
  for (HidDeviceList::const_iterator iter = devices.begin();
       iter != devices.end();
       ++iter) {
    IOHIDDeviceRef hid_device = *iter;
    PlatformAddDevice(hid_device);
  }
}

void HidServiceMac::PlatformAddDevice(IOHIDDeviceRef hid_device) {
  // Note that our ownership of hid_device is implied if calling this method.
  // It is balanced in PlatformRemoveDevice.
  DCHECK(thread_checker_.CalledOnValidThread());
  HidDeviceInfo device_info;
  device_info.device_id = hid_device;
  device_info.vendor_id =
      GetHidIntProperty(hid_device, CFSTR(kIOHIDVendorIDKey));
  device_info.product_id =
      GetHidIntProperty(hid_device, CFSTR(kIOHIDProductIDKey));
  device_info.product_name =
      GetHidStringProperty(hid_device, CFSTR(kIOHIDProductKey));
  device_info.serial_number =
      GetHidStringProperty(hid_device, CFSTR(kIOHIDSerialNumberKey));
  GetCollectionInfos(hid_device,
                     &device_info.has_report_id,
                     &device_info.collections);
  device_info.max_input_report_size =
      GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxInputReportSizeKey));
  if (device_info.has_report_id && device_info.max_input_report_size > 0) {
    device_info.max_input_report_size--;
  }
  device_info.max_output_report_size =
      GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxOutputReportSizeKey));
  if (device_info.has_report_id && device_info.max_output_report_size > 0) {
    device_info.max_output_report_size--;
  }
  device_info.max_feature_report_size =
      GetHidIntProperty(hid_device, CFSTR(kIOHIDMaxFeatureReportSizeKey));
  if (device_info.has_report_id && device_info.max_feature_report_size > 0) {
    device_info.max_feature_report_size--;
  }
  AddDevice(device_info);
}

void HidServiceMac::PlatformRemoveDevice(IOHIDDeviceRef hid_device) {
  DCHECK(thread_checker_.CalledOnValidThread());
  RemoveDevice(hid_device);
  CFRelease(hid_device);
}

scoped_refptr<HidConnection> HidServiceMac::Connect(
    const HidDeviceId& device_id) {
  DCHECK(thread_checker_.CalledOnValidThread());
  HidDeviceInfo device_info;
  if (!GetDeviceInfo(device_id, &device_info))
    return NULL;
  return scoped_refptr<HidConnection>(new HidConnectionMac(device_info));
}

}  // namespace device