#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "Presets/DeepPluckPresets.h"
#include "Synth/DeepPluckSynthesiser.h"

#include <algorithm>
#include <cmath>

DeepPluckAudioProcessor::DeepPluckAudioProcessor (juce::File injectedPresetRoot,
                                                  bool enablePresetFileIo)
    : AudioProcessor (BusesProperties()
                          .withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
      valueTreeState (*this, nullptr, stateIdentifier, createParameterLayout()),
      parameterCache (valueTreeState),
      parameterSnapshot (parameterCache.getSnapshot()),
      randomization (valueTreeState),
      presetManager (valueTreeState, JucePlugin_VersionString,
                     std::move (injectedPresetRoot)),
      factoryLibrary (deeppluck::loadEmbeddedFactoryLibrary (valueTreeState)),
      presetFileIoEnabled (enablePresetFileIo)
{
    deeppluck::configureDeepPluckSynthesiser (synthesiser, parameterSnapshot);
    if (presetFileIoEnabled
        && juce::MessageManager::getInstanceWithoutCreating() != nullptr)
        ensureInitialPresetScan();
}

void DeepPluckAudioProcessor::prepareToPlay (double sampleRate, int maximumExpectedSamplesPerBlock)
{
    jassert (std::isfinite (sampleRate) && sampleRate > 0.0);
    jassert (maximumExpectedSamplesPerBlock >= 0);
    const auto validatedSampleRate = std::isfinite (sampleRate) && sampleRate > 0.0
                                        ? sampleRate : 44100.0;

    synthesiser.setCurrentPlaybackSampleRate (validatedSampleRate);
    lastValidTempoBpm = 120.0;
    synthesiser.setHostTempoBpm (lastValidTempoBpm);
    effectsRack.prepare (validatedSampleRate, juce::jmax (1, maximumExpectedSamplesPerBlock));
    parameterSnapshot = parameterCache.getSnapshot();
    outputGain.reset (validatedSampleRate, smoothingSeconds);
    outputGain.setCurrentAndTargetValue (juce::Decibels::decibelsToGain (parameterSnapshot.outputGainDb));
    outputMeter.reset();
}

void DeepPluckAudioProcessor::releaseResources()
{
    synthesiser.allNotesOff (0, false);
    effectsRack.reset();
    outputMeter.reset();
}

bool DeepPluckAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
    return layouts.getMainInputChannelSet().isDisabled()
        && layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo();
}

void DeepPluckAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
                                            juce::MidiBuffer& midiMessages)
{
    const juce::ScopedNoDenormals noDenormals;
    buffer.clear();

    parameterSnapshot = parameterCache.getSnapshot();
    if (auto* currentPlayHead = getPlayHead())
        if (const auto position = currentPlayHead->getPosition())
            if (const auto tempo = position->getBpm(); tempo.hasValue()
                && std::isfinite (*tempo) && *tempo >= 20.0 && *tempo <= 400.0)
                lastValidTempoBpm = *tempo;

    synthesiser.setHostTempoBpm (lastValidTempoBpm);
    synthesiser.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples());
    effectsRack.process (buffer, parameterSnapshot, lastValidTempoBpm);
    outputGain.setTargetValue (juce::Decibels::decibelsToGain (parameterSnapshot.outputGainDb));

    float leftPeak = 0.0f;
    float rightPeak = 0.0f;
    for (auto sample = 0; sample < buffer.getNumSamples(); ++sample)
    {
        const auto gain = outputGain.getNextValue();

        for (auto channel = 0; channel < buffer.getNumChannels(); ++channel)
        {
            const auto output = buffer.getSample (channel, sample) * gain;
            buffer.setSample (channel, sample, output);
            if (std::isfinite (output))
            {
                if (channel == 0) leftPeak = juce::jmax (leftPeak, std::abs (output));
                if (channel == 1) rightPeak = juce::jmax (rightPeak, std::abs (output));
            }
        }
    }
    outputMeter.publish (leftPeak, buffer.getNumChannels() > 1 ? rightPeak : leftPeak);
}

void DeepPluckAudioProcessor::processBlockBypassed (juce::AudioBuffer<float>& buffer,
                                                    juce::MidiBuffer& midiMessages)
{
    juce::ignoreUnused (midiMessages);
    buffer.clear();
    outputMeter.reset();
}

std::array<float, 2> DeepPluckAudioProcessor::consumeOutputMeterPeaks() noexcept
{
    return outputMeter.consume();
}

bool DeepPluckAudioProcessor::isOutputMeterLockFree() const noexcept
{
    return outputMeter.isLockFree();
}

juce::AudioProcessorEditor* DeepPluckAudioProcessor::createEditor()
{
    return new DeepPluckAudioProcessorEditor (*this);
}

bool DeepPluckAudioProcessor::hasEditor() const
{
    return true;
}

const juce::String DeepPluckAudioProcessor::getName() const
{
    return JucePlugin_Name;
}

bool DeepPluckAudioProcessor::acceptsMidi() const
{
    return true;
}

bool DeepPluckAudioProcessor::producesMidi() const
{
    return false;
}

bool DeepPluckAudioProcessor::isMidiEffect() const
{
    return false;
}

double DeepPluckAudioProcessor::getTailLengthSeconds() const
{
    return 120.0;
}

int DeepPluckAudioProcessor::getNumPrograms()
{
    return 1;
}

int DeepPluckAudioProcessor::getCurrentProgram()
{
    return 0;
}

void DeepPluckAudioProcessor::setCurrentProgram (int index)
{
    juce::ignoreUnused (index);
}

const juce::String DeepPluckAudioProcessor::getProgramName (int index)
{
    juce::ignoreUnused (index);
    return {};
}

void DeepPluckAudioProcessor::changeProgramName (int index, const juce::String& newName)
{
    juce::ignoreUnused (index, newName);
}

void DeepPluckAudioProcessor::getStateInformation (juce::MemoryBlock& destinationData)
{
    if (const auto stateXml = valueTreeState.copyState().createXml())
        copyXmlToBinary (*stateXml, destinationData);
}

void DeepPluckAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    if (data == nullptr || sizeInBytes <= 0)
        return;

    const auto stateXml = getXmlFromBinary (data, sizeInBytes);

    if (stateXml == nullptr)
        return;

    const auto restoredState = juce::ValueTree::fromXml (*stateXml);

    const auto mergedState = deeppluck::mergeRestoredState (valueTreeState.copyState(), restoredState);

    if (mergedState.isValid())
    {
        valueTreeState.replaceState (mergedState);
        clearLoadBoundaryState();
    }
}

juce::AudioProcessorValueTreeState& DeepPluckAudioProcessor::getValueTreeState() noexcept
{
    return valueTreeState;
}

const juce::AudioProcessorValueTreeState& DeepPluckAudioProcessor::getValueTreeState() const noexcept
{
    return valueTreeState;
}

deeppluck::DeepPluckRandomization& DeepPluckAudioProcessor::getRandomization() noexcept
{
    return randomization;
}

const deeppluck::DeepPluckRandomization& DeepPluckAudioProcessor::getRandomization() const noexcept
{
    return randomization;
}

deeppluck::DeepPluckPresetManager& DeepPluckAudioProcessor::getPresetManager() noexcept
{
    return presetManager;
}

const deeppluck::DeepPluckPresetManager& DeepPluckAudioProcessor::getPresetManager() const noexcept
{
    return presetManager;
}

DeepPluckAudioProcessor::PreferredPreset
DeepPluckAudioProcessor::currentPreferenceForUndo() const
{
    if (preferredPreset.valid
        && deeppluck::parameterValuesMatch (valueTreeState, preferredPreset.fingerprint))
        return preferredPreset;

    for (const auto& entry : factoryLibrary.entries)
        if (deeppluck::parameterValuesMatch (valueTreeState, entry.values))
            return factoryContentPreference (entry);

    const auto factoryIndex = deeppluck::findMatchingFactoryPreset (valueTreeState);
    if (factoryIndex >= 0)
    {
        PreferredPreset result;
        result.valid = true;
        result.display.stableId = "factory:" + juce::String (factoryIndex);
        result.display.name = deeppluck::factoryPresets[static_cast<std::size_t> (factoryIndex)].name;
        result.display.custom = false;
        result.fingerprint = deeppluck::captureParameterValues (valueTreeState);
        return result;
    }
    return {};
}

bool DeepPluckAudioProcessor::randomizeAll()
{
    const auto previous = currentPreferenceForUndo();
    if (! randomization.randomizeAll())
        return false;
    undoPreferredPreset = previous;
    return true;
}

bool DeepPluckAudioProcessor::randomizeModule (deeppluck::RandomizationModule module)
{
    const auto previous = currentPreferenceForUndo();
    if (! randomization.randomizeModule (module))
        return false;
    undoPreferredPreset = previous;
    return true;
}

bool DeepPluckAudioProcessor::undoRandomization()
{
    if (! randomization.undo())
        return false;
    preferredPreset = undoPreferredPreset.valid
                          && deeppluck::parameterValuesMatch (
                              valueTreeState, undoPreferredPreset.fingerprint)
                          ? undoPreferredPreset : PreferredPreset {};
    undoPreferredPreset = {};
    return true;
}

void DeepPluckAudioProcessor::toggleRandomizationLock (
    deeppluck::RandomizationModule module) noexcept
{
    randomization.toggleModuleLocked (module);
}

DeepPluckAudioProcessor::PresetDisplayState
DeepPluckAudioProcessor::getPresetDisplayState() const
{
    if (preferredPreset.valid
        && deeppluck::parameterValuesMatch (valueTreeState, preferredPreset.fingerprint))
        return preferredPreset.display;

    for (const auto& entry : factoryLibrary.entries)
        if (deeppluck::parameterValuesMatch (valueTreeState, entry.values))
            return factoryContentPreference (entry).display;

    const auto factoryIndex = deeppluck::findMatchingFactoryPreset (valueTreeState);
    if (factoryIndex >= 0)
    {
        PresetDisplayState display;
        display.stableId = "factory:" + juce::String (factoryIndex);
        display.name = deeppluck::factoryPresets[static_cast<std::size_t> (factoryIndex)].name;
        display.custom = false;
        return display;
    }
    return {};
}

deeppluck::UserPresetCatalog DeepPluckAudioProcessor::getUserPresetCatalog() const
{
    return presetManager.getCatalog();
}

const deeppluck::FactoryLibrary& DeepPluckAudioProcessor::getFactoryLibrary() const noexcept
{
    return factoryLibrary;
}

DeepPluckAudioProcessor::PreferredPreset DeepPluckAudioProcessor::factoryContentPreference (
    const deeppluck::FactoryContentEntry& entry) const
{
    PreferredPreset result;
    result.valid = true;
    result.display.stableId = entry.stableId;
    result.display.name = entry.name;
    result.display.author = entry.author;
    result.display.custom = false;
    result.fingerprint = entry.values;
    return result;
}

void DeepPluckAudioProcessor::setPreferredFactory (int presetIndex)
{
    preferredPreset = {};
    preferredPreset.valid = true;
    preferredPreset.display.stableId = "factory:" + juce::String (presetIndex);
    preferredPreset.display.name =
        deeppluck::factoryPresets[static_cast<std::size_t> (presetIndex)].name;
    preferredPreset.display.custom = false;
    preferredPreset.fingerprint = deeppluck::captureParameterValues (valueTreeState);
}

void DeepPluckAudioProcessor::setPreferredEntry (const deeppluck::UserPresetEntry& entry)
{
    preferredPreset = {};
    preferredPreset.valid = true;
    preferredPreset.display.stableId = entry.stableId;
    preferredPreset.display.name = entry.name;
    preferredPreset.display.author = entry.author;
    preferredPreset.display.file = entry.file;
    preferredPreset.display.custom = false;
    preferredPreset.display.editable = entry.editable;
    preferredPreset.fingerprint = entry.values;
}

void DeepPluckAudioProcessor::clearLoadBoundaryState()
{
    preferredPreset = {};
    undoPreferredPreset = {};
    randomization.invalidateUndo();
    effectsRack.requestReset();
}

bool DeepPluckAudioProcessor::loadFactoryPreset (int presetIndex)
{
    if (presetIndex < 0
        || presetIndex >= static_cast<int> (deeppluck::factoryPresets.size()))
        return false;
    deeppluck::applyFactoryPreset (valueTreeState, presetIndex);
    clearLoadBoundaryState();
    setPreferredFactory (presetIndex);
    return true;
}

bool DeepPluckAudioProcessor::loadPresetByStableId (const juce::String& stableId,
                                                    juce::String& error)
{
    for (const auto& entry : factoryLibrary.entries)
        if (entry.stableId == stableId)
        {
            if (! deeppluck::applyParameterValues (valueTreeState, entry.values))
            {
                error = "Factory parameters could not be applied.";
                return false;
            }
            clearLoadBoundaryState();
            preferredPreset = factoryContentPreference (entry);
            return true;
        }
    if (stableId.startsWith ("factory:"))
    {
        const auto index = stableId.fromFirstOccurrenceOf (":", false, false).getIntValue();
        if (stableId == "factory:" + juce::String (index) && loadFactoryPreset (index))
            return true;
        error = "Factory preset is not available.";
        return false;
    }

    const auto catalog = presetManager.getCatalog();
    const auto iterator = std::find_if (catalog.entries.begin(), catalog.entries.end(),
                                       [&stableId] (const auto& entry)
    {
        return entry.stableId == stableId;
    });
    if (iterator == catalog.entries.end())
    {
        error = "Preset is no longer present in the catalog.";
        return false;
    }

    deeppluck::UserPresetData data;
    if (! presetManager.loadPreset (*iterator, data, error))
        return false;
    if (! deeppluck::applyParameterValues (valueTreeState, data.values))
    {
        error = "Preset parameters could not be applied.";
        return false;
    }

    clearLoadBoundaryState();
    auto loadedEntry = *iterator;
    loadedEntry.values = data.values;
    setPreferredEntry (loadedEntry);
    return true;
}

bool DeepPluckAudioProcessor::loadRelativePreset (int direction, juce::String& error)
{
    if (direction == 0)
        return false;

    std::vector<juce::String> ids;
    ids.reserve (deeppluck::factoryPresets.size() + factoryLibrary.entries.size()
                 + presetManager.getCatalog().entries.size());
    if (factoryLibrary.awaitingCuration || factoryLibrary.entries.empty())
        for (auto index = 0; index < static_cast<int> (deeppluck::factoryPresets.size()); ++index)
            ids.push_back ("factory:" + juce::String (index));
    else
        for (const auto& entry : factoryLibrary.entries) ids.push_back (entry.stableId);
    const auto catalog = presetManager.getCatalog();
    for (const auto& entry : catalog.entries)
        ids.push_back (entry.stableId);
    if (ids.empty())
        return false;

    const auto display = getPresetDisplayState();
    const auto current = std::find (ids.begin(), ids.end(), display.stableId);
    int target = direction > 0 ? 0 : static_cast<int> (ids.size()) - 1;
    if (! display.custom && current != ids.end())
    {
        const auto index = static_cast<int> (std::distance (ids.begin(), current));
        target = (index + (direction > 0 ? 1 : -1) + static_cast<int> (ids.size()))
               % static_cast<int> (ids.size());
    }
    return loadPresetByStableId (ids[static_cast<std::size_t> (target)], error);
}

deeppluck::PresetSaveResult DeepPluckAudioProcessor::saveUserPreset (
    const juce::String& name,
    const juce::String& author,
    bool allowOverwrite,
    bool overwriteCurrentUser,
    juce::String& error)
{
    juce::File explicitTarget;
    if (overwriteCurrentUser)
    {
        const auto display = getPresetDisplayState();
        if (! display.editable || display.file == juce::File {})
        {
            error = "The current sound is not an editable User preset.";
            return deeppluck::PresetSaveResult::failed;
        }
        explicitTarget = display.file;
    }

    deeppluck::UserPresetEntry saved;
    const auto result = presetManager.savePreset (
        valueTreeState, name, author, {}, allowOverwrite, saved, error, explicitTarget);
    if (result == deeppluck::PresetSaveResult::saved)
        setPreferredEntry (saved);
    return result;
}

bool DeepPluckAudioProcessor::deleteCurrentUserPreset (juce::String& error)
{
    const auto display = getPresetDisplayState();
    if (! display.editable || display.file == juce::File {})
    {
        error = "The current preset is not editable.";
        return false;
    }
    if (! presetManager.deleteUserPreset (display.file, error))
        return false;
    preferredPreset = {};
    return true;
}

void DeepPluckAudioProcessor::ensureInitialPresetScan()
{
    if (! presetFileIoEnabled || initialPresetScanRequested)
        return;
    initialPresetScanRequested = true;
    presetManager.requestScan();
}

void DeepPluckAudioProcessor::refreshUserPresets()
{
    if (presetFileIoEnabled)
        presetManager.requestScan();
}

bool DeepPluckAudioProcessor::revealPresetRoot (juce::String& error)
{
    return presetFileIoEnabled && presetManager.revealRootToUser (error);
}

bool DeepPluckAudioProcessor::isPresetFileIoEnabled() const noexcept
{
    return presetFileIoEnabled;
}

juce::AudioProcessorValueTreeState::ParameterLayout DeepPluckAudioProcessor::createParameterLayout()
{
    return deeppluck::createParameterLayout();
}

juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
    return new DeepPluckAudioProcessor ({}, true);
}
