package vermilion;

import java.util.ArrayList;

// TODO: Make methods that return columns, patterns etc.

/**
 * An event provider that includes a tracker interface for programming events
 * on machines.
 */
public class SystemComposer extends PluginClassVermilion implements EventProvider
{
    /**
     * Indicates that the event provider is not producing events.
     */
    private final int PLAYBACK_STATE_STOPPED = 0;

    /**
     * Indicates that the event provider will produce events when asked.
     */
    private final int PLAYBACK_STATE_PLAYING = 1;

    /**
     * Indicates that the event provider is seeking and will not produce events.
     * This extra state is so that the provider can have playback data in place
     * but not be affected by calls for events.
     */
    private final int PLAYBACK_STATE_SEEKING = 2;

    /**
     * The tracker panel displays a piano-roll interface for editing patterns.
     */
    private JTrackerPanel trackerPanel;

    /**
     * Indicates the current state of the event provider: stopped, seeking,
     * or playing.
     */
    private int playbackState = PLAYBACK_STATE_STOPPED;

    /**
     * Defines the time resolution to be in nanoseconds.
     */
    public static final long TIME_RES = 1000000000;

    /**
     * The current playback position, in nanoseconds.
     */
    private long playbackCursor = 0;
    
    /**
     * The next tick to play. TODO: For tempo slides have tick cursor in
     * nanoticks, then use that to fire pattern events.
     */
    private int nextPlaybackTick = 0;

    /**
     * The time we have calculated for the next tick. We keep this in advance
     * and when seeking we set the time of the first tick to zero.
     */
    private long nextTickTime = 0;

    /**
     * The list of patterns that are currently playing. We poll these for events
     * during each cycle.
     */
    private ArrayList<PlayingPattern> playingPatterns = new ArrayList<PlayingPattern>();

    /**
     * The current BPM, which we interpret as *bars* per minute. If two patterns
     * are played at the same time and their ticks per bar are different, they
     * will still have the same length bars.
     */
    private int barsPerTenMinutes = 600;

    // TODO: Renaming patterns, and preventing renaming of the master pattern.
    /**
     * Defines the name of the master pattern, where pattern cues are placed.
     */
    public static final String MASTER_PATTERN_NAME = "MASTER_PATTERN";

    /**
     * The machine whose patterns are currently listed.
     */
    public MachineNode currMachine;

    /**
     * The pattern which is currently showing in the tracker area.
     */
    public PatternNode currPattern;

    // We cache this so that we can avoid deserialising it repeatedly and so
    // we don't have to write it back to the document in order for cues to
    // be added during playback. TODO: Ditch this and cache the columns.
    /**
     * The master pattern, which is called upon at various times for comparison.
     */
    public PatternNode masterPattern;
    
    /**
     * The type for this plugin is 'STARTUP'.
     */
    @Override
    public String getType()
    {
        return "STARTUP";
    }

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

    /**
     * The initialise method of this plugin creates the interface tab and menu.
     */
    @Override
    public void initialise()
    {
        Main.audio.registerEventProviderClass(this);

        // Register the types of data we read and write to the document.
        UsefulFile.registerType(new PatternNode(null));
        UsefulFile.registerType(new ParameterPattern(null, null));
        UsefulFile.registerType(new PatternTrackNode(null, null, -1));

        trackerPanel = new JTrackerPanel(this);

        Main.window.addTab("Composer", trackerPanel);

        // TODO: Work out constants from samplerate etc. here then save
        // some maths below.
    }

    /**
     * The deinitialise method for this plugin removes the interface tab and menu.
     */
    @Override
    public void deinitialise()
    {
        Main.window.removeTab(trackerPanel);
        trackerPanel.removeMenu();
    }

    /**
     * Processes a timeframe from the current playback cursor to a maximum of
     * one internal buffer length later, returning either the events that
     * occur at this point in playback or the number of samples remaining
     * until the next event.
     * @return The events or the number of samples until the next event(s).
     */
    @Override
    public EventCollection processEvents()
    {
        int maxSamples = Main.audio.internalBufferSize/Main.audio.NUM_CHANNELS;

        if (playbackState == PLAYBACK_STATE_PLAYING)
        {
            //System.out.println("playing at: " + playbackCursor);

            long timeLeft = timeToNextEvent();
            //System.out.println("timeLeft: " + timeLeft);

            int samplesLeft = (int) (timeLeft * Main.audio.sampleRate / TIME_RES);
            //System.out.println("samplesLeft: " + samplesLeft);

            if (samplesLeft == 0)
            {
                // To keep things simple, we return the events and ask for
                // zero samples. TODO: We could get more complicated and be
                // a bit more efficient.
                return new EventCollection(getEvents(), 0);
            }
            else if (samplesLeft < maxSamples)
            {
                playbackCursor += samplesLeft * TIME_RES / Main.audio.sampleRate;
                return new EventCollection(null, samplesLeft);
            }
            else
            {
                playbackCursor += maxSamples * TIME_RES / Main.audio.sampleRate;
                return new EventCollection(null, maxSamples);
            }
        }

        return new EventCollection(null, maxSamples);
    }

    /**
     * The documentSaving method for this plugin tells the tracker panel to
     * save unsaved changes to the currently open pattern.
     */
    @Override
    public void documentSaving()
    {
        trackerPanel.documentSaving();
        //Main.document.setValue("Patterns/" + SystemComposer.MASTER_PATTERN_NAME, masterPattern);
    }

    /**
     * The documentLoaded method for this plugin *could* reload the machines from
     * the document. In fact we wait until the router tells us it has processed
     * and resaved the data.
     */
    @Override
    public void documentLoaded()
    {
        // We don't actally do a reload here because we are waiting for the
        // router to load and then resave the machines, ensuring things like
        // track names are correct in the document. The router will send us a
        // message once this is done.
        System.out.println("DOC LOADED.");

        // We need to clear the display of the current pattern, in case it
        // is holding changes.
        currMachine = null;
        currPattern = null;
        trackerPanel.documentLoaded();


        // We do grab a reference to the master pattern of the document (or
        // create it if it doesn't exist yet).
        String locn = "Patterns/" + SystemComposer.MASTER_PATTERN_NAME;
        masterPattern = (PatternNode) Main.document.getValue(locn);
        //masterColumns = Main.document.getChildren(locn);

        if (masterPattern == null)
        {
            masterPattern = new PatternNode(SystemComposer.MASTER_PATTERN_NAME, 10000);
            Main.document.setValue("Patterns/" + SystemComposer.MASTER_PATTERN_NAME, masterPattern);
        }
    }

    /**
     * Begins playback at the given master pattern tick. We do this by initialising
     * some cursors and setting the playback state to 'playing. A one second leadin
     * can be created (it is not for now).
     */
    protected void seek(int i)
    {
        playbackState = PLAYBACK_STATE_SEEKING;

        documentSaving();

        nextPlaybackTick = i;
        nextTickTime = 0;
        playbackCursor = 0; // -1 * TIME_RES;

        // Clear pattern event queue.
        playingPatterns.clear();

        playbackState = PLAYBACK_STATE_PLAYING;
    }

    /**
     * Stops playback by setting the playback state to 'stopped'.
     */
    protected void stop()
    {
        // TODO: Send machines a stop message.

        playbackState = PLAYBACK_STATE_STOPPED;
        playingPatterns.clear();
    }

    /**
     * Calculates the number of nanoseconds to the next event, which will
     * generally mean either the next master pattern tick or the next tick
     * on any currently playing patterns.
     */
    private long timeToNextEvent()
    {
        long timeRemaining = nextTickTime - playbackCursor;

        // Check queued pattern events.
        for (PlayingPattern pp : playingPatterns)
        {
            if (pp.nextTickTime - playbackCursor < timeRemaining)
            {
                timeRemaining = pp.nextTickTime - playbackCursor;
            }
        }

        return timeRemaining;
    }

    /**
     * Returns the events that are due to occur at the current sample. This is
     * only called when events are due, however it can be called at other times
     * resulting in an empty list.
     */
    private ArrayList<Object> getEvents()
    {
        ArrayList<Object> events = new ArrayList<Object>();

        // Note: We have to check for events that occur between now and next
        // sample, since the cursor is set at the start of the sample and this
        // is fired if there are less than 1 samples before the next event.
        long nextSampleTime = playbackCursor + TIME_RES / Main.audio.sampleRate;

        // Check the master tick event and if it is happening advance. If we
        // advance, we have to fire off all patterns on the tick and queue up
        // their future events. We also have to handle tempo changes before
        // we do either advancing or pattern launching.
        if (nextTickTime <= nextSampleTime)
        {
            System.out.println("Playing master tick " + nextPlaybackTick + " at sample from " + playbackCursor + " to " + (nextSampleTime-1));
            
            // 1 - TODO: Handle master commands. Will any be passed back to
            // the router?
            handleMasterCommands(nextPlaybackTick);

            // 2 - Launch patterns from master cues.
            handlePatternCues(nextPlaybackTick);

            nextPlaybackTick++;
            nextTickTime += 600 * TIME_RES / barsPerTenMinutes;
        }

        // Check playing pattern events and if there are any add them
        // to the list.
        for (PlayingPattern pp : playingPatterns)
        {
            if (pp.nextTickTime <= nextSampleTime)
            {
                pp.getEvents(events, barsPerTenMinutes);
            }
        }

        // Finally, we have to remove any patterns that have finished playing.
        for (int x = 0; x < playingPatterns.size(); x++)
        {
            PlayingPattern pp = playingPatterns.get(x);
            if (pp.nextPatternTick >= pp.pattern.patternLength)
            {
                playingPatterns.remove(x);
                x--;
                //System.out.println("removed " + pp.pattern.patternID);
            }
        }

        return events;
    }

    /**
     * Check the current master playback tick and see if any master pattern
     * tracks have pattern cues in them, launching them if found.
     */
    private void handlePatternCues(int playbackTick)
    {
        if (masterPattern != null)
        {
            ArrayList<UsefulFileObject> tracks = Main.document.getChildren("Patterns/" + MASTER_PATTERN_NAME);
            for (UsefulFileObject obj : tracks)
            {
                PatternTrackNode track = (PatternTrackNode) obj;

                ParameterPattern command = (ParameterPattern) track.commands.get(playbackTick);

                if (command != null)
                {
                    MachineNode machNode = findMachine(track.paramName);

                    if (machNode != null)
                    {
                        PatternNode pattNode = findPattern(machNode.machineName, command.value);

                        if (pattNode != null)
                        {
                            System.out.println("Launching " + pattNode + " on " + machNode);

                            playingPatterns.add(new PlayingPattern(pattNode, machNode.machineName, machNode.machineID, playbackCursor));
                        }
                    }
                }
            }
        }
    }

    /**
     * Handle things like tempo changes.
     */
    private void handleMasterCommands(int nextPlaybackTick)
    {
        // TODO: Implement (and then handle) things like tempo changes.
    }

    /**
     * Finds a machine node given the machineID. Used during finding patterns.
     */
    private MachineNode findMachine(String machID)
    {
        ArrayList<UsefulFileObject> machList = Main.document.getChildren("Machines");
        for (UsefulFileObject machObj : machList)
        {
            MachineNode mach = (MachineNode) machObj;
            if (mach.machineID.equals(machID))
            {
                return mach;
            }
        }

        return null;
    }

    /**
     * Finds a pattern, given the machine name and pattern name. Used to launch
     * pattern cues.
     */
    private PatternNode findPattern(String machName, String pattName)
    {
        ArrayList<UsefulFileObject> patterns = Main.document.getChildren("Patterns/" + machName);
        if (patterns != null)
        {
            for (UsefulFileObject pattObj : patterns)
            {
                PatternNode pattNode = (PatternNode) pattObj;
                if (pattNode.patternID.equals(pattName))
                {
                    return pattNode;
                }
            }
        }

        return null;
    }

    /**
     * Creates a new pattern in the composition for the selected machine and
     * displays it in the editor.
     */
    public void createPattern()
    {
        if (currMachine == null) return;

        ArrayList<UsefulFileObject> patterns = Main.document.getChildren("Patterns/" + currMachine.machineName);
        if (patterns == null) patterns = new ArrayList<UsefulFileObject>();

        int num = 1;
        String newName = "Pattern 1";
        boolean named = false;

        while (!named)
        {
            boolean found = false;
            for (int x = 0; x < patterns.size(); x++)
            {
                PatternNode node = (PatternNode) patterns.get(x);
                if (newName.equals(node.patternID)) found = true;
            }

            if (!found) named = true;
            else
            {
                num++;
                newName = "Pattern " + num;
            }
        }

        PatternNode newNode = new PatternNode(newName);
        Main.document.setValue("Patterns/" + currMachine.machineName + "/" + newName, newNode);

        // Load the machine and get the parameter list.
        MachineBase mach = (MachineBase) Main.plugins.getPlugin(currMachine.machineName);
        mach.setupMachine();

        for (ParameterBase param : mach.parameters)
        {
            doMachineTrack(param.parameterName, param.parameterType, newName);
        }

        // Update pattern list by reloading current machine.
        trackerPanel.showMachine();

        //patternListC.setSelectedValue(newNode, true); // TODO: select/show
    }

    /**
     * Removes the currently selected pattern from the machine it belongs to.
     */
    void deletePattern()
    {
        if (currPattern != null)
        {
            Main.document.delete("Patterns/" + currMachine.machineName + "/" + currPattern.patternID);
        }

        trackerPanel.showMachine();
    }

    /**
     * Shows a dialog box where the user can change a pattern's name, length
     * and time signature.
     */
    void showPatternProperties()
    {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    // Creates a new track for the current pattern, given the name of the parameter
    // (or machine) in question and the type or that parameter.
    void createTrack(String trackName, int parameterType)
    {
        if (currMachine == null || currPattern == null) return;

        if (currPattern == masterPattern)
        {
            // TODO: make use of the term column and track consistent.
            ArrayList<UsefulFileObject> existingTracks = Main.document.getChildren("Patterns/" + MASTER_PATTERN_NAME);
            ArrayList<String> existingNames = new ArrayList<String>();
            for (UsefulFileObject obj : existingTracks) existingNames.add(((PatternTrackNode) obj).columnName);

            String chosenName = Main.chooseName(trackName + " #", existingNames);
            PatternTrackNode newNode = new PatternTrackNode(chosenName, trackName, ParameterBase.PARAMETER_TYPE_PATTERN);
            Main.document.setValue("Patterns/" + MASTER_PATTERN_NAME + "/" + chosenName, newNode);

            trackerPanel.refreshPattern();
        }
        else
        {
            doMachineTrack(trackName, parameterType, currPattern.patternID);

            trackerPanel.refreshPattern();
        }
    }

    // Helper method to create a track for a 'normal' pattern, used also when
    // creating a new pattern for a machine.
    private void doMachineTrack(String trackName, int parameterType, String pattName)
    {
        ArrayList<UsefulFileObject> existingTracks = Main.document.getChildren("Patterns/" + currMachine.machineName + "/" + pattName);
        ArrayList<String> existingNames = new ArrayList<String>();
        for (UsefulFileObject obj : existingTracks) {
            existingNames.add(((PatternTrackNode) obj).columnName);
        }
        String chosenName = Main.chooseName(trackName + " #", existingNames);
        PatternTrackNode newNode = new PatternTrackNode(chosenName, trackName, parameterType);
        Main.document.setValue("Patterns/" + currMachine.machineName + "/" + pattName + "/" + chosenName, newNode);
    }

    /**
     * Deletes a track from the currently showing pattern, given the track name.
     * This is normally the name of the track where the cursor is sitting in
     * the tracker area.
     */
    void deleteTrack(String trackName)
    {
        if (currMachine == null || currPattern == null) return;

        if (currPattern == masterPattern)
        {
            Main.document.delete("Patterns/" + MASTER_PATTERN_NAME + "/" + trackName);
        }
        else
        {
            Main.document.delete("Patterns/" + currMachine.machineName + "/" + currPattern.patternID + "/" + trackName);
        }

        trackerPanel.refreshPattern();
    }

    /**
     * Processes messages from the system router, such as when a machine is
     * added or renamed.
     */
    @Override
    public void processMessage(String message)
    {
        if (message.equals("Reload Machines"))
        {
            trackerPanel.reloadMachines();
            return;
        }

        System.out.println("Composer couldn't understand message: " + message);
    }
}