/*
 * Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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 "update_gui.h"
#include "ult_ext.h"
#include "base_menu_gui.h"
#include "../format.h"
#include "../style.h"

#include <tesla.hpp>
#include <ultra.hpp>
#include <cstdio>
#include <string>


static constexpr u16 PANEL_H = 80;

class UpdateStatusPanel : public tsl::elm::Element {
public:
    void setState(int stage, int progress,
                  const std::string &pkg, const std::string &msg) {
        m_stage    = stage;
        m_progress = progress;
        m_pkg      = pkg;
        m_msg      = msg;
    }

    void draw(tsl::gfx::Renderer *renderer) override {
        if (this->getHeight() == 0) return;

        const s32 PAD = 20;
        const s32 x   = this->getX() + PAD;
        const s32 y   = this->getY();
        const s32 w   = this->getWidth() - PAD * 2;

        if (m_stage == 0) {
            return;
        }

        if (m_stage == 1 || m_stage == 2) {
            const char *opLabel = (m_stage == 1) ? "Downloading" : "Extracting";
            tsl::Color accent   = (m_stage == 1)
                ? tsl::Color{0x4, 0x8, 0xF, 0xF}   // blue
                : tsl::Color{0x3, 0xA, 0x8, 0xF};  // teal

            renderer->drawString(opLabel,       false, x,       y + 20, 15, accent);
            renderer->drawString(m_pkg.c_str(), false, x + 104, y + 20, 15,
                                 tsl::sectionTextColor);

            const s32 barY = y + 48;
            renderer->drawRect(x, barY, w, 7, tsl::Color{0x3, 0x3, 0x3, 0xF});

            int pct   = (m_progress < 0) ? 0 : (m_progress > 100 ? 100 : m_progress);
            s32 fillW = (s32)((long)w * pct / 100);
            if (fillW > 0)
                renderer->drawRect(x, barY, fillW, 7, accent);

            char buf[8];
            if (m_progress < 0)
                std::snprintf(buf, sizeof(buf), "...");
            else
                std::snprintf(buf, sizeof(buf), "%d%%", pct);

            u32 tw = renderer->getTextDimensions(buf, false, 13).first;
            renderer->drawString(buf, false,
                x + w - (s32)tw, barY - 14, 13, tsl::infoTextColor);

        } else {
            tsl::Color col;
            switch (m_stage) {
                case 3:  col = {0x3, 0xB, 0x5, 0xF}; break;  // Done    → green
                case 4:  col = {0xE, 0x3, 0x3, 0xF}; break;  // Failed  → red
                default: col = {0x7, 0x7, 0x7, 0xF}; break;  // Cancel  → grey
            }
            renderer->drawString(m_msg.c_str(), false, x, y + 30, 14, col);
        }
    }

    void layout(u16, u16, u16, u16) override {
        this->setBoundaries(this->getX(), this->getY(),
                            this->getWidth(), PANEL_H);
    }

    Element *requestFocus(Element *, tsl::FocusDirection) override {
        return nullptr;
    }

private:
    int         m_stage    = 0;
    int         m_progress = 0;
    std::string m_pkg;
    std::string m_msg;
};

UpdateGui::UpdateGui()  = default;
UpdateGui::~UpdateGui() { reapThread(); }

void UpdateGui::startJob(int packageIndex, bool extractOnly) {
    if (isBusy()) return;

    m_activePackage = packageIndex;
    m_extractOnly   = extractOnly;
    m_resultMessage.clear();

    ult::abortDownload.store(false, std::memory_order_release);
    ult::abortUnzip.store(false, std::memory_order_release);

    m_stage.store(UpdateStage::Downloading, std::memory_order_release);

    if (R_SUCCEEDED(threadCreate(&m_thread, jobEntry, this,
                                 nullptr, 0x8000, 0x20, -2))) {
        m_threadActive = true;
        threadStart(&m_thread);
    } else {
        m_resultMessage = "Failed to create thread";
        m_stage.store(UpdateStage::Failed, std::memory_order_release);
        m_threadActive = false;
    }
}

void UpdateGui::requestCancel() {
    ult::abortDownload.store(true, std::memory_order_release);
    ult::abortUnzip.store(true, std::memory_order_release);
}

void UpdateGui::reapThread() {
    if (m_threadActive) {
        threadWaitForExit(&m_thread);
        threadClose(&m_thread);
        m_threadActive = false;
    }
}

/* static */ void UpdateGui::jobEntry(void *arg) {
    static_cast<UpdateGui *>(arg)->jobBody();
}

void UpdateGui::jobBody() {
    const PackageInfo &pkg = kPackages[m_activePackage];

    if (!m_extractOnly) {
        m_stage.store(UpdateStage::Downloading, std::memory_order_release);
        ult::createDirectory("sdmc:/config/horizon-oc/");

        bool ok = ult::downloadFile(pkg.url, pkg.zipPath, false, false);
        if (!ok || ult::abortDownload.load(std::memory_order_acquire)) {
            m_resultMessage = ult::abortDownload.load(std::memory_order_acquire)
                ? "Download cancelled"
                : "Download failed -- check network";
            m_stage.store(
                ult::abortDownload.load() ? UpdateStage::Cancelled : UpdateStage::Failed,
                std::memory_order_release);
            return;
        }
    }

    m_stage.store(UpdateStage::Extracting, std::memory_order_release);

    bool ok = ult::unzipFile(pkg.zipPath, "sdmc:/");
    if (!ok || ult::abortUnzip.load(std::memory_order_acquire)) {
        m_resultMessage = ult::abortUnzip.load(std::memory_order_acquire)
            ? "Extraction cancelled"
            : "Extraction failed -- archive may be corrupt";
        m_stage.store(
            ult::abortUnzip.load() ? UpdateStage::Cancelled : UpdateStage::Failed,
            std::memory_order_release);
        return;
    }

    std::remove(pkg.zipPath);

    m_resultMessage = "Update complete! Restart required.";
    m_stage.store(UpdateStage::Done, std::memory_order_release);
}


void UpdateGui::listUI() {
    BaseMenuGui::refresh();
    if (!this->context) return;

    this->listElement->addItem(new tsl::elm::CategoryHeader("Releases"));

    for (int i = 0; i < 2; ++i) {
        auto *item = new tsl::elm::ListItem(kPackages[i].displayName);
        item->setValue("\uE0E0");

        const int idx = i;
        item->setClickListener([this, idx](u64 keys) -> bool {
            if (isBusy()) return false;
            if (keys & HidNpadButton_A) {
                startJob(idx, false);
                return true;
            }
            return false;
        });

        m_items[i] = item;
        this->listElement->addItem(item);
    }

    this->listElement->addItem(new tsl::elm::CategoryHeader("Status"));

    auto *panel = new UpdateStatusPanel();
    panel->setState(0, 0, "", "");
    m_status = panel;
    this->listElement->addItem(panel);
}

void UpdateGui::pollJob() {
    auto stage = m_stage.load(std::memory_order_acquire);
    if (stage == UpdateStage::Idle || !m_status) return;

    const char *pkgName = (m_activePackage >= 0 && m_activePackage < 2)
                          ? kPackages[m_activePackage].displayName : "";

    if (stage == UpdateStage::Downloading) {
        int pct = ult::downloadPercentage.load(std::memory_order_relaxed);
        m_status->setState(1, pct, pkgName, "");
        if (m_items[m_activePackage])
            m_items[m_activePackage]->setValue("...");
        return;
    }

    if (stage == UpdateStage::Extracting) {
        int pct = ult::unzipPercentage.load(std::memory_order_relaxed);
        m_status->setState(2, pct, pkgName, "");
        if (m_items[m_activePackage])
            m_items[m_activePackage]->setValue("...");
        return;
    }

    int panelStage = (stage == UpdateStage::Done)   ? 3
                   : (stage == UpdateStage::Failed)  ? 4 : 5;

    m_status->setState(panelStage, 100, pkgName, m_resultMessage);

    for (int i = 0; i < 2; ++i)
        if (m_items[i]) m_items[i]->setValue("\uE0E0");

    reapThread();
    m_stage.store(UpdateStage::Idle, std::memory_order_release);
}

void UpdateGui::update() {
    BaseMenuGui::update();
    pollJob();
}

bool UpdateGui::handleInput(u64 keysDown, u64 keysHeld,
                            const HidTouchState &touchPos,
                            HidAnalogStickState leftJoy,
                            HidAnalogStickState rightJoy) {
    if ((keysDown & HidNpadButton_R) && isBusy()) {
        requestCancel();
        return true;
    }

    return BaseMenuGui::handleInput(keysDown, keysHeld, touchPos, leftJoy, rightJoy);
}

std::string UpdateGui::getJumpToItemName() {
    return "Updates";
}
