#ifdef MESHTASTIC_INCLUDE_INKHUD

#include "./MenuApplet.h"

#include "RTC.h"

#include "airtime.h"
#include "main.h"
#include "power.h"

#if !MESHTASTIC_EXCLUDE_GPS
#include "GPS.h"
#endif

using namespace NicheGraphics;

static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes

// Options for the "Recents" menu
// These are offered to users as possible values for settings.recentlyActiveSeconds
static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120};

InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
{
    // No timer tasks at boot
    OSThread::disable();

    // Note: don't get instance if we're not actually using the backlight,
    // or else you will unintentionally instantiate it
    if (settings->optionalMenuItems.backlight) {
        backlight = Drivers::LatchingBacklight::getInstance();
    }
}

void InkHUD::MenuApplet::onForeground()
{
    // We do need this before we render, but we can optimize by just calculating it once now
    systemInfoPanelHeight = getSystemInfoPanelHeight();

    // Display initial menu page
    showPage(MenuPage::ROOT);

    // If device has a backlight which isn't controlled by aux button:
    // backlight on always when menu opens.
    // Courtesy to T-Echo users who removed the capacitive touch button
    if (settings->optionalMenuItems.backlight) {
        assert(backlight);
        if (!backlight->isOn())
            backlight->peek();
    }

    // Prevent user applets requesting update while menu is open
    // Handle button input with this applet
    SystemApplet::lockRequests = true;
    SystemApplet::handleInput = true;

    // Begin the auto-close timeout
    OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
    OSThread::enabled = true;

    // Upgrade the refresh to FAST, for guaranteed responsiveness
    inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}

void InkHUD::MenuApplet::onBackground()
{
    // If device has a backlight which isn't controlled by aux button:
    // Item in options submenu allows keeping backlight on after menu is closed
    // If this item is deselected we will turn backlight off again, now that menu is closing
    if (settings->optionalMenuItems.backlight) {
        assert(backlight);
        if (!backlight->isLatched())
            backlight->off();
    }

    // Stop the auto-timeout
    OSThread::disable();

    // Resume normal rendering and button behavior of user applets
    SystemApplet::lockRequests = false;
    SystemApplet::handleInput = false;

    // Restore the user applet whose tile we borrowed
    if (borrowedTileOwner)
        borrowedTileOwner->bringToForeground();
    Tile *t = getTile();
    t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one)
    borrowedTileOwner = nullptr;

    // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
    // We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu
    inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}

// Open the menu
// Parameter specifies which user-tile the menu will use
// The user applet originally on this tile will be restored when the menu closes
void InkHUD::MenuApplet::show(Tile *t)
{
    // Remember who *really* owns this tile
    borrowedTileOwner = t->getAssignedApplet();

    // Hide the owner, if it is a valid applet
    if (borrowedTileOwner)
        borrowedTileOwner->sendToBackground();

    // Break the owner's link with tile
    // Relink it to menu applet
    t->assignApplet(this);

    // Show menu
    bringToForeground();
}

// Auto-exit the menu applet after a period of inactivity
// The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open.
// By exiting the menu, we prevent users mistakenly believing that the data will update.
int32_t InkHUD::MenuApplet::runOnce()
{
    // runOnce's interval is pushed back when a button is pressed
    // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC,
    // so we close the menu.
    showPage(EXIT);

    // Timer should disable after firing
    // This is redundant, as onBackground() will also disable
    return OSThread::disable();
}

// Perform action for a menu item, then change page
// Behaviors for MenuActions are defined here
void InkHUD::MenuApplet::execute(MenuItem item)
{
    // Perform an action
    // ------------------
    switch (item.action) {

    // Open a submenu without performing any action
    // Also handles exit
    case NO_ACTION:
        break;

    case NEXT_TILE:
        inkhud->nextTile();
        break;

    case ROTATE:
        inkhud->rotate();
        break;

    case LAYOUT:
        // Todo: smarter incrementing of tile count
        settings->userTiles.count++;

        if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet
            settings->userTiles.count++;

        if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high
            settings->userTiles.count = 1;

        inkhud->updateLayout();
        break;

    case TOGGLE_APPLET:
        settings->userApplets.active[cursor] = !settings->userApplets.active[cursor];
        inkhud->updateAppletSelection();
        break;

    case TOGGLE_AUTOSHOW_APPLET:
        // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage()
        *items.at(cursor).checkState = !(*items.at(cursor).checkState);
        break;

    case TOGGLE_NOTIFICATIONS:
        settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications;
        break;

    case SET_RECENTS:
        // Set value of settings.recentlyActiveSeconds
        // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file)
        assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]));
        settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes
        break;

    case SHUTDOWN:
        LOG_INFO("Shutting down from menu");
        power->shutdown();
        // Menu is then sent to background via onShutdown
        break;

    case TOGGLE_BATTERY_ICON:
        inkhud->toggleBatteryIcon();
        break;

    case TOGGLE_BACKLIGHT:
        // Note: backlight is already on in this situation
        // We're marking that it should *remain* on once menu closes
        assert(backlight);
        if (backlight->isLatched())
            backlight->off();
        else
            backlight->latch();
        break;

    case TOGGLE_12H_CLOCK:
        config.display.use_12h_clock = !config.display.use_12h_clock;
        nodeDB->saveToDisk(SEGMENT_CONFIG);
        break;

    case TOGGLE_GPS:
        gps->toggleGpsMode();
        nodeDB->saveToDisk(SEGMENT_CONFIG);
        break;

    case ENABLE_BLUETOOTH:
        // This helps users recover from a bad wifi config
        LOG_INFO("Enabling Bluetooth");
        config.network.wifi_enabled = false;
        config.bluetooth.enabled = true;
        nodeDB->saveToDisk();
        rebootAtMsec = millis() + 2000;
        break;

    default:
        LOG_WARN("Action not implemented");
    }

    // Move to next page, as defined for the MenuItem
    showPage(item.nextPage);
}

// Display a new page of MenuItems
// May reload same page, or exit menu applet entirely
// Fills the MenuApplet::items vector
void InkHUD::MenuApplet::showPage(MenuPage page)
{
    items.clear();

    switch (page) {
    case ROOT:
        // Optional: next applet
        if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
            items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown

        // items.push_back(MenuItem("Send", MenuPage::SEND)); // TODO
        items.push_back(MenuItem("Options", MenuPage::OPTIONS));
        // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO
        items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
        items.push_back(MenuItem("Exit", MenuPage::EXIT));
        break;

    case SEND:
        items.push_back(MenuItem("Send Message", MenuPage::EXIT));
        items.push_back(MenuItem("Send NodeInfo", MenuAction::SEND_NODEINFO));
        items.push_back(MenuItem("Send Position", MenuAction::SEND_POSITION));
        items.push_back(MenuItem("Exit", MenuPage::EXIT));
        break;

    case OPTIONS:
        // Optional: backlight
        if (settings->optionalMenuItems.backlight)
            items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
                                     MenuAction::TOGGLE_BACKLIGHT,                                   // Action
                                     MenuPage::EXIT                                                  // Exit once complete
                                     ));

        // Optional: GPS
        if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED)
            items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT));
        if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
            items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT));

        // Optional: Enable Bluetooth, in case of lost wifi connection
        if (!config.bluetooth.enabled || config.network.wifi_enabled)
            items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT));

        items.push_back(MenuItem("Applets", MenuPage::APPLETS));
        items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW));
        items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS));
        if (settings->userTiles.maxCount > 1)
            items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
        items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
        items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
                                 &settings->optionalFeatures.notifications));
        items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS,
                                 &settings->optionalFeatures.batteryIcon));
        items.push_back(
            MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock));
        items.push_back(MenuItem("Exit", MenuPage::EXIT));
        break;

    case APPLETS:
        populateAppletPage();
        items.push_back(MenuItem("Exit", MenuPage::EXIT));
        break;

    case AUTOSHOW:
        populateAutoshowPage();
        items.push_back(MenuItem("Exit", MenuPage::EXIT));
        break;

    case RECENTS:
        populateRecentsPage();
        break;

    case EXIT:
        sendToBackground(); // Menu applet dismissed, allow normal behavior to resume
        break;

    default:
        LOG_WARN("Page not implemented");
    }

    // Reset the cursor, unless reloading same page
    // (or now out-of-bounds)
    if (page != currentPage || cursor >= items.size()) {
        cursor = 0;

        // ROOT menu has special handling: unselected at first, to emphasise the system info panel
        if (page == ROOT)
            cursorShown = false;
    }

    // Remember which page we are on now
    currentPage = page;
}

void InkHUD::MenuApplet::onRender()
{
    if (items.size() == 0)
        LOG_ERROR("Empty Menu");

    // Dimensions for the slots where we will draw menuItems
    const float padding = 0.05;
    const uint16_t itemH = fontSmall.lineHeight() * 2;
    const int16_t itemW = width() - X(padding) - X(padding);
    const int16_t itemL = X(padding);
    const int16_t itemR = X(1 - padding);
    int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu.

    // How many full menuItems will fit on screen
    uint8_t slotCount = (height() - itemT) / itemH;

    // System info panel at the top of the menu
    // =========================================

    uint16_t &siH = systemInfoPanelHeight;                   // System info - height. Calculated at onForeground
    const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel

    // System info - top
    // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen.
    // This is the same behavior we expect from the non-root menus.
    // Implementing this with the systemp panel is slightly annoying though,
    // and required adding the MenuApplet::getSystemInfoPanelHeight method
    int16_t siT;
    if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count)
        siT = 0;
    else
        siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH);

    // If showing ROOT menu,
    // and the panel isn't yet scrolled off screen top
    if (currentPage == ROOT) {
        drawSystemInfoPanel(0, siT, width()); // Draw the panel.
        itemT = max(siT + siH, 0);            // Offset the first menu entry, so menu starts below the system info panel
    }

    // Draw menu items
    // ===================

    // Which item will be drawn to the top-most slot?
    // Initially, this is the item 0, but may increase once we begin scrolling
    uint8_t firstItem;
    if (cursor < slotCount)
        firstItem = 0;
    else
        firstItem = cursor - (slotCount - 1);

    // Which item will be drawn to the bottom-most slot?
    // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow
    // This may be less than the slot-count, if we are reaching the end of the menuItems
    uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1);

    // -- Loop: draw each (visible) menu item --
    for (uint8_t i = firstItem; i <= lastItem; i++) {
        // Grab the menuItem
        MenuItem item = items.at(i);

        // Center-line for the text
        int16_t center = itemT + (itemH / 2);

        if (cursorShown && i == cursor)
            drawRect(itemL, itemT, itemW, itemH, BLACK);
        printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE);

        // Testing only: circle instead of check box
        if (item.checkState) {
            const uint16_t cbWH = fontSmall.lineHeight();  // Checkbox: width / height
            const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
            const int16_t cbT = center - (cbWH / 2);       // Checkbox : top
            // Checkbox ticked
            if (*(item.checkState)) {
                drawRect(cbL, cbT, cbWH, cbWH, BLACK);
                // First point of tick: pen down
                const int16_t t1Y = center;
                const int16_t t1X = cbL + 3;
                // Second point of tick: base
                const int16_t t2Y = center + (cbWH / 2) - 2;
                const int16_t t2X = cbL + (cbWH / 2);
                // Third point of tick: end of tail
                const int16_t t3Y = center - (cbWH / 2) - 2;
                const int16_t t3X = cbL + cbWH + 2;
                // Draw twice: faux bold
                drawLine(t1X, t1Y, t2X, t2Y, BLACK);
                drawLine(t2X, t2Y, t3X, t3Y, BLACK);
                drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK);
                drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK);
            }
            // Checkbox ticked
            else
                drawRect(cbL, cbT, cbWH, cbWH, BLACK);
        }

        // Increment the y value (top) as we go
        itemT += itemH;
    }
}

void InkHUD::MenuApplet::onButtonShortPress()
{
    // Push the auto-close timer back
    OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);

    // Move menu cursor to next entry, then update
    if (cursorShown)
        cursor = (cursor + 1) % items.size();
    else
        cursorShown = true;
    requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}

void InkHUD::MenuApplet::onButtonLongPress()
{
    // Push the auto-close timer back
    OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);

    if (cursorShown)
        execute(items.at(cursor));
    else
        showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close

    // If we didn't already request a specialized update, when handling a menu action,
    // then perform the usual fast update.
    // FAST keeps things responsive: important because we're dealing with user input
    if (!wantsToRender())
        requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}

// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
void InkHUD::MenuApplet::populateAppletPage()
{
    assert(items.size() == 0);

    for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
        const char *name = inkhud->userApplets.at(i)->name;
        bool *isActive = &(settings->userApplets.active[i]);
        items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive));
    }
}

// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data
// We only populate this menu page with applets which are actually active
// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient.
void InkHUD::MenuApplet::populateAutoshowPage()
{
    assert(items.size() == 0);

    for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
        // Only add a menu item if applet is active
        if (settings->userApplets.active[i]) {
            const char *name = inkhud->userApplets.at(i)->name;
            bool *isActive = &(settings->userApplets.autoshow[i]);
            items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive));
        }
    }
}

void InkHUD::MenuApplet::populateRecentsPage()
{
    // How many values are shown for use to choose from
    constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]);

    // Create an entry for each item in RECENTS_OPTIONS_MINUTES array
    // (Defined at top of this file)
    for (uint8_t i = 0; i < optionCount; i++) {
        std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins";
        items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT));
    }
}

// Renders the panel shown at the top of the root menu.
// Displays the clock, and several other pieces of instantaneous system info,
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight)
{
    // Reset the height
    // We'll add to this as we add elements
    uint16_t height = 0;

    // Clock (potentially)
    // ====================
    std::string clockString = getTimeString();
    if (clockString.length() > 0) {
        setFont(fontLarge);
        printAt(width / 2, top, clockString, CENTER, TOP);

        height += fontLarge.lineHeight();
        height += fontLarge.lineHeight() * 0.1; // Padding below clock
    }

    // Stats
    // ===================

    setFont(fontSmall);

    // Position of the label row for the system info
    const int16_t labelT = top + height;
    height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing

    // Position of the data row for the system info
    const int16_t valT = top + height;
    height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider)

    // Position of divider between the info panel and the menu entries
    const int16_t divY = top + height;
    height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item)

    // Create a variable number of columns
    // Either 3 or 4, depending on whether we have GPS
    // Todo
    constexpr uint8_t N_COL = 3;
    int16_t colL[N_COL];
    int16_t colC[N_COL];
    int16_t colR[N_COL];
    for (uint8_t i = 0; i < N_COL; i++) {
        colL[i] = left + ((width / N_COL) * i);
        colC[i] = colL[i] + ((width / N_COL) / 2);
        colR[i] = colL[i] + (width / N_COL);
    }

    // Info blocks, left to right

    // Voltage
    float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
    char voltageStr[6]; // "XX.XV"
    sprintf(voltageStr, "%.1fV", voltage);
    printAt(colC[0], labelT, "Bat", CENTER, TOP);
    printAt(colC[0], valT, voltageStr, CENTER, TOP);

    // Divider
    for (int16_t y = valT; y <= divY; y += 3)
        drawPixel(colR[0], y, BLACK);

    // Channel Util
    char chUtilStr[4]; // "XX%"
    sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent());
    printAt(colC[1], labelT, "Ch", CENTER, TOP);
    printAt(colC[1], valT, chUtilStr, CENTER, TOP);

    // Divider
    for (int16_t y = valT; y <= divY; y += 3)
        drawPixel(colR[1], y, BLACK);

    // Duty Cycle (AirTimeTx)
    char dutyUtilStr[4]; // "XX%"
    sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent());
    printAt(colC[2], labelT, "Duty", CENTER, TOP);
    printAt(colC[2], valT, dutyUtilStr, CENTER, TOP);

    /*
    // Divider
    for (int16_t y = valT; y <= divY; y += 3)
        drawPixel(colR[2], y, BLACK);

    // GPS satellites - todo
    printAt(colC[3], labelT, "Sats", CENTER, TOP);
    printAt(colC[3], valT, "ToDo", CENTER, TOP);
    */

    // Horizontal divider, at bottom of system info panel
    for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item
        drawPixel(x, divY, BLACK);

    if (renderedHeight != nullptr)
        *renderedHeight = height;
}

// Get the height of the the panel drawn at the top of the menu
// This is inefficient, as we do actually have to render the panel to determine the height
// It solves a catch-22 situation, where slotCount needs to know panel height, and panel height needs to know slotCount
uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
{
    // Render *far* off screen
    uint16_t height = 0;
    drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height);

    return height;
}

#endif