/*
  ==============================================================================

    PluginProcessor.cpp
    Created: 5 Sep 2023
    Author:  binya

  ==============================================================================
*/

#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "BuildInfo.h"

JustaSampleAudioProcessorEditor::JustaSampleAudioProcessorEditor(JustaSampleAudioProcessor& audioProcessor)
    : AudioProcessorEditor(&audioProcessor), p(audioProcessor), pluginState(p.getPluginState()),
    dummyParam(p.APVTS(), PluginParameters::State::UI_DUMMY_PARAM), 
    synthVoices(p.getSamplerVoices()),

    // Modules
    tuningLabel("", "Tuning"),
    attackLabel("", "Attack"),
    releaseLabel("", "Release"),
    playbackLabel("", "Playback"),
    loopingLabel("", "Loop"),
    masterLabel("", "Master"),

    // Tuning module
    semitoneRotaryAttachment(p.APVTS(), PluginParameters::SEMITONE_TUNING, semitoneRotary),
    centRotaryAttachment(p.APVTS(), PluginParameters::CENT_TUNING, centRotary),
    waveformSemitoneRotaryAttachment(p.APVTS(), PluginParameters::WAVEFORM_SEMITONE_TUNING, waveformSemitoneRotary),
    waveformCentRotaryAttachment(p.APVTS(), PluginParameters::WAVEFORM_CENT_TUNING, waveformCentRotary),

    tuningDetectLabel("", "Detect"),
    tuningDetectButton(getOutlineFromSVG(BinaryData::IconDetect_svg)),

    // Attack and release modules
    attackTimeAttachment(p.APVTS(), PluginParameters::ATTACK, attackTimeRotary),
    attackCurveAttachment(p.APVTS(), PluginParameters::ATTACK_SHAPE, attackCurve),
    releaseTimeAttachment(p.APVTS(), PluginParameters::RELEASE, releaseTimeRotary),
    releaseCurveAttachment(p.APVTS(), PluginParameters::RELEASE_SHAPE, releaseCurve),

    // Playback module
    lofiModeAttachment(p.APVTS(), PluginParameters::SKIP_ANTIALIASING, lofiModeButton),
    playbackModeButton(p.APVTS(), PluginParameters::PLAYBACK_MODE, PluginParameters::PLAYBACK_MODE_LABELS[0], PluginParameters::PLAYBACK_MODE_LABELS[1], this),
    playbackModeAttachment(*p.APVTS().getParameter(PluginParameters::PLAYBACK_MODE), [this](float) { enablementChanged(); }, &p.getUndoManager()),
    playbackSpeedAttachment(p.APVTS(), PluginParameters::SPEED_FACTOR, playbackSpeedRotary),

    // Loop module
    loopButton(true),
    loopStartButton(true),
    loopEndButton(true),
    loopAttachment(p.APVTS(), PluginParameters::IS_LOOPING, loopButton),
    loopStartAttachment(p.APVTS(), PluginParameters::LOOPING_HAS_START, loopStartButton),
    loopEndAttachment(p.APVTS(), PluginParameters::LOOPING_HAS_END, loopEndButton),

    // Master module
    monoOutputAttachment(p.APVTS(), PluginParameters::MONO_OUTPUT, monoOutputButton),
    gainSliderAttachment(p.APVTS(), PluginParameters::SAMPLE_GAIN, gainSlider),
    
    // Sample toolbar
    filenameComponent("", {}, true, false, false, p.getWildcardFilter(), "", "Select a file to load..."),
    linkSampleToggle(true, true),
    waveformModeLabel(true, true, this),
    waveformModeLabelAttachment(*p.APVTS().getParameter(PluginParameters::DISABLE_WAVETABLE_MODE), waveformModeLabel, &p.getUndoManager()),

    playStopButton({}, this),
    deviceSettingsButton(getOutlineFromSVG(BinaryData::IconRecordSettings_svg)),
    audioDeviceSettings(p.getDeviceManager(), 0, 2, true),

    fitButton(getOutlineFromSVG(BinaryData::IconFit_svg)),
    pinButton(true, true),
    pinButtonAttachment(pinButton, pluginState.pinView, &dummyParam),

    // Main controls
    sampleEditor(p.APVTS(), p.getPluginState(), synthVoices, 
        [this](const juce::MouseWheelDetails& details, int center) -> void { sampleNavigator.scrollView(details, center, true); }),
    sampleNavigator(p.APVTS(), p.getPluginState(), synthVoices),

    fxChain(p),

    // Footer
    preFXAttachment(p.APVTS(), PluginParameters::PRE_FX, preFXButton),
    showFXButton(true, true, this),
    showFXAttachment(showFXButton, pluginState.showFX, &dummyParam),
    eqEnablementAttachment(*p.APVTS().getParameter(PluginParameters::EQ_ENABLED), [this](float newValue) { eqEnabled = bool(newValue); fxEnablementChanged(); }, & p.getUndoManager()),
    reverbEnablementAttachment(*p.APVTS().getParameter(PluginParameters::REVERB_ENABLED), [this](float newValue) { reverbEnabled = bool(newValue); fxEnablementChanged(); }, & p.getUndoManager()),
    distortionEnablementAttachment(*p.APVTS().getParameter(PluginParameters::DISTORTION_ENABLED), [this](float newValue) { distortionEnabled = bool(newValue); fxEnablementChanged(); }, &p.getUndoManager()),
    chorusEnablementAttachment(*p.APVTS().getParameter(PluginParameters::CHORUS_ENABLED), [this](float newValue) { chorusEnabled = bool(newValue); fxEnablementChanged(); }, &p.getUndoManager()),

    lnf(dynamic_cast<CustomLookAndFeel&>(getLookAndFeel()))
{
    // Set the plugin sizing
    int width = pluginState.width;
    int height = pluginState.height;

    auto* desktopSize = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
    int maxWidth = desktopSize ? desktopSize->totalArea.getWidth() : 2000;
    int maxHeight = desktopSize ? desktopSize->totalArea.getHeight() : 1600;

    int minWidth = 250;
    int minHeight = 200;

    if (minWidth > width || width > maxWidth || minHeight > height || height > maxHeight)
        setSize(1200, 600);
    else
        setSize(width, height);
    setResizable(true, true);
    setResizeLimits(minWidth, minHeight, maxWidth, maxHeight);

    // Controls toolbar
    juce::Array moduleLabels{ &tuningLabel, &tuningDetectLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel };
    for (juce::Label* label : moduleLabels)
    {
        label->setJustificationType(juce::Justification::centred);
        addAndMakeVisible(label);
    }

    std::function convertWithSecondaryUnit = [](double value) {
        if (value >= 1000)
            return juce::String(value / 1000.f, 1);
        return juce::String(juce::roundToInt(value));
    };

    semitoneRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::SEMITONE_UNIT);
    waveformSemitoneRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::SEMITONE_UNIT);
    centRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::CENT_UNIT);
    waveformCentRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::CENT_UNIT);

    tuningDetectButton.onClick = [this] { promptPitchDetection(); };
    tuningDetectButton.setHelpText("Auto-tune to 440hz (experimental)");
    addAndMakeVisible(&tuningDetectButton);

    attackTimeRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::TIME_UNIT);
    attackTimeRotary.getProperties().set(ComponentProps::ROTARY_GREATER_UNIT, PluginParameters::TIME_UNIT_LONG);
    attackTimeRotary.textFromValueFunction = convertWithSecondaryUnit;
    attackTimeRotary.updateText();

    attackCurve.setLookAndFeel(&attackCurveLNF);
    attackCurve.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag);
    attackCurve.setMouseDragSensitivity(100);
    attackCurve.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0);
    addAndMakeVisible(&attackCurve);

    releaseTimeRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::TIME_UNIT);
    releaseTimeRotary.getProperties().set(ComponentProps::ROTARY_GREATER_UNIT, PluginParameters::TIME_UNIT_LONG);
    releaseTimeRotary.textFromValueFunction = convertWithSecondaryUnit;
    releaseTimeRotary.updateText();

    releaseCurve.setLookAndFeel(&releaseCurveLNF);
    releaseCurve.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag);
    releaseCurve.setMouseDragSensitivity(100);
    releaseCurve.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0);
    addAndMakeVisible(&releaseCurve);

    lofiModeButton.useShape(getOutlineFromSVG(BinaryData::IconLofi_svg));
    lofiModeButton.setHelpText("\"Lo-fi\" resampling");
    addAndMakeVisible(&lofiModeButton);

    playbackModeButton.setHelpText("Clear sound, higher pitches play quicker", "High CPU, maintains speed");
    addAndMakeVisible(&playbackModeButton);

    playbackSpeedRotary.getProperties().set(ComponentProps::ROTARY_ICON, new ReferenceCountedPath(getOutlineFromSVG(BinaryData::IconSpeed_svg)));
    playbackSpeedRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::SPEED_UNIT);

    loopButton.useShape(getOutlineFromSVG(BinaryData::IconLoop_svg));
    loopButton.setHelpText("Loop sample");
    addAndMakeVisible(&loopButton);

    loopStartButton.useShape(getOutlineFromSVG(BinaryData::IconLoopStart_svg));
    loopStartButton.setOwnerButton(&loopButton);
    loopStartButton.setHelpText("Loop with start portion");
    addAndMakeVisible(&loopStartButton);

    juce::Path loopEndIcon{ getOutlineFromSVG(BinaryData::IconLoopStart_svg) };
    loopEndIcon.applyTransform(juce::AffineTransform::scale(-1, 1));
    loopEndButton.useShape(loopEndIcon);
    loopEndButton.setOwnerButton(&loopButton);
    loopEndButton.setHelpText("Loop with release portion");
    addAndMakeVisible(&loopEndButton);

    monoOutputButton.useShape(getOutlineFromSVG(BinaryData::IconMono_svg));
    monoOutputButton.setHelpText("Mix output to mono");
    addAndMakeVisible(&monoOutputButton);

    gainSlider.setLookAndFeel(&gainSliderLNF);
    gainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearHorizontal);
    gainSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 0, 0);
    gainSlider.setTextValueSuffix(" " + PluginParameters::VOLUME_UNIT);
    gainSlider.setHelpText("Sample gain");
    addAndMakeVisible(&gainSlider);

    rotaries = { &semitoneRotary, &waveformSemitoneRotary, &centRotary, &waveformCentRotary, &attackTimeRotary, &releaseTimeRotary, &playbackSpeedRotary };
    for (auto rotary : rotaries)
    {
        rotary->setSliderStyle(juce::Slider::RotaryVerticalDrag);
        rotary->setMouseDragSensitivity(150);
        rotary->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 0, 0);
        rotary->addMouseListener(this, true);  // We need this to pass drag events through the labels
        addAndMakeVisible(rotary);
    }

    // Editor and navigator
    addAndMakeVisible(sampleEditor);

    sampleNavigator.setHelpText("Drag or scroll to navigate");
    addAndMakeVisible(sampleNavigator);

    statusLabel.setJustificationType(juce::Justification::centred);
    statusLabel.setInterceptsMouseClicks(false, false);
    addAndMakeVisible(statusLabel);
    
    // Sample controls
    editorOverlay.setInterceptsMouseClicks(false, false);
    addAndMakeVisible(editorOverlay);

    juce::StringArray recentFiles{ pluginState.recentFiles };
    for (int i = recentFiles.size() - 1; i >= 0; i--)
        filenameComponent.addRecentlyUsedFile(juce::File(recentFiles[i]));
    filenameComponent.addListener(this);
    filenameComponent.setHelpText("Choose a file");
    addAndMakeVisible(filenameComponent);

    waveformModeLabel.useShape(getOutlineFromSVG(BinaryData::IconWaveformMode_svg));
    waveformModeLabel.onStateChange = [this]
    {
        waveformModeLabel.setCustomHelpText(p.p(PluginParameters::DISABLE_WAVETABLE_MODE) ? "Waveform mode disabled" : "Waveform mode enabled");
        editorOverlay.setWaveformModeDisabled(waveformModeLabel.getToggleState() ^ waveformModeLabel.isDown());
    };
    waveformModeLabel.onStateChange();
    addChildComponent(waveformModeLabel);

    linkSampleToggle.useShape(getOutlineFromSVG(BinaryData::IconLinkEnabled_svg));
    linkSampleToggle.onClick = [this] { toggleLinkSample(); };
    linkSampleToggle.setHelpText("Link preset to file");
    addAndMakeVisible(linkSampleToggle);

    playPath = getOutlineFromSVG(BinaryData::IconPlay_svg);
    stopPath.addRoundedRectangle(juce::Rectangle(0, 0, 10, 10), 2.f);
    playStopButton.setShape(playPath);
    playStopButton.onClick = [this]
        {
            if (!currentlyPlaying)
                p.playVoice();
            else
                p.haltVoices();
        };
    playStopButton.setHelpText(playHelpText);
    addAndMakeVisible(playStopButton);

    juce::Path recordIcon;
    recordIcon.addEllipse(0, 0, 10, 10);
    recordButton.setShape(recordIcon);
    recordButton.onClick = [this] { startRecording(true); };
    recordButton.setHelpText("Record a sample");
    addAndMakeVisible(recordButton);

    deviceSettingsButton.onClick = [this] { promptDeviceSettings(); };
    deviceSettingsButton.setHelpText("Open input device settings");
    addAndMakeVisible(deviceSettingsButton);

    fitButton.onClick = [this] { sampleNavigator.fitView(); dummyParam.sendUIUpdate(); };
    fitButton.setHelpText("Fit viewport to bounds");
    addAndMakeVisible(fitButton);

    pinButton.useShape(getOutlineFromSVG(BinaryData::IconPin_svg));
    pinButton.setHelpText("Pin bounds to viewport");
    addAndMakeVisible(pinButton);

    // FX
    addChildComponent(fxChain);

    // Footer
    preFXButton.useShape(getOutlineFromSVG(BinaryData::IconPreFX_svg));
    preFXButton.setHelpText("Apply FX before Attack/Release");
    addAndMakeVisible(&preFXButton);

    showFXButton.onStateChange = [this, minHeight, maxHeight]
    {
        auto constrainedBounds = getConstrainedBounds();
        bool matchesBounds = constrainedBounds.getHeight() == getHeight();

        if ((fxChain.isVisible() != pluginState.showFX) && matchesBounds)
        {
            auto change = fxChain.isVisible() ? -scalei(Layout::fxChainHeight) : scalei(Layout::fxChainHeight);
            setSize(getWidth(), juce::jlimit(minHeight, maxHeight, getHeight() + int(change)));
        }
          
        showFXButton.setHelpText(pluginState.showFX ? "Hide the effects chain" : "Show the effects chain");
        resized();
    };

    showFXButton.useShape(getOutlineFromSVG(BinaryData::IconHideFX_svg), getOutlineFromSVG(BinaryData::IconShowFX_svg), juce::Justification::centredRight);
    addAndMakeVisible(showFXButton);

    versionInfo.setText("Build: " + juce::String(JAS_GIT_INFO) + " (" + JAS_BUILD_TIME + ")", juce::dontSendNotification);
    addAndMakeVisible(versionInfo);

    eqEnablementAttachment.sendInitialUpdate();
    reverbEnablementAttachment.sendInitialUpdate();
    distortionEnablementAttachment.sendInitialUpdate();
    chorusEnablementAttachment.sendInitialUpdate();

    addChildComponent(audioDeviceSettings);

    prompt.setHelpText("Click to close");
    addAndMakeVisible(prompt);

    // Update theme
    setTheme(darkTheme);

    addMouseListener(this, true);
    startTimerHz(PluginParameters::FRAME_RATE);

    enablementChanged();
}

JustaSampleAudioProcessorEditor::~JustaSampleAudioProcessorEditor()
{
    attackCurve.setLookAndFeel(nullptr);
    releaseCurve.setLookAndFeel(nullptr);
    gainSlider.setLookAndFeel(nullptr);
}

//==============================================================================
void JustaSampleAudioProcessorEditor::timerCallback()
{
    // If the processor has a new sample loaded, update the editor
    if (pluginState.sampleHash != expectedHash && p.getSampleBuffer().getNumSamples())
    {
        loadSample();

        // The file path should be updated by this point, in which case the filenameComponent should be updated
        if (filenameComponent.getCurrentFileText() != pluginState.filePath)
        {
            filenameComponent.setCurrentFile(juce::File(pluginState.filePath), true, juce::dontSendNotification);
            pluginState.recentFiles = filenameComponent.getRecentlyUsedFilenames();
        }

        resized();
    }

    // If playback state has changed, update the sampleEditor and sampleNavigator
    bool wasPlaying = currentlyPlaying;
    currentlyPlaying = std::ranges::any_of(synthVoices, [](auto* voice) -> bool{ return bool(voice->getCurrentlyPlayingSound()); });

    if ((wasPlaying && !currentlyPlaying) || currentlyPlaying)
    {
        sampleEditor.repaint();
        sampleNavigator.repaint();
    }

    if (wasPlaying != currentlyPlaying)
    {
        playStopButton.setShape(currentlyPlaying ? stopPath : playPath);
        playStopButton.setCustomHelpText(currentlyPlaying ? stopHelpText : playHelpText);
    }

    editorOverlay.setWaveformMode(CustomSamplerVoice::isWavetableModeAvailable(p.getBufferSampleRate(), pluginState.sampleStart, pluginState.sampleEnd) && !p.getRecorder().shouldRecordDevice());

    // If a new device manager state has been loaded from setStateInformation, this will trigger a refresh
    if (audioDeviceSettings.isVisible())
        p.initializeDeviceManager();

    // Handle recording changes (this must take place in a timer callback, since the recorder does not update synchronously)
    if (p.getRecorder().isRecordingDevice())
    {
        if (!sampleEditor.isRecordingMode() || !prompt.isPromptVisible())
        {
            sampleEditor.setRecordingMode(true);
            sampleNavigator.setRecordingMode(true);
            prompt.openPrompt({ &sampleEditor }, [this] {
                p.getRecorder().stopRecording();
                if (!pendingRecordingBuffer.getNumSamples())  // We'd rather wait until the sample is loaded to stop showing recording
                {
                    sampleEditor.setRecordingMode(false);
                    sampleNavigator.setRecordingMode(false);
                }
                enablementChanged();
            });
        }
        handleActiveRecording();
    }
    else if (sampleEditor.isRecordingMode())
    {
        prompt.closePrompt();
    }

    // We detect sample view scroll gestures as series of continuous scroll events while the view is changing and the mouse is up
    if (zoomGestureEndCallbacks >= 0)
        zoomGestureEndCallbacks--;
    if (mouseWheelDetected && pluginState.viewStart != lastViewStart && pluginState.viewEnd != lastViewEnd && !isMouseButtonDownAnywhere())
        zoomGestureEndCallbacks = maxCallbacksSinceInZoomGesture;

    if (zoomGestureEndCallbacks == 0)
        dummyParam.sendUIUpdate();

    mouseWheelDetected = false;
    lastViewStart = pluginState.viewStart;
    lastViewEnd = pluginState.viewEnd;

    enablementChanged();

    // This is to fix a lag issue found with Reaper and the regular software renderer 
    if (hostType.isReaper() && getPeer() && getPeer()->getCurrentRenderingEngine() == 0 && !openGLContext.isAttached())
        openGLContext.attachTo(*getTopLevelComponent());
}

void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g)
{
    auto colors = getTheme();

    g.fillAll(theme.light);

    auto bounds = getConstrainedBounds().toFloat();

    g.setColour(theme.background);
    g.fillRect(bounds);

    auto controls = bounds.removeFromTop(scalei(Layout::controlsHeight));

    juce::Path toolbarBackground;
    toolbarBackground.addRectangle(controls.toNearestInt());

    g.setColour(theme.foreground);
    g.fillPath(toolbarBackground);

    float divTop = controls.getY() + controls.getHeight() * 0.125f;
    float divBot = controls.getY() + controls.getHeight() * 0.875f;

    g.setColour(theme.slate.withAlpha(0.2f));
    g.drawVerticalLine((tuningDetectButton.getRight() + attackTimeRotary.getX()) / 2, divTop, divBot);
    g.drawVerticalLine((attackCurve.getRight() + releaseTimeRotary.getX()) / 2, divTop, divBot);
    g.drawVerticalLine((releaseCurve.getRight() + lofiModeButton.getX()) / 2, divTop, divBot);
    g.drawVerticalLine((playbackSpeedRotary.getRight() + loopStartButton.getX()) / 2, divTop, divBot);
    g.drawVerticalLine((loopEndButton.getRight() + monoOutputButton.getX()) / 2, divTop, divBot);

    auto footerBounds = bounds.removeFromBottom(scalei(Layout::footerHeight));
    g.setColour(theme.foreground);
    g.fillRect(footerBounds.toNearestInt());

    g.setColour(theme.slate.withAlpha(0.2f));
    g.drawVerticalLine(int(std::round(showFXButton.getX() - scalei(1.f))), footerBounds.getY() + scalei(13.f), footerBounds.getBottom() - scalei(13.f));

    bool fxEnabled = eqEnabled || reverbEnabled || distortionEnabled || chorusEnabled;
    if (fxEnabled)
    {
        auto fxBounds = showFXButton.getBounds().toFloat();
        auto indictatorBounds = fxBounds.removeFromTop(scalei(17.f)).removeFromRight(scalei(17.f)).translated(scalei(12.f), scalei(6.f));
        g.setColour(theme.highlight);
        g.fillEllipse(indictatorBounds);
    }

    auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples());
    if (pluginState.showFX)
    {
        bounds.removeFromBottom(scalei(Layout::fxChainHeight));
    }
    else if (sampleLoaded)
    {
        float border = scalei(2.5f);

        g.setColour(theme.slate.withAlpha(0.5f));
        g.fillRect(bounds.getX(), bounds.getBottom() - border / 2.f, bounds.getWidth(), border);
    }

    if (sampleLoaded || pluginState.showFX)
    {
        auto navigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
        g.setColour(theme.foreground);
        g.fillRect(navigatorBounds.toNearestInt());
    }
}

void EditorOverlay::paint(juce::Graphics& g)
{
    auto theme = getTheme();

    auto bounds = getLocalBounds().toFloat();
    bounds.removeFromTop(scale(Layout::sampleControlsMargin.getY()));

    auto sampleControls = bounds.removeFromTop(scale(Layout::sampleControlsHeight)).reduced(scale(Layout::sampleControlsMargin.getX()), 0.f);
    auto fileControls = sampleControls.removeFromLeft(scale(Layout::fileControlsWidth));
    auto playbackControls = sampleControls.removeFromRight(scale(Layout::playbackControlsWidth));

    juce::Path sampleControlRegionsPath;
    sampleControlRegionsPath.addRoundedRectangle(fileControls, scale(11.f));
    sampleControlRegionsPath.addRoundedRectangle(playbackControls, scale(11.f));

    sampleControlShadow.render(g, sampleControlRegionsPath);
    g.setColour(theme.foreground);
    g.fillPath(sampleControlRegionsPath);

    if (waveformModeAvailable)
    {
        sampleControls.removeFromLeft(scale(Layout::sampleControlsMargin.x));
        auto waveformModeBounds = sampleControls.removeFromLeft(scale(Layout::waveformModeWidth));

        sampleControlRegionsPath.clear();
        sampleControlRegionsPath.addRoundedRectangle(waveformModeBounds, scale(11.f));

        sampleControlShadow.render(g, sampleControlRegionsPath);
        g.setColour(disabled(theme.highlight, waveformModeDisabled));
        g.fillPath(sampleControlRegionsPath);
    }

    auto navControls = bounds.removeFromBottom(scale(Layout::navigatorControlsSize.y)).removeFromRight(scale(Layout::navigatorControlsSize.x))
        .getSmallestIntegerContainer().toFloat();
    
    juce::Path navControlRegionsPath;
    navControlRegionsPath.addRoundedRectangle(navControls.getX(), navControls.getY(), 
        navControls.getWidth(), navControls.getHeight(), scale(20.f), scale(20.f), 
        true, false, false, false);

    navControlShadow.render(g, navControlRegionsPath);
    g.setColour(theme.foreground);
    g.fillPath(navControlRegionsPath);
}

void EditorOverlay::resized()
{
    sampleControlShadow.setOffset({ int(scale(2.f)), int(scale(2.f)) });
    navControlShadow.setOffset({ int(-scale(2.f)), int(-scale(2.f)) });
}

void EditorOverlay::lookAndFeelChanged()
{
    auto colors = getTheme();

    sampleControlShadow.setColor(colors.slate.withAlpha(0.125f));
    navControlShadow.setColor(colors.slate.withAlpha(0.125f));
}

void JustaSampleAudioProcessorEditor::resized()
{
    pluginState.width = getWidth();
    pluginState.height = getHeight();

    auto bounds = getConstrainedBounds().toFloat();

    prompt.setBounds(bounds.toNearestInt());

    audioDeviceSettings.setBounds(juce::Rectangle(scalef(1200.f), scalef(700.f)).withCentre(bounds.getCentre()).toNearestInt());

    // Layout the controls toolbar
    auto controls = bounds.removeFromTop(scalei(Layout::controlsHeight));
    controls.removeFromLeft(scalei(Layout::controlsPaddingX));
    controls.removeFromTop(scalei(8));
    controls.removeFromBottom(scalei(10));
    
    juce::Array labels = { &tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel };
    for (juce::Label* label : labels)
        label->setFont(getInriaSans().withHeight(scalef(34.3f)));

    // Tuning module
    auto tuningModule = controls.removeFromLeft(scalei(Layout::tuningWidth));

    auto tuningLabelBounds = tuningModule.removeFromTop(scalei(Layout::moduleLabelHeight));
    tuningLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
    tuningLabel.setBounds(tuningLabelBounds.toNearestInt());
    tuningModule.removeFromTop(scalei(Layout::moduleLabelGap));

    // Rotaries take up a bit of extra space for the thumb, so we need to compensate by subtracting from other values
    float rotarySize = scalei(Layout::standardRotarySize);
    float rotaryPadding = rotarySize * Layout::rotaryPadding;

    tuningModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);

    auto semitoneBounds = tuningModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
    semitoneRotary.setBounds(semitoneBounds.toNearestInt());
    semitoneRotary.sendLookAndFeelChange();
    waveformSemitoneRotary.setBounds(semitoneBounds.toNearestInt());
    waveformSemitoneRotary.sendLookAndFeelChange();
    tuningModule.removeFromLeft(scalei(Layout::moduleControlsGap) - 2 * rotaryPadding);

    auto centBounds = tuningModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
    centRotary.setBounds(centBounds.toNearestInt());
    centRotary.sendLookAndFeelChange();
    waveformCentRotary.setBounds(centBounds.toNearestInt());
    waveformCentRotary.sendLookAndFeelChange();
    tuningModule.removeFromLeft(scalei(Layout::moduleControlsGap) - rotaryPadding);

    auto detectTuningBounds = tuningModule.removeFromLeft(scalei(71.f)).reduced(0.f, rotaryPadding);
    tuningDetectLabel.setBounds(detectTuningBounds.removeFromTop(scalei(29.f)).expanded(scalei(0.01f), 0.f).toNearestInt());
    tuningDetectLabel.setFont(getInriaSansBold().withHeight(scalef(27.5f)));
    detectTuningBounds.removeFromTop(scalei(5.f));
    tuningDetectButton.setBounds(detectTuningBounds.removeFromTop(scalei(46.f)).toNearestInt());

    controls.removeFromLeft(scalei(Layout::moduleGap));

    // Attack module
    auto attackModule = controls.removeFromLeft(scalei(Layout::attackWidth));

    auto attackLabelBounds = attackModule.removeFromTop(scalei(Layout::moduleLabelHeight));
    attackLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
    attackLabel.setBounds(attackLabelBounds.toNearestInt());

    attackModule.removeFromTop(scalei(Layout::moduleLabelGap));
    attackModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);

    auto attackTimeBounds = attackModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
    attackTimeRotary.setBounds(attackTimeBounds.toNearestInt());
    attackTimeRotary.sendLookAndFeelChange();
    attackModule.removeFromLeft(scalei(Layout::moduleControlsGap) - 2 * rotaryPadding);

    auto attackCurveBounds = attackModule.removeFromLeft(scalei(58.f) + 2 * rotaryPadding).reduced(0.f, scalei(16.5f));
    attackCurve.setBounds(attackCurveBounds.toNearestInt());

    controls.removeFromLeft(scalei(Layout::moduleGap));

    // Release module
    auto releaseModule = controls.removeFromLeft(scalei(Layout::releaseWidth));

    auto releaseLabelBounds = releaseModule.removeFromTop(scalei(Layout::moduleLabelHeight));
    releaseLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
    releaseLabel.setBounds(releaseLabelBounds.toNearestInt());

    releaseModule.removeFromTop(scalei(Layout::moduleLabelGap));
    releaseModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);

    auto releaseTimeBounds = releaseModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
    releaseTimeRotary.setBounds(releaseTimeBounds.toNearestInt());
    releaseTimeRotary.sendLookAndFeelChange();
    releaseModule.removeFromLeft(scalei(Layout::moduleControlsGap) - 2 * rotaryPadding);

    auto releaseCurveBounds = releaseModule.removeFromLeft(scalei(58.f) + 2 * rotaryPadding).reduced(0.f, scalei(16.5f));
    releaseCurve.setBounds(releaseCurveBounds.toNearestInt());

    controls.removeFromLeft(scalei(Layout::moduleGap));

    // Playback module
    auto playbackModule = controls.removeFromLeft(scalei(Layout::playbackWidth));

    auto playbackLabelBounds = playbackModule.removeFromTop(scalei(Layout::moduleLabelHeight));
    playbackLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
    playbackLabel.setBounds(playbackLabelBounds.toNearestInt());

    playbackModule.removeFromTop(scalei(Layout::moduleLabelGap));
    playbackModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);
    playbackModule.removeFromLeft(rotaryPadding);

    auto lofiButtonBounds = playbackModule.removeFromLeft(scalei(71.f)).reduced(0.f, rotaryPadding + scalei(7.5f));
    lofiModeButton.setBounds(lofiButtonBounds.toNearestInt());
    lofiModeButton.setBorder(scalef(2.5f), scalef(6.f));
    lofiModeButton.setPadding(scalef(12.f));

    playbackModule.removeFromLeft(scalei(Layout::moduleControlsGap));
    playbackModeButton.setBounds(playbackModule.removeFromLeft(scalef(300.f)).reduced(0.f, rotaryPadding).toNearestInt());
    playbackModeButton.setBorder(scalef(2.5f), scalef(6.f));

    playbackModule.removeFromLeft(scalei(Layout::moduleControlsGap) - rotaryPadding);
    playbackSpeedRotary.setBounds(playbackModule.removeFromLeft(rotarySize + 2 * rotaryPadding).toNearestInt());
    playbackSpeedRotary.sendLookAndFeelChange();
    
    controls.removeFromLeft(scalei(Layout::moduleGap));

    // Loop module
    auto loopModule = controls.removeFromLeft(scalei(Layout::loopWidth));

    auto loopingLabelBounds = loopModule.removeFromTop(scalei(Layout::moduleLabelHeight));
    loopingLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
    loopingLabel.setBounds(loopingLabelBounds.toNearestInt());

    loopModule.removeFromTop(scalei(Layout::moduleLabelGap));
    loopModule.reduce(scalei(16.f), scalei(6.f));

    loopStartButton.setBounds(loopModule.removeFromLeft(scalei(34.f)).toNearestInt());
    loopStartButton.setBorder(scalef(2.5f), scalef(6.f), true, false, true, false);
    loopStartButton.setPadding(scalef(8.f), scalef(4.f), scalef(7.5f), scalef(7.5f));

    loopModule.removeFromLeft(scalei(6.f));
    loopButton.setBounds(loopModule.removeFromLeft(scalei(105.f)).toNearestInt());
    loopButton.setBorder(scalef(2.5f));
    loopButton.setPadding(scalef(13.f));

    loopModule.removeFromLeft(scalei(6.f));
    loopEndButton.setBounds(loopModule.removeFromLeft(scalei(34.f)).toNearestInt());
    loopEndButton.setBorder(scalef(2.5f), scalef(6.f), false, true, false, true);
    loopEndButton.setPadding(scalef(4.f), scalef(8.f), scalef(7.5f), scalef(7.5f));

    controls.removeFromLeft(scalei(Layout::moduleGap));

    // Master module
    auto masterModule = controls.removeFromLeft(scalei(Layout::masterWidth));

    auto masterLabelBounds = masterModule.removeFromTop(scalei(Layout::moduleLabelHeight));
    masterLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
    masterLabel.setBounds(masterLabelBounds.toNearestInt());

    masterModule.removeFromTop(scalei(Layout::moduleLabelGap));
    masterModule.reduce(scalei(16.f), 0.f);

    auto monoButtonBounds = masterModule.removeFromLeft(scalei(92.f)).reduced(0.f, scalei(27.5f));
    monoOutputButton.setBounds(monoButtonBounds.toNearestInt());
    monoOutputButton.setBorder(scalef(2.5f), scalef(6.f));
    monoOutputButton.setPadding(scalef(12.f));

    masterModule.removeFromLeft(scalei(Layout::moduleControlsGap));
    gainSlider.setBounds(masterModule.removeFromLeft(scalei(153.f)).toNearestInt());
    gainSlider.sendLookAndFeelChange();

    // Footer
    auto footer = bounds.removeFromBottom(scalei(Layout::footerHeight));
    footer.removeFromBottom(scalei(2.f));

    footer.reduce(scalei(25.f), 0.f);
    
    auto showFXButtonBounds = footer.removeFromRight(scalei(205.f));
    showFXButton.setPadding(0.f, 0.f, scalei(19.f), scalei(19.f));
    showFXButton.setBounds(showFXButtonBounds.toNearestInt());

    footer.removeFromRight(scalei(30.f));

    auto preFXButtonBounds = footer.removeFromRight(scalei(100.f)).reduced(0.f, scalei(11.f));
    preFXButton.setBounds(preFXButtonBounds.toNearestInt());
    preFXButton.setBorder(scalef(2.5f), scalef(6.f));
    preFXButton.setPadding(scalef(11.f));

    versionInfo.setFont(getInter().withHeight(scalef(28.f)));
    versionInfo.setBounds(footer.reduced(0.f, scalei(18.f)).toNearestInt());

    // FX
    if (pluginState.showFX)
    {
        auto fxChainBounds = bounds.removeFromBottom(scalei(Layout::fxChainHeight));
        fxChain.setBounds(fxChainBounds.toNearestInt());
    }
    fxChain.setVisible(pluginState.showFX);

    // Navigator
    auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples());
    if (sampleLoaded || pluginState.showFX)
    {
        auto sampleNavigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
        sampleNavigator.setBounds(sampleNavigatorBounds.toNearestInt());
    }

    // Editor
    auto editorBounds = bounds;
    sampleEditor.setBounds(editorBounds.toNearestInt());
    editorOverlay.setBounds(editorBounds.toNearestInt());

    statusLabel.setFont(getInter().withHeight(scalef(50.f)));
    updateLabel();

    // Sample controls
    editorBounds.removeFromTop(scalei(Layout::sampleControlsMargin.getY()));

    auto sampleControls = editorBounds.removeFromTop(scalei(Layout::sampleControlsHeight)).reduced(scalei(Layout::sampleControlsMargin.getX()), 0.f);
    auto fileControls = sampleControls.removeFromLeft(scalei(Layout::fileControlsWidth));
    auto playbackControls = sampleControls.removeFromRight(scalei(Layout::playbackControlsWidth));

    fileControls.reduce(scalei(12.f), scalei(2.1f));
    auto filenameComponentBounds = fileControls.removeFromLeft(scalei(1095.f));
    filenameComponent.setBounds(filenameComponentBounds.toNearestInt());

    fileControls.removeFromLeft(scalei(20.f));
    auto linkSampleToggleBounds = fileControls.removeFromLeft(scalei(48.f)).reduced(0.f, scalei(2.85f));
    linkSampleToggle.setBounds(linkSampleToggleBounds.toNearestInt());
    linkSampleToggle.setPadding(scalef(3.f));
    linkSampleToggle.setBorder(0.f, scalef(5.f));

    sampleControls.removeFromLeft(scalei(Layout::sampleControlsMargin.x));
    auto waveformModeBounds = sampleControls.removeFromLeft(scalei(Layout::waveformModeWidth));

    waveformModeLabel.setBounds(waveformModeBounds.toNearestInt());
    waveformModeLabel.setPadding(scalef(12.1f));

    playbackControls.reduce(scalei(12.f), scalei(2.1f));
    auto playStopButtonBounds = playbackControls.removeFromLeft(scalei(40.32f)).reduced(scalei(4.98f), scalei(3.36f));
    playStopButton.setBounds(playStopButtonBounds.toNearestInt());

    playbackControls.removeFromLeft(scalei(14.f));
    auto recordControlBounds = playbackControls.removeFromLeft(scalei(58.6f)).reduced(scalei(2.f), scalei(3.36f));
    auto recordButtonBounds = recordControlBounds.removeFromLeft(scalei(33.6f));
    recordButton.setBounds(recordButtonBounds.toNearestInt());

    recordControlBounds.removeFromLeft(scalei(4.f));
    auto deviceSettingsButtonBounds = recordControlBounds.removeFromTop(scalei(23.f));
    deviceSettingsButton.setBounds(deviceSettingsButtonBounds.toNearestInt());

    auto navControlBounds = bounds.removeFromBottom(scalei(Layout::navigatorControlsSize.y)).removeFromRight(scalei(Layout::navigatorControlsSize.x));
    navControlBounds.removeFromLeft(scalei(20.f));

    auto fitButtonBounds = navControlBounds.removeFromLeft(scalei(34.f));
    fitButtonBounds.removeFromTop(scalei(10.f));
    fitButtonBounds.removeFromBottom(scalei(5.f));
    fitButton.setBounds(fitButtonBounds.toNearestInt());

    navControlBounds.removeFromLeft(scalei(12.f));
    auto pinButtonBounds = navControlBounds.removeFromLeft(scalei(44.f));
    pinButtonBounds.removeFromTop(scalei(7.5f));
    pinButtonBounds.removeFromBottom(scalei(2.5f));
    pinButton.setBounds(pinButtonBounds.toNearestInt());
    pinButton.setBorder(0.f, scalef(5.f));
    pinButton.setPadding(scalef(5.f));

    repaint();
}

void JustaSampleAudioProcessorEditor::lookAndFeelChanged()
{
    const auto shadow = theme.dark.withAlpha(0.25f);

    lofiModeButton.setColors(theme.dark, theme.light, shadow);
    loopButton.setColors(theme.dark, theme.loop, shadow);
    loopStartButton.setColors(theme.dark, theme.loop, shadow);
    loopEndButton.setColors(theme.dark, theme.loop, shadow);
    monoOutputButton.setColors(theme.dark, theme.light, shadow);
    linkSampleToggle.setColors(theme.dark, theme.slate.withAlpha(0.5f), shadow);
    waveformModeLabel.setColors(theme.foreground, theme.slate.withAlpha(0.f), shadow);
    pinButton.setColors(theme.dark, theme.slate.withAlpha(0.5f), shadow);
    preFXButton.setColors(theme.darkerSlate, theme.light, shadow);
    showFXButton.setColors(theme.dark, theme.slate.withAlpha(0.f), shadow);

    tuningDetectButton.setColor(theme.dark);
    playStopButton.setColor(theme.dark);
    recordButton.setColor(theme.highlight);
    deviceSettingsButton.setColor(theme.dark);
    fitButton.setColor(theme.dark);

    versionInfo.setColour(juce::Label::textColourId, theme.dark);

    playbackModeButton.setColors(theme.slate, theme.light, shadow);

    statusLabel.setColour(juce::Label::textColourId, theme.dark);
    statusLabel.setColour(juce::Label::backgroundColourId, theme.background.withAlpha(0.85f));
    sampleNavigator.setColour(Colors::painterColorId, theme.darkerSlate);

    juce::Array<Component*> foregroundComponents = {
        &tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel, &semitoneRotary, &waveformSemitoneRotary, &centRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
        &attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &lofiModeButton, &playbackModeButton, &playbackSpeedRotary, &loopStartButton, &loopButton, &loopEndButton,
        &monoOutputButton, &gainSlider, &filenameComponent, &linkSampleToggle, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator, &preFXButton, &showFXButton
    };

    for (Component* component : foregroundComponents)
        component->setColour(Colors::backgroundColorId, theme.foreground);
}

void JustaSampleAudioProcessorEditor::mouseDown(const juce::MouseEvent& event)
{
    auto parent = event.originalComponent->getParentComponent();
    for (auto rotary : rotaries)
        if (parent == rotary)
            rotary->mouseDown(event.getEventRelativeTo(rotary));
}

void JustaSampleAudioProcessorEditor::mouseUp(const juce::MouseEvent& event)
{
    auto parent = event.originalComponent->getParentComponent();
    for (auto rotary : rotaries)
        if (parent == rotary)
            rotary->mouseUp(event.getEventRelativeTo(rotary));
}

void JustaSampleAudioProcessorEditor::mouseDrag(const juce::MouseEvent& event)
{
    auto parent = event.originalComponent->getParentComponent();
    for (auto rotary : rotaries)
        if (parent == rotary)
            rotary->mouseDrag(event.getEventRelativeTo(rotary));

}

void JustaSampleAudioProcessorEditor::mouseWheelMove(const juce::MouseEvent&, const juce::MouseWheelDetails&)
{
    mouseWheelDetected = true;
}

void JustaSampleAudioProcessorEditor::setTheme(const Colors newTheme)
{
    theme = newTheme;
    lnf.setTheme(theme);
    attackCurveLNF.setTheme(theme);
    releaseCurveLNF.setTheme(theme);
    gainSliderLNF.setTheme(theme);

    enablementChanged();
    sendLookAndFeelChange();
}

Colors JustaSampleAudioProcessorEditor::getTheme() const
{
    return theme;
}

juce::Rectangle<int> JustaSampleAudioProcessorEditor::getConstrainedBounds() const
{
    auto fxSpace = !pluginState.showFX ? scalef(Layout::fxChainHeight) : 0;

    auto bounds = getLocalBounds();
    auto width = getWidth();
    auto height = getHeight();
    auto constrainedWidth = int(juce::jlimit<float>((height + fxSpace) * 0.8f, (height + fxSpace) * 2.f, float(width)));
    auto constrainedHeight = int(width / 0.8f - fxSpace);
    
    if (constrainedWidth < width)
        bounds.reduce((width - constrainedWidth) / 2, 0);
    else if (constrainedHeight < height)
        bounds.reduce(0, (height - constrainedHeight) / 2);

    return bounds;
}

//==============================================================================
void JustaSampleAudioProcessorEditor::loadSample()
{
    bool sampleLoaded = p.getSampleBuffer().getNumSamples();
    if (sampleLoaded)
    {
        expectedHash = pluginState.sampleHash;
        linkSampleToggle.setToggleState(pluginState.usingFileReference, juce::dontSendNotification);

        bool userLoad = userDraggedSample || p.hasLoadedFromReaper();
        sampleEditor.setSample(p.getSampleBuffer(), p.getBufferSampleRate(), userLoad);
        sampleNavigator.setSample(p.getSampleBuffer(), p.getBufferSampleRate(), userLoad);
        dummyParam.sendUIUpdate();
    }
    sampleEditor.setRecordingMode(false);
    sampleNavigator.setRecordingMode(false);
    prompt.closePrompt();
    userDraggedSample = false;
    p.setLoadedFromReaper(false);
}

void JustaSampleAudioProcessorEditor::handleActiveRecording()
{
    auto& recordingBuffer = p.getRecorder().getRecordingBufferQueue();
    RecordingBufferChange* bufferChange = recordingBuffer.peek();

    // These variables are to deal with the GUI sample updates
    int previousSampleSize = recordingBufferSize;
    int previousBufferSize = pendingRecordingBuffer.getNumSamples();
    int iterations = 0;
    while (bufferChange && iterations < 5)  // 5 is a safety measure
    {
        iterations++;
        switch (bufferChange->type)
        {
        case RecordingBufferChange::CLEAR:
            pendingRecordingBuffer.setSize(1, 0);
            recordingBufferSize = 0;
            break;
        case RecordingBufferChange::ADD:
            pendingRecordingBuffer.setSize(
                1,
                pendingRecordingBuffer.getNumSamples() < recordingBufferSize + bufferChange->addedBuffer.getNumSamples() ? juce::jmax<int>(recordingBufferSize + bufferChange->addedBuffer.getNumSamples(), 2 * pendingRecordingBuffer.getNumSamples()) :
                pendingRecordingBuffer.getNumSamples(),
                true, true, false
            );
            pendingRecordingBuffer.copyFrom(0, recordingBufferSize, bufferChange->addedBuffer, 0, 0, bufferChange->addedBuffer.getNumSamples());
            recordingBufferSize += bufferChange->addedBuffer.getNumSamples();
            break;
        }
        recordingBuffer.pop();
        bufferChange = recordingBuffer.peek();
    }

    if (pendingRecordingBuffer.getNumSamples() != previousBufferSize)  // Update when the buffer size changes
    {
        sampleEditor.setSample(pendingRecordingBuffer, 0.f, false);
        sampleNavigator.setSample(pendingRecordingBuffer, 0.f, false);
    } 
    else if (previousSampleSize != recordingBufferSize)  // Update the rendered paths when more data is added to the buffer (not changing the size)
    {
        sampleEditor.sampleUpdated(previousSampleSize, recordingBufferSize);
        sampleNavigator.sampleUpdated(previousSampleSize, recordingBufferSize);
    }
}

void JustaSampleAudioProcessorEditor::toggleLinkSample()
{
    // If the file path is empty or the file doesn't exist, prompt the user to save the sample, otherwise toggle the parameter
    if (!pluginState.usingFileReference && (pluginState.filePath.load().isEmpty() || !juce::File(pluginState.filePath).existsAsFile()))
    {
        p.openFileChooser("Save the sample to a file",
                          juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles, [this](const juce::FileChooser& chooser) -> void {
                              juce::File file = chooser.getResult();
                auto fileStream = std::make_unique<juce::FileOutputStream>(file);
                if (file.hasWriteAccess() && fileStream->openedOk())
                {
                    // Write the sample to file
                    fileStream->setPosition(0);
                    fileStream->truncate();

                    juce::WavAudioFormat wavFormat;
                    auto options = juce::AudioFormatWriterOptions{}
                        .withSampleRate(p.getBufferSampleRate())
                        .withNumChannels(p.getSampleBuffer().getNumChannels())
                        .withBitsPerSample(PluginParameters::STORED_BITRATE);

                    std::unique_ptr<juce::OutputStream> outputStream = std::move(fileStream);
                    auto formatWriter = wavFormat.createWriterFor(outputStream, options);
                    formatWriter->writeFromAudioSampleBuffer(p.getSampleBuffer(), 0, p.getSampleBuffer().getNumSamples());
                    outputStream.release();

                    const juce::String& filename = file.getFullPathName();
                    pluginState.filePath = filename;
                    linkSampleToggle.setToggleState(true, juce::dontSendNotification);
                    filenameComponent.setCurrentFile(file, true, juce::dontSendNotification);
                    pluginState.usingFileReference = linkSampleToggle.getToggleState();
                }
            }, true);
        linkSampleToggle.setToggleState(false, juce::dontSendNotification);
    }
    else
    {
        pluginState.usingFileReference = linkSampleToggle.getToggleState();
    }
}

void JustaSampleAudioProcessorEditor::startRecording(bool promptSettings)
{
    if (p.getRecorder().shouldRecordDevice())
        return;

    p.initializeDeviceManager();

    if (p.getDeviceManager().getCurrentAudioDevice() && p.getDeviceManager().getCurrentAudioDevice()->getActiveInputChannels().countNumberOfSetBits())
    {
        p.getRecorder().startRecording(true);
    }
    else if (promptSettings)
    {
        promptDeviceSettings(true);
    }
}

void JustaSampleAudioProcessorEditor::promptPitchDetection()
{
    prompt.openPrompt({ &sampleEditor }, [this] {
        sampleEditor.cancelBoundsSelection();
        enablementChanged();
    });
    sampleEditor.promptBoundsSelection([this](int startPos, int endPos) -> void {
        p.startPitchDetectionRoutine(startPos, endPos);
        enablementChanged();
        prompt.closePrompt();
    });
}

void JustaSampleAudioProcessorEditor::promptDeviceSettings(bool recordOnClose)
{
    audioDeviceSettings.setVisible(true);
    prompt.openPrompt({ &audioDeviceSettings }, [this, recordOnClose]() -> void
    {
        audioDeviceSettings.setVisible(false);
        if (recordOnClose)
            startRecording(false);
        dummyParam.sendUIUpdate();
    });
}

void JustaSampleAudioProcessorEditor::enablementChanged()
{
    auto isSampleLoaded = bool(p.getSampleBuffer().getNumSamples());
    auto isRecording = p.getRecorder().shouldRecordDevice() || sampleEditor.isInBoundsSelection();  // For UI purposes, treat bounds selection like recording
    auto isLoading = p.getSampleLoader().isLoading();
    auto isWaveformModeAvailable = CustomSamplerVoice::isWavetableModeAvailable(p.getBufferSampleRate(), pluginState.sampleStart, pluginState.sampleEnd) && !isRecording;
    auto isWaveformMode = isWaveformModeAvailable && !p.p(PluginParameters::DISABLE_WAVETABLE_MODE);

    // Note, it is very important that a sample's enablement is set to its correct value in one step, since internally JUCE repaints when the enablement changes
    juce::Array<Component*> sampleDependentComponents = {
        &semitoneRotary, &centRotary, &waveformSemitoneRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
        &attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &gainSlider,
        &playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &fxChain, &showFXButton
    };

    juce::Array<Component*> notWaveformModeComponents = { &playbackModeButton, &loopStartButton, &loopButton, &loopEndButton, &tuningDetectLabel, &tuningDetectButton };

    juce::Array<Component*> notLoadingDependentComponents = { &tuningDetectButton, &tuningDetectLabel, &recordButton, &deviceSettingsButton };

    juce::Array<Component*> notRecordingDependentComponents = { &filenameComponent, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator };

    // Set enablement on both
    juce::SortedSet<Component*> all;
    all.addArray(sampleDependentComponents.getRawDataPointer(), sampleDependentComponents.size());
    all.addArray(notWaveformModeComponents.getRawDataPointer(), notWaveformModeComponents.size());
    all.addArray(notLoadingDependentComponents.getRawDataPointer(), notLoadingDependentComponents.size());
    all.addArray(notRecordingDependentComponents.getRawDataPointer(), notRecordingDependentComponents.size());

    for (auto component : all)
    {
        auto sampleDependent = sampleDependentComponents.contains(component);
        auto notRecordingDependent = notRecordingDependentComponents.contains(component);
        auto notLoadingDependent = notLoadingDependentComponents.contains(component);
        auto notWaveformModeDependent = notWaveformModeComponents.contains(component);

        component->setEnabled((isSampleLoaded || !sampleDependent) && (!isRecording || !notRecordingDependent) && 
            ((isSampleLoaded && !isWaveformMode) || !notWaveformModeDependent) && (!isLoading || !notLoadingDependent));
    }

    // Some controls need more specialized rules...
    semitoneRotary.setVisible(!isWaveformMode);
    centRotary.setVisible(!isWaveformMode);
    waveformSemitoneRotary.setVisible(isWaveformMode);
    waveformCentRotary.setVisible(isWaveformMode);

    lofiModeButton.setEnabled(isSampleLoaded && (!playbackModeButton.getChoice() || isWaveformMode));

    playbackSpeedRotary.setEnabled(isSampleLoaded && playbackModeButton.getChoice() && !isWaveformMode);
    monoOutputButton.setEnabled(isSampleLoaded && p.getSampleBuffer().getNumChannels() > 1);

    linkSampleToggle.setEnabled(isSampleLoaded && !isRecording && !p.sampleBufferNeedsReference());

    waveformModeLabel.setVisible(isSampleLoaded && isWaveformModeAvailable);
    editorOverlay.setWaveformMode(isSampleLoaded && isWaveformModeAvailable);

    const auto navigatorWasVisible = sampleNavigator.isVisible();
    sampleNavigator.setVisible(isSampleLoaded || pluginState.showFX);
    if (navigatorWasVisible != sampleNavigator.isVisible())
        resized();

    preFXButton.setEnabled(isSampleLoaded && (reverbEnabled || distortionEnabled || chorusEnabled || eqEnabled)),

    // Handle status label
    statusLabel.setVisible(!p.getSampleBuffer().getNumSamples() || p.getSampleLoader().isLoading() || sampleEditor.isInBoundsSelection() || sampleEditor.isRecordingMode() || fileDragging);
    statusLabel.setColour(juce::Label::outlineColourId, theme.dark.withAlpha(float(fileDragging)));
    if (p.getSampleLoader().isLoading())
        updateLabel("Loading...");
    else if (sampleEditor.isInBoundsSelection())
        updateLabel("Select a region to analyze");
    else if (sampleEditor.isRecordingMode())
        updateLabel("Recording...");
    else if (!p.getSampleBuffer().getNumSamples())
        updateLabel("Drop a sample!");
    else if (fileDragging)
        updateLabel("Replace sample");
}

void JustaSampleAudioProcessorEditor::fxEnablementChanged()
{
    enablementChanged();
    repaint(showFXButton.getBounds().expanded(int(scalei(15.f)), 0));
}

//==============================================================================
bool JustaSampleAudioProcessorEditor::isInterestedInFile(const juce::String& file) const
{
    return p.canLoadFileExtension(file) && !prompt.isPromptVisible();
}

bool JustaSampleAudioProcessorEditor::isInterestedInFileDrag(const juce::StringArray& files)
{
    return std::ranges::any_of(files, [this](const auto& f) { return isInterestedInFile(f); }) && !p.getRecorder().shouldRecordDevice();
}

void JustaSampleAudioProcessorEditor::filesDropped(const juce::StringArray& files, int, int)
{
    for (const juce::String& file : files)
    {
        if (isInterestedInFileDrag(file))
        {
            fileDragging = false;
            enablementChanged();
            userDraggedSample = true;
            p.loadSampleFromPath(file, true, "", false, 
                [this](bool loaded) { if (!loaded) userDraggedSample = false; });
            break;
        }
    }
}

void JustaSampleAudioProcessorEditor::filenameComponentChanged(juce::FilenameComponent* fileComponentThatHasChanged)
{
    juce::File file = fileComponentThatHasChanged->getCurrentFile();
    userDraggedSample = true;
    p.loadSampleFromPath(file.getFullPathName(), true, "", false, [this, fileComponentThatHasChanged, file](bool fileLoaded) -> void
    {
        if (!fileLoaded)
        {
            fileComponentThatHasChanged->setCurrentFile(juce::File(pluginState.filePath), false, juce::dontSendNotification);
            auto recentFiles = fileComponentThatHasChanged->getRecentlyUsedFilenames();
            recentFiles.removeString(file.getFullPathName());
            fileComponentThatHasChanged->setRecentlyUsedFilenames(recentFiles);
            userDraggedSample = false;
        }
    });
}

void JustaSampleAudioProcessorEditor::fileDragEnter(const juce::StringArray& files, int /*x*/, int /*y*/)
{
    if (isInterestedInFileDrag(files))
    {
        fileDragging = true;
        enablementChanged();
    }
}

void JustaSampleAudioProcessorEditor::fileDragExit(const juce::StringArray&)
{
    fileDragging = false;
    enablementChanged();
}

void JustaSampleAudioProcessorEditor::updateLabel(const juce::String& text)
{
    if (text.isNotEmpty())
        statusLabel.setText(text, juce::dontSendNotification);

    auto labelWidth = juce::TextLayout::getStringWidth(statusLabel.getFont(), statusLabel.getText());
    statusLabel.setBounds(juce::Rectangle(0, 0, int(labelWidth), int(statusLabel.getFont().getHeight()))
        .withCentre(sampleEditor.getBounds().getCentre()).expanded(int(scalei(40.f))).toNearestInt());
}
