/*
 * 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.util.HashMap;
import java.util.Map;

/**
 * Parses music strings, and fires events for <code>ParserListener</code> interfaces
 * when tokens are interpreted. The <code>ParserListener</code> does intelligent things
 * with the resulting events, such as create music, draw sheet music, or
 * transform the data.
 *
 * As of Version 3.0, the Parser supports turning MIDI Sequences into JFugue Patterns with the parse(Sequence)
 * method.  In this case, the ParserListeners established by a ParserBuilder use the parsed
 * events to construct the Pattern string.
 *
 *@author David Koelle
 *@version 3.0
 *@version 4.0 - Note parsing split up into many separate methods; verification added for testing purposes
 */
public final class MusicStringParser extends Parser
{
    private Map<String, Object> dictionaryMap;
    private byte keySig = 0;

    /**
     * Creates a new Parser object, and populates the dictionary with initial entries.
     * @see JFugueDefinitions
     */
    public MusicStringParser()
    {
        dictionaryMap = new HashMap<String, Object>();
        JFugueDefinitions.populateDictionary(dictionaryMap);
    }


    /**
     * Parses a <code>Pattern</code> and fires events to subscribed <code>ParserListener</code>
     * interfaces.  As the Pattern is parsed, events are sent
     * to <code>ParserLisener</code> interfaces, which are responsible for doing
     * something interesting with the music data, such as playing the music,
     * displaying it as sheet music, or transforming the pattern.
     *
     * <p>
     * The parser breaks a music string into tokens, which are separated by spaces.
     * It then determines the type of command based on the first character of the
     * token.  If the parser does not recognize the first character of the token,
     * which is limited to the command letters (K, V, T, I, L, X, #, $, @, &, +, *, |),
     * the notes (A, B, C, D, E, F, G, R),
     * and the open-bracket character ( [ ), then the token will be ignored.
     * </p>
     *
     * @param pattern the <code>Pattern</code> to parse
     * @throws Exception if there is an error parsing the pattern
     */
    public void parse(Pattern pattern) throws JFugueException
    {
        String[] tokens = pattern.getTokens();

        // If the user hasn't specified a tempo as the first token, use the default of 120
        if (tokens.length > 0) {
            if (tokens[0].toUpperCase().charAt(0) != 'T') {
                parseTempoElement("T120");
            }
        }
        
        int counter = 0;
        for (int t=0; t < tokens.length; t++)
        {
            parseToken(tokens[t]);
            counter++;
            fireProgressReported("Parsing music string...", counter, tokens.length);
        }
    }

    /**
     * This method takes a single token, and distributes it to a specific
     * element parser based on the first character in the string.
     * If the parser does not recognize the first character of the string,
     * the token will be ignored.
     *
     * @param s the single token to parse
     * @throws JFugueException if there is a problem parsing the string
     */
    private void parseToken(String s) throws JFugueException
    {
        // If there are any spaces, get out
        if (s.indexOf(" ") != -1) {
            throw new JFugueException(JFugueException.PARSER_SPACES_EXC,s,s);
        }

        s = s.toUpperCase();
        trace("--------Processing Token: ",s);

        switch(s.charAt(0))
        {
            case 'V' : parseVoiceElement(s);           break;
            case 'T' : parseTempoElement(s);           break;
            case 'I' : parseInstrumentElement(s);      break;
            case 'L' : parseLayerElement(s);           break;  // New in 3.0
            case 'K' : parseKeySignatureElement(s);    break;  // New in 3.0
            case 'X' : parseControllerElement(s);      break;  // New in 2.0
            case '@' : parseTimeElement(s);            break;  // New in 3.0
            case '*' : parsePolyPressureElement(s);    break;  // New in 3.0, also known as Key Pressure
            case '+' : parseChannelPressureElement(s); break;  // New in 3.0
            case '&' : parsePitchBendElement(s);       break;  // New in 3.0
            case '|' : parseMeasureElement(s);         break;  // New in 3.0
            case '$' : parseDictionaryElement(s);      break;  // New in 2.0
            case 'A' :
            case 'B' :
            case 'C' :
            case 'D' :
            case 'E' :
            case 'F' :
            case 'G' :
            case 'R' :
            case '[' : parseNoteElement(s); break;
            default  : break;  // Unknown characters are okay
        }
    }

    /**
     * Parses a voice element.
     * @param s the token that contains a voice element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseVoiceElement(String s) throws JFugueException
    {
        String voiceNumberString = s.substring(1,s.length());
        byte voiceNumber = getByteFromDictionary(voiceNumberString);
        if (voiceNumber > 15) {
//            throw new JFugueException(JFugueException.VOICE_EXC,voiceNumberString,s);
            return;
        }
        trace("Voice element: voice = ",voiceNumber);
        fireVoiceEvent(new Voice(voiceNumber));
    }

    /**
     * Parses a tempo element.
     * As of JFugue 4.0, Tempo can be specified in Beats Per Minute, which is much more intuitive than
     * the original Milliseconds Per Quarter Note.  To maintain compatibility with existing JFugue
     * Music Strings, those wishing to specify Tempo using BPM need to use the full word "Tempo" in
     * their music string, instead of just the initial "T".
     * To summarize:
     * "Tempo120" (or "Tempo[Allegro]") --> Tempo will be is 120 beats per minute
     * "T120" --> Tempo will be 120 milliseconds per beat.  Divide into 60000000 to get BPM.
     * @param s the token that contains a tempo element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseTempoElement(String s) throws JFugueException
    {
        String tempoNumberString = s.substring(1,s.length());
        int tempoNumber = getIntFromDictionary(tempoNumberString);
        trace("Tempo element: tempo = ",tempoNumber);
        fireTempoEvent(new Tempo(tempoNumber));
    }

    /**
     * Parses an instrument element.
     * @param s the token that contains an instrument element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseInstrumentElement(String s) throws JFugueException
    {
        String instrumentNumberString = s.substring(1,s.length());
        byte instrumentNumber = getByteFromDictionary(instrumentNumberString);
        trace("Instrument element: instrument = ",instrumentNumber);
        fireInstrumentEvent(new Instrument(instrumentNumber));
    }

    /**
     * Parses a layer element.
     * @param s the token that contains a layer element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseLayerElement(String s) throws JFugueException
    {
        String layerNumberString = s.substring(1,s.length());
        byte layerNumber = getByteFromDictionary(layerNumberString);
        trace("Layer element: layer = ",layerNumber);
        fireLayerEvent(new Layer(layerNumber));
    }

    /**
     * Parses a time element.
     * @param s the token that contains a time element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseTimeElement(String s) throws JFugueException
    {
        String timeNumberString = s.substring(1,s.length());
        long timeNumber = getLongFromDictionary(timeNumberString);
        trace("Time element: time = ",timeNumber);
        fireTimeEvent(new Time(timeNumber));
    }

    /**
     * Parses a key signature element.
     * @param s the token that contains a key signature
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseKeySignatureElement(String s) throws JFugueException
    {
        String rootNote = null;
        String majOrMin = null;
        if (s.length() == 5)
        {
            rootNote = s.substring(1, 5);
            majOrMin = s.substring(2, 5);
        } else {
            rootNote = s.substring(1, 6);
            majOrMin = s.substring(3, 6);
        }
        trace("Key signature element: root=",rootNote," majOrMin=",majOrMin);
        if (!(majOrMin.equalsIgnoreCase("MAJ") || (majOrMin.equalsIgnoreCase("MIN")))) {
            throw new JFugueException(JFugueException.KEYSIG_EXC, majOrMin, s);
        }
        int scale = (majOrMin.equalsIgnoreCase("MAJ") ? 0 : 1);
        int keySig = 0;

        if (rootNote.equalsIgnoreCase("CBMAJ") || rootNote.equalsIgnoreCase("ABMIN")) keySig = -7;
        else if (rootNote.equalsIgnoreCase("GBMAJ") || rootNote.equalsIgnoreCase("EBMIN")) keySig = -6;
        else if (rootNote.equalsIgnoreCase("DBMAJ") || rootNote.equalsIgnoreCase("BBMIN")) keySig = -5;
        else if (rootNote.equalsIgnoreCase("ABMAJ") || rootNote.equalsIgnoreCase("FMIN")) keySig = -4;
        else if (rootNote.equalsIgnoreCase("EBMAJ") || rootNote.equalsIgnoreCase("CMIN")) keySig = -3;
        else if (rootNote.equalsIgnoreCase("BBMAJ") || rootNote.equalsIgnoreCase("GMIN")) keySig = -2;
        else if (rootNote.equalsIgnoreCase("FMAJ") || rootNote.equalsIgnoreCase("DMIN")) keySig = -1;
        else if (rootNote.equalsIgnoreCase("CMAJ") || rootNote.equalsIgnoreCase("AMIN")) keySig = 0;
        else if (rootNote.equalsIgnoreCase("GMAJ") || rootNote.equalsIgnoreCase("EMIN")) keySig = +1;
        else if (rootNote.equalsIgnoreCase("DMAJ") || rootNote.equalsIgnoreCase("BMIN")) keySig = +2;
        else if (rootNote.equalsIgnoreCase("AMAJ") || rootNote.equalsIgnoreCase("F#MIN")) keySig = +3;
        else if (rootNote.equalsIgnoreCase("EMAJ") || rootNote.equalsIgnoreCase("C#MIN")) keySig = +4;
        else if (rootNote.equalsIgnoreCase("BMAJ") || rootNote.equalsIgnoreCase("G#MIN")) keySig = +5;
        else if (rootNote.equalsIgnoreCase("F#MAJ") || rootNote.equalsIgnoreCase("D#MIN")) keySig = +6;
        else if (rootNote.equalsIgnoreCase("C#MAJ") || rootNote.equalsIgnoreCase("A#MIN")) keySig = +7;
        else {
            throw new JFugueException(JFugueException.KEYSIG_EXC,s);
        }
        trace("Key signature: sig=",keySig," scale=",scale);
        fireKeySignatureEvent(new KeySignature((byte)keySig, (byte)scale));
        this.keySig = (byte)keySig;
    }

    /**
     * Parses a measure element.
     * @param s the token that contains a measure element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseMeasureElement(String s) throws JFugueException
    {
        trace("Measure element.");
        fireMeasureEvent(new Measure());
    }

    /**
     * Parses a controller element.
     * @param s the token that contains a controller element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseControllerElement(String s) throws JFugueException
    {
        int indexOfEquals = s.indexOf("=");
        if (-1 == indexOfEquals) {
            throw new JFugueException(JFugueException.CONTROL_FORMAT_EXC,s,s);
        }

        //
        //   Get the Control Index from this token.  The Control Index can be one
        //   of two things:
        //    1. A byte.  In this case, simply use the controller event referred to
        //       by that byte.
        //    2. An int.  In this case, the coarse adjuster is the high bits (div),
        //       and the fine adjuster is the low bits (mod).
        //
        String controlIndexString = s.substring(1,indexOfEquals);
        byte controlIndex = 0;
        int controlIndexInt = -1;
        try {
            controlIndex = getByteFromDictionary(controlIndexString);
        } catch (JFugueException e) {
            controlIndexInt = getIntFromDictionary(controlIndexString);
        }

        String controlValueString = s.substring(indexOfEquals+1,s.length());

        // An int was found as the Contoller Index number.  Therefore, assume
        // that the value passed to this Index is also an int, and should be
        // divided among multiple controllers
        if (-1 != controlIndexInt)
        {
            int controlValue = getIntFromDictionary(controlValueString);
            byte coarseIndex = (byte)(controlIndexInt / 128);
            byte fineIndex = (byte)(controlIndexInt % 128);

            // Special case for BANK_SELECT, which has a high byte of 0
            if (16383 == controlValue) {
                coarseIndex = 0;
                fineIndex = 32;
            }

            byte coarseValue = (byte)(controlValue / 128);
            byte fineValue = (byte)(controlValue % 128);
            trace("Combined controller element: coarse-index = ",coarseIndex,", coarse-value = ",coarseValue,"; fine-index = ",fineIndex,", fine-value = ",fineValue);
            fireControllerEvent(new Controller(coarseIndex, coarseValue));
            fireControllerEvent(new Controller(fineIndex, fineValue));
        } else {
            byte controlValue = getByteFromDictionary(controlValueString);
            trace("Controller element: index = ",controlIndex,", value = ",controlValue);
            fireControllerEvent(new Controller(controlIndex, controlValue));
        }
    }

    /**
     * Parses a channel pressure element.
     * @param s the token that contains a channel pressure element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseChannelPressureElement(String s) throws JFugueException
    {
        // A ChannelPressure token looks like this:
        //      +pressure
        //
        // where "pressure" can each be bytes or dictionary items

        String pressureString = s.substring(1,s.length());
        byte pressureNumber = getByteFromDictionary(pressureString);

        trace("ChannelPressure element: pressure = ",pressureNumber);
        fireChannelPressureEvent(new ChannelPressure(pressureNumber));
    }

    /**
     * Parses a polyphonic pressure element.
     * @param s the token that contains a polyphonic pressure element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parsePolyPressureElement(String s) throws JFugueException
    {
        // A PolyphonicPressure token looks like this:
        //      *key,pressure
        //
        // where "key" and "pressure" can each be bytes or dictionary items

        String keyString = s.substring(1,s.indexOf(','));
        byte keyNumber = getByteFromDictionary(keyString);

        String pressureString = s.substring(s.indexOf(',')+1, s.length());
        byte pressureNumber = getByteFromDictionary(pressureString);

        trace("PolyphonicPressure element: key = ",keyNumber,", pressure = ",pressureNumber);
        firePolyphonicPressureEvent(new PolyphonicPressure(keyNumber, pressureNumber));
    }

    /**
     * Parses a pitch bend element.
     * @param s the token that contains a pitch bend pressure element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parsePitchBendElement(String s) throws JFugueException
    {
        // A PitchBend token looks like one of the following:
        //      &lsb,msb
        //      &int
        //
        // where "byte1" and "byte2" or "int" can be bytes/ints or dictionary items

        byte lsb = 0;
        byte msb = 0;

        if (s.indexOf(',') > -1) {
            // We're dealing with two bytes
            String b1String = s.substring(1,s.indexOf(','));
            lsb = getByteFromDictionary(b1String);

            String b2String = s.substring(s.indexOf(',')+1, s.length());
            msb = getByteFromDictionary(b2String);
        } else {
            // We're dealing with a single integer, which we will break into bytes
            String valueString = s.substring(1,s.length());
            int value = getIntFromDictionary(valueString);
            lsb = (byte)(value % 128);
            msb = (byte)(value / 128);
        }

        trace("PitchBend element: byte1 = ",lsb,", byte2 = ",msb);
        firePitchBendEvent(new PitchBend(lsb, msb));
    }


    /**
     * Parses a dictionary element.
     * @param s the token that contains a dictionary element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseDictionaryElement(String s) throws JFugueException
    {
        int indexOfEquals = s.indexOf("=");
        String word = s.substring(1,indexOfEquals);
        String definition = s.substring(indexOfEquals+1,s.length());
        // Replace tilde's with spaces.  I don't think this will work, though, since the
        // MusicString has already been tokenized.
        definition.replace('~', ' ');
        word = word.toUpperCase();
        trace("Dictionary Definition element: word = ",word,", value = ",definition);
        dictionaryMap.put(word, definition);
    }

    class NoteContext
    {
        boolean isRest                  = false;
        boolean isNumericNote           = false;
        boolean isChord                 = false;
        boolean isFirstNote             = true;
        boolean isSequentialNote        = false;
        boolean isParallelNote          = false;
        boolean isNatural               = false;
        boolean existAnotherNote        = true;
        boolean anotherNoteIsSequential = false;
        boolean anotherNoteIsParallel   = false;
        boolean isStartOfTie            = false;
        boolean isEndOfTie              = false;
        byte[] halfsteps                = new byte[5];
        byte numHalfsteps               = 0;
        byte noteNumber                 = 0;
        int octaveNumber                = 0;
        double decimalDuration          = 0.0;
        long duration                   = 0L;
        byte attackVelocity             = Note.DEFAULT_VELOCITY;
        byte decayVelocity              = Note.DEFAULT_VELOCITY;

        public NoteContext() {
            for (int i=0; i < 5; i++) {
                halfsteps[i] = 0;
            }
        }
    }

    /**
     * Parses a note element.
     * @param s the token that contains a note element
     * @throws JFugueException if there is a problem parsing the element
     */
    private void parseNoteElement(String s) throws JFugueException
    {
        NoteContext context = new NoteContext();

        while (context.existAnotherNote) {
            trace("--Parsing note from token "+s);
            decideSequentialOrParallel(context);
            int index = 0;
            int slen = s.length(); // We pass the length of the string because it is an invariant value that is used often
            index = parseNoteRoot(s, slen, index, context);
            index = parseNoteOctave(s, slen, index, context);
            index = parseNoteChord(s, slen, index, context);
            computeNoteValue(context);
            index = parseNoteChordInversion(s, slen, index, context);
            index = parseNoteDuration(s, slen, index, context);
            index = parseNoteVelocity(s, slen, index, context);
            s = parseNoteConnector(s, slen, index, context);
            fireNoteEvents(context);
        }
    }

    private void decideSequentialOrParallel(NoteContext context)
    {
        // Test whether this note is already known to be sequential (was connected with _) or parallel (was connected with +)
        context.isSequentialNote = false;
        if (context.anotherNoteIsSequential) {
            context.isSequentialNote = true;
            context.anotherNoteIsSequential = false;
            trace("This note is sequential");
        }
        
        context.isParallelNote = false;
        if (context.anotherNoteIsParallel) {
            context.isParallelNote = true;
            context.anotherNoteIsParallel = false;
            trace("This note is parallel");
        }
    }
    
    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNoteRoot(String s, int slen, int index, NoteContext context)
    {
        switch (s.charAt(index)) {
            case '[' : return parseNumericNote(s, slen, index, context);
            case 'R' : return parseRest(s, slen, index, context);
            default  : return parseLetterNote(s, slen, index, context);
        }
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNumericNote(String s, int slen, int index, NoteContext context)
    {
        int indexOfEndBracket = s.indexOf(']', index);
        String stringInBrackets = s.substring(1,indexOfEndBracket);
        context.noteNumber = getByteFromDictionary(stringInBrackets);
        context.isNumericNote = true;

        trace("This note is a numeric note with value ", context.noteNumber);
        return indexOfEndBracket+1;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseRest(String s, int slen, int index, NoteContext context)
    {
        context.isRest = true;

        trace("This note is a Rest");
        return index+1;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
     private int parseLetterNote(String s, int slen, int index, NoteContext context)
     {
         switch(s.charAt(index)) {
             case 'C' : context.noteNumber = 0; break;
             case 'D' : context.noteNumber = 2; break;
             case 'E' : context.noteNumber = 4; break;
             case 'F' : context.noteNumber = 5; break;
             case 'G' : context.noteNumber = 7; break;
             case 'A' : context.noteNumber = 9; break;
             case 'B' : context.noteNumber = 11; break;
             default : throw new JFugueException(JFugueException.NOTE_EXC, s);
         }
         index++;

         // Check for #, b, or n (sharp, flat, or natural) modifier
         boolean checkForModifiers = true;
         while (checkForModifiers) {
             if (index < slen)
             {
                 switch(s.charAt(index)) {
                     case '#' : index++; context.noteNumber++;  /*if (context.noteNumber == 12) context.noteNumber = 0; */ break;
                     case 'B' : index++; context.noteNumber--;  /*if (context.noteNumber == -1) context.noteNumber = 11;*/ break;
                     case 'N' : index++; context.isNatural = true; checkForModifiers = false; break;
                     default : checkForModifiers = false; break;
                 }
             } else {
                 checkForModifiers = false;
             }
         }

        trace("Note number within an octave (C=0, B=11): ", context.noteNumber);
        return index;
    }

     /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNoteOctave(String s, int slen, int index, NoteContext context)
    {
        // Don't parse an octave for a rest or a numeric note
        if (context.isRest || context.isNumericNote) {
            return index;
        }

        // Check for octave.  Remember that octaves are optional.
        char possibleOctave1 = '.';
        char possibleOctave2 = '.';

        if (index < slen) {
            possibleOctave1 = s.charAt(index);
        }

        if (index+1 < slen) {
            possibleOctave2 = s.charAt(index+1);
        }

        byte definiteOctaveLength = 0;
        if ((possibleOctave1 >= '0') && (possibleOctave1 <= '9')) {
            definiteOctaveLength = 1;
            if ((possibleOctave2 >= '0') && (possibleOctave2 <= '9')) {
                definiteOctaveLength = 2;
            }

            String octaveNumberString = s.substring(index, index+definiteOctaveLength);
            try {
                context.octaveNumber = Byte.parseByte(octaveNumberString);
            } catch (NumberFormatException e) {
                throw new JFugueException(JFugueException.OCTAVE_EXC, octaveNumberString, s);
            }
            if (context.octaveNumber > 10) {
                throw new JFugueException(JFugueException.OCTAVE_EXC, octaveNumberString, s);
            }
        }

        return index+definiteOctaveLength;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNoteChord(String s, int slen, int index, NoteContext context)
    {
        // Don't parse chord for a rest 
        if (context.isRest) {
            return index;
        }

        String possibleChord3 = null;
        String possibleChord4 = null;
        String possibleChord5 = null;
        String possibleChord6 = null;
        String possibleChord7 = null;
        String possibleChord8 = null;
        try {
            possibleChord3 = s.substring(index, index+3);
            possibleChord4 = s.substring(index, index+4);
            possibleChord5 = s.substring(index, index+5);
            possibleChord6 = s.substring(index, index+6);
            possibleChord7 = s.substring(index, index+7);
            possibleChord8 = s.substring(index, index+8);
        } catch (IndexOutOfBoundsException e)
        {
            // Nothing to do... just needed to catch
        }

        int lengthOfChordString = 0;  // This represents the length of the string, not the number of halfsteps

        // Below, 'chordLength' refers to the size of the text for the chord (for example, "min"=3, "dim7"=4),
        // and 'numHalfsteps' refers to the number of elements in the halfsteps array.
        // This must be done in order from smaller to larger strings, so the longer string names
        // take effect.  This means 'min' can be overwritten by 'minmaj7', or 'maj' by 'maj7', for example.

        if (possibleChord3 != null) {
            if (possibleChord3.equals("MAJ"))
                { lengthOfChordString = 3; context.numHalfsteps = 2; context.halfsteps[0] = 4; context.halfsteps[1] = 7; }
            else if (possibleChord3.equals("MIN"))
                { lengthOfChordString = 3; context.numHalfsteps = 2; context.halfsteps[0] = 3; context.halfsteps[1] = 7; }
            else if (possibleChord3.equals("AUG"))
                { lengthOfChordString = 3; context.numHalfsteps = 2; context.halfsteps[0] = 4; context.halfsteps[1] = 8; }
            else if (possibleChord3.equals("DIM"))
                { lengthOfChordString = 3; context.numHalfsteps = 2; context.halfsteps[0] = 3; context.halfsteps[1] = 6; }
        }
        if (possibleChord4 != null) {
            if (possibleChord4.equalsIgnoreCase("DOM7"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 7; context.halfsteps[2] = 10; }
            else if (possibleChord4.equalsIgnoreCase("MAJ7"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 7; context.halfsteps[2] = 11; }
            else if (possibleChord4.equalsIgnoreCase("MIN7"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 3; context.halfsteps[1] = 7; context.halfsteps[2] = 10; }
            else if (possibleChord4.equalsIgnoreCase("SUS4"))
                { lengthOfChordString = 4; context.numHalfsteps = 2; context.halfsteps[0] = 5; context.halfsteps[1] = 7; }
            else if (possibleChord4.equalsIgnoreCase("SUS2"))
                { lengthOfChordString = 4; context.numHalfsteps = 2; context.halfsteps[0] = 2; context.halfsteps[1] = 7; }
            else if (possibleChord4.equalsIgnoreCase("MAJ6"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 7; context.halfsteps[2] = 9; }
            else if (possibleChord4.equalsIgnoreCase("MIN6"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 3; context.halfsteps[1] = 7; context.halfsteps[2] = 9; }
            else if (possibleChord4.equalsIgnoreCase("DOM9"))
                { lengthOfChordString = 4; context.numHalfsteps = 4; context.halfsteps[0] = 4; context.halfsteps[1] = 7; context.halfsteps[2] = 10; context.halfsteps[3] = 14; }
            else if (possibleChord4.equalsIgnoreCase("MAJ9"))
                { lengthOfChordString = 4; context.numHalfsteps = 4; context.halfsteps[0] = 4; context.halfsteps[1] = 7; context.halfsteps[2] = 11; context.halfsteps[3] = 14; }
            else if (possibleChord4.equalsIgnoreCase("MIN9"))
                { lengthOfChordString = 4; context.numHalfsteps = 4; context.halfsteps[0] = 3; context.halfsteps[1] = 7; context.halfsteps[2] = 10; context.halfsteps[3] = 14; }
            else if (possibleChord4.equalsIgnoreCase("DIM7"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 3; context.halfsteps[1] = 6; context.halfsteps[2] = 9; }
            else if (possibleChord4.equalsIgnoreCase("ADD9"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 7; context.halfsteps[2] = 14; }
            else if (possibleChord4.equalsIgnoreCase("DAVE"))
                { lengthOfChordString = 4; context.numHalfsteps = 3; context.halfsteps[0] = 7; context.halfsteps[1] = 14; context.halfsteps[2] = 21;}
        }

        if (possibleChord5 != null) {
            if (possibleChord5.equalsIgnoreCase("MIN11"))
                { lengthOfChordString = 5; context.numHalfsteps = 5; context.halfsteps[0] = 7; context.halfsteps[1] = 10; context.halfsteps[2] = 14; context.halfsteps[3] = 15; context.halfsteps[4] = 17; }
            else if (possibleChord5.equalsIgnoreCase("DOM11"))
                { lengthOfChordString = 5; context.numHalfsteps = 4; context.halfsteps[0] = 7; context.halfsteps[1] = 10; context.halfsteps[2] = 14; context.halfsteps[3] = 17; }
            else if (possibleChord5.equalsIgnoreCase("DOM13"))
                { lengthOfChordString = 5; context.numHalfsteps = 5; context.halfsteps[0] = 7; context.halfsteps[1] = 10; context.halfsteps[2] = 14; context.halfsteps[3] = 16; context.halfsteps[4] = 21; }
            else if (possibleChord5.equalsIgnoreCase("MIN13"))
                { lengthOfChordString = 5; context.numHalfsteps = 5; context.halfsteps[0] = 7; context.halfsteps[1] = 10; context.halfsteps[2] = 14; context.halfsteps[3] = 15; context.halfsteps[4] = 21; }
            else if (possibleChord5.equalsIgnoreCase("MAJ13"))
                { lengthOfChordString = 5; context.numHalfsteps = 5; context.halfsteps[0] = 7; context.halfsteps[1] = 11; context.halfsteps[2] = 14; context.halfsteps[3] = 16; context.halfsteps[4] = 21; }
        }

        if (possibleChord6 != null) {
            if (possibleChord6.equalsIgnoreCase("DOM7<5"))
                { lengthOfChordString = 6; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 6; context.halfsteps[2] = 10; }
            else if (possibleChord6.equalsIgnoreCase("DOM7>5"))
                { lengthOfChordString = 6; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 8; context.halfsteps[2] = 10; }
            else if (possibleChord6.equalsIgnoreCase("MAJ7<5"))
                { lengthOfChordString = 6; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 6; context.halfsteps[2] = 11; }
            else if (possibleChord6.equalsIgnoreCase("MAJ7>5"))
                { lengthOfChordString = 6; context.numHalfsteps = 3; context.halfsteps[0] = 4; context.halfsteps[1] = 8; context.halfsteps[2] = 11; }
        }

        if (possibleChord7 != null) {
            if (possibleChord7.equalsIgnoreCase("minmaj7"))
                { lengthOfChordString = 7; context.numHalfsteps = 3; context.halfsteps[0] = 3; context.halfsteps[1] = 7; context.halfsteps[2] = 11; }
        }

        if (possibleChord8 != null) {
            if (possibleChord8.equalsIgnoreCase("DOM7<5<9"))
                { lengthOfChordString = 8; context.numHalfsteps = 4; context.halfsteps[0] = 4; context.halfsteps[1] = 6; context.halfsteps[2] = 10; context.halfsteps[3] = 13; }
            else if (possibleChord8.equalsIgnoreCase("DOM7<5>9"))
                { lengthOfChordString = 8; context.numHalfsteps = 4; context.halfsteps[0] = 4; context.halfsteps[1] = 6; context.halfsteps[2] = 10; context.halfsteps[3] = 15; }
            else if (possibleChord8.equalsIgnoreCase("DOM7>5<9"))
                { lengthOfChordString = 8; context.numHalfsteps = 4; context.halfsteps[0] = 4; context.halfsteps[1] = 8; context.halfsteps[2] = 10; context.halfsteps[3] = 13; }
            else if (possibleChord8.equalsIgnoreCase("DOM7>5>9"))
                { lengthOfChordString = 8; context.numHalfsteps = 4; context.halfsteps[0] = 4; context.halfsteps[1] = 8; context.halfsteps[2] = 10; context.halfsteps[3] = 15; }
        }

        if (lengthOfChordString > 0) {
            context.isChord = true;
            trace("Chord: chordLength=", lengthOfChordString, ", so chord is one of the following: [ 3=", possibleChord3, " 4=", possibleChord4, " 5=", possibleChord5, " 6=", possibleChord6, " 7=", possibleChord7, " 8=", possibleChord8, " ]");
        }

        return index+lengthOfChordString;
    }

    /** This method does a variety of calculations to get the actual value of the note. */
    private void computeNoteValue(NoteContext context)
    {
        // Don't compute note value for a rest 
        if (context.isRest) {
            return;
        }
        
        // If we happen not to have an octave yet, set it to a default value.
        // Default octave: 5 for notes, 3 for chords
        if ((context.octaveNumber == 0) && (!context.isNumericNote)) {
            if (context.isChord) {
                context.octaveNumber = 3;
            } else {
                context.octaveNumber = 5;
            }
        }
        trace("Octave: ", context.octaveNumber);

        // Adjust for Key Signature
        if ((keySig != 0) && (!context.isNatural)) {
            if ((keySig <= -1) && (context.noteNumber == 11)) context.noteNumber = 10;
            if ((keySig <= -2) && (context.noteNumber == 4)) context.noteNumber = 3;
            if ((keySig <= -3) && (context.noteNumber == 9)) context.noteNumber = 8;
            if ((keySig <= -4) && (context.noteNumber == 2)) context.noteNumber = 1;
            if ((keySig <= -5) && (context.noteNumber == 7)) context.noteNumber = 6;
            if ((keySig <= -6) && (context.noteNumber == 0)) { context.noteNumber = 11; context.octaveNumber--; }
            if ((keySig <= -7) && (context.noteNumber == 5)) context.noteNumber = 4;
            if ((keySig >= +1) && (context.noteNumber == 5)) context.noteNumber = 6;
            if ((keySig >= +2) && (context.noteNumber == 0)) context.noteNumber = 1;
            if ((keySig >= +3) && (context.noteNumber == 7)) context.noteNumber = 8;
            if ((keySig >= +4) && (context.noteNumber == 2)) context.noteNumber = 3;
            if ((keySig >= +5) && (context.noteNumber == 9)) context.noteNumber = 10;
            if ((keySig >= +6) && (context.noteNumber == 4)) context.noteNumber = 5;
            if ((keySig >= +7) && (context.noteNumber == 11)) { context.noteNumber = 0; context.octaveNumber++; }
            trace("After adjusting for Key Signature, noteNumber=", context.noteNumber," octave=", context.octaveNumber);
        }

        // Compute the actual note number, based on octave and note
        if (!context.isNumericNote)
        {
            int intNoteNumber = (context.octaveNumber * 12) + context.noteNumber;
            if ( intNoteNumber > 127) {
                throw new JFugueException(JFugueException.NOTE_OCTAVE_EXC, Integer.toString(intNoteNumber), "");
            }
            context.noteNumber = (byte)intNoteNumber;
            trace("Computed note number: ", context.noteNumber);
        }
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNoteChordInversion(String s, int slen, int index, NoteContext context)
    {
        if (!context.isChord) {
            return index;
        }

        int inversionCount = 0;
        int inversionRootNote = -1;
        int inversionOctave = -1;

        boolean checkForInversion = true;
        while (checkForInversion) {
            if (index < slen)
            {
                switch(s.charAt(index)) {
                    case '^' : index++; inversionCount++; break;
                    case 'C' : index++; inversionRootNote = 0; break;
                    case 'D' : index++; inversionRootNote = 2; break;
                    case 'E' : index++; inversionRootNote = 4; break;
                    case 'F' : index++; inversionRootNote = 5; break;
                    case 'G' : index++; inversionRootNote = 7; break;
                    case 'A' : index++; inversionRootNote = 9; break;
                    // For 'B', need to differentiate between B note and 'b' flat
                    case 'B' : index++; if (inversionRootNote == -1) { inversionRootNote = 11; } else { inversionRootNote--; } break;
                    case '#' : index++; inversionRootNote++; break;
                    // For '0', need to differentiate between initial 0 and 0 as a second digit (i.e., 10)
                    case '0' : index++; if (inversionOctave == -1) { inversionOctave = 0; } else { inversionOctave = inversionOctave*10; } break;
                    case '1' : index++; inversionOctave = 1; break;
                    case '2' : index++; inversionOctave = 2; break;
                    case '3' : index++; inversionOctave = 3; break;
                    case '4' : index++; inversionOctave = 4; break;
                    case '5' : index++; inversionOctave = 5; break;
                    case '6' : index++; inversionOctave = 6; break;
                    case '7' : index++; inversionOctave = 7; break;
                    case '8' : index++; inversionOctave = 8; break;
                    case '9' : index++; inversionOctave = 9; break;
                    // If [, whoo boy, we're checking for a note number
                    case '[' : int indexEndBracket = s.indexOf(']', index); inversionRootNote = Integer.parseInt(s.substring(index+1, indexEndBracket-1)); index = indexEndBracket+1; break;
                    default : checkForInversion = false; break;
                }
            } else {
                checkForInversion = false;
            }
        }

        // Modify the note values based on the inversion
        if (inversionCount > 0) {
            if (inversionRootNote == -1) {
                // The root is determined by a number of carets.  Increase each half-step
                // before the inversion by 12, the number of notes in an octave.
                trace("Inversion is base on count: "+inversionCount);
                trace("Inverting "+context.noteNumber+" to be "+(context.noteNumber+12));
                context.noteNumber += 12;
                for (int i=inversionCount-1; i < context.numHalfsteps; i++)
                {
                    trace("Inverting "+context.halfsteps[i]+" to be "+(context.halfsteps[i]-12));
                    context.halfsteps[i] -= 12;
                }
            } else {
                // The root is determined by an inversionRoot.  This is much trickier, but we can
                // still figure it out.
                if (inversionOctave != -1) {
                    inversionRootNote += inversionOctave * 12;
                }
                else if (inversionRootNote < 12) {
                    int currentOctave = context.noteNumber / 12;
                    inversionRootNote += currentOctave * 12;
                }
                // Otherwise, inversionRootNote is a numeric note value, like [60]

                trace("Inversion is base on note: "+inversionRootNote);

                if ((inversionRootNote > context.noteNumber + context.halfsteps[context.numHalfsteps-1]) || (inversionRootNote < context.noteNumber)) {
                    throw new JFugueException(JFugueException.INVERSION_EXC);
                }

                trace("Inverting "+context.noteNumber+" to be "+(context.noteNumber+12));
                context.noteNumber += 12;
                for (int i=0; i < context.numHalfsteps; i++)
                {
                    if (context.noteNumber + context.halfsteps[i] >= inversionRootNote + 12) {
                        trace("Inverting "+context.halfsteps[i]+" to be "+(context.halfsteps[i]-12));
                        context.halfsteps[i]-=12;
                    }
                }
            }
        }

        return index;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNoteDuration(String s, int slen, int index, NoteContext context)
    {
        context.decimalDuration = 0.0;
        if (index < slen) {
            switch (s.charAt(index)) {
                case '/' : index = parseNumericDuration(s, slen, index, context); break;
                case 'W' :
                case 'H' :
                case 'Q' :
                case 'I' :
                case 'S' :
                case 'T' :
                case 'X' :
                case 'O' :
                case '-' : index = parseLetterDuration(s, slen, index, context); break;
                default : break;
            }
            index = parseTuplet(s, slen, index, context);
        } else {
            context.decimalDuration = 1.0/4.0; // Default duration is a quarter note
        }

//        context.duration = (long) (120.0 * 4.0 * context.decimalDuration); // javax.sound.midi.Sequence resolution is 120
        context.duration = (long) (120.0 * context.decimalDuration); // DMK 9/27/08: The *4.0 makes quarter notes 4 times as long as they should be

//        // Below is incorrect, as identified by M. Ahluwalia
//        // Tempo is now in Beats Per Minute.  Convert this to Pulses Per Quarter (PPQ), then to
//        // Pulses Per Whole (PPW), then multiply that by durationNumber for WHQITXN notes
//        double ppq = 60000000.0D / (double)this.getTempo();
//        double ppw = ppq * 4.0; // 4 quarter notes in a whole note
//        context.duration = (long)(ppw * context.decimalDuration) / 4000; 

        trace("Decimal duration is ", context.decimalDuration);
        trace("Actual duration is ", context.duration);

        return index;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseLetterDuration(String s, int slen, int index, NoteContext context)
    {
        // Check duration
        boolean durationExists = true;
        boolean isDotted = false;

        while (durationExists == true) {
            int durationNumber = 0;
            // See if the note has a duration
            // Duration is optional; default is Q (4)
            if (index < slen) {
                char durationChar = s.charAt(index);
                switch (durationChar) {
                    case '-' : if ((context.decimalDuration == 0) && (!context.isEndOfTie)) {
                                   context.isEndOfTie = true;
                                   trace("Note is end of tie");
                               } else {
                                   context.isStartOfTie = true;
                                   trace("Note is start of tie");
                               }
                               break;
                    case 'W' : durationNumber = 1; break;
                    case 'H' : durationNumber = 2; break;
                    case 'Q' : durationNumber = 4; break;
                    case 'I' : durationNumber = 8; break;
                    case 'S' : durationNumber = 16; break;
                    case 'T' : durationNumber = 32; break;
                    case 'X' : durationNumber = 64; break;
                    case 'O' : durationNumber = 128; break;
                    default  : index--; durationExists = false; break;
                }
                index++;
                if ((index < slen) && (s.charAt(index) == '.')) {
                    isDotted = true;
                    index++;
                }

                if (durationNumber > 0) {
                    double d = 1.0/durationNumber;
                    if (isDotted) {
                        context.decimalDuration += d + (d/2.0);
                    } else {
                        context.decimalDuration += d;
                    }
                }
            } else {
                durationExists = false;
            }
        }

        return index;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNumericDuration(String s, int slen, int index, NoteContext context)
    {
        // The duration has come in as a number, like 0.25 for a quarter note.
        // Advance pointer past the initial slash (/)
        index++;

        // Decimal duration is not required to be enclosed by brackets,
        // but since most of the other numerical input to a MusicString
        // is required to be in brackets, we should support it.
        if ('[' == s.charAt(index)) {
            int indexOfEndingBracket = s.indexOf(']', index);
            context.decimalDuration += getDoubleFromDictionary(s.substring(index+1, indexOfEndingBracket));
            index = indexOfEndingBracket+1;
        } else {
            int endingIndex = index;
            boolean keepAdvancingPointer = true;
            while (keepAdvancingPointer) {
                try {
                    char numericDurationChar = s.charAt(endingIndex);
                    if ((numericDurationChar >= '0') && (numericDurationChar <= '9') || (numericDurationChar == '.'))  // Decimal dot, not dotted duration
                    {
                        endingIndex++;
                    } else {
                        keepAdvancingPointer = false;
                    }
                } catch (IndexOutOfBoundsException e) {
                    keepAdvancingPointer = false;
                }
            }
            String durationNumberString = s.substring(index, endingIndex);
            context.decimalDuration += Double.parseDouble(durationNumberString);
            index = endingIndex;
        }

        trace("Decimal duration is ", context.decimalDuration);
        return index;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseTuplet(String s, int slen, int index, NoteContext context)
    {
        if (index < slen) {
            if (s.charAt(index) == '*') {
                trace("Note is a tuplet");
                index++;

                // Figure out tuplet ratio, or figure out when to stop looking for tuplet info
                boolean stopTupletParsing = false;
                int indexOfUnitsToMatch = 0;
                int indexOfNumNotes = 0;
                int counter = -1;
                while (!stopTupletParsing) {
                    counter++;
                    if (slen > index+counter) {
                        if (s.charAt(index+counter) == ':') {
                            indexOfNumNotes = index+counter+1;
                        }
                        else if ((s.charAt(index+counter) >= '0') && (s.charAt(index+counter) <= '9')) {
                            if (indexOfUnitsToMatch == 0) {
                                indexOfUnitsToMatch = index+counter;
                            }
                        }
                        else if ((s.charAt(index+counter) == '*')) {
                            // no op... artifact of parsing
                        }
                        else {
                            stopTupletParsing = true;
                        }
                    } else {
                        stopTupletParsing = true;
                    }
                }

                index += counter;

                double numerator = 2.0;
                double denominator = 3.0;
                if ((indexOfUnitsToMatch > 0) && (indexOfNumNotes > 0)) {
                    numerator = Double.parseDouble(s.substring(indexOfUnitsToMatch, indexOfNumNotes-1));
                    denominator = Double.parseDouble(s.substring(indexOfNumNotes, index));
                }
                trace("Tuplet ratio is "+numerator+":"+denominator);
                double tupletRatio = numerator / denominator;
                context.decimalDuration = context.decimalDuration * tupletRatio;
                trace("Decimal duration after tuplet is ", context.decimalDuration);
            }
        }

        return index;
    }

    /** Returns the index with which to start parsing the next part of the string, once this method is done with its part */
    private int parseNoteVelocity(String s, int slen, int index, NoteContext context)
    {
        // Don't compute note velocity for a rest 
        if (context.isRest) {
            return index;
        }

        // Process velocity attributes, if they exist
        while (index < slen) {
            int startPoint = index+1;
            int endPoint = startPoint;

            char velocityChar = s.charAt(index);
            int lengthOfByte = 0;
            if ((velocityChar == '+') || (velocityChar == '_')) break;
            trace("Identified Velocity character ", velocityChar);
            boolean byteDone = false;
            while (!byteDone && (index + lengthOfByte+1 < slen)) {
                char possibleByteChar = s.charAt(index + lengthOfByte+1);
                if ((possibleByteChar >= '0') && (possibleByteChar <= '9')) {
                    lengthOfByte++;
                } else {
                    byteDone = true;
                }
            }
            endPoint = index + lengthOfByte+1;

            // Or maybe a bracketed string was passed in, instead of a byte
            if ((index+1 < slen) && (s.charAt(index+1) == '[')) {
                endPoint = s.indexOf(']',startPoint)+1;
            }

            byte velocityNumber = getByteFromDictionary(s.substring(startPoint,endPoint));

            switch (velocityChar) {
                case 'A' : context.attackVelocity = velocityNumber;  break;
                case 'D' : context.decayVelocity = velocityNumber;   break;
                default  : throw new JFugueException(JFugueException.NOTE_VELOCITY_EXC, s.substring(startPoint,endPoint), s);
            }
            index = endPoint;
        }
        trace("Attack velocity = ", context.attackVelocity, "; Decay velocity = ", context.decayVelocity);
        return index;
    }

    /** Returns the String of the next sub-token (the parts after + or _), if one exists; otherwise, returns null */
    private String parseNoteConnector(String s, int slen, int index, NoteContext context)
    {
        context.existAnotherNote = false;
        // See if there's another note to process
        if ((index < slen) && ((s.charAt(index) == '+') || (s.charAt(index) == '_'))) {
            trace("Another note: string = ", s.substring(index, s.length()-1));
            if (s.charAt(index) == '_') {
                context.anotherNoteIsSequential = true;
                trace("Next note will be sequential");
            } else {
                context.anotherNoteIsParallel = true;
                trace("Next note will be parallel");
            }
            index++;
            context.existAnotherNote = true;
            return s.substring(index, slen);
        }
        return null;
    }

    private void fireNoteEvents(NoteContext context)
    {
        // Set up the note
        Note note = new Note();

        if (context.isRest) {
            note.setRest(true);
            note.setDuration(context.duration);
            note.setDecimalDuration(context.decimalDuration);
            note.setAttackVelocity( (byte)0 );          // turn off sound for rest notes
            note.setDecayVelocity( (byte)0 );
        } else {
            note.setValue(context.noteNumber);
            note.setDuration(context.duration);
            note.setStartOfTie(context.isStartOfTie);
            note.setEndOfTie(context.isEndOfTie);
            note.setDecimalDuration(context.decimalDuration);
            note.setAttackVelocity(context.attackVelocity);
            note.setDecayVelocity(context.decayVelocity);
        }
        note.setHasAccompanyingNotes(context.existAnotherNote || context.isChord);

        // Fire note events
        if (context.isFirstNote) {
            note.setType(Note.FIRST);
            trace("Firing first note event");
            fireNoteEvent(note);
        } else if (context.isSequentialNote) {
            note.setType(Note.SEQUENTIAL);
            trace("Firing sequential note event");
            fireSequentialNoteEvent(note);
        } else if (context.isParallelNote) {
            note.setType(Note.PARALLEL);
            trace("Firing parallel note event");
            fireParallelNoteEvent(note);
        }

        if (context.isChord) {
            for (int i=0; i < context.numHalfsteps; i++) {
                Note chordNote = new Note((byte)(context.noteNumber+context.halfsteps[i]), context.duration);
                chordNote.setDecimalDuration(context.decimalDuration); // This won't have any effect on the note, but it's good bookkeeping to have it around.
                chordNote.setType(Note.PARALLEL);
                trace("Chord note number: ", (context.noteNumber+context.halfsteps[i]));
                if (i == context.numHalfsteps-1) {
                    chordNote.setHasAccompanyingNotes(context.existAnotherNote);
                } else {
                    chordNote.setHasAccompanyingNotes(context.existAnotherNote || context.isChord);
                }
                fireParallelNoteEvent(chordNote);
            }
        }
        context.isFirstNote = false;
    }

    /**
     * Looks up a string's value in the dictionary.  The dictionary is used to
     * keep memorable names of obscure numbers - for example, the string FLUTE
     * is set to a value of 73, so when users want to play music with a flute,
     * they can say "I[Flute]" instead of "I[73]".
     *
     * <p>
     * The Dictionary feature also lets users define constants so that if the
     * value of something were to change, it only needs to be changed in one
     * place.  For example, MY_FAVORITE_INSTRUMENT could be set to 73, then you
     * can say "I[My_Favorite_Instrument]" when you want to play with that
     * instrument.  If your favorite instrument were ever to change, you only
     * have to make the change in one place, instead of every place where you
     * give the Instrument command.
     * </p>
     *
     * @param bracketedString the string to look up in the dictionary
     * @returns the definition of the string
     * @throws JFugueException if there is a problem looking up bracketedString
     */
    private String dictionaryLookup(String bracketedString) throws JFugueException
    {
        int indexOfOpeningBracket = bracketedString.indexOf("[");
        int indexOfClosingBracket = bracketedString.indexOf("]");

        String word = null;
        if ((indexOfOpeningBracket != -1) && (indexOfClosingBracket != -1)) {
            word = bracketedString.substring(indexOfOpeningBracket+1,indexOfClosingBracket);
        }
        else {
            // It appears that "bracketedString" wasn't bracketed.
            word = bracketedString;
        }
        word = word.toUpperCase();

        String definition = (String)dictionaryMap.get(word);
        while ((definition != null) && (dictionaryMap.containsKey(definition.toUpperCase()))) {
            definition = (String)dictionaryMap.get(definition.toUpperCase());
        }

        // If there is no definition for this word, see if the word is actually a number.
        if (null == definition) {
            char ch = 0;
            boolean isNumber = true;
            for (int i=0; i < word.length(); i++) {
                ch = word.charAt(i);
                if ((!Character.isDigit(ch) && (ch != '.'))) {
                    isNumber = false;
                }
            }
            if (isNumber) {
                trace("Dictionary lookup returning the number ",word);
                return word;
            } else {
                throw new JFugueException(JFugueException.WORD_NOT_DEFINED_EXC,word,bracketedString);
            }
        }
        trace("Word ",word," is defined as ",definition);
        return definition;
    }

    /**
     * Look up a byte from the dictionary
     * @param bracketedString the string to look up
     * @returns the byte value of the definition
     * @throws JFugueException if there is a problem getting a byte from the dictionary look-up
     */
    private byte getByteFromDictionary(String bracketedString) throws JFugueException
    {
        String definition = dictionaryLookup(bracketedString);
        Byte newbyte = null;
        try {
            newbyte = new Byte(definition);
        } catch (NumberFormatException e) {
            throw new JFugueException(JFugueException.EXPECTED_BYTE, definition, bracketedString);
        }
        return newbyte.byteValue();
    }

    /**
     * Look up a long from the dictionary
     * @param bracketedString the string to look up
     * @returns the long value of the definition
     * @throws JFugueException if there is a problem getting a long from the dictionary look-up
     */
    private long getLongFromDictionary(String bracketedString) throws JFugueException
    {
        String definition = dictionaryLookup(bracketedString);
        Long newlong = null;
        try {
            newlong = new Long(definition);
        } catch (NumberFormatException e) {
            throw new JFugueException(JFugueException.EXPECTED_LONG,definition,bracketedString);
        }
        return newlong.longValue();
    }

    /**
     * Look up an int from the dictionary
     * @param bracketedString the string to look up
     * @returns the int value of the definition
     * @throws JFugueException if there is a problem getting a int from the dictionary look-up
     */
    private int getIntFromDictionary(String bracketedString) throws JFugueException
    {
        String definition = dictionaryLookup(bracketedString);
        Integer newint = null;
        try {
            newint = new Integer(definition);
        } catch (NumberFormatException e) {
            throw new JFugueException(JFugueException.EXPECTED_INT,definition,bracketedString);
        }
        return newint.intValue();
    }

    /**
     * Look up a double from the dictionary
     * @param bracketedString the string to look up
     * @returns the double value of the definition
     * @throws JFugueException if there is a problem getting a double from the dictionary look-up
     */
    private double getDoubleFromDictionary(String bracketedString) throws JFugueException
    {
        String definition = dictionaryLookup(bracketedString);
        Double newdouble = null;
        try {
            newdouble = new Double(definition);
        } catch (NumberFormatException e) {
            throw new JFugueException(JFugueException.EXPECTED_DOUBLE,definition,bracketedString);
        }
        return newdouble.doubleValue();
    }

    /**
     * Checks whether a token is valid.  This method is provided for testing purposes,
     * and is not used during normal operation.
     * @param token the token to test for validity
     * @return <code>true</code> is the token is valid; <code>false</code> otherwise.
     */
    public boolean isValidToken(String token)
    {
        boolean valid = true;
        try {
            parseToken(token);
        } catch (Exception e) {
            valid = false;
        }

        return valid;
    }

    public void verifyToken(String token, final String verifyString)
    {
        ParserListener listener = new CollatedParserListener() {
            private StringBuilder results = new StringBuilder();

            public void jfugueEvent(JFugueElement e)
            {
                results.append(e.getVerifyString());

                if (!verifyString.startsWith(results.toString())) {
                    throw new JFugueException(JFugueException.VERIFICATION_EXCEPTION, results.toString(), verifyString);
                }

                results.append("; ");
            }
        };

        addParserListener(listener);
        parseToken(token);
        removeParserListener(listener);
    }

    /**
     * Parses a string which presumably contains one token, which is a note.
     *
     * @param string The String that contains one token with a note, like "C5"
     * @return a Note object representing the note parsed from the string
     */
    public static Note getNote(String string)
    {
        return getNote(new Pattern(string));
    }

    /**
     * Parses a pattern which presumably contains one token, which is a note.
     *
     * @param pattern The Pattern that contains one token with a note, like "C5"
     * @return a Note object representing the note parsed from the pattern
     */
    public static Note getNote(Pattern pattern)
    {
        final Note rootNote = new Note();

        MusicStringParser parser = new MusicStringParser();
        ParserListener renderer = new ParserListenerAdapter() {
            public void noteEvent(Note note)
            {
                rootNote.setValue(note.getValue());
                rootNote.setDuration(note.getDuration());
                rootNote.setDecimalDuration(note.getDecimalDuration());
                rootNote.setStartOfTie(note.isStartOfTie());
                rootNote.setEndOfTie(note.isEndOfTie());
                rootNote.setAttackVelocity(note.getAttackVelocity());
                rootNote.setDecayVelocity(note.getDecayVelocity());
                rootNote.setRest(note.isRest());
            }
        };

        parser.addParserListener(renderer);
        parser.parse(pattern);

        return rootNote;
    }

    /**
     * Used for diagnostic purposes.  main() makes calls to test the Pattern-to-MIDI
     * parser.
     * If you make any changes to this parser, run
     * this method ("java org.jfugue.MusicStringParser"), and make sure everything
     * works correctly.
     * @param args not used
     */
    public static void main(String[] args)
    {
        verifyTokenParsing();
    }

    /**
     * Used for diagnostic purposes.  Contains an assortment of tokens that
     * are known to parse correctly.
     */
    private static void verifyTokenParsing()
    {
        MusicStringParser parser = new MusicStringParser();
        parser.setTracing(MusicStringParser.TRACING_ON);
        try {
            long startTime = System.currentTimeMillis();

            // Don't forget -- individual tokens ONLY!  No strings with spaces!
            parser.verifyToken("C", Note.createVerifyString(60, 0.25));
            parser.verifyToken("C3", Note.createVerifyString(36, 0.25));
            parser.verifyToken("Cb3", Note.createVerifyString(35, 0.25));
            parser.verifyToken("B#3", Note.createVerifyString(48, 0.25));
            parser.verifyToken("C#3q", Note.createVerifyString(37, 0.25));
            parser.verifyToken("C3i", Note.createVerifyString(36, 0.125));
            parser.verifyToken("C3qh", Note.createVerifyString(36, 0.75));
            parser.verifyToken("C5minw", Note.createCompoundVerifyString(Note.createVerifyString(60, 1.0), Note.createVerifyString(63, 1.0, false, true, false), Note.createVerifyString(67, 1.0, false, true, false)));
            parser.verifyToken("Cmaj", Note.createCompoundVerifyString(Note.createVerifyString(36, 0.25), Note.createVerifyString(40, 0.25, false, true, false), Note.createVerifyString(43, 0.25, false, true, false)));
            parser.parseToken("Cdom9");
            parser.parseToken("Cmin11");
            parser.parseToken("Cdom7<5");
            parser.parseToken("Cminmaj7");
            parser.parseToken("Cdom7<5<9");
            parser.parseToken("Cwhqistxo");

            parser.parseToken("C10");

            parser.parseToken("V0");
            parser.parseToken("V15");
            parser.parseToken("I0");
            parser.parseToken("I[13]");
            parser.parseToken("I[Acoustic_Grand]");
            parser.parseToken("IFlute");
            parser.parseToken("Cmaj7W");
            parser.parseToken("C#5Q");
            parser.parseToken("eb3Q.");
            parser.parseToken("[Cowbell]O");

            parser.parseToken("P50"); // An unknown token should just pass through

            parser.parseToken("A");
            parser.parseToken("A+B+C");
            parser.parseToken("A_B_C");
            parser.parseToken("RW");
            parser.parseToken("[105]X");

            parser.parseToken("[105]Xa20+[98]X+[78]X");
            parser.parseToken("AW+[18]X+[cabasa]Q+Dmin");

            parser.parseToken("A/0.25");
            parser.parseToken("[70]o");

            // 2.0  Dictionary Definition and Controller Events
            parser.parseToken("$UKELE=72");
            parser.parseToken("IUKELE");
            parser.parseToken("$Volume=43");
            parser.parseToken("X[Volume]=10");
            parser.parseToken("X[PORTAMENTO_TIME]=777");

            // 2.0  Dictionary Definition in odd situations that should work
            parser.parseToken("XVolume=ON");
            parser.parseToken("[Ukele]q");

            // 2.0  Dictionary Definition and non-bytes
            parser.parseToken("$number1=1");
            parser.parseToken("$quarter=0.25");
            parser.parseToken("C4/[quarter]");
            parser.parseToken("C4q");
            parser.parseToken("[Number1]/[Quarter]");

            // 2.0  Note velocity
            parser.parseToken("Cb4qa45");
            parser.parseToken("Gb4qd67");
            parser.parseToken("F#4qa55d77");
            parser.parseToken("B4qa[Volume]d[Number1]");

            // 3.0  Layers
            parser.parseToken("L8");
            parser.parseToken("$number1=1");
            parser.parseToken("L[Number1]");

            // 3.0  Times
            parser.parseToken("@100002");

            // 3.0  Measures
            parser.parseToken("|");

            // 3.0  Tied notes
            parser.parseToken("Cq-");
            parser.parseToken("C5q-");
            parser.parseToken("C5q.-");
            parser.parseToken("C5qh-");
            parser.parseToken("C-q");
            parser.parseToken("C5-q");
            parser.parseToken("C5-q.");
            parser.parseToken("C5-qh");
            parser.parseToken("C-q-");
            parser.parseToken("C--");

            // 3.0 Pitch Bend
            parser.parseToken("&100,50");
            parser.parseToken("&512");
            parser.parseToken("$number110=110");
            parser.parseToken("&[number110],50");
            parser.parseToken("&[number110],[number110]");
            parser.parseToken("$number1010=1010");
            parser.parseToken("&[number1010]");

            // 3.0 Channel Pressure
            parser.parseToken("+100");
            parser.parseToken("$number110=110");
            parser.parseToken("+[number110]");
            parser.parseToken("+[number110]");

            // 3.0 Polyphonic Pressure
            parser.parseToken("*100,20");
            parser.parseToken("$number110=110");
            parser.parseToken("*[number110],50");
            parser.parseToken("*[number110],[number110]");

            // 4.0 Chord Inversions
            parser.parseToken("Cmaj");
            parser.parseToken("C7maj");
            parser.parseToken("C7maj^");
            parser.parseToken("C7maj^^");
            parser.parseToken("C7maj^^^");
            parser.parseToken("C7maj^E");
            parser.parseToken("C7maj^G");
            parser.parseToken("C7maj^E7");
            parser.parseToken("C7maj^G7");
            parser.parseToken("[60]maj^");
            parser.parseToken("[60]maj^^");
            parser.parseToken("[60]maj^^^");
            parser.parseToken("[60]maj^E");
            parser.parseToken("[60]maj^[67]");
            parser.parseToken("Bb6min13^^^^^^");

            // 4.0 Tuplets
            parser.parseToken("Cq*");
            parser.parseToken("Ci*5:4");
            parser.parseToken("Cs.*7:8");
            parser.parseToken("Chx.*10:11");

            // 3.0 Key Signatures
            parser.parseToken("KC#maj");
            parser.parseToken("KAbmin");
            parser.parseToken("Cn");
            parser.parseToken("Cn6");

            // 4.0 New parser
            parser.parseToken("D3");
            parser.parseToken("C##3"); // Should be like D3
            
            long endTime = System.currentTimeMillis();
            System.out.println("Time taken: "+(endTime-startTime)+"ms");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
