#include "ConnectionWorker.hpp"
#include "RTLogger.hpp"

using namespace juce;

static std::string KeyPressToDescription(const KeyPress & k) {
    std::string root;
    int code = k.getKeyCode();
    if(code == KeyPress::spaceKey) {
        root = " ";
    } else if(code == KeyPress::backspaceKey) {
        root = "backspace";
    } else if(code == KeyPress::deleteKey) {
        root = "delete";
    } else if(code == KeyPress::tabKey) {
        root = "tab";
    } else if(code == KeyPress::leftKey) {
        root = "arrowleft";
    } else if(code == KeyPress::upKey) {
        root = "arrowup";
    } else if(code == KeyPress::rightKey) {
        root = "arrowright";
    } else if(code == KeyPress::downKey) {
        root = "arrowdown";
    } else if(code == KeyPress::numberPadEquals) {
        root = "=";
    } else if(code == KeyPress::numberPadDelete) {
        root = "delete";
    } else if(code == KeyPress::numberPadDecimalPoint) {
        root = ".";
    } else if(code == KeyPress::numberPadSeparator) {
        root = ",";
    } else if(code == KeyPress::escapeKey) {
        root = "escape";
    } else if(code == KeyPress::returnKey) {
        root = "return";
    } else if(code == KeyPress::numberPad0) {
        root = "0";
    } else if(code == KeyPress::numberPad1) {
        root = "1";
    } else if(code == KeyPress::numberPad2) {
        root = "2";
    } else if(code == KeyPress::numberPad3) {
        root = "3";
    } else if(code == KeyPress::numberPad4) {
        root = "4";
    } else if(code == KeyPress::numberPad5) {
        root = "5";
    } else if(code == KeyPress::numberPad6) {
        root = "6";
    } else if(code == KeyPress::numberPad7) {
        root = "7";
    } else if(code == KeyPress::numberPad8) {
        root = "8";
    } else if(code == KeyPress::numberPad9) {
        root = "9";
    } else if(code == KeyPress::numberPadAdd) {
        root = "+";
    } else if(code == KeyPress::numberPadSubtract) {
        root = "-";
    } else if(code == KeyPress::numberPadMultiply) {
        root = "*";
    } else if(code == KeyPress::numberPadDivide) {
        root = "/";
    } else {
        root = " ";
        root[0] = std::tolower(code);
    }
    
    std::string modifiers;
    auto mods = k.getModifiers();
    if(mods.isCommandDown()) {
        modifiers += "ctrl+";
    }
    if(mods.isAltDown()) {
        modifiers += "alt+";
    }
    if(mods.isShiftDown()) {
        modifiers += "shift+";
    }
    
    return modifiers + root;
}

class WavtoolKeyListener final : public KeyListener
{
public:
    WavtoolKeyListener(std::function<void(const KeyDescription &)> && keyHandler)
    : keyHandler(std::move(keyHandler))
    { }
    
    virtual bool keyPressed(const KeyPress & key, Component *) override {
        if(heldKeys.find(key.getKeyCode()) != heldKeys.end()) {
            return true;
        }
        auto desc = KeyDescription{
            .keyDown = true,
            .keyDescription = KeyPressToDescription(key)
        };
        keyHandler(desc);
        heldKeys.insert(std::make_pair(key.getKeyCode(), desc.keyDescription));
        return true;
    }
    
    virtual bool keyStateChanged(bool, Component *) override {
        std::set<int> upKeys;
        for(auto & p : heldKeys) {
            if(!KeyPress::isKeyCurrentlyDown(p.first)) {
                upKeys.insert(p.first);
            }
        }
        for(auto code : upKeys) {
            keyHandler(KeyDescription{
                .keyDown = false,
                .keyDescription = heldKeys[code]
            });
            heldKeys.erase(code);
        }
        return !upKeys.empty();
    }
    
    void reset() {
        for(auto & p : heldKeys) {
            keyHandler(KeyDescription{
                .keyDown = false,
                .keyDescription = p.second
            });
        }
        heldKeys.clear();
    }
    
private:
    std::map<int, std::string> heldKeys;
    std::function<void(const KeyDescription &)> keyHandler;
};

#ifdef _MSC_VER
constexpr const int HEADER_HEIGHT = 20;

class EditorKeyboardGrabWrapper final : public Component, public Button::Listener, public ComponentListener, public Timer
{
public:
    EditorKeyboardGrabWrapper(AudioProcessorEditor* p, std::function<void(bool)> && toggleCallbackIn)
        : p(p), toggleCallback(std::move(toggleCallbackIn)), buttonToggleState(true)
    {
        keyboardGrabEnabled = juce::ImageCache::getFromMemory(BinaryData::EnabledState_png, BinaryData::EnabledState_pngSize);
        keyboardGrabDisabled = juce::ImageCache::getFromMemory(BinaryData::DisabledState_png, BinaryData::DisabledState_pngSize);
        keyboardGrabMiniEnabled = juce::ImageCache::getFromMemory(BinaryData::MiniEnabledState_png, BinaryData::MiniEnabledState_pngSize);
        keyboardGrabMiniDisabled = juce::ImageCache::getFromMemory(BinaryData::MiniDisabledState_png, BinaryData::MiniDisabledState_pngSize);
        frameLogo = juce::ImageCache::getFromMemory(BinaryData::FrameLogo_png, BinaryData::FrameLogo_pngSize);
        activityIndicator = juce::ImageCache::getFromMemory(BinaryData::ActivityIndicator_png, BinaryData::ActivityIndicator_pngSize);
       
        button.addListener(this);
        addAndMakeVisible(button);

        keyboardActivityIndicator.setSize(HEADER_HEIGHT, HEADER_HEIGHT);
        keyboardActivityIndicator.setImage(activityIndicator);
        keyboardActivityIndicator.setAlpha(0.2);
        addAndMakeVisible(keyboardActivityIndicator);

        addAndMakeVisible(logo);

        addAndMakeVisible(p);
        p->setTopLeftPosition(0, 0);
        p->addComponentListener(this);
        setSize(p->getWidth(), p->getHeight() + HEADER_HEIGHT);

        layout();
    }

    ~EditorKeyboardGrabWrapper() {
        p->removeComponentListener(this);
        button.removeListener(this);
        if (isTimerRunning()) {
            stopTimer();
        }
        delete p;
    }

    void resized() override
    {
        if (p->getWidth() != getWidth() || p->getHeight() + HEADER_HEIGHT != getHeight()) {
            p->setSize(getWidth(), getHeight() - HEADER_HEIGHT);
        }
        layout();
    }

    void layout()
    {
        if (getWidth() < 180) {
            button.setImages(true, true, false, buttonToggleState ? keyboardGrabMiniEnabled : keyboardGrabMiniDisabled, 0.9, Colour{}, Image{}, 1.0, Colour{}, Image{}, 1.0, Colour{}, 0.0);
        }
        else {
            button.setImages(true, true, false, buttonToggleState ? keyboardGrabEnabled : keyboardGrabDisabled, 0.9, Colour{}, Image{}, 1.0, Colour{}, Image{}, 1.0, Colour{}, 0.0);
        }
        button.setSize(button.getWidth() / 2, button.getHeight() / 2);
        button.setTopLeftPosition((getWidth() - button.getWidth()) / 2, 0);
        auto buttonBounds = button.getBoundsInParent();
        keyboardActivityIndicator.setTopLeftPosition(buttonBounds.getTopRight().x, (HEADER_HEIGHT - keyboardActivityIndicator.getHeight()) / 2);

        logo.setTopLeftPosition(0, 0);
        logo.setSize(66, HEADER_HEIGHT);
        if (getWidth() >= 300) {
            logo.setImage(frameLogo);
        }
        else {
            logo.setImage(Image{});
        }
        p->setTopLeftPosition(0, HEADER_HEIGHT);
    }

    ComponentBoundsConstrainer* getConstrainer()
    {
        return &constrainer;
    }

    void componentMovedOrResized(Component& c, bool, bool) override {
        if (&c != p) {
            return;
        }
        if (c.getWidth() != getWidth() || c.getHeight() + HEADER_HEIGHT != getHeight()) {
            setSize(c.getWidth(), c.getHeight() + HEADER_HEIGHT);
        }
        layout();
    }

    void buttonClicked(Button* b) override {
        if (b != &button) {
            return;
        }
        toggleCallback(!buttonToggleState);
    }

    void setToggleState(bool newState) {
        buttonToggleState = newState;
        layout();
    }

    void paint(Graphics& g) override {
        g.fillAll(Colour{ 0x11, 0x11, 0x11 });
    }

    void keyboardActivity() {
        keyboardActivityIndicator.setAlpha(0.7);
        startTimer(100);
    }

    void timerCallback() override {
        keyboardActivityIndicator.setAlpha(0.2);
        stopTimer();
    }
private:
    AudioProcessorEditor* p;
    std::function<void(bool)> toggleCallback;
    ImageButton button{ "keyboardCaptureModeToggle" };
    ImageComponent logo, keyboardActivityIndicator;
    Image keyboardGrabEnabled, keyboardGrabDisabled, keyboardGrabMiniEnabled, keyboardGrabMiniDisabled, frameLogo, activityIndicator;
    bool buttonToggleState;

    class C final : public BorderedComponentBoundsConstrainer
    {
    public:
        explicit C(EditorKeyboardGrabWrapper& comp)
            : comp(comp) {}

        ComponentBoundsConstrainer* getWrappedConstrainer() const override
        {
            return comp.p != nullptr ? comp.p->getConstrainer() : nullptr;
        }

        BorderSize<int> getAdditionalBorder() const override
        {
            return BorderSize<int>(0, 0, HEADER_HEIGHT, 0);
        }

    private:
        EditorKeyboardGrabWrapper& comp;
    };

    C constrainer{ *this };
};
#endif

Component* createPlatformEditorWrapper(AudioProcessorEditor* ui, std::function<void(bool)> && toggleCallback) {
#ifdef _MSC_VER
    return new EditorKeyboardGrabWrapper(ui, std::move(toggleCallback));
#else
    return ui;
#endif
}

ComponentBoundsConstrainer* tryGetConstrainer(Component* comp) {
    auto* asEditor = dynamic_cast<AudioProcessorEditor*> (comp);
    if (asEditor) {
        return asEditor->getConstrainer();
    }
#ifdef _MSC_VER
    auto* asWrapper = dynamic_cast<EditorKeyboardGrabWrapper*> (comp);
    if (asWrapper) {
        return asWrapper->getConstrainer();
    }
#endif
    return nullptr;
}

struct CallbackTimer final : public Timer
{
    CallbackTimer(std::function<void()>&& callback, int interval)
        : callback(std::move(callback))
    {
        startTimer(interval);
    }

    ~CallbackTimer()
    {
        stopTimer();
    }

    void timerCallback() override {
        callback();
    }

private:
    std::function<void()> callback;
};

static bool rectangleOverlapsWithSomeDisplay(const juce::Rectangle<int> & rect) {
    auto lst = Desktop::getInstance().getDisplays().getRectangleList(false);
    std::cerr << "Display rectangles:\n";
    for (auto r : lst) {
        std::cerr << r.getTopLeft().getX() << "," << r.getTopLeft().getY() << " " << r.getWidth() << "x" << r.getHeight() << "\n";
    }
    std::cerr << "Testing: " << rect.getTopLeft().getX() << "," << rect.getTopLeft().getY() << " " << rect.getWidth() << "x" << rect.getHeight() << "\n";
    auto res = Desktop::getInstance().getDisplays().getRectangleList(false).intersectsRectangle(rect);
    std::cerr << "Result: " << res << "\n";
    return res;
}

class PluginWindow final : public DocumentWindow
{
public:
    PluginWindow (AudioProcessor* p, ConnectionWorker* cw, std::optional<std::pair<int, int>> position)
        : DocumentWindow (p->getName(),
                          LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId),
                          DocumentWindow::minimiseButton | DocumentWindow::closeButton),
          processor (p),
          context (cw)
    {
        std::cerr << "Creating plugin window for " << p << "\n";
        
        setSize(400, 300);
        setOpaque(true);
        setUsingNativeTitleBar(true);

        auto* editorUi = p->createEditorIfNeeded();
        if (!editorUi) {
            context->closePluginWindow();
            return;
        }
        setContentOwned(createPlatformEditorWrapper(editorUi, [this](bool enabled) {
            setKeyboardEnabled(enabled, true);
        }), true);
        setResizable(editorUi->isResizable(), false);

        setConstrainer (&constrainer);
        Point<int> proposedPosition(40, 40);
        if(position.has_value()) {
            const auto [x, y] = position.value();
            proposedPosition.setX(x);
            proposedPosition.setY(y);
        } else {
            auto primaryDisplay = Desktop::getInstance().getDisplays().getPrimaryDisplay();
            if(primaryDisplay) {
                auto r = primaryDisplay->userArea;
                proposedPosition.setX((r.getWidth() - getWidth()) / 2);
                proposedPosition.setY((r.getHeight() - getHeight()) / 2);
            }
        }
        
#ifdef _MSC_VER
        // on Windows, window rect excludes title bar
        juce::Rectangle<int> titleBarApprox(proposedPosition.getX(), proposedPosition.getY() - 20, getWidth(), 20);
#else
        // on Mac, window rect includes title bar
        juce::Rectangle<int> titleBarApprox(proposedPosition.getX(), proposedPosition.getY(), getWidth(), 20);
#endif

        if(!rectangleOverlapsWithSomeDisplay(titleBarApprox)) {
            proposedPosition.setX(40);
            proposedPosition.setY(40);
        }
        
        setTopLeftPosition(proposedPosition);
        
        context->currentWindowPosition = std::make_pair(getX(), getY());
        context->editorMovedHandler(context->currentWindowPosition);
#ifndef _MSC_VER
        addKeyListener(cw->keyListener.get());
        
        if(context->alwaysOnTop) {
#endif
            setVisible (true);
            toFront (true);
            setAlwaysOnTop(true);
            
#ifndef _MSC_VER
        }
        Process::setDockIconVisible(false);
#endif
        
        startActivationPoller();
    }

    ~PluginWindow() override
    {
#ifndef _MSC_VER
        removeKeyListener(context->keyListener.get());
#endif
        clearContentComponent();
    }

    void startActivationPoller() {
#ifdef _MSC_VER
        activationPoller = std::make_unique<CallbackTimer>([this] {
            HWND thisWindow = (HWND)getPeer()->getNativeHandle();
            if (thisWindow == NULL) {
                return;
            }
            if (IsWindowVisible(thisWindow)) {
                context->launchingChangedHandler(false);
                activationPoller.reset();
            }
        }, 50);
#endif
    }

    void activate() {
        if (isVisible() && isAlwaysOnTop()) {
            return;
        }
#ifdef _MSC_VER
        context->launchingChangedHandler(true);
#endif
        setVisible(true);
#ifndef _MSC_VER
        Process::setDockIconVisible(false);
#endif
        setAlwaysOnTop(true);
        toFront(true);
        startActivationPoller();
    }

    void deactivate() {
        activationPoller.reset();
        context->launchingChangedHandler(false);
        setVisible(false);
    }
    
#ifndef _MSC_VER
    void focusLost(FocusChangeType) override
    {
        if(context->keyListener) {
            context->keyListener->reset();
        }
    }
#endif

    void setKeyboardEnabled(bool isEnabled, bool callHandler)
    {
#ifdef _MSC_VER
        auto* asWrapper = dynamic_cast<EditorKeyboardGrabWrapper*> (getContentComponent());
        if (!asWrapper) {
            return;
        }
        asWrapper->setToggleState(isEnabled);
#endif
        if (callHandler) {
            context->keyboardEnabledHandler(isEnabled);
        }
    }

    void keyboardActivity()
    {
#ifdef _MSC_VER
        auto* asWrapper = dynamic_cast<EditorKeyboardGrabWrapper*> (getContentComponent());
        if (!asWrapper) {
            return;
        }
        asWrapper->keyboardActivity();
#endif
    }

    void moved() override
    {
        ResizableWindow::moved();
        auto newPosn = std::make_pair(getX(), getY());
        if(newPosn == context->currentWindowPosition) {
            return;
        }
        context->currentWindowPosition = newPosn;
        context->lastWindowPositionChangeTime = ConnectionWorker::clock_t::now();
        context->hasWindowPositionChange = true;
    }

    void closeButtonPressed() override
    {
        context->editorClosedHandler();
        context->closePluginWindow();
    }

    const AudioProcessor * processor;
    ConnectionWorker* context;

private:
    class DecoratorConstrainer final : public BorderedComponentBoundsConstrainer
    {
    public:
        explicit DecoratorConstrainer (DocumentWindow& windowIn)
            : window (windowIn) {}

        ComponentBoundsConstrainer* getWrappedConstrainer() const override
        {
            return tryGetConstrainer(window.getContentComponent());
        }

        BorderSize<int> getAdditionalBorder() const override
        {
            const auto nativeFrame = [&]() -> BorderSize<int>
            {
                if (auto* peer = window.getPeer())
                    if (const auto frameSize = peer->getFrameSizeIfPresent())
                        return *frameSize;

                return {};
            }();

            return nativeFrame.addedTo(window.getContentComponentBorder());
        }

    private:
        DocumentWindow& window;
    };

    DecoratorConstrainer constrainer { *this };

    float getDesktopScaleFactor() const override     { return 1.0f; }

    std::unique_ptr<CallbackTimer> activationPoller;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginWindow)
};

class WavtoolPlayhead final : public AudioPlayHead {
public:
    WavtoolPlayhead() {
        p.setTimeInSamples(0);
        p.setTimeInSeconds(0.0);
        p.setBpm(120.0);
        p.setTimeSignature(TimeSignature{.numerator=4, .denominator=4});
        p.setBarCount(0.0);
        p.setPpqPositionOfLastBarStart(0.0);
        p.setPpqPosition(0.0);
        p.setLoopPoints(LoopPoints{.ppqStart=0, .ppqEnd=0});
        p.setIsPlaying(false);
        p.setIsRecording(false);
        p.setIsLooping(false);
    }
    
    void setFromShm(const ShmSetTransportData & data, int sampleRate) {
        p.setTimeInSamples(data.samplePosition);
        p.setTimeInSeconds((double)data.samplePosition / (double)sampleRate);
        p.setBpm(data.bpm);
        p.setTimeSignature(TimeSignature{.numerator=data.beatNumerator, .denominator=data.beatDenominator});
        
        const auto beat = samplesToBeats(data.samplePosition, sampleRate, data.bpm);
        const auto barCount = std::floor(beat / (double)data.beatNumerator);
        
        const auto loopStartBeat = samplesToBeats(data.sampleLoopStart, sampleRate, data.bpm);
        const auto loopEndBeat = samplesToBeats(data.sampleLoopEnd, sampleRate, data.bpm);
        
        p.setBarCount(barCount);
        p.setPpqPositionOfLastBarStart(barCount * (double)data.beatNumerator);
        p.setPpqPosition(beat);
        p.setLoopPoints(LoopPoints{.ppqStart=loopStartBeat, .ppqEnd=loopEndBeat});
        p.setIsPlaying(data.playing);
        p.setIsRecording(data.recording);
        p.setIsLooping(data.looping);
    }
    
    void advance(int numSamples, int sampleRate) {
        if(!p.getIsPlaying()) {
            return;
        }
        const auto bpm = *p.getBpm();
        const auto lastBeat = samplesToBeats(*p.getTimeInSamples(), sampleRate, bpm);
        
        const auto loopPoints = *p.getLoopPoints();
        if(p.getIsLooping()
           && lastBeat >= loopPoints.ppqStart
           && lastBeat <= loopPoints.ppqEnd
           && loopPoints.ppqEnd != loopPoints.ppqStart
           && bpm > 0.0)
        {
            const auto deltaBeat = samplesToBeats(numSamples, sampleRate, bpm);
            const auto nextBeat = std::fmod(lastBeat + deltaBeat - loopPoints.ppqStart, loopPoints.ppqEnd - loopPoints.ppqStart) + loopPoints.ppqStart;
            p.setTimeInSamples(nextBeat / (double)bpm * 60.0 * (double)sampleRate);
        } else {
            p.setTimeInSamples(*p.getTimeInSamples() + numSamples);
        }
        
        p.setTimeInSeconds(*p.getTimeInSamples() / (double)sampleRate);
        
        const auto numerator = p.getTimeSignature()->numerator;
        const auto beat = samplesToBeats(*p.getTimeInSamples(), sampleRate, bpm);
        const auto barCount = std::floor(beat / (double)numerator);
        
        p.setBarCount(barCount);
        p.setPpqPositionOfLastBarStart(barCount * (double)numerator);
        p.setPpqPosition(beat);
    }
    
    virtual Optional<PositionInfo> getPosition() const override {
        return p;
    }

    virtual bool canControlTransport() override {
        return false;
    }

    virtual void transportPlay (bool) override { };
    virtual void transportRecord (bool) override { };
    virtual void transportRewind() override { };
    
private:
    double samplesToBeats(int64_t samples, int sampleRate, double bpm) {
        return (double)samples / ((double)sampleRate * 60.0) * bpm;
    }
    PositionInfo p;
};

ConnectionWorker::ConnectionWorker(const PluginDescription & desc,
                                   int bufferSize,
                                   int sampleRate,
                                   std::string && bridgeKeySeed,
                                   std::function<void()> && loadedHandler,
                                   std::function<void(const std::string &)> && errorHandler,
                                   std::function<void(const std::string_view &, bool)> && saveHandler,
                                   std::function<void(const ParameterDescription &)> && parameterGestureHandler,
                                   std::function<void(const KeyDescription &)> && keyHandler,
                                   std::function<void()> && editorClosedHandler,
                                   std::function<void(std::pair<int,int>)> && editorMovedHandler,
                                   std::function<void(const std::string_view &)> && trustHandler,
                                   std::function<void(int)> && latencyChangedHandler,
                                   std::function<void(bool)> && launchingChangedHandler,
                                   std::function<void(bool)>&& keyboardEnabledHandler)
: bufferSize(bufferSize),
  sampleRate(sampleRate),
  loadedHandler(std::move(loadedHandler)),
  errorHandler(std::move(errorHandler)),
  saveHandler(std::move(saveHandler)),
  parameterGestureHandler(std::move(parameterGestureHandler)),
  editorClosedHandler(std::move(editorClosedHandler)),
  editorMovedHandler(std::move(editorMovedHandler)),
  trustHandler(std::move(trustHandler)),
  latencyChangedHandler(std::move(latencyChangedHandler)),
  launchingChangedHandler(std::move(launchingChangedHandler)),
  keyboardEnabledHandler(std::move(keyboardEnabledHandler)),
  keyListener(std::make_unique<WavtoolKeyListener>(std::move(keyHandler))),
  playhead(std::make_unique<WavtoolPlayhead>()),
  isLoadingState(false),
  thunkQ(100),
  hasUncommittedStateUpdate(false),
  gesturesActive(0),
  alwaysOnTop(false),
  audioBuffer(2, 1024),
  triggerAsyncUpdateSema(0),
  triggerAsyncUpdateShouldExit(false),
  stateIntegrity(bridgeKeySeed),
  waitingForTrustResolution(false),
  sendLatencyChangedNextBlock(false),
  lastPeriodicSaveTime(clock_t::now()),
  hasWindowPositionChange(false),
  lastWindowPositionChangeTime(clock_t::now()),
  currentWindowPosition(std::make_pair(0,0)),
  unsetLaunchingArmed(true),
  keyboardEnabled(false),
  previousInputBuses(-1),
  previousOutputBuses(-1),
  desiredInputBuses(-1),
  desiredOutputBuses(-1),
  wantsBusLayoutUpdate(false)
{
    RTLogger::log("Loading plugin %s from %s", desc.name.toRawUTF8(), desc.fileOrIdentifier.toRawUTF8());
    
    triggerAsyncUpdateThread = std::make_unique<std::thread>([this] {
        for(;;) {
            bool didAcquire = triggerAsyncUpdateSema.try_acquire_for(std::chrono::milliseconds(100));
            if(triggerAsyncUpdateShouldExit) {
                break;
            }
            if(didAcquire) {
                triggerAsyncUpdate();
            }
        }
    });
    
    pluginToLoad = std::make_unique<PluginDescription>(desc);
    enqueueThunk([this]{
        loadPlugin();
    });
}

ConnectionWorker::~ConnectionWorker() {
    pluginWindow.reset();
    launchingChangedHandler(false);
    plugin.reset();
    triggerAsyncUpdateShouldExit = true;
    triggerAsyncUpdateThread->join();
    triggerAsyncUpdateThread.reset();
}

MidiBuffer ConnectionWorker::getClearMidiBuffer() {
    MidiBuffer res;
    for(int i = 0; i < 128; i++) {
        res.addEvent(MidiMessage{0x80, i, 64}, 0);
    }
    return res;
}

void ConnectionWorker::setKeyboardEnabled(bool isEnabled) {
    enqueueThunk([this,isEnabled] {
        keyboardEnabled = isEnabled;
        if (pluginWindow) {
            pluginWindow->setKeyboardEnabled(isEnabled, false);
        }
    });
}

void ConnectionWorker::keyboardActivity() {
    enqueueThunk([this] {
        if (pluginWindow) {
            pluginWindow->keyboardActivity();
        }
    });
}

void ConnectionWorker::audioProcessorParameterChanged(AudioProcessor *processor, int parameterIndex, float newValue) {
    // TODO we want to capture changes of non-automated paramters that also aren't mapped to MIDI CCs
    if(processor != plugin.get()) {
        return;
    }
    RTLogger::log("audioProcessorParameterChanged");
    if(gesturesActive > 0 || parameterIndex < 0 || isLoadingState) {
        return;
    }
    auto didChange = parameterIndex >= pluginParameterValues.size() && parametersDidChange(); // need to rebuild values vector
    if(didChange || pluginParameterValues[parameterIndex] != newValue) {
        RTLogger::log("audioProcessorParameterChanged: gesturesActive was already zero and parametersDidChange");
        pluginParameterValues[parameterIndex] = newValue;
        stateUpdateCallbackFired();
    }
}

void ConnectionWorker::audioProcessorChanged(AudioProcessor *processor, const ChangeDetails &details) {
    auto paramsDidChange = parametersDidChange();
    if(details.latencyChanged) {
        sendLatencyChangedNextBlock = true;
    }
    if(isLoadingState || !(details.parameterInfoChanged || paramsDidChange)) {
        RTLogger::log("ignoring audioProcessorChanged: latencyChanged=%d, parameterInfoChanged=%d, isLoadingState=%d, paramsDidChange=%d", (int)details.latencyChanged, (int)details.parameterInfoChanged, (int)isLoadingState, (int)paramsDidChange);
        return;
    }
    RTLogger::log("audioProcessorChanged latencyChanged=%d, nonParam=%d, paramInfo=%d, programChange=%d", (int)details.latencyChanged, (int)details.nonParameterStateChanged, (int)details.parameterInfoChanged, (int)details.programChanged);
    if(processor != plugin.get()) {
        return;
    }
    stateUpdateCallbackFired();
}

void ConnectionWorker::audioProcessorParameterChangeGestureBegin(AudioProcessor * processor, int parameterIndex) {
    if(processor != plugin.get()) {
        return;
    }
    gesturesActive++;
    if(parameterIndex < 0 || parameterIndex >= pluginParameters.size()) {
        return;
    }
    auto param = pluginParameters[parameterIndex];
    if(!param) {
        return;
    }
    parameterGestureHandler(ParameterDescription{.index = parameterIndex, .name = param->getName(128).toStdString()});
}

void ConnectionWorker::audioProcessorParameterChangeGestureEnd(AudioProcessor *processor, int parameterIndex) {
    if(processor != plugin.get()) {
        return;
    }
    gesturesActive--;
    if(gesturesActive > 0 || parameterIndex < 0 || parameterIndex >= pluginParameters.size() || !pluginParameters[parameterIndex]) {
        return;
    }
    auto newValue = pluginParameters[parameterIndex]->getValue();
    auto didChange = parameterIndex >= pluginParameterValues.size() && parametersDidChange(); // need to rebuild values vector
    if(didChange || pluginParameterValues[parameterIndex] != newValue) {
        RTLogger::log("gesturesActive hit zero and parametersDidChange");
        pluginParameterValues[parameterIndex] = newValue;
        stateUpdateCallbackFired();
    }
}

bool ConnectionWorker::parametersDidChange() {
    if(pluginParameterValues.size() != pluginParameters.size()) {
        pluginParameterValues.resize(pluginParameters.size());
        for(size_t i = 0; i < pluginParameters.size(); i++) {
            if(!pluginParameters[i]) {
                continue;
            }
            pluginParameterValues[i] = pluginParameters[i]->getValue();
        }
        return true;
    }
    
    bool didChange = false;
    for(size_t i = 0; i < pluginParameters.size(); i++) {
        if(!pluginParameters[i]) {
            continue;
        }
        auto currentVal = pluginParameters[i]->getValue();
        if(currentVal != pluginParameterValues[i]) {
            didChange = true;
        }
        pluginParameterValues[i] = currentVal;
    }
    
    return didChange;
}

void ConnectionWorker::stateUpdateCallbackFired() {
    lastStateUpdateCallbackTime = clock_t::now();
    hasUncommittedStateUpdate = true;
}

void ConnectionWorker::handleBlock(ShmData & block) {
    if(block.messageType == ShmMessageType::SET_TRANSPORT) {
        setTransport(block.setTransport);
    } else if(block.messageType == ShmMessageType::PROCESS_BLOCK) {
        processBlock(block.processBlock, block.shouldReset);
        block.shouldReset = false;
    } else {
        throw std::runtime_error("Bad block type");
    }
}

void ConnectionWorker::loadState(const std::string_view & data) {
    auto dataLocal = std::string(data);
    enqueueThunk([this, dataLocal=std::move(dataLocal)]() mutable {
        if(!loadStateOnMessageThread(dataLocal)) {
            pendingStateChanges.emplace_back(std::move(dataLocal));
        }
    });
}

bool ConnectionWorker::loadStateOnMessageThread(const std::string_view & data) {
    if(!plugin) {
        return true;
    }
    std::string detachedState;
    try {
        detachedState = stateIntegrity.verify(data);
    } catch(const StateIntegrity::PublicKeyNotTrusted & e) {
        if(e.getIsKnown()) {
            std::cerr << "WARNING: refusing to load state signed by denylisted key\n";
            return true;
        }
        trustHandler(e.getPublicKey());
        waitingForTrustResolution = true;
        alwaysOnTopEdgeTrigger();
        return false;
    } catch(const StateIntegrity::SignatureVerificationFailed &) {
        std::cerr << "WARNING: refusing to load VST state with malformed signature\n";
        return true;
    }
    if(detachedState == lastSavedState) {
        std::cerr << "Not reloading state equal to last saved state\n";
        return true;
    }
    std::cerr << "Loading state!\n";
    lastSavedState = std::move(detachedState);
    isLoadingState = true;
    plugin->setStateInformation(lastSavedState.data(), (int)lastSavedState.size());
    parametersDidChange();
    isLoadingState = false;
    return true;
}

void ConnectionWorker::trustResolution(const XmlElement & elem) {
    XmlElement localElem(elem);
    enqueueThunk([this,localElem=std::move(localElem)] {
        stateIntegrity.loadTrustStore(localElem);
        waitingForTrustResolution = false;
        while(!pendingStateChanges.empty()) {
            auto data = std::move(pendingStateChanges.front());
            pendingStateChanges.pop_front();
            if(!loadStateOnMessageThread(data)) {
                pendingStateChanges.emplace_front(std::move(data));
                break;
            }
        }
        alwaysOnTopEdgeTrigger();
    });
}

void ConnectionWorker::requestSaveState(bool periodic) {
    hasUncommittedStateUpdate = false;
    lastPeriodicSaveTime = clock_t::now();
    enqueueThunk([=,this] {
        if(!plugin) {
            return;
        }
        MemoryBlock mb;
        plugin->getStateInformation(mb);
        auto view = std::string_view((char*)mb.getData(), mb.getSize());
        if(lastSavedState != view) {
            std::cerr << "Saving state! periodic=" << periodic << "\n";
            lastSavedState = view;
            saveHandler(stateIntegrity.sign(view), periodic);
        }
    });
}

void ConnectionWorker::setTransport(ShmSetTransportData & data)
{
    if(!plugin) {
        return;
    }
    std::stringstream ss;
    ss << "setTransport\n"
       << "  bpm:       " << data.bpm << "\n"
       << "  signature: " << data.beatNumerator << "/" << data.beatDenominator << "\n"
       << "  position:  " << data.samplePosition << "\n"
       << "  loop:      [" << data.sampleLoopStart << ", " << data.sampleLoopEnd << "] " << (data.looping ? "on" : "off") << "\n"
       << "  playing:   " << (data.playing ? "yes" : "no") << "\n"
       << "  recording: " << (data.recording ? "yes" : "no");
    auto str = ss.str();
    RTLogger::log("%s", str.c_str());
    playhead->setFromShm(data, sampleRate);
    
    data.samplesLatency = plugin->getLatencySamples();
}

void ConnectionWorker::processBlock(ShmProcessBlockData & block, bool shouldReset)
{
    if(!plugin) {
        return;
    }
    
    auto cexExpectedValue = true;
    if(sendLatencyChangedNextBlock.compare_exchange_strong(cexExpectedValue, false)) {
        latencyChangedHandler(plugin->getLatencySamples());
    }
    
    if(block.numSamplesPerChannel > bufferSize || block.numSamplesPerChannel == 0) {
        throw std::runtime_error("block.numSamplesPerChannel must be between 1 and bufferSize");
    }
    if(block.parameterSampleStride > block.numSamplesPerChannel || block.parameterSampleStride == 0) {
        throw std::runtime_error("block.parameterSampleStride must be between 1 and block.numSamplesPerChannel");
    }
    if(block.numSamplesPerChannel % block.parameterSampleStride != 0) {
        throw std::runtime_error("block.numSamplesPerChannel must be divisible by block.parameterSampleStride");
    }

    // configure buses
    if(desiredInputBuses != block.numInputs
       || desiredOutputBuses != block.numOutputs
       || previousInputBuses != desiredInputBuses
       || previousOutputBuses != desiredOutputBuses) {
        desiredInputBuses = block.numInputs;
        desiredOutputBuses = block.numOutputs;
        wantsBusLayoutUpdate = true;
    }
    
    if(wantsBusLayoutUpdate) {
        for(int i = 0; i < 2; i++) {
            memset(block.channelss[0].samples[i], 0, sizeof(float) * MAX_BUFFER_SIZE);
        }
        block.numMidiEvents = 0;
        return;
    }
    
    const auto pluginNumChannels = jmax(plugin->getTotalNumInputChannels(), plugin->getTotalNumOutputChannels());
    if(pluginNumChannels > audioBuffer.getNumChannels() || bufferSize > audioBuffer.getNumSamples()) {
        audioBuffer.setSize(pluginNumChannels, bufferSize);
    }
    
    if(shouldReset) {
        auto clearMidiBuffer = getClearMidiBuffer();
        AudioBuffer<float> clearBuffer(audioBuffer.getArrayOfWritePointers(), pluginNumChannels, 64);
        plugin->processBlock(clearBuffer, clearMidiBuffer);
        plugin->reset();
    }
    
    const uint32_t maxSubblockCount = block.numSamplesPerChannel / block.parameterSampleStride;
    
    bool subblockCuts[MAX_BUFFER_SIZE];
    for(uint32_t i = 0; i < maxSubblockCount - 1; i++) {
        subblockCuts[i] = false;
    }
    for(uint32_t i = 0; i < block.numParameterAutomations; i++) {
        const auto & aut = block.automations[i];
        for(uint32_t j = 0; j < maxSubblockCount - 1; j++) {
            const auto leftVal = aut.values[j];
            const auto rightVal = aut.values[j + 1];
            if(leftVal != rightVal) {
                subblockCuts[j] = true;
            }
        }
    }
    uint32_t subblockLengths[MAX_BUFFER_SIZE];
    for(uint32_t i = 0; i < maxSubblockCount; i++) {
        subblockLengths[i] = 0;
    }
    uint32_t numSubblocks = 0;
    for(uint32_t i = 0; i < maxSubblockCount - 1; i++) {
        subblockLengths[numSubblocks] += block.parameterSampleStride;
        if(subblockCuts[i]) {
            numSubblocks += 1;
        }
    }
    subblockLengths[numSubblocks] += block.parameterSampleStride;
    numSubblocks += 1;
    
    auto outputMidiBuffer = MidiBuffer();
    
    uint32_t subblockStartSample = 0;
    for(uint32_t subblockIndex = 0; subblockIndex < numSubblocks; subblockIndex++)
    {
        // set parameters
        for(uint32_t i = 0; i < block.numParameterAutomations; i++) {
            const auto & aut = block.automations[i];
            if(aut.parameterIndex >= pluginParameters.size()) {
                continue;
            }
            auto * param = pluginParameters[aut.parameterIndex];
            if(param == nullptr) {
                continue;
            }
            param->setValue(aut.values[subblockStartSample / block.parameterSampleStride]);
        }
        
        // load input audio
        const auto subblockLength = subblockLengths[subblockIndex];
        AudioBuffer<float> subblockBuffer(audioBuffer.getArrayOfWritePointers(), pluginNumChannels, subblockLength);
        const auto writePtrs = subblockBuffer.getArrayOfWritePointers();
        const auto inBusesToLoad = jmin(plugin->getBusCount(true), (int)block.numInputs);
        for(int busIndex = 0; busIndex < inBusesToLoad; busIndex++) {
            const auto channelLayout = plugin->getChannelLayoutOfBus(true, busIndex);
            const auto leftIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::left);
            const auto rightIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::right);
            const auto centreIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::centre);
            
            if(leftIdx != -1 && rightIdx != -1) {
                // bus is stereo
                const auto leftBufIdx = plugin->getChannelIndexInProcessBlockBuffer(true, busIndex, leftIdx);
                const auto rightBufIdx = plugin->getChannelIndexInProcessBlockBuffer(true, busIndex, rightIdx);
                memcpy(writePtrs[leftBufIdx],
                       block.channelss[busIndex].samples[0] + subblockStartSample,
                       sizeof(float) * subblockLength);
                memcpy(writePtrs[rightBufIdx],
                       block.channelss[busIndex].samples[1] + subblockStartSample,
                       sizeof(float) * subblockLength);
            } else if(centreIdx != -1) {
                // bus is mono
                const auto centreBufIdx = plugin->getChannelIndexInProcessBlockBuffer(true, busIndex, centreIdx);
                const auto & inSamples = block.channelss[busIndex].samples;
                for(size_t i = 0; i < subblockLength; i++) {
                    writePtrs[centreBufIdx][i] = (inSamples[0][i + subblockStartSample] + inSamples[1][i + subblockStartSample]) / 2.f;
                }
            }
        }
        
        // load input midi
        auto midiBuffer = MidiBuffer();
        for(int i = 0; i < block.numMidiEvents; i++) {
            const auto & evt = block.midiEvents[i];
            if(evt.sampleIndex >= subblockStartSample && evt.sampleIndex < subblockStartSample + subblockLength) {
                midiBuffer.addEvent(evt.data, evt.length, evt.sampleIndex - subblockStartSample);
            }
        }
        
        plugin->processBlock(subblockBuffer, midiBuffer);
        
        // unload output midi
        outputMidiBuffer.addEvents(midiBuffer, 0, subblockLength, subblockStartSample);
        
        // unload output audio
        const auto outBusesToLoad = jmin(plugin->getBusCount(false), (int)block.numOutputs);
        for(int busIndex = 0; busIndex < outBusesToLoad; busIndex++) {
            const auto channelLayout = plugin->getChannelLayoutOfBus(false, busIndex);
            const auto leftIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::left);
            const auto rightIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::right);
            const auto centreIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::centre);
            
            if(leftIdx != -1 && rightIdx != -1) {
                // bus is stereo
                const auto leftBufIdx = plugin->getChannelIndexInProcessBlockBuffer(false, busIndex, leftIdx);
                const auto rightBufIdx = plugin->getChannelIndexInProcessBlockBuffer(false, busIndex, rightIdx);
                memcpy(block.channelss[busIndex].samples[0] + subblockStartSample,
                       writePtrs[leftBufIdx],
                       sizeof(float) * subblockLength);
                memcpy(block.channelss[busIndex].samples[1] + subblockStartSample,
                       writePtrs[rightBufIdx],
                       sizeof(float) * subblockLength);
            } else if(centreIdx != -1) {
                // bus is mono
                const auto centreBufIdx = plugin->getChannelIndexInProcessBlockBuffer(false, busIndex, centreIdx);
                auto & outSamples = block.channelss[busIndex].samples;
                for(size_t i = 0; i < subblockLength; i++) {
                    auto val = writePtrs[centreBufIdx][i];
                    val /= 2.f;
                    outSamples[0][i + subblockStartSample] = val;
                    outSamples[1][i + subblockStartSample] = val;
                }
            }
        }
        
        playhead->advance((int)subblockLength, sampleRate);
        subblockStartSample += subblockLength;
    }
    
    // unload output midi
    block.numMidiEvents = 0;
    for(const auto juceEvt : outputMidiBuffer) {
        if(block.numMidiEvents >= MAX_MIDI_EVENTS) {
            break;
        }
        if(juceEvt.numBytes > MAX_MIDI_EVENT_LENGTH) {
            continue;
        }
        auto & evt = block.midiEvents[block.numMidiEvents++];
        evt.sampleIndex = juceEvt.samplePosition;
        evt.length = juceEvt.numBytes;
        memcpy(evt.data, juceEvt.data, juceEvt.numBytes);
    }
}

void ConnectionWorker::doBusLayoutUpdate() {
            plugin->releaseResources();
    auto numInputs = desiredInputBuses.load();
    auto numOutputs = desiredOutputBuses.load();
    for(const auto & [isInput, desiredBusCount] : {
        std::make_pair(false, numOutputs),
        std::make_pair(true, numInputs)
    }) {
        const int deltaBuses = plugin->getBusCount(isInput) - desiredBusCount;
        if(deltaBuses > 0) {
            for(int i = 0; i < deltaBuses; i++) {
                if(!plugin->removeBus(isInput)) {
                    break;
                }
            }
        } else {
            for(int i = 0; i < -deltaBuses; i++) {
                if(!plugin->addBus(isInput)) {
                    break;
                }
            }
        }
        for(int busIndex = 0; busIndex < plugin->getBusCount(isInput); busIndex++) {
            auto bus = plugin->getBus(isInput, busIndex);
            if(busIndex < desiredBusCount) {
                bus->setCurrentLayout(AudioChannelSet::stereo());
            } else {
                bus->setCurrentLayout(AudioChannelSet::disabled());
            }
        }
    }
    plugin->prepareToPlay(sampleRate, bufferSize);
    previousInputBuses = numInputs;
    previousOutputBuses = numOutputs;
}

void ConnectionWorker::enqueueThunk(uWS::MoveOnlyFunction<void()> && thunk) {
    // To prevent livelock between the message thread and realtime thread, this cannot block
    if(!thunkQ.try_emplace(std::move(thunk))) {
        RTLogger::log("ConnectionWorker::enqueueThunk failed because the queue is full!");
    }
    try {
        triggerAsyncUpdateSema.release();
    } catch(std::system_error &) {
        RTLogger::log("failed to release triggerAsyncUpdateSema");
    }
}

void ConnectionWorker::handleAsyncUpdate()
{
    for(;;) {
        uWS::MoveOnlyFunction<void()> thunk;
        if(!thunkQ.try_pop(thunk)) {
            break;
        }
        thunk();
    }
}

void ConnectionWorker::loadPlugin()
{
    if(!formatManager) {
        formatManager = std::make_unique<AudioPluginFormatManager>();
        formatManager->addDefaultFormats();
    }
    
    formatManager->createPluginInstanceAsync(*pluginToLoad,
                                             44100.0,
                                             1024,
                                             [this] (std::unique_ptr<AudioPluginInstance> instance, const String& error)
                                             {
                                                 addPluginCallback(std::move(instance), error);
                                             });
}

void ConnectionWorker::addPluginCallback(std::unique_ptr<AudioPluginInstance> instance, const String& error)
{
    if(!instance) {
        errorHandler("Failed to initialize plugin!\n");
        return;
    }
    
    instance->setProcessingPrecision(AudioProcessor::ProcessingPrecision::singlePrecision);
    instance->enableAllBuses();
    instance->disableNonMainBuses();
    instance->setNonRealtime(false);
    instance->setPlayHead(playhead.get());
    instance->addListener(this);
    instance->setPlayConfigDetails(2, 2, sampleRate, bufferSize);
    instance->prepareToPlay(sampleRate, bufferSize);
    
    auto params = instance->getParameters();
    size_t maxIndex = 0;
    for(auto param : params) {
        auto index = param->getParameterIndex();
        if(index < 0) {
            continue;
        }
        maxIndex = std::max(maxIndex, (size_t)param->getParameterIndex());
    }
    
    pluginParameters.clear();
    pluginParameters.resize(maxIndex + 1, nullptr);
    
    for(auto param : params) {
        auto index = param->getParameterIndex();
        if(index < 0 || index >= pluginParameters.size()) {
            continue;
        }
        pluginParameters[index] = param;
    }

    plugin = std::move(instance);
    lastStateUpdateCallbackTime = clock_t::now();
    parametersDidChange();
    loadedHandler();
    pluginLoadTime = clock_t::now();
    startTimer(50);
}

void ConnectionWorker::openPluginWindow(std::optional<std::pair<int,int>> pos)
{
    enqueueThunk([=,this] {
        if (!plugin) {
            return;
        }
        if(!pluginWindow) {
#ifdef _MSC_VER
            launchingChangedHandler(true);
#endif
            pluginWindow = std::make_unique<PluginWindow>(plugin.get(), this, pos);
            pluginWindow->setKeyboardEnabled(keyboardEnabled, false);
        }
        if (pluginWindow && pluginWindow->isVisible() && pluginWindow->isAlwaysOnTop()) {
            pluginWindow->toFront(true);
        }
        alwaysOnTopEdgeTrigger();
    });
}

void ConnectionWorker::closePluginWindow()
{
    enqueueThunk([this] {
        launchingChangedHandler(false);
        pluginWindow.reset();
        keyListener->reset();
    });
}

void ConnectionWorker::alwaysOnTopEdgeTrigger()
{
    if(!pluginWindow) {
        return;
    }
    if(alwaysOnTop && !waitingForTrustResolution) {
        pluginWindow->activate();
    } else {
        pluginWindow->deactivate();
    }
}

void ConnectionWorker::setPluginWindowAlwaysOnTop(bool onTop)
{
    enqueueThunk([this, onTop] {
        if (onTop == alwaysOnTop) {
            return;
        }
        alwaysOnTop = onTop;
        alwaysOnTopEdgeTrigger();
    });
}

void ConnectionWorker::timerCallback()
{
    auto now = clock_t::now();
    if(hasUncommittedStateUpdate && now - lastStateUpdateCallbackTime.load() > std::chrono::milliseconds(100)) {
        requestSaveState(false);
    }
    if(now - lastPeriodicSaveTime.load() > std::chrono::seconds(5)) {
        requestSaveState(true);
    }
    if(hasWindowPositionChange && now - lastWindowPositionChangeTime.load() > std::chrono::milliseconds(300)) {
        enqueueThunk([this] {
            hasWindowPositionChange = false;
            editorMovedHandler(currentWindowPosition);
        });
    }
    if (unsetLaunchingArmed && now - pluginLoadTime.load() > std::chrono::milliseconds(200)) {
        unsetLaunchingArmed = false;
        enqueueThunk([this] {
            launchingChangedHandler(false);
        });
    }
    if(wantsBusLayoutUpdate) {
        enqueueThunk([this] {
            if(wantsBusLayoutUpdate) {
                doBusLayoutUpdate();
                wantsBusLayoutUpdate = false;
            }
        });
    }
}
