#ifdef MESHTASTIC_INCLUDE_INKHUD

#include "./InkHUD.h"

#include "./Applet.h"
#include "./Events.h"
#include "./Persistence.h"
#include "./Renderer.h"
#include "./SystemApplet.h"
#include "./Tile.h"
#include "./WindowManager.h"
#include "FSCommon.h"
#include "MessageStore.h"
#include "SPILock.h"
#include "concurrency/LockGuard.h"

using namespace NicheGraphics;

// Get or create the singleton
InkHUD::InkHUD *InkHUD::InkHUD::getInstance()
{
    // Create the singleton instance of our class, if not yet done
    static InkHUD *instance = nullptr;
    if (!instance) {
        instance = new InkHUD;

        instance->persistence = new Persistence;
        instance->windowManager = new WindowManager;
        instance->renderer = new Renderer;
        instance->events = new Events;
    }

    return instance;
}

// Connect the (fully set-up) E-Ink driver to InkHUD
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called
void InkHUD::InkHUD::setDriver(Drivers::EInk *driver)
{
    renderer->setDriver(driver);
}

// Set the target number of FAST display updates in a row, before a FULL update is used for display health
// This value applies only to updates with an UNSPECIFIED update type
// If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many
// subsequent FULL updates will be performed, in an attempt to restore the display's health
void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
{
    renderer->setDisplayResilience(fastPerFull, stressMultiplier);
}

// Register a user applet with InkHUD
// A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method
// Passing an applet to this method is all that is required to make it available to the user in your InkHUD build
void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile)
{
    windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
}

void InkHUD::InkHUD::notifyApplyingChanges()
{
    if (events) {
        events->applyingChanges();
    }
}

// One-time migration from the old per-channel InkHUD message files (/NicheGraphics/ch*.msgs)
// to the firmware-wide MessageStore format (/Messages_default.msgs).
// Only runs when the new store loaded empty, meaning this is the first boot after the format change.
// Old files are deleted once migrated.
static void migrateOldInkHUDMessages()
{
#ifdef FSCom
    bool migrated = false;
    constexpr uint8_t MAX_CHANNELS = 8;
    constexpr uint32_t OLD_MAX_MSG_SIZE = 250;

    for (uint8_t ch = 0; ch < MAX_CHANNELS; ch++) {
        std::string path = "/NicheGraphics/ch";
        path += std::to_string(ch);
        path += ".msgs";

        spiLock->lock();
        bool exists = FSCom.exists(path.c_str());
        spiLock->unlock();
        if (!exists)
            continue;

        concurrency::LockGuard guard(spiLock);

        auto f = FSCom.open(path.c_str(), FILE_O_READ);
        if (!f || f.size() == 0) {
            if (f)
                f.close();
            FSCom.remove(path.c_str());
            continue;
        }

        uint8_t count = 0;
        f.readBytes(reinterpret_cast<char *>(&count), 1);

        std::vector<StoredMessage> channelMsgs;
        for (uint8_t i = 0; i < count; i++) {
            StoredMessage sm;
            f.readBytes(reinterpret_cast<char *>(&sm.timestamp), sizeof(sm.timestamp));
            f.readBytes(reinterpret_cast<char *>(&sm.sender), sizeof(sm.sender));
            f.readBytes(reinterpret_cast<char *>(&sm.channelIndex), sizeof(sm.channelIndex));

            char textBuf[OLD_MAX_MSG_SIZE + 1] = {};
            uint32_t textLen = 0;
            char c;
            while (textLen < OLD_MAX_MSG_SIZE) {
                if (f.readBytes(&c, 1) != 1)
                    break;
                if (c == '\0')
                    break;
                textBuf[textLen++] = c;
            }

            sm.dest = NODENUM_BROADCAST;
            sm.type = MessageType::BROADCAST;
            sm.isBootRelative = false;
            sm.ackStatus = AckStatus::ACKED;
            size_t storedLen = (textLen >= MAX_MESSAGE_SIZE) ? MAX_MESSAGE_SIZE - 1 : textLen;
            sm.textOffset = MessageStore::storeText(textBuf, storedLen);
            sm.textLength = static_cast<uint16_t>(storedLen);

            channelMsgs.push_back(sm);
        }

        // Old format stored newest-first (push_front); insert oldest-first for correct chronological order
        for (int i = static_cast<int>(channelMsgs.size()) - 1; i >= 0; i--)
            messageStore.addLiveMessage(channelMsgs[i]);
        if (!channelMsgs.empty())
            migrated = true;

        f.close();
        FSCom.remove(path.c_str());
        LOG_INFO("Migrated %u messages from %s", static_cast<uint32_t>(count), path.c_str());
    }

    // Delete the old latest.msgs; the latestMessage cache will be re-derived from migrated channel messages
    spiLock->lock();
    if (FSCom.exists("/NicheGraphics/latest.msgs"))
        FSCom.remove("/NicheGraphics/latest.msgs");
    spiLock->unlock();

    if (migrated) {
        LOG_INFO("InkHUD message migration complete, saving to new format");
        messageStore.saveToFlash();
    }
#endif
}

// Start InkHUD!
// Call this only after you have configured InkHUD
void InkHUD::InkHUD::begin()
{
    persistence->loadSettings();
    messageStore.loadFromFlash(); // Load persisted messages before deriving latestMessage cache
    if (messageStore.getLiveMessages().empty())
        migrateOldInkHUDMessages(); // First boot after format change: import old per-channel files
    persistence->loadLatestMessage();

    windowManager->begin();
    events->begin();
    renderer->begin();
    // LogoApplet shows boot screen here
}

void InkHUD::InkHUD::setTouchEnabledProvider(TouchEnabledProvider provider)
{
    touchEnabledProvider = provider;
}

bool InkHUD::InkHUD::hasTouchEnabledProvider() const
{
    return touchEnabledProvider != nullptr;
}

bool InkHUD::InkHUD::isTouchEnabled() const
{
    if (!touchEnabledProvider)
        return true;

    return touchEnabledProvider();
}

// Call this when your user button gets a short press
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
void InkHUD::InkHUD::shortpress()
{
    events->onButtonShort();
}

// Call this when your user button gets a long press
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
void InkHUD::InkHUD::longpress()
{
    events->onButtonLong();
}

// Call this when your exit button gets a short press
void InkHUD::InkHUD::exitShort()
{
    events->onExitShort();
}

// Call this when your exit button gets a long press
void InkHUD::InkHUD::exitLong()
{
    events->onExitLong();
}

// Call this when your joystick gets an up input
void InkHUD::InkHUD::navUp()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onNavLeft();
        break;
    case 2: // 180 deg
        events->onNavDown();
        break;
    case 3: // 270 deg
        events->onNavRight();
        break;
    default: // 0 deg
        events->onNavUp();
        break;
    }
}

// Call this when your joystick gets a down input
void InkHUD::InkHUD::navDown()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onNavRight();
        break;
    case 2: // 180 deg
        events->onNavUp();
        break;
    case 3: // 270 deg
        events->onNavLeft();
        break;
    default: // 0 deg
        events->onNavDown();
        break;
    }
}

// Call this when your joystick gets a left input
void InkHUD::InkHUD::navLeft()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onNavDown();
        break;
    case 2: // 180 deg
        events->onNavRight();
        break;
    case 3: // 270 deg
        events->onNavUp();
        break;
    default: // 0 deg
        events->onNavLeft();
        break;
    }
}

// Call this when your joystick gets a right input
void InkHUD::InkHUD::navRight()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onNavUp();
        break;
    case 2: // 180 deg
        events->onNavLeft();
        break;
    case 3: // 270 deg
        events->onNavDown();
        break;
    default: // 0 deg
        events->onNavRight();
        break;
    }
}

// Call this when touch input needs joystick-like up navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavUp()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onTouchNavLeft();
        break;
    case 2: // 180 deg
        events->onTouchNavDown();
        break;
    case 3: // 270 deg
        events->onTouchNavRight();
        break;
    default: // 0 deg
        events->onTouchNavUp();
        break;
    }
}

// Call this when touch input needs joystick-like down navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavDown()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onTouchNavRight();
        break;
    case 2: // 180 deg
        events->onTouchNavUp();
        break;
    case 3: // 270 deg
        events->onTouchNavLeft();
        break;
    default: // 0 deg
        events->onTouchNavDown();
        break;
    }
}

// Call this when touch input needs joystick-like left navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavLeft()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onTouchNavDown();
        break;
    case 2: // 180 deg
        events->onTouchNavRight();
        break;
    case 3: // 270 deg
        events->onTouchNavUp();
        break;
    default: // 0 deg
        events->onTouchNavLeft();
        break;
    }
}

// Call this when touch input needs joystick-like right navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavRight()
{
    switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
    case 1: // 90 deg
        events->onTouchNavUp();
        break;
    case 2: // 180 deg
        events->onTouchNavLeft();
        break;
    case 3: // 270 deg
        events->onTouchNavDown();
        break;
    default: // 0 deg
        events->onTouchNavRight();
        break;
    }
}

void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y)
{
    events->onTouchTap(x, y, false);
}

void InkHUD::InkHUD::touchLongPress(uint16_t x, uint16_t y)
{
    events->onTouchTap(x, y, true);
}

// Call this for keyboard input
// The Keyboard Applet also calls this
void InkHUD::InkHUD::freeText(char c)
{
    events->onFreeText(c);
}

// Call this to complete a freetext input
void InkHUD::InkHUD::freeTextDone()
{
    events->onFreeTextDone();
}

// Call this to cancel a freetext input
void InkHUD::InkHUD::freeTextCancel()
{
    events->onFreeTextCancel();
}

// Cycle the next user applet to the foreground
// Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
void InkHUD::InkHUD::nextApplet()
{
    windowManager->nextApplet();
}

// Cycle the previous user applet to the foreground
// Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
void InkHUD::InkHUD::prevApplet()
{
    windowManager->prevApplet();
}

// Returns the currently active applet
InkHUD::Applet *InkHUD::InkHUD::getActiveApplet()
{
    return windowManager->getActiveApplet();
}

// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
{
    windowManager->openMenu();
}

// Show touch-friendly app switcher (on the focused tile)
void InkHUD::InkHUD::openAppSwitcher()
{
    windowManager->openAppSwitcher();
}

// Bring AlignStick applet to the foreground
void InkHUD::InkHUD::openAlignStick()
{
    windowManager->openAlignStick();
}

// Open the on-screen keyboard
void InkHUD::InkHUD::openKeyboard()
{
    windowManager->openKeyboard();
}

// Close the on-screen keyboard
void InkHUD::InkHUD::closeKeyboard()
{
    windowManager->closeKeyboard();
}

// In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::nextTile()
{
    windowManager->nextTile();
}

// In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::prevTile()
{
    windowManager->prevTile();
}

bool InkHUD::InkHUD::showApplet(uint8_t appletIndex)
{
    return windowManager->showApplet(appletIndex);
}

bool InkHUD::InkHUD::selectTileAt(uint16_t x, uint16_t y)
{
    return windowManager->selectTileAt(x, y);
}

// Rotate the display image by 90 degrees
void InkHUD::InkHUD::rotate()
{
    windowManager->rotate();
}

// rotate the joystick in 90 degree increments
void InkHUD::InkHUD::rotateJoystick(uint8_t angle)
{
    persistence->settings.joystick.alignment += angle;
    persistence->settings.joystick.alignment %= 4;
}

// Show / hide the battery indicator in top-right
void InkHUD::InkHUD::toggleBatteryIcon()
{
    windowManager->toggleBatteryIcon();
}

// An applet asking for the display to be updated
// This does not occur immediately
// Instead, rendering is scheduled ASAP, for the next Renderer::runOnce call
// This allows multiple applets to observe the same event, and then share the same opportunity to update
// Applets should requestUpdate, whether or not they are currently displayed ("foreground")
// This is because they *might* be automatically brought to foreground by WindowManager::autoshow
void InkHUD::InkHUD::requestUpdate()
{
    renderer->requestUpdate();
}

// Demand that the display be updated
// Ignores all diplomacy:
//  - the display *will* update
//  - the specified update type *will* be used
// If the all parameter is true, the whole screen buffer is cleared and re-rendered
// If the async parameter is false, code flow is blocked while the update takes place
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async)
{
    renderer->forceUpdate(type, all, async);
}

// Wait for any in-progress display update to complete before continuing
void InkHUD::InkHUD::awaitUpdate()
{
    renderer->awaitUpdate();
}

// Ask the window manager to potentially bring a different user applet to foreground
// An applet will be brought to foreground if it has just received new and relevant info
// For Example: AllMessagesApplet has just received a new text message
// Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis
// If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event
void InkHUD::InkHUD::autoshow()
{
    windowManager->autoshow();
}

// Tell the window manager that the Persistence::Settings value for applet activation has changed,
// and that it should reconfigure accordingly.
// This is triggered at boot, or when the user enables / disabled applets via the on-screen menu
void InkHUD::InkHUD::updateAppletSelection()
{
    windowManager->changeActivatedApplets();
}

// Tell the window manager that the Persistence::Settings value for layout or rotation has changed,
// and that it should reconfigure accordingly.
// This is triggered at boot, or by rotate / layout options in the on-screen menu
void InkHUD::InkHUD::updateLayout()
{
    windowManager->changeLayout();
}

// Width of the display, in the context of the current rotation
uint16_t InkHUD::InkHUD::width()
{
    return renderer->width();
}

// Height of the display, in the context of the current rotation
uint16_t InkHUD::InkHUD::height()
{
    return renderer->height();
}

// A collection of any user tiles which do not have a valid user applet
// This can occur in various situations, such as when a user enables fewer applets than their layout has tiles
// The tiles (and which regions the occupy) are private information of the window manager
// The renderer needs to know which regions (if any) are empty,
// in order to fill them with a "placeholder" pattern.
// -- There may be a tidier way to accomplish this --
std::vector<InkHUD::Tile *> InkHUD::InkHUD::getEmptyTiles()
{
    return windowManager->getEmptyTiles();
}

// Get a system applet by its name
// This isn't particularly elegant, but it does avoid:
// - passing around a big set of references
// - having two sets of references (systemApplet vector for iteration)
InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name)
{
    for (SystemApplet *sa : systemApplets) {
        if (strcmp(name, sa->name) == 0)
            return sa;
    }

    assert(false); // Invalid name
}

// Place a pixel into the image buffer
// The x and y coordinates are in the context of the current display rotation
// - Applets pass "relative" pixels to tiles
// - Tiles pass translated pixels to this method
// - this methods (Renderer) places rotated pixels into the image buffer
// This method provides the final formatting step required. The image buffer is suitable for writing to display
void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c)
{
    renderer->handlePixel(x, y, c);
}

#endif
