/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2013-2014 - Jason Fetters
 *  Copyright (C) 2011-2017 - Daniel De Matteis
 *
 *  RetroArch is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include "wiiu_hid.h"

static wiiu_event_list events;
static wiiu_adapter_list adapters;

static void log_buffer(uint8_t *data, uint32_t len);

static bool wiiu_hid_joypad_query(void *data, unsigned slot)
{
   wiiu_hid_t *hid = (wiiu_hid_t *)data;
   if (!hid || !hid->driver)
      return false;

   return slot < hid->driver->max_slot;
}

static const char *wiiu_hid_joypad_name(void *data, unsigned slot)
{
   if (!wiiu_hid_joypad_query(data, slot))
      return NULL;

   wiiu_hid_t *hid = (wiiu_hid_t *)data;

   return hid->driver->pad_list[slot].iface->get_name(data);
}

static void wiiu_hid_joypad_get_buttons(void *data, unsigned port, retro_bits_t *state)
{
   (void)data;
   (void)port;

   BIT256_CLEAR_ALL_PTR(state);
}

static bool wiiu_hid_joypad_button(void *data, unsigned port, uint16_t joykey)
{
   (void)data;
   (void)port;
   (void)joykey;

   return false;
}

static bool wiiu_hid_joypad_rumble(void *data, unsigned pad,
      enum retro_rumble_effect effect, uint16_t strength)
{
   (void)data;
   (void)pad;
   (void)effect;
   (void)strength;

   return false;
}

static int16_t wiiu_hid_joypad_axis(void *data, unsigned port, uint32_t joyaxis)
{
   (void)data;
   (void)port;
   (void)joyaxis;

   return 0;
}

static void *wiiu_hid_init(hid_driver_instance_t *driver)
{
   RARCH_LOG("[hid]: initializing HID subsystem\n");
   wiiu_hid_t *hid = new_hid();
   HIDClient *client = new_hidclient();

   if (!hid || !client)
      goto error;

   hid->driver = driver;

   wiiu_hid_init_lists();
   start_polling_thread(hid);
   if (!hid->polling_thread)
      goto error;

   HIDAddClient(client, wiiu_attach_callback);
   hid->client = client;

   RARCH_LOG("[hid]: init success\n");
   return hid;

error:
   RARCH_LOG("[hid]: initialization failed. cleaning up.\n");

   stop_polling_thread(hid);
   delete_hid(hid);
   delete_hidclient(client);
   return NULL;
}

static void wiiu_hid_free(const void *data)
{
   wiiu_hid_t *hid = (wiiu_hid_t*)data;

   if (!hid)
      return;

   stop_polling_thread(hid);
   delete_hidclient(hid->client);
   delete_hid(hid);

   if (events.list)
   {
      wiiu_attach_event *event = NULL;
      while( (event = events.list) != NULL)
      {
         events.list = event->next;
         delete_attach_event(event);
      }
      memset(&events, 0, sizeof(events));
   }
}

static void wiiu_hid_poll(void *data)
{
   wiiu_hid_t *hid = (wiiu_hid_t *)data;
   if(hid == NULL)
      return;

   synchronized_process_adapters(hid);
}

static void wiiu_hid_send_control(void *data, uint8_t *buf, size_t size)
{
   wiiu_adapter_t *adapter = (wiiu_adapter_t *)data;
   int32_t result;

   if (!adapter) {
      RARCH_ERR("[hid]: send_control: bad adapter.\n");
      return;
   }

   memset(adapter->tx_buffer, 0, adapter->tx_size);
   memcpy(adapter->tx_buffer, buf, size);

   /* From testing, HIDWrite returns an error that looks like it's two
    * int16_t's bitmasked together. For example, one error I saw when trying
    * to write a single byte was 0xffe2ff97, which works out to -30 and -105.
    *  I have no idea what these mean. */
   result = HIDWrite(adapter->handle, adapter->tx_buffer, adapter->tx_size, NULL, NULL);
   if(result < 0)
   {
      int16_t r1 =  (result & 0x0000FFFF);
      int16_t r2 = ((result & 0xFFFF0000) >> 16);
      RARCH_LOG("[hid]: write failed: %08x (%d:%d)\n", result, r2, r1);
   }
}

static int32_t wiiu_hid_set_report(void *data, uint8_t report_type,
               uint8_t report_id, void *report_data, uint32_t report_length)
{
   wiiu_adapter_t *adapter = (wiiu_adapter_t *)data;
   if (!adapter)
      return -1;

   return HIDSetReport(adapter->handle,
         report_type,
         report_id,
         report_data,
         report_length,
         NULL, NULL);
}

static int32_t wiiu_hid_set_idle(void *data, uint8_t duration)
{
   wiiu_adapter_t *adapter = (wiiu_adapter_t *)data;
   if (!adapter)
      return -1;

   return HIDSetIdle(adapter->handle,
         adapter->interface_index,
         duration,
         NULL, NULL);
}

static int32_t wiiu_hid_set_protocol(void *data, uint8_t protocol)
{
   wiiu_adapter_t *adapter = (wiiu_adapter_t *)data;
   if (!adapter)
      return -1;

   return HIDSetProtocol(adapter->handle,
         adapter->interface_index,
         protocol,
         NULL, NULL);
}

static int32_t wiiu_hid_read(void *data, void *buffer, size_t size)
{
  wiiu_adapter_t *adapter = (wiiu_adapter_t *)data;

  if(!adapter)
    return -1;

  if(size > adapter->rx_size)
    return -1;

  return HIDRead(adapter->handle, buffer, size, NULL, NULL);
}


static void start_polling_thread(wiiu_hid_t *hid)
{
   OSThreadAttributes attributes = OS_THREAD_ATTRIB_AFFINITY_CPU2;
   BOOL result                   = false;
   int32_t stack_size            = 0x8000;
   int32_t priority              = 10;
   OSThread *thread              = new_thread();
   void *stack                   = alloc_zeroed(16, stack_size);

   RARCH_LOG("[hid]: starting polling thread.\n");

   if (!thread || !stack)
   {
      RARCH_LOG("[hid]: allocation failed, aborting thread start.\n");
      goto error;
   }

   if (!OSCreateThread(thread,
            wiiu_hid_polling_thread,
            1, (char *)hid,
            stack+stack_size, stack_size,
            priority,
            attributes))
   {
      RARCH_LOG("[hid]: OSCreateThread failed.\n");
      goto error;
   }

   OSSetThreadCleanupCallback(thread, wiiu_hid_polling_thread_cleanup);

   hid->polling_thread       = thread;
   hid->polling_thread_stack = stack;
   OSResumeThread(thread);
   return;

error:
   if (thread)
      free(thread);
   if (stack)
      free(stack);

   return;
}


static void stop_polling_thread(wiiu_hid_t *hid)
{
   int thread_result = 0;
   RARCH_LOG("[hid]: stopping polling thread.\n");

   if (!hid || !hid->polling_thread)
      return;

   /* Unregister our HID client so we don't get any new events. */
   if(hid->client) {
     HIDDelClient(hid->client);
     hid->client = NULL;
   }

   /* tell the thread it's time to stop. */
   hid->polling_thread_quit = true;
   /* This returns once the thread runs and the cleanup method completes. */
   OSJoinThread(hid->polling_thread, &thread_result);
   free(hid->polling_thread);
   free(hid->polling_thread_stack);
   hid->polling_thread = NULL;
   hid->polling_thread_stack = NULL;
}

static void log_device(HIDDevice *device)
{
   if (!device)
   {
      RARCH_LOG("NULL device.\n");
   }

   RARCH_LOG("                handle: %d\n", device->handle);
   RARCH_LOG("  physical_device_inst: %d\n", device->physical_device_inst);
   RARCH_LOG("                   vid: 0x%x\n", device->vid);
   RARCH_LOG("                   pid: 0x%x\n", device->pid);
   RARCH_LOG("       interface_index: %d\n", device->interface_index);
   RARCH_LOG("             sub_class: %d\n", device->sub_class);
   RARCH_LOG("              protocol: %d\n", device->protocol);
   RARCH_LOG("    max_packet_size_rx: %d\n", device->max_packet_size_rx);
   RARCH_LOG("    max_packet_size_tx: %d\n", device->max_packet_size_tx);
}


static uint8_t try_init_driver(wiiu_adapter_t *adapter)
{
   adapter->driver_handle = adapter->driver->init(adapter);
   if(adapter->driver_handle == NULL) {
     RARCH_ERR("[hid]: Failed to initialize driver: %s\n",
        adapter->driver->name);
     return ADAPTER_STATE_DONE;
   }

   return ADAPTER_STATE_READY;
}

static void synchronized_process_adapters(wiiu_hid_t *hid)
{
   wiiu_adapter_t *adapter = NULL;

   OSFastMutex_Lock(&(adapters.lock));
   for(adapter = adapters.list; adapter != NULL; adapter = adapter->next)
   {
     switch(adapter->state)
     {
       case ADAPTER_STATE_NEW:
          adapter->state = try_init_driver(adapter);
          break;
       case ADAPTER_STATE_READY:
       case ADAPTER_STATE_READING:
       case ADAPTER_STATE_DONE:
          break;
       default:
          RARCH_ERR("[hid]: Invalid adapter state: %d\n", adapter->state);
          break;
     }
   }
   OSFastMutex_Unlock(&(adapters.lock));
}

static void synchronized_add_event(wiiu_attach_event *event)
{
   OSFastMutex_Lock(&(events.lock));
   event->next = events.list;
   events.list = event;
   OSFastMutex_Unlock(&(events.lock));
}

static wiiu_attach_event *synchronized_get_events_list(void)
{
   wiiu_attach_event *list;
   OSFastMutex_Lock(&(events.lock));
   list = events.list;
   events.list = NULL;
   OSFastMutex_Unlock(&(events.lock));

   return list;
}

static wiiu_adapter_t *synchronized_remove_from_adapters_list(uint32_t handle)
{
   OSFastMutex_Lock(&(adapters.lock));
   wiiu_adapter_t *iterator, *prev = NULL;

   for(iterator = adapters.list; iterator != NULL; iterator = iterator->next)
   {
      if(iterator->handle == handle)
      {
         /* we're at the start of the list, so just re-assign head */
         if(prev == NULL)
           adapters.list = iterator->next;
         else
           prev->next = iterator->next;
         break;
      }
      prev = iterator;
   }
   OSFastMutex_Unlock(&(adapters.lock));

   return iterator;
}

static void synchronized_add_to_adapters_list(wiiu_adapter_t *adapter)
{
   OSFastMutex_Lock(&(adapters.lock));
   adapter->next = adapters.list;
   adapters.list = adapter;
   OSFastMutex_Unlock(&(adapters.lock));
}

static int32_t wiiu_attach_callback(HIDClient *client,
      HIDDevice *device, uint32_t attach)
{
   wiiu_attach_event *event = NULL;

   switch(attach)
   {
      case HID_DEVICE_ATTACH:
         log_device(device);
      case HID_DEVICE_DETACH:
         if (device)
            event = new_attach_event(device);

         if(!event)
            goto error;

         event->type = attach;
         synchronized_add_event(event);
         return DEVICE_USED;
      default:
         break;
   }

error:
   delete_attach_event(event);
   return DEVICE_UNUSED;
}

static void wiiu_hid_detach(wiiu_hid_t *hid, wiiu_attach_event *event)
{
   wiiu_adapter_t *adapter = synchronized_remove_from_adapters_list(event->handle);

   if(adapter) {
      RARCH_LOG("[hid]: freeing detached pad\n");
      delete_adapter(adapter);
   }
}


static void wiiu_hid_attach(wiiu_hid_t *hid, wiiu_attach_event *event)
{
   wiiu_adapter_t *adapter = new_adapter(event);

   if(!adapter)
   {
      RARCH_ERR("[hid]: Failed to allocate adapter.\n");
      goto error;
   }

   adapter->hid    = hid;
   adapter->driver = event->driver;
   adapter->state  = ADAPTER_STATE_NEW;

   synchronized_add_to_adapters_list(adapter);
   wiiu_start_read_loop(adapter);

   return;

error:
   delete_adapter(adapter);
}

void wiiu_start_read_loop(wiiu_adapter_t *adapter)
{
  HIDRead(adapter->handle,
          adapter->rx_buffer,
          adapter->rx_size,
          wiiu_hid_read_loop_callback,
          adapter);
}

/**
 * Takes a buffer and formats it for the log file, 16 bytes per line.
 *
 * When the end of the buffer is reached, it is padded out with 0xff. So e.g.
 * a 5-byte buffer might look like:
 *
 * 5 bytes read fro HIDRead:
 * 0102030405ffffff  ffffffffffffffff
 * ==================================
 */
static void log_buffer(uint8_t *data, uint32_t len)
{
   int i, o;
   int padding = len % 16;
   uint8_t buf[16];

   (uint8_t *)data;
   (uint32_t)len;

   RARCH_LOG("%d bytes read from HIDRead:\n", len);

   for(i = 0, o = 0; i < len; i++)
   {
      buf[o] = data[i];
      o++;
      if(o == 16)
      {
         o = 0;
         RARCH_LOG("%02x%02x%02x%02x%02x%02x%02x%02x  %02x%02x%02x%02x%02x%02x%02x%02x\n",
               buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
               buf[8], buf[9], buf[10], buf[11], buf[12],  buf[13], buf[14], buf[15]);
      }
   }

   if(padding)
   {
      for(i = padding; i < 16; i++)
         buf[i] = 0xff;

      RARCH_LOG("%02x%02x%02x%02x%02x%02x%02x%02x  %02x%02x%02x%02x%02x%02x%02x%02x\n",
            buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
            buf[8], buf[9], buf[10], buf[11], buf[12],  buf[13], buf[14], buf[15]);
   }
   RARCH_LOG("==================================\n");

}

static void wiiu_hid_read_loop_callback(uint32_t handle, int32_t error,
              uint8_t *buffer, uint32_t buffer_size, void *userdata)
{
   wiiu_adapter_t *adapter = (wiiu_adapter_t *)userdata;
   if(!adapter)
   {
      RARCH_ERR("read_loop_callback: bad userdata\n");
      return;
   }

   if(adapter->hid->polling_thread_quit)
   {
      RARCH_LOG("Shutting down read loop for device: %s\n",
       adapter->driver->name);
      adapter->state = ADAPTER_STATE_DONE;
   }

   if(adapter->state == ADAPTER_STATE_READY ||
      adapter->state == ADAPTER_STATE_READING) {

      adapter->state = ADAPTER_STATE_READING;

      if(error)
      {
         int16_t r1 =  (error & 0x0000FFFF);
         int16_t r2 = ((error & 0xFFFF0000) >> 16);
         RARCH_ERR("[hid]: read failed: %08x (%d:%d)\n", error, r2, r1);
      } else {
         adapter->driver->handle_packet(adapter->driver_handle, buffer, buffer_size);
      }
   }

   /* this can also get set if something goes wrong in initialization */
   if(adapter->state == ADAPTER_STATE_DONE)
      return;

   HIDRead(adapter->handle, adapter->rx_buffer, adapter->rx_size,
      wiiu_hid_read_loop_callback, adapter);
}

/**
 * Block until all the HIDRead() calls have returned.
 */
static void wiiu_hid_polling_thread_cleanup(OSThread *thread, void *stack)
{
   int incomplete          = 0;
   int retries             = 0;
   wiiu_adapter_t *adapter = NULL;

   RARCH_LOG("Waiting for in-flight reads to finish.\n");

   do
   {
      OSFastMutex_Lock(&(adapters.lock));
      incomplete = 0;
      for(adapter = adapters.list; adapter != NULL; adapter = adapter->next)
      {
         if(adapter->state == ADAPTER_STATE_READING)
            incomplete++;
      }
      /* We are clear for shutdown. Clean up the list
       * while we are holding the lock. */
      if(incomplete == 0)
      {
         RARCH_LOG("All in-flight reads complete.\n");
         while(adapters.list != NULL)
         {
            RARCH_LOG("[hid]: shutting down adapter..\n");
            adapter = adapters.list;
            adapters.list = adapter->next;
            delete_adapter(adapter);
         }
      }
      OSFastMutex_Unlock(&(adapters.lock));

      if(incomplete)
         usleep(5000);

      if(++retries >= 1000)
      {
         RARCH_WARN("[hid]: timed out waiting for in-flight read to finish.\n");
         incomplete = 0;
      }
   }while(incomplete);

}

static void wiiu_handle_attach_events(wiiu_hid_t *hid, wiiu_attach_event *list)
{
   wiiu_attach_event *event, *event_next = NULL;

   if(!hid || !list)
      return;

   for(event = list; event != NULL; event = event_next)
   {
      event_next  = event->next;
      if(event->type == HID_DEVICE_ATTACH)
         wiiu_hid_attach(hid, event);
      else
         wiiu_hid_detach(hid, event);
      delete_attach_event(event);
   }
}

static int wiiu_hid_polling_thread(int argc, const char **argv)
{
   wiiu_hid_t *hid = (wiiu_hid_t *)argv;
   int i           = 0;

   RARCH_LOG("[hid]: polling thread is starting\n");

   while(!hid->polling_thread_quit)
   {
      wiiu_handle_attach_events(hid, synchronized_get_events_list());
      usleep(10000);
      i += 10000;
      if(i >= (1000 * 1000 * 3))
         i = 0;
   }

   RARCH_LOG("[hid]: polling thread is stopping\n");
   return 0;
}

static OSThread *new_thread(void)
{
   OSThread *t = alloc_zeroed(8, sizeof(OSThread));

   if (!t)
      return NULL;

   t->tag      = OS_THREAD_TAG;

   return t;
}

static void wiiu_hid_init_lists(void)
{
   RARCH_LOG("[hid]: Initializing events list\n");
   memset(&events, 0, sizeof(events));
   OSFastMutex_Init(&(events.lock), "attach_events");
   RARCH_LOG("[hid]: Initializing adapters list\n");
   memset(&adapters, 0, sizeof(adapters));
   OSFastMutex_Init(&(adapters.lock), "adapters");
}

static wiiu_hid_t *new_hid(void)
{
   RARCH_LOG("[hid]: new_hid()\n");
   return alloc_zeroed(4, sizeof(wiiu_hid_t));
}

static void delete_hid(wiiu_hid_t *hid)
{
   RARCH_LOG("[hid]: delete_hid()\n");
   if(hid)
      free(hid);
}

static HIDClient *new_hidclient(void)
{
   RARCH_LOG("[hid]: new_hidclient()\n");
   return alloc_zeroed(32, sizeof(HIDClient));
}

static void delete_hidclient(HIDClient *client)
{
   RARCH_LOG("[hid]: delete_hidclient()\n");
   if(client)
      free(client);
}

static wiiu_adapter_t *new_adapter(wiiu_attach_event *event)
{
   wiiu_adapter_t *adapter  = alloc_zeroed(32, sizeof(wiiu_adapter_t));

   if (!adapter)
      return NULL;

   adapter->handle          = event->handle;
   adapter->interface_index = event->interface_index;
   adapter->rx_size         = event->max_packet_size_rx;
   adapter->rx_buffer       = alloc_zeroed(32, adapter->rx_size);
   adapter->tx_size         = event->max_packet_size_tx;
   adapter->tx_buffer       = alloc_zeroed(32, adapter->tx_size);

   return adapter;
}

static void delete_adapter(wiiu_adapter_t *adapter)
{
   if (!adapter)
      return;

   if(adapter->rx_buffer)
   {
      free(adapter->rx_buffer);
      adapter->rx_buffer = NULL;
   }
   if(adapter->tx_buffer)
   {
      free(adapter->tx_buffer);
      adapter->tx_buffer = NULL;
   }
   if(adapter->driver && adapter->driver_handle) {
      adapter->driver->free(adapter->driver_handle);
      adapter->driver_handle = NULL;
      adapter->driver = NULL;
   }

   free(adapter);
}

static wiiu_attach_event *new_attach_event(HIDDevice *device)
{
   hid_device_t *driver = hid_device_driver_lookup(device->vid, device->pid);
   if(!driver)
   {
      RARCH_ERR("[hid]: Failed to locate driver for device vid=%04x pid=%04x\n",
        device->vid, device->pid);
      return NULL;
   }
   RARCH_LOG("[hid]: Found HID device driver: %s\n", driver->name);
   wiiu_attach_event *event = alloc_zeroed(4, sizeof(wiiu_attach_event));
   if(!event)
      return NULL;

   event->driver             = driver;
   event->handle             = device->handle;
   event->vendor_id          = device->vid;
   event->product_id         = device->pid;
   event->interface_index    = device->interface_index;
   event->is_keyboard        = (device->sub_class == 1
         && device->protocol == 1);
   event->is_mouse           = (device->sub_class == 1
         && device->protocol == 2);
   event->max_packet_size_rx = device->max_packet_size_rx;
   event->max_packet_size_tx = device->max_packet_size_tx;

   return event;
}

static void delete_attach_event(wiiu_attach_event *event)
{
   if(event)
      free(event);
}


void *alloc_zeroed(size_t alignment, size_t size)
{
   void *result = memalign(alignment, size);
   if(result)
      memset(result, 0, size);

   return result;
}


hid_driver_t wiiu_hid = {
   wiiu_hid_init,
   wiiu_hid_joypad_query,
   wiiu_hid_free,
   wiiu_hid_joypad_button,
   wiiu_hid_joypad_get_buttons,
   wiiu_hid_joypad_axis,
   wiiu_hid_poll,
   wiiu_hid_joypad_rumble,
   wiiu_hid_joypad_name,
   "wiiu",
   wiiu_hid_send_control,
   wiiu_hid_set_report,
   wiiu_hid_set_idle,
   wiiu_hid_set_protocol,
   wiiu_hid_read,
};