# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006, 2007, 2008  Tom Cato Amundsen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import absolute_import
r"""
REMEMBER: down is positive, up is negative.

All voices begin at the beginning of the staff. It is
not possible to split a voice in two like in Lilypond.

The parser will not handle fis and f in the same octave on one stem.

Rules:
 * It can be different timesignatures in different staffs.
 * \key has to come before \time

The parser does not care if you have correct number of notes in a bar.
To get bar lines you have to insert a '|'
"""

import logging
import weakref
import operator

import re
from solfege.mpd.duration import Duration
from solfege.mpd.musicalpitch import MusicalPitch, InvalidNotenameException
from solfege.mpd.track import Track, PercussionTrack
from solfege.mpd.engravers import *
from solfege.mpd.requests import *
from solfege.mpd.rat import Rat
from solfege.mpd import const
from solfege.mpd import mpdutils
from solfege.mpd import _exceptions

class ParseError(_exceptions.MpdException):
    def __init__(self, msg, lexer):
        _exceptions.MpdException.__init__(self, msg)
        self.m_lineno, self.m_linepos1, self.m_linepos2 = lexer.get_error_location()

class LexerError(_exceptions.MpdException):
    def __init__(self, msg, lexer):
        _exceptions.MpdException.__init__(self, msg)
        self.m_lineno, self.m_linepos1, self.m_linepos2 = lexer.get_error_location()

def musicalpitch_relative(first, second):
    """
    think:  \relative c'{ first second }
    Tritone handling is the same as GNU Lilypond

    I placed here instead of in MusicalPitch since it is only used
    once in parse_to_score_object and I don't think anyone need this
    in MusicalPitch.
    """
    assert isinstance(first, MusicalPitch)
    assert isinstance(second, MusicalPitch)
    n1 = second.clone()
    n1.m_octave_i = first.m_octave_i
    n2 = n1.clone()
    if n1 < first:
        n2.m_octave_i += 1
    else:
        n1.m_octave_i -= 1
    if n2.steps() - first.steps() < first.steps() - n1.steps():
        # we go  up
        n2.m_octave_i += second.m_octave_i
        return n2
    else:
        # we go down
        n1.m_octave_i += second.m_octave_i
        return n1

class UnknownClefException(_exceptions.MpdException):
    def __init__(self, clef):
        _exceptions.MpdException.__init__(self)
        self.m_clef = clef
    def __str__(self):
        return "'%s' is not a valid clef. Maybe a bug in your lessonfile?" % self.m_clef

class Clef(object):
    # Use these constants to access the data in clefdata.
    SYM = 0
    # Which staff line should the clef be on lines 1 to 5. 1 is the lowest line
    LINE = 1
    # On which position in the staff is the middle C. 0 is the middle line
    # in the staff. Positive values are up, negative are down.
    POS = 2
    clefdata = {
            'treble': ('G', 2, -6),
            'violin': ('G', 2, -6),
                 'G': ('G', 2, -6),
                'G2': ('G', 2, -6),
            'french': ('G', 1, -8),
            #
            'subbass': ('F', 5, 8),
               'bass': ('F', 4, 6),
                  'F': ('F', 4, 6),
        'varbaritone': ('F', 3, 4),
        #
           'baritone': ('C', 5, 4),
              'tenor': ('C', 4, 2),
               'alto': ('C', 3, 0),
                  'C': ('C', 3, 0),
       'mezzosoprano': ('C', 2, -2),
            'soprano': ('C', 1, -4),
    }
    octaviation_re = re.compile("(?P<name>[A-Za-z1-9]+)(?P<oct>([_^])(8|15))?$")
    def __init__(self, clefname):
        m = self.octaviation_re.match(clefname)
        if not m:
            raise UnknownClefException(clefname)
        if m.group('name') not in self.clefdata:
            raise UnknownClefException(clefname)
        try:
            self.m_octaviation = {'_8': -7, '_15': -14, '^8': 7, '^15': 14,
                             None: 0}[m.group('oct')]
        except KeyError:
            raise UnknownClefException(clefname)
        self.m_name = m.group('name')
    def get_symbol(self):
        return self.clefdata[self.m_name][self.SYM]
    def get_stafflinepos(self):
        return self.clefdata[self.m_name][self.LINE]
    def steps_to_ylinepos(self, steps):
        return 7-self.clefdata[self.m_name][self.POS] - steps + self.m_octaviation
    def an_to_ylinepos(self, an):
        def notename_to_ylinepos(n, clef):
            n = MusicalPitch.new_from_notename(n)
            i = n.steps()
            return self.steps_to_ylinepos(i)
        if an[-2:] == 'es':
            l = 3
            h = -3
        else:
            l = 1
            h = -5
        i = notename_to_ylinepos(an, self)
        while i > l:
            an = an + "'"
            i =  notename_to_ylinepos(an, self)
        while i < h:
            an = an + ","
            i = notename_to_ylinepos(an, self)
        return i


class TimeSignature:
    def __init__(self, num, den):
        self.m_num = num
        self.m_den = den

class VoiceColObj:
    def __init__(self):
        self.m_rest = None
        # the key for this dictionary is semitonepitch for the notehead
        self.m_music = {}
        self.m_beaminfo = None
        self.m_tupletinfo = None
        self.m_duration = None
        self.m_stempos = 0

class StaffColObj:
    def __init__(self):
        self.m_timesignature = None
        self.m_keysignature = None
        self.m_clef = None
        self.m_barline = None
        self.m_ledger_up = 0
        self.m_ledger_down = 0


class ScoreColObj:
    """
    """
    def __init__(self):
        self.m_timesignature_obj = None
        # These variables is used by Score to remember how wide the
        # different elements of a score column is.
        self.m_clef = 0
        self.m_keysignature = 0
        self.m_barline = 0
        self.m_timesignature = 0
        self.m_accidentals = 0
        self.m_music = 0
        self.m_leftshift = 0
        self.m_rightshift = 0

class Score:
    def __init__(self):
        # m_timeposdict is a dictionary of ScoreColObj objects, one
        # object for each column that has music or rests. The keys
        # for the dict is timepos.
        self.m_timeposdict = {}
        self.m_staffs = []
    def announce_timepos(self, timepos):
        if timepos not in self.m_timeposdict:
            self.m_timeposdict[timepos] = ScoreColObj()
    def get_engravers(self, fontsize):
        tv = self.m_timeposdict.keys()
        tv.sort()
        return self._generate_engravers(tv, fontsize)
    def get_first_engravers(self, fontsize):
        tv = self.m_timeposdict.keys()
        tv.sort()
        return self._generate_engravers([tv[0]], fontsize)
    def _generate_engravers(self, tv, fontsize):
        V = []
        clef = None
        self.m_spanner_list = []
        self.m_stem_list = []
        for staff in self.m_staffs:
            staff.refill_accidentals_info(("c", "major"))
            key = ('c', 'major')
            se = []
            V.append(se)
            ###################
            beam = None
            tuplet = None
            for voice in staff.m_voice_list:
                for timepos in tv:
                    if timepos not in staff.m_coldict:
                        continue
                    if timepos not in voice.m_coldict:
                        continue
                    if staff.m_coldict[timepos].m_clef:
                        clef = staff.m_coldict[timepos].m_clef
                    ########
                    # stems
                    v = []
                    for n in voice.m_coldict[timepos].m_music.values():
                        v.append(clef.steps_to_ylinepos(n.m_pitch.steps()))
                    if v and (voice.m_coldict[timepos].m_music.values()[0].m_duration.m_nh > 1):
                        v.sort()
                        if voice.m_coldict[timepos].m_beaminfo == 'start':
                            beam = BeamEngraver(fontsize)
                            self.m_spanner_list.append(beam)
                            se.append(beam)
                        if beam and not voice.m_coldict[timepos].m_beaminfo:
                            beam = None
                        se.append(StemEngraver(timepos, fontsize, v,
                                  voice.m_coldict[timepos],
                                  beam is not None))
                        self.m_stem_list.append(se[-1])
                        if beam:
                            beam.add_stem(se[-1])
                    ################################
                    # tuplets are created per-voice
                    ################################
                    if voice.m_coldict[timepos].m_tupletinfo:
                        if voice.m_coldict[timepos].m_tupletinfo == 'continue':
                            tuplet.add_stem(se[-1])
                        else:
                            tuplet = TupletEngraver(fontsize, voice.m_coldict[timepos].m_tupletinfo)
                            tuplet.add_stem(se[-1])
                            self.m_spanner_list.append(tuplet)
                            se.append(tuplet)
                    else:
                        #if tuplet is not None: tuplet = None
                        tuplet = None
            ###################
            # this loop takes care of stuff that is decided on a per-staff-basis
            for timepos in tv:
                if timepos not in staff.m_coldict:
                    continue
                # clef
                if staff.m_coldict[timepos].m_clef:
                    clef = staff.m_coldict[timepos].m_clef
                    se.append(ClefEngraver(timepos, fontsize,
                                           staff.m_coldict[timepos].m_clef))
                    self.m_timeposdict[timepos].m_clef \
                       = max(self.m_timeposdict[timepos].m_clef, se[-1].get_width())
                #key signature
                if staff.m_coldict[timepos].m_keysignature:
                    se.append(KeySignatureEngraver(timepos, fontsize, key,
                                     staff.m_coldict[timepos].m_keysignature, clef))
                    self.m_timeposdict[timepos].m_keysignature \
                       = max(self.m_timeposdict[timepos].m_keysignature,
                             se[-1].get_width())
                    key = staff.m_coldict[timepos].m_keysignature
                    staff.refill_accidentals_info(key)
                # barline
                if staff.m_coldict[timepos].m_barline:
                    se.append(BarlineEngraver(timepos, fontsize, "|"))
                    self.m_timeposdict[timepos].m_barline \
                       = max(self.m_timeposdict[timepos].m_barline,
                             se[-1].get_width())
                    staff.refill_accidentals_info(key)
                # time signature
                if self.m_timeposdict[timepos].m_timesignature_obj:
                    se.append(TimeSignatureEngraver(timepos, fontsize,
                       self.m_timeposdict[timepos].m_timesignature_obj))
                    self.m_timeposdict[timepos].m_timesignature \
                       = max(self.m_timeposdict[timepos].m_timesignature,
                             se[-1].get_width())
                ##############
                # accidentals
                v = {}
                for voice in staff.m_voice_list:
                    if timepos not in voice.m_coldict:
                        continue
                    for music in voice.m_coldict[timepos].m_music.itervalues():
                        e = staff.needed_accidental(music.m_pitch)
                        if e is not None:
                            v[clef.steps_to_ylinepos(music.m_pitch.steps())] = e
                if v:
                    se.append(AccidentalsEngraver(timepos, fontsize, v))
                    self.m_timeposdict[timepos].m_accidentals \
                       = max(self.m_timeposdict[timepos].m_accidentals,
                            se[-1].get_width())
                ################################
                # xshift noteheads that need it

                # first we have to find out what voice has the highest tones,
                # because we have to lay out the noteheads in the highest
                # voice first.
                voicelist = []
                if len(staff.m_voice_list) == 1:
                    voicelist = [staff.m_voice_list[0]]
                else:
                    def f(A, B, timepos=timepos):
                        if timepos not in B.m_coldict:
                            return 1
                        if timepos not in A.m_coldict:
                            return 1
                        return cmp(B.m_coldict[timepos].m_music.values()[0].m_pitch.semitone_pitch(), A.m_coldict[timepos].m_music.values()[0].m_pitch.semitone_pitch())
                        return 1
                    staff.m_voice_list.sort(f)
                    voicelist = staff.m_voice_list
                voice1_lowest_ylinepos = None
                for voice in voicelist:
                    if timepos not in voice.m_coldict:
                        continue
                    # nd:
                    # * the keys in the dictionary is the position on the
                    #   staff the notehead will have. 0 is the middle line,
                    #   1 is below the middle line, -2 is on the line above
                    #   the middle line.
                    # * the values is the requests.MusicRequest that represents
                    #   the notehead.
                    nd = {}
                    for mm in voice.m_coldict[timepos].m_music.itervalues():
                        nd[clef.steps_to_ylinepos(mm.m_pitch.steps())] = mm
                    if nd == {}:
                        # nd == {} when there are not noteheads, for example
                        # when there is a rest here.
                        break
                    v = nd.keys()
                    v.sort()
                    if voice.m_coldict[timepos].m_stemdir == const.UP:
                        # if we are stemUp, we assume this is the first voice
                        # to be layed out.
                        voice1_lowest_ylinepos = v[-1]
                        v.reverse()
                        for n in range(1, len(v)):
                            if nd[v[n]].m_pitch.steps() == nd[v[n-1]].m_pitch.steps()+1 and (not nd[v[n-1]].m_shift):
                                nd[v[n]].m_shift = 1
                                self.m_timeposdict[timepos].m_rightshift = 1
                    else:
                        # the first notehead will decide where to place
                        # the stem
                        stempos = 0 # default
                        if voice1_lowest_ylinepos is not None and v[0] == voice1_lowest_ylinepos + 1:
                            stempos = 1 # stem moved right
                            voice.m_coldict[timepos].m_stempos = 1
                            nd[v[0]].m_shift = 1
                        for n in range(1, len(v)):
                            if nd[v[n]].m_pitch.steps()+1 == nd[v[n-1]].m_pitch.steps() and nd[v[n-1]].m_shift == stempos:
                                nd[v[n]].m_shift = stempos - 1
                                if stempos == 0:
                                    self.m_timeposdict[timepos].m_leftshift = 1
                            else:
                                nd[v[n]].m_shift = stempos
                #####################################
                # create notehead and rest engravers
                for voice in staff.m_voice_list:
                    if timepos not in voice.m_coldict:
                        continue
                    for music in voice.m_coldict[timepos].m_music.itervalues():
                        if music.m_duration.m_nh < 2:
                            head = const.NOTEHEAD_0
                        elif music.m_duration.m_nh > 2:
                            head = const.NOTEHEAD_2
                        else:
                            head = const.NOTEHEAD_1
                        ylinepos = clef.steps_to_ylinepos(music.m_pitch.steps())
                        se.append(NoteheadEngraver(timepos, fontsize, music.m_shift,
                             ylinepos, head, music.m_duration.m_dots,
                             music.m_pitch.semitone_pitch(),
                             voice.m_coldict[timepos]))
                        self.m_timeposdict[timepos].m_music \
                           = max(self.m_timeposdict[timepos].m_music, se[-1].get_width())
                    l = self.m_timeposdict[timepos].m_leftshift
                    r = self.m_timeposdict[timepos].m_rightshift
                    self.m_timeposdict[timepos].m_music \
                       = max(self.m_timeposdict[timepos].m_music, (1+l+r)*dimentions[fontsize].xshift) + 2
                    #
                    if voice.m_coldict[timepos].m_rest:
                        se.append(RestEngraver(timepos, fontsize, 0,
                              voice.m_coldict[timepos].m_rest.m_duration))
                        self.m_timeposdict[timepos].m_music \
                           = max(self.m_timeposdict[timepos].m_music, se[-1].get_width())
                ####################################
                # Find out if wee need ledger lines
                up = 0
                down = 0
                for voice in staff.m_voice_list:
                    if timepos not in voice.m_coldict:
                        continue
                    for music in voice.m_coldict[timepos].m_music.itervalues():
                        ypos = music.m_pitch.steps()
                        ypos = clef.steps_to_ylinepos(ypos)
                        if up > ypos < -5:
                            up = ypos
                        if down < ypos > 5:
                            down = ypos
                ###############################
                # Create ledger line engravers
                if timepos in staff.m_coldict:
                    if up:
                        up = - up / 2 - 2
                    else:
                        up = 0
                    if down:
                        down = down / 2 - 2
                    else:
                        down = 0
                    e = LedgerLineEngraver(timepos, fontsize, up, down)
                    se.append(e)
            for voice in staff.m_voice_list:
                for tie in voice.m_ties:
                    if tie[0] in tv and tie[1] in tv:
                        sh1 = tie[3].m_shift
                        sh2 = tie[4].m_shift
                        se.append(TieEngraver(fontsize, tie[0], tie[1],
                                              sh1, sh2,
                                              clef.steps_to_ylinepos(tie[2][0])))
        #########################################
        xv = {}
        p = 0
        pv = self.m_timeposdict.keys()
        pv.sort()
        class Dummy:
            pass
        for timepos in pv:
            xv[timepos] = Dummy()
            xv[timepos].m_clef = p
            p += self.m_timeposdict[timepos].m_clef
            xv[timepos].m_keysignature = p
            p += self.m_timeposdict[timepos].m_keysignature
            xv[timepos].m_barline = p
            p += self.m_timeposdict[timepos].m_barline
            xv[timepos].m_timesignature = p
            p += self.m_timeposdict[timepos].m_timesignature
            xv[timepos].m_accidentals = p
            p += self.m_timeposdict[timepos].m_accidentals
            if self.m_timeposdict[timepos].m_leftshift:
                p += 10
            xv[timepos].m_music = p
            p += self.m_timeposdict[timepos].m_music
        for ev in V:
            for e in ev:
                #FIXME all engravers should take two arguments, i think...
                if isinstance(e, AccidentalsEngraver):
                    e.set_xpos(xv, self.m_timeposdict)
                else:
                    e.set_xpos(xv)
        for e in self.m_spanner_list:
            e.do_layout()
        # we delete it because it is not used any more, and to help avoid
        # circular references
        del self.m_spanner_list
        for e in self.m_stem_list:
            e.calc_xpos()
        del self.m_stem_list
        return V
    def add_staff(self):
        staff = Staff(self)
        self.m_staffs.append(staff)
        return staff
    def get_midi_events(self, start=None, end=None):
        kv = self.m_timeposdict.keys()
        kv.sort()
        if start is None and end is not None:
            return self._generate_midi_events(
                     [i for i in kv if i < end], Track)
        elif start is not None and end is None:
            return self._generate_midi_events(
                     [i for i in kv if i >= start], Track)
        elif start is None and end is None:
            return self._generate_midi_events(kv, Track)
        else:
            assert start is not None and end is not None
            return self._generate_midi_events(
               [i for i in kv if start <= i < end], Track)
    def get_first_beat_midi_events(self):
        kv = self.m_timeposdict.keys()
        kv.sort()
        return self._generate_midi_events([kv[0]], Track)
    def get_last_beat_midi_events(self):
        kv = self.m_timeposdict.keys()
        kv.sort()
        return self._generate_midi_events([kv[-1]], Track)
    def get_midi_events_as_percussion(self):
        kv = self.m_timeposdict.keys()
        kv.sort()
        return self._generate_midi_events(kv, PercussionTrack)
    def _generate_midi_events(self, kv, tracktype):
        """
        kv is a list of rat.Rat that tell the timepos for all the tones
        we should generate midi events for.
        Return a list of tracks, one track for each voice.
        """
        track_list = []
        for staff in self.m_staffs:
            for voice in staff.m_voice_list:
                track_list.append(voice.generate_track_for_voice(staff, kv, tracktype))
        return track_list


class Staff:
    def __init__(self, score):
        self.w_score = weakref.ref(score)
        self.m_coldict = {}
        self.m_voice_list = []
    def add_voice(self):
        voice = Voice(self)
        self.m_voice_list.append(voice)
        return voice
    def refill_accidentals_info(self, key):
        """Fill the .m_accidentals_info dict with the accidentals
        that exist in the key signature `key`.
        """
        self.m_accidentals_info = {}
        for step in range(MusicalPitch.LOWEST_STEPS, MusicalPitch.HIGHEST_STEPS+1):
            self.m_accidentals_info[step] = 0
        for a in mpdutils.key_to_accidentals(key):
            n = MusicalPitch.new_from_notename(a)
            for oct in range(-4, 7):
                n.m_octave_i = oct
                if n.semitone_pitch() < 128:
                    if a[-4:] == 'eses':
                        self.m_accidentals_info[n.steps()] = -2
                    elif a[-2:] == 'es':
                        self.m_accidentals_info[n.steps()] = -1
                    elif a[-4:] == 'isis':
                        self.m_accidentals_info[n.steps()] = 2
                    else:
                        self.m_accidentals_info[n.steps()] = 1
    def needed_accidental(self, m):
        steps = m.steps()
        if m.m_accidental_i != self.m_accidentals_info[steps]:
            if (self.m_accidentals_info[steps] == 2 and m.m_accidental_i == 1) \
                    or (self.m_accidentals_info[steps] == -2 and m.m_accidental_i == -1):
                self.m_accidentals_info[steps] = m.m_accidental_i
                return [0, m.m_accidental_i]
            self.m_accidentals_info[steps] = m.m_accidental_i
            return [m.m_accidental_i]
    def barline(self, timepos):
        #assert timepos not in self.m_coldict
        self.m_coldict[timepos] = StaffColObj()
        self.m_coldict[timepos].m_barline = 1
    def add_timesignature(self, t, timepos):
        #self.m_coldict[timepos].m_timesignature = t
        self.w_score().announce_timepos(timepos)
        self.w_score().m_timeposdict[timepos].m_timesignature_obj = t
    def add_keysignature(self, timepos, key):
        self.m_coldict[timepos].m_keysignature = key
    def add_clef(self, timepos, clef):
        self.m_coldict[timepos].m_clef = Clef(clef)
    def announce_timepos(self, timepos):
        if timepos not in self.m_coldict:
            self.m_coldict[timepos] = StaffColObj()
            self.w_score().announce_timepos(timepos)


class Voice:
    def __init__(self, parent_staff):
        self.m_coldict = {}
        self.w_parent_staff = weakref.ref(parent_staff)
        self._tmp_tie = {}
        self.m_ties = []
        self.m_beams = []
        self.m_is_beaming = None
        self.m_doing_tuplet = None
    def add_notehead(self, timepos, music, stemdir):
        assert timepos in self.m_coldict
        key = (music.m_pitch.steps(), music.m_pitch.m_accidental_i)
        if __debug__:
            if key in self.m_coldict[timepos].m_music:
                logging.warning("Voice: warning, adding the same notehead twice %s" % str(key))
            if (self.m_coldict[timepos].m_duration is not None) \
                    and (self.m_coldict[timepos].m_duration != music.m_duration):
                logging.warning("mpd: warning: All noteheads on the same stem should have the same length")
        self.m_coldict[timepos].m_duration = music.m_duration
        if self.m_is_beaming and self.m_coldict[timepos].m_duration.get_rat_value() >= Rat(1, 4):
            logging.warning("mpd: warning: beamed stems has to be 1/8-note or shorter. Ignoring invalid beam request.")
            self.m_is_beaming = None
        music.m_shift = 0
        self.m_coldict[timepos].m_music[key] = music
        self.m_coldict[timepos].m_stemdir = stemdir
    def add_rest(self, rest, timepos):
        assert timepos in self.m_coldict
        self.m_coldict[timepos].m_rest = rest
    def announce_timepos2(self, timepos):
        if timepos not in self.m_coldict:
            self.m_coldict[timepos] = VoiceColObj()
            self.w_parent_staff().announce_timepos(timepos)
    def announce_timepos(self, timepos):
        if timepos not in self.m_coldict:
            self.m_coldict[timepos] = VoiceColObj()
            if self.m_is_beaming:
                self.m_coldict[timepos].m_beaminfo = 'continue'
            if self.m_doing_tuplet:
                self.m_coldict[timepos].m_tupletinfo = 'continue'
            self.w_parent_staff().announce_timepos(timepos)
    def start_tuplet(self, timepos, times, dir):
        self.m_coldict[timepos].m_tupletinfo = (times.m_den, dir)
        self.m_doing_tuplet = 1
    def end_tuplet(self):
        self.m_doing_tuplet = None
    def start_beam(self, timepos):
        if self.m_is_beaming:
            logging.warning("mpd-warning: we are already beaming, ignoring start_beam request")
            return
        self.m_is_beaming = 1
        self.m_coldict[timepos].m_beaminfo = 'start'
    def end_beam(self):
        if not self.m_is_beaming:
            logging.warning("mpd-warning: we are not beaming, ignoring stop_beam request")
        self.m_is_beaming = 0
    def do_tie_end(self, pos1, pos2):
        for p in self.m_coldict[pos1].m_music:
            if p in self.m_coldict[pos2].m_music:
                self.m_ties.append((pos1, pos2, p,
                       # need these to generate midi track
                       self.m_coldict[pos1].m_music[p],
                       self.m_coldict[pos2].m_music[p]))
    def generate_track_for_voice(self, staff, kv, tracktype):
        # first we find the id()'s of music to tie
        tie_from_v = []
        tie_to_v = []
        for t in self.m_ties:
            tie_from_v.append(id(t[3]))
            tie_to_v.append(id(t[4]))
        ################
        D = {}
        i = 2
        musictimepos = Rat(0, 1)
        last_timepos = kv[0]
        id_D = {}
        for idx in range(len(kv)):
            coltimepos = kv[idx]
            musictimepos = musictimepos + (coltimepos - last_timepos)
            if coltimepos in self.m_coldict:
                for n in sorted(self.m_coldict[coltimepos].m_music.values(),
                        key=operator.attrgetter('m_pitch')):
                    #FIXME this is ugly
                    if id(n) in tie_from_v and (id(n) not in tie_to_v):
                        id_D[n.m_pitch.semitone_pitch()] = i
                        if musictimepos not in D:
                            D[musictimepos] = []
                        D[musictimepos].append((i, const.START_NOTE, n.m_pitch.semitone_pitch()))
                    elif id(n) in tie_to_v:
                        stop_pos = musictimepos + n.m_duration.get_rat_value()
                        if stop_pos not in D:
                            D[stop_pos] = []
                        D[stop_pos].append((id_D[n.m_pitch.semitone_pitch()],
                               const.STOP_NOTE, n.m_pitch.semitone_pitch()))
                    else:
                        if musictimepos not in D:
                            D[musictimepos] = []
                        D[musictimepos].append((i, const.START_NOTE, n.m_pitch.semitone_pitch()))

                        stop_pos = musictimepos + n.m_duration.get_rat_value()
                        if stop_pos not in D:
                            D[stop_pos] = []
                        D[stop_pos].append((i, const.STOP_NOTE, n.m_pitch.semitone_pitch()))
                        i = i + 1
            last_timepos = coltimepos
        return self.__gen_midi_last_step(D, tracktype)
    def __gen_midi_last_step(self, D, tracktype):
        keys = D.keys()
        keys.sort()
        prev_time = Rat(0)
        ms = tracktype()
        for k in keys:
            delta = None
            if k != Rat(0, 1):
                delta = k-prev_time
            prev_time = k
            for e in D[k]:
                if e[1] == const.START_NOTE:
                    if delta:
                        ms.notelen_time(delta)
                    ms.start_note(e[2], const.DEFAULT_VELOCITY)
                elif e[1] == const.STOP_NOTE:
                    if delta:
                        ms.notelen_time(delta)
                    ms.stop_note(e[2], const.DEFAULT_VELOCITY)
                delta = None
        return ms

def parse_to_score_object(music):
    lexer = Lexer(music)
    relative_mode = None
    relto=None
    transpose_pitch = None
    TOPLEVEL = 1#'toplevel'
    NOTES = 2#'notes'
    START_OF_CHORD = 3#'start-of-chord'
    CHORD = 4#'chord'
    context = TOPLEVEL
    score = Score()
    chord_duration = None
    tie_is_in_the_air = 0
    x = len(lexer.m_string)
    times = None
    cur_staff = None
    while True:
        try:
            toc, toc_data = lexer.get()
        except Exception, e:
            if 'm_mpd_badcode' not in dir(e):
                e.m_lineno, e.m_linepos1, e.m_linepos2 = lexer.get_error_location()
            raise
        if toc == Lexer.STAFF:
            assert context == TOPLEVEL
            cur_staff = score.add_staff()
            cur_voice = cur_staff.add_voice()
            stem_dir = const.BOTH
            tuplet_dir = const.BOTH
            relative_mode = None
            timepos = Rat(0)
            last_pos = timepos
            cur_staff.announce_timepos(timepos)
            cur_staff.add_clef(timepos, "violin")
        elif toc == Lexer.VOICE:
            if not cur_staff:
                raise ParseError("Don't use \\addvoice before \\staff", lexer)
            relative_mode = None
            timepos = Rat(0)
            cur_voice = cur_staff.add_voice()
        elif toc == Lexer.RELATIVE:
            assert not relative_mode
            relative_mode = 1
            relto = toc_data
        elif toc == Lexer.TRANSPOSE:
            transpose_pitch = toc_data
        elif toc == Lexer.TIME:
            cur_staff.announce_timepos(timepos)
            cur_staff.add_timesignature(toc_data, timepos)
        elif toc == Lexer.KEY:
            p = MusicalPitch.new_from_notename(toc_data[0])
            if transpose_pitch:
                p.transpose_by_musicalpitch(transpose_pitch)
            k = (p.get_notename(), toc_data[1])
            cur_staff.announce_timepos(timepos)
            cur_staff.add_keysignature(timepos, k)
        elif toc == Lexer.TIMES:
            if not times:
                times = toc_data
                cur_voice.announce_timepos(timepos)
                cur_voice.start_tuplet(timepos, times, tuplet_dir)
            else:
                raise ParseError(r"\times nn/nn does not nest", lexer)
        elif toc == Lexer.CLEF:
            cur_staff.announce_timepos(timepos)
            try:
                cur_staff.add_clef(timepos, toc_data)
            except UnknownClefException, e:
                e.m_lineno, e.m_linepos1, e.m_linepos2 = lexer.get_error_location()
                raise
        elif toc == '|':
            cur_staff.announce_timepos(timepos)
            cur_staff.barline(timepos)
        elif toc == '{':
            if (context == TOPLEVEL):
                context = NOTES
                if not cur_staff.m_coldict[Rat(0, 1)].m_keysignature:
                    if transpose_pitch:
                        k = (transpose_pitch.get_notename(), 'major')
                    else:
                        k = ('c', 'major')
                    cur_staff.add_keysignature(Rat(0, 1), k)
            else:
                raise ParseError("Token '{' not allowed here.", lexer)
        elif toc == '<':
            if context == NOTES:
                context = START_OF_CHORD
            else:
                raise ParseError("Token '<' not allowed here.", lexer)
        elif toc == '>':
            if context == CHORD:
                if tie_is_in_the_air:
                    tie_is_in_the_air = 0
                    cur_voice.do_tie_end(last_pos, timepos)
                last_pos = timepos
                timepos = timepos + chord_duration.get_rat_value()
                chord_duration = None
                relto = relto_backup; relto_backup = None
                context = NOTES
            else:
                raise ParseError("Token '>' not allowed here.", lexer)
        elif toc == '}':
            if context == NOTES:
                if times:
                    times = None
                    cur_voice.end_tuplet()
                else:
                    context = TOPLEVEL
            else:
                raise ParseError("Token '}' not allowed here.", lexer)
        elif toc == '[':
            cur_voice.announce_timepos(timepos)
            cur_voice.start_beam(timepos)
        elif toc == ']':
            cur_voice.end_beam()
            # we call announce_timepos2, because the regular
            # version breaks tuplets. FIXME
            cur_voice.announce_timepos2(timepos)
        elif toc == '~':
            tie_is_in_the_air = 1
        elif toc == Lexer.NOTE and (context in [NOTES, CHORD, START_OF_CHORD]):
            if times:
                toc_data.m_duration.m_tuplet = times
            if context in [NOTES, START_OF_CHORD]:
                cur_voice.announce_timepos(timepos)
            if relative_mode:
                toc_data.m_pitch = musicalpitch_relative(
                                          relto, toc_data.m_pitch)
                relto = toc_data.m_pitch.clone()
            if transpose_pitch:
                toc_data.transpose(transpose_pitch)
            cur_voice.add_notehead(timepos, toc_data, stem_dir)
            if context == NOTES:
                if tie_is_in_the_air:
                    cur_voice.do_tie_end(last_pos, timepos)
                    tie_is_in_the_air = 0
                last_pos = timepos
                timepos = timepos + toc_data.m_duration.get_rat_value()
            if context == START_OF_CHORD:
                relto_backup = relto
                chord_duration = toc_data.m_duration
                context = CHORD
        elif toc == Lexer.SKIP and context == NOTES:
            last_pos = timepos
            timepos = timepos + toc_data.get_rat_value()
        elif toc == Lexer.REST and context == NOTES:
            cur_voice.announce_timepos(timepos)
            cur_voice.add_rest(toc_data, timepos)
            last_pos = timepos
            timepos = timepos + toc_data.m_duration.get_rat_value()
        elif toc == Lexer.STEMDIR:
            stem_dir = toc_data
        elif toc == Lexer.TUPLETDIR:
            tuplet_dir = toc_data
        elif (toc, toc_data) == (None, None):
            return score
        else:
            raise ParseError(toc, lexer)


class Lexer:
    STAFF = 1
    VOICE = 2
    CLEF = 3
    STEMDIR = 4
    TRANSPOSE = 5
    TIME = 6
    KEY = 7
    NOTE = 8
    SKIP = 9
    REST = 10
    RELATIVE = 11
    TIMES = 12
    TUPLETDIR = 13
    re_staff = re.compile(r"\\staff", re.UNICODE)
    re_voice = re.compile(r"\\addvoice", re.UNICODE)
    re_clef = re.compile(r"\\clef\s+(\w*)", re.UNICODE)
    re_clef_quoted = re.compile(r"\\clef\s+\"([A-Za-z1-9]+[_^1-9]*)\"", re.UNICODE)
    re_stem_updown = re.compile(r"(\\stem)(Up|Down|Both)\s+", re.UNICODE)
    re_tuplet_updown = re.compile(r"(\\tuplet)(Up|Down|Both)\s+", re.UNICODE)
    re_relative = re.compile(r"\\relative\s+(([a-zA-Z]+)([',]*))", re.UNICODE)
    re_transpose = re.compile(r"\\transpose\s+(([a-zA-Z]+)([',]*))", re.UNICODE)
    re_rest = re.compile(r"(r)([\d]*)(\.*)", re.UNICODE)
        #FIXME we are a little more strict than Lilypond, since ~ has to
        # be before ]
        #FIXME don't use named regex if we don't need it.
    re_melodic = re.compile(r"""(?x)
                             ((?P<notename>[a-zA-Z]+)
                             (?P<octave>[',]*))
                             (?P<len>[\d]*)
                             (?P<dots>\.*)""", re.UNICODE)
    re_skip = re.compile(r"""(?x)
                             (s)
                             (?P<len>[\d]*)
                             (?P<dots>\.*)""", re.UNICODE)
    re_time = re.compile(r"\\time\s+(\d+)\s*/\s*(\d+)", re.UNICODE)
    re_key = re.compile(r"\\key\s+([a-z]+)\s*\\(major|minor)", re.UNICODE)
    re_times = re.compile(r"\\times\s+(\d+)\s*/\s*(\d+)\s*{", re.UNICODE)
    def __init__(self, s):
        self.m_string = s
        self.m_notelen = Duration(4, 0)
        self.m_idx = 0
        self.m_last_idx = None
    def get(self):
        """
        return None, None when there are no more tokens.
        """
        # Doing this while loop inside the exception clause is a little
        # faster than using a regular expression.
        try:
            while self.m_string[self.m_idx] in (' ', '\n', '\t'):
                self.m_idx += 1
        except IndexError:
            pass
        self.m_last_idx = self.m_idx
        m = self.re_rest.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            resttype, notelen, dots = m.groups()
            numdots = len(dots)
            if notelen:
                notelen = int(notelen)
            else:
                notelen = 0
                if numdots:
                    raise LexerError('Need a digit before dots. Write "%(goodcode)s", not "%(badcode)s".' % {
                        'badcode': m.group().strip(),
                        'goodcode':'%s%i%s' % (resttype, self.m_notelen.m_nh, dots)
                        },
                        self)
            r = RestRequest(notelen, numdots)
            if not r.m_duration:
                r.m_duration = self.m_notelen.clone()
                self.m_notelen.m_dots = numdots
            else:
                self.m_notelen = r.m_duration.clone()
            return self.REST, r

        m = self.re_skip.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            IGN1, skiplen, dots = m.groups()
            numdots = len(dots)
            if skiplen:
                skiplen = int(skiplen)
            else:
                skiplen = 0
                if numdots:
                    raise LexerError('Need a digit before dots. Write "%(goodcode)s", not "%(badcode)s".' % {
                        'badcode': m.group().strip(),
                        'goodcode':'s%i%s' % (self.m_notelen.m_nh, dots)
                        }, lexer)
            if skiplen is 0:
                retval = self.m_notelen
            else:
                self.m_notelen = retval = Duration(skiplen, numdots)
            return self.SKIP, retval
        m = self.re_melodic.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            notename, IGN1, IGN2, notelen, dots = m.groups()
            numdots = len(dots)
            if notelen:
                notelen = int(notelen)
            else:
                notelen = 0
                if dots:
                    raise LexerError('Need a digit before dots. Write "%(goodcode)s", not "%(badcode)s".' % {
                        'badcode': m.group().strip(),
                        'goodcode':'%s%i%s' % (notename, self.m_notelen.m_nh, dots)
                        }, self)
            n = MusicRequest(notename, notelen, numdots)
            if not n.m_duration:
                n.m_duration = self.m_notelen.clone()
            else:
                self.m_notelen = n.m_duration.clone()
            return self.NOTE, n
        m = self.re_staff.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.STAFF, None
        m = self.re_voice.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.VOICE, None
        m = self.re_relative.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.RELATIVE, MusicalPitch.new_from_notename(m.group(1))
        m = self.re_clef_quoted.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.CLEF, m.group(1)
        m = self.re_clef.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.CLEF, m.group(1)
        m = self.re_stem_updown.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            d = [const.UP, const.DOWN, const.BOTH][['Up', 'Down', 'Both'].index(m.group(2))]
            return self.STEMDIR, d
        m = self.re_tuplet_updown.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            d = [const.UP, const.DOWN, const.BOTH][['Up', 'Down', 'Both'].index(m.group(2))]
            return self.TUPLETDIR, d
        m = self.re_transpose.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.TRANSPOSE, MusicalPitch.new_from_notename(m.group(1))
        m = self.re_time.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.TIME, TimeSignature(int(m.group(1)), int(m.group(2)))
        m = self.re_key.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.KEY, (m.group(1), m.group(2))
        m = self.re_times.match(self.m_string, self.m_idx)
        if m:
            self.m_idx += len(m.group())
            return self.TIMES, Rat(int(m.groups()[0]), int(m.groups()[1]))
        if self.m_idx == len(self.m_string):
            return None, None
        self.m_idx += 1
        return self.m_string[self.m_idx-1], None
    def get_error_location(self):
        """
        Return a tuple
        (lineno, pos1, pos2)
        lineno is the 0-index line where the error occoured.
        string[pos1:pos2] will return the exact text that caused the error.
        """
        # Let us first count lines to find which line we are on.
        # line numbers are 0-indexed
        lineno = self.m_string[:self.m_last_idx].count("\n")
        line_start = self.m_last_idx
        while line_start > 0 and self.m_string[line_start] != "\n":
            line_start -= 1
        if self.m_string[line_start] == "\n":
            line_start += 1
        line_end = line_start
        while line_end < len(self.m_string) and self.m_string[line_end] != '\n':
            line_end += 1
        return (lineno, self.m_last_idx - line_start, self.m_idx - line_start)


def validate_only_notenames(s):
    """
    Return (None, None, None) if the string s is only notenames
    (pitch and duration). No ties or other tokens are allowed.
    """
    lex = Lexer(s)
    while 1:
        try:
            toc, toc_data = lex.get()
        except InvalidNotenameException:
            return lex.get_error_location()
        if not toc:
            break
        if toc != lex.NOTE:
            return lex.get_error_location()
    return None, None, None
