#include "ui/nvg_util.hpp"
#include "log.hpp"
#include <cstddef>
#include <cstdio>
#include <cstdarg>
#include <array>
#include <utility>
#include <algorithm>
#include <cmath>

namespace sphaira::ui::gfx {
namespace {

constexpr auto ALIGN_HOR = NVG_ALIGN_LEFT|NVG_ALIGN_CENTER|NVG_ALIGN_RIGHT;
constexpr auto ALIGN_VER = NVG_ALIGN_TOP|NVG_ALIGN_MIDDLE|NVG_ALIGN_BOTTOM|NVG_ALIGN_BASELINE;

constexpr std::array buttons = {
    std::pair{Button::A, "\uE0E0"},
    std::pair{Button::B, "\uE0E1"},
    std::pair{Button::X, "\uE0E2"},
    std::pair{Button::Y, "\uE0E3"},
    std::pair{Button::L, "\uE0E4"},
    std::pair{Button::R, "\uE0E5"},
    std::pair{Button::L2, "\uE0E6"},
    std::pair{Button::R2, "\uE0E7"},
    std::pair{Button::UP, "\uE0EB"},
    std::pair{Button::DOWN, "\uE0EC"},
    std::pair{Button::LEFT, "\uE0ED"},
    std::pair{Button::RIGHT, "\uE0EE"},
    std::pair{Button::START, "\uE0EF"},
    std::pair{Button::SELECT, "\uE0F0"},
    // std::pair{Button::LS, "\uE101"},
    // std::pair{Button::RS, "\uE102"},
    std::pair{Button::L3, "\uE104"},
    std::pair{Button::R3, "\uE105"},
};

// software based clipping, saves a few cpu cycles.
bool ClipRect(float x, float y) {
    return x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT;
}

bool ClipText(float x, float y, int align) {
    if ((!(align & ALIGN_HOR) || (align & NVG_ALIGN_LEFT)) && x >= SCREEN_WIDTH) {
        return true;
    }

    if ((!(align & ALIGN_VER) || (align & NVG_ALIGN_TOP)) && y >= SCREEN_HEIGHT) {
        return true;
    }

    return false;
}

// NEW ---------------------
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
    if (ClipRect(v.x, v.y)) {
        return;
    }

    nvgBeginPath(vg);
    nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
    nvgFillColor(vg, c);
    nvgFill(vg);
}

void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
    if (ClipRect(v.x, v.y)) {
        return;
    }

    nvgBeginPath(vg);
    nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
    nvgFillPaint(vg, p);
    nvgFill(vg);
}

void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
    float gradientX, gradientY, color;
    getHighlightAnimation(&gradientX, &gradientY, &color);

    const auto strokeWidth = 5.F;
    auto v2 = v;
    v2.x -= strokeWidth / 2.F;
    v2.y -= strokeWidth / 2.F;
    v2.w += strokeWidth;
    v2.h += strokeWidth;
    const auto corner_radius = 0.5F;

    const auto shadow_width = 2.F;
    const auto shadow_offset = 10.F;
    const auto shadow_feather = 10.F;
    const auto shadow_opacity = 128.F;

    // Shadow
    NVGpaint shadowPaint = nvgBoxGradient(vg,
        v2.x, v2.y + shadow_width,
        v2.w, v2.h,
        corner_radius * 2, shadow_feather,
        nvgRGBA(0, 0, 0, shadow_opacity * 1.f), nvgRGBA(0, 0, 0, 0));

    nvgBeginPath(vg);
    nvgRect(vg, v2.x - shadow_offset, v2.y - shadow_offset,
        v2.w + shadow_offset * 2, v2.h + shadow_offset * 3);
    nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
    nvgPathWinding(vg, NVG_HOLE);
    nvgFillPaint(vg, shadowPaint);
    nvgFill(vg);

    const auto color1 = theme->GetColour(ThemeEntryID_HIGHLIGHT_1);
    const auto color2 = theme->GetColour(ThemeEntryID_HIGHLIGHT_2);
    const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
    const auto transparent = nvgRGBA(0, 0, 0, 0);

    const auto pulsationColor = nvgRGBAf((color * color1.r) + (1 - color) * color2.r,
        (color * color1.g) + (1 - color) * color2.g,
        (color * color1.b) + (1 - color) * color2.b,
        1.f);

    const auto border1Paint = nvgRadialGradient(vg,
        v2.x + gradientX * v2.w, v2.y + gradientY * v2.h,
        strokeWidth * 10, strokeWidth * 40,
        borderColor, transparent);

    const auto border2Paint = nvgRadialGradient(vg,
        v2.x + (1 - gradientX) * v2.w, v2.y + (1 - gradientY) * v2.h,
        strokeWidth * 10, strokeWidth * 40,
        borderColor, transparent);

    nvgBeginPath(vg);
    nvgStrokeColor(vg, pulsationColor);
    nvgStrokeWidth(vg, strokeWidth);
    nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
    nvgStroke(vg);

    nvgBeginPath(vg);
    nvgStrokePaint(vg, border1Paint);
    nvgStrokeWidth(vg, strokeWidth);
    nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
    nvgStroke(vg);

    nvgBeginPath(vg);
    nvgStrokePaint(vg, border2Paint);
    nvgStrokeWidth(vg, strokeWidth);
    nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
    nvgStroke(vg);
}

void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
    if (ClipRect(v.x, v.y)) {
        return;
    }

    const auto corner_radius = 0.5;
    drawRectOutlineInternal(vg, theme, size, v);
    nvgBeginPath(vg);
    nvgRoundedRect(vg, v.x, v.y, v.w, v.h, corner_radius);
    nvgFillColor(vg, c);
    nvgFill(vg);
}

void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
    if (ClipText(v.x, v.y, align)) {
        return;
    }

    nvgBeginPath(vg);
    nvgFontSize(vg, size);
    nvgTextAlign(vg, align);
    nvgFillColor(vg, c);
    nvgText(vg, v.x, v.y, str, end);
}

void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
    nvgBeginPath(vg);
    nvgMoveTo(vg, aX, aY);
    nvgLineTo(vg, bX, bY);
    nvgLineTo(vg, cX, cY);
    nvgFillColor(vg, c);
    nvgFill(vg);
}

void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
    nvgBeginPath(vg);
    nvgMoveTo(vg, aX, aY);
    nvgLineTo(vg, bX, bY);
    nvgLineTo(vg, cX, cY);
    nvgFillPaint(vg, p);
    nvgFill(vg);
}

} // namespace

const char* getButton(const Button want) {
    for (auto& [key, val] : buttons) {
        if (key == want) {
            return val;
        }
    }
    std::unreachable();
}

void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) {
    std::va_list v;
    va_start(v, str);
    char buffer[0x100];
    std::vsnprintf(buffer, sizeof(buffer), str, v);
    va_end(v);
    drawText(vg, x, y, size, buffer, nullptr, align, c);
}

void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
    const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
    drawRect(vg, v, paint, rounded);
}

void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
    drawImage(vg, Vec4(x, y, w, h), texture, rounded);
}

void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
    if (ClipText(x, y, align)) {
        return;
    }

    nvgBeginPath(vg);
    nvgFontSize(vg, size);
    nvgTextAlign(vg, align);
    nvgFillColor(vg, c);
    nvgTextBox(vg, x, y, bound, str, end);
}

void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
    char buf[0x100];
    va_list v;
    va_start(v, str);
    std::vsnprintf(buf, sizeof(buf), str, v);
    va_end(v);
    nvgTextBounds(vg, x, y, buf, nullptr, bounds);
}

// NEW-----------

void dimBackground(NVGcontext* vg) {
    drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
}

void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
    drawRectIntenal(vg, {x,y,w,h}, c, rounded);
}

void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
    drawRectIntenal(vg, v, c, rounded);
}

void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
    drawRectIntenal(vg, {x,y,w,h}, p, rounded);
}

void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
    drawRectIntenal(vg, v, p, rounded);
}

void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, float x, float y, float w, float h) {
    drawRectOutlineInternal(vg, theme, size, {x,y,w,h}, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
}

void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
    drawRectOutlineInternal(vg, theme, size, v, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
}

void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c) {
    drawTextIntenal(vg, {x,y}, size, str, end, align, c);
}

void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor& c, const char* str, int align, const char* end) {
    drawTextIntenal(vg, {x,y}, size, str, end, align, c);
}

void drawText(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
    drawTextIntenal(vg, v, size, str, end, align, c);
}

void drawText(NVGcontext* vg, const Vec2& v, float size, const NVGcolor& c, const char* str, int align, const char* end) {
    drawTextIntenal(vg, v, size, str, end, align, c);
}

void drawScrollbar(NVGcontext* vg, const Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
    const s64 SCROLL = index_off;
    const s64 max_entry_display = max_per_page;
    const s64 entry_total = count;
    const float scc2 = 8.0;
    const float scw = 2.0;

    // only draw scrollbar if needed
    if (entry_total > max_entry_display) {
        const float sb_h = 1.f / (float)entry_total * h;
        const float sb_y = SCROLL;
        gfx::drawRect(vg, x, y, scc2, h, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND), false);
        gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(max_entry_display) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
    }
}

void drawScrollbar(NVGcontext* vg, const Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
    drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
}

void drawScrollbar2(NVGcontext* vg, const Theme* theme, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page) {
    // round up
    if (count % row) {
        count = count + (row - count % row);
    }

    const float scc2 = 6.0;
    const float scw = 2.0;

    // only draw scrollbar if needed
    if (count > page) {
        const float sb_h = 1.f / (float)count * h;
        const float sb_y = index_off;
        gfx::drawRect(vg, x, y, scc2, h, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND), false);
        gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(page) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
    }
}

void drawScrollbar2(NVGcontext* vg, const Theme* theme, s64 index_off, s64 count, s64 row, s64 page) {
    drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
}

void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
    drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
}

void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
    drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
}

void drawAppLable(NVGcontext* vg, const Theme* theme, ScrollingText& st, float x, float y, float w, const char* name) {
    // todo: no more 5am code
    const float max_box_w = 392.f;
    const float box_h = 48.f;
    // used for adjusting the position of the box.
    const float clip_pad = 25.f;
    const float clip_left = clip_pad;
    const float clip_right = 1220.f - clip_pad;
    const float text_pad = 25.f;
    const float font_size = 22.f;

    nvgTextAlign(vg, NVG_ALIGN_LEFT);
    nvgFontSize(vg, font_size);
    float bounds[4]{};
    nvgTextBounds(vg, 0, 0, name, NULL, bounds);

    const float trinaglex = x + (w / 2.f) - 9.f;
    const float trinagley = y - 14.f;
    const float center_x = x + (w / 2.f);
    const float y_offset = y - 62.f; // top of box
    const float text_width = bounds[2];
    float box_w = text_width + text_pad * 2;
    if (box_w > max_box_w) {
        box_w = max_box_w;
    }

    float box_x = center_x - (box_w / 2.f);
    if (box_x < clip_left) {
        box_x = clip_left;
    }
    if ((box_x + box_w) > clip_right) {
        // box_x -= ((box_x + box_w) - clip_right) / 2;
        box_x = (clip_right - box_w);
    }

    const float text_x = box_x + text_pad;
    const float text_y = y_offset + (box_h / 2.f);

    drawRect(vg, {x-4, y-4, w+8, w+8}, theme->GetColour(ThemeEntryID_GRID));
    nvgBeginPath(vg);

    nvgRoundedRect(vg, box_x, y_offset, box_w, box_h, 3.f);
    nvgFillColor(vg, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
    nvgFill(vg);

    drawTriangle(vg, trinaglex, trinagley, trinaglex + 18.f, trinagley, trinaglex + 9.f, trinagley + 12.f, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
    st.Draw(vg, true, text_x, text_y, box_w - text_pad * 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), name);
}

#define HIGHLIGHT_SPEED 350.0

static double highlightGradientX = 0;
static double highlightGradientY = 0;
static double highlightColor     = 0;

void updateHighlightAnimation() {
    const auto currentTime = svcGetSystemTick() * 10 / 192 / 1000;

    // Update variables
    highlightGradientX = (std::cos((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
    highlightGradientY = (std::sin((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
    highlightColor     = (std::sin((double)currentTime / HIGHLIGHT_SPEED * 2.0) + 1.0) / 2.0;
}

void getHighlightAnimation(float* gradientX, float* gradientY, float* color) {
    if (gradientX)
        *gradientX = (float)highlightGradientX;

    if (gradientY)
        *gradientY = (float)highlightGradientY;

    if (color)
        *color = (float)highlightColor;
}

} // namespace sphaira::ui::gfx
