#define BLUETOOTH_VERSION_USE_CURRENT

#include "Common/Common.h"
#include "Common/Logging/Log.h"
#include "Core/HW/WiimoteEmu/WiimoteHid.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"

@interface SearchBT : NSObject
{
@public
  unsigned int maxDevices;
  bool done;
}
@end

@interface ConnectBT : NSObject
{
}
@end

namespace WiimoteReal
{
class WiimoteDarwin final : public Wiimote
{
public:
  WiimoteDarwin(IOBluetoothDevice* device);
  ~WiimoteDarwin() override;

  // These are not protected/private because ConnectBT needs them.
  void DisconnectInternal() override;
  IOBluetoothDevice* m_btd;
  unsigned char* m_input;
  int m_inputlen;

protected:
  bool ConnectInternal() override;
  bool IsConnected() const override;
  void IOWakeup() override;
  int IORead(u8* buf) override;
  int IOWrite(u8 const* buf, size_t len) override;
  void EnablePowerAssertionInternal() override;
  void DisablePowerAssertionInternal() override;

private:
  IOBluetoothL2CAPChannel* m_ichan;
  IOBluetoothL2CAPChannel* m_cchan;
  bool m_connected;
  CFRunLoopRef m_wiimote_thread_run_loop;
  IOPMAssertionID m_pm_assertion;
};

class WiimoteDarwinHid final : public Wiimote
{
public:
  WiimoteDarwinHid(IOHIDDeviceRef device);
  ~WiimoteDarwinHid() override;

protected:
  bool ConnectInternal() override;
  void DisconnectInternal() override;
  bool IsConnected() const override;
  void IOWakeup() override;
  int IORead(u8* buf) override;
  int IOWrite(u8 const* buf, size_t len) override;

private:
  static void ReportCallback(void* context, IOReturn result, void* sender, IOHIDReportType type,
                             u32 reportID, u8* report, CFIndex reportLength);
  static void RemoveCallback(void* context, IOReturn result, void* sender);
  void QueueBufferReport(int length);
  IOHIDDeviceRef m_device;
  bool m_connected;
  std::atomic<bool> m_interrupted;
  Report m_report_buffer;
  Common::FifoQueue<Report> m_buffered_reports;
};

WiimoteScanner::WiimoteScanner()
{
}

WiimoteScanner::~WiimoteScanner()
{
}

void WiimoteScanner::Update()
{
}

void WiimoteScanner::FindWiimotes(std::vector<Wiimote*>& found_wiimotes, Wiimote*& found_board)
{
  // TODO: find the device in the constructor and save it for later
  IOBluetoothHostController* bth;
  IOBluetoothDeviceInquiry* bti;
  IOHIDManagerRef hid;
  SearchBT* sbt;
  NSEnumerator* en;
  found_board = nullptr;

  bool btFailed = false;
  bool hidFailed = false;

  bth = [[IOBluetoothHostController alloc] init];
  btFailed = [bth addressAsString] == nil;
  if (btFailed)
    WARN_LOG(WIIMOTE, "No Bluetooth host controller");

  hid = IOHIDManagerCreate(NULL, kIOHIDOptionsTypeNone);
  hidFailed = CFGetTypeID(hid) != IOHIDManagerGetTypeID();
  if (hidFailed)
    WARN_LOG(WIIMOTE, "No HID manager");

  if (hidFailed && btFailed)
  {
    CFRelease(hid);
    [bth release];
    return;
  }

  if (!btFailed)
  {
    sbt = [[SearchBT alloc] init];
    sbt->maxDevices = 32;
    bti = [[IOBluetoothDeviceInquiry alloc] init];
    [bti setDelegate:sbt];
    [bti setInquiryLength:2];

    if ([bti start] != kIOReturnSuccess)
    {
      ERROR_LOG(WIIMOTE, "Unable to do Bluetooth discovery");
      [bth release];
      [sbt release];
      btFailed = true;
    }

    do
    {
      CFRunLoopRun();
    } while (!sbt->done);

    int found_devices = [[bti foundDevices] count];

    if (found_devices)
      NOTICE_LOG(WIIMOTE, "Found %i Bluetooth devices", found_devices);

    en = [[bti foundDevices] objectEnumerator];
    for (int i = 0; i < found_devices; i++)
    {
      IOBluetoothDevice* dev = [en nextObject];
      if (!IsValidBluetoothName([[dev name] UTF8String]))
        continue;

      Wiimote* wm = new WiimoteDarwin([dev retain]);

      if (IsBalanceBoardName([[dev name] UTF8String]))
      {
        found_board = wm;
      }
      else
      {
        found_wiimotes.push_back(wm);
      }
    }

    [bth release];
    [bti release];
    [sbt release];
  }

  if (!hidFailed)
  {
    NSArray* criteria = @[
      @{ @kIOHIDVendorIDKey : @0x057e,
         @kIOHIDProductIDKey : @0x0306 },
      @{ @kIOHIDVendorIDKey : @0x057e,
         @kIOHIDProductIDKey : @0x0330 },
    ];
    IOHIDManagerSetDeviceMatchingMultiple(hid, (CFArrayRef)criteria);
    if (IOHIDManagerOpen(hid, kIOHIDOptionsTypeNone) != kIOReturnSuccess)
      WARN_LOG(WIIMOTE, "Failed to open HID Manager");
    CFSetRef devices = IOHIDManagerCopyDevices(hid);
    if (devices)
    {
      int found_devices = CFSetGetCount(devices);
      if (found_devices)
      {
        NOTICE_LOG(WIIMOTE, "Found %i HID devices", found_devices);

        IOHIDDeviceRef values[found_devices];
        CFSetGetValues(devices, reinterpret_cast<const void**>(&values));
        for (int i = 0; i < found_devices; i++)
        {
          Wiimote* wm = new WiimoteDarwinHid(values[i]);
          found_wiimotes.push_back(wm);
        }
      }
    }
    CFRelease(hid);
  }
}

bool WiimoteScanner::IsReady() const
{
  // TODO: only return true when a BT device is present
  return true;
}

WiimoteDarwin::WiimoteDarwin(IOBluetoothDevice* device) : m_btd(device)
{
  m_inputlen = 0;
  m_connected = false;
  m_wiimote_thread_run_loop = nullptr;
  m_pm_assertion = kIOPMNullAssertionID;
}

WiimoteDarwin::~WiimoteDarwin()
{
  Shutdown();
  if (m_wiimote_thread_run_loop)
  {
    CFRelease(m_wiimote_thread_run_loop);
    m_wiimote_thread_run_loop = nullptr;
  }
  [m_btd release];
  m_btd = nil;
  DisablePowerAssertionInternal();
}

// Connect to a Wiimote with a known address.
bool WiimoteDarwin::ConnectInternal()
{
  if (IsConnected())
    return false;

  ConnectBT* cbt = [[ConnectBT alloc] init];

  m_cchan = m_ichan = nil;

  IOReturn ret = [m_btd openConnection];
  if (ret)
  {
    ERROR_LOG(WIIMOTE, "Unable to open Bluetooth connection to Wiimote %i: %x", m_index + 1, ret);
    [cbt release];
    return false;
  }

  ret = [m_btd openL2CAPChannelSync:&m_cchan withPSM:kBluetoothL2CAPPSMHIDControl delegate:cbt];
  if (ret)
  {
    ERROR_LOG(WIIMOTE, "Unable to open control channel for Wiimote %i: %x", m_index + 1, ret);
    goto bad;
  }
  // Apple docs claim:
  // "The L2CAP channel object is already retained when this function returns
  // success; the channel must be released when the caller is done with it."
  // But without this, the channels get over-autoreleased, even though the
  // refcounting behavior here is clearly correct.
  [m_cchan retain];

  ret = [m_btd openL2CAPChannelSync:&m_ichan withPSM:kBluetoothL2CAPPSMHIDInterrupt delegate:cbt];
  if (ret)
  {
    WARN_LOG(WIIMOTE, "Unable to open interrupt channel for Wiimote %i: %x", m_index + 1, ret);
    goto bad;
  }
  [m_ichan retain];

  NOTICE_LOG(WIIMOTE, "Connected to Wiimote %i at %s", m_index + 1,
             [[m_btd addressString] UTF8String]);

  m_connected = true;

  [cbt release];

  m_wiimote_thread_run_loop = (CFRunLoopRef)CFRetain(CFRunLoopGetCurrent());

  return true;

bad:
  DisconnectInternal();
  [cbt release];
  return false;
}

// Disconnect a Wiimote.
void WiimoteDarwin::DisconnectInternal()
{
  [m_ichan closeChannel];
  [m_ichan release];
  m_ichan = nil;

  [m_cchan closeChannel];
  [m_cchan release];
  m_cchan = nil;

  [m_btd closeConnection];

  if (!IsConnected())
    return;

  NOTICE_LOG(WIIMOTE, "Disconnecting Wiimote %i", m_index + 1);

  m_connected = false;
}

bool WiimoteDarwin::IsConnected() const
{
  return m_connected;
}

void WiimoteDarwin::IOWakeup()
{
  if (m_wiimote_thread_run_loop)
  {
    CFRunLoopStop(m_wiimote_thread_run_loop);
  }
}

int WiimoteDarwin::IORead(unsigned char* buf)
{
  m_input = buf;
  m_inputlen = -1;

  CFRunLoopRun();

  return m_inputlen;
}

int WiimoteDarwin::IOWrite(const unsigned char* buf, size_t len)
{
  IOReturn ret;

  if (!IsConnected())
    return 0;

  ret = [m_ichan writeAsync:const_cast<void*>((void*)buf) length:(int)len refcon:nil];

  if (ret == kIOReturnSuccess)
    return len;
  else
    return 0;
}

void WiimoteDarwin::EnablePowerAssertionInternal()
{
  if (m_pm_assertion == kIOPMNullAssertionID)
  {
    if (IOReturn ret = IOPMAssertionCreateWithName(
            kIOPMAssertPreventUserIdleDisplaySleep, kIOPMAssertionLevelOn,
            CFSTR("Dolphin Wiimote activity"), &m_pm_assertion))
      ERROR_LOG(WIIMOTE, "Could not create power management assertion: %08x", ret);
  }
}

void WiimoteDarwin::DisablePowerAssertionInternal()
{
  if (m_pm_assertion != kIOPMNullAssertionID)
  {
    if (IOReturn ret = IOPMAssertionRelease(m_pm_assertion))
      ERROR_LOG(WIIMOTE, "Could not release power management assertion: %08x", ret);
  }
}

WiimoteDarwinHid::WiimoteDarwinHid(IOHIDDeviceRef device) : m_device(device)
{
  CFRetain(m_device);
  m_connected = false;
  m_report_buffer = Report(MAX_PAYLOAD);
}

WiimoteDarwinHid::~WiimoteDarwinHid()
{
  Shutdown();
  CFRelease(m_device);
}

bool WiimoteDarwinHid::ConnectInternal()
{
  IOReturn ret = IOHIDDeviceOpen(m_device, kIOHIDOptionsTypeNone);
  m_connected = ret == kIOReturnSuccess;
  if (m_connected)
  {
    IOHIDDeviceScheduleWithRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    IOHIDDeviceRegisterInputReportCallback(m_device, m_report_buffer.data() + 1, MAX_PAYLOAD - 1,
                                           &WiimoteDarwinHid::ReportCallback, this);
    IOHIDDeviceRegisterRemovalCallback(m_device, &WiimoteDarwinHid::RemoveCallback, this);
    NOTICE_LOG(WIIMOTE, "Connected to Wiimote %i", m_index + 1);
  }
  else
  {
    ERROR_LOG(WIIMOTE, "Could not open IOHID Wiimote: %08x", ret);
  }

  return m_connected;
}

void WiimoteDarwinHid::DisconnectInternal()
{
  IOHIDDeviceUnscheduleFromRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
  IOWakeup();
  IOReturn ret = IOHIDDeviceClose(m_device, kIOHIDOptionsTypeNone);
  if (ret != kIOReturnSuccess)
    ERROR_LOG(WIIMOTE, "Error closing IOHID Wiimote: %08x", ret);

  if (!IsConnected())
    return;

  NOTICE_LOG(WIIMOTE, "Disconnecting Wiimote %i", m_index + 1);

  m_buffered_reports.Clear();

  m_connected = false;
}

bool WiimoteDarwinHid::IsConnected() const
{
  return m_connected;
}

void WiimoteDarwinHid::IOWakeup()
{
  m_interrupted.store(true);
  CFRunLoopStop(CFRunLoopGetCurrent());
}

int WiimoteDarwinHid::IORead(u8* buf)
{
  Report rpt;
  m_interrupted.store(false);
  while (m_buffered_reports.Empty() && !m_interrupted.load())
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);

  if (m_buffered_reports.Pop(rpt))
  {
    memcpy(buf, rpt.data(), rpt.size());
    return rpt.size();
  }
  return -1;
}

int WiimoteDarwinHid::IOWrite(u8 const* buf, size_t len)
{
  IOReturn ret = IOHIDDeviceSetReport(m_device, kIOHIDReportTypeOutput, buf[1], buf + 1, len - 1);
  if (ret != kIOReturnSuccess)
  {
    ERROR_LOG(WIIMOTE, "Error writing to Wiimote: %08x", ret);
    return 0;
  }
  return len;
}

void WiimoteDarwinHid::QueueBufferReport(int length)
{
  Report rpt(m_report_buffer);
  rpt[0] = 0xa1;
  rpt.resize(length + 1);
  m_buffered_reports.Push(std::move(rpt));
}

void WiimoteDarwinHid::ReportCallback(void* context, IOReturn result, void*, IOHIDReportType type,
                                      u32 report_id, u8* report, CFIndex report_length)
{
  WiimoteDarwinHid* wm = static_cast<WiimoteDarwinHid*>(context);
  report[0] = report_id;
  wm->QueueBufferReport(report_length);
}

void WiimoteDarwinHid::RemoveCallback(void* context, IOReturn result, void*)
{
  WiimoteDarwinHid* wm = static_cast<WiimoteDarwinHid*>(context);
  wm->DisconnectInternal();
}

}  // namespace

@implementation SearchBT
- (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry*)sender
                        error:(IOReturn)error
                      aborted:(BOOL)aborted
{
  done = true;
  CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry*)sender device:(IOBluetoothDevice*)device
{
  NOTICE_LOG(WIIMOTE, "Discovered Bluetooth device at %s: %s", [[device addressString] UTF8String],
             [[device name] UTF8String]);

  if ([[sender foundDevices] count] == maxDevices)
    [sender stop];
}
@end

@implementation ConnectBT
- (void)l2capChannelData:(IOBluetoothL2CAPChannel*)l2capChannel
                    data:(unsigned char*)data
                  length:(NSUInteger)length
{
  IOBluetoothDevice* device = [l2capChannel device];
  WiimoteReal::WiimoteDarwin* wm = nullptr;

  std::lock_guard<std::recursive_mutex> lk(WiimoteReal::g_refresh_lock);

  for (int i = 0; i < MAX_WIIMOTES; i++)
  {
    wm = static_cast<WiimoteReal::WiimoteDarwin*>(WiimoteReal::g_wiimotes[i]);
    if (!wm)
      continue;
    if ([device isEqual:wm->m_btd])
      break;
    wm = nullptr;
  }

  if (wm == nullptr)
  {
    ERROR_LOG(WIIMOTE, "Received packet for unknown Wiimote");
    return;
  }

  if (length > MAX_PAYLOAD)
  {
    WARN_LOG(WIIMOTE, "Dropping packet for Wiimote %i, too large", wm->GetIndex() + 1);
    return;
  }

  if (wm->m_inputlen != -1)
  {
    WARN_LOG(WIIMOTE, "Dropping packet for Wiimote %i, queue full", wm->GetIndex() + 1);
    return;
  }

  memcpy(wm->m_input, data, length);
  wm->m_inputlen = length;

  CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel
{
  IOBluetoothDevice* device = [l2capChannel device];
  WiimoteReal::WiimoteDarwin* wm = nullptr;

  std::lock_guard<std::recursive_mutex> lk(WiimoteReal::g_refresh_lock);

  for (int i = 0; i < MAX_WIIMOTES; i++)
  {
    wm = static_cast<WiimoteReal::WiimoteDarwin*>(WiimoteReal::g_wiimotes[i]);
    if (!wm)
      continue;
    if ([device isEqual:wm->m_btd])
      break;
    wm = nullptr;
  }

  if (wm == nullptr)
  {
    ERROR_LOG(WIIMOTE, "Channel for unknown Wiimote was closed");
    return;
  }

  WARN_LOG(WIIMOTE, "Lost channel to Wiimote %i", wm->GetIndex() + 1);

  wm->DisconnectInternal();
}
@end
