// Copyright 2023 Kushview, LLC <info@kushview.net>
// SPDX-License-Identifier: GPL-3.0-or-later

#include "nodes/baseprocessor.hpp"
#include "nodes/audiorouter.hpp"
#include "common.hpp"

#define TRACE_AUDIO_ROUTER(output)

namespace element {

AudioRouterNode::AudioRouterNode (int ins, int outs)
    : Processor (0),
      numSources (ins),
      numDestinations (outs),
      state (ins, outs),
      toggles (ins, outs),
      nextToggles (ins, outs)
{
    setName ("Audio Router");

    fadeIn.setFadesIn (true);
    fadeIn.setLength (fadeLengthSeconds);
    fadeOut.setFadesIn (false);
    fadeOut.setLength (fadeLengthSeconds);

    clearPatches();

    auto* program = programs.add (new Program ("Linear Stereo"));
    program->matrix.resize (ins, outs);
    for (int i = 0; i < jmin (ins, outs); ++i)
        program->matrix.set (i, i, true);
    setMatrixState (program->matrix);

    if (ins == 4 && outs == 4)
    {
        program = programs.add (new Program ("Inverse Stereo"));
        program->matrix.resize (ins, outs);
        program->matrix.set (0, 1, true);
        program->matrix.set (1, 0, true);
        program->matrix.set (2, 3, true);
        program->matrix.set (3, 2, true);

        program = programs.add (new Program ("1-2 to 1-2"));
        program->matrix.resize (ins, outs);
        program->matrix.set (0, 0, true);
        program->matrix.set (1, 1, true);

        program = programs.add (new Program ("1-2 to 3-4"));
        program->matrix.resize (ins, outs);
        program->matrix.set (0, 2, true);
        program->matrix.set (1, 3, true);

        program = programs.add (new Program ("3-4 to 1-2"));
        program->matrix.resize (ins, outs);
        program->matrix.set (2, 0, true);
        program->matrix.set (3, 1, true);

        program = programs.add (new Program ("3-4 to 3-4"));
        program->matrix.resize (ins, outs);
        program->matrix.set (2, 2, true);
        program->matrix.set (3, 3, true);
    }
}

AudioRouterNode::~AudioRouterNode() {}

void AudioRouterNode::setCurrentProgram (int index)
{
    if (auto* program = programs[index])
    {
        currentProgram = index;
        setMatrixState (program->matrix);
    }
}

void AudioRouterNode::applyMatrix (const MatrixState& matrix)
{
    jassert (matrix.sameSizeAs (state));
    ToggleGrid newPatches (matrix);
    {
        ScopedLock sl (getLock());
        nextToggles.swapWith (newPatches);
        togglesChanged = true; // initiate the crossfade
    }

    sendChangeMessage();
}

String AudioRouterNode::getSizeString() const
{
    int s = 0, d = 0;
    {
        ScopedLock sl (lock);
        s = numSources;
        d = numDestinations;
    }

    String result (s);
    result << "x" << d;
    return result;
}

void AudioRouterNode::setSize (int newIns, int newOuts, bool async)
{
    newIns = jmax (1, newIns);
    newOuts = jmax (1, newOuts);

    {
        ScopedLock sl1 (getLock());
        if (newIns == numSources && newOuts == numDestinations)
            return;
    }

    state.resize (newIns, newOuts, true);
    ToggleGrid newPatches (state);
    ToggleGrid newNextPatches (state);

    {
        ScopedLock sl (getLock());
        nextToggles.swapWith (newNextPatches);
        toggles.swapWith (newPatches);
        numSources = newIns;
        numDestinations = newOuts;
        sizeChanged = true; // initiate the size change
    }

    rebuildPorts = true;
    if (async)
    {
        triggerPortReset();
    }
    else
    {
        refreshPorts();
        portsChanged();
    }

    sendChangeMessage();
}

void AudioRouterNode::setMatrixState (const MatrixState& matrix)
{
    state = matrix;
    applyMatrix (state);
}

MatrixState AudioRouterNode::getMatrixState() const
{
    return state;
}

void AudioRouterNode::render (RenderContext& rc)
{
    jassert (rc.midi.getNumBuffers() == 1);
    const int numFrames = rc.audio.getNumSamples();
    const int numChannels = rc.audio.getNumChannels();

    tempAudio.setSize (numChannels, numFrames, false, false, true);
    tempAudio.clear (0, numFrames);

    if (sizeChanged)
    {
        fadeIn.reset();
        fadeOut.reset();
        sizeChanged = false;
        TRACE_AUDIO_ROUTER ("size changed");
    }

    if (togglesChanged)
    {
        fadeIn.reset();
        fadeIn.startFading();
        fadeOut.reset();
        fadeOut.startFading();
        togglesChanged = false;
        TRACE_AUDIO_ROUTER ("fade start");
    }

    if (numSources > numChannels || numDestinations > numChannels)
    {
        rc.audio.clear();
        rc.midi.clear();
        return;
    }

    if (fadeIn.isActive() || fadeOut.isActive())
    {
        auto framesToProcess = numFrames;
        int frame = 0;
        ScopedLock sl (lock);

        float fadeInGain = 0.0f;
        float fadeOutGain = 1.0f;

        while (--framesToProcess >= 0)
        {
            fadeInGain = fadeIn.isActive() ? fadeIn.getNextEnvelopeValue() : 1.0f;
            fadeOutGain = fadeOut.isActive() ? fadeOut.getNextEnvelopeValue() : 0.0f;

            if (! fadeIn.isActive() && ! fadeOut.isActive())
            {
                TRACE_AUDIO_ROUTER ("last frame fade in gain  : " << fadeInGain);
                TRACE_AUDIO_ROUTER ("last frame fade out gain : " << fadeOutGain);
            }

            for (int i = 0; i < numSources; ++i)
            {
                for (int j = 0; j < numDestinations; ++j)
                {
                    if (toggles.get (i, j) && nextToggles.get (i, j))
                    {
                        // no patch change and on means 1 to 1 mix
                        tempAudio.getWritePointer (j)[frame] +=
                            rc.audio.getReadPointer (i)[frame];
                    }
                    else if (! toggles.get (i, j) && ! nextToggles.get (i, j))
                    {
                        // no patch change and off means no fade and zero'd
                        tempAudio.getWritePointer (j)[frame] += 0.0f;
                    }
                    else if (! toggles.get (i, j) && nextToggles.get (i, j))
                    {
                        tempAudio.getWritePointer (j)[frame] +=
                            (rc.audio.getReadPointer (i)[frame] * fadeInGain);
                    }
                    else if (toggles.get (i, j) && ! nextToggles.get (i, j))
                    {
                        tempAudio.getWritePointer (j)[frame] +=
                            (rc.audio.getReadPointer (i)[frame] * fadeOutGain);
                    }
                }
            }

            ++frame;

            if (! fadeIn.isActive() && ! fadeOut.isActive())
                break;
        }

        if (! fadeOut.isActive() && ! fadeIn.isActive())
        {
            TRACE_AUDIO_ROUTER ("fade stopped @ frame: " << (frame));
            TRACE_AUDIO_ROUTER ("fade in level  : " << fadeInGain);
            TRACE_AUDIO_ROUTER ("fade out level : " << fadeOutGain);

            if (framesToProcess > 0)
            {
                TRACE_AUDIO_ROUTER ("rendering " << framesToProcess << " remainging frames");
                for (int i = 0; i < numSources; ++i)
                {
                    for (int j = 0; j < numDestinations; ++j)
                    {
                        if (toggles.get (i, j) && nextToggles.get (i, j))
                        {
                            // no patch change and on means 1 to 1 mix
                            tempAudio.addFrom (j, frame, rc.audio.getReadPointer (i, frame), framesToProcess);
                        }
                        else if (! toggles.get (i, j) && ! nextToggles.get (i, j))
                        {
                            // patch not changed and of, so clear the out channel.
                            // nothing to do
                        }
                        else if (! toggles.get (i, j) && nextToggles.get (i, j))
                        {
                            tempAudio.addFromWithRamp (j, frame, rc.audio.getReadPointer (i, frame), framesToProcess, fadeInGain, 1.0f);
                        }
                        else if (toggles.get (i, j) && ! nextToggles.get (i, j))
                        {
                            tempAudio.addFromWithRamp (j, frame, rc.audio.getReadPointer (i, frame), framesToProcess, fadeOutGain, 0.0f);
                        }
                        else
                        {
                            // this would be an unhandled fading scenario
                            jassertfalse;
                        }
                    }
                }
            }

            toggles.swapWith (nextToggles);
        }
    }
    else
    {
        ScopedLock sl (lock);
        for (int i = 0; i < numSources; ++i)
            for (int j = 0; j < numDestinations; ++j)
                if (toggles.get (i, j))
                    tempAudio.addFrom (j, 0, rc.audio, i, 0, numFrames);
    }

    for (int c = 0; c < numChannels; ++c)
        rc.audio.copyFrom (c, 0, tempAudio.getReadPointer (c), numFrames);
    rc.midi.clear();
}

void AudioRouterNode::getState (MemoryBlock& block)
{
    MemoryOutputStream stream (block, false);
    state.createValueTree().writeToStream (stream);
}

void AudioRouterNode::setState (const void* data, int sizeInBytes)
{
    const auto tree = ValueTree::readFromData (data, (size_t) sizeInBytes);

    if (tree.isValid())
    {
        MatrixState matrix;
        matrix.restoreFromValueTree (tree);
        jassert (matrix.getNumRows() > 0 && matrix.getNumColumns() > 0);
        if (matrix.getNumRows() > 0 && matrix.getNumColumns() > 0)
        {
            state = matrix;

            ToggleGrid newPatches (state);
            ToggleGrid newNextPatches (state);
            {
                ScopedLock sl (getLock());
                numSources = matrix.getNumRows();
                numDestinations = matrix.getNumColumns();
                nextToggles.swapWith (newNextPatches);
                toggles.swapWith (newPatches);
                sizeChanged = true;
            }

            rebuildPorts = true;
            sendChangeMessage();
            triggerPortReset();
        }
    }
}

void AudioRouterNode::setWithoutLocking (int src, int dst, bool set)
{
    jassert (src >= 0 && src < numSources && dst >= 0 && dst < numDestinations);
    toggles.set (src, dst, set);
    state.set (src, dst, set);
}

void AudioRouterNode::set (int src, int dst, bool patched)
{
    jassert (src >= 0 && src < numSources && dst >= 0 && numDestinations < 4);
    toggles.set (src, dst, patched);
    state.set (src, dst, patched);
}

void AudioRouterNode::clearPatches()
{
    {
        ScopedLock sl (getLock());
        toggles.clear();
        nextToggles.clear();
    }

    for (int r = 0; r < state.getNumRows(); ++r)
        for (int c = 0; c < state.getNumColumns(); ++c)
            state.set (r, c, false);
}

} // namespace element
