/*
 * 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.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.swing.event.EventListenerList;

/**
 * This class represents a segment of music.  By representing segments of music
 * as patterns, JFugue gives users the opportunity to play around with pieces
 * of music in new and interesting ways.  Patterns may be added together, transformed,
 * or otherwise manipulated to expand the possibilities of creative music.
 *
 * @author David Koelle
 * @version 2.0
 * @version 4.0 - Added Pattern Properties
 * @version 4.0.3 - Now implements Serializable
 */
public class Pattern implements Serializable
{
    private StringBuilder musicString;
    private Map<String, String> properties;

    /**
     * Instantiates a new pattern
     */
    public Pattern()
    {
        this("");
    }

    /**
     * Instantiates a new pattern using the given music string
     * @param s the music string
     */
    public Pattern(String musicString)
    {
        setMusicString(musicString);
        properties = new HashMap<String, String>();
    }

    /** Copy constructor */
    public Pattern(Pattern pattern)
    {
        this(new String(pattern.getMusicString()));
        Iterator<String> iter = pattern.getProperties().keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            String value = pattern.getProperty(key);
            setProperty(key, value);
        }
    }

    /**
     * This constructor creates a new Pattern that contains each of the given patterns
     * @version 4.0
     * */
    public Pattern(Pattern... patterns)
    {
        this();
        for (Pattern p : patterns) {
            this.add(p);
        }
    }

    /**
     * Creates a Pattern given a MIDI file - do not use.
     * Note the Package scope, limiting this method to be called
     * only by JFugue.  If you want to load MIDI, use Player.loadMidi,
     * which sets the sequence timing correctly.
     * @param file
     * @throws IOException
     * @throws InvalidMidiDataException
     */
    static Pattern loadMidi(File file) throws IOException, InvalidMidiDataException
    {
        MidiParser parser = new MidiParser();
        MusicStringRenderer renderer = new MusicStringRenderer();
        parser.addParserListener(renderer);
        parser.parse(MidiSystem.getSequence(file));
        Pattern pattern = new Pattern(renderer.getPattern().getMusicString());
        return pattern;
    }

    /**
     * Sets the music string kept by this pattern.
     * @param s the music string
     */
    public void setMusicString(String musicString)
    {
        this.musicString = new StringBuilder();
        this.musicString.append(musicString);
    }

    /**
     * Adds to the music string kept by this pattern.
     * @param s the music string to add
     */
    private void appendMusicString(String appendString)
    {
        this.musicString.append(appendString);
    }

    /**
     * Returns the music string kept in this pattern
     * @return the music string
     */
    public String getMusicString()
    {
        return this.musicString.toString();
    }

    /**
     * Inserts a MusicString before this music string.
     * NOTE - this does not call fragmentAdded!
     * @param musicString the string to insert
     */
    public void insert(String musicString)
    {
       this.musicString.insert(0, " ");
       this.musicString.insert(0, musicString);
    }

    /**
     * Adds an additional pattern to the end of this pattern.
     * @param pattern the pattern to add
     */
    public void add(Pattern pattern)
    {
        fireFragmentAdded(pattern);
        appendMusicString(" ");
        appendMusicString(pattern.getMusicString());
    }

    /**
     * Adds a music string to the end of this pattern.
     * @param musicString the music string to add
     */
    public void add(String musicString)
    {
        add(new Pattern(musicString));
    }

    /**
     * Adds an additional pattern to the end of this pattern.
     * @param pattern the pattern to add
     */
    public void add(Pattern pattern, int numTimes)
    {
        for (int i=0; i < numTimes; i++)
        {
            fireFragmentAdded(pattern);
            appendMusicString(" ");
            appendMusicString(pattern.getMusicString());
        }
    }

    /**
     * Adds a music string to the end of this pattern.
     * @param musicString the music string to add
     */
    public void add(String musicString, int numTimes)
    {
        add(new Pattern(musicString), numTimes);
    }

    /**
     * Adds a number of patterns sequentially
     * @param musicString the music string to add
     * @version 4.0
     */
    public void add(Pattern... patterns)
    {
        for (Pattern pattern : patterns) {
            add(pattern);
        }
    }

    /**
     * Adds a number of patterns sequentially
     * @param musicString the music string to add
     * @version 4.0
     */
    public void add(String... musicStrings)
    {
        for (String string : musicStrings) {
            add(string);
        }
    }

    /**
     * Adds an individual element to the pattern.  This takes into
     * account the possibility that the element may be a sequential or
     * parallel note, in which case no space is placed before it.
     * @param element the element to add
     */
    public void addElement(JFugueElement element)
    {
        String elementMusicString = element.getMusicString();

        // Don't automatically add a space if this is a continuing note event
        if ((elementMusicString.charAt(0) == '+') ||
            (elementMusicString.charAt(0) == '_')) {
            appendMusicString(elementMusicString);
        } else {
            appendMusicString(" ");
            appendMusicString(elementMusicString);
            fireFragmentAdded(new Pattern(elementMusicString));
        }
    }

    /**
     * Sets the title for this Pattern.
     * As of JFugue 4.0, the title is set as a property with the key Pattern.TITLE
     * @param title the title for this Pattern
     */
    public void setTitle(String title)
    {
        setProperty(TITLE, title);
    }

    /**
     * Returns the title of this Pattern
     * As of JFugue 4.0, the title is set as a property with the key Pattern.TITLE
     * @return the title of this Pattern
     */
    public String getTitle()
    {
        return getProperty(TITLE);
    }

    /**
     * Get a property on this pattern, such as "author" or "date".
     * @version 4.0
     */
    public String getProperty(String key)
    {
        return properties.get(key);
    }

    /**
     * Set a property on this pattern, such as "author" or "date".
     * @version 4.0
     */
    public void setProperty(String key, String value)
    {
        properties.put(key, value);
    }

    /**
     * Get all properties set on this pattern, such as "author" or "date".
     * @version 4.0
     */
    public Map<String, String> getProperties()
    {
        return properties;
    }

    /**
     * Repeats the music string in this pattern
     * by the given number of times.
     * Example: If the pattern is "A B", calling <code>repeat(4)</code> will
     * make the pattern "A B A B A B A B".
     * @version 3.0
     */
    public void repeat(int times)
    {
        repeat(null, getMusicString(), times, null);
    }

    /**
     * Only repeats the portion of this music string
     * that starts at the string index
     * provided.  This allows some initial header information to only be specified
     * once in a repeated pattern.
     * Example: If the pattern is "T0 A B", calling <code>repeat(4, 3)</code> will
     * make the pattern "T0 A B A B A B A B".
     * @version 3.0
     */
    public void repeat(int times, int beginIndex)
    {
        String string = getMusicString();
        repeat(string.substring(0, beginIndex), string.substring(beginIndex), times, null);
    }

    /**
     * Only repeats the portion of this music string
     * that starts and ends at the
     * string indices provided.  This allows some initial header information and
     * trailing information to only be specified once in a repeated pattern.
     * Example: If the pattern is "T0 A B C", calling <code>repeat(4, 3, 5)</code>
     * will make the pattern "T0 A B A B A B A B C".
     * @version 3.0
     */
    public void repeat(int times, int beginIndex, int endIndex)
    {
        String string = getMusicString();
        repeat(string.substring(0, beginIndex), string.substring(beginIndex, endIndex), times, string.substring(endIndex));
    }

    private void repeat(String header, String repeater, int times, String trailer)
    {
        StringBuffer buffy = new StringBuffer();

        // Add the header, if it exists
        if (header != null)
        {
            buffy.append(header);
        }

        // Repeat and add the repeater
        for (int i=0; i < times; i++)
        {
            buffy.append(repeater);
            if (i < times-1) {
                buffy.append(" ");
            }
        }

        // Add the trailer, if it exists
        if (trailer != null)
        {
            buffy.append(trailer);
        }

        // Create the new Pattern and return it
        this.setMusicString(buffy.toString());
    }

    /**
     * Returns a new Pattern that is a subpattern of this pattern.
     * @return subpattern of this pattern
     * @version 3.0
     */
    public Pattern subPattern(int beginIndex)
    {
        return new Pattern(substring(beginIndex));
    }

    /**
     * Returns a new Pattern that is a subpattern of this pattern.
     * @return subpattern of this pattern
     * @version 3.0
     */
    public Pattern subPattern(int beginIndex, int endIndex)
    {
        return new Pattern(substring(beginIndex, endIndex));
    }

    protected String substring(int beginIndex)
    {
        return getMusicString().substring(beginIndex);
    }

    protected String substring(int beginIndex, int endIndex)
    {
        return getMusicString().substring(beginIndex, endIndex);
    }

    public static Pattern loadPattern(File file) throws IOException
    {
        StringBuffer buffy = new StringBuffer();

        Pattern pattern = new Pattern();

        BufferedReader bread = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
        while (bread.ready()) {
            String s = bread.readLine();
            if ((s != null) && (s.length() > 1)) {
                if (s.charAt(0) != '#') {
                    buffy.append(" ");
                    buffy.append(s);
                } else {
                    String key = s.substring(1, s.indexOf(':')).trim();
                    String value = s.substring(s.indexOf(':')+1, s.length()).trim();
                    if (key.equalsIgnoreCase(TITLE)) {
                        pattern.setTitle(value);
                    } else {
                        pattern.setProperty(key, value);
                    }
                }
            }
        }
        bread.close();
        pattern.setMusicString(buffy.toString());

        return pattern;
    }

    /**
     * Saves the pattern as a text file
     * @param filename the filename to save under
     */
    public void savePattern(File file) throws IOException
    {
        BufferedWriter out = new BufferedWriter(new FileWriter(file));
        if ((getProperties().size() > 0) || (getTitle() != null)) {
            out.write("#\n");
            if (getTitle() != null) {
                out.write("# ");
                out.write("Title: ");
                out.write(getTitle());
                out.write("\n");
            }
            Iterator<String> iter = getProperties().keySet().iterator();
            while (iter.hasNext()) {
                String key = iter.next();
                if (!key.equals(TITLE)) {
                    String value = getProperty(key);
                    out.write("# ");
                    out.write(key);
                    out.write(": ");
                    out.write(value);
                    out.write("\n");
                }
            }
            out.write("#\n");
            out.write("\n");
        }
        String musicString = getMusicString();
        while (musicString.length() > 0) {
            if ((musicString.length() > 80) && (musicString.indexOf(' ', 80) > -1)) {
                int indexOf80ColumnSpace = musicString.indexOf(' ', 80);
                out.write(musicString.substring(0, indexOf80ColumnSpace));
                out.newLine();
                musicString = musicString.substring(indexOf80ColumnSpace, musicString.length());
            } else {
                out.write(musicString);
                musicString = "";
            }
        }
        out.close();
    }

    /**
     * Returns a String containing key-value pairs stored in this object's properties,
     * separated by semicolons and spaces.
     * Values are returned in the following form:
     * key1: value1; key2: value2; key3: value3
     *
     * @return a String containing key-value pairs stored in this object's properties, separated by semicolons and spaces
     */
    public String getPropertiesAsSentence()
    {
        StringBuilder buddy = new StringBuilder();
        Iterator<String> iter = getProperties().keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            String value = getProperty(key);
            buddy.append(key);
            buddy.append(": ");
            buddy.append(value);
            buddy.append("; ");
        }
        String result = buddy.toString();
        return result.substring(0, result.length()-2); // Take off the last semicolon-space
    }

    /**
     * Returns a String containing key-value pairs stored in this object's properties,
     * separated by newline characters.
     *
     * Values are returned in the following form:
     * key1: value1\n
     * key2: value2\n
     * key3: value3\n
     *
     * @return a String containing key-value pairs stored in this object's properties, separated by newline characters
     */
    public String getPropertiesAsParagraph()
    {
        StringBuilder buddy = new StringBuilder();
        Iterator<String> iter = getProperties().keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            String value = getProperty(key);
            buddy.append(key);
            buddy.append(": ");
            buddy.append(value);
            buddy.append("\n");
        }
        String result = buddy.toString();
        return result.substring(0, result.length());
    }

    /**
     * Changes all timestamp values by the offsetTime passed in.
     * NOTE: This method is only useful for patterns that have been converted from a MIDI file.
     * @param offsetTime
     */
    public void offset(long offsetTime)
    {
        StringBuffer buffy = new StringBuffer();
        String[] tokens = getMusicString().split(" ");
        for (int i=0; i < tokens.length; i++)
        {
            if ((tokens[i].length() > 0) && (tokens[i].charAt(0) == '@')) {
                String timeNumberString = tokens[i].substring(1,tokens[i].length());
                if (timeNumberString.indexOf("[") == -1) {
                    long timeNumber = new Long(timeNumberString).longValue();
                    long newTime = timeNumber + offsetTime;
                    if (newTime < 0) newTime = 0;
                    buffy.append("@" + newTime);
                } else {
                    buffy.append(tokens[i]);
                }
            } else {
                buffy.append(tokens[i]);
            }
            buffy.append(" ");
        }
        setMusicString(buffy.toString());
    }

    /**
     * Returns an array of strings representing each token in the Pattern.
     * @return
     */
    public String[] getTokens()
    {
        StringTokenizer strtok = new StringTokenizer(musicString.toString()," \n\t");

        List<String> list = new ArrayList<String>();
        while (strtok.hasMoreTokens()) {
            String token = strtok.nextToken();
            if (token != null) {
                list.add(token);
            }
        }

        String[] retVal = new String[list.size()];
        list.toArray(retVal);
        return retVal;
    }

    /**
     * Indicates whether the provided musicString is composed of valid elements
     * that can be parsed by the Parser.
     * @param musicString the musicString to test
     * @return whether the musicString is valid
     * @version 3.0
     */
//    public static boolean isValidMusicString(String musicString)
//    {
//        try {
//            Parser parser = new Parser();
//            parser.parse(musicString);
//        } catch (JFugueException e)
//        {
//            return false;
//        }
//        return true;
//    }

    //
    // Listeners
    //

    /** List of ParserListeners */
    protected EventListenerList listenerList = new EventListenerList ();

    /**
     * Adds a <code>PatternListener</code>.  The listener will receive events when new
     * parts are added to the pattern.
     *
     * @param listener the listener that is to be notified when new parts are added to the pattern
     */
    public void addPatternListener (PatternListener l) {
        listenerList.add (PatternListener.class, l);
    }

    /**
     * Removes a <code>PatternListener</code>.
     *
     * @param listener the listener to remove
     */
    public void removePatternListener (PatternListener l) {
        listenerList.remove (PatternListener.class, l);
    }

    protected void clearPatternListeners () {
        EventListener[] l = listenerList.getListeners (PatternListener.class);
        int numListeners = l.length;
        for (int i = 0; i < numListeners; i++) {
            listenerList.remove (PatternListener.class, (PatternListener)l[i]);
        }
    }

    /** Tells all PatternListener interfaces that a fragment has been added. */
    private void fireFragmentAdded(Pattern fragment)
    {
        Object[] listeners = listenerList.getListenerList ();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == PatternListener.class) {
                ((PatternListener)listeners[i + 1]).fragmentAdded(fragment);
            }
        }
    }

    /**
     * @version 3.0
     */
    public String toString()
    {
        return getMusicString();
    }

    public static final String TITLE = "Title";
}
