#include "PluginProcessor.h"
#include "Presets/DeepPluckPresets.h"

#include <algorithm>
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <vector>

// Deliberately independent of the new generator/codec. Preserve this executable
// against the unchanged V0.11.5 processor before changing product sources.
namespace
{
void require (bool condition, const juce::String& message)
{
    if (! condition)
        throw std::runtime_error (message.toStdString());
}

struct Options
{
    juce::File preset, output;
    int legacy = -1;
    int rate = 48000;
    int block = 128;
    int voices = 8;
    juce::String mode = "audio";
    juce::String scene = "short";
    bool dry = false;
};

int integer (const juce::String& text)
{
    require (text.isNotEmpty() && text.containsOnly ("0123456789"), "Invalid integer");
    const auto value = text.getLargeIntValue();
    require (value <= 1000000, "Integer exceeds bound");
    return static_cast<int> (value);
}

juce::File absoluteFile (const juce::String& path)
{
    require (juce::File::isAbsolutePath (path), "An absolute path is required");
    return juce::File (path);
}

Options parse (int argc, char** argv)
{
    Options options;
    juce::StringArray seen;
    for (int i = 1; i < argc; ++i)
    {
        const juce::String key (argv[i]);
        require (! seen.contains (key), "Duplicate option " + key);
        seen.add (key);
        require (++i < argc, "Missing value for " + key);
        const juce::String value (argv[i]);
        if (key == "--preset") options.preset = absoluteFile (value);
        else if (key == "--output") options.output = absoluteFile (value);
        else if (key == "--legacy") options.legacy = integer (value);
        else if (key == "--rate") options.rate = integer (value);
        else if (key == "--buffer") options.block = integer (value);
        else if (key == "--voices") options.voices = integer (value);
        else if (key == "--mode") options.mode = value;
        else if (key == "--scene") options.scene = value;
        else if (key == "--fx")
        {
            require (value == "dry" || value == "wet", "Invalid FX mode");
            options.dry = value == "dry";
        }
        else require (false, "Unknown option " + key);
    }
    require ((options.preset != juce::File {}) != (options.legacy >= 0),
             "Supply exactly one of --preset or --legacy");
    require (options.legacy < 4, "Legacy index must be 0..3");
    require (options.output != juce::File {} && ! options.output.exists(),
             "Output must be a new absolute directory");
    require (options.rate == 44100 || options.rate == 48000 || options.rate == 96000,
             "Rate must be 44100, 48000 or 96000");
    require (options.block == 64 || options.block == 128 || options.block == 512,
             "Buffer must be 64, 128 or 512");
    require (options.voices >= 1 && options.voices <= 16, "Voices must be 1..16");
    require (options.mode == "audio" || options.mode == "timing", "Invalid mode");
    require (options.scene == "short" || options.scene == "full", "Invalid scene");
    return options;
}

struct Event { int sample; juce::MidiMessage message; };

std::vector<Event> eventsFor (const Options& options)
{
    std::vector<Event> events;
    const auto add = [&] (double seconds, juce::MidiMessage message)
    {
        events.push_back ({ static_cast<int> (std::llround (seconds * options.rate)), message });
    };
    const auto note = [&] (double start, double length, int pitch, int velocity)
    {
        add (start, juce::MidiMessage::noteOn (1, pitch, static_cast<juce::uint8> (velocity)));
        add (start + length, juce::MidiMessage::noteOff (1, pitch));
    };
    if (options.mode == "timing")
    {
        for (int step = 0; step < 4; ++step)
            for (int voice = 0; voice < options.voices; ++voice)
                note (step * 0.5, 0.3, 48 + voice, 100);
    }
    else if (options.scene == "short")
    {
        note (0.0, 0.25, 48, 80);
        note (0.5, 0.25, 60, 100);
        note (1.0, 0.25, 72, 127);
        for (const int pitch : { 48, 55, 60, 63 }) note (1.5, 0.5, pitch, 100);
    }
    else
    {
        note (0.0, 0.25, 48, 32);
        note (0.75, 0.25, 60, 80);
        note (1.5, 0.25, 72, 127);
        for (const int pitch : { 48, 55, 60, 63 }) note (2.25, 0.5, pitch, 100);
        for (const int pitch : { 48, 51, 55, 58, 60, 63, 67, 70 }) note (3.25, 0.5, pitch, 100);
        for (int pitch = 48; pitch < 64; ++pitch) note (4.25, 0.5, pitch, 127);
        for (int pitch = 64; pitch < 72; ++pitch) note (4.35, 0.4, pitch, 127);
        add (4.85, juce::MidiMessage::allNotesOff (1));
        constexpr int pattern[] { 48, 55, 60, 63, 67, 63, 60, 55 };
        for (int step = 0; step < 24; ++step) note (5.0 + step * 0.125, 0.060, pattern[step % 8], 100);
    }
    std::stable_sort (events.begin(), events.end(), [] (const auto& a, const auto& b)
    { return a.sample < b.sample; });
    return events;
}

class PlayHead final : public juce::AudioPlayHead
{
public:
    int sample = 0;
    int rate = 48000;
    juce::Optional<PositionInfo> getPosition() const override
    {
        PositionInfo info;
        info.setBpm (120.0);
        info.setTimeInSamples (sample);
        info.setTimeInSeconds (static_cast<double> (sample) / rate);
        info.setPpqPosition (2.0 * sample / rate);
        info.setIsPlaying (true);
        return info;
    }
};

void apply (DeepPluckAudioProcessor& processor, const Options& options)
{
    juce::String error;
    if (options.legacy >= 0)
        require (processor.loadFactoryPreset (options.legacy), "Legacy load failed");
    else
    {
        deeppluck::UserPresetData data;
        require (processor.getPresetManager().loadPresetFile (options.preset, data, error), error);
        require (deeppluck::applyParameterValues (processor.getValueTreeState(), data.values),
                 "Preset apply failed");
    }
    if (options.dry)
        for (const int index : { 44, 49, 56 })
            require (deeppluck::setParameterValueNotifyingHost (processor.getValueTreeState(), index, 1.0f),
                     "Dry bypass failed");
    processor.setNonRealtime (true);
    processor.setRateAndBufferSizeDetails (options.rate, options.block);
    processor.prepareToPlay (options.rate, options.block);
}

int run (const Options& options)
{
    // A disposable processor warms code/data before the measured fresh instance.
    if (options.mode == "timing")
    {
        DeepPluckAudioProcessor warm;
        apply (warm, options);
        juce::AudioBuffer<float> buffer (2, options.block);
        juce::MidiBuffer midi;
        midi.ensureSize (4096);
        for (int sample = 0; sample < options.rate / 2; sample += options.block)
        {
            midi.clear();
            if (sample == 0)
                for (int voice = 0; voice < options.voices; ++voice)
                    midi.addEvent (juce::MidiMessage::noteOn (1, 48 + voice, static_cast<juce::uint8> (100)), 0);
            warm.processBlock (buffer, midi);
        }
        warm.releaseResources();
    }

    DeepPluckAudioProcessor processor;
    apply (processor, options);
    const auto before = deeppluck::captureParameterValues (processor.getValueTreeState());
    const auto events = eventsFor (options);
    const int phraseEnd = options.mode == "timing" ? options.rate * 2
                       : options.scene == "full" ? options.rate * 8 : options.rate * 2;
    const int limit = options.mode == "timing" ? phraseEnd : phraseEnd + options.rate * 120;
    juce::AudioBuffer<float> buffer (2, options.block);
    juce::MidiBuffer midi;
    midi.ensureSize (4096);
    std::vector<float> interleaved (static_cast<std::size_t> (options.block * 2));
    std::vector<double> durations;
    durations.reserve (static_cast<std::size_t> (limit / options.block + 1));
    PlayHead playHead;
    playHead.rate = options.rate;
    processor.setPlayHead (&playHead);
    require (options.output.createDirectory().wasOk(), "Could not create output directory");
    std::unique_ptr<juce::FileOutputStream> raw;
    if (options.mode == "audio")
    {
        raw = options.output.getChildFile ("audio.f32").createOutputStream();
        require (raw != nullptr && raw->openedOk(), "Cannot write raw render");
    }
    std::size_t nextEvent = 0;
    int silentSamples = 0, rendered = 0, deadlineMisses = 0;
    double peak = 0.0, energy = 0.0, monoEnergy = 0.0, durationSum = 0.0;
    double activeEnergy = 0.0;
    int activeSamples = 0;
    const double tickSeconds = 1.0 / static_cast<double> (juce::Time::getHighResolutionTicksPerSecond());
    double calibrationMinimum = 1.0;
    for (int i = 0; i < 128; ++i)
    {
        const auto start = juce::Time::getHighResolutionTicks();
        const auto end = juce::Time::getHighResolutionTicks();
        calibrationMinimum = std::min (calibrationMinimum, static_cast<double> (end - start) * tickSeconds);
    }

    for (int position = 0; position < limit; position += options.block)
    {
        const int count = std::min (options.block, limit - position);
        if (buffer.getNumSamples() != count) buffer.setSize (2, count, false, false, true);
        midi.clear();
        while (nextEvent < events.size() && events[nextEvent].sample < position + count)
        {
            const auto& event = events[nextEvent++];
            midi.addEvent (event.message, event.sample - position);
        }
        playHead.sample = position;
        const auto start = juce::Time::getHighResolutionTicks();
        processor.processBlock (buffer, midi);
        const auto end = juce::Time::getHighResolutionTicks();
        const double elapsed = static_cast<double> (end - start) * tickSeconds;
        durations.push_back (elapsed);
        durationSum += elapsed;
        if (elapsed > static_cast<double> (count) / options.rate) ++deadlineMisses;
        for (int i = 0; i < count; ++i)
        {
            const double left = buffer.getSample (0, i), right = buffer.getSample (1, i);
            require (std::isfinite (left) && std::isfinite (right), "Nonfinite raw audio");
            const double samplePeak = std::max (std::abs (left), std::abs (right));
            peak = std::max (peak, samplePeak);
            energy += left * left + right * right;
            monoEnergy += (left + right) * (left + right) * 0.25;
            if (position + i < phraseEnd)
            {
                activeEnergy += left * left + right * right;
                ++activeSamples;
            }
            silentSamples = position + i >= phraseEnd && samplePeak < 0.0000316227766
                          ? silentSamples + 1 : 0;
            interleaved[static_cast<std::size_t> (i * 2)] = static_cast<float> (left);
            interleaved[static_cast<std::size_t> (i * 2 + 1)] = static_cast<float> (right);
        }
        if (raw != nullptr)
            require (raw->write (interleaved.data(), static_cast<std::size_t> (count * 2) * sizeof (float)),
                     "Raw render write failed");
        rendered += count;
        if (options.mode == "audio" && silentSamples >= options.rate / 2) break;
    }
    if (raw != nullptr) { raw->flush(); require (raw->getStatus().wasOk(), "Raw flush failed"); }
    require (deeppluck::parameterValuesMatch (processor.getValueTreeState(), before), "Render mutated parameters");
    processor.releaseResources();
    std::sort (durations.begin(), durations.end());
    auto result = std::make_unique<juce::DynamicObject>();
    result->setProperty ("format", "DeepPluckPresetAudit");
    result->setProperty ("auditVersion", 1);
    result->setProperty ("pluginVersion", DEEPPLUCK_PRESET_AUDIT_VERSION);
    result->setProperty ("configuration", DEEPPLUCK_PRESET_AUDIT_CONFIG);
    result->setProperty ("sampleRate", options.rate);
    result->setProperty ("buffer", options.block);
    result->setProperty ("voices", options.voices);
    result->setProperty ("mode", options.mode);
    result->setProperty ("scene", options.scene);
    result->setProperty ("dry", options.dry);
    result->setProperty ("frames", rendered);
    result->setProperty ("finite", true);
    result->setProperty ("peak", peak);
    result->setProperty ("rms", std::sqrt (energy / (rendered * 2.0)));
    result->setProperty ("activeRms", std::sqrt (activeEnergy / (activeSamples * 2.0)));
    result->setProperty ("monoRms", std::sqrt (monoEnergy / rendered));
    result->setProperty ("tailQuiet", silentSamples >= options.rate / 2);
    result->setProperty ("cpuSeconds", durationSum);
    result->setProperty ("rtf", durationSum / (static_cast<double> (rendered) / options.rate));
    result->setProperty ("medianBlockSeconds", durations[durations.size() / 2]);
    result->setProperty ("p99BlockSeconds", durations[static_cast<std::size_t> (0.99 * (durations.size() - 1))]);
    result->setProperty ("maxBlockSeconds", durations.back());
    result->setProperty ("deadlineMisses", deadlineMisses);
    result->setProperty ("timerMinimumSeconds", calibrationMinimum);
    const bool pass = peak > 0.000001 && peak <= 0.95
                   && (options.mode == "timing" || silentSamples >= options.rate / 2);
    result->setProperty ("contentPass", pass);
    juce::Array<juce::var> applied;
    for (const auto value : before) applied.add (static_cast<double> (value));
    result->setProperty ("appliedValues", applied);
    require (options.output.getChildFile ("result.json").replaceWithText (
        juce::JSON::toString (juce::var (result.release())), false, false, "\n"), "Cannot write receipt");
    std::cout << "Preset audit " << (pass ? "PASS" : "CONTENT REVIEW")
              << "; peak=" << peak << "; frames=" << rendered << '\n';
    return pass ? 0 : 2;
}
}

int main (int argc, char** argv)
{
    if (argc == 2 && juce::String (argv[1]) == "--help")
    {
        std::cout << "DeepPluckPresetAudit --preset ABSOLUTE_FILE | --legacy 0..3\n"
                     "--output NEW_ABSOLUTE_DIRECTORY --rate 44100|48000|96000\n"
                     "--buffer 64|128|512 --mode audio|timing --scene short|full\n"
                     "--voices 1..16 --fx dry|wet\n"
                     "Timing: 0.5 s disposable warmup, 2 s measurement. Audio: bounded complete tail.\n";
        return 0;
    }
    juce::ScopedJuceInitialiser_GUI initialiseJuce;
    try { return run (parse (argc, argv)); }
    catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; }
}
