// SPDX-FileCopyrightText: 2026 0064ff
// SPDX-License-Identifier: MIT-0

#include "PluginEditor.h"

//==============================================================================

int const smallSliderWidth = 50;
int const smallSliderHeight = 50;
int const smallSliderBottomPadding = 60;

int const mediumSliderWidth = 300;
int const mediumSliderHeight = 20;
int const mediumSliderBottomPadding = 30;

int const largeSliderWidth = 40;
int const largeSliderHeight = 600;
int const largeSliderBottomPadding = 0;

int const sliderLeftPadding = 20;
int const sliderLeftPaddingNewSection = 60;

int const sliderBottomPaddingNewSubSection = 20;

char const font[] = "Arial";
int const fontSize = 12;
juce::Font::FontStyleFlags const fontStyle = juce::Font::FontStyleFlags::plain;

int const bgPadding = 60;
int const bgHeight = largeSliderHeight + 2 * bgPadding;

char const* bgNoiseImage = BinaryData::noise_png;
int const bgNoiseSize = BinaryData::noise_pngSize;

struct ColorPalette
{
    juce::Colour bgColor, textColor, textColorHighlighted, highlightColor;
    juce::Colour sliderColor, sliderThumbColor, sliderThumbColorInv;
};

static ColorPalette const normalColorPalette = {
    juce::Colour(0, 100, 255), juce::Colour(255, 255, 255), juce::Colour(0, 0, 0), juce::Colour(255, 255, 0),
    juce::Colour(255, 255, 255), juce::Colour(0, 100, 255), juce::Colour(255, 155, 0)
};

static ColorPalette const liveModeColorPalette = {
    juce::Colour(0, 0, 0), juce::Colour(255, 255, 255), juce::Colour(0, 0, 0), juce::Colour(0, 255, 255),
    juce::Colour(30, 30, 30), juce::Colour(255, 0, 100), juce::Colour(0, 255, 155)
};

juce::Colour const transparentColor = juce::Colours::transparentBlack;

//==============================================================================

ChannelAudioProcessorEditor::ChannelAudioProcessorEditor(ChannelAudioProcessor& p) :
    AudioProcessorEditor(&p), ap(p)
{
    // load image of noise in for overlay
    noiseImage = juce::ImageCache::getFromMemory(bgNoiseImage, bgNoiseSize);

    setLookAndFeel(&lookAndFeel);

    buildLayout();

    // add parameter listeners
    for (auto* parameter : ap.getParameters())
        if (auto* param = dynamic_cast<juce::RangedAudioParameter*>(parameter))
            ap.getAPVTS().addParameterListener(param->getParameterID(), this);

    // prevent plugin from intercepting keyboard as midi
    setWantsKeyboardFocus(false);
}

//==============================================================================

ChannelAudioProcessorEditor::~ChannelAudioProcessorEditor()
{
    setLookAndFeel(nullptr);

    // remove parameter listeners
    for (auto* parameter : ap.getParameters())
        if (auto* p = dynamic_cast<juce::RangedAudioParameter*>(parameter))
            ap.getAPVTS().removeParameterListener(p->getParameterID(), this);
}

//==============================================================================

void ChannelAudioProcessorEditor::buildLayout()
{
    // remove any existing sliders from the interface
    for (auto* slider : builtSliders)
    {
        slider->attachment.reset();
        slider->strip.reset();
        slider->label.reset();
        slider->label2.reset();
    }
    builtSliders.clear();

    // initialize plugin dimensions and draw position
    xpos = bgPadding;
    ypos = bgPadding;
    backgroundWidth = bgPadding * 2;
    lastSliderType = SliderType::None;

    // set colors
    setColorPalette();

    // create full interface if not in live mode
    if (!static_cast<bool>(ap.getParameterValueByID(ParamIDs::liveMode)))
    {
        // effects loop section
        createSlider(SliderType::Large, fxLoopMix, ParamIDs::fxLoopMix);
        createSlider(SliderType::Small, fxLoopSendDB, ParamIDs::fxLoopSendDB, true);
        createSlider(SliderType::Small, fxLoopReturnDB, ParamIDs::fxLoopReturnDB, true);
        createSlider(SliderType::Small, fxLoopPosition, ParamIDs::fxLoopPosition);

        // stereo section
        createSlider(SliderType::Small, width, ParamIDs::width, false, true);
        createSlider(SliderType::Small, pan, ParamIDs::pan, true);
        createSlider(SliderType::Small, stereoOrder, ParamIDs::stereoOrder);

        // filter and eq section
        createSlider(SliderType::Large, hpFreq, ParamIDs::hpFreq, false, true);
        createSlider(SliderType::Large, lpFreq, ParamIDs::lpFreq);
        createSlider(SliderType::Large, eqFreq, ParamIDs::eqFreq);
        createSlider(SliderType::Small, eqPeakDB, ParamIDs::eqPeakDB, true);
        createSlider(SliderType::Small, eqTiltDB, ParamIDs::eqTiltDB, true);
        createSlider(SliderType::Small, eqPosition, ParamIDs::eqPosition, true);
        createSlider(SliderType::Small, eqQ, ParamIDs::eqQ);
        createSlider(SliderType::Small, hpQ, ParamIDs::hpQ);
        createSlider(SliderType::Small, lpQ, ParamIDs::lpQ);

        // dynamics large and medium sliders
        createSlider(SliderType::Large, dynDriveDB, ParamIDs::dynDriveDB, false, true);
        createSlider(SliderType::Large, dynThreshold, ParamIDs::dynThreshold);
        createSlider(SliderType::Medium, dynAttackTime, ParamIDs::dynAttackTime);
        createSlider(SliderType::Medium, dynReleaseTime, ParamIDs::dynReleaseTime);
        createSlider(SliderType::Medium, dynCompensation, ParamIDs::dynCompensation);
        createSlider(SliderType::Medium, dynAdaptability, ParamIDs::dynAdaptability);
        createSlider(SliderType::Medium, dynCrossover1, ParamIDs::dynCrossover1, false, false, true);
        createSlider(SliderType::Medium, dynCrossover2, ParamIDs::dynCrossover2);
        createSlider(SliderType::Medium, dynLowCompPower, ParamIDs::dynLowCompPower, false, false, true);
        createSlider(SliderType::Medium, dynHighCompPower, ParamIDs::dynHighCompPower);
        createSlider(SliderType::Medium, dynLowCompOffset, ParamIDs::dynLowCompOffset, false, false, true);
        createSlider(SliderType::Medium, dynHighCompOffset, ParamIDs::dynHighCompOffset);
        createSlider(SliderType::Medium, dynResponsePower, ParamIDs::dynResponsePower, false, false, true);

        // dynamics first column of small sliders
        createSlider(SliderType::Small, dynTransientTime, ParamIDs::dynTransientTime);
        createSlider(SliderType::Small, dynThreshold2, ParamIDs::dynThreshold2);
        createSlider(SliderType::Small, dynRatio, ParamIDs::dynRatio);
        createSlider(SliderType::Small, dynSmoothTime, ParamIDs::dynSmoothTime);
        createSlider(SliderType::Small, dynKneeDB, ParamIDs::dynKneeDB);
        createSlider(SliderType::Small, dynMakeupDB, ParamIDs::dynMakeupDB, true);

        // dynamics second column of small sliders
        createSlider(SliderType::Small, dynLowerThreshold, ParamIDs::dynLowerThreshold, false, false, true);
        createSlider(SliderType::Small, dynNegMix, ParamIDs::dynNegMix);
        createSlider(SliderType::Small, dynVariMuMult, ParamIDs::dynVariMuMult);
        createSlider(SliderType::Small, dynSagTime, ParamIDs::dynSagTime);
        createSlider(SliderType::Small, dynSlew, ParamIDs::dynSlew);
        createSlider(SliderType::Small, dynMix, ParamIDs::dynMix);

        // master section
        createSlider(SliderType::Small, filterAndDynByp, ParamIDs::filterAndDynByp, false, true);
        createSlider(SliderType::Small, outputDB, ParamIDs::outputDB, true);
        createSlider(SliderType::Small, polarity, ParamIDs::polarity);
        createSlider(SliderType::Small, dynBandLink, ParamIDs::dynBandLink);
        createSlider(SliderType::Small, dynStereoLink, ParamIDs::dynStereoLink);
        createSlider(SliderType::Small, liveMode, ParamIDs::liveMode);
        createSlider(SliderType::Large, volumeLinear, ParamIDs::volumeLinear);

        //    createSlider(SliderType::Small, dynTestValue1, ParamIDs::dynTestValue1);
        //    createSlider(SliderType::Small, dynTestValue2, ParamIDs::dynTestValue2);
        //    createSlider(SliderType::Small, dynTestValue3, ParamIDs::dynTestValue3);
    }
    else // in live mode, create a subset of the interface
    {
        createSlider(SliderType::Large, fxLoopMix, ParamIDs::fxLoopMix);
        createSlider(SliderType::Large, hpFreq, ParamIDs::hpFreq);
        createSlider(SliderType::Large, lpFreq, ParamIDs::lpFreq);
        createSlider(SliderType::Large, volumeLinear, ParamIDs::volumeLinear);
        createSlider(SliderType::Small, liveMode, ParamIDs::liveMode);
        createSlider(SliderType::Small, eqFreq, ParamIDs::eqFreq);
        createSlider(SliderType::Small, eqPeakDB, ParamIDs::eqPeakDB, true);
        createSlider(SliderType::Small, eqTiltDB, ParamIDs::eqTiltDB, true);
        createSlider(SliderType::Small, hpQ, ParamIDs::hpQ);
        createSlider(SliderType::Small, lpQ, ParamIDs::lpQ);
    }

    // initialize text for all built slider labels
    updateBuiltSliderLabels();

    // set final background size
    setSize(backgroundWidth, bgHeight);
}

//==============================================================================

void ChannelAudioProcessorEditor::createSlider(SliderType sliderType, SliderStruct& slider,
                                               juce::String parameterID, bool isBipolar,
                                               bool newSection, bool newSubSection)
{
    juce::String parameterName = ap.getAPVTS().getParameter(parameterID)->getName(99);

    slider.strip = std::make_unique<juce::Slider>();
    slider.label = std::make_unique<juce::Label>();
    slider.label2 = std::make_unique<juce::Label>();
    slider.labelText = juce::String(parameterName);
    slider.parameterID = juce::String(parameterID);
    slider.attachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>
        (ap.getAPVTS(), slider.parameterID, *slider.strip.get());

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

    // determine slider dimensions based on type
    int sliderWidth=0, sliderHeight=0, sliderBottomPadding=0;
    if (sliderType == SliderType::Small)
    {
        sliderWidth = smallSliderWidth;
        sliderHeight = smallSliderHeight;
        sliderBottomPadding = smallSliderBottomPadding;
    }
    else if (sliderType == SliderType::Medium)
    {
        sliderWidth = mediumSliderWidth;
        sliderHeight = mediumSliderHeight;
        sliderBottomPadding = mediumSliderBottomPadding;
    }
    else if (sliderType == SliderType::Large)
    {
        sliderWidth = largeSliderWidth;
        sliderHeight = largeSliderHeight;
        sliderBottomPadding = largeSliderBottomPadding;
    }

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

    // determine dimensions of last slider for potential spacing calculations
    int lastSliderWidth = 0;
    if (lastSliderType == SliderType::Small)
        lastSliderWidth = smallSliderWidth;
    else if (lastSliderType == SliderType::Medium)
        lastSliderWidth = mediumSliderWidth;
    else if (lastSliderType == SliderType::Large)
        lastSliderWidth = largeSliderWidth;

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

    // automatically create new columns for large sliders, sliders of different types,
    // and for explicit new sections
    if (sliderType != lastSliderType || sliderType == SliderType::Large || newSection)
    {
        xpos += lastSliderWidth + sliderLeftPadding;
        ypos = bgPadding;
        backgroundWidth += sliderLeftPadding + sliderWidth;

        if (lastSliderType == SliderType::None) // first element added to the interface is a special case
        {
            xpos = bgPadding;
            backgroundWidth -= sliderLeftPadding;
        }
        else if (newSection) // if new section, add new section left padding
        {
            xpos -= sliderLeftPadding;
            xpos += sliderLeftPaddingNewSection;
            backgroundWidth -= sliderLeftPadding;
            backgroundWidth += sliderLeftPaddingNewSection;
        }
    }
    else // non-large sliders of the same type should be placed in the same column unless noted otherwise
    {
        ypos += sliderHeight + sliderBottomPadding;

        // a new subsection for medium sliders means extra bottom padding
        if (newSubSection && sliderType == SliderType::Medium)
        {
            ypos += sliderBottomPaddingNewSubSection;
        }
        // a new subsection for small sliders means a new column
        else if (newSubSection && sliderType == SliderType::Small)
        {
            xpos += lastSliderWidth + sliderLeftPadding;
            ypos = bgPadding;
            backgroundWidth += sliderLeftPadding + sliderWidth;
        }
    }

    lastSliderType = sliderType;

    //--------------------------------------------------------------------------
    // strip

    bool const isBool = ap.getAPVTS().getParameter(parameterID)->isBoolean();

    slider.strip->setBounds(xpos, ypos, sliderWidth, sliderHeight);

    if (sliderWidth > sliderHeight)
        slider.strip->setSliderStyle(juce::Slider::SliderStyle::LinearBar);
    else
        slider.strip->setSliderStyle(juce::Slider::SliderStyle::LinearBarVertical);

    // calculate slider drag distance
    float dragDistance;
    float const boolDragDistance = 20.0f; // make bools a short drag
    if (sliderWidth != sliderHeight) // bars
    {
        float const lSize = static_cast<float>(std::max(sliderWidth, sliderHeight));
        float const thumbLSizeRaw = lSize/8.0f;
        dragDistance = !isBool ? lSize - thumbLSizeRaw
                               : boolDragDistance;
    }
    else // circles
    {
        dragDistance = !isBool ? (!isBipolar ? static_cast<float>(sliderWidth)*3.0f
                                             : static_cast<float>(sliderWidth)*6.0f)
                               : boolDragDistance;
    }
    slider.strip->setMouseDragSensitivity(static_cast<int>(dragDistance));

    slider.strip->setTextBoxStyle(juce::Slider::TextEntryBoxPosition::NoTextBox, true, 0, 0);
    slider.strip->setSliderSnapsToMousePosition(false);

    if (isBool)
        slider.strip->getProperties().set("isBool", true);

    if (isBipolar)
        slider.strip->getProperties().set("isBipolar", true);

    //--------------------------------------------------------------------------
    // label (slider name)

    slider.label->setText(slider.labelText, juce::dontSendNotification);

    slider.label->setTopLeftPosition(xpos, ypos - mediumSliderHeight);
    slider.label->setSize(sliderWidth, sliderHeight);
    slider.label->setFont(juce::FontOptions(font, fontSize, fontStyle));
    slider.label->setJustificationType(juce::Justification::topLeft);
    slider.label->setColour(juce::Label::ColourIds::textColourId, textColor);
    slider.label->setInterceptsMouseClicks(false, false);

    if (sliderType == SliderType::Small || sliderType == SliderType::Large)
    {
        slider.label->setJustificationType(juce::Justification::centredTop);
    }
    if (sliderType == SliderType::Medium)
    {
        slider.label->setSize(static_cast<int>(static_cast<float>(sliderWidth)/2.0f),
                              static_cast<int>(static_cast<float>(sliderHeight)*1.5f));
    }

    //--------------------------------------------------------------------------
    // label 2 (slider value)

    slider.label2->setText("ERROR", juce::dontSendNotification);

    slider.label2->setTopLeftPosition(xpos, ypos - mediumSliderHeight);
    slider.label2->setSize(sliderWidth, sliderHeight);
    slider.label2->setFont(juce::FontOptions(font, fontSize, fontStyle));
    slider.label2->setJustificationType(juce::Justification::topRight);
    slider.label2->setColour(juce::Label::ColourIds::textColourId, textColor);
    slider.label2->setInterceptsMouseClicks(false, false);

    if (sliderType == SliderType::Small || sliderType == SliderType::Large)
    {
        slider.label2->setTopLeftPosition(xpos, ypos + mediumSliderHeight);
        slider.label2->setJustificationType(juce::Justification::centredBottom);
    }

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

    // add sliders to interface and make them visible
    addAndMakeVisible(slider.label.get());
    addAndMakeVisible(slider.label2.get());
    addAndMakeVisible(slider.strip.get());

    // register slider as built
    builtSliders.push_back(&slider);
}

//==============================================================================

void ChannelAudioProcessorEditor::parameterChanged(juce::String const& parameterID, float /*newValue*/)
{
    if (parameterID == ParamIDs::liveMode)
        rebuildPending = true;

    triggerAsyncUpdate();
}

void ChannelAudioProcessorEditor::handleAsyncUpdate()
{
    if (rebuildPending)
    {
        rebuildPending = false;
        buildLayout();
    }
    else
        updateBuiltSliderLabels();
}

void ChannelAudioProcessorEditor::updateBuiltSliderLabels()
{
    // update all built sliders at once rather than just the changed slider
    // because some slider values depend on other slider values
    for (auto* slider : builtSliders)
        setSliderLabelText(slider, ap.getParameterAsText(slider->parameterID));
}

void ChannelAudioProcessorEditor::setSliderLabelText(SliderStruct* slider, juce::String labelText)
{
    if (!slider->label)
        return;

    juce::String parameterID = slider->parameterID;

    slider->label2->setText(labelText, juce::dontSendNotification);

    auto currentValue = ap.getAPVTS().getParameter(parameterID)->getValue();
    auto defaultValue = ap.getAPVTS().getParameter(parameterID)->getDefaultValue();

    // if slider value is different from default, highlight the slider's label
    if (std::abs(currentValue - defaultValue) < 1e-6f)
    {
        slider->label->setColour(juce::Label::ColourIds::textColourId, textColor);
        slider->label->setColour(juce::Label::ColourIds::backgroundColourId, transparentColor);
    }
    else
    {
        slider->label->setColour(juce::Label::ColourIds::textColourId, textColorHighlighted);
        slider->label->setColour(juce::Label::ColourIds::backgroundColourId, highlightColor);
    }
}

void ChannelAudioProcessorEditor::setColorPalette()
{
    ColorPalette const& colorPalette = !static_cast<bool>(ap.getParameterValueByID(ParamIDs::liveMode))
        ? normalColorPalette
        : liveModeColorPalette;

    bgColor = colorPalette.bgColor;
    textColor = colorPalette.textColor;
    textColorHighlighted = colorPalette.textColorHighlighted;
    highlightColor = colorPalette.highlightColor;

    lookAndFeel.sliderColor = colorPalette.sliderColor;
    lookAndFeel.sliderThumbColor = colorPalette.sliderThumbColor;
    lookAndFeel.sliderThumbColorInv = colorPalette.sliderThumbColorInv;
}

//==============================================================================

void ChannelAudioProcessorEditor::paint(juce::Graphics& g)
{
    g.fillAll(bgColor);
}

//==============================================================================

void ChannelAudioProcessorEditor::paintOverChildren(juce::Graphics& g)
{
    // the overlay image needs to be transformed to compensate for scaling
    float scale = g.getInternalContext().getPhysicalPixelScaleFactor();
    auto affineTransform = juce::AffineTransform::scale(1.0f/scale);
    juce::FillType fillType(noiseImage, affineTransform);

    // fill interface with overlay image
    g.setFillType(fillType);
    g.fillAll();

    juce::String const projectName = juce::String(ProjectInfo::projectName);
    juce::String const projectVer = juce::String(ProjectInfo::versionString);
    int const textPadding = 5;

    // place project name and version number at bottom-right corner
    g.setFont(juce::FontOptions(font, fontSize, fontStyle));
    g.setColour(textColor);
    g.drawText(projectName + " build " + projectVer,
               -textPadding, -textPadding,
               getWidth(), getHeight(),
               juce::Justification::bottomRight);
}
