/*
 * MIT License
 * Author: Mark Allyn
 *
 * scope_ppi.cpp — abstract PPIScope implementation.
 */

#include "scope_ppi.h"
#include <GLFW/glfw3.h>
#include <glad/glad.h>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <algorithm>

bool PPIScope::init(const std::string& shaderDir) {
    if (!phosphor_.init(shaderDir)) {
        fprintf(stderr, "PPIScope: phosphor init failed\n");
        return false;
    }
    if (!graticule_.init(shaderDir, tr_,
                         SCOPE_CENTER_X,
                         SCOPE_CENTER_Y_TOP,
                         SCOPE_RADIUS_PX)) {
        fprintf(stderr, "PPIScope: graticule init failed\n");
        return false;
    }
    panel_.setLines(panelLines());
    return true;
}

// ----------------------------------------------------------------
// Held-key acceleration
// ----------------------------------------------------------------

// Apply one step for the given key.  Called both on initial press
// and repeatedly from updateHeldKeys() as hold time builds.
void PPIScope::applyKeyStep(int key, float /*unused*/) {
    const float maxMi = maxRangeM() / MILES_TO_METERS;

    switch (key) {
    case GLFW_KEY_R:  // cursor bearing right
        cursorBrgDeg_ = std::fmod(cursorBrgDeg_ + KEY_INITIAL_STEP_BRG + 360.f, 360.f);
        break;
    case GLFW_KEY_L:  // cursor bearing left
        cursorBrgDeg_ = std::fmod(cursorBrgDeg_ - KEY_INITIAL_STEP_BRG + 360.f, 360.f);
        break;
    case GLFW_KEY_T:  // cursor range increase
        cursorRngMi_ = std::min(cursorRngMi_ + KEY_INITIAL_STEP_RNG, maxMi);
        break;
    case GLFW_KEY_Y:  // cursor range decrease
        cursorRngMi_ = std::max(cursorRngMi_ - KEY_INITIAL_STEP_RNG, 0.0f);
        break;
    case GLFW_KEY_K:  // display offset clockwise
        brgOffsetDeg_ = std::fmod(brgOffsetDeg_ + KEY_INITIAL_STEP_BRG + 360.f, 360.f);
        break;
    case GLFW_KEY_J:  // display offset counter-clockwise
        brgOffsetDeg_ = std::fmod(brgOffsetDeg_ - KEY_INITIAL_STEP_BRG + 360.f, 360.f);
        break;
    case GLFW_KEY_1: {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        srs_.gain = std::clamp(srs_.gain + KEY_GAIN_STEP, MIN_GAIN, MAX_GAIN);
        break;
    }
    case GLFW_KEY_2: {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        srs_.gain = std::clamp(srs_.gain - KEY_GAIN_STEP, MIN_GAIN, MAX_GAIN);
        break;
    }
    case GLFW_KEY_3: {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        srs_.rainClutter = std::clamp(srs_.rainClutter + KEY_CLUTTER_STEP, 0.f, 1.f);
        break;
    }
    case GLFW_KEY_4: {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        srs_.rainClutter = std::clamp(srs_.rainClutter - KEY_CLUTTER_STEP, 0.f, 1.f);
        break;
    }
    case GLFW_KEY_5: {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        srs_.waveClutter = std::clamp(srs_.waveClutter + KEY_CLUTTER_STEP, 0.f, 1.f);
        break;
    }
    case GLFW_KEY_6: {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        srs_.waveClutter = std::clamp(srs_.waveClutter - KEY_CLUTTER_STEP, 0.f, 1.f);
        break;
    }
    default: break;
    }
}

void PPIScope::updateHeldKeys(float dt) {
    const float INITIAL_RATE = 1.5f;
    const float ACCEL        = 12.0f;
    const float MAX_RATE     = 30.0f;

    for (auto& [key, ks] : heldKeys_) {
        ks.holdTime += dt;
        float rate = INITIAL_RATE
                   + std::max(0.f, ks.holdTime - KEY_HOLD_ACCEL_START_SEC) * ACCEL;
        rate = std::min(rate, MAX_RATE);
        ks.stepAccum += rate * dt;
        while (ks.stepAccum >= 1.0f) {
            applyKeyStep(key, 1.0f);
            ks.stepAccum -= 1.0f;
        }
    }
}

// ----------------------------------------------------------------
// Key handling
// ----------------------------------------------------------------

void PPIScope::handleKey(int key, int /*scancode*/, int action, int mods) {
    (void)mods;

    // u/d go directly to the derived class; not in held-key system.
    if (key == GLFW_KEY_U || key == GLFW_KEY_D) {
        if (action == GLFW_PRESS || action == GLFW_REPEAT) {
            handleKeyRange(key, action);
            // Re-clamp cursor after range change
            float maxMi = maxRangeM() / MILES_TO_METERS;
            cursorRngMi_ = std::min(cursorRngMi_, maxMi);
        }
        return;
    }

    // All other keys: manage held state
    if (action == GLFW_PRESS) {
        heldKeys_[key] = {};
        applyKeyStep(key, 1.0f);   // immediate first step on press
    } else if (action == GLFW_RELEASE) {
        heldKeys_.erase(key);
    }
}

// ----------------------------------------------------------------
// Frame render
// ----------------------------------------------------------------

void PPIScope::render(float dt, float viewportW, float viewportH) {
    // Advance sweep angle (clockwise)
    sweepAnglePrev_ = sweepAngle_;
    sweepAngle_     = std::fmod(sweepAngle_ + dt * 360.0f / sweepPeriod_, 360.0f);

    updateHeldKeys(dt);

    float gain;
    {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        gain = srs_.gain;
    }

    // Snapshot targets
    std::array<TargetState, MAX_TARGETS> tBuf{};
    int tCount = tb_.read(tBuf);

    // Update phosphor FBO (changes viewport to FBO size internally)
    float maxM = maxRangeM();
    phosphor_.update(dt,
                     sweepAngle_, sweepAnglePrev_,
                     tBuf, tCount,
                     ringRadii(), ringCount(),
                     maxM);

    // Restore viewport to window size before rendering to screen
    glViewport(0, 0, static_cast<int>(viewportW), static_cast<int>(viewportH));

    // Clear screen
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 1. Phosphor display (scope circle only; rest stays black)
    phosphor_.renderToScreen(SCOPE_CENTER_X, SCOPE_CENTER_Y_GL,
                              SCOPE_RADIUS_PX, gain);

    // 2. Incandescent bearing graticule
    float gratI;
    {
        std::lock_guard<std::mutex> lk(srs_.mutex);
        gratI = srs_.gratIntensity;
    }
    graticule_.render(viewportW, viewportH, gratI, brgOffsetDeg_);

    // 3. Yellow cursor overlay
    float cursorNorm = (maxM > 0.f)
                     ? std::min((cursorRngMi_ * MILES_TO_METERS) / maxM, 1.0f)
                     : 0.f;
    // Cursor bearing in display coordinates (subtract offset so it tracks the display)
    float dispBrg = std::fmod(cursorBrgDeg_ - brgOffsetDeg_ + 360.f, 360.f);
    graticule_.renderCursor(viewportW, viewportH, dispBrg, cursorNorm);

    // 4. Cursor readout
    renderReadout(viewportW, viewportH);

    // 5. Left panel description
    panel_.render(static_cast<float>(LEFT_PANEL_WIDTH), viewportH,
                  viewportW, viewportH);
}

void PPIScope::renderReadout(float viewportW, float viewportH) const {
    char buf[160];
    std::snprintf(buf, sizeof(buf),
        "Range: %.2f mi   Bearing: %.1f\xC2\xB0T   Max: %.0f mi   Offset: %.1f\xC2\xB0",
        cursorRngMi_,
        cursorBrgDeg_,
        maxRangeM() / MILES_TO_METERS,
        brgOffsetDeg_);

    float ty    = SCOPE_CENTER_Y_TOP + SCOPE_RADIUS_PX + 22.0f;
    float scale = static_cast<float>(CURSOR_READOUT_FONT_SIZE)
                / static_cast<float>(tr_.basePixelHeight());

    tr_.renderText(buf,
                   SCOPE_CENTER_X - 230.0f, ty,
                   scale,
                   1.0f, 1.0f, 1.0f,
                   viewportW, viewportH);
}
