/* SPDX-License-Identifier: GPL-3.0-or-later
 * Copyright 2024 King Kévin <kingkevin@cuvoodoo.info>
 */
#include "stm32f0xx_hal.h"

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

#include "bsp/board_api.h"
#include "tusb.h"

#include "usb_descriptors.h"
#include "keylayouts.h"
#include "WjCryptLib_Sha256.h"
#include "date.h"

// get the length of an array
#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
// print debug messages
#define DEBUG (true)

#define BUTTON1_PORT           GPIOA
#define BUTTON1_PIN            GPIO_PIN_5
#define BUTTON1_STATE_ACTIVE   0
#define BUTTON2_PORT           GPIOA
#define BUTTON2_PIN            GPIO_PIN_7
#define BUTTON2_STATE_ACTIVE   0

#define BUTTON_DEBOUNCE (50U) // debounce time in ms

#define TIMEOUT_GLOBAL (12U * 60) // maximum time to hold the credentials, in minutes
#define TIMEOUT_REPEAT (3U * 60) // maximum time after last repeat to hold the credentials, in minutes

char manuf_key[32 + 1]; // the key set by the manufacturer to authenticate the device

static struct config_t {
  uint8_t layout; // keyboard layout
  char user_key[32 + 1]; // the key set by the user to authenticate the device
  bool button_swap; // the order of the buttons (which is for username or password)
  uint16_t timeout_global; // time to hold the credentials, in minutes
  uint16_t timeout_repeat; // time after last repeat to hold the credentials, in minutes
  uint8_t crc; // simple XOR CRC to check config validity
} config;

// page to store manufacturer key (can't be corrupted by save config)
#define FLASH_MANUF_ADDR         ((uint32_t)0x08000000 + 0x400 * 30)
// last available page of last sector to store config
#define FLASH_CONFIG_ADDR       ((uint32_t)0x08000000 + 0x400 * 31)

// the credentials to store and repeat
static char username[64] = {0}; // the credentials username
static char password[64] = {0}; // the credentials password
static bool credentials = false; // if the credentials are valid
static uint32_t entered_when = 0; // when credentials have been entered
static uint32_t pasted_when = 0; // last time the credentials have been pasted

#define KEY_INTERVAL (20U) // time in ms between keypress
static bool key_pressed = false; // if HID keyboard press report is sent
static uint32_t key_when = 0; // when HID keyboard press report is sent
static uint8_t user_paste = 0; // how much of the username is pasted
static uint8_t pass_paste = 0; // how much of the username is pasted

// LED blink period, in ms
#define BLINK_TIME (500U)

// split long string into USB packets
static const char* help_str[] = {
  "\r\npress key to enter menu\r\n",
  "h help\r\n",
  "c enter credentials (ACSII only)\r\n",
  "b swap buttons\r\n",
  "g set global timeout\r\n",
  "r set repeat timeout\r\n",
  "k set authentication key\r\n",
  "a authenticate device\r\n",
  "l set keyboard layout\r\n",
};

static const char* prog_str[] = {
  "K set manufacturer key\r\n",
  "L lock device\r\n",
  "E erase firmware\r\n",
};

enum {
  MENU_HOME,
  MENU_USER,
  MENU_PASS,
  MENU_MANUF,
  MENU_KEY,
  MENU_AUTH,
  MENU_LAYOUT,
  MENU_TIMEOUT_GLOBAL,
  MENU_TIMEOUT_REPEAT,
} menu = MENU_HOME;

void led_blinking_task(void);
void hid_task(void);
void cdc_task(void);
void button_task(void);

static void board_supplement_init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct;
  GPIO_InitStruct.Pin = BUTTON1_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(BUTTON1_PORT, &GPIO_InitStruct);
  GPIO_InitStruct.Pin = BUTTON2_PIN;
  HAL_GPIO_Init(BUTTON2_PORT, &GPIO_InitStruct);
}

uint32_t board_button1_read(void)
{
  return BUTTON1_STATE_ACTIVE == HAL_GPIO_ReadPin(BUTTON1_PORT, BUTTON1_PIN);
}

uint32_t board_button2_read(void)
{
  return BUTTON2_STATE_ACTIVE == HAL_GPIO_ReadPin(BUTTON2_PORT, BUTTON2_PIN);
}

static bool locked(void)
{  
  FLASH_OBProgramInitTypeDef ob;
  HAL_FLASHEx_OBGetConfig(&ob);
  return (OB_RDP_LEVEL_2 == ob.RDPLevel);
}

static void clear_credentials(void)
{
  // clear credentials
  credentials = false;
  memset(username, 0, sizeof(username));
  memset(password, 0, sizeof(password));
  pasted_when = 0;
  entered_when = 0;
  printf("\r\ncredentials cleared\r\n");
}

static void load_config(void)
{
  memcpy(manuf_key, (uint8_t*)FLASH_MANUF_ADDR, sizeof(manuf_key));
  bool manuf_ok = false;
  for (uint8_t i = 0; i < sizeof(manuf_key); i++) {
    if (0 == manuf_key[i]) {
      manuf_ok = true;
      break;
    }
  }
  if (!manuf_ok) {
    memset(manuf_key, 0, sizeof(manuf_key));
  }

  memcpy(&config, (uint8_t*)FLASH_CONFIG_ADDR, sizeof(config));
  uint8_t crc = 0x42; // start with non-zero value to have a checksum work with empty config
  for (uint16_t i = 0; i < sizeof(config); i++) {
    crc ^= *(((uint8_t*)&config) + i);
  }
  if (0 == crc) {
    printf("config loaded\r\n");
  } else {
    memset(&config, 0, sizeof(config));
    printf("config initialized\r\n");
  }
  if (config.layout >= LENGTH(name_asciimap)) {
    config.layout = 0;
  }
  if (0 == config.timeout_global || config.timeout_global > TIMEOUT_GLOBAL) {
    config.timeout_global = TIMEOUT_GLOBAL;
  }
  if (0 == config.timeout_repeat || config.timeout_repeat > TIMEOUT_REPEAT) {
    config.timeout_repeat = TIMEOUT_REPEAT;
  }
}

static void save_config(void)
{
  // update CRC
  config.crc = 0; // clear CRC for correct calcuation
  uint8_t crc = 0x42; // start with non-zero value to have a checksum work with empty config
  for (uint16_t i = 0; i < sizeof(config); i++) {
    crc ^= *(((uint8_t*)&config) + i);
  }
  config.crc = crc;

  // save to flash
  HAL_FLASH_Unlock();
  FLASH_EraseInitTypeDef EraseInitStruct;
  EraseInitStruct.TypeErase   = FLASH_TYPEERASE_PAGES;
  EraseInitStruct.PageAddress = FLASH_CONFIG_ADDR;
  EraseInitStruct.NbPages     = 1;
  uint32_t error;
  if (HAL_FLASHEx_Erase(&EraseInitStruct, &error) != HAL_OK) {
    printf("earsing page failed\r\n");
    goto end;
  }
  for (uint16_t i = 0; i < sizeof(config); i += 4) {
    const uint32_t address = FLASH_CONFIG_ADDR + i;
    uint32_t data;
    memcpy(&data, (uint8_t*)&config + i, sizeof(data)); // works even if the data it not aligned
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data) != HAL_OK) {
      printf("flash page failed\r\n");
      goto end;
    }
  }
  printf("config saved\r\n");
end:
  HAL_FLASH_Lock();
}

static void save_manuf(void)
{
  // save to flash
  HAL_FLASH_Unlock();
  FLASH_EraseInitTypeDef EraseInitStruct;
  EraseInitStruct.TypeErase   = FLASH_TYPEERASE_PAGES;
  EraseInitStruct.PageAddress = FLASH_MANUF_ADDR;
  EraseInitStruct.NbPages     = 1;
  uint32_t error;
  if (HAL_FLASHEx_Erase(&EraseInitStruct, &error) != HAL_OK) {
    printf("earsing page failed\r\n");
    goto end;
  }
  for (uint16_t i = 0; i < sizeof(manuf_key); i += 4) {
    const uint32_t address = FLASH_MANUF_ADDR + i;
    uint32_t data;
    memcpy(&data, (uint8_t*)&manuf_key[i], sizeof(data)); // works even if the data it not aligned
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data) != HAL_OK) {
      printf("flash page failed\r\n");
      goto end;
    }
  }
  printf("manufacturer key saved\r\n");
end:
  HAL_FLASH_Lock();
}

// convert bytes to ASCII hex (does not add end zero)
static void b2h(const uint8_t* b, char* h, uint8_t len)
{
  if (!b || !h || !len) {
    return;
  }
  for (uint8_t i = 0; i < len; i++) {
    uint8_t nibble = b[i] >> 4;
    if (nibble <= 9) {
      h[i * 2 + 0] = '0' + nibble;
    } else {
      h[i * 2 + 0] = 'a' + nibble - 0xa;
    }
    nibble = b[i] & 0xf;
    if (nibble <= 9) {
      h[i * 2 + 1] = '0' + nibble;
    } else {
      h[i * 2 + 1] = 'a' + nibble - 0xa;
    }
  }
}

// ensure string is sent
void tud_cdc_write_str_flush(const char* str)
{
  while (tud_cdc_write_available() < CFG_TUD_CDC_TX_BUFSIZE) {
    tud_task();
  }
  tud_cdc_write_str(str);
  tud_cdc_write_flush();
  while (tud_cdc_write_available() < CFG_TUD_CDC_TX_BUFSIZE) {
    tud_task();
  }
}

int main(void)
{
  board_init();
  board_supplement_init();
  // init device stack on configured roothub port
  tud_init(BOARD_TUD_RHPORT);

  printf("\r\npasskey\r\n");
  printf("hardware version: 2\r\n"); // for now we just have version 2
	printf("firmware date: %04u-%02u-%02u\r\n", BUILD_YEAR, BUILD_MONTH, BUILD_DAY); // show firmware build date
  load_config();

  FLASH_OBProgramInitTypeDef ob;
  HAL_FLASHEx_OBGetConfig(&ob);
  if (OB_RDP_LEVEL_2 != ob.RDPLevel) {
    printf("device not locked\r\n");
  }
  if (0 == strlen(manuf_key)) {
    printf("manufacturer key not set\r\n");
  }

  if (board_init_after_tusb) {
    board_init_after_tusb();
  }
  while (true)
  {
    tud_task(); // tinyusb device task

    // check for credential timeout
    const uint32_t minutes = board_millis();
    if (credentials && ((minutes > entered_when + config.timeout_global * 1000 * 60) || (minutes > entered_when + TIMEOUT_GLOBAL * 1000 * 60))) {
      const char str[] = "global timeout reached, clearing cedentials\r\n";
      tud_cdc_write_str(str);
      tud_cdc_write_flush();
      printf(str);
      clear_credentials();
    }
    if (credentials && ((minutes > pasted_when + config.timeout_repeat * 1000 * 60) || (minutes > pasted_when + TIMEOUT_REPEAT * 1000 * 60))) {
      const char str[] = "repeat timeout reached, clearing cedentials\r\n";
      tud_cdc_write_str(str);
      tud_cdc_write_flush();
      printf(str);
      clear_credentials();
    }

    // run the usual tasks
    led_blinking_task();
    button_task();
    hid_task();
    cdc_task();
  }
}

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

// Invoked when device is mounted
void tud_mount_cb(void)
{
  clear_credentials();
}

// Invoked when device is unmounted
void tud_umount_cb(void)
{
  clear_credentials();
}

// 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;
}

// Invoked when usb bus is resumed
void tud_resume_cb(void)
{
}

//--------------------------------------------------------------------+
// USB CDC
//--------------------------------------------------------------------+
void cdc_task(void)
{
  static char tmp[64];

  // connected() check for DTR bit
  // Most but not all terminal client set this when making connection
  // if ( tud_cdc_connected() )
  {
    // connected and there are data available
    if (tud_cdc_available()) {
      // read data
      char buf[CFG_TUD_CDC_RX_BUFSIZE];
      const uint32_t count = tud_cdc_read(buf, sizeof(buf));
      bool echo = true; // if we echo back input
      if (0 == count) {
        return;
      }

      const char* str = NULL; // message to print
      static uint16_t i = 0; // generic array index
      switch (menu) {
        case MENU_HOME:
          switch (buf[0]) {
            case 'c': // input credentials
              i = 0; // reset index
              menu = MENU_USER; // go to corresponding menu
              str = "\r\nenter credentials to paste\r\nusername: ";
              break;
            case 'b': // swap buttons
              config.button_swap = !config.button_swap;
              str = "\r\nbuttons swapped\r\n";
              save_config();
              break;
            case 'g': // set global timeout
              i = 0; // reset index
              menu = MENU_TIMEOUT_GLOBAL;
              snprintf(tmp, sizeof(tmp), "\r\nenter global timeout in minutes [%u]: ", config.timeout_global);
              str = tmp;
              break;
            case 'r': // set repeat timeout
              i = 0; // reset index
              menu = MENU_TIMEOUT_REPEAT;
              snprintf(tmp, sizeof(tmp), "\r\nenter repeat timeout in minutes [%u]: ", config.timeout_repeat);
              str = tmp;
              break;
            case 'k': // set user key
              i = 0; // reset index
              menu = MENU_KEY;
              str = "\r\nenter authentication key (up to 32 char): ";
              break;
            case 'a': // run authentication
              if (0 == strlen(config.user_key) && 0 == strlen(manuf_key)) {
                str = "\r\nno key set\r\n";
              } else {
                i = 0; // reset index
                menu = MENU_AUTH;
                str = "\r\nenter random string (up to 60 char): ";
              }
              break;
            case 'l': // set keyboard layout
              echo = false;
              tud_cdc_write(buf, 1); // since echo will be off
              tud_cdc_write_str("\n\r");
              tud_cdc_write_flush();
              for (uint8_t j = 0; j < LENGTH(name_asciimap); j++) {
                snprintf(tmp, sizeof(tmp), "%02u %s\r\n", j, name_asciimap[j]);
                tud_cdc_write_str_flush(tmp);
              }
              snprintf(tmp, sizeof(tmp), "set keyboard layout [%s]: ", name_asciimap[config.layout]);
              tud_cdc_write_str_flush(tmp);
              i = 0; // reset index
              menu = MENU_LAYOUT;
              break;
            // hidden menu
            case 'K': // set manufacturer key
              if (!locked()) {
                i = 0; // reset index
                menu = MENU_MANUF;
                str = "\r\nenter manufacturer key (up to 32 char): ";
              }
              break;
            case 'L': // lock device
              if (!locked()) {
                tud_cdc_write_str_flush("\r\nlocking device\r\n");
                FLASH_OBProgramInitTypeDef ob;
                HAL_FLASHEx_OBGetConfig(&ob);
                ob.RDPLevel = OB_RDP_LEVEL_2; // level 2, disabling debug, and write protecting option byte
                ob.WRPState = OB_WRPSTATE_ENABLE; // write protect pages
                ob.WRPPage = 0x7; // all but last sector holding the config
                HAL_FLASH_Unlock();
                HAL_FLASH_OB_Unlock();
                //HAL_FLASHEx_OBProgram(&ob);
                HAL_FLASH_OB_Lock();
                HAL_FLASH_Lock();
                HAL_FLASH_OB_Launch();
              }
              break;
            case 'E': // mass erase to restart STM32 bootloader
              if (!locked()) {
                tud_cdc_write_str_flush("\r\nerasing firmware\r\n");
                HAL_FLASH_Unlock();
                FLASH_EraseInitTypeDef EraseInitStruct;
                EraseInitStruct.TypeErase   = FLASH_TYPEERASE_MASSERASE;
                uint32_t error;
                if (HAL_FLASHEx_Erase(&EraseInitStruct, &error) != HAL_OK) {
                  printf("mass erase failed\r\n");
                }
                HAL_FLASH_Lock();
                HAL_FLASH_OB_Launch(); // just to restart
              }
              break;
            case '\r':
            case '\n':
              break; // nothing to do
            case 'h':
            default:
              tud_cdc_write(buf, 1); // since echo will be off
              tud_cdc_write_flush();
              echo = false;
              for (uint8_t j = 0; j < LENGTH(help_str); j++) {
                tud_cdc_write_str_flush(help_str[j]);
              }
              if (!locked()) {
                for (uint8_t j = 0; j < LENGTH(prog_str); j++) {
                  tud_cdc_write_str_flush(prog_str[j]);
                }
              }
          }
          break;
        case MENU_USER:
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              username[i] = 0; // end string
              str = NULL;
              i = 0; // reset index
              menu = MENU_PASS; // go to next menu
              str = "\r\npassword: ";
            } else if (i >= sizeof(username) - 2) {
              memset(username, 0, sizeof(username)); // clear username
              str = "\r\nlimit reached\r\n";
            } else {
              username[i++] = buf[j]; // save username
            }
          }
          break;
        case MENU_PASS:
          echo = false; // keep secret
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              password[i] = 0; // end password
              if (strlen(username) && strlen(password)) {
                credentials = true;
                entered_when = board_millis();
                pasted_when = board_millis();
                str = "\r\ncredentials saved\r\n";
              } else {
                credentials = false;
                str = "\r\ninvalid credentials\r\n";
              }
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(password) - 2) {
              memset(password, 0, sizeof(password)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              password[i++] = buf[j]; // save password
            }
          }
          break;
        case MENU_MANUF:
          echo = false; // keep secret
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              tmp[i] = 0; // end key
              if (strlen(tmp)) {
                memset(manuf_key, 0, sizeof(manuf_key));
                memcpy(manuf_key, tmp, i);
                save_manuf();
                str = "\r\nmanufacturer key saved\r\n";
              } else {
                str = "\r\ninvalid input\r\n";
              }
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(config.user_key) - 2) {
              memset(tmp, 0, sizeof(tmp)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              tmp[i++] = buf[j]; // save password
            }
          }
          break;
        case MENU_KEY:
          echo = false; // keep secret
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              tmp[i] = 0; // end key
              if (strlen(tmp)) {
                memset(config.user_key, 0, sizeof(config.user_key));
                memcpy(config.user_key, tmp, i);
                save_config();
                str = "\r\nauthentication key saved\r\n";
              } else {
                echo = true;
                str = "\r\ninvalid input\r\n";
              }
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(config.user_key) - 2) {
              memset(tmp, 0, sizeof(tmp)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              tmp[i++] = buf[j]; // save password
            }
          }
          break;
        case MENU_AUTH:
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              tmp[i] = 0; // end string
              if (strlen(tmp)) {
                echo = false; // we need to write now
                if (strlen(manuf_key)) {
                  char uid[4 * 3 * 2 + 1];
                  b2h((uint8_t*)UID_BASE, uid, 4 * 3);
                  uid[sizeof(uid) - 1] = 0; // end string
                  tud_cdc_write_str_flush("\r\ngo to https://passkey.cuvoodoo.info/?auth=");
                  tud_cdc_write_str_flush(uid);
                  tud_cdc_write_str_flush(tmp);
                  uint8_t hash_in[sizeof(manuf_key) + sizeof(tmp)];
                  memcpy(&hash_in[0], manuf_key, strlen(manuf_key));
                  memcpy(&hash_in[strlen(manuf_key)], tmp, strlen(tmp));
                  SHA256_HASH hash_out;
                  Sha256Calculate(hash_in, strlen(manuf_key) + strlen(tmp), &hash_out);
                  char hash_str[SHA256_HASH_SIZE * 2 + 1];
                  b2h(hash_out.bytes, hash_str, SHA256_HASH_SIZE);
                  hash_str[SHA256_HASH_SIZE * 2] = 0; // end string
                  tud_cdc_write_str_flush("\r\nexpected response: ");
                  tud_cdc_write_str_flush(hash_str); // luckily the USB packet len is the same as the string
                }
                if (strlen(config.user_key)) {
                  uint8_t hash_in[sizeof(config.user_key) + sizeof(tmp)];
                  memcpy(&hash_in[0], config.user_key, strlen(config.user_key));
                  memcpy(&hash_in[strlen(config.user_key)], tmp, strlen(tmp));
                  SHA256_HASH hash_out;
                  Sha256Calculate(hash_in, strlen(config.user_key) + strlen(tmp), &hash_out);
                  char hash_str[SHA256_HASH_SIZE * 2 + 1];
                  b2h(hash_out.bytes, hash_str, SHA256_HASH_SIZE);
                  hash_str[SHA256_HASH_SIZE * 2] = 0; // end string
                  tud_cdc_write_str_flush("\r\necho -n \"<key>");
                  tud_cdc_write_str_flush(tmp);
                  tud_cdc_write_str_flush("\" | sha256sum\r\n");
                  tud_cdc_write_str_flush(hash_str); // luckily the USB packet len is the same as the string
                }
                str = "\r\n";
              } else {
                str = "\r\ninvalid input\r\n";
              }
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(config.user_key) - 2) {
              memset(tmp, 0, sizeof(tmp)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              tmp[i++] = buf[j]; // save password
            }
          }
          break;
        case MENU_LAYOUT:
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              tmp[i] = 0; // end time input
              const uint16_t layout = atoi(tmp);
              if (0 == strlen(tmp)) {
                str = "\r\n"; // leave previous
              } else if (layout > LENGTH(map_asciimap)) {
                str = "\r\ninvalid entry\r\n";
              } else {
                config.layout = layout;
                save_config();
                str = "\r\nlayout set\r\n";
              }
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(tmp) - 2) {
              memset(tmp, 0, sizeof(tmp)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              tmp[i++] = buf[j]; // save user entry
            }
          }
          break;
        case MENU_TIMEOUT_GLOBAL:
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              tmp[i] = 0; // end time input
              const uint16_t time = atoi(tmp);
              if (0 == strlen(tmp)) {
                str = "\r\n"; // leave previous
              } else {
                if (0 == time || config.timeout_global > TIMEOUT_GLOBAL) {
                  config.timeout_global = TIMEOUT_GLOBAL;
                }
                save_config();
              }
              snprintf(tmp, sizeof(tmp), "\r\nglobal timeout set to %u minutes\r\n", config.timeout_global);
              str = tmp;
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(tmp) - 2) {
              memset(tmp, 0, sizeof(tmp)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              tmp[i++] = buf[j]; // save password
            }
          }
          break;
        case MENU_TIMEOUT_REPEAT:
          for (uint16_t j = 0; j < count; j++) {
            if ('\r' == buf[j] || '\n' == buf[j]) { // end received
              tmp[i] = 0; // end time input
              const uint16_t time = atoi(tmp);
              if (0 == strlen(tmp)) {
                str = "\r\n"; // leave previous
              } else {
                if (0 == time || config.timeout_repeat > TIMEOUT_REPEAT) {
                  config.timeout_repeat = TIMEOUT_REPEAT;
                }
                save_config();
              }
              snprintf(tmp, sizeof(tmp), "\r\nrepeat timeout set to %u minutes\r\n", config.timeout_repeat);
              str = tmp;
              i = 0; // reset index
              menu = MENU_HOME; // go to next menu
            } else if (i >= sizeof(tmp) - 2) {
              memset(tmp, 0, sizeof(tmp)); // clear password
              str = "\r\nlimit reached\r\n";
            } else {
              tmp[i++] = buf[j]; // save password
            }
          }
          break;
        default: // unknown menu
          menu = MENU_HOME; // revert to home
      }

      if (echo) {
        tud_cdc_write(buf, count);
      }
      if (str) {
        tud_cdc_write_str(str);
      }
      if (echo || str) {
        tud_cdc_write_flush();
      }
    }
  }
}

// Invoked when cdc when line state changed e.g connected/disconnected
void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts)
{
  (void) itf;
  (void) rts;

  // TODO set some indicator
  if (dtr) {
    // Terminal connected
  } else {
    // Terminal disconnected
  }
}

// Invoked when CDC interface received data from host
void tud_cdc_rx_cb(uint8_t itf)
{
  (void) itf;
}

//--------------------------------------------------------------------+
// USB HID
//--------------------------------------------------------------------+
static void send_hid_report(uint8_t modifier, uint8_t keycode)
{
  // skip if hid is not ready yet
  if ( !tud_hid_ready() ) {
    return;
  }

  uint8_t keycodes[6] = {keycode, 0, 0, 0, 0, 0};
  if (keycode) {
    key_when = board_millis(); // remember when we sent the key
    key_pressed = true; // remember we pressed a key
  } else {
    key_pressed = false; // key released
  }
  tud_hid_keyboard_report(REPORT_ID_KEYBOARD, modifier, keycodes);
}

static void send_hid_char(char c)
{
  // set default to US
  if (config.layout >= LENGTH(map_asciimap)) {
    config.layout = 0;
  }

  if (0 == c) {
	send_hid_report(0, 0);
  } else if ('\t' == c) {
	send_hid_report(0, KEY_TAB);
  } else if ('\r' == c || '\n' == c) {
	send_hid_report(0, KEY_ENTER);
  } else if (c >= ' ' && c <= '~') {
    const uint16_t code = map_asciimap[config.layout][c - ' '];
    const uint8_t modifier = code >> 8;
    const uint8_t keycode = code & 0xff;
    send_hid_report(modifier, keycode);
  }
}

void hid_task(void)
{
  if (key_when) { // pasting started
    if (board_millis() < key_when + KEY_INTERVAL) {
      return; // wait more before next key press
    }
    if (!credentials) { // nothing to send
      key_when = 0;
      user_paste = 0;
      pass_paste = 0;
      return;
    }
    if (pass_paste) { // send password
      if (pass_paste < strlen(password)) { // send password
        send_hid_char(password[pass_paste++]);
      } else if (pass_paste == strlen(password)) { // press enter
        send_hid_char('\n');
        pass_paste++;
      } else if (pass_paste == (strlen(password) + 1)) { // finished pasting
        user_paste = 0;
        pass_paste = 0;
        key_when = 0;
      }
    }
    if (user_paste) { // send username
      if (user_paste < strlen(username)) { // send username
        send_hid_char(username[user_paste++]);
      } else if (user_paste == strlen(username)) { // switch to password
        send_hid_char('\t');
        user_paste++;
      } else if (user_paste == (strlen(username) + 1)) { // start sending password
        pass_paste = 0;
        send_hid_char(password[pass_paste++]);
        user_paste++;
      }
    }
  }
}

static void paste_credentials(bool user)
{
  if (!credentials || 0 == strlen(username) || 0 == strlen(password)) {
    printf("no credentials to paste\r\n");
    return;
  }

  user_paste = 0;
  pass_paste = 0;
  pasted_when = board_millis();

  if (user) {
    send_hid_char(username[user_paste++]);
    printf("pasting username and password\r\n");
  } else {
    send_hid_char(password[pass_paste++]);
    printf("pasting password\r\n");
  }
}

void button_task(void)
{
  static uint32_t button1_ms = 0; // when the button has been pressed (in ms)
  static bool button1_ok = false; // if the button is pressed (after debounce)
  static uint32_t button2_ms = 0; // when the button has been pressed (in ms)
  static bool button2_ok = false; // if the button is pressed (after debounce)

  // Poll every 10ms
  const uint32_t interval_ms = 10;
  static uint32_t start_ms = 0;

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

  const bool button1_pressed = board_button1_read();
  const bool button2_pressed = board_button2_read();

  // Remote wakeup
  if ( tud_suspended() && (button1_pressed || button2_pressed) ) {
    // Wake up host if we are in suspend mode
    // and REMOTE_WAKEUP feature is enabled by host
    tud_remote_wakeup();
    return;
  }

  // ensure button is pressed
  if (button1_pressed && !button1_ok) {
    if (0 == button1_ms) {
      button1_ms = board_millis();
    } else if ( board_millis() + BUTTON_DEBOUNCE > button1_ms ) {
      button1_ok = true;
    }
  }
  if (button2_pressed && !button2_ok) {
    if (0 == button2_ms) {
      button2_ms = board_millis();
    } else if ( board_millis() + BUTTON_DEBOUNCE > button2_ms ) {
      button2_ok = true;
    }
  }

  // check which button is pressed/released
  if (button1_pressed && button2_pressed && button1_ok && button2_ok) { // two buttons pressed
    if (credentials) {
      clear_credentials();
      printf("clearing credentials\r\n");
    }
  } else if (!button1_pressed && button1_ok && !button2_ok) { // button 1 released
    //past_credentials(!config.button_swap); // I have no idea why this does not work
    if (config.button_swap) {
      paste_credentials(false);
    } else {
      paste_credentials(true);
    }
  } else if (!button2_pressed && !button1_ok && button2_ok) { // button 2 released
    if (config.button_swap) {
      paste_credentials(true);
    } else {
      paste_credentials(false);
    }
  }

  // clear button state
  if (!button1_pressed) {
    button1_ok = false;
    button1_ms = 0;
  }
  if (!button2_pressed) {
    button2_ok = false;
    button2_ms = 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, uint16_t len)
{
  (void) instance;
  (void) report;
  (void) len;

  if ((report[0] == REPORT_ID_KEYBOARD) && key_pressed) {
    //tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, NULL); // release press
    send_hid_report(0, 0); // release key
    //return;
  }
}

// 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;
  static uint8_t kbd_leds_last = 0; // last state
  static uint32_t kbd_leds_when = 0; // when did the state change
  static uint8_t kbd_leds_count = 0; // how many times it changed (within a certain period)

  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];
      uint8_t const kbd_leds_change = kbd_leds ^ kbd_leds_last;
      if (kbd_leds_change & (KEYBOARD_LED_CAPSLOCK | KEYBOARD_LED_NUMLOCK)) { // numlock or capslock pressed
        if (board_millis() - kbd_leds_when > 500) { // not fast enough
          kbd_leds_count = 0; // reset count
        }
        kbd_leds_count++;
        kbd_leds_when = board_millis();
        if (kbd_leds_count > 3 && credentials) {
          clear_credentials();
        }
      }
      kbd_leds_last = kbd_leds; // remember for next comparision
    }
  }
}

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

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

  if (!credentials) {
    led_state = !led_state; // toggle
  } else {
    led_state = true;
  }
  board_led_write(led_state);

  board_uart_write(".", 1); // debug heartbeat
}
