/*
 * MIT License
 * Author: Mark Allyn
 *
 * left_panel.cpp — TextRenderer and LeftPanel implementations.
 */

#include "left_panel.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include <fstream>
#include <sstream>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <vector>

// ---- Shader helpers ----
static std::string lpReadFile(const std::string& p) {
    std::ifstream f(p); if(!f.is_open()){fprintf(stderr,"text shader: can't open %s\n",p.c_str());return "";}
    std::ostringstream ss; ss << f.rdbuf(); return ss.str();
}
static GLuint lpCompile(GLenum type, const std::string& src) {
    const char* s=src.c_str(); GLuint sh=glCreateShader(type);
    glShaderSource(sh,1,&s,nullptr); glCompileShader(sh);
    GLint ok=0; glGetShaderiv(sh,GL_COMPILE_STATUS,&ok);
    if(!ok){char l[1024];glGetShaderInfoLog(sh,1024,nullptr,l);fprintf(stderr,"text shader: %s\n",l);}
    return sh;
}
static GLuint lpLink(const std::string& vp, const std::string& fp) {
    auto vs=lpReadFile(vp),fs=lpReadFile(fp);
    if(vs.empty()||fs.empty()) return 0;
    GLuint v=lpCompile(GL_VERTEX_SHADER,vs),f=lpCompile(GL_FRAGMENT_SHADER,fs);
    GLuint p=glCreateProgram(); glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p);
    GLint ok=0; glGetProgramiv(p,GL_LINK_STATUS,&ok);
    if(!ok){char l[1024];glGetProgramInfoLog(p,1024,nullptr,l);fprintf(stderr,"text prog: %s\n",l);}
    glDeleteShader(v); glDeleteShader(f); return p;
}

// ================================================================
// TextRenderer
// ================================================================

bool TextRenderer::init(const std::string& fontPath,
                        int pixelHeight,
                        const std::string& shaderDir)
{
    pixelH_ = pixelHeight;

    FT_Library ft;
    if (FT_Init_FreeType(&ft)) {
        fprintf(stderr, "TextRenderer: FreeType init failed\n");
        return false;
    }

    FT_Face face;
    if (FT_New_Face(ft, fontPath.c_str(), 0, &face)) {
        fprintf(stderr, "TextRenderer: can't load font %s\n", fontPath.c_str());
        FT_Done_FreeType(ft);
        return false;
    }
    FT_Set_Pixel_Sizes(face, 0, pixelHeight);

    // ---- Build atlas ----
    glGenTextures(1, &atlasTex_);
    glBindTexture(GL_TEXTURE_2D, atlasTex_);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlasW_, atlasH_, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    // Ensure single byte row alignment for glyph upload
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    int penX = 1, penY = 1, rowH = 0;

    for (int c = FIRST_CHAR; c < LAST_CHAR; ++c) {
        if (FT_Load_Char(face, c, FT_LOAD_RENDER)) continue;
        FT_Bitmap& bm = face->glyph->bitmap;

        if (penX + (int)bm.width + 1 > atlasW_) {
            penX  = 1;
            penY += rowH + 2;
            rowH  = 0;
        }
        if (penY + (int)bm.rows + 1 > atlasH_) {
            fprintf(stderr, "TextRenderer: atlas overflow at char %d\n", c);
            break;
        }

        glTexSubImage2D(GL_TEXTURE_2D, 0,
                        penX, penY,
                        bm.width, bm.rows,
                        GL_RED, GL_UNSIGNED_BYTE, bm.buffer);

        GlyphMetric& m = metrics_[c - FIRST_CHAR];
        m.atlasU  = static_cast<float>(penX)    / atlasW_;
        m.atlasV  = static_cast<float>(penY)    / atlasH_;
        m.atlasW  = static_cast<float>(bm.width)/ atlasW_;
        m.atlasH  = static_cast<float>(bm.rows) / atlasH_;
        m.bearX   = static_cast<float>(face->glyph->bitmap_left);
        m.bearY   = static_cast<float>(face->glyph->bitmap_top);
        m.sizeW   = static_cast<float>(bm.width);
        m.sizeH   = static_cast<float>(bm.rows);
        m.advanceX= static_cast<float>(face->glyph->advance.x >> 6);

        penX += bm.width + 2;
        rowH  = (int)bm.rows > rowH ? bm.rows : rowH;
    }

    glBindTexture(GL_TEXTURE_2D, 0);

    FT_Done_Face(face);
    FT_Done_FreeType(ft);

    // ---- Shader + VAO/VBO ----
    prog_ = lpLink(shaderDir + "text.vert", shaderDir + "text.frag");
    if (!prog_) return false;

    glGenVertexArrays(1, &vao_);
    glGenBuffers(1, &vbo_);
    glBindVertexArray(vao_);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_);
    // Reserve space for up to 512 characters × 6 vertices × 4 floats
    glBufferData(GL_ARRAY_BUFFER, 512 * 6 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4*sizeof(float), nullptr);
    glBindVertexArray(0);

    initialised_ = true;
    return true;
}

float TextRenderer::measureWidth(const std::string& text, float scale) const {
    float w = 0;
    for (char ch : text) {
        if (ch < FIRST_CHAR || ch >= LAST_CHAR) continue;
        w += metrics_[ch - FIRST_CHAR].advanceX * scale;
    }
    return w;
}

void TextRenderer::renderText(const std::string& text,
                              float x, float y,
                              float scale,
                              float r, float g, float b,
                              float viewportW, float viewportH,
                              bool centred) const
{
    if (!initialised_) return;

    if (centred) {
        float w = measureWidth(text, scale);
        x -= w * 0.5f;
    }

    // Build vertex array for all glyphs in the string
    std::vector<float> verts;
    verts.reserve(text.size() * 6 * 4);

    float curX = x;
    for (char ch : text) {
        if (ch < FIRST_CHAR || ch >= LAST_CHAR) {
            if (ch == ' ') {
                const GlyphMetric& sp = metrics_[0];  // space char at index 0
                curX += sp.advanceX * scale;
            }
            continue;
        }
        const GlyphMetric& m = metrics_[ch - FIRST_CHAR];

        float gx = curX + m.bearX * scale;
        float gy = y    - m.bearY * scale;   // bearY is up from baseline; screen y is down
        float gw = m.sizeW * scale;
        float gh = m.sizeH * scale;

        // 6 vertices per quad (two triangles)
        float u0 = m.atlasU,        v0 = m.atlasV;
        float u1 = m.atlasU+m.atlasW, v1 = m.atlasV+m.atlasH;

        // triangle 1
        verts.insert(verts.end(), {gx,    gy,    u0, v0});
        verts.insert(verts.end(), {gx,    gy+gh, u0, v1});
        verts.insert(verts.end(), {gx+gw, gy,    u1, v0});
        // triangle 2
        verts.insert(verts.end(), {gx,    gy+gh, u0, v1});
        verts.insert(verts.end(), {gx+gw, gy+gh, u1, v1});
        verts.insert(verts.end(), {gx+gw, gy,    u1, v0});

        curX += m.advanceX * scale;
    }
    if (verts.empty()) return;

    glUseProgram(prog_);
    glUniform2f(glGetUniformLocation(prog_, "u_viewportSize"), viewportW, viewportH);
    glUniform3f(glGetUniformLocation(prog_, "u_textColor"),    r, g, b);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, atlasTex_);
    glUniform1i(glGetUniformLocation(prog_, "u_glyphAtlas"), 0);

    // Text needs alpha blending
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glBindVertexArray(vao_);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_);

    size_t maxVerts = 512 * 6;
    size_t n = verts.size() / 4;
    if (n > maxVerts) n = maxVerts;
    glBufferSubData(GL_ARRAY_BUFFER, 0,
                    n * 4 * sizeof(float), verts.data());
    glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(n));

    glBindVertexArray(0);
    glDisable(GL_BLEND);
}

TextRenderer::~TextRenderer() {
    if (atlasTex_) glDeleteTextures(1, &atlasTex_);
    if (prog_)     glDeleteProgram(prog_);
    if (vao_)      glDeleteVertexArrays(1, &vao_);
    if (vbo_)      glDeleteBuffers(1, &vbo_);
}

// ================================================================
// LeftPanel
// ================================================================

void LeftPanel::render(float panelW, float panelH,
                       float viewportW, float viewportH) const
{
    float scale    = static_cast<float>(LEFT_PANEL_FONT_SIZE)
                   / static_cast<float>(tr_.basePixelHeight());
    float lineH    = tr_.basePixelHeight() * scale + LINE_GAP;
    float curY     = MARGIN_Y + tr_.basePixelHeight() * scale;  // baseline of first line

    for (const auto& line : lines_) {
        if (curY > panelH) break;
        tr_.renderText(line,
                       MARGIN_X, curY,
                       scale,
                       1.0f, 1.0f, 1.0f,  // white text
                       viewportW, viewportH);
        curY += lineH;
    }
}
