# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
#
# Copyright (c) 2014 - 2015 by Wilbert Berendsen
#
# 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 2
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
# See http://www.gnu.org/licenses/ for more information.

"""
The Reader is used to construct a tree structure of (musical and other)
items in a LilyPond document. The item types that are used are in the items
module.

You instantiate a Reader with a ly.document.Source that reads tokens from
any (part of a) ly.document.Document.

Whitespace and comments are left out.

All nodes (instances of Item) have a 'position' attribute that indicates 
where the item starts in the source text. Almost all items have the token 
that starts the expression in the 'token' attribute and possibly other 
tokens in the 'tokens' attribute, as a tuple. 

The 'end_position()' method returns the position where the node (including 
its child nodes) ends.


"""

from __future__ import unicode_literals
from __future__ import division

import itertools
from fractions import Fraction

import ly.duration
import ly.pitch

from ly import lex
from ly.lex import lilypond
from ly.lex import scheme

from .items import *


def skip(source, what=(lex.Space, lex.Comment)):
    """Yield tokens from source, skipping items of classes specified in what.
    
    By default, comments and whitespace are skipped.
    
    """
    for t in source:
        if not isinstance(t, what):
            yield t


class dispatcher(object):
    """Decorator creator to dispatch commands, keywords, etc. to a method."""
    def __init__(self):
        self.d = {}
    
    def read_arg(self, a):
        return a
    
    def __call__(self, *args):
        d = self.d
        def wrapper(func):
            for a in args:
                d[a] = func
            doc = "handle " + ", ".join(map(self.read_arg, args))
            func.__doc__ = doc if not func.__doc__ else func.__doc__ + '\n\n' + doc
            return func
        return wrapper
    
    def method(self, token):
        """The registered method to call for the token. May return None."""
        return self.d.get(token)


class dispatcher_class(dispatcher):
    """Decorator creator to dispatch the class of a token to a method."""
    def read_arg(self, a):
        return a.__name__
    
    def method(self, token):
        """The registered method to call for the token's class. May return None."""
        cls = token.__class__
        d = self.d
        try:
            return d[cls]
        except KeyError:
            for c in cls.__mro__[1:]:
                try:
                    meth = d[cls] = d[c]
                except KeyError:
                    if c is not lex.Token:
                        continue
                    meth = d[cls] = None
                return meth


class Reader(object):
    """Reads tokens from a Source and builds a meaningful tree structure."""
    
    _commands = dispatcher()
    _keywords = dispatcher()
    _tokencls = dispatcher_class()
    _markup = dispatcher_class()
    _scheme = dispatcher_class()
    
    def __init__(self, source):
        """Initialize with a ly.document.Source.
        
        The language is set to "nederlands".
        
        """
        self.source = source
        self.language = "nederlands"
        self.in_chord = False
        self.prev_duration = Fraction(1, 4), 1
    
    def set_language(self, lang):
        r"""Changes the pitch name language to use.
        
        Called internally when \language or \include tokens are encountered
        with a valid language name/file.
        
        Sets the language attribute to the language name.
        
        """
        if lang in ly.pitch.pitchInfo:
            self.language = lang
            return True

    def add_duration(self, item, token=None, source=None):
        """Add a duration attribute to the item.
        
        When there are tokens, a Duration item is also appended to the item.
        
        """
        source = source or self.source
        tokens = []
        if not token or isinstance(token, lilypond.Duration):
            if token:
                tokens.append(token)
            for token in source:
                if isinstance(token, lilypond.Duration):
                    if tokens and isinstance(token, lilypond.Length):
                        self.source.pushback()
                        break
                    tokens.append(token)
                elif not isinstance(token, lex.Space):
                    self.source.pushback()
                    break
        if tokens:
            d = self.factory(Duration, tokens[0])
            d.tokens = tuple(tokens[1:])
            item.append(d)
            item.duration = self.prev_duration = ly.duration.base_scaling(tokens)
        else:
            item.duration = self.prev_duration
    
    def consume(self, last_token=None):
        """Yield the tokens from our source until a parser is exit.
        
        If last_token is given, it is called with the last token that is read.
        
        """
        t = None
        for t in self.source.until_parser_end():
            yield t
        if last_token and t is not None:
            last_token(t)

    def factory(self, cls, token=None, consume=False, position=None):
        """Create Item instance for token.
        
        If consume is True, consume()s the source into item.tokens.
        If you don't specify a token, you must specify the position (>= 0).
        
        """
        item = cls()
        item.document = self.source.document
        if token:
            item.token = token
            item.position = token.pos
        elif position is None:
            raise ValueError("position must be specified if no token")
        else:
            item.position = position
        if consume:
            item.tokens = tuple(self.consume())
            if not token and item.tokens:
                item.position = item.tokens[0].pos
        return item
    
    def add_bracketed(self, item, source):
        """Append the arguments between brackets to the item.
        
        Returns True if that succeeded, else False.
        
        """
        for t in source:
            if isinstance(t, lilypond.OpenBracket):
                tokens = [t]
                item.extend(self.read(self.consume(tokens.append)))
                item.tokens = tuple(tokens)
                return True
            elif not isinstance(t, lex.Space):
                self.source.pushback()
                break
        return False
        
    def read(self, source=None):
        """Yield Item instances reading from source."""
        source = source or self.source
        for t in skip(source):
            item = self.read_item(t, source)
            if item:
                yield item
    
    def read_item(self, t, source=None):
        """Return one Item that starts with token t. May return None."""
        meth = self._tokencls.method(t)
        if meth:
            return meth(self, t, source or self.source)
    
    @_tokencls(lilypond.SchemeStart)
    @_markup(lilypond.SchemeStart)
    def handle_scheme_start(self, t, source=None):
        return self.read_scheme_item(t)
    
    @_tokencls(lex.StringStart)
    @_markup(lex.StringStart)
    @_scheme(lex.StringStart)
    def handle_string_start(self, t, source=None):
        return self.factory(String, t, True)
    
    @_tokencls(
        lilypond.DecimalValue,
        lilypond.IntegerValue,
        lilypond.Fraction,
    )
    def handle_number_class(self, t, source=None):
        return self.factory(Number, t)
    
    @_tokencls(lilypond.MusicItem)
    def handle_music_item(self, t, source):
        return self.read_music_item(t, source)
    
    @_tokencls(lilypond.Length)
    def handle_length(self, t, source):
        item = self.factory(Unpitched, position=t.pos)
        self.add_duration(item, t, source)
        return item
    
    @_tokencls(lilypond.ChordStart)
    def handle_chord_start(self, t, source):
        if not self.in_chord:
            self.in_chord = True
            chord = self.factory(Chord, t)
            def last(t): chord.tokens += (t,)
            chord.extend(self.read(self.consume(last)))
            self.in_chord = False
            self.add_duration(chord, None, source)
            return chord
    
    @_tokencls(
        lilypond.OpenBracket, lilypond.OpenSimultaneous,
        lilypond.SimultaneousOrSequentialCommand,
    )
    def handle_music_list(self, t, source):
        item, it = self.test_music_list(t)
        if item:
            if it:
                item.extend(self.read(it))
            return item
    
    @_tokencls(lilypond.Command)
    def read_command(self, t, source):
        """Read the rest of a command given in t from the source."""
        meth = self._commands.method(t)
        if meth:
            return meth(self, t, source)
        return self.factory(Command, t)
    
    @_tokencls(lilypond.Keyword)
    def read_keyword(self, t, source):
        """Read the rest of a keyword given in t from the source."""
        meth = self._keywords.method(t)
        if meth:
            return meth(self, t, source)
        return self.factory(Keyword, t)
    
    @_tokencls(lilypond.UserCommand)
    def read_user_command(self, t, source):
        """Read a user command, this can be a variable reference."""
        return self.factory(UserCommand, t)
    
    @_tokencls(lilypond.ChordSeparator)
    def read_chord_specifier(self, t, source=None):
        """Read stuff behind notes in chordmode."""
        item = self.factory(ChordSpecifier, position=t.pos)
        item.append(self.factory(ChordItem, t))
        for t in self.consume():
            if isinstance(t, lilypond.ChordItem):
                item.append(self.factory(ChordItem, t))
            elif isinstance(t, lilypond.Note):
                r = ly.pitch.pitchReader(self.language)(t)
                if r:
                    note = self.factory(Note, t)
                    note.pitch = ly.pitch.Pitch(*r)
                    item.append(note)
        return item
    
    @_tokencls(lilypond.TremoloColon)
    def read_tremolo(self, t, source=None):
        """Read a tremolo."""
        item = self.factory(Tremolo, t)
        for t in self.source:
            if isinstance(t, lilypond.TremoloDuration):
                item.append(self.factory(Duration, t))
                item.duration = ly.duration.base_scaling_string(t)
            else:
                self.source.pushback()
            break
        return item
    
    @_tokencls(lilypond.Name, lilypond.ContextProperty)
    def handle_name(self, t, source):
        return self.read_assignment(t)
    
    @_tokencls(
        lilypond.PaperVariable,
        lilypond.LayoutVariable,
        lilypond.HeaderVariable,
        lilypond.UserVariable,
    )
    def handle_variable_assignment(self, t, source):
        item = self.read_assignment(t)
        if item:
            # handle \pt, \in etc.
            for t in skip(self.source):
                if isinstance(t, lilypond.Unit):
                    item.append(self.factory(Command, t))
                else:
                    self.source.pushback()
                break
            return item
    
    _direct_items = {
        lilypond.VoiceSeparator: VoiceSeparator,
        lilypond.PipeSymbol: PipeSymbol,
        lilypond.Dynamic: Dynamic,
        lilypond.Tie: Tie,
    }
    @_tokencls(*_direct_items)
    def handle_direct_items(self, t, source):
        """Tokens that directly translate to an Item."""
        return self.factory(self._direct_items[t.__class__], t)
    
    @_tokencls(lilypond.Direction)
    def handle_direction(self, t, source):
        item = self.factory(Postfix, t)
        item.direction = '_-^'.index(t) - 1
        for t in skip(source):
            if isinstance(t, (
                lex.StringStart,
                lilypond.MarkupStart,
                lilypond.Articulation,
                lilypond.Slur,
                lilypond.Beam,
                lilypond.Dynamic,
                )):
                item.append(self.read_item(t))
            elif isinstance(t, lilypond.Command) and t in ('\\tag'):
                item.append(self.read_item(t))
            elif isinstance(t, lilypond.Keyword) and t in ('\\tweak'):
                item.append(self.read_item(t))
            else:
                self.source.pushback()
            break
        return item
    
    @_tokencls(lilypond.Slur)
    def handle_slurs(self, t, source=None):
        cls = PhrasingSlur if t.startswith('\\') else Slur
        item = self.factory(cls, t)
        item.event = 'start' if t.endswith('(') else 'stop'
        return item
    
    @_tokencls(lilypond.Beam)
    def handle_beam(self, t, source=None):
        item = self.factory(Beam, t)
        item.event = 'start' if t == '[' else 'stop'
        return item
    
    @_tokencls(lilypond.Articulation)
    def handle_articulation(self, t, source=None):
        return self.factory(Articulation, t)
    
    def read_assignment(self, t):
        """Read an assignment from the variable name. May return None."""
        item = self.factory(Assignment, t)
        for t in skip(self.source):
            if isinstance(t, (lilypond.Variable, lilypond.UserVariable, lilypond.DotPath)):
                item.append(self.factory(PathItem, t))
            elif isinstance(t, lilypond.EqualSign):
                item.tokens = (t,)
                for i in self.read():
                    item.append(i)
                    break
                return item
            elif isinstance(t, lilypond.SchemeStart):
                # accept only one scheme item, if another one is found,
                # return the first, and discard the Assignment item
                # (should not normally happen)
                for s in item.find(Scheme):
                    self.source.pushback()
                    return s
                item.append(self.read_scheme_item(t))
            else:
                self.source.pushback()
                return
    
    def test_music_list(self, t):
        r"""Test whether a music list ({ ... }, << ... >>, starts here.
        
        Also handles \simultaneous { ... } and \sequential { ... }
        correctly. These obscure commands are not even highlighted by 
        lex, but they exist in LilyPond... \simultaneous { ... } is
        like << ... >> but \sequential << ... >> just behaves like << ... >>

        Returns a two-tuple(item; iterable), both may be None. If 
        item is not None, it can be either a UserCommand or a MusicList.  If 
        iterable is None, the item is a UserCommand (namely \simultaneous
        or \sequential, but not followed by a { or <<); else the item is a
        MusicList, and the iterable should be read fully to get all the 
        tokens inside the MusicList. If item is None, there is no MusicList 
        and no token is read.
        
        This way you can handle the { ... } and << ... >> transparently in every
        input mode.
        
        """
        def make_music_list(t, simultaneous, tokens=()):
            """Make the MusicList item."""
            item = self.factory(MusicList, t)
            item.simultaneous = simultaneous
            item.tokens = tokens
            def last(t): item.tokens += (t,)
            return item, self.consume(last)
            
        if isinstance(t, (lilypond.OpenBracket, lilypond.OpenSimultaneous)):
            return make_music_list(t, t == '<<')
        elif isinstance(t, lilypond.SimultaneousOrSequentialCommand):
            for t1 in skip(self.source):
                if isinstance(t1, (lilypond.OpenBracket, lilypond.OpenSimultaneous)):
                    return make_music_list(t, t == '\\simultaneous' or t1 == '<<', (t1,))
                else:
                    self.source.pushback()
                    return self.factory(Keyword, t), None
        return None, None
                    
    def read_music_item(self, t, source):
        r"""Read one music item (note, rest, s, \skip, or q) from t and source."""
        item = None
        in_pitch_command = isinstance(self.source.state.parser(), lilypond.ParsePitchCommand)
        if t.__class__ == lilypond.Note:
            r = ly.pitch.pitchReader(self.language)(t)
            if r:
                item = self.factory(Note, t)
                p = item.pitch = ly.pitch.Pitch(*r)
                for t in source:
                    if isinstance(t, lilypond.Octave):
                        p.octave = ly.pitch.octaveToNum(t)
                        item.octave_token = t
                    elif isinstance(t, lilypond.Accidental):
                        item.accidental_token = p.accidental = t
                    elif isinstance(t, lilypond.OctaveCheck):
                        p.octavecheck = ly.pitch.octaveToNum(t)
                        item.octavecheck_token = t
                        break
                    elif not isinstance(t, lex.Space):
                        self.source.pushback()
                        break
        else:
            cls = {
                lilypond.Rest: Rest,
                lilypond.Skip: Skip,
                lilypond.Spacer: Skip,
                lilypond.Q: Q,
                lilypond.DrumNote: DrumNote,
            }[t.__class__]
            item = self.factory(cls, t)
        if item:
            if not self.in_chord and not in_pitch_command:
                self.add_duration(item, None, source)
        return item
    
    @_commands('\\relative')
    def handle_relative(self, t, source):
        item = self.factory(Relative, t)
        # get one pitch and exit on a non-comment
        pitch_found = False
        for i in self.read(source):
            item.append(i)
            if not pitch_found and isinstance(i, Note):
                pitch_found = True
                continue
            break
        return item
    
    @_commands('\\absolute')
    def handle_absolute(self, t, source):
        item = self.factory(Absolute, t)
        for i in self.read(source):
            item.append(i)
            break
        return item
    
    @_commands('\\transpose')
    def handle_transpose(self, t, source):
        item = self.factory(Transpose, t)
        # get two pitches
        pitches_found = 0
        for i in self.read(source):
            item.append(i)
            if pitches_found < 2 and isinstance(i, Note):
                pitches_found += 1
                continue
            break
        return item
    
    @_commands('\\clef')
    def handle_clef(self, t, source):
        item = self.factory(Clef, t)
        for t in skip(source):
            if isinstance(t, lilypond.ClefSpecifier):
                item._specifier = t
            elif isinstance(t, lex.StringStart):
                item._specifier = self.factory(String, t, True)
            break
        return item
    
    @_commands('\\key')
    def handle_key(self, t, source):
        item = self.factory(KeySignature, t)
        item.extend(itertools.islice(self.read(source), 2))
        return item
    
    @_commands('\\times', '\\tuplet', '\\scaleDurations')
    def handle_scaler(self, t, source):
        item = self.factory(Scaler, t)
        item.scaling = 1
        if t == '\\scaleDurations':
            for i in self.read(source):
                item.append(i)
                if isinstance(i, Number):
                    item.scaling = i.value()
                elif isinstance(i, Scheme):
                    try:
                        pair = i.get_pair_ints()
                        if pair:
                            item.scaling = Fraction(*pair)
                        else:
                            val = i.get_fraction()
                            if val is not None:
                                item.scaling = val
                    except ZeroDivisionError:
                        pass
                break
        elif t == '\\tuplet':
            for t in source:
                if isinstance(t, lilypond.Fraction):
                    item.append(self.factory(Number, t))
                    item.numerator, item.denominator = map(int, t.split('/'))
                    item.scaling = 1 / Fraction(t)
                elif isinstance(t, lilypond.Duration):
                    self.add_duration(item, t, source)
                    break
                elif not isinstance(t, lex.Space):
                    self.source.pushback()
                    break
        else: # t == '\\times'
            for t in source:
                if isinstance(t, lilypond.Fraction):
                    item.append(self.factory(Number, t))
                    item.numerator, item.denominator = map(int, t.split('/'))
                    item.scaling = Fraction(t)
                    break
                elif not isinstance(t, lex.Space):
                    self.source.pushback()
                    break
        for i in self.read(source):
            item.append(i)
            break
        return item
    
    @_commands('\\tag', '\\keepWithTag', '\\removeWithTag', '\\appendToTag', '\\pushToTag')
    def handle_tag(self, t, source):
        item = self.factory(Tag, t)
        argcount = 3 if t in ('\\appendToTag', '\\pushToTag') else 2
        item.extend(itertools.islice(self.read(), argcount))
        return item
    
    @_commands('\\grace', '\\acciaccatura', '\\appoggiatura', '\\slashedGrace')
    def handle_grace(self, t, source):
        item = self.factory(Grace, t)
        for i in self.read(source):
            item.append(i)
            break
        return item
    
    @_commands('\\afterGrace')
    def handle_after_grace(self, t, source):
        item = self.factory(AfterGrace, t)
        for i in itertools.islice(self.read(source), 2):
            item.append(i)
        # put the grace music in a Grace item
        if len(item) > 1:
            i = self.factory(Grace, position=item[-1].position)
            i.append(item[-1])
            item.append(i)
        return item
    
    @_commands('\\repeat')
    def handle_repeat(self, t, source):
        item = self.factory(Repeat, t)
        item._specifier = None
        item._repeat_count = None
        for t in skip(source):
            if isinstance(t, lilypond.RepeatSpecifier):
                item._specifier = t
            elif not item.specifier and isinstance(t, lex.StringStart):
                item._specifier = self.factory(String, t, True)
            elif isinstance(t, lilypond.RepeatCount):
                item._repeat_count = t
            elif isinstance(t, lilypond.SchemeStart):
                # the specifier or count may be specified using scheme
                s = self.read_scheme_item(t)
                if item._specifier:
                    if item._repeat_count:
                        item.append(s)
                        break
                    item._repeat_count = s
                else:
                    item._specifier = s
            else:
                self.source.pushback()
                for i in self.read(source):
                    item.append(i)
                    break
                for t in skip(source):
                    if t == '\\alternative' and isinstance(t, lilypond.Command):
                        item.append(self.handle_alternative(t, source))
                    else:
                        self.source.pushback()
                    break
                break
        return item
    
    @_commands('\\alternative')
    def handle_alternative(self, t, source):
        item = self.factory(Alternative, t)
        for i in self.read(source):
            item.append(i)
            break
        return item
    
    @_commands('\\tempo')
    def handle_tempo(self, t, source):
        item = self.factory(Tempo, t)
        source = self.consume()
        equal_sign_seen = False
        text_seen = False
        t = None
        for t in source:
            if not equal_sign_seen:
                if (not text_seen and isinstance(t,
                        (lilypond.SchemeStart, lex.StringStart, lilypond.Markup))):
                    item.append(self.read_item(t))
                    t = None
                    text_seen = True
                elif isinstance(t, lilypond.Length):
                    self.add_duration(item, t, source)
                    t = None
                elif isinstance(t, lilypond.EqualSign):
                    item.tokens = (t,)
                    equal_sign_seen = True
                    t = None
            elif isinstance(t, (lilypond.IntegerValue, lilypond.SchemeStart)):
                item.append(self.read_item(t))
            elif t == "-":
                item.tokens += (t,)
        ## if the last token does not belong to the \\tempo expression anymore,
        ## push it back
        if t and not isinstance(t, (lex.Space, lex.Comment)):
            self.source.pushback()
        return item
    
    @_commands('\\time')
    def handle_time(self, t, source):
        item = self.factory(TimeSignature, t)
        for t in skip(source):
            if isinstance(t, lilypond.SchemeStart):
                item._beatstructure = self.read_scheme_item(t)
                continue
            elif isinstance(t, lilypond.Fraction):
                item._num, den = map(int, t.split('/'))
                item._fraction = Fraction(1, den)
            else:
                self.source.pushback()
            break
        return item
    
    @_commands('\\partial')
    def handle_partial(self, t, source):
        item = self.factory(Partial, t)
        self.add_duration(item, None, source)
        return item
    
    @_commands('\\new', '\\context', '\\change')
    def handle_translator(self, t, source):
        cls = Change if t == '\\change' else Context 
        item = self.factory(cls, t)
        isource = self.consume()
        for t in skip(isource):
            if isinstance(t, (lilypond.ContextName, lilypond.Name)):
                item._context = t
                for t in isource:
                    if isinstance(t, lilypond.EqualSign):
                        for t in isource:
                            if isinstance(t, lex.StringStart):
                                item._context_id = self.factory(String, t, True)
                                break
                            elif isinstance(t, lilypond.Name):
                                item._context_id = t
                                break
                            elif not isinstance(t, lex.Space):
                                self.source.pushback()
                                break
                    elif not isinstance(t, lex.Space):
                        self.source.pushback()
                        break
            else:
                self.source.pushback()
            break
        if cls is not Change:
            for i in self.read(source):
                item.append(i)
                if not isinstance(i, With):
                    break
        return item
    
    _inputmode_commands = {
        '\\notemode': NoteMode,
        '\\notes': NoteMode,
        '\\chordmode': ChordMode,
        '\\chords': ChordMode,
        '\\figuremode': FigureMode,
        '\\figures': FigureMode,
        '\\drummode': DrumMode,
        '\\drums': DrumMode,
    }
    @_commands(*_inputmode_commands)
    def handle_inputmode(self, t, source):
        cls = self._inputmode_commands[t]
        item = self.factory(cls, t)
        for i in self.read():
            item.append(i)
            break
        return item
    
    _lyricmode_commands = {
        '\\lyricmode': LyricMode,
        '\\lyrics': LyricMode,
        '\\oldaddlyrics': LyricMode,
        '\\addlyrics': LyricMode,
        '\\lyricsto': LyricsTo,
    }
    @_commands(*_lyricmode_commands)
    def handle_lyricmode(self, t, source):
        cls = self._lyricmode_commands[t]
        item = self.factory(cls, t)
        if cls is LyricsTo:
            for t in skip(source):
                if isinstance(t, lilypond.Name):
                    item._context_id = t
                elif isinstance(t, (lex.String, lilypond.SchemeStart)):
                    item._context_id = self.read_item(t)
                else:
                    self.source.pushback()
                break
        for t in skip(self.consume()):
            i = self.read_lyric_item(t) or self.read_item(t)
            if i:
                item.append(i)
            break
        return item
    
    def read_lyric_item(self, t):
        """Read one lyric item. Returns None for tokens it does not handle."""
        if isinstance(t, (lex.StringStart, lilypond.MarkupStart)):
            item = self.factory(LyricText, position=t.pos)
            item.append(self.read_item(t))
            self.add_duration(item)
            return item
        elif isinstance(t, lilypond.LyricText):
            item = self.factory(LyricText, t)
            self.add_duration(item)
            return item
        elif isinstance(t, lilypond.Lyric):
            return self.factory(LyricItem, t)
        else:
            item, source = self.test_music_list(t)
            if item:
                if source:
                    for t in skip(source):
                        i = self.read_lyric_item(t) or self.read_item(t)
                        if i:
                            item.append(i)
                return item
    
    @_commands('\\stringTuning')
    def handle_string_tuning(self, t, source):
        item = self.factory(StringTuning, t)
        for arg in self.read(source):
            item.append(arg)
            break
        return item
    
    @_commands('\\partcombine')
    def handle_partcombine(self, t, source=None):
        item = self.factory(PartCombine, t)
        item.extend(itertools.islice(self.read(), 2))
        return item
    
    @_keywords('\\language')
    def handle_language(self, t, source):
        item = self.factory(Language, t)
        for name in self.read(source):
            item.append(name)
            if isinstance(name, String):
                value = item.language = name.value()
                if value in ly.pitch.pitchInfo:
                    self.language = value
            break
        return item
    
    @_keywords('\\include')
    def handle_include(self, t, source):
        item = None
        name = None
        for name in self.read(source):
            if isinstance(name, String):
                value = name.value()
                if value.endswith('.ly') and value[:-3] in ly.pitch.pitchInfo:
                    item = self.factory(Language, t)
                    item.language = self.language = value[:-3]
                    item.append(name)
            break
        if not item:
            item = self.factory(Include, t)
            if name:
                item.append(name)
        return item
    
    @_keywords('\\version')
    def handle_version(self, t, source):
        item = self.factory(Version, t)
        for arg in self.read(source):
            item.append(arg)
            break
        return item
    
    _bracketed_keywords = {
        '\\header': Header,
        '\\score': Score,
        '\\bookpart': BookPart,
        '\\book': Book,
        '\\paper': Paper,
        '\\layout': Layout,
        '\\midi': Midi,
        '\\with': With,
        '\\context': LayoutContext,
    }
    @_keywords(*_bracketed_keywords)
    def handle_bracketed(self, t, source):
        cls = self._bracketed_keywords[t]
        item = self.factory(cls, t)
        if not self.add_bracketed(item, source) and t == '\\with':
            # \with also supports one other argument instead of { ... }
            for i in self.read(source):
                item.append(i)
                break
        return item
    
    @_keywords('\\set')
    def handle_set(self, t, source):
        item = self.factory(Set, t)
        tokens = []
        for t in skip(source):
            tokens.append(t)
            if isinstance(t, lilypond.EqualSign):
                item.tokens = tuple(tokens)
                for i in self.read(source):
                    item.append(i)
                    break
                break
        return item
    
    @_keywords('\\unset')
    def handle_unset(self, t, source):
        item = self.factory(Unset, t)
        tokens = []
        for t in skip(self.consume()):
            if type(t) not in lilypond.ParseUnset.items:
                self.source.pushback()
                break
            tokens.append(t)
        item.tokens = tuple(tokens)
        return item
    
    @_keywords('\\override')
    def handle_override(self, t, source):
        item = self.factory(Override, t)
        for t in skip(self.consume()):
            if isinstance(t, (lex.StringStart, lilypond.SchemeStart)):
                item.append(self.read_item(t))
            elif isinstance(t, lilypond.EqualSign):
                item.tokens = (t,)
                for i in self.read():
                    item.append(i)
                    break
                break
            else:
                item.append(self.factory(PathItem, t))
        return item
    
    @_keywords('\\revert')
    def handle_revert(self, t, source):
        item = self.factory(Revert, t)
        t = None
        for t in skip(self.consume()):
            if type(t) in lilypond.ParseRevert.items:
                item.append(self.factory(PathItem, t))
            else:
                break
        if isinstance(t, lilypond.SchemeStart) and not any(
                isinstance(i.token, lilypond.GrobProperty) for i in item):
            item.append(self.read_scheme_item(t))
        else:
            self.source.pushback()
        return item
    
    @_keywords('\\tweak')
    def handle_tweak(self, t, source):
        item = self.factory(Tweak, t)
        t = None
        for t in skip(self.consume()):
            if type(t) in lilypond.ParseTweak.items:
                item.append(self.factory(PathItem, t))
            else:
                self.source.pushback()
                break
        if len(item) == 0 and isinstance(t, lilypond.SchemeStart):
            item.append(self.read_scheme_item(t))
        for i in self.read():
            item.append(i)
            break
        return item
    
    @_commands('\\markup', '\\markuplist', '\\markuplines')
    def handle_markup(self, t, source=None):
        item = self.factory(Markup, t)
        self.add_markup_arguments(item)
        return item
        
    def read_markup(self, t):
        """Read LilyPond markup (recursively)."""
        meth = self._markup.method(t)
        if meth:
            return meth(self, t)
    
    @_markup(lilypond.MarkupScore)
    def handle_markup_score(self, t):
        item = self.factory(MarkupScore, t)
        for t in self.consume():
            if isinstance(t, lilypond.OpenBracket):
                item.tokens = (t,)
                def last(t): item.tokens += (t,)
                item.extend(self.read(self.consume(last)))
                return item
            elif not isinstance(t, lex.Space):
                self.source.pushback()
                break
        return item
    
    @_markup(lilypond.MarkupCommand)
    def handle_markup_command(self, t):
        item = self.factory(MarkupCommand, t)
        self.add_markup_arguments(item)
        return item
    
    @_markup(lilypond.MarkupUserCommand)
    def handle_markup_user_command(self, t):
        item = self.factory(MarkupUserCommand, t)
        return item
    
    @_markup(lilypond.OpenBracketMarkup)
    def handle_markup_open_bracket(self, t):
        item = self.factory(MarkupList, t)
        self.add_markup_arguments(item)
        return item
    
    @_markup(lilypond.MarkupWord)
    def handle_markup_word(self, t):
        return self.factory(MarkupWord, t)
    
    def add_markup_arguments(self, item):
        """Add markup arguments to the item."""
        for t in self.consume():
            i = self.read_markup(t)
            if i:
                item.append(i)
            elif isinstance(item, MarkupList) and isinstance(t, lilypond.CloseBracketMarkup):
                item.tokens = (t,)
        return item
    
    def read_scheme_item(self, t):
        """Reads a Scheme expression (just after the # in LilyPond mode)."""
        item = self.factory(Scheme, t)
        for t in self.consume():
            if not isinstance(t, lex.Space):
                i = self.read_scheme(t)
                if i:
                    item.append(i)
                    break
        return item

    def read_scheme(self, t):
        """Return a Scheme item from the token t."""
        meth = self._scheme.method(t)
        if meth:
            return meth(self, t)
        
    @_scheme(scheme.Quote)
    def handle_scheme_quote(self, t):
        item = self.factory(SchemeQuote, t)
        for t in self.consume():
            if not isinstance(t, lex.Space):
                i = self.read_scheme(t)
                if i:
                    item.append(i)
                    break
        return item
    
    @_scheme(scheme.OpenParen)
    def handle_scheme_open_parenthesis(self, t):
        item = self.factory(SchemeList, t)
        def last(t): item.tokens = (t,)
        for t in self.consume(last):
            if not isinstance(t, lex.Space):
                i = self.read_scheme(t)
                if i:
                    item.append(i)
        return item
    
    @_scheme(
        scheme.Dot,
        scheme.Bool,
        scheme.Char,
        scheme.Word,
        scheme.Number,
        scheme.Fraction,
        scheme.Float,
    )
    def handle_scheme_token(self, t):
        return self.factory(SchemeItem, t)
    
    @_scheme(scheme.LilyPondStart)
    def handle_scheme_lilypond_start(self, t):
        item = self.factory(SchemeLily, t)
        def last(t): item.tokens = (t,)
        item.extend(self.read(self.consume(last)))
        return item


