// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Andrea De Murtas

#include "PluginEditor.h"
#include "Presets.h"

namespace
{
constexpr int baseW = 900;
constexpr int baseH = 360;

// Every control carries a tooltip; the map is keyed by parameter ID. The pedal
// knobs get one line per voicing, because they genuinely mean three different
// things.
const char* tipFor(const juce::String& id)
{
    static const std::map<juce::String, const char*> tips = {
        { pid::inTrim,     "Level into the chain, before the gate sees anything" },
        { pid::gateOn,     "Noise gate on the clean signal, ahead of every gain stage" },
        { pid::gateThresh, "The gate opens above this level; hum and hiss stay below" },
        { pid::pedalOn,    "Boost pedal in front of the amp" },
        { pid::pedalMode,  "Dist: cheap 90s box. Chainsaw: HM-2. Fuzz: starved transistor" },
        { pid::pedalDrive, "Dist/Fuzz: clipping gain. Chainsaw: how hard it squares off" },
        { pid::pedalTone,  "Dist: tilt at 800 Hz. Chainsaw: upper peak 700-1500 Hz. Fuzz: low-pass 1-8 kHz" },
        { pid::pedalLevel, "Pedal output trim" },
        { pid::ampGain,    "Two solid-state stages, up to +36 dB; level is compensated" },
        { pid::tight,      "High-pass ahead of the clipping: the higher, the less flub" },
        { pid::bass,       "Low shelf at 120 Hz" },
        { pid::mid,        "Peak at 700 Hz, wide" },
        { pid::treble,     "High shelf at 3.2 kHz" },
        { pid::presence,   "High shelf at 5 kHz, after the power section" },
        { pid::master,     "Drives the power section, 0 to +12 dB. This is the volume" },
        { pid::cabMode,    "Analytic cabinet voicing, your own IR, or nothing at all" },
        { pid::frost,      "Tilt at 1 kHz: right is brighter and thinner, left is darker" },
        { pid::outTrim,    "Output level after everything" },
        { pid::quality,    "Eco is 2x oversampling, Full is 4x. Full aliases less" },
    };
    auto it = tips.find(id);
    return it != tips.end() ? it->second : nullptr;
}
} // namespace

GjallaAudioProcessorEditor::GjallaAudioProcessorEditor(GjallaAudioProcessor& p)
    : AudioProcessorEditor(&p), processorRef(p)
{
    sections.reserve(3);

    // ---- Pedal: the input stage and the box in front of the amp
    {
        auto& s = addSection("Pedal", { 10, 62, 268, 288 });
        auto& c = comboRow(s);
        addToggle(c, pid::pedalOn, "On");
        addCombo(c, pid::pedalMode, 2);

        auto& r1 = knobRow(s);
        addKnob(r1, pid::pedalDrive, "Drive");
        addKnob(r1, pid::pedalTone, "Tone");
        addKnob(r1, pid::pedalLevel, "Level");

        auto& c2 = comboRow(s);
        addToggle(c2, pid::gateOn, "Gate");

        auto& r2 = knobRow(s);
        addKnob(r2, pid::inTrim, "Input");
        addKnob(r2, pid::gateThresh, "Thresh");

        addMeter(s, processorRef.meters.inPeak, MeterBar::Style::level);
    }

    // ---- Amp: preamp, tone stack, power section
    {
        auto& s = addSection("Amp", { 286, 62, 380, 288 });
        auto& r1 = knobRow(s);
        addKnob(r1, pid::ampGain, "Gain");
        addKnob(r1, pid::tight, "Tight");
        addKnob(r1, pid::master, "Master");

        auto& r2 = knobRow(s);
        addKnob(r2, pid::bass, "Bass");
        addKnob(r2, pid::mid, "Mid");
        addKnob(r2, pid::treble, "Treble");
        addKnob(r2, pid::presence, "Presence");

        addMeter(s, processorRef.meters.gateGR, MeterBar::Style::reduction);
    }

    // ---- Cab and output
    {
        auto& s = addSection("Cab and Output", { 674, 62, 216, 288 });
        addCombo(comboRow(s), pid::cabMode);

        auto& irRow = comboRow(s);
        addPlainButton(irRow, irButton, 5);
        addPlainButton(irRow, irClearButton, 1);

        auto& r1 = knobRow(s);
        addKnob(r1, pid::frost, "Frost");
        addKnob(r1, pid::outTrim, "Output");

        addCombo(comboRow(s), pid::quality);
        addMeter(s, processorRef.meters.outPeak, MeterBar::Style::level);
    }

    irButton.setTooltip("Load a WAV or AIFF impulse response, up to 2 seconds. "
                        "Only used when the cabinet is set to User IR");
    irButton.onClick = [this] { chooseIrFile(); };
    irClearButton.setTooltip("Forget the loaded impulse response");
    irClearButton.onClick = [this]
    {
        processorRef.clearIr();
        refreshIrButton();
    };
    refreshIrButton();

    // ---- header: preset combo
    presetBox.setTextWhenNothingSelected("Presets");
    presetBox.setTooltip("Factory presets");
    {
        int id = 1;
        for (const auto& preset : gjalla::presets::all())
            presetBox.addItem(preset.name, id++);
    }
    presetBox.onChange = [this]
    {
        const auto index = presetBox.getSelectedId() - 1;
        if (index >= 0)
            processorRef.applyPreset(index);
    };
    addAndMakeVisible(presetBox);

    // Must come after the children exist: sliders snapshot their text-box
    // colours when notified of a look-and-feel change.
    setLookAndFeel(&lnf);

    setResizable(true, true);
    getConstrainer()->setFixedAspectRatio((double) baseW / (double) baseH);
    setResizeLimits(baseW * 3 / 4, baseH * 3 / 4, baseW * 2, baseH * 2);
    setSize(baseW, baseH);

    setWantsKeyboardFocus(true);
    startTimerHz(4);
}

GjallaAudioProcessorEditor::~GjallaAudioProcessorEditor()
{
    setLookAndFeel(nullptr);
}

void GjallaAudioProcessorEditor::timerCallback()
{
    // Group the parameter edits since the last tick into one undoable step.
    processorRef.undoManager.beginNewTransaction();

    // Keep the combo honest: cleared while edited away from a preset, and
    // pointed at the right factory entry after a session restore.
    const auto name = processorRef.apvts.state.getProperty(prop::presetName).toString();
    if (name.isEmpty() || processorRef.presetDirty.load())
    {
        presetBox.setSelectedId(0, juce::dontSendNotification);
    }
    else
    {
        const auto& list = gjalla::presets::all();
        for (int i = 0; i < (int) list.size(); ++i)
            if (name == list[(size_t) i].name)
            {
                if (presetBox.getSelectedId() != i + 1)
                    presetBox.setSelectedId(i + 1, juce::dontSendNotification);
                break;
            }
    }

    refreshIrButton();
}

bool GjallaAudioProcessorEditor::keyPressed(const juce::KeyPress& key)
{
    const auto cmd = juce::ModifierKeys::commandModifier;
    if (key == juce::KeyPress('z', cmd, 0))
        return processorRef.undoManager.undo();
    if (key == juce::KeyPress('z', cmd | juce::ModifierKeys::shiftModifier, 0)
        || key == juce::KeyPress('y', cmd, 0))
        return processorRef.undoManager.redo();
    return false;
}

void GjallaAudioProcessorEditor::refreshIrButton()
{
    const auto file = processorRef.getIrFile();
    const auto text = file == juce::File() ? juce::String("Load IR...")
                                           : file.getFileNameWithoutExtension();
    if (irButton.getButtonText() != text)
        irButton.setButtonText(text);
}

void GjallaAudioProcessorEditor::chooseIrFile()
{
    fileChooser = std::make_unique<juce::FileChooser>("Choose an impulse response",
                                                      juce::File(), "*.wav;*.aiff;*.aif");
    const auto chooserFlags = juce::FileBrowserComponent::openMode
                              | juce::FileBrowserComponent::canSelectFiles;

    fileChooser->launchAsync(chooserFlags, [this](const juce::FileChooser& chooser)
    {
        const auto file = chooser.getResult();
        if (file == juce::File())
            return;

        processorRef.loadIrFile(file);
        refreshIrButton();

        // Choosing a file is a clear statement of intent: switch the cabinet
        // over rather than leaving the user wondering why nothing changed.
        if (auto* param = processorRef.apvts.getParameter(pid::cabMode))
        {
            param->beginChangeGesture();
            param->setValueNotifyingHost(
                param->convertTo0to1((float) (int) choices::CabMode::ir));
            param->endChangeGesture();
        }
    });
}

void GjallaAudioProcessorEditor::showParamMenu(GjallaSlider& slider, const juce::String& paramID)
{
    auto* param = processorRef.apvts.getParameter(paramID);
    if (param == nullptr)
        return;

    juce::PopupMenu menu;
    menu.setLookAndFeel(&lnf);
    menu.addItem(1, "Reset to default");
    if (slider.getTextBoxPosition() != juce::Slider::NoTextBox)
        menu.addItem(2, "Enter value...");

    juce::Component::SafePointer<juce::Slider> safeSlider(&slider);
    menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(slider),
        [safeSlider, param](int result)
        {
            switch (result)
            {
                case 1:
                    param->beginChangeGesture();
                    param->setValueNotifyingHost(param->getDefaultValue());
                    param->endChangeGesture();
                    break;
                case 2:
                    if (safeSlider != nullptr)
                        safeSlider->showTextBox();
                    break;
                default: break;
            }
        });
}

GjallaAudioProcessorEditor::Section&
GjallaAudioProcessorEditor::addSection(const juce::String& title, juce::Rectangle<int> baseBounds)
{
    auto& s = sections.emplace_back();
    s.title = title;
    s.baseBounds = baseBounds;
    s.bounds = baseBounds;
    return s;
}

GjallaAudioProcessorEditor::Row& GjallaAudioProcessorEditor::comboRow(Section& s)
{
    auto& r = s.rows.emplace_back();
    r.tall = false;
    return r;
}

GjallaAudioProcessorEditor::Row& GjallaAudioProcessorEditor::knobRow(Section& s)
{
    auto& r = s.rows.emplace_back();
    r.tall = true;
    return r;
}

void GjallaAudioProcessorEditor::addKnob(Row& row, const char* paramID, const juce::String& name)
{
    auto* k = knobs.add(new Knob());
    k->slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag);
    k->slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 62, 14);
    k->slider.onRightClick = [this, k, id = juce::String(paramID)] { showParamMenu(k->slider, id); };
    if (auto* tip = tipFor(paramID))
        k->slider.setTooltip(tip);
    addAndMakeVisible(k->slider);

    k->label.setText(name, juce::dontSendNotification);
    k->label.setJustificationType(juce::Justification::centred);
    addAndMakeVisible(k->label);

    sliderAttachments.push_back(
        std::make_unique<SliderAttachment>(processorRef.apvts, paramID, k->slider));
    row.comps.push_back(&k->slider);
    row.labels.push_back(&k->label);
    row.weights.push_back(1);
}

void GjallaAudioProcessorEditor::addCombo(Row& row, const char* paramID, int weight)
{
    auto* c = combos.add(new juce::ComboBox());
    if (auto* choice = dynamic_cast<juce::AudioParameterChoice*>(
            processorRef.apvts.getParameter(paramID)))
        c->addItemList(choice->choices, 1);
    if (auto* tip = tipFor(paramID))
        c->setTooltip(tip);
    addAndMakeVisible(c);

    comboAttachments.push_back(
        std::make_unique<ComboBoxAttachment>(processorRef.apvts, paramID, *c));
    row.comps.push_back(c);
    row.labels.push_back(nullptr);
    row.weights.push_back(weight);
}

void GjallaAudioProcessorEditor::addToggle(Row& row, const char* paramID,
                                           const juce::String& name, int weight)
{
    auto* t = toggles.add(new juce::TextButton(name));
    t->setClickingTogglesState(true);
    t->setColour(juce::TextButton::buttonOnColourId, theme::blood.withAlpha(0.6f));
    if (auto* tip = tipFor(paramID))
        t->setTooltip(tip);
    addAndMakeVisible(t);

    buttonAttachments.push_back(
        std::make_unique<ButtonAttachment>(processorRef.apvts, paramID, *t));
    row.comps.push_back(t);
    row.labels.push_back(nullptr);
    row.weights.push_back(weight);
}

// A button with no parameter behind it: the IR loader and its clear button.
void GjallaAudioProcessorEditor::addPlainButton(Row& row, juce::Button& button, int weight)
{
    addAndMakeVisible(button);
    row.comps.push_back(&button);
    row.labels.push_back(nullptr);
    row.weights.push_back(weight);
}

void GjallaAudioProcessorEditor::addMeter(Section& s, std::atomic<float>& source,
                                          MeterBar::Style style)
{
    auto* m = meterBars.add(new MeterBar(source, style));
    addAndMakeVisible(m);
    s.meter = m;
}

void GjallaAudioProcessorEditor::layoutSection(Section& s, float scale)
{
    auto sc = [scale](int v) { return juce::roundToInt((float) v * scale); };

    auto body = s.bounds;
    if (s.meter != nullptr)
        s.meter->setBounds(body.removeFromRight(sc(14)).withTrimmedTop(sc(22)).reduced(1, sc(4)));

    int y = body.getY() + sc(20);
    for (auto& row : s.rows)
    {
        const int h = row.tall ? sc(94) : sc(26);
        const int n = (int) row.comps.size();
        if (n == 0)
            continue;

        int totalWeight = 0;
        for (auto w : row.weights)
            totalWeight += w;

        const int available = body.getWidth() - sc(12);
        int x = body.getX() + sc(6);

        for (int i = 0; i < n; ++i)
        {
            const int cw = available * row.weights[(size_t) i] / juce::jmax(1, totalWeight);
            juce::Rectangle<int> cell(x, y, cw, h);
            x += cw;

            if (row.tall)
            {
                row.labels[(size_t) i]->setBounds(cell.removeFromTop(sc(16)));
                if (auto* slider = dynamic_cast<juce::Slider*>(row.comps[(size_t) i]))
                    slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, sc(62), sc(14));
                row.comps[(size_t) i]->setBounds(cell.reduced(1, 0));
            }
            else
            {
                row.comps[(size_t) i]->setBounds(cell.reduced(sc(3), 2));
            }
        }
        y += h + 2;
    }
}

void GjallaAudioProcessorEditor::resized()
{
    const float scale = (float) getWidth() / (float) baseW;
    lnf.uiScale = scale;
    auto sc = [scale](int v) { return juce::roundToInt((float) v * scale); };

    for (auto& s : sections)
    {
        s.bounds = { sc(s.baseBounds.getX()), sc(s.baseBounds.getY()),
                     sc(s.baseBounds.getWidth()), sc(s.baseBounds.getHeight()) };
        layoutSection(s, scale);
    }

    presetBox.setBounds(sc(baseW - 182), sc(18), sc(170), sc(24));

    repaint();
}

void GjallaAudioProcessorEditor::paint(juce::Graphics& g)
{
    const auto w = (float) getWidth();
    const auto h = (float) getHeight();
    const float scale = w / (float) baseW;

    juce::ColourGradient sky(juce::Colour(0xff111116), 0.0f, 0.0f,
                             juce::Colour(0xff07070a), 0.0f, h, false);
    g.setGradientFill(sky);
    g.fillAll();

    drawRidge(g, 0.72f, 0.20f, 3, juce::Colour(0xff0e0e12));
    drawRidge(g, 0.86f, 0.14f, 5, juce::Colour(0xff121218));

    juce::ColourGradient vignette(juce::Colours::transparentBlack, w * 0.5f, h * 0.45f,
                                  juce::Colour(0xaa000000), 0.0f, 0.0f, true);
    g.setGradientFill(vignette);
    g.fillAll();

    // header
    auto sc = [scale](int v) { return juce::roundToInt((float) v * scale); };
    auto titleArea = juce::Rectangle<int>(sc(18), sc(6), sc(260), sc(38));
    g.setFont(lnf.getTitleFont(34.0f * scale));
    g.setColour(theme::blood.withAlpha(0.6f));
    g.drawText("Gjalla", titleArea.translated(0, sc(2)), juce::Justification::centredLeft);
    g.setColour(theme::bone);
    g.drawText("Gjalla", titleArea, juce::Justification::centredLeft);

    g.setFont(lnf.getBodyFont(13.0f * scale));
    g.setColour(theme::boneDim);
    g.drawText("to shriek, to resound: a black metal guitar amp",
               sc(20), sc(42), sc(460), sc(14), juce::Justification::centredLeft);

    // section panels
    for (const auto& s : sections)
    {
        auto b = s.bounds;
        g.setColour(theme::panel.withAlpha(0.55f));
        g.fillRect(b);
        g.setColour(theme::outline.withAlpha(0.8f));
        g.drawRect(b, 1);

        g.setFont(lnf.getBodyFont(15.0f * scale));
        g.setColour(theme::boneDim);
        g.drawText(s.title, b.getX() + sc(8), b.getY() + 2, b.getWidth() - sc(16), sc(16),
                   juce::Justification::centredLeft);
        g.setColour(theme::blood);
        g.fillRect((float) b.getX() + 8.0f * scale, (float) b.getY() + 18.0f * scale,
                   24.0f * scale, 1.5f);
    }

    // outer frame and corner brackets
    g.setColour(theme::outline);
    g.drawRect(getLocalBounds(), 2);
    g.setColour(theme::blood);
    const float m = 6.0f, len = 16.0f, t = 2.0f;
    g.fillRect(m, m, len, t);                   g.fillRect(m, m, t, len);
    g.fillRect(w - m - len, m, len, t);         g.fillRect(w - m - t, m, t, len);
    g.fillRect(m, h - m - t, len, t);           g.fillRect(m, h - m - len, t, len);
    g.fillRect(w - m - len, h - m - t, len, t); g.fillRect(w - m - t, h - m - len, t, len);
}

void GjallaAudioProcessorEditor::drawRidge(juce::Graphics& g, float baseY, float amplitude,
                                           int seedStep, juce::Colour colour) const
{
    static const float peaks[] = { 0.55f, 0.95f, 0.30f, 0.75f, 0.15f, 0.85f,
                                   0.45f, 0.65f, 0.25f, 0.90f, 0.40f };
    constexpr int numPoints = 16;
    const auto w = (float) getWidth();
    const auto h = (float) getHeight();

    juce::Path ridge;
    ridge.startNewSubPath(0.0f, h);
    for (int i = 0; i < numPoints; ++i)
    {
        auto xPos = w * (float) i / (float) (numPoints - 1);
        auto peak = peaks[(i * seedStep) % 11];
        ridge.lineTo(xPos, h * baseY - h * amplitude * peak);
    }
    ridge.lineTo(w, h);
    ridge.closeSubPath();

    g.setColour(colour);
    g.fillPath(ridge);
}
