/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2023 Scott Shawcroft for Adafruit Industries
 * Copyright (c) 2023 Jeff Epler for Adafruit Industries
 *
 * 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 "tusb.h"

#include "py/ringbuf.h"
#include "py/runtime.h"
#include "shared/runtime/interrupt_char.h"
#include "supervisor/usb.h"
#include "supervisor/background_callback.h"
#include "supervisor/shared/tick.h"

#ifndef DEBUG
#define DEBUG (0)
#endif

// Buffer the incoming serial data in the background so that we can look for the
// interrupt character.
STATIC ringbuf_t _incoming_ringbuf;
STATIC uint8_t _buf[16];

STATIC uint8_t _dev_addr;
STATIC uint8_t _interface;

#define FLAG_SHIFT (1)
#define FLAG_NUMLOCK (2)
#define FLAG_CTRL (4)
#define FLAG_STRING (8)

STATIC uint8_t user_keymap[384];
STATIC size_t user_keymap_len = 0;

void usb_keymap_set(const uint8_t *buf, size_t len) {
    user_keymap_len = len = MIN(len, sizeof(user_keymap));
    memcpy(user_keymap, buf, len);
    memset(user_keymap + len, 0, sizeof(user_keymap) - len);
}

struct keycode_mapper {
    uint8_t first, last, code, flags;
    const char *data;
};

#define SEP "\0" // separator in FLAG_STRING sequences
#define NOTHING "" // in FLAG_STRING sequences
#define CURSOR_UP "\e[A"
#define CURSOR_DOWN "\e[B"
#define CURSOR_LEFT "\e[D"
#define CURSOR_RIGHT "\e[C"
#define CURSOR_PGUP "\e[5~"
#define CURSOR_PGDN "\e[6~"
#define CURSOR_HOME "\e[H"
#define CURSOR_END "\e[F"
#define CURSOR_INS "\e[2~"
#define CURSOR_DEL "\e[3~"

STATIC struct keycode_mapper keycode_to_ascii[] = {
    { HID_KEY_A, HID_KEY_Z, 'a', 0, NULL},

    { HID_KEY_1, HID_KEY_9, 0, FLAG_SHIFT, "!@#$%^&*()" },
    { HID_KEY_1, HID_KEY_9, '1', 0, },
    { HID_KEY_0, HID_KEY_0, ')', FLAG_SHIFT, },
    { HID_KEY_0, HID_KEY_0, '0', 0, },

    { HID_KEY_ENTER, HID_KEY_ENTER, '\n', FLAG_CTRL },
    { HID_KEY_ENTER, HID_KEY_SLASH, 0, FLAG_SHIFT, "\n\x1b\177\t _+{}|~:\"~<>?" },
    { HID_KEY_ENTER, HID_KEY_SLASH, 0, 0, "\r\x1b\10\t -=[]\\#;'`,./" },

    { HID_KEY_F1, HID_KEY_F1, 0x1e, 0, }, // help key on xerox 820 kbd

    { HID_KEY_KEYPAD_DIVIDE, HID_KEY_KEYPAD_DECIMAL, 0, FLAG_NUMLOCK | FLAG_STRING,
      "/\0" "*\0" "-\0" "+\0" "\n\0" CURSOR_END SEP CURSOR_DOWN SEP CURSOR_PGDN SEP CURSOR_LEFT SEP NOTHING SEP CURSOR_RIGHT SEP CURSOR_HOME SEP CURSOR_UP SEP CURSOR_PGDN SEP CURSOR_INS SEP CURSOR_DEL},
    { HID_KEY_KEYPAD_DIVIDE, HID_KEY_KEYPAD_DECIMAL, 0, 0, "/*-+\n1234567890." },

    { HID_KEY_ARROW_RIGHT, HID_KEY_ARROW_UP, 0, FLAG_STRING, CURSOR_RIGHT SEP CURSOR_LEFT SEP CURSOR_DOWN SEP CURSOR_UP },

};

STATIC bool report_contains(const hid_keyboard_report_t *report, uint8_t key) {
    for (int i = 0; i < 6; i++) {
        if (report->keycode[i] == key) {
            return true;
        }
    }
    return false;
}

STATIC const char *old_buf = NULL;
STATIC size_t buf_size = 0;
// this matches Linux default of 500ms to first repeat, 1/20s thereafter
enum { initial_repeat_time = 500, default_repeat_time = 50 };
STATIC uint64_t repeat_deadline;
STATIC void repeat_f(void *unused);
background_callback_t repeat_cb = {repeat_f, NULL, NULL, NULL};

STATIC void set_repeat_deadline(uint64_t new_deadline) {
    repeat_deadline = new_deadline;
    background_callback_add_core(&repeat_cb);
}

STATIC void send_bufn_core(const char *buf, size_t n) {
    old_buf = buf;
    buf_size = n;
    // repeat_timeout = millis() + repeat_time;
    for (; n--; buf++) {
        int code = *buf;
        if (code == mp_interrupt_char) {
            mp_sched_keyboard_interrupt();
            return;
        }
        if (ringbuf_num_empty(&_incoming_ringbuf) == 0) {
            // Drop on the floor
            return;
        }
        ringbuf_put(&_incoming_ringbuf, code);
    }
}

STATIC void send_bufn(const char *buf, size_t n) {
    send_bufn_core(buf, n);
    set_repeat_deadline(supervisor_ticks_ms64() + initial_repeat_time);
}

STATIC void send_bufz(const char *buf) {
    send_bufn(buf, strlen(buf));
}

STATIC void send_byte(uint8_t code) {
    static char buf[1];
    buf[0] = code;
    send_bufn(buf, 1);
}

STATIC void send_repeat(void) {
    if (old_buf) {
        uint64_t now = supervisor_ticks_ms64();
        if (now >= repeat_deadline) {
            send_bufn_core(old_buf, buf_size);
            set_repeat_deadline(now + default_repeat_time);
        } else {
            background_callback_add_core(&repeat_cb);
        }
    }
}

STATIC void repeat_f(void *unused) {
    send_repeat();
}

hid_keyboard_report_t old_report;

STATIC const char *skip_nuls(const char *buf, size_t n) {
    while (n--) {
        buf += strlen(buf) + 1;
    }
    return buf;
}

STATIC void process_event(uint8_t dev_addr, uint8_t instance, const hid_keyboard_report_t *report) {
    bool has_altgr = (user_keymap_len > 256);
    bool altgr = has_altgr && report->modifier & 0x40;
    bool alt = has_altgr ? report->modifier & 0x4 : report->modifier & 0x44;
    bool shift = report->modifier & 0x22;
    bool ctrl = report->modifier & 0x11;
    bool caps = old_report.reserved & 1;
    bool num = old_report.reserved & 2;
    uint8_t code = 0;

    if (report->keycode[0] == 1 && report->keycode[1] == 1) {
        // keyboard says it has exceeded max kro
        return;
    }

    // something was pressed or released, so cancel any key repeat
    old_buf = NULL;

    for (int i = 0; i < 6; i++) {
        uint8_t keycode = report->keycode[i];
        if (keycode == 0) {
            continue;
        }
        if (report_contains(&old_report, keycode)) {
            continue;
        }

        /* key is newly pressed */
        if (keycode == HID_KEY_NUM_LOCK) {
            num = !num;
        } else if (keycode == HID_KEY_CAPS_LOCK) {
            caps = !caps;
        } else {
            size_t idx = keycode + (altgr ? 256 : shift ? 128 : 0);
            uint8_t ascii = user_keymap[idx];
            #if DEBUG
            mp_printf(&mp_plat_print, "lookup HID keycode %d mod %x at idx %d -> ascii %d (%c)\n",
                keycode, report->modifier, idx, ascii, ascii >= 32 && ascii <= 126 ? ascii : '.');
            #endif
            if (ascii != 0) {
                if (ctrl) {
                    ascii &= 0x1f;
                } else if (ascii >= 'a' && ascii <= 'z' && caps) {
                    ascii ^= ('a' ^ 'A');
                }
                send_byte(ascii);
                continue;
            }

            for (size_t j = 0; j < MP_ARRAY_SIZE(keycode_to_ascii); j++) {
                struct keycode_mapper *mapper = &keycode_to_ascii[j];
                if (!(keycode >= mapper->first && keycode <= mapper->last)) {
                    continue;
                }
                if (mapper->flags & FLAG_SHIFT && !shift) {
                    continue;
                }
                if (mapper->flags & FLAG_NUMLOCK && !num) {
                    continue;
                }
                if (mapper->flags & FLAG_CTRL && !ctrl) {
                    continue;
                }
                if (mapper->flags & FLAG_STRING) {
                    const char *msg = skip_nuls(mapper->data, keycode - mapper->first);
                    send_bufz(msg);
                    break;
                } else if (mapper->data) {
                    code = mapper->data[keycode - mapper->first];
                } else {
                    code = keycode - mapper->first + mapper->code;
                }
                if (code >= 'a' && code <= 'z' && (shift ^ caps)) {
                    code ^= ('a' ^ 'A');
                }
                if (ctrl) {
                    code &= 0x1f;
                }
                if (alt) {
                    code ^= 0x80;
                }
                send_byte(code);
                break;
            }
        }
    }

    uint8_t leds = (caps | (num << 1));
    if (leds != old_report.reserved) {
        tuh_hid_set_report(dev_addr, instance /*idx*/, 0 /*report_id*/, HID_REPORT_TYPE_OUTPUT /*report_type*/, &leds, sizeof(leds));
    }
    old_report = *report;
    old_report.reserved = leds;
}

bool usb_keyboard_in_use(uint8_t dev_addr, uint8_t interface) {
    return _dev_addr == dev_addr && _interface == interface;
}

void usb_keyboard_detach(uint8_t dev_addr, uint8_t interface) {
    if (!usb_keyboard_in_use(dev_addr, interface)) {
        return;
    }
    _dev_addr = 0;
    _interface = 0;
}

void usb_keyboard_attach(uint8_t dev_addr, uint8_t interface) {
    if (usb_keyboard_in_use(dev_addr, interface) || _dev_addr != 0) {
        return;
    }
    uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, interface);
    if (itf_protocol == HID_ITF_PROTOCOL_KEYBOARD) {
        _dev_addr = dev_addr;
        _interface = interface;
        tuh_hid_receive_report(dev_addr, interface);
    }
}

void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t interface, uint8_t const *desc_report, uint16_t desc_len) {
    usb_keyboard_attach(dev_addr, interface);
}

void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t interface) {
    usb_keyboard_detach(dev_addr, interface);
}

void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const *report, uint16_t len) {
    if (len != sizeof(hid_keyboard_report_t)) {
        return;
    } else {
        process_event(dev_addr, instance, (hid_keyboard_report_t *)report);
    }
    // continue to request to receive report
    tuh_hid_receive_report(dev_addr, instance);
}

void usb_keyboard_init(void) {
    ringbuf_init(&_incoming_ringbuf, _buf, sizeof(_buf) - 1);
}

bool usb_keyboard_chars_available(void) {
    return ringbuf_num_filled(&_incoming_ringbuf) > 0;
}

char usb_keyboard_read_char(void) {
    if (ringbuf_num_filled(&_incoming_ringbuf) > 0) {
        return ringbuf_get(&_incoming_ringbuf);
    }
    return -1;
}
