#ifdef MESHTASTIC_INCLUDE_INKHUD

#include "gps/RTC.h"

#include "GeoCoord.h"
#include "NodeDB.h"

#include "./NodeListApplet.h"

using namespace NicheGraphics;

InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name)
{
    // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule
    // For all other packets, we manually act as if isPromiscuous=false, in wantPacket
    MeshModule::isPromiscuous = true;
}

// Do we want to process this packet with handleReceived()?
bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p)
{
    // Only interested if:
    return isActive()                                                  // Applet is active
           && !isFromUs(p)                                             // Packet is incoming (not outgoing)
           && (isToUs(p) || isBroadcast(p->to) ||                      // Either: intended for us,
               p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo

    // To match the behavior seen in the client apps:
    // - NodeInfoModule's ProtoBufModule base is "promiscuous"
    // - All other activity is *not* promiscuous

    // To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here,
    // to match the code in MeshModule::callModules
}

// MeshModule packets arrive here
// Extract the info and pass it to the derived applet
// Derived applet will store the CardInfo, and perform any required sorting of the CardInfo collection
// Derived applet might also need to keep other tallies (active nodes count?)
ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
    // Abort if applet fully deactivated
    // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return
    if (!isActive())
        return ProcessMessage::CONTINUE;

    // Assemble info: from this event
    CardInfo c;
    c.nodeNum = mp.from;
    c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);

    // Assemble info: from nodeDB (needed to detect changes)
    const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
    const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
    if (node) {
        if (node->has_hops_away)
            c.hopsAway = node->hops_away;

        if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
            meshtastic_PositionLite ourPos;
            meshtastic_PositionLite theirPos;
            if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
                float ourLat = ourPos.latitude_i * 1e-7;
                float ourLong = ourPos.longitude_i * 1e-7;
                float theirLat = theirPos.latitude_i * 1e-7;
                float theirLong = theirPos.longitude_i * 1e-7;

                c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
            }
        }
    }

    // Pass to the derived applet
    // Derived applet is responsible for requesting update, if justified
    // That request will eventually trigger our class' onRender method
    handleParsed(c);

    return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}

// Calculate maximum number of cards we may ever need to render, in our tallest layout config
// Number might be slightly in excess of the true value: applet header text not accounted for
uint8_t InkHUD::NodeListApplet::maxCards()
{
    // Cache result. Shouldn't change during execution
    static uint8_t maxCardCount = 0;

    if (!maxCardCount) {
        const uint16_t height = Tile::maxDisplayDimension();

        // Use a loop instead of arithmetic, because it's easier for my brain to follow
        // Add cards one by one, until the latest card extends below screen

        uint16_t y = cardH; // First card: no margin above
        maxCardCount = 1;

        while (y < height) {
            y += cardMarginH;
            y += cardH;
            maxCardCount++;
        }
    }

    return maxCardCount;
}

// Draw, using info which derived applet placed into NodeListApplet::cards for us
void InkHUD::NodeListApplet::onRender(bool full)
{

    // ================================
    // Draw the standard applet header
    // ================================

    drawHeader(getHeaderText()); // Ask derived applet for the title

    // Dimensions of the header
    int16_t headerDivY = getHeaderHeight() - 1;
    constexpr uint16_t padDivH = 2;

    // ========================
    // Draw the main node list
    // ========================

    // Leave a small gutter between long-name text and right-side card content
    constexpr uint8_t rightContentGap = 2;

    // Truncate with trailing "...", sized using the current font.
    auto ellipsizeToWidth = [this](std::string text, uint16_t maxWidth) {
        constexpr const char *ellipsis = "...";
        const uint16_t ellipsisW = getTextWidth(ellipsis);
        uint16_t textW = getTextWidth(text);
        if (maxWidth == 0)
            return std::string();
        if (textW <= maxWidth)
            return text;
        if (ellipsisW > maxWidth)
            return std::string();
        while (!text.empty() && (textW + ellipsisW > maxWidth)) {
            text.pop_back();
            textW = getTextWidth(text);
        }
        return text + ellipsis;
    };

    // Y value (top) of the current card. Increases as we draw.
    uint16_t cardTopY = headerDivY + padDivH;

    // Clean up deleted nodes before drawing
    cards.erase(
        std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }),
        cards.end());

    // -- Each node in list --
    for (auto card = cards.begin(); card != cards.end(); ++card) {

        // Gather info
        // ========================================
        const NodeNum &nodeNum = card->nodeNum;
        SignalStrength &signal = card->signal;
        std::string longName;  // handled below
        std::string shortName; // handled below
        std::string distance;  // handled below
        const uint8_t &hopsAway = card->hopsAway;

        meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);

        // Skip deleted nodes
        if (!node) {
            continue;
        }

        // -- Shortname --
        // Parse special chars in the short name
        // Use "?" if unknown
        if (node)
            shortName = parseShortName(node);
        else
            shortName = "?";

        // -- Longname --
        // Parse special chars in long name
        // Use node id if unknown
        if (nodeInfoLiteHasUser(node))
            longName = parse(node->long_name); // Found in nodeDB
        else {
            // Not found in nodeDB, show a hex nodeid instead
            longName = hexifyNodeNum(nodeNum);
        }

        // -- Distance --
        if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN)
            distance = localizeDistance(card->distanceMeters);

        // Draw the info
        // ====================================

        // Define two lines of text for the card
        // We will center our text on these lines
        uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2);
        uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2);

        // Print the short name
        setFont(fontMedium);
        printAt(0, lineAY, shortName, LEFT, MIDDLE);

        // Right-side labels and long name are rendered in small font.
        setFont(fontSmall);
        uint16_t rightContentW = 0;

        // Bottom row right: distance.
        if (!distance.empty()) {
            rightContentW = std::max(rightContentW, getTextWidth(distance));
            printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
        }

        // Top row right: direct-link signal only.
        if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
            uint16_t signalW = getTextWidth("Xkm"); // Indicator width tuned to a short right-side label
            uint16_t signalH = fontMedium.lineHeight() * 0.75;
            int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75);
            int16_t signalX = width() - signalW;
            rightContentW = std::max(rightContentW, signalW);
            drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
        } else if (hopsAway != CardInfo::HOPS_UNKNOWN) {
            std::string hopString = to_string(hopsAway) + (hopsAway == 1 ? " Hop" : " Hops");
            rightContentW = std::max(rightContentW, getTextWidth(hopString));
            printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
        }

        // Give long names as much room as possible while still avoiding right side signal and hop space
        const uint16_t longNameMaxW =
            (rightContentW + rightContentGap < width()) ? (width() - rightContentW - rightContentGap) : 0;
        const std::string longNameShown = ellipsizeToWidth(longName, longNameMaxW);

        // Safety crop
        setCrop(0, cardTopY, longNameMaxW, cardH);
        printAt(0, lineBY, longNameShown, LEFT, MIDDLE);

        resetCrop();

        // Draw separator between cards
        const int16_t separatorY = cardTopY + cardH - 1;
        if (separatorY < height() - 1 && (card + 1) != cards.end()) {
            for (int16_t xSep = 0; xSep < width(); xSep += 2)
                drawPixel(xSep, separatorY, BLACK);
        }

        // Prepare to draw the next card
        cardTopY += cardH;

        // Once we've run out of screen, stop drawing cards
        // Depending on tiles / rotation, this may be before we hit maxCards
        if (cardTopY > height())
            break;
    }
}

// Draw element: a "mobile phone" style signal indicator
// We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc
// This prevents issues with premature rounding when rendering tiny elements
void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength)
{

    /*
    +-------------------------------------------+
    |                                           |
    |                                           |
    |                                  barHeightRelative=1.0
    |                                  +--+ ^   |
    |        gutterW          +--+     |  | |   |
    |          <-->  +--+     |  |     |  | |   |
    |     +--+       |  |     |  |     |  | |   |
    |     |  |       |  |     |  |     |  | |   |
    | <-> +--+       +--+     +--+     +--+ v   |
    | paddingW             ^                    |
    |             paddingH |                    |
    |                      v                    |
    +-------------------------------------------+
    */

    constexpr float paddingW = 0.1; // Either side
    constexpr float paddingH = 0.1; // Above and below
    constexpr float gutterW = 0.1;  // Between bars

    constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest
    constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.

    // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
    float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount;
    float barHMax = 1.0 - (paddingH + paddingH);

    // Draw signal bar rectangles, then placeholder lines once strength reached
    for (uint8_t i = 0; i < barCount; i++) {
        // Coords for this specific bar
        float barH = barHMax * barHRel[i];
        float barX = paddingW + (i * (gutterW + barW));
        float barY = paddingH + (barHMax - barH);

        // Rasterize to px coords at the last moment
        int16_t rX = (x + (w * barX)) + 0.5;
        int16_t rY = (y + (h * barY)) + 0.5;
        uint16_t rW = (w * barW) + 0.5;
        uint16_t rH = (h * barH) + 0.5;

        // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
        if (i <= strength)
            drawRect(rX, rY, rW, rH, BLACK);
        else {
            // Just draw a placeholder line
            float lineY = barY + barH;
            uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
            drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
        }
    }
}

#endif
