// Copyright (c) 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_connection_mac.h"

#include "base/bind.h"
#include "base/mac/foundation_util.h"
#include "base/message_loop/message_loop.h"
#include "base/threading/thread_restrictions.h"
#include "device/hid/hid_connection_mac.h"

namespace device {

HidConnectionMac::HidConnectionMac(HidDeviceInfo device_info)
    : HidConnection(device_info),
      device_(device_info.device_id, base::scoped_policy::RETAIN) {
  DCHECK(thread_checker_.CalledOnValidThread());

  message_loop_ = base::MessageLoopProxy::current();

  DCHECK(device_.get());
  inbound_buffer_.reset((uint8_t*)malloc(device_info.input_report_size));
  IOHIDDeviceRegisterInputReportCallback(device_.get(),
                                         inbound_buffer_.get(),
                                         device_info.input_report_size,
                                         &HidConnectionMac::InputReportCallback,
                                         this);
  IOHIDDeviceOpen(device_, kIOHIDOptionsTypeNone);
}

HidConnectionMac::~HidConnectionMac() {
  DCHECK(thread_checker_.CalledOnValidThread());

  while (!pending_reads_.empty()) {
    pending_reads_.front().callback.Run(false, 0);
    pending_reads_.pop();
  }

  IOHIDDeviceClose(device_, kIOHIDOptionsTypeNone);
}

void HidConnectionMac::Read(scoped_refptr<net::IOBufferWithSize> buffer,
                            const IOCallback& callback) {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!device_) {
    callback.Run(false, 0);
    return;
  }
  PendingHidRead read;
  read.buffer = buffer;
  read.callback = callback;
  pending_reads_.push(read);
  ProcessReadQueue();
}

void HidConnectionMac::Write(uint8_t report_id,
                             scoped_refptr<net::IOBufferWithSize> buffer,
                             const IOCallback& callback) {
  DCHECK(thread_checker_.CalledOnValidThread());
  WriteReport(kIOHIDReportTypeOutput, report_id, buffer, callback);
}

void HidConnectionMac::GetFeatureReport(
    uint8_t report_id,
    scoped_refptr<net::IOBufferWithSize> buffer,
    const IOCallback& callback) {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (device_info().feature_report_size == 0) {
    callback.Run(false, 0);
    return;
  }

  if (buffer->size() < device_info().feature_report_size) {
    callback.Run(false, 0);
    return;
  }

  uint8_t* feature_report_buffer = reinterpret_cast<uint8_t*>(buffer->data());
  CFIndex feature_report_size = device_info().feature_report_size;
  IOReturn result = IOHIDDeviceGetReport(device_,
                                         kIOHIDReportTypeFeature,
                                         report_id,
                                         feature_report_buffer,
                                         &feature_report_size);
  if (result == kIOReturnSuccess)
    callback.Run(true, feature_report_size);
  else
    callback.Run(false, 0);
}

void HidConnectionMac::SendFeatureReport(
    uint8_t report_id,
    scoped_refptr<net::IOBufferWithSize> buffer,
    const IOCallback& callback) {
  DCHECK(thread_checker_.CalledOnValidThread());
  WriteReport(kIOHIDReportTypeFeature, report_id, buffer, callback);
}

void HidConnectionMac::InputReportCallback(void* context,
                                           IOReturn result,
                                           void* sender,
                                           IOHIDReportType type,
                                           uint32_t report_id,
                                           uint8_t* report_bytes,
                                           CFIndex report_length) {
  HidConnectionMac* connection = static_cast<HidConnectionMac*>(context);
  // report_id is already contained in report_bytes
  scoped_refptr<net::IOBufferWithSize> buffer;
  buffer = new net::IOBufferWithSize(report_length);
  memcpy(buffer->data(), report_bytes, report_length);

  connection->message_loop_->PostTask(
      FROM_HERE,
      base::Bind(
          &HidConnectionMac::ProcessInputReport, connection, type, buffer));
}

void HidConnectionMac::ProcessReadQueue() {
  DCHECK(thread_checker_.CalledOnValidThread());
  while (pending_reads_.size() && pending_reports_.size()) {
    PendingHidRead read = pending_reads_.front();
    pending_reads_.pop();
    PendingHidReport report = pending_reports_.front();
    if (read.buffer->size() < report.buffer->size()) {
      read.callback.Run(false, report.buffer->size());
    } else {
      memcpy(read.buffer->data(), report.buffer->data(), report.buffer->size());
      pending_reports_.pop();
      read.callback.Run(true, report.buffer->size());
    }
  }
}

void HidConnectionMac::ProcessInputReport(
    IOHIDReportType type,
    scoped_refptr<net::IOBufferWithSize> buffer) {
  DCHECK(thread_checker_.CalledOnValidThread());
  PendingHidReport report;
  report.buffer = buffer;
  pending_reports_.push(report);
  ProcessReadQueue();
}

void HidConnectionMac::WriteReport(IOHIDReportType type,
                                   uint8_t report_id,
                                   scoped_refptr<net::IOBufferWithSize> buffer,
                                   const IOCallback& callback) {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!device_) {
    callback.Run(false, 0);
    return;
  }
  scoped_refptr<net::IOBufferWithSize> output_buffer;
  if (report_id != 0) {
    output_buffer = new net::IOBufferWithSize(buffer->size() + 1);
    output_buffer->data()[0] = static_cast<uint8_t>(report_id);
    memcpy(output_buffer->data() + 1, buffer->data(), buffer->size());
  } else {
    output_buffer = new net::IOBufferWithSize(buffer->size());
    memcpy(output_buffer->data(), buffer->data(), buffer->size());
  }
  IOReturn res =
      IOHIDDeviceSetReport(device_.get(),
                           type,
                           report_id,
                           reinterpret_cast<uint8_t*>(output_buffer->data()),
                           output_buffer->size());
  if (res != kIOReturnSuccess) {
    callback.Run(false, 0);
  } else {
    callback.Run(true, output_buffer->size());
  }
}

}  // namespace device