#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

Laborejo2 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/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")

#Standard Library Modules
from weakref import WeakKeyDictionary, WeakValueDictionary
from collections import OrderedDict

#Third Party Modules

#Template Modules
import template.engine.sequencer

#Our modules
from .block import Block
from .ccsubtrack import GraphTrackCC
from .items import * #parseRight and trackState need all items.

class DynamicSettingsSignature(object):
    """Holds the meaning of dynamic keywords for one track.
    Only used once per Track. Cannot be inserted.

    Each track needs a different one because "Forte" for a brass instrument
    has a different meaning than for a string instruments. Tracks
    are instruments.

    Do not confuse with trackState.dynamicSignatures, which is
    a stack of actual dyn-signatures like "forte".
    """
    def __init__(self):
        #self.dynamics = filled in by self.reset
        self.reset()
        self._secondInit(parentTrack = None)

    def reset(self):
        """Reset to the programs default values.
        This is a function and not in init because the api wants to reset as well."""
        self.dynamics = { #This is a dict instead of direct values so that dynamicSignature can use a string keyword to get their value and don't have to use getattr().
            "ppppp":15,
            "pppp":31,
            "ppp":43,
            "pp":53,
            "p":69,

            "mp":77,
            "mf":90,

            "f":98,
            "ff":108,
            "fff":116,
            "ffff":127,

            "custom":64,

            "tacet":0,

            "fp":98,
            "sf":98,
            "sff":108,
            "sp":69,
            "spp":53,
            "sfz":116,
            #TODO: accent > ?
            }

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        self.sanityCheck()

    def _secondInit(self, parentTrack):
        """see Item._secondInit"""
        #ignore parentTrack
        pass

    @classmethod
    def instanceFromSerializedData(cls, serializedData, parentObject):
        """see Score.instanceFromSerializedData"""
        assert cls.__name__ == serializedData["class"]
        self = cls.__new__(cls)
        self.dynamics = {key:int(value) for key, value in serializedData["dynamics"].items()}
        self._secondInit(parentTrack = parentObject)
        return self

    def serialize(self):
        result = {}
        result["class"] = self.__class__.__name__
        result["dynamics"] = self.dynamics
        return result

    def copy(self):
        """
        Since there is only one DynSig, directly in the track,
        this copy is only called when doing a track-copy which copies
        the track itself.
        """
        new = DynamicSettingsSignature()
        new.dynamics = self.dynamics.copy()
        return new

    def sanityCheck(self):
        for key, value in self.dynamics.items():
            if not 0 <= value <= 127:
                warn("Dynamic {} of value {} not allowed. Choose a dynamic between 0 and 127. Now set to 0, please edit manually.".format(key, value))
                self.dynamics[key] = 0

class DurationSettingsSignature(object):
    """Only used once per Track. Cannot be inserted, is already in the track.
    Can and should be modified though."""

    class DurationMod(object):
        """A descriptor that converts a string to a function
        and checks if a duration mod string is valid.

        We need this as descriptor because we want the user to change a dur-sig by writing a new
        calculation in string form as e.g. defaultOff. Each time a new string is set we create
        a new DurationMod which holds the evaluated calculation as a function, which was tested if
        valid."""

        class EvaluatedFunction(object):
            """This is a callable class as substitute for a function. We need this as a class
            to get the __str__ parameter since we use strings as a method of user input.
            These strings get saved and loaded.
            """
            def __init__(self, string):
                try:
                    testFunction = lambda x: eval(string)
                    testFunction(D4)
                    self.evaluated = lambda x: eval(string)
                    self.string = string
                    self.evaluated.__str__ = self.__str__
                except Exception as err:
                    warn("Syntax Warning: duration processing not possible. Only 'x' and numbers are allowed for calculations. Please check your input: {} ".format(string))
                    self.string = string
                    self.evaluated = lambda x: 0

            def __call__(self, parameter):
                return self.evaluated(parameter)

            def __str__(self):
                return self.string

        def __init__(self):
            self.data = WeakKeyDictionary()

        def __get__(self, instance, owner):
            # we get here when someone calls instance.attribute, and attribute is a DurationMod instance
            #return self.data.get(instance)
            return self.data[instance]

        def __set__(self, instance, string):
            # we get here when someone calls instance.attribute = val, and attribute is a DurationMod instance
            self.data[instance] = self.EvaluatedFunction(string)

    #Descriptors belong in the class scope
    defaultOn = DurationMod()
    defaultOff = DurationMod()
    staccatoOn = DurationMod()
    staccatoOff = DurationMod()
    tenutoOn = DurationMod()
    tenutoOff = DurationMod()
    legatoOn = DurationMod()
    legatoOff = DurationMod()

    def __init__(self):
        """-On- means the modification on the left side of a note duration,
        -off- means the right side.
        Note on and off.

        All values will be added to the start/end of the notes duration. Since the start as always
        "0" in this reference frame you essentially set the note on here.

        Duration keywords like D4 are supported.
        The special variable "x" will be provided in the scope to be used in the string.

        Examples:
        "0" means no change.

        defaultOn= "D8" will result in a duration starting an eighth later than expected

        defaultOff = "-0.10 * x"  will evaluate to negative 10% of the original duration (x) which
        will then be added to the original duration, resulting in the note off to be 10% less than
        normally. However, that scales quite unnaturally to large duration and we better use
        a fixed offset.

        If you want to set your duration, e.g. note off, to an absolute value start by adding the
        negative original duration and build up from there. "-1*x + D8" always results in an eigth
        note duration.
        """

        self.reset() #fills in all our values
        self._secondInit(parentTrack = None)


    def reset(self):
        """This is not in init because the api wants to reset as well"""
        self.defaultOn = "0"
        self.defaultOff = "-1 * D64 if x <= D16 else -1 * D32"
        self.staccatoOn = "0"
        self.staccatoOff = "-1 * x + D64"
        self.tenutoOn = "0"
        self.tenutoOff = "0"
        self.legatoOn = "0"  #for slurs
        self.legatoOff = "D128" #overlap

    def _secondInit(self, parentTrack):
        """see Item._secondInit"""
        #ignore parentTrack
        pass

    @classmethod
    def instanceFromSerializedData(cls, serializedData, parentObject):
        """see Score.instanceFromSerializedData"""
        assert cls.__name__ == serializedData["class"]
        self = cls.__new__(cls)
        self.defaultOn = str(serializedData["defaultOn"])
        self.defaultOff = str(serializedData["defaultOff"])
        self.staccatoOn = str(serializedData["staccatoOn"])
        self.staccatoOff = str(serializedData["staccatoOff"])
        self.tenutoOn = str(serializedData["tenutoOn"])
        self.tenutoOff = str(serializedData["tenutoOff"])
        self.legatoOn = str(serializedData["legatoOn"])
        self.legatoOff = str(serializedData["legatoOff"])
        self._secondInit(parentTrack = parentObject)
        return self

    def serialize(self):
        result = {}
        result["class"] = self.__class__.__name__
        result["defaultOn"] = str(self.defaultOn)
        result["defaultOff"] = str(self.defaultOff)
        result["staccatoOn"] = str(self.staccatoOn)
        result["staccatoOff"] = str(self.staccatoOff)
        result["tenutoOn"] = str(self.tenutoOn)
        result["tenutoOff"] = str(self.tenutoOff)
        result["legatoOn"] = str(self.legatoOn)
        result["legatoOff"] = str(self.legatoOff)
        return result

    def copy(self):
        #Strings are not mutable so they are just copied by new assignment
        #Also the strings will trigger the creation of new functions immediately
        new = DurationSettingsSignature()
        new.defaultOn = str(self.defaultOn)
        new.defaultOff = str(self.defaultOff)
        new.staccatoOn = str(self.staccatoOn)
        new.staccatoOff = str(self.staccatoOff)
        new.tenutoOn = str(self.tenutoOn)
        new.tenutoOff = str(self.tenutoOff)
        new.legatoOn = str(self.legatoOn)
        new.legatoOff = str(self.legatoOff)
        return new

class TrackState(object):
    """TrackState stays not the same for one track but gets recreated, at least in track.head().
    This means init is always position 0."""

    #in init these will get recreated all the time. This confuses cashing and hashing
    defaultClef = Clef("treble")
    defaultDynamicSignature = DynamicSignature("custom")
    defaultMetricalInstruction = MetricalInstruction(tuple(), isMetrical = False)
    defaultKeySignature = KeySignature(20, [0,0,0,0,0,0,0])

    def __init__(self, track):
        self.track = track
        self.tickindex = 0 #The tickindex has no metrical connection. It doesn't care about upbeats, barlines or timesigs.
        self.ticksSinceLastMeasureStartLive = -1*track.upbeatInTicks
        self.blockindex = 0
        self.ticksSinceLastMeasureStart = 0 #Only for export.
        self.barlines = [] #a list of ints/tickpositions. These are the non-editable, non-movable single barlines.
        #stacks. There is always one item left in the stack, the default:
        self.keySignatures = [self.defaultKeySignature] #C Major
        self.clefs = [self.defaultClef]
        self.metricalInstructions = [self.defaultMetricalInstruction] #no barlines, no metrical information.
        self.dynamicSignatures = [self.defaultDynamicSignature]
        self.dynamicRamp = None #Not for cursor movement so it is not a stack.
        self.EXPORTtiedNoteExportObjectsWaitingForClosing = {} #pitch:noteExportObject . Empty during cursor movement, filled during export.
        self.duringLegatoSlur = False #there are no nested legato slurs
        self.duringBeamGroup = False #no nested beam groups.
        self.midiChannels = [track.initialMidiChannel] #these are just integers, not items. items.ChannelChange parsing changes this and items automatically get the new value.  A stack of midi channels allows the cursor to know the current channel for immediate playback/feedback.
        self.instrumentChanges = [InstrumentChange(track.initialMidiProgram, track.initialMidiBankMsb, track.initialMidiBankLsb, track.initialShortInstrumentName, )]
        self.midiTranspose = track.midiTranspose

        # TODO: When a change item for the following functions is introduced they need to get parsed in track.parseLeft and track.parseRight

    def position(self):
        """The position in the track"""
        #[0, 1, 2, 3][:3] means slice item 0, 1, 2  but not include 3.
        result = 0
        for block in self.track.blocks[:self.blockindex]: #all blocks without the current one.
            #TODO: the following assert fails. somewhere in the code the local cursor index gets not updated properly. most likely in insert, delete or track.tail, track.head
            #This is the reason why inserting in contend linket blocks results in the wrong cursor position if we rely on keeping track of the track position manually or using the local block positions. We can only rely on the current one.
            #assert len(block.data) == block.localCursorIndex #does not hold for the current block because it was not completely traversed yet.
            result += len(block.data) + 1 #+1 for the appending position
        result += self.track.currentBlock().localCursorIndex #finally add the position in the current block which is somewhere in the middle or so. at least not the same as len(currentBlock)
        return result

    def index(self):
        if self.track in self.track.parentData.tracks:
            return self.track.parentData.tracks.index(self.track)
        else:
            return None #hidden

    def clef(self):
        return self.clefs[-1]

    def keySignature(self):
        return self.keySignatures[-1]

    def metricalInstruction(self):
        return self.metricalInstructions[-1]

    def dynamicSignature(self):
        return self.dynamicSignatures[-1]

    def midiChannel(self):
        return self.midiChannels[-1]

    def instrumentChange(self):
        return self.instrumentChanges[-1]

    def isAppending(self):
        """Return if the cursor is on the end of a block (which is
        also the end of the track)"""
        return self.track.currentBlock().isAppending()


    #You cannot cache the trackState, especially not keysignatures. Leave this in as a reminder.
    #An item can be in two blocks which places it in two different contexts/states. If you want to know the context of an item you really have to put the cursor on it.
    def createCached(self):
        """Create a static version of the current state which can be
        put into items. Apply to selection does not have to do a local
        cursor walk then to get keysigs.
        This does not change anything for the items. They still get a
        keysig as parameter, if they need it. The alternative would be
        to let the item get the cached version themselves. That is not
        versatile enough for complex musical operations.
        The cache is created during track.staticRepresentation so we
        can be sure that it is always up-to-date.

        The format and the names are different as well. You cannot
        use a cached trackState as a drop-in replacement for the real state.
        """
        #TODO: for now we only cache values that we know we need. This should keep this function under control and prevent misuse from lazyness, like explained in the docstring.
        #return {
        #    "keySignature" : self.keySignatures[-1],
        #}
        pass

class Track(object):
    allTracks = WeakValueDictionary() #key is the trackId, value is the weak reference to the Track. Deleted tracks (from the current session) and hidden tracks are in here as well.

    #def __repr__(self) -> str:
    #    return f"Laborejo Track: {self.sequencerInterface.name}"

    def __init__(self, parentData, name=None):
        self.parentData = parentData
        self.sequencerInterface = template.engine.sequencer.SequencerInterface(parentTrack=self, name=name)

        self.ccGraphTracks = {} #cc number, graphTrackCC. Every CC is its own SequencerTrack that routes to this tracks jack midi out
        self.ccChannels = tuple() #unsorted numbers from 0-15 which represent the midi channels all CCs are sent to. Only replaced by a new tuple by the user directly. From json this becomes a list, so don't test for type. If empty then CC uses the initial midi channel.

        self.blocks = [Block(track = self)]
        self.durationSettingsSignature = DurationSettingsSignature() #only one per track
        self.dynamicSettingsSignature = DynamicSettingsSignature() #only one per track
        self.upbeatInTicks = 0 #playback does not care about upbeats. But exporting does. for example barlines.
        self.double = False #if true add 5 stafflines extra below. Clef will always stay in the upper staff.  This is a representation-hint. E.g. a gui can offer a bigger staff system for the user.

        #A set of starting values. They are user editable and represent the tracks playback state on head(). Therefore they get saved.
        self.initialMidiChannel = 0 # 0-15
        self.initialMidiProgram = -1 # 0-127. -1 is "don't send"
        self.initialMidiBankMsb = 0 # 0-127. Depends on program >= 0
        self.initialMidiBankLsb = 0 # 0-127. Depends on program >= 0
        self.midiTranspose = 0 # -127 to +127  but the result is never outside of 0-127.1    Cannot change during the track.

        #The instrument names are also handled by the trackState so that the cursor knows about instrument changes
        self.initialInstrumentName = "" #different than the track name. e.g. "Violin"
        self.initialShortInstrumentName = "" # e.g. "vl"

        self.asMetronomeData = None #This track as metronome version. Is always up to date through export.

        self._processAfterInit()

    def _processAfterInit(self):
        """Call this after either init or instanceFromSerializedData"""
        self.state = TrackState(self)
        Track.allTracks[id(self)] = self #remember track as weakref
        #weakref_finalize(self, print, "deleted track "+str(id(self)))


    @property
    def hidden(self):
        """Is this editable or read-only (and not shown in a possible GUI).
        hidden still emits playback and exports to other formats"""
        return not self in self.parentData.tracks

    @property
    def name(self):
        return self.sequencerInterface.name

    @name.setter
    def name(self, newValue):
        self.sequencerInterface.name = newValue

    def duration(self):
        """Return the duration of the whole track, in ticks"""
        result = 0
        for block in self.blocks:
            result += block.duration()
        return result

    def listOfCCsInThisTrack(self):
        return list(self.ccGraphTracks.keys())

    def appendBlock(self):
        new = Block(self)
        self.blocks.append(new)
        return new

    def appendExistingBlock(self, block):
        assert type(block) is Block, block
        block.parentTrack = self
        self.blocks.append(block)

    def deleteBlock(self, block):
        """Undo of deleteBlock is rearrange blocks."""
        if len(self.blocks) > 1:
            originalPosition = self.state.position()
            #block.parentTrack = None  We keep the parent track for undo. Through the linear nature of undo it is guaranteed that the track instance will still exists when this delete attempts undo.
            self.blocks.remove(block) #eventhough this must be undone we cannot keep the deleted block in self.blocks. Self.blocks is designed for only active blocks.
            block.parentTrack = None
            self.toPosition(originalPosition, strict = False) #has head() in it. strict=False just leaves the cursor at head if we can't return to the position because it got deleted.
            return self, block #self is parent Track
        else:
            return False

    def currentBlock(self):
        block = self.blocks[self.state.blockindex]
        return block

    def duplicateBlock(self, block):
        originalPosition = self.state.position()
        index = self.blocks.index(block)
        copy = block.copy(newParentTrack = self)
        self.blocks.insert(index +1, copy)
        self.toPosition(originalPosition) #has head() in it

    def duplicateContentLinkBlock(self, block):
        originalPosition = self.state.position()
        index = self.blocks.index(block)
        linked = block.contentLink()
        self.blocks.insert(index +1, linked)
        self.toPosition(originalPosition) #has head() in it

    def hasContentLinks(self):
        return any(len(block.linkedContentBlocks) > 1 for block in self.blocks)

    def asListOfBlockIds(self):
        """Return an ordered list of block ids"""
        result = []
        for block in self.blocks:
            result.append(id(block))
        return result

    def rearrangeBlocks(self, listOfBlockIds):
        """Reorder the blocks in this track.
        Achtung! Not including a block will delete this block.

        Undo is done by the api."""

        originalPosition = self.state.position()
        #blocksDict = self.blocksAsDict()
        newBlockArrangement = []

        #first remove all parent tracks from the current blocks. They will be added later again for the tracks that really stay in the track.

        for block in self.blocks:
            block.parentTrack = None

        for idLong in listOfBlockIds:
            actualBlock = Block.allBlocks[idLong]
            actualBlock.parentTrack = self
            newBlockArrangement.append(actualBlock)
            #This also checks if there are IDs which match no blocks. allBlocks includes deleted ones for undo as well.

        #a block is a unique unit in Laborejo2. Let's make sure there are no duplicates.
        #http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
        seen = set()
        seen_add = seen.add
        self.blocks = [ x for x in newBlockArrangement if x not in seen and not seen_add(x)]

        assert self.blocks
        #The old position may not exist anymore (block moved to other track) but we can at least try (strict = False) to return to it.
        self.toPosition(originalPosition, strict = False) #has head() in it.

    def blocksAsDict(self):
        """Key is the block id, value the block instance"""
        result = {}
        for block in self.blocks:
            result[id(block)] = block
        return result

    def head(self):
        """no left-parsing needed. The beginning of a track
        has always the same parameters, an empty TrackState."""
        for block in self.blocks:
            block.head()
        self.state = TrackState(self)

    def tail(self):
        """no shortcuts. Parse right till the end, parsing every
        item on its way."""
        while self.right():
            True
        assert self.currentItem() is None #appending position

    def left(self):
        if self.currentBlock().left(): #side effect: actual moving
            self.parseLeft()
            assert self.state.position() >= 0
            return 1
        elif self.state.blockindex > 0:
            blockBeforeCrossing = self.currentBlock()
            self.state.blockindex -= 1
            assert not blockBeforeCrossing is self.currentBlock()
            assert self.currentBlock() is self.blocks[self.state.blockindex]
            self.currentBlock().tail()  #in case we have a block multiple times in the track this resets it from the last time we traversed it
            #block end. This is already the next state. right now we are in the next block left already.
            #instead of parsing left:
            lastBlock = self.blocks[self.state.blockindex]
            self.state.tickindex -= lastBlock.staticExportEndMarkerDuration()
            return 2 #moving succeeded, but no Item was returned. This is a gap between two blocks.
        else:
            return 0
            #track beginning

    def right(self):
        if self.currentBlock().right(): #side effect: actual moving
            self.parseRight()
            return 1
        elif self.state.blockindex+1 < len(self.blocks): #block counts from 0, len from 1. If block+1 is smaller than the amount of blocks this means this is not the final block.
            #block end. This is already the previous state. right now we are in the next block already.
            self.state.blockindex += 1
            self.currentBlock().head()  #in case we have a block multiple times in the track this resets it from the last time we traversed it
            #instead of parsing right:
            lastBlock = self.blocks[self.state.blockindex-1]
            self.state.tickindex += lastBlock.staticExportEndMarkerDuration() #why -1? see comment above
            return 2 #moving succeeded, but no Item was returned. This is a gap between two blocks.
        else:
            return 0
            #track end

    def measureLeft(self):
        """We don't use goToTickIndex for this because that
        is a slow function that starts from head()"""
        goalInTicks = self.state.tickindex - self.state.metricalInstruction().oneMeasureInTicks

        if self.state.metricalInstruction().oneMeasureInTicks == 0:
            goalInTicks -= D1


        while self.left():
            if self.state.tickindex <= goalInTicks:
                break

    def measureRight(self):
        """We don't use goToTickIndex for this because that
        is a slow function that starts from head()"""
        if type(self.currentItem()) is MetricalInstruction:  #this mostly happens in the beginning of a track when the metrical instruction is the first item
            goalInTicks = self.state.tickindex + self.currentItem().oneMeasureInTicks
        else:
            goalInTicks = self.state.tickindex + self.state.metricalInstruction().oneMeasureInTicks

        if self.state.metricalInstruction().oneMeasureInTicks == 0:
            goalInTicks += D1

        while self.right():
            if self.state.tickindex >= goalInTicks:
                break

    def measureStart(self):
        if self.state.ticksSinceLastMeasureStartLive < 0: #upbeats are lower than 0
            while not self.state.ticksSinceLastMeasureStartLive == 0:
                self.right()
        else:
            while not self.state.ticksSinceLastMeasureStartLive <= 0:
                self.left()
            while self.left(): #stops automatically at the beginning of the track
                curItem = self.currentItem()
                if curItem and curItem.logicalDuration() > 0 or not self.state.ticksSinceLastMeasureStartLive == 0: #we found the boundary between this measure and the one before it
                    self.right()
                    break
            else:
                self.head()

    def startOfBlock(self):
        currentBlock = self.currentBlock() #we stay in this block so this doesn't need to change.
        while not currentBlock.localCursorIndex == 0:
            self.left()

    def endOfBlock(self):
        """Go to the end of this block"""
        currentBlock = self.currentBlock() #we stay in this block so this doesn't need to change.
        while not currentBlock.isAppending():
            self.right()

    def goToTickindex(self, targetTickIndex, skipOverZeroDurations = True):
        """Used for moving the active Track up/down.
        If you want to go to a specific position, use the normal
        item index."""
        #if not targetTickIndex == self.state.tickindex: #we are already there? #TODO: this is purely for performance. Leave it out until profiling.

        self.head() #resets everything, including the state.
        while self.state.tickindex < targetTickIndex:
            if not self.right(): #track ends? very well.
                break

        if skipOverZeroDurations:
            while self.currentItem() and self.currentItem().logicalDuration() == 0: #skip over zero-duration items to find the next likely edit point
                self.right()

        return True

    def toPosition(self, position, strict = True):
        """wants track.state.position() as parameter"""
        self.head()
        while not self.state.position() == position:
            if not self.right():
                if strict:
                    raise ValueError("Position does not exist in track", position, self)
                else:
                    self.head()
                    break

    def toBlockAndLocalCursorIndex(self, blockindex, localCursorIndex):
        """A very good way to find a position, while looking the other
        way if the item at this position is indeed the item we are
        looking for or maybe just a copy.

        Remember: Blocks are unique! Items are are only
        unique within a block"""
        self.head()
        while True:
            if self.state.blockindex == blockindex and self.currentBlock().localCursorIndex == localCursorIndex:  #self.currentBlock() is only executed if the blockindex is already correct
                return True
            else:
                if not self.right(): #end of track
                    raise RuntimeError("Position or block not found. Either the block has been deleted or is too short now.", (self.state.blockindex, self.currentBlock().localCursorIndex))

    def toItemInBlock(self, targetItem, targetBlock):
        """A block is the only unique element in a Laborejo score.
        Items are unique per-block. So if we find the block we can find
        the correct item

        We start from head to make sure we parse the actual content of
        the block and not some confused leftovers after a content linked
        insert/delete. This was indeed the reason why this function
        was introduced."""
        self.head()
        while True:
            if self.currentBlock() is targetBlock and self.currentItem() is targetItem:
                return True
            else:
                if not self.right(): #end of track
                    raise RuntimeError("Item {} in block {} not found.".format(targetItem, targetBlock))

    def currentItem(self):
        return self.currentBlock().currentItem()

    def previousItem(self):
        return self.currentBlock().previousItem()

    def nextItem(self):
        return self.currentBlock().nextItem()

    """
    def lookaheadItems(self, number):
        #return a list of items, including the current one.
        #number=2 is the current and the next item.
        startItem = self.currentItem()
        startBlock = self.currentBlock() #this is the only unique thing we can rely on.
        result = [startItem]
        for i in range(number-1):
            self.right()
            result.append(self.currentItem())
        self.toItemInBlock(startItem, startBlock) #find the correct cursor position and trackState
        assert len(result) == number
        return result
    """ #Never used, never tested.


    def insert(self, item):
        """We want the cursor to stay on the the same item
        (or appending) as before the insert.

        If there is a simple item insert the old item will be one
        position to the right. But with linked content blocks the
        step to the right gets multiplied and so the trackState
        counting goes wrong. Sometimes horribly. We need to reset.
        """
        startItem = self.currentItem()
        startBlock = self.currentBlock() #this is the only unique thing we can rely on.
        startBlock.insert(item)
        self.toItemInBlock(startItem, startBlock) #find the correct cursor position and trackState
        return True

    def delete(self):
        """Delete stops at a block boundary. It returns None
        and nothing happens. Item stay the same, cursor stays the same"""
        startBlock = self.currentBlock()
        nextItem = self.nextItem() #we want to return to that item because all items "gravitate to the left" after deletion. Like like in a text editor.
        result = startBlock.delete()
        self.toItemInBlock(nextItem, startBlock) #find the correct cursor position and trackState
        return result

    def parseLeft(self):
        item = self.currentItem()
        dur = item.logicalDuration()
        self.state.tickindex -= dur

        #Metrical position
        self.state.ticksSinceLastMeasureStartLive -= dur #can become < 0, we crossed a measure-border left.
        if self.state.ticksSinceLastMeasureStartLive < 0 :
            self.state.ticksSinceLastMeasureStartLive = self.state.metricalInstruction().oneMeasureInTicks + self.state.ticksSinceLastMeasureStartLive

        if isinstance(item, Chord):
            pass
        elif isinstance(item, Rest):
            pass
        elif isinstance(item, LegatoSlur):
            self.state.duringLegatoSlur = not self.state.duringLegatoSlur #there is only level of slurs and it is not allowed to insert a legatoSlurOpen when self.state.duringLegatoSlur is True or ..close if the state is false. This makes just toggling the value possible,
        elif isinstance(item, MultiMeasureRest):
            pass
        elif isinstance(item, Clef):
            self.state.clefs.pop()
        elif isinstance(item, KeySignature):
            self.state.keySignatures.pop()
        elif isinstance(item, MetricalInstruction):
            self.state.metricalInstructions.pop()
        elif isinstance(item, DynamicSignature):
            self.state.dynamicSignatures.pop()
        elif isinstance(item, InstrumentChange):
            self.state.instrumentChanges.pop()
        #else:
            #items.Item

    def parseRight(self):
        item = self.previousItem()
        dur = item.logicalDuration()
        self.state.tickindex += dur #Anything that is right of the cursor (including the current item, which is technically right of the cursor as well) does not matter for the tickindex and is not saved in here.

        #Metrical position
        self.state.ticksSinceLastMeasureStartLive += dur
        tickRest = self.state.ticksSinceLastMeasureStartLive - self.state.metricalInstruction().oneMeasureInTicks
        if tickRest >= 0: #Measure Number calculation does not protect against human error.  A user can add a MetricalInstruction at the beginning of a measure which will create problems here and there but is allowed nevertheless as temporary state while editing
            self.state.ticksSinceLastMeasureStartLive = tickRest  #If one measure was overfull the rest ist chosen as next startpoint.

        if isinstance(item, Chord):
            pass
        elif isinstance(item, Rest):
            pass
        elif isinstance(item, LegatoSlur):
            self.state.duringLegatoSlur = not self.state.duringLegatoSlur #there is only level of slurs and it is not allowed to insert a legatoSlurOpen when self.state.duringLegatoSlur is True or ..close if the state is false. This makes just toggling the value possible,
        elif isinstance(item, MultiMeasureRest):
            pass
        elif isinstance(item, Clef):
            self.state.clefs.append(item)
        elif isinstance(item, KeySignature):
            self.state.keySignatures.append(item)
        elif isinstance(item, MetricalInstruction):
            self.state.metricalInstructions.append(item)
        elif isinstance(item, DynamicSignature):
            self.state.dynamicSignatures.append(item)
            self.state.dynamicRamp = None #reset
        elif isinstance(item, DynamicRamp): #This is only for a complete parse, not for cursor movement. That is why this is not stack and there is nothing in parseLeft() to unset this. head() is enough to unset.
            self.state.dynamicRamp = item
        elif isinstance(item, ChannelChange):
            self.state.midiChannels.append(item.value)
        elif isinstance(item, InstrumentChange):
            self.state.instrumentChanges.append(item)

    def newGraphTrackCC(self, cc):
        self.ccGraphTracks[cc] = GraphTrackCC(cc, parentTrack = self)

    def addExistingGraphTrackCC(self, cc, graphTrackObject):
        graphTrackObject.parentTrack = self #in case of undo this is redundant. In other cases it might be neccessary
        graphTrackObject.cc = cc #this makes this function the basis for "change cc" or even moving to other tracks
        self.ccGraphTracks[cc] = graphTrackObject

    def deleteGraphTrackCC(self, cc):
        result = self.ccGraphTracks[cc]
        self.ccGraphTracks[cc].prepareForDeletion() #this only deletes the current midi track. not the midi out. If we add this track again through undo we don't need to do anything. it will be regenerated automatically.
        del self.ccGraphTracks[cc]
        return result
        #we don't need to unregister anything from cbox.


    #Save / Load / Export
    def lilypond(self):
        """Called by score.lilypond(), returns a string.

        We carry a dict around to hold lilypond on/off markers like tuplets
        that need to set as ranges. This dict begins here. Each track
        gets its own.
        """
        for item in self.blocks[0].data[:4]:
            if type(item) is MetricalInstruction:
                timeSig = ""
                break
        else:
            timeSig = "\\once \\override Staff.TimeSignature   #'stencil = ##f \\cadenzaOn "

        carryLilypondRanges = {} #handed from item to item for ranges such as tuplets. Can act like a stack or simply remember stuff.

        upbeatLy = "\\partial {} ".format(Duration.createByGuessing(self.upbeatInTicks).lilypond(carryLilypondRanges)) if self.upbeatInTicks else ""


        data = " ".join(block.lilypond(carryLilypondRanges) for block in self.blocks)
        if data:
            return timeSig + upbeatLy + data
        else:
            return ""


    def serialize(self)->dict:
        return {
            "sequencerInterface" : self.sequencerInterface.serialize(),
            "blocks" : [block.serialize() for block in self.blocks],
            "ccGraphTracks" : {ccNumber:graphTrackCC.serialize() for ccNumber, graphTrackCC in self.ccGraphTracks.items()},
            "durationSettingsSignature" : self.durationSettingsSignature.serialize(),
            "dynamicSettingsSignature" : self.dynamicSettingsSignature.serialize(),

            "double" : self.double,
            "initialMidiChannel" : self.initialMidiChannel,
            "initialMidiProgram" : self.initialMidiProgram,
            "initialMidiBankMsb" : self.initialMidiBankMsb,
            "initialMidiBankLsb" : self.initialMidiBankLsb,
            "ccChannels" : self.ccChannels,
            "midiTranspose" : self.midiTranspose,
            "initialInstrumentName" : self.initialInstrumentName,
            "initialShortInstrumentName" : self.initialShortInstrumentName,
            "upbeatInTicks" : self.upbeatInTicks,
        }



    @classmethod
    def instanceFromSerializedData(cls, parentData, serializedData):
        self = cls.__new__(cls)
        self.parentData = parentData
        self.sequencerInterface = template.engine.sequencer.SequencerInterface.instanceFromSerializedData(self, serializedData["sequencerInterface"])

        self.upbeatInTicks = int(serializedData["upbeatInTicks"])
        self.blocks = [Block.instanceFromSerializedData(block, parentObject = self) for block in serializedData["blocks"]]
        self.durationSettingsSignature = DurationSettingsSignature.instanceFromSerializedData(serializedData["durationSettingsSignature"], parentObject = self)
        self.dynamicSettingsSignature = DynamicSettingsSignature.instanceFromSerializedData(serializedData["dynamicSettingsSignature"], parentObject = self)
        self.double = serializedData["double"]
        self.initialMidiChannel = serializedData["initialMidiChannel"]
        self.initialMidiProgram = serializedData["initialMidiProgram"]
        self.initialMidiBankMsb = serializedData["initialMidiBankMsb"]
        self.initialMidiBankLsb = serializedData["initialMidiBankLsb"]
        self.ccGraphTracks = {int(ccNumber):GraphTrackCC.instanceFromSerializedData(graphTrackCC, parentTrack = self) for ccNumber, graphTrackCC in serializedData["ccGraphTracks"].items()}
        self.ccChannels = serializedData["ccChannels"]
        self.midiTranspose = serializedData["midiTranspose"]
        self.initialInstrumentName = serializedData["initialInstrumentName"]
        self.initialShortInstrumentName = serializedData["initialShortInstrumentName"]

        self._processAfterInit()
        return self

    def staticBlocksRepresentation(self):
        """Only the blocks"""
        result = []
        tickindex = 0
        for block in self.blocks:
            duration = block.duration()
            d = {
                "id" : id(block),
                "name" : block.name,
                "tickindex" : tickindex,
                #"data" : block,
                "completeDuration" : duration,
                "minimumInTicks" : block.minimumInTicks,
                }
            result.append(d)
            tickindex += duration
        return result

    def staticTrackRepresentation(self)->dict:
        """Only the minimal track data itself, no items.
        Can be used to re-order tracks or keep the number of tracks
        in sync."""
        result = {
            "id" : id(self),
            "name" : self.name,
            "index" : self.state.index(),
            "upbeatInTicks": int(self.upbeatInTicks),
            "double" : self.double,
            "audible" : self.sequencerInterface.enabled,
            "initialMidiChannel" : self.initialMidiChannel,
            "initialMidiProgram" : self.initialMidiProgram,
            "initialMidiBankMsb" : self.initialMidiBankMsb,
            "initialMidiBankLsb" : self.initialMidiBankLsb,
            "ccChannels" : self.ccChannels,
            "midiTranspose" : self.midiTranspose,
            "initialInstrumentName" : self.initialInstrumentName,
            "initialShortInstrumentName" : self.initialShortInstrumentName,
            #DurationSettings, all strings
            "duration.defaultOn" : str(self.durationSettingsSignature.defaultOn),
            "duration.defaultOff" : str(self.durationSettingsSignature.defaultOff),
            "duration.staccatoOn" : str(self.durationSettingsSignature.staccatoOn),
            "duration.staccatoOff" : str(self.durationSettingsSignature.staccatoOff),
            "duration.tenutoOn" : str(self.durationSettingsSignature.tenutoOn),
            "duration.tenutoOff" : str(self.durationSettingsSignature.tenutoOff),
            "duration.legatoOn" : str(self.durationSettingsSignature.legatoOn),
            "duration.legatoOff" : str(self.durationSettingsSignature.legatoOff),
            }

        for key, value in self.dynamicSettingsSignature.dynamics.items():
            #keys are all strings. we prefix with dynamics for clarity
            result["dynamics." + key] = value

        return result

    def export(self)->dict:
        return {
            "sequencerInterface" : self.sequencerInterface.export(),
        }


    def getPreliminaryData(self):
        """Parse the track and gather information:
            All Dynamic Signatures
            All Dynamic Ramps.

        This data will be used in self.staticRepresentation.
        """
        self.head()
        localRight = self.right #performance
        localPreviousItem = self.previousItem #performance
        localType = type #performance
        lastDynamicRamp = None
        countBlockIndex = 0

        #Parse from left to right. Remember that the item we are interested in is always the previous item!
        while localRight():
            if self.state.blockindex > countBlockIndex:
                assert self.state.blockindex == countBlockIndex+1
                #we crossed a block boundary. previousItem() will fail because it assumes we do not call it on a block beginning.
                countBlockIndex += 1
                continue

            item = localPreviousItem()
            itemtype = localType(item)
            if itemtype is DynamicRamp:
                lastDynamicRamp = item
                item._cachedTickIndex = self.state.tickindex
                item._cachedVelocity = self.dynamicSettingsSignature.dynamics[self.state.dynamicSignature().keyword]
            elif itemtype is DynamicSignature and lastDynamicRamp: # A dynamic signature directly after a DynamicRamp. This is what we are looking for.
                lastDynamicRamp._cachedTargetVelocity = self.dynamicSettingsSignature.dynamics[item.keyword]
                lastDynamicRamp._cachedTargetTick = self.state.tickindex
                lastDynamicRamp = None

    def staticRepresentation(self):
        """The basis for all representations. Be it a GUI or playback.
        Representation does NOT mean export. Lilypond Export works
        in a different way.

        Not for saving, which is called serialize.

        One special case is that it takes block minimumTicks into
        account.

        It also generates the metronome for this track, which is not included in the export dict

        For the changes to take effect a GUI must react to them and calfbox must be updated.
        This is done on the API level by special functions and callbacks.
        """
        #TODO: this is a hack. This makes Laborejo dependent on the api beeing used. This is only the case with GUI or Midi but not as a script. However, we need to expand the tempo track constantly.
        #TODO: However, it is even less elegant to put this call in all rhythm editing methods and functions. inserts, block duplicate, content links, augment, tuplets undo etc.
        #Taken out an placed in tempo Export. #self.score.tempoTrack.expandLastBlockToScoreDuration() #we guarantee that the tempo track is always at least as long as the music tracks.

        midiNotesBinaryCboxData = bytes() # Create a binary blob that contains the MIDI events
        instrumentChangesBinaryCboxData = bytes() # same as above, but only for instrument changes.

        originalPosition = self.state.position()
        self.getPreliminaryData()
        barlines = OrderedDict() #tick:metricalInstruction . We do not use selft.state.barlines, which is simply barlines for the live cursor. This is to send Barlines to a UI and also to generate the Metronome by associating a metricalInstruction with each barline.
        result = []
        metaData = {} #this will be the last item in the representation and can be popped by a GUI. It holds information like the position of barlines.

        #if self.hasContentLinks():
        self.head() #reset the state
        #At this point there is NOTHING in the track state except the init data. You can't look up anything in the state until the cursor moved at least once right. That means a metrical instruction is impossible to detect through the trackstate right now.

        #Initial Midi values
        initialProgamChange = self.state.instrumentChange()
        assert self.state.midiChannel() == self.initialMidiChannel
        assert self.initialMidiProgram == initialProgamChange.program
        assert self.initialMidiBankMsb == initialProgamChange.msb
        assert self.initialMidiBankLsb == initialProgamChange.lsb
        if initialProgamChange.program >= 0: #-1 is off.
            instrumentChangesBinaryCboxData += cbox.Pattern.serialize_event(0, 0xC0 + self.initialMidiChannel, initialProgamChange.program, 0)
            instrumentChangesBinaryCboxData += cbox.Pattern.serialize_event(0, 0xB0 + self.initialMidiChannel, 0, self.initialMidiBankMsb) #position, status byte+channel, controller number, controller value
            instrumentChangesBinaryCboxData += cbox.Pattern.serialize_event(0, 0xB0 + self.initialMidiChannel, 32, self.initialMidiBankLsb) #position, status byte+channel, controller number, controller value

        localRight = self.right #performance
        resultAppend = result.append #performance
        _allExportedChords = [] #a shortcuts for easier beam creation
        _allExportedChordsAppend = _allExportedChords.append

        #Process the items, most likely notes. Remember that the item we are interested in is the previous item because it is left of our cursor.
        previousBarlineTickIndex = self.upbeatInTicks
        previousItem = None
        while True:
            r = localRight()
            if r == 1:  #Exporting the items and adding them to the result is completely done in this block. expObj is not used anywhere else in this function.
                previousItem = self.previousItem()
                expObj = previousItem.exportObject(self.state) #yes, it is correct that the state in the parameter is ahead by one position. Why is it that way? Because everything that matters, like new dynamics will only be parsed afterwards. The trackState is always correct except for the tickindex when exporting after parsing. Thats why exportObject sometimes substracts its own duration for finding its starting tick.
                resultAppend(expObj)
                dur = expObj["completeDuration"]
                if expObj["type"] != "InstrumentChange":
                    for blob in expObj["midiBytes"]: #a list of
                        midiNotesBinaryCboxData += blob
                    if expObj["type"] == "Chord" or expObj["type"] == "Rest": #save for later when we create Beams. No other use.
                        _allExportedChordsAppend(expObj)
                else:
                    for blob in expObj["midiBytes"]: #a list of
                        instrumentChangesBinaryCboxData += blob

            elif r == 2: #block end. Again, this is already the previous state. right now we are in the next block already.
                lastBlock = self.blocks[self.state.blockindex-1] #why -1? see comment above
                dur = lastBlock.staticExportEndMarkerDuration()
                resultAppend(BlockEndMarker().exportObject(self.state)) #this instance of BlockEndMarker does only exist during this export.
            else:
                break

            #Check if any new Barlines need to get created. It is possible that since the last item more than one barline needs to get created (Multi Measure Rests)
            #Achtung! Barlines are calculated by completed measure. That means it counts until a measure is full, according to the current metrical instruction.
            #But metrical instructions themselves need to be calculated at the beginning of the measure. To put them in sync we need to keep track. Therefore we use an ordered dict which provides pairing as well as order.
            #Additional benefit is that multiple metrical instructions on the same tick can be handled, eventhough we consider them a user error (according to the users manual)

            if type(previousItem) is MetricalInstruction and not(self.state.tickindex == 0 and self.upbeatInTicks) : #always start a new metrical section with a barline. Otherwise we don't see a barlines after a long section without barlines.
                previousBarlineTickIndex = self.state.tickindex
                barlines[self.state.tickindex] = self.state.metricalInstruction()
            else:
                #There is a distinction between metrical and non-metrical instructions. However, that doesn't matter for barlines. Both produce barlines.
                #Empty Metrical Instructions (the default for a new track) disable barlines until a metrical instruction appears which then (re)starts the metrical cycles.
                if dur and self.state.metricalInstruction().oneMeasureInTicks > 0:
                    self.state.ticksSinceLastMeasureStart += dur
                    tickRest = self.state.ticksSinceLastMeasureStart - self.state.metricalInstruction().oneMeasureInTicks
                    while tickRest >= self.upbeatInTicks: #we crossed into the next measure. Or even more measures, for MMRests. By definition the metricalInstruction cannot have changed in the meantime since every tick was occupied by a sinle tick.
                        barlines[previousBarlineTickIndex] = self.state.metricalInstruction()
                        previousBarlineTickIndex = self.state.tickindex - tickRest + self.upbeatInTicks
                        self.state.ticksSinceLastMeasureStart = tickRest  #If one measure was overfull the rest ist chosen as next startpoint.
                        tickRest = self.state.ticksSinceLastMeasureStart - self.state.metricalInstruction().oneMeasureInTicks

        #Loop Over
        #In the end add one final barline if the last measure was complete. This will create the effect, for the GUI and the user, that a measure "closes" once it is complete. There is no chance for a double barline because we use a dict.
        if barlines:
            barlines[previousBarlineTickIndex] = self.state.metricalInstruction()

        #We are now at the end the the track.


        #########
        #Converting laborejo objects to export dicts is done. From here on everything uses the exported data
        #########

        #Metronome start
        #The metronome cannot be calculated by simply looking at metricalInstructions.
        #We need to look at the barlines. Two instructions in a row at the same tick are wrong,
        #but technically possible. These are not two full measures of metronome
        self.asMetronomeData = tuple((pos, m.isMetrical, m.treeOfInstructions) for pos, m in barlines.items())
        #Metronome end. Nothing below is connected with the metronome subtrack.

        #Calculate the beam positions for the static groups.
        #We send this data to the GUI.
        beamGroups = [] #a list of list of exportChords.
        currentlyDuringBeaming = False
        if _allExportedChords:
            lastExpChord = _allExportedChords[-1] #for performance
        for expChord in _allExportedChords:
            if "beamGroup" in expChord and expChord["beamGroup"]:
                if currentlyDuringBeaming: #switch it off then
                    expChord["beamGroup"] = "close" #for the GUI
                    currentlyDuringBeaming = False
                    beamGroups[-1].append(expChord)
                else:
                    expChord["beamGroup"] = "open" #for the GUI
                    if expChord is lastExpChord:
                        continue
                    currentlyDuringBeaming = True
                    beamGroups.append(list())
                    beamGroups[-1].append(expChord)

            elif currentlyDuringBeaming:
                if expChord is lastExpChord or expChord["baseDuration"] > D8: #invalid beaming. An open beaming group encountering either the end of the track or >D8 is invalid and not counted.
                    currentlyDuringBeaming = False
                    beamGroups.pop() #the group so far was invalid because it was not closed properly.
                else:
                    beamGroups[-1].append(expChord)

        resultBeamGroups = [] #will be added to the exported meta-data
        for beamGroup in beamGroups:
            if beamGroup: #with content
                #stem means in staffline coordinates (dots on lines): (starting point, length, 1|-)] 1 stem is on the right or -1 left side of the note.
                #We don't deal with the length, stem[1], here. By the time writing this function it was just constant "5" anyway
                #min and max below are "reversed" (min for the highest position in upward stems) because we set coordinates in lines and rows with the middle line as origin/0. Important: Stems do not begin at the same line/space as their note. Upstems begin notehead-1, downstems notehead+1
                stems = [o["stem"] for o in beamGroup] #this cannot be a generator because we are using it multiple times.
                assert stems
                minimum = min(s[0] for s in stems)
                maximum = max(s[0] for s in stems)
                if sum(s[2] for s in stems) >= 0: #beam upwards direction
                    direction = 1
                    length = -5
                    beamPosition = minimum + length - 1 #if not -1 it will be the same as the highest note, in wide intervals
                    startDotOnLineKeyword = "lowestPitchAsDotOnLine"
                    #assert beamPosition <= 0
                else: #beam down direction
                    length = 5
                    beamPosition = maximum + length + 2 #if not +2 it will be in the position of lowest note, in wide intervals
                    direction = -1
                    startDotOnLineKeyword = "highestPitchAsDotOnLine"
                    #assert beamPosition > 0

                #beams have the same syntax and structure as a stem.
                #Until now we figured out the position of the beams as well as their length. However: Mixed duration (e.g. 8th + 16th) groups are allowed and also common.  We need to split these groups into sub-groups
                #These groups are different than the real groups because they share the same baseline for the beams and also have at least one uninterrupted beam connecting all sub-groups.

                subBeamGroups = [[]]
                currentSubGroupFlag = beamGroup[0]["flag"]
                for obj in beamGroup:
                    if obj["flag"] == currentSubGroupFlag:
                        subBeamGroups[-1].append(obj)
                    else:
                        subBeamGroups.append(list())
                        currentSubGroupFlag = obj["flag"]
                        subBeamGroups[-1].append(obj)

                for subBeamGroup in subBeamGroups:
                    for obj in subBeamGroup:
                        startStaffLine = obj[startDotOnLineKeyword]
                        length = beamPosition - startStaffLine
                        obj["beam"]  = (startStaffLine, length, direction) #(starting point, length, 1|-)] 1 stem is on the right or -1 left side of the note.
                    firstItem, lastItem = subBeamGroup[0], subBeamGroup[-1]
                    connectorLength = lastItem["completeDuration"] if not lastItem == beamGroup[-1] else 0
                    resultBeamGroups.append((firstItem["tickindex"], lastItem["tickindex"] + connectorLength, abs(firstItem["flag"]), beamPosition, direction)) #tick-start, tick-end, duration-type, position as staffline. Extra simple for the GUI.
        #Beams finished

        resultAppend(metaData)
        metaData["barlines"] = barlines.keys()
        metaData["duration"] = self.state.tickindex #tickindex is now at the end, so this is the end duration. This includes Blocks minimumDuration as well since it is included in left/right
        metaData["beams"] = resultBeamGroups

        #Notes
        t = (midiNotesBinaryCboxData, 0, self.state.tickindex)
        self.sequencerInterface.setTrack([t])  #(bytes-blob, position, length) #tickindex is still on the last position, which means the second parameter is the length

        #Instrument Changes
        self.sequencerInterface.setSubtrack(key="instrumentChanges", blobs=[(instrumentChangesBinaryCboxData, 0, self.state.tickindex),]) #(bytes-blob, position, length)

        self.toPosition(originalPosition, strict = False) #has head() in it
        return result

#Dependency Injections.
template.engine.sequencer.Score.TrackClass = Track #Score will look for Track in its module.
