/*
Copyright 2018 Massdrop Inc.

This program 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 Foundation, either version 2 of the License, or
(at your option) any later version.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "samd51j18a.h"
#include "keyboard.h"

#include "report.h"
#include "host.h"
#include "host_driver.h"
#include "suspend.h"
#include "keycode_config.h"
#include <string.h>

// From protocol directory
#include "arm_atsam_protocol.h"

// From keyboard's directory
#include "config_led.h"

uint8_t g_usb_state = USB_FSMSTATUS_FSMSTATE_OFF_Val; // Saved USB state from hardware value to detect changes

void    main_subtasks(void);
uint8_t keyboard_leds(void);
void    send_keyboard(report_keyboard_t *report);
void    send_nkro(report_nkro_t *report);
void    send_mouse(report_mouse_t *report);
void    send_extra(report_extra_t *report);

#ifdef DEFERRED_EXEC_ENABLE
void deferred_exec_task(void);
#endif // DEFERRED_EXEC_ENABLE

host_driver_t arm_atsam_driver = {keyboard_leds, send_keyboard, send_nkro, send_mouse, send_extra};

uint8_t led_states;

uint8_t keyboard_leds(void) {
#ifdef NKRO_ENABLE
    if (keymap_config.nkro)
        return udi_hid_nkro_report_set;
    else
#endif // NKRO_ENABLE
        return udi_hid_kbd_report_set;
}

void send_keyboard(report_keyboard_t *report) {
    uint32_t irqflags;

    while (udi_hid_kbd_b_report_trans_ongoing) {
        main_subtasks();
    } // Run other tasks while waiting for USB to be free

    irqflags = __get_PRIMASK();
    __disable_irq();
    __DMB();

    memcpy(udi_hid_kbd_report, report, UDI_HID_KBD_REPORT_SIZE);
    udi_hid_kbd_b_report_valid = 1;
    udi_hid_kbd_send_report();

    __DMB();
    __set_PRIMASK(irqflags);
}

void send_nkro(report_nkro_t *report) {
#ifdef NKRO_ENABLE
    uint32_t irqflags;

    while (udi_hid_nkro_b_report_trans_ongoing) {
        main_subtasks();
    } // Run other tasks while waiting for USB to be free

    irqflags = __get_PRIMASK();
    __disable_irq();
    __DMB();

    memcpy(udi_hid_nkro_report, report, UDI_HID_NKRO_REPORT_SIZE);
    udi_hid_nkro_b_report_valid = 1;
    udi_hid_nkro_send_report();

    __DMB();
    __set_PRIMASK(irqflags);
#endif
}

void send_mouse(report_mouse_t *report) {
#ifdef MOUSEKEY_ENABLE
    uint32_t irqflags;

    irqflags = __get_PRIMASK();
    __disable_irq();
    __DMB();

    memcpy(udi_hid_mou_report, report, UDI_HID_MOU_REPORT_SIZE);
    udi_hid_mou_b_report_valid = 1;
    udi_hid_mou_send_report();

    __DMB();
    __set_PRIMASK(irqflags);
#endif // MOUSEKEY_ENABLE
}

void send_extra(report_extra_t *report) {
#ifdef EXTRAKEY_ENABLE
    uint32_t irqflags;

    irqflags = __get_PRIMASK();
    __disable_irq();
    __DMB();

    memcpy(udi_hid_exk_report, report, UDI_HID_EXK_REPORT_SIZE);
    udi_hid_exk_b_report_valid = 1;
    udi_hid_exk_send_report();

    __DMB();
    __set_PRIMASK(irqflags);
#endif // EXTRAKEY_ENABLE
}

#ifdef CONSOLE_ENABLE
#    define CONSOLE_PRINTBUF_SIZE 512
static char     console_printbuf[CONSOLE_PRINTBUF_SIZE];
static uint16_t console_printbuf_len = 0;

int8_t sendchar(uint8_t c) {
    if (console_printbuf_len >= CONSOLE_PRINTBUF_SIZE) return -1;

    console_printbuf[console_printbuf_len++] = c;
    return 0;
}

void main_subtask_console_flush(void) {
    while (udi_hid_con_b_report_trans_ongoing) {
    } // Wait for any previous transfers to complete

    uint16_t result = console_printbuf_len;
    uint32_t irqflags;
    char *   pconbuf  = console_printbuf; // Pointer to start send from
    int      send_out = CONSOLE_EPSIZE;   // Bytes to send per transfer

    while (result > 0) { // While not error and bytes remain
        while (udi_hid_con_b_report_trans_ongoing) {
        } // Wait for any previous transfers to complete

        irqflags = __get_PRIMASK();
        __disable_irq();
        __DMB();

        if (result < CONSOLE_EPSIZE) {                     // If remaining bytes are less than console epsize
            memset(udi_hid_con_report, 0, CONSOLE_EPSIZE); // Clear the buffer
            send_out = result;                             // Send remaining size
        }

        memcpy(udi_hid_con_report, pconbuf, send_out); // Copy data into the send buffer

        udi_hid_con_b_report_valid = 1; // Set report valid
        udi_hid_con_send_report();      // Send report

        __DMB();
        __set_PRIMASK(irqflags);

        result -= send_out;  // Decrement result by bytes sent
        pconbuf += send_out; // Increment buffer point by bytes sent
    }

    console_printbuf_len = 0;
}

#endif // CONSOLE_ENABLE

void main_subtask_usb_state(void) {
    static uint64_t fsmstate_on_delay = 0;                         // Delay timer to be sure USB is actually operating before bringing up hardware
    uint8_t         fsmstate_now      = USB->DEVICE.FSMSTATUS.reg; // Current state from hardware register

    if (fsmstate_now == USB_FSMSTATUS_FSMSTATE_SUSPEND_Val) // If USB SUSPENDED
    {
        fsmstate_on_delay = 0; // Clear ON delay timer

        if (g_usb_state != USB_FSMSTATUS_FSMSTATE_SUSPEND_Val) // If previously not SUSPENDED
        {
            suspend_power_down();       // Run suspend routine
            g_usb_state = fsmstate_now; // Save current USB state
        }
    } else if (fsmstate_now == USB_FSMSTATUS_FSMSTATE_SLEEP_Val) // Else if USB SLEEPING
    {
        fsmstate_on_delay = 0; // Clear ON delay timer

        if (g_usb_state != USB_FSMSTATUS_FSMSTATE_SLEEP_Val) // If previously not SLEEPING
        {
            suspend_power_down();       // Run suspend routine
            g_usb_state = fsmstate_now; // Save current USB state
        }
    } else if (fsmstate_now == USB_FSMSTATUS_FSMSTATE_ON_Val) // Else if USB ON
    {
        if (g_usb_state != USB_FSMSTATUS_FSMSTATE_ON_Val) // If previously not ON
        {
            if (fsmstate_on_delay == 0) // If ON delay timer is cleared
            {
                fsmstate_on_delay = timer_read64() + 250;  // Set ON delay timer
            } else if (timer_read64() > fsmstate_on_delay) // Else if ON delay timer is active and timed out
            {
                suspend_wakeup_init();      // Run wakeup routine
                g_usb_state = fsmstate_now; // Save current USB state
            }
        }
    } else // Else if USB is in a state not being tracked
    {
        fsmstate_on_delay = 0; // Clear ON delay timer
    }
}

void main_subtask_power_check(void) {
    static uint64_t next_5v_checkup = 0;

    if (timer_read64() > next_5v_checkup) {
        next_5v_checkup = timer_read64() + 5;

        v_5v     = adc_get(ADC_5V);
        v_5v_avg = 0.9 * v_5v_avg + 0.1 * v_5v;

#ifdef RGB_MATRIX_ENABLE
        gcr_compute();
#endif
    }
}

void main_subtask_usb_extra_device(void) {
    static uint64_t next_usb_checkup = 0;

    if (timer_read64() > next_usb_checkup) {
        next_usb_checkup = timer_read64() + 10;

        USB_HandleExtraDevice();
    }
}

#ifdef RAW_ENABLE
void main_subtask_raw(void) {
    udi_hid_raw_receive_report();
}
#endif

void main_subtasks(void) {
    main_subtask_usb_state();
    main_subtask_power_check();
    main_subtask_usb_extra_device();
#ifdef CONSOLE_ENABLE
    main_subtask_console_flush();
#endif
#ifdef RAW_ENABLE
    main_subtask_raw();
#endif
}

int main(void) {
    DBG_LED_ENA;
    DBG_1_ENA;
    DBG_1_OFF;
    DBG_2_ENA;
    DBG_2_OFF;
    DBG_3_ENA;
    DBG_3_OFF;

    debug_code_init();

    CLK_init();

    ADC0_init();

    SR_EXP_Init();

#ifdef RGB_MATRIX_ENABLE
    i2c1_init();
#endif // RGB_MATRIX_ENABLE

    USB_Hub_init();

    DBGC(DC_MAIN_UDC_START_BEGIN);
    udc_start();
    DBGC(DC_MAIN_UDC_START_COMPLETE);

    DBGC(DC_MAIN_CDC_INIT_BEGIN);
    CDC_init();
    DBGC(DC_MAIN_CDC_INIT_COMPLETE);

    while (USB_Hub_Port_Detect_Init() == 0) {
    }

    DBG_LED_OFF;

#ifdef RGB_MATRIX_ENABLE
    while (I2C3733_Init_Control() != 1) {
    }
    while (I2C3733_Init_Drivers() != 1) {
    }

    I2C_DMAC_LED_Init();

    i2c_led_q_init();

    for (uint8_t drvid = 0; drvid < ISSI3733_DRIVER_COUNT; drvid++)
        I2C_LED_Q_ONOFF(drvid); // Queue data
#endif                          // RGB_MATRIX_ENABLE

    keyboard_setup();

    keyboard_init();

    host_set_driver(&arm_atsam_driver);

#ifdef CONSOLE_ENABLE
    uint64_t next_print = 0;
#endif // CONSOLE_ENABLE

    v_5v_avg = adc_get(ADC_5V);

    debug_code_disable();

    while (1) {
        main_subtasks(); // Note these tasks will also be run while waiting for USB keyboard polling intervals

        if (g_usb_state == USB_FSMSTATUS_FSMSTATE_SUSPEND_Val || g_usb_state == USB_FSMSTATUS_FSMSTATE_SLEEP_Val) {
            if (suspend_wakeup_condition()) {
                udc_remotewakeup(); // Send remote wakeup signal
                wait_ms(50);
            }

            continue;
        }

        keyboard_task();

#ifdef CONSOLE_ENABLE
        if (timer_read64() > next_print) {
            next_print = timer_read64() + 250;
            // Add any debug information here that you want to see very often
            // dprintf("5v=%u 5vu=%u dlow=%u dhi=%u gca=%u gcd=%u\r\n", v_5v, v_5v_avg, v_5v_avg - V5_LOW, v_5v_avg - V5_HIGH, gcr_actual, gcr_desired);
        }
#endif // CONSOLE_ENABLE

#ifdef DEFERRED_EXEC_ENABLE
        // Run deferred executions
        deferred_exec_task();
#endif // DEFERRED_EXEC_ENABLE

        // Run housekeeping
        housekeeping_task();
    }

    return 1;
}
