/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2025 by Aidan MacDonald
 *
 * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include "kernel/kernel-internal.h"
#include "system.h"
#include "power.h"
#include "lcd.h"
#include "backlight.h"
#include "button.h"
#include "storage.h"
#include "disk.h"
#include "file.h"
#include "file_internal.h"
#include "usb.h"
#include "elf.h"
#include "elf_loader.h"
#include "rbversion.h"
#include "system-echoplayer.h"
#include "gpio-stm32h7.h"
#include "regs/cortex-m/cm_debug.h"

#define SDRAM_SIZE          (MEMORYSIZE * 1024 * 1024)

/* Address where Rockbox .elf binary will be cached in RAM */
#define LOAD_SIZE           (2 * 1024 * 1024)
#define LOAD_BUFFER_ADDR    (STM32_SDRAM1_BASE + SDRAM_SIZE - LOAD_SIZE)

/* Values at GDB_MAGICx */
#define GDB_MAGICVAL1   0x726f636b
#define GDB_MAGICVAL2   0x424f4f54
#define GDB_MAGICVAL3   0x6764626c
#define GDB_MAGICVAL4   0x6f616455

/* Addresses used by GDB boot protocol */
#define GDB_MAGIC1      (*(volatile uint32_t*)(STM32_SRAM4_BASE + 0x00))
#define GDB_MAGIC2      (*(volatile uint32_t*)(STM32_SRAM4_BASE + 0x04))
#define GDB_MAGIC3      (*(volatile uint32_t*)(STM32_SRAM4_BASE + 0x08))
#define GDB_MAGIC4      (*(volatile uint32_t*)(STM32_SRAM4_BASE + 0x0c))
#define GDB_ELFADDR     (*(volatile uint32_t*)(STM32_SRAM4_BASE + 0x10))
#define GDB_ELFSIZE     (*(volatile uint32_t*)(STM32_SRAM4_BASE + 0x14))

/* Events for the monitor callback to signal the main thread */
#define EV_POWER_PRESSED    MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0)
#define EV_POWER_RELEASED   MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 1)
#define EV_USB_UNPLUGGED    MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 2)

/* Long press duration for power button */
#define POWERBUTTON_LONG_PRESS_TIME     (HZ)

/* Time to remain powered with no USB cable inserted */
#define USB_UNPLUGGED_ACTIVE_TIME       (30 * HZ)
#define USB_UNPLUGGED_INACTIVE_TIME     (3 * HZ)

static const struct elf_memory_map rb_elf_mmap[] = {
    {
        .addr  = STM32_ITCM_BASE,
        .size  = STM32_ITCM_SIZE,
        .flags = PF_R | PF_X,
    },
    {
        .addr  = STM32_DTCM_BASE,
        .size  = STM32_DTCM_SIZE,
        .flags = PF_R | PF_W,
    },
    {
        .addr  = STM32_SDRAM1_BASE,
        .size  = SDRAM_SIZE - LOAD_SIZE,
        .flags = PF_R | PF_W | PF_X,
    },
};

static const struct elf_load_context rb_elf_ctx = {
    .mmap       = rb_elf_mmap,
    .num_mmap   = ARRAYLEN(rb_elf_mmap),
};

/* Power button monitor state */
static bool pwr_curr_state;
static bool pwr_prev_state;
struct timeout pwr_stable_tmo;

/* USB monitor state */
static bool usb_curr_state;
static bool usb_prev_state;
struct timeout usb_unplugged_tmo;

static volatile bool restart_pwr_stable_tmo;
static volatile bool restart_usb_unplugged_tmo;

/*
 * Because power is always enabled while USB is plugged in the
 * bootloader decides whether to appear "active" or "inactive"
 * to the user.
 */
static bool is_active;

/*
 * This flag is set if the bootloader is entered after software
 * poweroff. The user may still be holding the power button and
 * we don't want to boot Rockbox because of this, so we have to
 * wait for the power button to be released first.
 */
static bool wait_for_power_released;

/* Optional error message displayed on LCD */
static const char *status_msg = NULL;

/* Location of Rockbox ELF binary in memory */
static void *elf_load_addr = NULL;
static size_t elf_load_size = 0;

/* Helper functions */
static bool is_power_button_pressed(void)
{
    return button_status() & BUTTON_POWER;
}

static bool is_usbmode_button_pressed(void)
{
    return button_status() & BUTTON_DOWN;
}

static bool is_sysdebug_button_pressed(void)
{
    const int mask = BUTTON_A | BUTTON_B;

    /* Both buttons must be held */
    return (button_status() & mask) == mask;
}

static int send_event_on_tmo(struct timeout *tmo)
{
    button_queue_post(tmo->data, 0);
    return 0;
}

/*
 * Monitors the state of the power button and USB cable
 * insertion status. It will post an event to the main
 * thread when (a) the power button is continously held
 * or released for long enough, or (b) the USB cable is
 * unplugged for long enough.
 */
static void monitor_tick(void)
{
    /* Power button state */
    pwr_prev_state = pwr_curr_state;
    pwr_curr_state = is_power_button_pressed();

    if (pwr_curr_state != pwr_prev_state || restart_pwr_stable_tmo)
    {
        long event = pwr_curr_state ? EV_POWER_PRESSED : EV_POWER_RELEASED;
        int ticks = POWERBUTTON_LONG_PRESS_TIME;

        restart_pwr_stable_tmo = false;
        timeout_register(&pwr_stable_tmo, send_event_on_tmo, ticks, event);
    }

    /* USB cable state */
    usb_prev_state = usb_curr_state;
    usb_curr_state = usb_inserted();

    /* Ignore cable state change in inactive state */
    if (usb_curr_state != usb_prev_state || restart_usb_unplugged_tmo)
    {
        long event = EV_USB_UNPLUGGED;
        int ticks = is_active ? USB_UNPLUGGED_ACTIVE_TIME : USB_UNPLUGGED_INACTIVE_TIME;

        restart_usb_unplugged_tmo = false;

        if (usb_curr_state)
            timeout_cancel(&usb_unplugged_tmo);
        else
            timeout_register(&usb_unplugged_tmo, send_event_on_tmo, ticks, event);
    }
}

static void monitor_init(void)
{
    pwr_curr_state = is_power_button_pressed();
    usb_curr_state = usb_inserted();

    /* Make sure events fire even if inputs don't change after boot */
    restart_pwr_stable_tmo = true;
    restart_usb_unplugged_tmo = true;

    tick_add_task(monitor_tick);
}

static void go_active(void)
{
    is_active = true;
    restart_usb_unplugged_tmo = true;

    gpio_set_level(GPIO_CPU_POWER_ON, 1);
    storage_enable(true);
    disk_mount_all();
}

static void go_inactive(void)
{
    is_active = false;
    restart_usb_unplugged_tmo = true;

    storage_enable(false);
    gpio_set_level(GPIO_CPU_POWER_ON, 0);
}

static void refresh_display(void)
{
    if (!is_active)
    {
        lcd_shutdown();
        return;
    }

    int y = 0;

    lcd_clear_display();

    lcd_putsf(0, y++, "Rockbox on %s", MODEL_NAME);
    y++;

    if (status_msg)
    {
        lcd_putsf(0, y++, "Error: %s", status_msg);
        y++;
    }

    if (!usb_inserted())
    {
        lcd_putsf(0, y++, "Hold POWER to power off");
        lcd_putsf(0, y++, "Connect USB cable for USB mode");
        y++;
    }
    else
    {
        lcd_putsf(0, y++, "Bootloader USB mode");

        if (charging_state())
        {
            lcd_putsf(0, y++, "Battery charging (%d mA)",
                      usb_charging_maxcurrent());
        }
        else
        {
            lcd_putsf(0, y++, "Battery charged");
        }

        y++;
    }

    lcd_putsf(0, y++, "Version: %s", RBVERSION);

    lcd_update();
    lcd_enable(true);
}

static bool load_rockbox(void)
{
    int fd = open(BOOTDIR "/" BOOTFILE, O_RDONLY);
    if (fd < 0)
    {
        status_msg = "Rockbox not found";
        return false;
    }

    void *tmp_buf = (void *)LOAD_BUFFER_ADDR;
    size_t tmp_size = LOAD_SIZE;

    ssize_t ret = read(fd, tmp_buf, tmp_size);
    if (ret < 0)
    {
        status_msg = "I/O error loading Rockbox";
        goto out;
    }

    if ((size_t)ret == tmp_size)
    {
        status_msg = "Rockbox binary too large";
        goto out;
    }

    elf_load_addr = tmp_buf;
    elf_load_size = tmp_size;

out:
    close(fd);
    return elf_load_addr != NULL;
}

static void launch_elf(void)
{
    void *entrypoint = NULL;
    void (*entry_fn) (void) = NULL;

    if (elf_load_addr == NULL || elf_load_size == 0)
        return;

    int err = elf_loadmem(elf_load_addr, elf_load_size, &rb_elf_ctx, &entrypoint);
    if (err)
    {
        status_msg = "Failed to execute Rockbox";
        return;
    }

    disk_unmount_all();
    storage_enable(false);
    lcd_shutdown();

    entry_fn = entrypoint;
    commit_discard_idcache();
    disable_irq();
    stm32_systick_disable();

    entry_fn();
}

static void launch(void)
{
    /* No-op if USB mode was requested */
    if (is_usbmode_button_pressed())
        return;

    load_rockbox();
    launch_elf();
}

static bool handle_gdb_boot(void)
{
    /* Look for magic values that signal the GDB boot protocol */
    bool gdb_boot = (GDB_MAGIC1 == GDB_MAGICVAL1 &&
                     GDB_MAGIC2 == GDB_MAGICVAL2 &&
                     GDB_MAGIC3 == GDB_MAGICVAL3 &&
                     GDB_MAGIC4 == GDB_MAGICVAL4);

    /* Clear them so they won't hang around on a system reset */
    GDB_MAGIC1 = 0;
    GDB_MAGIC2 = 0;
    GDB_MAGIC3 = 0;
    GDB_MAGIC4 = 0;

    if (!gdb_boot)
        return false;

    /* "Call" GDB by entering breakpoint */
    GDB_ELFADDR = 0;
    GDB_ELFSIZE = 0;
    asm volatile("bkpt");

    /* Read location of the loaded binary */
    elf_load_addr = (void *)GDB_ELFADDR;
    elf_load_size = (size_t)GDB_ELFSIZE;

    return true;
}

void main(void)
{
    system_init();
    kernel_init();
    power_init();
    button_init();

    /* Enable system debugging if button combo held or debugger attached */
    if (reg_readf(CM_DEBUG_DHCSR, C_DEBUGEN) ||
        is_sysdebug_button_pressed())
    {
        system_debug_enable(true);
    }

    /* Start monitoring power button / usb state */
    monitor_init();

    /* Prepare LCD in case we need to display something */
    lcd_init();
    backlight_init();

    /*
     * Prepare storage subsystem, but keep SD card unpowered
     * until we actually need to access it.
     */
    storage_init();
    storage_enable(false);

    /*
     * Initialize fs/disk internal state, the disk will not
     * be mountable due to being disabled but this will not
     * cause any fatal errors.
     */
    filesystem_init();
    disk_mount_all();

    /* GDB assisted boot takes precedence */
    if (handle_gdb_boot())
        launch_elf();

    if (echoplayer_boot_reason == ECHOPLAYER_BOOT_REASON_SW_REBOOT ||
        usb_detect() == USB_INSERTED)
    {
        /*
         * For software reboot we want to immediately launch Rockbox.
         *
         * We also do so if USB is plugged in at boot, because there's
         * no way to dynamically switch between charge only and mass
         * storage mode depending on the active/inactive state; to avoid
         * confusion, it is simpler to just boot Rockbox.
         */
        go_active();
        launch();
    }
    else if (echoplayer_boot_reason == ECHOPLAYER_BOOT_REASON_SW_POWEROFF)
    {
        /* Ignore power button pressed event until button is first released */
        wait_for_power_released = true;
    }

    /*
     * Initialize USB so we can enumerate with the host and
     * negotiate charge current (needed even in inactive mode)
     */
    usb_init();
    usb_start_monitoring();
    usb_charging_enable(USB_CHARGING_FORCE);

    for (;;)
    {
        refresh_display();

        long refresh_tmo = is_active ? HZ : TIMEOUT_BLOCK;

        switch (button_get_w_tmo(refresh_tmo))
        {
        case EV_POWER_PRESSED:
            if (wait_for_power_released)
                break;

            if (!is_active)
            {
                /* Launch Rockbox due to user pressing power button */
                go_active();
                launch();
            }
            else
            {
                /*
                 * NOTE: The check for USB insertion here is only
                 * because we can't change to charging only mode.
                 */
                if (!usb_inserted())
                    go_inactive();
            }

            break;

        case EV_POWER_RELEASED:
            /*
             * This cuts power if the power button is not
             * held and we're not in the active state due
             * to USB mode, etc.
             */
            wait_for_power_released = false;
            gpio_set_level(GPIO_CPU_POWER_ON, is_active);
            break;

        case EV_USB_UNPLUGGED:
            go_inactive();
            break;

        case SYS_USB_CONNECTED:
            go_active();
            usb_acknowledge(SYS_USB_CONNECTED_ACK, button_get_data());
            break;
        }
    }
}
