// LevelNormalizer — offline preset loudness calibration tool.
//
// Renders every factory preset through the real DSP chain (voice + FX) at a
// fixed master gain of 1.0, measures the resulting peak, and computes the
// "masterLevel" value that brings the peak to the target. Used to keep all
// factory presets at a consistent loudness.
//
//   ./level_normalizer            dry run (prints a table, changes nothing)
//   ./level_normalizer --apply    rewrites masterLevel in PresetManager.h
//
// Note: matches presets to masterLevel entries by insertion order — every
// preset must contain exactly one "masterLevel" parameter.

#include <juce_core/juce_core.h>
#include <juce_audio_processors/juce_audio_processors.h>
#include "../Source/PresetManager.h"
#include "../Source/DSP/Voice.h"
#include "../Source/DSP/Distortion.h"
#include "../Source/DSP/Compressor.h"
#include "../Source/DSP/Limiter.h"
#include "../Source/DSP/Delay.h"
#include "../Source/DSP/Reverb.h"

#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>

namespace {

constexpr double sampleRate = 44100.0;
constexpr int blockSize = 512;
constexpr double holdSeconds = 3.0;   // typical held-note reference length
constexpr double tailSeconds = 5.0;
constexpr float targetPeak = 0.8f;
constexpr float targetRms = 0.15f;

float param(const Preset& p, const char* id, float def) {
    auto it = p.params.find(juce::String(id));
    return it != p.params.end() ? it->second : def;
}

float paramInt(const Preset& p, const char* id, int def) {
    return static_cast<float>(static_cast<int>(param(p, id, static_cast<float>(def))));
}

struct Result {
    float peak = 0.0f;
    float rms = 0.0f;
    float masterLevel = 0.7f;
};

void renderPreset(const Preset& preset, Result& out) {
    juce::Synthesiser synth;
    synth.setCurrentPlaybackSampleRate(sampleRate);
    synth.addSound(new SubtractiveSound());
    synth.addVoice(new SubtractiveVoice());
    auto* voice = dynamic_cast<SubtractiveVoice*>(synth.getVoice(0));

    voice->setParameters(
        static_cast<Waveform>(static_cast<int>(param(preset, "osc1Wave", 1))),
        static_cast<Waveform>(static_cast<int>(param(preset, "osc2Wave", 2))),
        param(preset, "osc1Oct", 0.0f),
        param(preset, "osc2Oct", -1.0f),
        param(preset, "osc1Semi", 0.0f),
        param(preset, "osc2Semi", 0.0f),
        param(preset, "osc1Fine", 0.0f),
        param(preset, "osc2Fine", 7.0f),
        param(preset, "osc1Level", 0.7f),
        param(preset, "osc2Level", 0.7f),
        param(preset, "phaseOffset", 0.5f),
        param(preset, "filterCutoff", 8000.0f),
        param(preset, "filterRes", 0.3f),
        static_cast<FilterType>(static_cast<int>(param(preset, "filterType", 0))),
        param(preset, "filterEnvAmt", 0.0f),
        param(preset, "keyTrack", 0.5f),
        param(preset, "envAttack", 0.01f),
        param(preset, "envDecay", 0.3f),
        param(preset, "envSustain", 0.7f),
        param(preset, "envRelease", 0.5f),
        param(preset, "fEnvAttack", 0.01f),
        param(preset, "fEnvDecay", 0.3f),
        param(preset, "fEnvSustain", 0.5f),
        param(preset, "fEnvRelease", 0.5f),
        param(preset, "pan", 0.0f),
        param(preset, "drive", 1.5f),
        1.0f,  // outputLevel (master gain applied separately, see processBlock)
        param(preset, "lfo1Rate", 2.0f),
        param(preset, "lfo1Depth", 0.0f),
        static_cast<int>(paramInt(preset, "lfo1Shape", 0)),
        static_cast<int>(paramInt(preset, "lfo1Dest", 0)),
        param(preset, "lfo2Rate", 2.0f),
        param(preset, "lfo2Depth", 0.0f),
        static_cast<int>(paramInt(preset, "lfo2Shape", 0)),
        static_cast<int>(paramInt(preset, "lfo2Dest", 0)));

    Distortion distortion;
    Compression compression;
    Limiter limiter;
    Delay delay;
    ReverbFX reverb;
    distortion.prepare(sampleRate);
    compression.prepare(sampleRate);
    limiter.prepare(sampleRate);
    delay.prepare(sampleRate);
    reverb.prepare(sampleRate);

    distortion.setParameters(
        static_cast<DistortionType>(static_cast<int>(param(preset, "distType", 0))),
        param(preset, "distAmount", 0.0f),
        param(preset, "distSymmetry", 0.0f),
        param(preset, "distTone", 1.0f));
    compression.setParameters(
        param(preset, "compThreshold", -20.0f),
        param(preset, "compRatio", 4.0f),
        param(preset, "compAttack", 10.0f),
        param(preset, "compRelease", 100.0f),
        param(preset, "compMakeup", 0.0f));
    limiter.setParameters(1.0f, 0.9f, 50.0f);  // plugin defaults (presets don't override)
    delay.setParameters(param(preset, "delayTime", 200.0f),
                        param(preset, "delayFeedback", 0.0f),
                        param(preset, "delayMix", 0.0f),
                        param(preset, "delayPingPong", 0.0f) > 0.5f);
    reverb.setParameters(param(preset, "reverbSize", 0.5f),
                         param(preset, "reverbDamping", 0.5f),
                         0.8f,
                         param(preset, "reverbMix", 0.0f));

    const float autoPanRate = param(preset, "autoPanRate", 2.0f);
    const float autoPanDepth = param(preset, "autoPanDepth", 0.0f);
    float autoPanPhase = 0.0f;
    constexpr float twoPi = 6.2831853f;

    juce::AudioBuffer<float> buffer(2, blockSize);
    std::vector<float> leftSamples, rightSamples;
    const size_t capacity = static_cast<size_t>((holdSeconds + tailSeconds) * sampleRate);
    leftSamples.reserve(capacity);
    rightSamples.reserve(capacity);

    auto renderAndApply = [&](double seconds) {
        int total = static_cast<int>(seconds * sampleRate);
        int done = 0;
        while (done < total) {
            const int n = std::min(blockSize, total - done);
            buffer.clear();
            juce::MidiBuffer midi;
            synth.renderNextBlock(buffer, midi, 0, n);

            float* l = buffer.getWritePointer(0);
            float* r = buffer.getWritePointer(1);
            for (int i = 0; i < n; ++i) {
                float left = l[i];
                float right = r[i];

                left = distortion.process(left);
                right = distortion.process(right);
                left = compression.process(left);
                right = compression.process(right);
                left = limiter.process(left);
                right = limiter.process(right);

                if (autoPanDepth > 0.001f) {
                    const float panLfo = std::sin(autoPanPhase * twoPi);
                    const float leftGain = 1.0f - autoPanDepth * 0.5f * (1.0f + panLfo);
                    const float rightGain = 1.0f - autoPanDepth * 0.5f * (1.0f - panLfo);
                    left *= leftGain;
                    right *= rightGain;
                    autoPanPhase += autoPanRate / static_cast<float>(sampleRate);
                    if (autoPanPhase >= 1.0f) autoPanPhase -= 1.0f;
                }

                delay.process(left, right);
                l[i] = left;
                r[i] = right;
            }

            reverb.process(l, r, n);
            for (int i = 0; i < n; ++i) {
                leftSamples.push_back(l[i]);
                rightSamples.push_back(r[i]);
            }
            done += n;
        }
    };

    synth.noteOn(1, 60, 1.0f);
    renderAndApply(holdSeconds);
    synth.noteOff(1, 60, 0.0f, true);
    renderAndApply(tailSeconds);

    float peak = 0.0f;
    double sumSquares = 0.0;
    for (size_t i = 0; i < leftSamples.size(); ++i) {
        const float mix = (leftSamples[i] + rightSamples[i]) * 0.5f;
        peak = std::max(peak, std::abs(mix));
        sumSquares += static_cast<double>(mix) * mix;
    }

    out.peak = peak;
    out.rms = static_cast<float>(std::sqrt(sumSquares / static_cast<double>(leftSamples.size())));

    // masterGain = masterLevel^2 (perceptual taper in processBlock).
    // Pick the more conservative of the peak and RMS targets so loud presets
    // are reined in on both fronts and nothing clips.
    float masterLevel = 0.7f;
    if (peak > 1.0e-4f) {
        const float gainToPeak = targetPeak / peak;
        const float gainToRms = out.rms > 1.0e-4f ? targetRms / out.rms : 1.0f;
        const float gain = std::min(gainToPeak, gainToRms);
        masterLevel = std::sqrt(std::min(gain, 1.0f));
    }
    out.masterLevel = juce::jlimit(0.0f, 1.0f, masterLevel);
}

std::string formatValue(float v) {
    char buf[32];
    std::snprintf(buf, sizeof(buf), "%.3f", v);
    std::string s(buf);
    if (s.find('.') != std::string::npos) {
        while (!s.empty() && s.back() == '0') s.pop_back();
        if (!s.empty() && s.back() == '.') s.pop_back();
    }
    if (s.empty() || s == "-") return "0.0";
    if (s.find('.') == std::string::npos) s += ".0";  // keep a decimal point
    return s;
}

bool isNumberChar(char c) {
    return (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E';
}

int applyToHeader(const std::vector<Result>& results) {
    const char* path = "Source/PresetManager.h";
    std::ifstream in(path, std::ios::binary);
    if (!in) {
        std::fprintf(stderr, "could not open %s\n", path);
        return 1;
    }
    std::stringstream ss;
    ss << in.rdbuf();
    std::string content = ss.str();

    const std::string marker = "\"masterLevel\"";
    size_t pos = 0;
    size_t idx = 0;
    while ((pos = content.find(marker, pos)) != std::string::npos) {
        const size_t comma = content.find(',', pos);
        if (comma == std::string::npos) break;
        size_t numStart = content.find_first_not_of(" \t", comma + 1);
        if (numStart == std::string::npos) break;
        size_t numEnd = numStart;
        while (numEnd < content.size() && isNumberChar(content[numEnd])) ++numEnd;

        const bool hadF = numEnd < content.size() && (content[numEnd] == 'f' || content[numEnd] == 'F');
        const std::string token = content.substr(numStart, numEnd - numStart);

        float oldVal = 0.0f;
        try { oldVal = std::stof(token); } catch (...) {}

        const float newVal = idx < results.size() ? results[idx].masterLevel : oldVal;

        std::string replacement = formatValue(newVal);
        if (hadF) replacement += "f";

        if (std::abs(oldVal - newVal) > 1.0e-4f) {
            std::printf("    %s : %s -> %s\n",
                        marker.c_str(), token.c_str(), replacement.c_str());
        }
        const size_t replaceEnd = numEnd + (hadF ? 1 : 0);
        content.replace(numStart, replaceEnd - numStart, replacement);
        pos = numStart + replacement.size();
        ++idx;
    }

    if (idx != results.size()) {
        std::fprintf(stderr,
                     "preset/masterLevel count mismatch (%zu presets, %zu entries) — aborting.\n",
                     results.size(), idx);
        return 1;
    }

    std::ofstream out(path, std::ios::binary | std::ios::trunc);
    if (!out) {
        std::fprintf(stderr, "could not write %s\n", path);
        return 1;
    }
    out << content;
    return 0;
}

}  // namespace

int main(int argc, char** argv) {
    const bool apply = argc > 1 && std::string(argv[1]) == "--apply";

    PresetManager manager;
    const auto& presets = manager.getPresets();
    std::vector<Result> results;
    results.reserve(presets.size());

    std::printf("%-22s %8s %8s %8s %8s\n", "PRESET", "PEAK", "RMS", "MASTER", "NEW");
    std::printf("%-22s %8s %8s %8s %8s\n", "------", "----", "---", "------", "---");

    float minNew = 1.0f, maxNew = 0.0f;
    for (const auto& preset : presets) {
        Result res;
        renderPreset(preset, res);
        results.push_back(res);

        const float oldMaster = param(preset, "masterLevel", 0.7f);
        minNew = std::min(minNew, res.masterLevel);
        maxNew = std::max(maxNew, res.masterLevel);

        std::printf("%-22s %8.3f %8.3f %8.3f %8.3f\n",
                    preset.name.toRawUTF8(), res.peak, res.rms, oldMaster, res.masterLevel);
    }

    std::printf("\n%zu presets measured. new masterLevel range: %.3f .. %.3f (peak %.2f, rms %.2f)\n",
                presets.size(), minNew, maxNew, targetPeak, targetRms);

    if (apply) {
        std::printf("\napplying changes to Source/PresetManager.h:\n");
        return applyToHeader(results);
    }

    std::printf("\nrun with --apply to write the NEW column into PresetManager.h\n");
    return 0;
}
