/* 
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Ha Thach (tinyusb.org)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "bsp/board.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
#include "tusb.h"

#include "usb_descriptors.h"

//--------------------------------------------------------------------+
// MACRO CONSTANT TYPEDEF PROTYPES
//--------------------------------------------------------------------+

/* Blink pattern
 * - 250 ms  : device not mounted
 * - 1000 ms : device mounted
 * - 2500 ms : device is suspended
 */
enum  {
  BLINK_NOT_MOUNTED = 250,
  BLINK_MOUNTED = 1000,
  BLINK_SUSPENDED = 2500,
};

#define J_UP        21
#define J_DOWN      20
#define J_LEFT      19
#define J_RIGHT     18
#define J_FIRE1     8 /* south */
#define J_FIRE2     7 /* north */
#define J_FIRE3     10 /* east */
#define J_FIRE4     6  /* west */
#define J_FIRE5     12 /* TL */
#define J_FIRE6     13 /* TR */
#define J_FIRE7     4  /* TL2 */
#define J_FIRE8     9  /* TR2 */
#define J_START     5
#define J_SELECT    11

#define POT         27 /* Sensitivity / mode change */
#define POT_ADC     1 /* ADC1 */

#define LED_DIGITAL 28

static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;

void led_blinking_task(void);
void hid_task(void);

/*------------- MAIN -------------*/
int main(void)
{
  board_init();
  tusb_init();

  while (1)
  {
    tud_task(); // tinyusb device task
    led_blinking_task();

    hid_task();
  }

  return 0;
}

//--------------------------------------------------------------------+
// Device callbacks
//--------------------------------------------------------------------+

// Invoked when device is mounted
void tud_mount_cb(void)
{
  blink_interval_ms = BLINK_MOUNTED;
}

// Invoked when device is unmounted
void tud_umount_cb(void)
{
  blink_interval_ms = BLINK_NOT_MOUNTED;
}

// Invoked when usb bus is suspended
// remote_wakeup_en : if host allow us  to perform remote wakeup
// Within 7ms, device must draw an average of current less than 2.5 mA from bus
void tud_suspend_cb(bool remote_wakeup_en)
{
  (void) remote_wakeup_en;
  blink_interval_ms = BLINK_SUSPENDED;
}

// Invoked when usb bus is resumed
void tud_resume_cb(void)
{
  blink_interval_ms = BLINK_MOUNTED;
}

//--------------------------------------------------------------------+
// USB HID
//--------------------------------------------------------------------+
//

static void send_hid_report(uint8_t report_id, uint32_t btn)
{
  (void)btn;
  // skip if hid is not ready yet
  if ( !tud_hid_ready() ) return;

  switch(report_id)
  {
    case REPORT_ID_GAMEPAD:
    {
      // use to avoid send multiple consecutive zero report for keyboard
      static bool has_gamepad_key = false;
      static int32_t prog_x = 0, prog_y = 0;
      uint32_t pot_val = 0;
      uint32_t gpio_mask = gpio_get_all();


      hid_gamepad_report_t report =
      {
        .x   = 0, .y = 0, .z = 0, .rz = 0, .rx = 0, .ry = 0,
        .hat = 0, .buttons = 0
      };

      if ((gpio_mask & (1<<J_UP)) == 0) {
          report.y = -1;
      } else if ((gpio_mask & (1<<J_DOWN)) == 0) {
          report.y = 1;
      }
      if ((gpio_mask & (1<<J_RIGHT)) == 0) {
          report.x = 1;
      } else if ((gpio_mask & (1<<J_LEFT)) == 0) {
          report.x = -1;
      }

      adc_select_input(POT_ADC);
      pot_val = adc_read();

       if (pot_val < 4000) {
           if (pot_val >  32) {
              /* Tune intensity */
              report.x *= (pot_val >> 5);
              report.y *= (pot_val >> 5);
           }
           gpio_put(LED_DIGITAL, 0);
      } else { 
          /* Digital mode */
          report.x *= 127;
          report.y *= 127;
          gpio_put(LED_DIGITAL, 1);
      }

      report.buttons = 0;
      if ((gpio_mask & (1<<J_FIRE1)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_SOUTH;
      }
      if ((gpio_mask & (1<<J_FIRE2)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_NORTH;
      }
      if ((gpio_mask & (1<<J_FIRE3)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_EAST;
      }
      if ((gpio_mask & (1<<J_FIRE4)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_WEST;
      }
      if ((gpio_mask & (1<<J_FIRE5)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_TL;
      }
      if ((gpio_mask & (1<<J_FIRE6)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_TR;
      }
      if ((gpio_mask & (1<<J_FIRE7)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_TL2;
      }
      if ((gpio_mask & (1<<J_FIRE8)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_TR2;
      }
      if ((gpio_mask & (1<<J_START)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_START;
      }
      if ((gpio_mask & (1<<J_SELECT)) == 0) {
          report.buttons |= GAMEPAD_BUTTON_SELECT;
      }
      tud_hid_report(REPORT_ID_GAMEPAD, &report, sizeof(report));
    }
    break;
  }
}

// Every 5ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..)
// tud_hid_report_complete_cb() is used to send the next report after previous one is complete
void hid_task(void)
{
  // Poll every 10ms
  const uint32_t interval_ms = 5;
  static uint32_t start_ms = 0;
  static int gpio_initialized = 0;


  if (!gpio_initialized) {
      /* Set up GPIO */
      gpio_init(J_UP);
      gpio_init(J_DOWN);
      gpio_init(J_LEFT);
      gpio_init(J_RIGHT);
      gpio_init(J_FIRE1);
      gpio_init(J_FIRE2);
      gpio_init(J_FIRE3);
      gpio_init(J_FIRE4);
      gpio_init(J_FIRE5);
      gpio_init(J_FIRE6);
      gpio_init(J_FIRE7);
      gpio_init(J_FIRE8);
      gpio_init(J_START);
      gpio_init(J_SELECT);
      gpio_init(POT);
      gpio_init(LED_DIGITAL);

      gpio_set_dir(J_UP, GPIO_IN);
      gpio_set_dir(J_DOWN, GPIO_IN);
      gpio_set_dir(J_LEFT, GPIO_IN);
      gpio_set_dir(J_RIGHT, GPIO_IN);
      gpio_set_dir(J_FIRE1, GPIO_IN);
      gpio_set_dir(J_FIRE2, GPIO_IN);
      gpio_set_dir(J_FIRE3, GPIO_IN);
      gpio_set_dir(J_FIRE4, GPIO_IN);
      gpio_set_dir(J_FIRE5, GPIO_IN);
      gpio_set_dir(J_FIRE6, GPIO_IN);
      gpio_set_dir(J_FIRE7, GPIO_IN);
      gpio_set_dir(J_FIRE8, GPIO_IN);
      gpio_set_dir(J_START, GPIO_IN);
      gpio_set_dir(J_SELECT, GPIO_IN);

      gpio_set_dir(LED_DIGITAL, GPIO_OUT);

      adc_init();
      adc_gpio_init(POT);

      gpio_pull_up(J_UP);
      gpio_pull_up(J_DOWN);
      gpio_pull_up(J_LEFT);
      gpio_pull_up(J_RIGHT);
      gpio_pull_up(J_FIRE1);
      gpio_pull_up(J_FIRE2);
      gpio_pull_up(J_FIRE3);
      gpio_pull_up(J_FIRE4);
      gpio_pull_up(J_FIRE5);
      gpio_pull_up(J_FIRE6);
      gpio_pull_up(J_FIRE7);
      gpio_pull_up(J_FIRE8);
      gpio_pull_up(J_START);
      gpio_pull_up(J_SELECT);
      gpio_initialized++;
  }

  if ( board_millis() - start_ms < interval_ms) return; // not enough time
      start_ms += interval_ms;

  // Remote wakeup
  if ( tud_suspended())
  {
    // Wake up host if we are in suspend mode
    // and REMOTE_WAKEUP feature is enabled by host
    tud_remote_wakeup();
  }else
  {
    // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()
    send_hid_report(REPORT_ID_GAMEPAD, 0);
  }
}

// Invoked when sent REPORT successfully to host
// Application can use this to send the next report
// Note: For composite reports, report[0] is report ID
void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint8_t len)
{
  (void) instance;
  (void) len;
  (void) report;
}

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
{
  // TODO not Implemented
  (void) instance;
  (void) report_id;
  (void) report_type;
  (void) buffer;
  (void) reqlen;

  return 0;
}

// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize)
{
  (void) instance;

  if (report_type == HID_REPORT_TYPE_OUTPUT)
  {
    // Set keyboard LED e.g Capslock, Numlock etc...
    if (report_id == REPORT_ID_KEYBOARD)
    {
      // bufsize should be (at least) 1
      if ( bufsize < 1 ) return;

      uint8_t const kbd_leds = buffer[0];

      if (kbd_leds & KEYBOARD_LED_CAPSLOCK)
      {
        // Capslock On: disable blink, turn led on
        blink_interval_ms = 0;
        board_led_write(true);
      }else
      {
        // Caplocks Off: back to normal blink
        board_led_write(false);
        blink_interval_ms = BLINK_MOUNTED;
      }
    }
  }
}

//--------------------------------------------------------------------+
// BLINKING TASK
//--------------------------------------------------------------------+
void led_blinking_task(void)
{
  static uint32_t start_ms = 0;
  static bool led_state = false;

  // blink is disabled
  if (!blink_interval_ms) return;

  // Blink every interval ms
  if ( board_millis() - start_ms < blink_interval_ms) return; // not enough time
  start_ms += blink_interval_ms;

  board_led_write(led_state);
  led_state = 1 - led_state; // toggle
}
