package vermilion;

import java.util.ArrayList;
//import java.util.TreeMap;

// TODO: Make methods that return columns, patterns etc. and use them elsewhere.

/**
 * An event provider that includes a tracker interface for programming events
 * on machines.
 */
public class SystemComposerPlayback 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. As soon as this is done, the
     * system will progress to the playing state.
     */
    private final int PLAYBACK_STATE_SEEKING = 2;

    /**
     * The tracker panel displays a piano-roll interface for editing patterns
     * and ancillary controls for creating and manipulating them.
     */
    private JTrackerPanel trackerPanel;

    /**
     * Defines the name of the master pattern, where pattern cues are placed.
     */
    public static final String MASTER_PATTERN_NAME = "MASTER_PATTERN";

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

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

    /**
     * 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 time at which the current tick started. This is really only used
     * to calculate the tick cursor.
     */
    private long currTickTime = 0;

    /**
     * The current tick cursor, only used for display.
     */
    private double tickCursor;

    /**
     * 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;

    // This is used to cache instances of machines, since we sometimes need
    // to know things like the parameters etc. on the machine.
    //private TreeMap<String, MachineBase> refInstances = new TreeMap<String, MachineBase>();

    private SystemComposerPatternData patterns = new SystemComposerPatternData();
    
    /**
     * 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, -1));

        trackerPanel = new JTrackerPanel(patterns, this);

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

        //positionListeners = new ArrayList<AudioPositionListener>();
        // 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)
        {
            long timeLeft = timeToNextEvent();

            int samplesLeft = (int) (timeLeft * Main.audio.sampleRate / TIME_RES);

            tickCursor = nextPlaybackTick-1;
            tickCursor += (playbackCursor - currTickTime) / (double) (nextTickTime - currTickTime);

            // TODO: We could get more complicated and be a bit more efficient
            // with mixing by never asking for zero samples.

            if (samplesLeft == 0)
            {
                // To keep things simple, we return the events and ask for
                // zero samples.
                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);
    }

    /**
     * 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);

            // 3 - Advance the playback cursor.
            nextPlaybackTick++;
            currTickTime = nextTickTime;
            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--;
            }
        }

        return events;
    }

    /**
     * The documentSaving method for this plugin tells the tracker panel to
     * save unsaved changes to the currently open pattern.
     */
    @Override
    public void documentSaving()
    {
        // Save the current pattern with its cues if required.
        trackerPanel.documentSaving();
    }

    /**
     * Clears the interface when we load a new document.
     */
    @Override
    public void documentLoaded()
    {
        // We need to stop playback of the old composition data if it is going
        // on right now.
        stop();

        // We need to clear the display of the current pattern, in case it
        // is holding changes.
        trackerPanel.clearWithoutSaving();
    }

    /**
     * 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();

        // Set playback positions.
        nextPlaybackTick = i;
        nextTickTime = 0;
        playbackCursor = -1 * TIME_RES;
        currTickTime = playbackCursor;

        // 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 so they note off when we stop.

        playbackState = PLAYBACK_STATE_STOPPED;
        playingPatterns.clear();

        nextPlaybackTick = 0;
        nextTickTime = 0;
        playbackCursor = -1 * TIME_RES;
        currTickTime = playbackCursor;
    }

    /**
     * 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;
    }

    /**
     * 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)
    {
        for (UsefulFileObject obj : patterns.getPatternTracks(MASTER_PATTERN_NAME, MASTER_PATTERN_NAME))
        {
            PatternTrackNode track = (PatternTrackNode) obj;

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

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

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

                    if (pattNode != null)
                    {
                        System.out.println("Launching " + pattNode + " on " + machNode);
                        playingPatterns.add(new PlayingPattern(pattNode, 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.
     * TODO: Can we get the node by name rather than searching the children?
     */
    public MachineNode findMachine(String machID)
    {
        // TODO: Cache this, clear on reload.
        return (MachineNode) Main.document.getValue("Machines/" + machID);
    }



    /**
     * Processes messages from the system router, such as when a machine is
     * added or renamed.
     */
    @Override
    public void processMessage(String message, Object msgVal)
    {
        if (message.equals("Reload Machines"))
        {
            System.out.println("Composer reloading...");

            // Refresh the pattern data cache.
            patterns.refreshPatterns();

            // Refresh the patterns on the screen.
            trackerPanel.reloadMachines();

            return;
        }
        else if (message.equals("Machine Added"))
        {
            // Refresh the machines on the screen.
            trackerPanel.reloadMachines();

            return;
        }
        else if (message.equals("Machine Removed"))
        {
            MachineBase mach = (MachineBase) msgVal;

            // When a machine is removed, we should delete its patterns and
            // remove tracks from the master pattern. TODO: Move to pattdata
            PatternNode masterPattern = patterns.getPattern(MASTER_PATTERN_NAME, MASTER_PATTERN_NAME);
            ArrayList<UsefulFileObject> masterTracks = patterns.getPatternTracks(MASTER_PATTERN_NAME, MASTER_PATTERN_NAME);

            for (UsefulFileObject node : masterTracks)
            {
                PatternTrackNode track = (PatternTrackNode) node;
                System.out.println(track.paramName + " " + mach.machineID);
                if (track.paramName.equals(mach.machineID))
                {
                    //System.out.println(track.columnName);
                    patterns.deleteTrack(null, masterPattern, track.columnName);
                }
            }

            // TODO: Delete patterns when a machine is removed, but only if
            // it is the last if its kind, and probably only after asking.

            trackerPanel.reloadMachines();
            return;
        }

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

    public static final int numCursors = 3;
    private double[] cursors = new double[numCursors];
    private double[] emptyCursor = new double[0];

    /**
     * Returns a list of playback positions for displaying over the current
     * pattern. The positions are updated regularly so that the user can track
     * playback with reasonable accuracy.
     */
    public double[] getPlaybackCursors(PatternNode currPattern)
    {
        if (currPattern == null) return emptyCursor;
        if (playbackState != PLAYBACK_STATE_PLAYING) return emptyCursor;

        int cursorNum = 0;
        if (currPattern.patternID.equals(SystemComposerPlayback.MASTER_PATTERN_NAME))
        {
            cursors[0] = tickCursor;
            cursorNum = 1;
        }
        else
        {
            for (PlayingPattern pp : playingPatterns)
            {
                if (pp.pattern.patternID.equals(currPattern.patternID))
                {
                    if (cursorNum < numCursors) cursors[cursorNum] = pp.tickCursor;
                    cursorNum++;
                }
            }
        }

        for (int x = cursorNum; x < numCursors; x++) cursors[x] = -1;

        return cursors;
    }

    /**
     * Returns and caches machine instances so we can look them up to get the
     * list of parameters and other information. TODO: Do we need this and
     * the list by machineID as well?
     */
    /*public MachineBase getMachInstance(String machineName)
    {
        MachineBase cached = refInstances.get(machineName);
        if (cached != null) return cached;

        MachineBase mach = (MachineBase) Main.plugins.getPlugin(machineName);
        mach.setupMachine();
        refInstances.put(machineName, mach);
        return mach;
    }*/
}