package vermilion;

import java.util.ArrayList;

/**
 * An audio provider that includes a simple interface for connecting plugins
 * together and routing signals to a mixer for playback.
 */
public class SystemRouter extends PluginClassVermilion implements AudioProvider
{
    /**
     * A reference to the audio thread's internal mixing buffer.
     */
    private float[][] audioBuffer = null;

    /**
     * The number of samples in the internal mixing buffer. We use this if we
     * want to blank the entire buffer.
     */
    private int numSamples = 0;

    /**
     * Contains the instantiated machine plugins which are in the audio chain.
     */
    private ArrayList<MachineBase> machines = new ArrayList<MachineBase>();

    /**
     * Contains the wire connections between machine plugins in the audio chain.
     */
    private ArrayList<SimpleWire> wires = new ArrayList<SimpleWire>();

    /**
     * The mixer machine is where we begin a cycle of audio processing.
     */
    private MachineBase mixer = null;

    /**
     * The JWirePanel implements an interactive audio chain. From there the user
     * can create machines and connect them with wires.
     */
    private JWirePanel wirePanel = null;

    /**
     * Indicates that non-critical data such as the machine positions or param
     * values was changed by the user, so a save is required.
     */
    public static boolean secondarySavingNeeded = false;

    /**
     * The type for this plugin is 'STARTUP'.
     */
    @Override
    public String getType()
    {
        return "STARTUP";
    }

    /**
     * The name for this plugin is 'Router'.
     */
    @Override
    public String getName()
    {
        return "Router";
    }

    /**
     * The initialise method of this plugin creates the interface tab.
     */
    @Override
    public void initialise()
    {
        // Register this class as the audio data provider.
        Main.audio.registerAudioProviderClass(this);

        // Register the types of data we read and write to the document.
        UsefulFile.registerType(new MachineNode(null));
        UsefulFile.registerType(new WireNode(null));

        UsefulFile.registerType(new ParameterSlider(0));
        UsefulFile.registerType(new ParameterNote(ParameterNote.NOTE_OFF));
        UsefulFile.registerType(new ParameterCommand(0, 0));
        UsefulFile.registerType(new ParameterSample(0));

        wirePanel = new JWirePanel(this);

        Main.window.addTab("Router", wirePanel);

        wirePanel.setupMachineBoxes(machines, wires);
    }

    /**
     * The deinitialise method for this plugin removes the interface tab and
     * clears the audio chain, stopping audio processing.
     */
    @Override
    public void deinitialise()
    {
        mixer = null;
        wires.clear();
        machines.clear();
        Main.window.removeTab(wirePanel);
    }

    /**
     * The documentSaving method for this plugin copies the working audio chain
     * to a UsefulFileObject representation.
     */
    @Override
    public void documentSaving()
    {
        // The document always directly reflects what we have, so we don't
        // call these unless we make a change to data not read by other systems.
        // TODO: Set secondarySavingNeeded for changing wire volume/channels
        // and probably more.
        if (secondarySavingNeeded)
        {
            writeMachinesToDocument();
            writeWiresToDocument();
        }

        secondarySavingNeeded = false;
    }

    /**
     * The documentLoaded method of this plugin reads from the composition,
     * creating the machines and wires.
     */
    @Override
    public void documentLoaded()
    {
        machines.clear();
        wires.clear();

        // Read the composition from the document.
        ArrayList<UsefulFileObject> machList = Main.document.getChildren("Machines");
        if (machList != null)
        {
            for (UsefulFileObject obj : machList)
            {
                MachineNode node = (MachineNode) obj;

                // Instantiate machines.
                try
                {
                    MachineBase tMachine = addMachine(node.machineName, node.machineID, node.xLoc, node.yLoc, node.isMaster, false, false);

                    // Read parameter values.
                    for (ParameterBase pObj : tMachine.parameters)
                    {
                        ParameterBase pVal = (ParameterBase) Main.document.getValue("Machines/" + node.machineID + "/" + pObj.parameterName);

                        if (pVal != null) pObj.copyValue(pVal);
                    }

                    if (node.isMaster)
                    {
                        // Make the 'mixer only' hack modifications.
                        tMachine.output = audioBuffer;
                        mixer = tMachine;
                    }
                }
                catch (Exception e) { System.out.println(e.getMessage()); }
            }
        }

        // Read the wires
        // Read the composition from the document.
        UsefulFileObjectList wireList = (UsefulFileObjectList) Main.document.getValue("Wires");
        if (wireList != null)
        {
            for (UsefulFileObject obj : wireList.arrayValue)
            {
                WireNode node = (WireNode) obj;

                // Instantiate wires.
                MachineBase source = findMachine(node.input);
                MachineBase dest = findMachine(node.output);

                //System.out.println(node.input + " -> " + node.output + ": " + node.amplitudeL);

                addWire(source, dest, node.amplitudeL, node.amplitudeR, node.channel, false);
            }
        }

        // If we read an empty composition, create a Mixer.
        if (machines.size() == 0)
        {
            try
            {
                // Since we have to write to the composition to add a mixer,
                // we reset the needsSaving flag for the case where we have
                // just created a new document.
                boolean docHadChanges = Main.document.needsSaving;
                MachineBase tMixer = (MachineBase) addMachine("Mixer", "Master", 500 - JWirePanel.MACHINE_WIDTH/2, 500 - JWirePanel.MACHINE_HEIGHT/2, true, true, false);
                Main.document.needsSaving = docHadChanges;

                if (tMixer != null)
                {
                    // Make the 'mixer only' hack modifications.
                    tMixer.output = audioBuffer;

                    // When we set this variable we will start asking this plugin for
                    // sound.
                    mixer = tMixer;
                }
            }
            catch (Exception e)
            {
                System.out.println("Unable to load mixer plugin!");
                e.printStackTrace();
            }
        }

        wirePanel.setupMachineBoxes(machines, wires);

        Main.plugins.sendMessage("Reload Machines", null);
    }


    /**
     * Accepts the buffer reference from the audio thread so we know where to
     * place output audio.
     */
    @Override
    public void setBufferRef(float[][] buffer, int buffSize)
    {
        audioBuffer = buffer;
        numSamples = buffSize;
    }

    /**
     * Processes messages from the system composer (if any ever exist).
     * @param message A message from another system component.
     */
    @Override
    public void processMessage(String message, Object msgVal)
    {
        if (message.equals("Sample Bank Ready"))
        {
             MachineBase.sampleBank = (SystemSampleBank) msgVal;
        }
    }

    /**
     * The mixing algorithm for this audio provider simply
     * calls for audio data from the registered mixer. It
     * fills the buffer with a test sound if no mixer is
     * registered.
     */
    @Override
    public void fillBuffer(int samplesToProcess)
    {
        for (MachineBase mach : machines)
        {
            mach.hasWorkedThisCycle = false;
        }

        if (mixer != null)
        {
            mixer.fillBufferInternal(samplesToProcess);
        }
        else
        {
            for (int x = 0; x < numSamples; x++)
            {
                audioBuffer[0][x] = 0.0f;
                audioBuffer[1][x] = 0.0f;
            }
        }
    }

    @Override
    public void processEvents(ArrayList<Object> events)
    {
       if (events == null) return;
       
       for (Object obj : events)
       {
            // TODO: Handle things not being found while processing events.
            PlaybackEvent event = (PlaybackEvent) obj;
            //System.out.println(event);

            MachineBase mach = findMachine(event.machineID);
            ParameterBase param = mach.findParameter(event.parameterID);
            if (param == null) System.out.println("no " + event.commandVal.parameterName);
            else
            {
                param.copyValue(event.commandVal);
                mach.parameterChange(event);
            }
        }
    }

    /**
     * Adds the given machine to the composition. The machine is given a name
     * if none is supplied.
     * @param saveData Indicates we are adding machines as a result of loading
     * the document, so we should not write back.
     * @return The machine instance that was added, or null if something goes
     * wrong.
     */
    public MachineBase addMachine(String machineName, String machineID, int xLoc, int yLoc, boolean isMaster, boolean saveData, boolean notify)
    {
        MachineBase newMach = (MachineBase) Main.plugins.getPlugin(machineName);
        if (newMach == null) return null;

        newMach.isMaster = isMaster;
        newMach.setupMachine();

        int counter = 1;
        boolean named = (machineID != null);
        if (!named)
        {
            String newName = "";
            while (!named)
            {
                newName = newMach.getName() + " #" + counter;

                boolean found = false;
                for (int x = 0; x < machines.size(); x++)
                {
                    String machName = machines.get(x).machineID;
                    if (machName.equals(newName)) found = true;
                }

                counter++;
                if (!found) named = true;
            }

            newMach.machineID = newName;
        }
        else newMach.machineID = machineID;

        newMach.xLoc = xLoc;
        newMach.yLoc = yLoc;

        // Add the machine.
        machines.add(newMach);

        // Update the display.
        wirePanel.setupMachineBoxes(machines, wires);

        // Update the composition.
        if (saveData)
        {
            writeMachinesToDocument();
        }

        if (notify)
        {
            Main.plugins.sendMessage("Machine Added", newMach);
        }

        return newMach;
    }

    /**
     * Removes the specified machine from the compositon. Deletes wires
     * accordingly and updates the display.
     * @param machine THe machine being removed.
     */
    public void removeMachine(MachineBase machine)
    {
        SimpleWire[] tWires = machine.inputs;

        // This step is probably redundant, we could do it all below.
        // TODO: Implement locking throughout the router.
        for (int x = 0; x < tWires.length; x++)
        {
            removeWire(tWires[x]);
        }

        // Remove (remaining) wires.
        tWires = new SimpleWire[wires.size()];
        wires.toArray(tWires);
        for (int x = 0; x < tWires.length; x++)
        {
            SimpleWire aWire = tWires[x];

            if (aWire.input == machine || aWire.output == machine) removeWire(aWire);
        }

        // Remove the machine.
        machines.remove(machine);

        // Update the display.
        wirePanel.setupMachineBoxes(machines, wires);

        // Update the composition.
        writeMachinesToDocument();
        Main.plugins.sendMessage("Machine Removed", machine);
    }


    /**
     * Adds a wire to the composition with the given source and destination machines.
     * Adjusts machine input list accordingly, and updates the display.
     * @param source The machine the wire will 'read from'.
     * @param dest The machine this wire will 'write to'.
     * @param saveData Indicates we are adding wires as a result of loading
     * the document, so we should not write back.
     */
    public void addWire(MachineBase source, MachineBase dest, float amplitudeL, float amplitudeR, int channel, boolean saveData)
    {
        if (source == null || dest == null) return;

        SimpleWire newWire = new SimpleWire();

        newWire.input = source;
        newWire.output = dest;

        newWire.amplitudeL = amplitudeL;
        newWire.amplitudeR = amplitudeR;
        newWire.channel = channel;

        SimpleWire[] newInputs = new SimpleWire[dest.inputs.length+1];
        for (int x = 0; x < newInputs.length-1; x++)
        {
            newInputs[x] = dest.inputs[x];
        }

        newInputs[dest.inputs.length] = newWire;
        dest.inputs = newInputs;

        // Add the wire.
        wires.add(newWire);

        // Update the display.
        wirePanel.setupMachineBoxes(machines, wires);

        // Update the composition.
        if (saveData) writeWiresToDocument();
    }

    /**
     * Removes the specified wire from the composition. Adjusts machine
     * input list accordingly and updates the display.
     * @param wire The wire that is being removed.
     */
    public void removeWire(SimpleWire wire)
    {
        MachineBase target = wire.output;

        // Stop the target machine from reading the wire.
        SimpleWire[] oldWires = target.inputs;
        SimpleWire[] newWires = new SimpleWire[oldWires.length-1];
        int outputIndex = 0;
        for (int x = 0; x < oldWires.length; x++)
        {
            SimpleWire aWire = oldWires[x];
            if (aWire != wire)
            {
                newWires[outputIndex++] = oldWires[x];
            }
        }
        target.inputs = newWires;

        // Delete the wire.
        wires.remove(wire);

        // Update the display.
        wirePanel.setupMachineBoxes(machines, wires);

        // Update the composition.
        writeWiresToDocument();
    }

    /**
     * 'Serialises' the machines in the audio chain and saves them.
     */
    private void writeMachinesToDocument()
    {
        Main.document.delete("Machines");

        for (MachineBase mach : machines)
        {
            Main.document.setValue("Machines/" + mach.machineID, new MachineNode(mach));

            for (ParameterBase param : mach.parameters)
            {
                Main.document.setValue("Machines/" + mach.machineID + "/" + param.parameterName, param);
            }
        }
    }

    /**
     * 'Serialises' the wires in the audio chain and saves them.
     */
    private void writeWiresToDocument()
    {
        UsefulFileObjectList wireList = new UsefulFileObjectList();

        for (SimpleWire wire : wires)
        {
            wireList.add(new WireNode(wire));
        }

        Main.document.setValue("Wires", wireList);
    }

    /**
     * Finds a machine in the audio chain using its ID.
     * @return The machine found, or null if it was not found.
     */
    private MachineBase findMachine(String machID)
    {
        for (MachineBase mach : machines)
        {
            if (mach.machineID.equals(machID)) return mach;
        }

        return null;
    }
}