/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2008  David Koelle
 *
 * http://www.jfugue.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

package org.jfugue;

import java.io.File;
import java.io.IOException;
import java.net.URL;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiFileFormat;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;

/**
 * Prepares a pattern to be turned into music by the Renderer.  This class
 * also handles saving the sequence derived from a pattern as a MIDI file.
 *
 *@see MidiRenderer
 *@see Pattern
 *@author David Koelle
 *@version 2.0
 */
public class Player
{
    private Sequencer sequencer;
    private MusicStringParser parser;
    private MidiRenderer renderer;
    private float sequenceTiming = Sequence.PPQ;
    private int resolution = 120;
    private boolean paused = false;
    private boolean started = false;
    private boolean finished = false;

    /**
     * Instantiates a new Player object, which is used for playing music.
     */
    public Player()
    {
        this(true);
    }

    /**
     * Instantiates a new Player object, which is used for playing music.
     * The <code>connected</code> parameter is passed directly to MidiSystem.getSequencer.
     * Pass false when you do not want to copy a live synthesizer - for example,
     * if your Player is on a server, and you don't want to create new synthesizers every time
     * the constructor is called. 
     */
    public Player(boolean connected)
    {
        try {
            // Get default sequencer.
            setSequencer(MidiSystem.getSequencer(connected)); // use non connected sequencer so no copy of live synthesizer will be created.
        } catch (MidiUnavailableException e)
        {
            throw new JFugueException(JFugueException.SEQUENCER_DEVICE_NOT_SUPPORTED_WITH_EXCEPTION + e.getMessage());
        }
        initParser();
    }

    /**
     * Creates a new Player instance using a Sequencer that you have provided.
     * @param sequencer The Sequencer to send the MIDI events
     */
    public Player(Sequencer sequencer)
    {
        setSequencer(sequencer);
        initParser();
    }

    /**
     * Creates a new Player instance using a Sequencer obtained from the Synthesizer that you have provided.
     * @param synth The Synthesizer you want to use for this Player.
     */
    public Player(Synthesizer synth) throws MidiUnavailableException
    {
        this(Player.getSequencerConnectedToSynthesizer(synth));
    }

    private void initParser()
    {
        this.parser = new MusicStringParser();
        this.renderer = new MidiRenderer(sequenceTiming, resolution);
        this.parser.addParserListener(this.renderer);
    }
    
    private void initSequencer()
    {
        // Close the sequencer and synthesizer
        getSequencer().addMetaEventListener(new MetaEventListener() {
            public void meta(MetaMessage event)
            {
                if (event.getType() == 47)
                {
                    close();
                }
            }
        });
    }

    private void openSequencer()
    {
        if (getSequencer() == null)
        {
            throw new JFugueException(JFugueException.SEQUENCER_DEVICE_NOT_SUPPORTED);
        }

        // Open the sequencer, if it is not already open
        if (!getSequencer().isOpen()) {
            try {
                getSequencer().open();
            } catch (MidiUnavailableException e)
            {
                throw new JFugueException(JFugueException.SEQUENCER_DEVICE_NOT_SUPPORTED_WITH_EXCEPTION + e.getMessage());
            }
        }
    }

    /**
     * Plays a pattern by setting up a Renderer and feeding the pattern to it.
     * @param pattern the pattern to play
     * @see MidiRenderer
     */
    public void play(Pattern pattern)
    {
        Sequence sequence = getSequence(pattern);
        play(sequence);
    }

    /**
     * Plays a pattern by setting up a Renderer and feeding the pattern to it.
     * @param pattern the pattern to play
     * @see MidiRenderer
     */
    public void play(Rhythm rhythm)
    {
        Pattern pattern = rhythm.getPattern();
        Sequence sequence = getSequence(pattern);
        play(sequence);
    }

    /**
     * Plays a MIDI Sequence
     * @param sequence the Sequence to play
     * @throws JFugueException if there is a problem playing the music
     * @see MidiRenderer
     */
    private void play(Sequence sequence)
    {
        // Open the sequencer
        openSequencer();

        // Set the sequence
        try {
            getSequencer().setSequence(sequence);
        } catch (Exception e)
        {
            throw new JFugueException(JFugueException.ERROR_PLAYING_MUSIC + e.getMessage());
        }

        setStarted(true);

        // Start the sequence
        getSequencer().start();

        // Wait for the sequence to finish
        while (isPlaying() || isPaused())
        {
            try {
                Thread.sleep(20);  // don't hog all of the CPU
            } catch (InterruptedException e)
            {
                throw new JFugueException(JFugueException.ERROR_SLEEP);
            }
        }

        // Close the sequencer
        getSequencer().close();

        setStarted(false);
        setFinished(true);
    }

    /**
     * Plays a string of music.  Be sure to call player.close() after play() has returned.
     * @param musicString the MusicString (JFugue-formatted string) to play
     * @version 3.0
     */
    public void play(String musicString)
    {
        if (musicString.indexOf(".mid") > 0)
        {
            // If the user tried to call this method with "filename.mid" or "filename.midi", throw the following exception
            throw new JFugueException(JFugueException.PLAYS_STRING_NOT_FILE_EXC);
        }

        Pattern pattern = new Pattern(musicString);
        play(pattern);
    }

    /**
     * Plays a MIDI file, without doing any conversions to MusicStrings.
     * Be sure to call player.close() after play() has returned.
     * @param file the MIDI file to play
     * @throws IOException
     * @throws InvalidMidiDataException
     * @version 3.0
     */
    public void playMidiDirectly(File file) throws IOException, InvalidMidiDataException
    {
        Sequence sequence = MidiSystem.getSequence(file);
        play(sequence);
    }

    /**
     * Plays a URL that contains a MIDI sequence.  Be sure to call player.close() after play() has returned.
     * @param url the URL to play
     * @throws IOException
     * @throws InvalidMidiDataException
     * @version 3.0
     */
    public void playMidiDirectly(URL url) throws IOException, InvalidMidiDataException
    {
        Sequence sequence = MidiSystem.getSequence(url);
        play(sequence);
    }

    public void play(Anticipator anticipator, Pattern pattern, long offset)
    {
        Sequence sequence = getSequence(pattern);
        Sequence sequence2 = getSequence(pattern);
        play(anticipator, sequence, sequence2, offset);
    }

    public void play(Anticipator anticipator, Sequence sequence, Sequence sequence2, long offset)
    {
        anticipator.play(sequence);

        if (offset > 0)
        {
            try {
                Thread.sleep(offset);
            } catch (InterruptedException e) {
                throw new JFugueException(JFugueException.ERROR_SLEEP);
            }
        }

        play(sequence2);
    }

    /**
     * Closes MIDI resources - be sure to call this after play() has returned.
     */
    public void close()
    {
        getSequencer().close();
        try {
            if (MidiSystem.getSynthesizer() != null) {
                MidiSystem.getSynthesizer().close();
            }
        } catch (MidiUnavailableException e) {
            throw new JFugueException(JFugueException.GENERAL_ERROR + e.getMessage());
        }
    }

    private void setStarted(boolean started)
    {
        this.started = started;
    }

    private void setFinished(boolean finished)
    {
        this.finished = finished;
    }

    public boolean isStarted()
    {
        return this.started;
    }

    public boolean isFinished()
    {
        return this.finished;
    }

    public boolean isPlaying()
    {
        return getSequencer().isRunning();
    }

    public boolean isPaused()
    {
        return paused;
    }

    public void pause()
    {
        paused = true;
        if (isPlaying()) {
            getSequencer().stop();
        }
    }

    public void resume()
    {
        paused = false;
        getSequencer().start();
    }

    public void stop()
    {
        paused = false;
        getSequencer().stop();
        getSequencer().setMicrosecondPosition(0);
    }

    public void jumpTo(long microseconds)
    {
        getSequencer().setMicrosecondPosition(microseconds);
    }

    public long getSequenceLength(Sequence sequence)
    {
        return sequence.getMicrosecondLength();
    }

    public long getSequencePosition()
    {
        return getSequencer().getMicrosecondPosition();
    }

    /**
     * Saves the MIDI data from a pattern into a file.
     * @param pattern the pattern to save
     * @param file the File to save the pattern to.  Should include file extension, such as .mid
     */
    public void saveMidi(Pattern pattern, File file) throws IOException
    {
        Sequence sequence = getSequence(pattern);

        int[] writers = MidiSystem.getMidiFileTypes(sequence);
        if (writers.length == 0) return;

        MidiSystem.write(sequence, writers[0], file);
    }

    /**
     * Saves the MIDI data from a MusicString into a file.
     * @param musicString the MusicString to save
     * @param file the File to save the MusicString to.  Should include file extension, such as .mid
     */
    public void saveMidi(String musicString, File file) throws IOException
    {
        Pattern pattern = new Pattern(musicString);
        saveMidi(pattern, file);
    }

    /**
     * Parses a MIDI file and returns a Pattern.  This is an excellent example
     * of JFugue's Parser-Renderer architecture:
     *
     * <pre>
     *  MidiParser parser = new MidiParser();
     *  MusicStringRenderer renderer = new MusicStringRenderer();
     *  parser.addParserListener(renderer);
     *  parser.parse(sequence);
     * </pre>
     *
     * @param filename The name of the MIDI file
     * @return a Pattern containing the MusicString representing the MIDI music
     * @throws IOException If there is a problem opening the MIDI file
     * @throws InvalidMidiDataException If there is a problem obtaining MIDI resources
     */
    public Pattern loadMidi(File file) throws IOException, InvalidMidiDataException
    {
        MidiFileFormat format = MidiSystem.getMidiFileFormat(file);
        this.sequenceTiming = format.getDivisionType();
        this.resolution = format.getResolution();
        return Pattern.loadMidi(file);
    }

    public static void allNotesOff()
    {
        try {
            allNotesOff(MidiSystem.getSynthesizer());
        } catch (MidiUnavailableException e)
        {
            throw new JFugueException(JFugueException.GENERAL_ERROR);
        }
    }

    /**
     * Stops all notes from playing on all MIDI channels.
     */
    public static void allNotesOff(Synthesizer synth)
    {
        try {
            if (!synth.isOpen()) {
                synth.open();
            }
            MidiChannel[] channels = synth.getChannels();
            for (int i=0; i < channels.length; i++)
            {
                channels[i].allNotesOff();
            }
        } catch (MidiUnavailableException e)
        {
            throw new JFugueException(JFugueException.GENERAL_ERROR);
        }
    }

    /**
     * Returns the sequencer containing the MIDI data from a pattern that has been parsed.
     * @return the Sequencer from the pattern that was recently parsed
     */
    public Sequencer getSequencer()
    {
        return this.sequencer;
    }

    private void setSequencer(Sequencer sequencer)
    {
        this.sequencer = sequencer;
        initSequencer();
    }

    /**
     * Returns the sequence containing the MIDI data from the given pattern.
     * @return the Sequence from the given pattern
     */
    public Sequence getSequence(Pattern pattern)
    {
        this.renderer.reset();
        this.parser.parse(pattern);
        Sequence sequence = this.renderer.getSequence();
        return sequence;
    }

    /**
     * Returns an instance of a Sequencer that uses the provided Synthesizer as its receiver.
     * This is useful when you have made changes to a specific Synthesizer--for example, you've
     * loaded in new patches--that you want the Sequencer to use.  You can then pass the Sequencer
     * to the Player constructor.
     *
     * @param synth The Synthesizer to use as the receiver for the returned Sequencer
     * @return a Sequencer with the provided Synthesizer as its receiver
     * @throws MidiUnavailableException
     * @version 4.0
     */
    public static Sequencer getSequencerConnectedToSynthesizer(Synthesizer synth) throws MidiUnavailableException
    {
        Sequencer sequencer = MidiSystem.getSequencer(false); // Get Sequencer which is not connected to new Synthesizer.
        sequencer.open();
        if (!synth.isOpen()) {
            synth.open();
        }
        sequencer.getTransmitter().setReceiver(synth.getReceiver()); // Connect the Synthesizer to our synthesizer instance.
        return sequencer;
    }
}