/*
 * MIT License
 * Author: Mark Allyn
 *
 * phosphor.cpp — P7 phosphor FBO ping-pong implementation.
 */

#include "phosphor.h"
#include <fstream>
#include <sstream>
#include <cstdio>
#include <cmath>

// ----------------------------------------------------------------
// Static helpers
// ----------------------------------------------------------------

static std::string readFile(const std::string& path) {
    std::ifstream f(path);
    if (!f.is_open()) {
        fprintf(stderr, "phosphor: cannot open %s\n", path.c_str());
        return "";
    }
    std::ostringstream ss;
    ss << f.rdbuf();
    return ss.str();
}

static GLuint compileSingle(GLenum type, const std::string& src) {
    const char* ptr = src.c_str();
    GLuint s = glCreateShader(type);
    glShaderSource(s, 1, &ptr, nullptr);
    glCompileShader(s);
    GLint ok = 0;
    glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[1024];
        glGetShaderInfoLog(s, sizeof(log), nullptr, log);
        fprintf(stderr, "phosphor shader compile error:\n%s\n", log);
    }
    return s;
}

GLuint PhosphorRenderer::compileProgram(const std::string& vp, const std::string& fp) {
    std::string vsrc = readFile(vp);
    std::string fsrc = readFile(fp);
    if (vsrc.empty() || fsrc.empty()) return 0;

    GLuint vs = compileSingle(GL_VERTEX_SHADER,   vsrc);
    GLuint fs = compileSingle(GL_FRAGMENT_SHADER, fsrc);

    GLuint prog = glCreateProgram();
    glAttachShader(prog, vs);
    glAttachShader(prog, fs);
    glLinkProgram(prog);

    GLint ok = 0;
    glGetProgramiv(prog, GL_LINK_STATUS, &ok);
    if (!ok) {
        char log[1024];
        glGetProgramInfoLog(prog, sizeof(log), nullptr, log);
        fprintf(stderr, "phosphor program link error:\n%s\n", log);
    }
    glDeleteShader(vs);
    glDeleteShader(fs);
    return prog;
}

// ----------------------------------------------------------------

void PhosphorRenderer::buildQuad() {
    float verts[] = {
        -1.f, -1.f,
         1.f, -1.f,
         1.f,  1.f,
        -1.f, -1.f,
         1.f,  1.f,
        -1.f,  1.f,
    };
    glGenVertexArrays(1, &quadVAO_);
    glGenBuffers(1, &quadVBO_);
    glBindVertexArray(quadVAO_);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO_);
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), nullptr);
    glBindVertexArray(0);
}

bool PhosphorRenderer::init(const std::string& shaderDir) {
    sweepProg_   = compileProgram(shaderDir + "sweep.vert",
                                  shaderDir + "sweep.frag");
    displayProg_ = compileProgram(shaderDir + "phosphor.vert",
                                  shaderDir + "phosphor.frag");
    if (!sweepProg_ || !displayProg_) return false;

    buildQuad();

    // Create ping-pong FBOs with GL_R32F single-channel float textures
    for (int i = 0; i < 2; ++i) {
        glGenFramebuffers(1,  &fbo_[i]);
        glGenTextures(1,      &tex_[i]);

        glBindTexture(GL_TEXTURE_2D, tex_[i]);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F,
                     fboSize_, fboSize_, 0,
                     GL_RED, GL_FLOAT, 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);

        glBindFramebuffer(GL_FRAMEBUFFER, fbo_[i]);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                               GL_TEXTURE_2D, tex_[i], 0);

        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
            fprintf(stderr, "phosphor: FBO[%d] incomplete\n", i);
            return false;
        }
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    clear();
    initialised_ = true;
    return true;
}

PhosphorRenderer::~PhosphorRenderer() {
    if (sweepProg_)   glDeleteProgram(sweepProg_);
    if (displayProg_) glDeleteProgram(displayProg_);
    for (int i = 0; i < 2; ++i) {
        if (fbo_[i]) glDeleteFramebuffers(1, &fbo_[i]);
        if (tex_[i]) glDeleteTextures(1,     &tex_[i]);
    }
    if (quadVAO_) glDeleteVertexArrays(1, &quadVAO_);
    if (quadVBO_) glDeleteBuffers(1,      &quadVBO_);
}

void PhosphorRenderer::clear() {
    for (int i = 0; i < 2; ++i) {
        glBindFramebuffer(GL_FRAMEBUFFER, fbo_[i]);
        glClearColor(0, 0, 0, 0);
        glClear(GL_COLOR_BUFFER_BIT);
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    curr_ = 0;
}

// ----------------------------------------------------------------

void PhosphorRenderer::update(
    float  dt,
    float  beamAngle,
    float  beamAnglePrev,
    const std::array<TargetState, MAX_TARGETS>& targets,
    int    targetCount,
    const float* ringRadii,
    int    ringCount,
    float  maxRangeM
) {
    if (!initialised_) return;

    int prev = curr_;
    int next = 1 - curr_;

    float decayFactor = std::exp(-P7_DECAY_RATE * dt);

    // ---- Sweep / decay pass ----
    glBindFramebuffer(GL_FRAMEBUFFER, fbo_[next]);
    glViewport(0, 0, fboSize_, fboSize_);
    glDisable(GL_BLEND);

    glUseProgram(sweepProg_);

    // Bind previous texture
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, tex_[prev]);
    glUniform1i(glGetUniformLocation(sweepProg_, "u_prevPhosphor"), 0);

    glUniform1f(glGetUniformLocation(sweepProg_, "u_decayFactor"),    decayFactor);
    glUniform1f(glGetUniformLocation(sweepProg_, "u_beamAngle"),      beamAngle);
    glUniform1f(glGetUniformLocation(sweepProg_, "u_beamAnglePrev"),  beamAnglePrev);
    glUniform1f(glGetUniformLocation(sweepProg_, "u_sweepBg"),        SWEEP_BACKGROUND_ENERGY);
    glUniform1f(glGetUniformLocation(sweepProg_, "u_halfBeamDeg"),    SWEEP_HALF_BEAM_DEG);

    // Ring radii and count
    int rClamped = (ringCount < 4) ? ringCount : 4;
    glUniform1fv(glGetUniformLocation(sweepProg_, "u_ringRadii"),     rClamped, ringRadii);
    glUniform1i (glGetUniformLocation(sweepProg_, "u_ringCount"),     rClamped);
    glUniform1f (glGetUniformLocation(sweepProg_, "u_ringWidth"),     RING_WIDTH_NORM);
    glUniform1f (glGetUniformLocation(sweepProg_, "u_ringBrightness"),RING_BRIGHTNESS);

    // Target array: pack into vec4 array
    float tdata[MAX_TARGETS * 4] = {};
    int   tCount = (targetCount < MAX_TARGETS) ? targetCount : MAX_TARGETS;
    float invRange = (maxRangeM > 0.f) ? 1.0f / maxRangeM : 0.f;
    for (int i = 0; i < tCount; ++i) {
        tdata[i*4+0] = targets[i].range_m * invRange;    // normalised 0-1
        tdata[i*4+1] = targets[i].bearing_deg;
        tdata[i*4+2] = targets[i].brightness;
        tdata[i*4+3] = targets[i].size_m   * invRange;   // normalised blob half-radius
    }
    glUniform4fv(glGetUniformLocation(sweepProg_, "u_targets"),      tCount, tdata);
    glUniform1i (glGetUniformLocation(sweepProg_, "u_targetCount"),  tCount);

    glBindVertexArray(quadVAO_);
    glDrawArrays(GL_TRIANGLES, 0, 6);
    glBindVertexArray(0);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    curr_ = next;
}

// ----------------------------------------------------------------

void PhosphorRenderer::renderToScreen(
    float scopeCX, float scopeCY,
    float scopeRadius,
    float gain
) {
    if (!initialised_) return;

    glUseProgram(displayProg_);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, tex_[curr_]);
    glUniform1i(glGetUniformLocation(displayProg_, "u_phosphor"),      0);

    glUniform2f(glGetUniformLocation(displayProg_, "u_scopeCenter"),   scopeCX, scopeCY);
    glUniform1f(glGetUniformLocation(displayProg_, "u_scopeRadius"),   scopeRadius);
    glUniform1f(glGetUniformLocation(displayProg_, "u_gain"),          gain);
    glUniform1f(glGetUniformLocation(displayProg_, "u_bloomStep"),     BLOOM_SAMPLE_STEP_UV);
    glUniform1f(glGetUniformLocation(displayProg_, "u_bloomStrength"), BLOOM_BLEND_STRENGTH);

    glDisable(GL_BLEND);
    glBindVertexArray(quadVAO_);
    glDrawArrays(GL_TRIANGLES, 0, 6);
    glBindVertexArray(0);
}
