/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2014-2017 - Ali Bouhlel
 *  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/>.
 */

/**
 * This driver handles the Wii U Gamepad.
 *
 * - For wiimote and wiimote attachments, see kpad_driver.c
 * - For HID controllers, see hid_driver.c
 */

#include "../../include/wiiu/input.h"

#define PANIC_BUTTON_MASK (VPAD_BUTTON_R | VPAD_BUTTON_L | VPAD_BUTTON_STICK_R | VPAD_BUTTON_STICK_L)

#define WPAD_INVALID_CHANNEL -1

static VPADChan wpad_to_gamepad_channel(unsigned pad)
{
   unsigned i;

   for (i = 0; i < WIIU_GAMEPAD_CHANNELS; i++)
   {
      if (joypad_state.wpad.channel_slot_map[i] == pad)
         return i;
   }

   return WPAD_INVALID_CHANNEL;
}

static void wpad_deregister(unsigned channel)
{
   unsigned slot;

   if (channel >= WIIU_GAMEPAD_CHANNELS)
      return;

   /* See if Gamepad is already disconnected */
   if (joypad_state.wpad.channel_slot_map[channel] == WPAD_INVALID_CHANNEL)
      return;

   /* Sanity check, about to use as unsigned */
   if (joypad_state.wpad.channel_slot_map[channel] < 0)
   {
      joypad_state.wpad.channel_slot_map[channel] = WPAD_INVALID_CHANNEL;
      return;
   }

   slot = (unsigned)joypad_state.wpad.channel_slot_map[channel];
   if (slot >= MAX_USERS)
      return;

   input_autoconfigure_disconnect(slot, wpad_driver.ident);
   joypad_state.pads[slot].connected           = false;
   joypad_state.wpad.channel_slot_map[channel] = WPAD_INVALID_CHANNEL;
}

static void wpad_register(unsigned channel)
{
   int slot;

   if (channel >= WIIU_GAMEPAD_CHANNELS)
      return;

   /* Check if gamepad is already handled
      Other checks not needed here - about to overwrite 
      joypad_state.wpad.channel_slot_map entry*/
   if (joypad_state.wpad.channel_slot_map[channel] != WPAD_INVALID_CHANNEL)
      return;

   if ((slot = pad_connection_find_vacant_pad(joypad_state.pads)) < 0)
      return;

   joypad_state.pads[slot].connected = true;
   joypad_state.pads[slot].input_driver = &wpad_driver;
   input_pad_connect(slot, &wpad_driver);
   joypad_state.wpad.channel_slot_map[channel] = slot;
}

static void wpad_update_button_state(uint64_t *state, uint32_t held_buttons)
{
   *state = held_buttons & VPAD_MASK_BUTTONS;
}

static void wpad_update_analog_state(int16_t state[3][2], VPADStatus *vpad)
{
   state[RETRO_DEVICE_INDEX_ANALOG_LEFT] [RETRO_DEVICE_ID_ANALOG_X] = WIIU_READ_STICK(vpad->leftStick.x);
   state[RETRO_DEVICE_INDEX_ANALOG_LEFT] [RETRO_DEVICE_ID_ANALOG_Y] = WIIU_READ_STICK(vpad->leftStick.y);
   state[RETRO_DEVICE_INDEX_ANALOG_RIGHT][RETRO_DEVICE_ID_ANALOG_X] = WIIU_READ_STICK(vpad->rightStick.x);
   state[RETRO_DEVICE_INDEX_ANALOG_RIGHT][RETRO_DEVICE_ID_ANALOG_Y] = WIIU_READ_STICK(vpad->rightStick.y);
}

static int16_t wpad_scale_touchpad(int16_t from_min, int16_t from_max,
      int16_t to_min,   int16_t to_max, int16_t value )
{
   int32_t from_range = from_max - from_min;
   int32_t to_range   = to_max - to_min;

   return (((value - from_min) * to_range) / from_range) + to_min;
}

static void wpad_get_calibrated_point(VPADTouchData *point,
      struct video_viewport *viewport, VPADStatus *vpad, unsigned channel)
{
   VPADTouchData calibrated720p = {0};

   VPADGetTPCalibratedPoint(channel, &calibrated720p, &(vpad->tpNormal));
   point->x = wpad_scale_touchpad(12, 1268, 0, viewport->full_width, calibrated720p.x);
   point->y = wpad_scale_touchpad(12, 708, 0, viewport->full_height, calibrated720p.y);
}

static void wpad_apply_clamping(VPADTouchData *point, struct video_viewport *viewport, bool *clamped)
{
   /* clamp the x domain to the viewport */
   if (point->x < viewport->x)
   {
      point->x = viewport->x;
      *clamped = true;
   }
   else if (point->x > (viewport->x + viewport->width))
   {
      point->x = viewport->x + viewport->width;
      *clamped = true;
   }

   /* clamp the y domain to the viewport */
   if (point->y < viewport->y)
   {
      point->y = viewport->y;
      *clamped = true;
   }
   else if (point->y > (viewport->y + viewport->height))
   {
      point->y =  viewport->y + viewport->height;
      *clamped = true;
   }
}

static void wpad_get_touch_coordinates(VPADTouchData *point, VPADStatus *vpad,
      VPADChan channel, struct video_viewport *viewport, bool *clamped)
{
   wpad_get_calibrated_point(point, viewport, vpad, channel);
   wpad_apply_clamping(point, viewport, clamped);
}

#if 0
/**
 * Get absolute value of a signed integer using bit manipulation.
 */
static int16_t bitwise_abs(int16_t value)
{
   bool is_negative = value & 0x8000;
   if (!is_negative)
      return value;

   value = value &~ 0x8000;
   return (~value & 0x7fff)+1;
}

/**
 * printf doesn't have a concept of a signed hex digit, so we fake it.
 */
static void wpad_log_coords(int16_t x, int16_t y)
{
   bool x_negative = x & 0x8000;
   bool y_negative = y & 0x8000;

   int16_t x_digit = bitwise_abs(x);
   int16_t y_digit = bitwise_abs(y);

   RARCH_LOG("[wpad]: calibrated point: %s%04x, %s%04x\n",
         x_negative ? "-" : "",
         x_digit,
         y_negative ? "-" : "",
         y_digit);
}
#endif

static void wpad_update_touch_state(int16_t state[3][2],
      uint64_t *buttons, VPADStatus *vpad, VPADChan channel)
{
   VPADTouchData point            = {0};
   struct video_viewport viewport = {0};
   bool touch_clamped             = false;

   if (!vpad->tpNormal.touched || vpad->tpNormal.validity != VPAD_VALID)
   {
      *buttons &= ~VPAD_BUTTON_TOUCH;
      return;
   }

   video_driver_get_viewport_info(&viewport);
   wpad_get_touch_coordinates(&point, vpad, channel, &viewport, &touch_clamped);

   state[WIIU_DEVICE_INDEX_TOUCHPAD][RETRO_DEVICE_ID_ANALOG_X] = wpad_scale_touchpad(
         viewport.x, viewport.x + viewport.width, -0x7fff, 0x7fff, point.x);
   state[WIIU_DEVICE_INDEX_TOUCHPAD][RETRO_DEVICE_ID_ANALOG_Y] = wpad_scale_touchpad(
         viewport.y, viewport.y + viewport.height, -0x7fff, 0x7fff, point.y);

#if 0
   wpad_log_coords(state[WIIU_DEVICE_INDEX_TOUCHPAD][RETRO_DEVICE_ID_ANALOG_X],
         state[WIIU_DEVICE_INDEX_TOUCHPAD][RETRO_DEVICE_ID_ANALOG_Y]);
#endif

   if (!touch_clamped)
      *buttons |= VPAD_BUTTON_TOUCH;
   else
      *buttons &= ~VPAD_BUTTON_TOUCH;
}

static void wpad_check_panic_button(uint32_t held_buttons)
{
   if ((held_buttons & PANIC_BUTTON_MASK) == PANIC_BUTTON_MASK)
      command_event(CMD_EVENT_QUIT, NULL);
}

static void wpad_poll(void)
{
   VPADStatus vpad;
   VPADReadError error;
   VPADChan channel;

   for (channel = VPAD_CHAN_0; channel < WIIU_GAMEPAD_CHANNELS; channel++)
   {
      VPADRead(channel, &vpad, 1, &error);

      /* Gamepad is connected! */
      if (error == VPAD_READ_SUCCESS || error == VPAD_READ_NO_SAMPLES)
         wpad_register(channel);
      else if (error == VPAD_READ_INVALID_CONTROLLER)
         wpad_deregister(channel);

      if (error == VPAD_READ_SUCCESS)
      {
         wpad_update_button_state(&joypad_state.wpad.pads[channel].button_state, vpad.hold);
         wpad_update_analog_state(joypad_state.wpad.pads[channel].analog_state, &vpad);
         wpad_update_touch_state(joypad_state.wpad.pads[channel].analog_state, &joypad_state.wpad.pads[channel].button_state, &vpad, channel);
         wpad_check_panic_button(vpad.hold);
      }
   }
}

static void *wpad_init(void *data)
{
   int i;
   memset(&joypad_state.wpad, 0, sizeof(joypad_state.wpad));
   for (i = 0; i < WIIU_GAMEPAD_CHANNELS; i++)
      joypad_state.wpad.channel_slot_map[i] = WPAD_INVALID_CHANNEL;
   wpad_poll();
   return (void*)-1;
}

static bool wpad_query_pad(unsigned port)
{
   return port < MAX_USERS && 
      (wpad_to_gamepad_channel(port) != WPAD_INVALID_CHANNEL);
}

static void wpad_destroy(void) { }

static int32_t wpad_button(unsigned port, uint16_t joykey)
{
   if (wpad_query_pad(port))
   {
      VPADChan channel = wpad_to_gamepad_channel(port);
      if (channel >= 0)
         return (joypad_state.wpad.pads[channel].button_state & (UINT64_C(1) << joykey));
   }
   return 0;
}

static void wpad_get_buttons(unsigned port, input_bits_t *state)
{
   VPADChan channel;

   if (!wpad_query_pad(port))
   {
      BIT256_CLEAR_ALL_PTR(state);
      return;
   }

   if ((channel = wpad_to_gamepad_channel(port)) < 0)
   {
      BIT256_CLEAR_ALL_PTR(state);
      return;
   }

   BITS_COPY32_PTR(state, joypad_state.wpad.pads[channel].button_state);
}

static int16_t wpad_axis(unsigned port, uint32_t axis)
{
   axis_data data;
   VPADChan channel;

   if (!wpad_query_pad(port))
      return 0;

   channel = wpad_to_gamepad_channel(port);
   if (channel < 0)
      return 0;

   pad_functions.read_axis_data(axis, &data);
   return pad_functions.get_axis_value(data.axis,
         joypad_state.wpad.pads[channel].analog_state, data.is_negative);
}

static int16_t wpad_state(
      rarch_joypad_info_t *joypad_info,
      const struct retro_keybind *binds,
      unsigned port)
{
   unsigned i;
   int16_t ret                          = 0;
   uint16_t port_idx                    = joypad_info->joy_idx;

   for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++)
   {
      /* Auto-binds are per joypad, not per user. */
      const uint64_t joykey  = (binds[i].joykey != NO_BTN)
         ? binds[i].joykey  : joypad_info->auto_binds[i].joykey;
      const uint32_t joyaxis = (binds[i].joyaxis != AXIS_NONE)
         ? binds[i].joyaxis : joypad_info->auto_binds[i].joyaxis;
      if (
               (uint16_t)joykey != NO_BTN 
            && wpad_button(port_idx, (uint16_t)joykey))
         ret |= ( 1 << i);
      else if (joyaxis != AXIS_NONE &&
            ((float)abs(wpad_axis(port_idx, joyaxis)) 
             / 0x8000) > joypad_info->axis_threshold)
         ret |= (1 << i);
   }

   return ret;
}

static const char *wpad_name(unsigned port)
{
   return PAD_NAME_WIIU_GAMEPAD;
}

input_device_driver_t wpad_driver =
{
  wpad_init,
  wpad_query_pad,
  wpad_destroy,
  wpad_button,
  wpad_state,
  wpad_get_buttons,
  wpad_axis,
  wpad_poll,
  NULL, /* set_rumble */
  NULL, /* set_rumble_gain */
  NULL, /* set_sensor_state */
  NULL, /* get_sensor_input */
  wpad_name,
  "gamepad",
};
