#! /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")

#Standar Library
import sys

#3rd Party

#Template
import template.engine.sequencer

#Our modules
from .block import Block
from .track import Track
from .cursor import Cursor
from .tempotrack import TempoTrack
from .lilypond import fromTemplate

class Data(template.engine.sequencer.Score):

    def __init__(self, parentSession):
        super().__init__(parentSession)
        self.tracks = [Track(parentData = self)] #this is the track order and all visible tracks. For getting a specific track use Track.allTracks with a track id.
        self.hiddenTracks = {} #track-instance:original Position. The value can exist multiple times. These still create a playback representation but are read-only and do not react to editing or GUI requests because you can't access them except through Track.allTrack (which is exactly the same for deleted tracks). Hidden tracks are saved though, deleted ones not.
        self.tempoTrack = TempoTrack(parentData = self)  #The tempoTrack is a Laborejo class. TempoMap is a template.sequencer class that is used by TempoTrack internally

        #Metadata has only strings as keys, even the numbers.
        self.metaData = {key:"" for key in ("title", "subtitle", "dedication","composer","subsubtitle","instrument","meter","arranger", "poet","piece","opus","copyright","tagline", "subtext")}
        self.currentMetronomeTrack = self.tracks[0] #A Laborejo Track, indepedent of currentTrack. The metronome is in self.metronome, set by the template Score.
        self._processAfterInit()

    def _processAfterInit(self):
        self.cursor = Cursor(self)
        self.trackIndex = 0 #a temp variable.
        self.cursorWhenSelectionStarted = None #A cursor dict, from self.cursorExport(). Is None when there is no selection. Can be used for "if selection:" questions. Gets changed quite often.
        self.copyObjectsBuffer = [] #for copy and paste. obviously empty after load file. Also not saved.
        self.cachedTrackDurations = {} #updated after every track export
        #track.asMetronomeData is a generated value from staticExport. Not available yet. needs to be done in api.startEngine #self.metronome.generate(data=self.currentMetronomeTrack.asMetronomeData, label=self.currentMetronomeTrack.name)


    def duration(self):
        """Return the duration of the whole score, in ticks"""
        #TODO: use cached duration? How often is this used?  Pretty often. 3 Times for a single track note update.
        #TODO: Measure before trying to improve performance.
        result = []
        for track in self.tracks:
            result.append(track.duration())
        return max(result)

    def currentTrack(self):
        tr = self.tracks[self.trackIndex]
        return tr

    def trackById(self, trackId):
        """Also looks up hidden and deleted tracks"""
        try:
            ret = Track.allTracks[trackId]
        except:
            print ("Error while trying to get track by id")
            print (sys.exc_info()[0])
            print (trackId, hex(trackId), Track.allTracks, self.tracks, [(track.name, id(track)) for track in self.tracks], self.hiddenTracks, self.trackIndex)
            raise
        return ret

    def noteById(self, noteId):
        """A note, and its parent Chord/Item, can be in multiple tracks.
        But only ever in one block"""
        return Note.allNotes[noteId].parentChord.parentTracks, Note.allNotes[noteId] #this returns the actual instances, not the weak references.

    #def itemById(self, itemId):
    #    """An item can be in multiple tracks. Return all those"""
    #    return self.allItems[itemId].parentTracks, self.allItems[itemId] #this returns the actual instances, not the weak references.

    def allBlocks(self):
        """return all blocks in all tracks in order from top left
        to bottom right, thinking in score-layout."""
        for track in self.tracks + list(self.hiddenTracks.keys()):
            for block in track.blocks:
                yield block

    def blockById(self, blockId):
        """A block (not it's content!) is unique. There is
        exactly one instance with this id, in one track"""
        return Block.allBlocks[blockId].parentTrack, Block.allBlocks[blockId] #returns the actual instances, not the weak references.

    def currentTrackId(self):
        trId = id(self.currentTrack())
        return trId

    def currentBlock(self):
        """There is always a block, this is guaranteed"""
        return self.currentTrack().currentBlock()

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

    #def currentItemAndItsTrack(self):
    #    curItem = self.currentItem()
    #    if curItem:
    #        return curItem.currentItem.parentTracks, curItem
    #    else:
    #        return None

    def currentContentLinkAndItsBlocksInAllTracks(self):
        curBlock = self.currentBlock()
        trackList = list(set([block.parentTrack for block in curBlock.linkedContentBlocksInScore()])) #for this list(set( it doesn't matter that the trackList will not be in order.
        return trackList, curBlock

    def insertTrack(self, atIndex, trackObject):
        """This is also newTrack. the api creates a track object for that.
        atIndex inserts the track before the item currently
        in this position. If atIndex is bigger than the highest current
        index it will be appended through standard python behaviour

        We don't use the template-function addTrack.
        """
        assert trackObject.sequencerInterface
        assert trackObject.parentData == self
        self.tracks.insert(atIndex, trackObject)
        self.trackIndex = self.tracks.index(trackObject)
        assert atIndex == self.trackIndex

    def tracksAsDict(self):
        """used by rearrangeTracks
        Key is the track id, value the track instance"""
        result = {}
        for track in self.tracks:
            result[id(track)] = track
        return result

    def asListOfTrackIds(self):
        """Return an ordered list of track ids"""
        result = []
        for track in self.tracks:
            result.append(id(track))
        return result

    def rearrangeTracks(self, listOfTrackIds):
        """Reorder the tracks.
        Achtung! Not including a track will delete this track. This is
        not the right place to delete a track. So we check for that case"""
        startTrack = self.currentTrack() #we will return later to this track
        originalLength = len(self.tracks)
        tracksDict = self.tracksAsDict()
        newArrangement = []

        for idLong in listOfTrackIds:
            newArrangement.append(tracksDict[idLong])

        seen = set()
        seen_add = seen.add
        self.tracks = [ x for x in newArrangement if x not in seen and not seen_add(x)]

        assert originalLength == len(self.tracks)
        assert self.tracks
        self.trackIndex = self.tracks.index(startTrack)


    def deleteCurrentTrack(self):
        if len(self.tracks) > 1:
            nowInTicks = self.currentTrack().state.tickindex
            result = self.currentTrack(), self.trackIndex #return to the API for undo.

            super().deleteTrack(self.currentTrack())
            #self.currentTrack().prepareForDeletion() #remove calfbox objects
            #self.tracks.remove(self.currentTrack())

            if self.trackIndex+1 > len(self.tracks):  #trackIndex from 0, len from 1.
                self.trackIndex -= 1
                self.currentTrack().head()
            self.currentTrack().goToTickindex(nowInTicks) #that is already the new current track
            return result

    def deleteTrack(self, track):
        if track == self.currentTrack():
            return self.deleteCurrentTrack() #redirect. This way we have no problems with cursor positions etc.

        if len(self.tracks) > 1:
            #We need to know if the old current track was AFTER the deleted one. In this case the trackindex needs to get subtracted by one (before the track is actually deleted)
            result = self.currentTrack(), self.trackIndex #so undo has something to save
            if self.trackIndex > self.tracks.index(track): #there is only > or <. == is ruled out above.
                self.trackIndex -= 1

            super().deleteTrack(track)
            #track.prepareForDeletion()
            #self.tracks.remove(track)
            return result

    def hideTrack(self, track):
        """This track cannot be edited while hidden.
        We don't delete any calfbox data here, contrary to delete Track.
        That means we don't need to touch anything and the playback
        version will remain correct.
        Also make sure we are in the right track afterwards since
        hideTrack changes the trackIndices"""
        assert track in self.tracks #track is visible so it should be in the list of visible tracks
        assert not track in self.hiddenTracks #track is visible so it should not be in the list of hidden tracks
        if len(self.tracks) > 1: #same rules as for delete. do not hide the last visible track
            self.hiddenTracks[track] = self.tracks.index(track)
            if self.trackIndex == self.tracks.index(track): #the track with the active cursor gets hidden
                self.tracks.remove(track)
                self.trackIndex = 0
            else:
                currentTrack = self.tracks[self.trackIndex]
                self.tracks.remove(track)
                self.trackIndex = self.tracks.index(currentTrack) #find out where the current track is now.
            return True #succesful

    def unhideTrack(self, track):
        assert not track in self.tracks #track is hidden so it should not be in the list of visible tracks
        assert track in self.hiddenTracks #track is hidden so it should be in the list of hidden tracks
        currentTrack = self.tracks[self.trackIndex]
        self.tracks.insert(self.hiddenTracks[track], track)
        del self.hiddenTracks[track]
        self.trackIndex = self.tracks.index(currentTrack) #find out where the current track is now.
        return True

    def trackUp(self):
        if self.trackIndex > 0:
            #there is still a track above. At least index 0.
            nowInTicks = self.currentTrack().state.tickindex
            self.trackIndex -= 1
            self.currentTrack().goToTickindex(nowInTicks)
            return True

    def trackDown(self):
        if self.trackIndex < len(self.tracks)-1:
            #there is still a track below
            nowInTicks = self.currentTrack().state.tickindex
            self.trackIndex += 1
            self.currentTrack().goToTickindex(nowInTicks)
            return True

    def trackFirst(self):
        nowInTicks = self.currentTrack().state.tickindex
        self.trackIndex = 0
        self.currentTrack().goToTickindex(nowInTicks)

    def trackLast(self):
        nowInTicks = self.currentTrack().state.tickindex
        self.trackIndex = len(self.tracks)-1
        self.currentTrack().goToTickindex(nowInTicks)

    def cursorExport(self):
        return self.cursor.exportObject(self.currentTrack().state)


    def _cursorExportTrackTick(self, track, tickPosition):
        originalPosition = track.state.position()
        track.goToTickindex(tickPosition)
        exportCursor = self.cursor.exportObject(track.state)
        track.toPosition(originalPosition) #has head() in it
        return exportCursor

    def setSelectionBeginning(self):
        if self.cursorWhenSelectionStarted:
            return False #because we did nothing
        else:
            self.cursorWhenSelectionStarted = self.cursorExport() #There was no selection before. setSelectionBeginning is called by selection-creating functions. So this was the first step of shift+right or so.
            return True #because we did something

    def goToSelectionEnd(self):
        """Keeps the selection alive.
        Written for processing functions like apply to selection.
        Or maybe as a user command.
        Is not used in the internal score selection commands because
        we want to keep everything original."""

        result = self.checkSelectionOrder()
        if not result:
            raise RuntimeError("called score.goToSelectionEnd without a selection")
        else:
            topLeftCursor, bottomRightCursor = result

        #The cursor when selection started is always the opposite of the current cursor position. except if the selection has zero width, which is ruled out by the test above.
        if self.cursorWhenSelectionStarted == topLeftCursor: #already at the end
            return False
        else: #self.cursorWhenSelectionStarted == bottomRightCursor:
            self.cursorWhenSelectionStarted = topLeftCursor
            self.goTo(bottomRightCursor["trackIndex"], bottomRightCursor["blockindex"], bottomRightCursor["localCursorIndex"]) #does not work if the number of items change. e.g. split
            return True
        #else: This fires when the selection goes from bottomLeft to topRight or reverse. Make the "elif" above the new "else".
            #raise ValueError("Cursor was neither at the start nor the end of the selection but a selection was present. This is a serious error. Only selections with the cursor on either end are allowed")

    def goToSelectionStart(self):
        result = self.checkSelectionOrder()
        if not result:
            raise RuntimeError("called score.goToSelectionEnd without a selection")
        else:
            topLeftCursor, bottomRightCursor = result #one of these is always the current cursor, not a cached value. You don't need to test if the current cursor is actually one of these two.

        #The cursor when selection started is always the opposite of the current cursor position. except if the selection has zero width, which is ruled out by the test above.
        if self.cursorWhenSelectionStarted == bottomRightCursor: #already at the beginning
            return False
        else:# self.cursorWhenSelectionStarted == topLeftCursor:
            self.cursorWhenSelectionStarted = bottomRightCursor
            self.goTo(topLeftCursor["trackIndex"], topLeftCursor["blockindex"], topLeftCursor["localCursorIndex"]) #contrary to goToSelectionEnd this works always. No duration or number change can affect this position.
            return True
        #else: This fires when the selection goes from bottomLeft to topRight or reverse. Make the "elif" above the new "else".
        #    raise ValueError("Cursor was neither at the start nor the end of the selection but a selection was present. This is a serious error. Only selections with the cursor on either end are allowed")

    def checkSelectionOrder(self):
        """Return the correct order of the selection boundaries
        (topLeft, bottomRight)
        Returns empty tuple if we don't have a selection or
        a selection of size 0."""
        if self.cursorWhenSelectionStarted: #is None if we don't have a selection at all. Otherwise a cursor export object like self.cursorExport()
            current = self.cursorExport()
            if self.cursorWhenSelectionStarted["tickindex"] == current["tickindex"]:
                #The only time this should happen is after a selection modification by the user which results in having no selections. e.g. "select right", "select left".
                #A special case if a selection changes direction, or "inverts".
                #Note that we don't reset self.cursorWhenSelectionStarted. The selection process is still going on.
                return tuple()

            #we have a selection with dimensions > 0.
            if self.cursorWhenSelectionStarted["trackIndex"] < current["trackIndex"]: #selection order GUI-view: top to bottom
                if self.cursorWhenSelectionStarted["tickindex"] < current["tickindex"]: #selection order left to right
                    result = self.cursorWhenSelectionStarted, current
                else: #selection order right to left. Both topLeft and bottomRight are not created by the user directly. We have to guess what start item the user wants which can be hard if zero-duration items are involved.
                    #There is no other way to go the tickindex of the real first item and force a cursor export.
                    topLeft = self._cursorExportTrackTick(self.cursorWhenSelectionStarted["track"], current["tickindex"]) #yes, the tickindex of the other track
                    bottomRight = self._cursorExportTrackTick(current["track"], self.cursorWhenSelectionStarted["tickindex"]) #yes, the tickindex of the other track
                    return topLeft, bottomRight

            elif self.cursorWhenSelectionStarted["trackIndex"] > current["trackIndex"]: #selection order GUI-view: bottom to top
                if self.cursorWhenSelectionStarted["tickindex"] > current["tickindex"]: #selection order left to right
                    result = current, self.cursorWhenSelectionStarted
                else: #selection order right to left. Both topLeft and bottomRight are not created by the user directly. We have to guess what start item the user wants which can be hard if zero-duration items are involved.
                    #There is no other way to go the tickindex of the real first item and force a cursor export.
                    bottomRight = self._cursorExportTrackTick(self.cursorWhenSelectionStarted["track"], current["tickindex"]) #yes, the tickindex of the other track
                    topLeft = self._cursorExportTrackTick(current["track"], self.cursorWhenSelectionStarted["tickindex"]) #yes, the tickindex of the other track
                    return topLeft, bottomRight

            else: #in the same track
                assert self.cursorWhenSelectionStarted["trackIndex"] == current["trackIndex"] #anything else is logically impossible.
                if self.cursorWhenSelectionStarted["position"] <= current["position"]:
                    result = self.cursorWhenSelectionStarted, current
                else:
                    result = current, self.cursorWhenSelectionStarted
            return result
        else:
            return tuple()


    def selectionExport(self):
        """The boundaries of the selection as two cursor export
        objects. So not the selected items.
        The selectionExport is sorted.
        See api.callbacks _setSelection for more documentation

        For the sake of simplicity a selection is only valid
        if the start-tick and end-tick in all tracks are the same.
        An invalid selection does not prevent the extension or
        modification of a selection itself but it prevents
        processing its content.
        """
        #TODO: If this is a performance problem it has the potential to get cached. Functions like listOfSelectedItems can then use the cached value.
        result = self.checkSelectionOrder()
        if not result:
            return result #empty tuple

        topLeft, bottomRight = result
        startTick = topLeft["tickindex"]
        endTick = bottomRight["tickindex"]
        tracksWithSelectedItems = [track for track in self.tracks[topLeft["trackIndex"]:bottomRight["trackIndex"]+1]] #slice does not include the last value, so +1

        for track in tracksWithSelectedItems:
            originalPosition = track.state.position()
            track.goToTickindex(startTick) #includes track.head() which resets the state
            if (not track.state.tickindex == startTick):
                break
            track.goToTickindex(endTick)
            if (not track.state.tickindex == endTick):
                break
            track.toPosition(originalPosition)

        else: #not executed if break. Means everything was all right.
            return (True, topLeft, bottomRight)
        #We only get here through a for-loop break, which means the selection is invalid. But we still export the selection boundaries for the GUI
        #Reset the last track
        track.toPosition(originalPosition) #has head() in it
        return (False, topLeft, bottomRight)

    def listOfSelectedItems(self, removeContentLinkedData:bool):
        """
        validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = returnedStuff

        Most important is the data, which is in selectedTracksAndItems and has this format:

        [
            [(object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), ...], #track 1
            [(object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), ...], #track 2
        ]
        Please note that the 2nd in the tuple, the cached track state, will be taken out by copyObjects.
        If you want to create a buffer by hand (like the midi module does) just use a list of track-lists with items in it


        Block boundaries are ignored. Just the content.
        We keep the relative position to each other and the relative
        tracks.

        Eleminates duplicate items. That means content-linked block.data
        is only in here once.
        Use this for processing, not for analysis.
        Also don't depend on len() or use a for item in ... loop as
        counter.

        You can deactivate this removal by setting the parameter
        removeContentLinkedData to False. self.copyObjects() does this.

        There is no processing in this function. All items stay the same
        and subsequently have the same durations, positions and
        tickindices.
        When it gets to processing you need to be careful because those
        values change. The only value which is constant is the topLeft
        tickindex and position.
        We take care of duration-changed content linked items left of the selection as well.
        """

        t = self.selectionExport() #is there a selection at all?
        if not t:
            return [False, None, None, [],  []]  #mimickes the true result. the api can use this to decide to do nothing instead of processing the selection
        else:
            selectionValid, topLeft, bottomRight = t
            if not selectionValid:
                return [False, topLeft, bottomRight, [], []]  #mimics the result format. the api can use this to decide to do nothing instead of processing the selection
            startTick = topLeft["tickindex"]
            endTick = bottomRight["tickindex"]
            seenItems = set() #keep duplicate items, aka. content link block data, out of the result.
            tracksWithSelectedItems = [track for track in self.tracks[topLeft["trackIndex"]:bottomRight["trackIndex"]+1]] #slice does not include the last value, so +1
            #listOfChangedTrackIds = set([id(track) for track in tracksWithSelectedItems])  #this is not the same as tracksWithSelectedItems. It gets more ids than the initial ones here. A content-linked note outside a selected track will still be affected. We start with the trackIds we already know.
            listOfChangedTrackIds = set()
            finalResult = [selectionValid, topLeft, bottomRight, listOfChangedTrackIds] #listOfChangedTrackIds is mutable. It will change in the loop below

            firstRound = True
            for track in tracksWithSelectedItems:
                assert track in self.tracks, track #selecting from hidden or deleted tracks is impossible
                originalPosition = track.state.position()
                result = []

                if firstRound: #first track must be accurate. The user selected a specific item, find that, not the tick position which may include zero duration items
                    track.toItemInBlock(topLeft["item"], topLeft["block"])
                    firstRound = False
                else: #all other tracks are only approximations
                    track.goToTickindex(startTick, skipOverZeroDurations = False) #includes track.head() which resets the state

                while track.state.tickindex <= endTick: #we must use the tickindex here because we do not know the end items of the tracks
                    r = track.right()
                    if r == 1:
                        # the item we want is already left of the cursor. So we want the previous item.
                        if track.state.tickindex <= endTick: #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.
                            item = track.previousItem()
                            if (not removeContentLinkedData) or (not item in seenItems):
                                result.append((item, {"keySignature" : track.state.keySignature()})) #The keysig is needed for undo operations after apply to selection
                                seenItems.add(item)
                                for parentBlock in item.parentBlocks: #TODO: just be naive and add them all for now. Performance can be improved later.
                                    #assert parentBlock, item
                                    #assert parentBlock.parentTrack, (parentBlock, parentBlock.parentTrack)
                                    if parentBlock.parentTrack:
                                        assert parentBlock.parentTrack in self.tracks, (parentBlock.parentTrack, id(parentBlock.parentTrack), hex(id(parentBlock.parentTrack)))
                                        listOfChangedTrackIds.add(id(parentBlock.parentTrack)) #there are items without parent Tracks. They have None. We must filter them out otherwise our changedTrackIds get polluted with id(None)
                        else:
                            continue
                    elif r == 2: #block end. Again, this is already the previous state. right now we are in the next block already.
                        continue
                    else: #When does this happen again?
                        break

                track.toPosition(originalPosition) #has head() in it
                finalResult.append(result)

            for changedTrId in listOfChangedTrackIds: #It is not the track ids that change but tracks changed and these are their IDs:
                assert self.trackById(changedTrId) in self.tracks, changedTrId #selecting from hidden or deleted tracks is impossible

            return finalResult

    #The next two functions have "Objects" in their name so copy gets not confused with block.copy or item.copy. This is Ctrl+C as in copy and paste.
    #Cut is done in the api module

    def copyObjects(self, writeInSessionBuffer = True):
        """This is Ctrl+c

        Only works with valid selections.
        We can assume the same tick-duration in all tracks.
        We can also assume that tracks and objects are continuous.
        There are no gaps in the data.

        for external functions: len(result) gives the number of tracks.

        You can just return the buffer without overwriting the clipboard if you set writeInBuffer
        to False. api.duplicate uses this.
        """
        #selectedItems = self.listOfSelectedItems(removeContentLinkedData = False) #list of tracks with selected objects. no block data, just the objects.
        validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = self.listOfSelectedItems(removeContentLinkedData = False)
        result = []
        if validSelection:
            for track in selectedTracksAndItems:
                result.append([item.copy() for item, cachedTrackState in track])
            if writeInSessionBuffer:
                self.copyObjectsBuffer =  result
            return result
        else:
            #self.copyObjectsBuffer = [] #maybe that is a bit harsh. If you press Cltr+C by accident, without a selection, you loose your copied data. This is especially bad for cut and paste
            return []

    def pasteObjects(self, customBuffer = None, overwriteSelection = True):
        """This is ctrl+v

        Rules:

        If you have copied n tracks you need at least n-1 tracks below
        the current. In other words: The number of tracks you copied
        is the number of tracks you paste.

        There needs to be a cursor position, aka slot, at exactly the
        current tickindex in all tracks you are going to paste to.
        Since the copied area is always a 'rectangle' with the same
        tick-duration in each track we want this to be a perfect
        paste as well: everything starts and ends at the same position.
        """
        if (not self.copyObjectsBuffer) and (not customBuffer):
            return False

        if customBuffer:
            workBuffer = customBuffer
        else:
            workBuffer = self.copyObjectsBuffer

        #First do a few tests if we are allowed to paste
        ########################################################
        tickStart = self.currentTrack().state.tickindex
        trackIndexStart = self.trackIndex
        startPosition = self.where() #trackIndex, blockindex, localCursorIndexInBlock
        startItem = self.currentItem()
        #Check if there are enough tracks below
        if len(workBuffer)+trackIndexStart > len(self.tracks):
            #not enough tracks
            broken = True
            #Don't leave this function here because we need to return to the start position below.
        else:
            broken = False

        if not broken:
            #Check if all tracks have a slot at our tickindex
            for track in workBuffer:
                curTrack = self.currentTrack()
                curTrack.goToTickindex(tickStart)
                if not curTrack.state.tickindex == tickStart: #no slot
                    #not the same tick positions
                    broken = True
                    break
                self.trackDown()
            else:
                broken = False

        #return
        #while not self.trackIndex == trackIndexStart:
        #    self.trackUp()
        #self.currentTrack().goToTickindex(tickStart)
        self.goTo(*startPosition) #has assertions as test within

        if broken:
            return False
        #Up to this point no modifications were made. The cursor is in the same position as it was before.

        #########################################################

        #We are ready to paste. There are enough tracks and an empty slots at exactly the tickindex we need.

        if self.cursorWhenSelectionStarted and overwriteSelection:
            listOfChangedTrackIdsFromDelete = self.deleteSelection()
            startPosition = self.where()
            startItem = self.currentItem()
            #TODO: check if the cursor is still in the correct position after delete selection

        listOfChangedTrackIds = set()
        for number, track in enumerate(workBuffer):
            curTrack = self.currentTrack()
            #Here comes an additional condition. Most of the time pastes are single-track. If so, or we simply are in the starting track, we want to paste at the actual position, not the tickindex. This circumvents problems with block boundaries and zero-duration items.
            #TODO: In the future add a "smart paste" that checks if the cursor starts at the beginning or at the end of zero duration items or a block boundary and try to match that position in the other tracks, eventhough we can't be certain if these conditions actually match
            if self.trackIndex == trackIndexStart:
                self.goTo(*startPosition) #has assertions as test within
            else:
                curTrack.goToTickindex(tickStart)

            #assert curTrack.state.tickindex == tickStart #does not work with overwrite selection
            for item in track:
                newItem = item.copy()
                curTrack.insert(newItem) #eventhough copyObjectsBuffer is already a copy of the original notes we don't want every paste to be the same instances.
                #newItem has now a parentBlock and a parentTrack. We add this parent track to the list of changed Tracks
                listOfChangedTrackIds.update(newItem.parentTrackIds) #TODO: profiling. All items in a block have the same parentTrack. This is highly redundant BUT can be different for items which originated from block boundaries
            if not number+1 == len(workBuffer): #enumerate from 0, len from 1.
                #we have to prevent the track going down one too far in the last step and messing with the tick index though.
                self.trackDown()

        #once again, return to the start track and go to the final tick index. Make the cursor ready to paste again in succession.
        finalTickIndex = self.currentTrack().state.tickindex
        while not self.trackIndex == trackIndexStart:
            self.trackUp()
        assert self.trackIndex == trackIndexStart == startPosition[0]
        assert self.currentTrack().state.tickindex == finalTickIndex #this is not enough to pinpoint the real location.

        #Return to the item where pasting starting. Pasting counts as insert, and insert sticks to the item right of cursor.
        #Therefore we go to the starting track and block, but not to the starting localCursorIndexInBlock. Instead we search for the specific item in a second step
        self.goTo(trackIndex=startPosition[0], blockindex=startPosition[1], localCursorIndexInBlock=0)
        self.goToItemInCurrentBlock(startItem) #we actually want to be at the same item where we started, not just the tick index which gets confused with block boundaries and zero duration items

        try: #overwrite? #TODO: What is that? assert with AssertionError pass?
            assert listOfChangedTrackIds == listOfChangedTrackIdsFromDelete
        except AssertionError:
            pass
        except UnboundLocalError:
            pass
        return listOfChangedTrackIds


    def deleteSelection(self):
        """Mortals, Hear The Prophecy: Delete Selection is notoriously tricky. It was hard in Laborejo 1 and it is still hard
        here. Please use as many asserts and tests as you possibly can. The next bug or regression
        WILL come. And it will be here.

        Will only delete from visible tracks. No hidden, no deleted.
        """
        validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = self.listOfSelectedItems(removeContentLinkedData = True)
        #selectedTracksAndItems format: [track1[(item, itsKeysig), (item, itsKeysig), (item, itsKeysig)], track2[(item, itsKeysig), (item, itsKeysig), (item, itsKeysig)]]

        if validSelection:
            numberOfTracks = len(selectedTracksAndItems)
            firstSelectedItem = selectedTracksAndItems[0][0][0]
            lastSelectedItem = selectedTracksAndItems[-1][-1][0]

            #We need to find the topLeft selected item. This is NOT goToSelectionStart() which would be the actual starting point of the user, which includes bottomRight.

            #go to first selected track, starting tick position. selections are always top left to bottom right
            while not self.trackIndex == topLeftCursor["trackIndex"]:
                self.trackUp()
            #self.currentTrack().goToTickindex(topLeftCursor["tickindex"], skipOverZeroDurations = False)
            self.currentTrack().toItemInBlock(topLeftCursor["item"], topLeftCursor["block"])  #includes track.head() which resets the state

            #print ("start deleting from", topLeftCursor["item"], "to BEFORE this item", bottomRightCursor["item"])

            assert topLeftCursor["track"] is self.currentTrack(), (topLeftCursor["track"], self.currentTrack())
            assert topLeftCursor["block"] is self.currentTrack().currentBlock(), (topLeftCursor["block"], self.currentTrack().currentBlock())
            assert topLeftCursor["item"] is self.currentTrack().currentItem(), (topLeftCursor["item"], self.currentTrack().currentItem())
            assert topLeftCursor["itemId"] == id(self.currentTrack().currentItem()), (topLeftCursor["itemId"], id(self.currentTrack().currentItem()))
            if topLeftCursor["item"]:
                assert topLeftCursor["item"] is firstSelectedItem, (topLeftCursor["item"], firstSelectedItem)
            #else: yet unknown set of scenarios. One known is that the selection was from right to left, included normal duration items but ends on a block split which is at the tracks beginning. most likely a leftover from some deletion.

            #NO! bottomRight is not the last selected item but the first item that is not selected anymore!! assert bottomRightCursor["item"] is lastSelectedItem, (bottomRightCursor["item"], lastSelectedItem)

            assert self.currentTrack().currentBlock().localCursorIndex == topLeftCursor["localCursorIndex"], (self.currentTrack().currentBlock().localCursorIndex, topLeftCursor["localCursorIndex"])

            firstRound = True

            for number, selectionTrack in enumerate(selectedTracksAndItems):
                curTrack = self.currentTrack()

                if firstRound: #In the first track the user selected a specific item, which may be zero duration or not. We must delete this, not more not less. All other tracks are approximations and can work with the tickindex.
                    self.currentTrack().toItemInBlock(topLeftCursor["item"], topLeftCursor["block"])  #includes track.head() which resets the state
                    firstRound = False
                else:
                    curTrack.goToTickindex(topLeftCursor["tickindex"], skipOverZeroDurations = False)

                assert curTrack.state.tickindex == topLeftCursor["tickindex"], (curTrack.state.tickindex, topLeftCursor["tickindex"])

                #We delete always from the same cursor position. After one deletion the rest items gravitate left to our position. Except block boundaries, those will not be crossed so we have to check for them:
                for item, cachedState in selectionTrack:
                    if item is bottomRightCursor["item"]:
                        break #without this the program will delete the first non-selected item, if zero duration, as well

                    endOfTrackBreaker = False
                    while curTrack.currentBlock().isAppending(): #Block end, or even track end
                        if not curTrack.right(): #end of track.
                            endOfTrackBreaker = True
                            break

                    if endOfTrackBreaker:
                        break

                    assert curTrack.currentItem() is item, (curTrack.currentItem(), item) #since listOfSelectedItems already removed all duplicate items this will work.
                    curTrack.delete() # side effect: delete the actual item. returns none on block boundary.

                #Don't go down if we are already in the last track of the selection
                if not number+1 == numberOfTracks: #enumerate from 0, len from 1.
                    self.trackDown()

            #assert curTrack.state.tickindex == topLeftCursor["tickindex"] We cannot be sure of this. The selection could have started at a content-linked item that got deleted and the tickindex has no item anymore
            #return [id(track) for track in self.tracks] #TODO: only for debug reasons. the GUI will draw everything with this! Either this function or the api has to figure out the tracks that really changed. we could do a len check of the blocks data.

            return listOfChangedTrackIds

    def getBlockAndItemOrder(self):
        """
        Docstring for getBlockAndItemOrder and putBlockAndItemOrder.

        getBlockAndItemOrder creates a complete snapshot of
        the whole score. Since saving tracks and blocks in the
        usual musical order of magnitude is no performance-problem
        for python.

        It can be put back in the score with putBlockAndItemOrder.

        We use this for undo/redo of deleteSelection and paste.

        Even after items got deleted deleteSelection,
        they are still avaible to get re-inserted as long as the data
        was kept around somewhere, for example in the undo/redo stack.
        Effectively we discard any additional items introduced by paste
        or restore lost items through deleteSelection.
        This is only the order of items, not the state of the items,
        but since undo/redo works lineary there can be no changes
        to the items left. They would have been undone before this
        step was possibly at all.

        It is very important to be aware of content-linked blocks data.
        If there is such a block in the given data all tracks with its
        link must be in dictWithTrackIDsAndDataLists as well, with
        the data already linked again.

        This is easy when we save a complete snapshot instead of
        selectively choosing tracks. However, the api callbacks
        must only update tracks that really changed, not the whole
        score. This would be far too expensive for a GUI.

        Other functions do not need to filter the dict when using
        putBlockAndItemOrder. It looks for true changes itself
        and returns a list of trackIDs that need callback updates.

        No blocks will be deleted or replaced with this because
        deleteSelection leaves empty blocks and paste is not capable
        of creating new blocks.
        """
        result = {} #track id, blockAndItemOrder
        seen = {} # originalDataId : sliceCopy. Takes care of content linked blocks data.

        for track in self.tracks:
            newWorldOrder = []
            for block in track.blocks:
                blockDataId = id(block.data)
                if not blockDataId in seen:
                    seen[blockDataId] = block.data[:]
                newWorldOrder.append(seen[blockDataId])

            result[id(track)] = newWorldOrder
        return result

    def putBlockAndItemOrder(self, dictWithTrackIDsAndDataLists):
        """see getBlockAndItemOrder for documentation"""

        for trackId, listWithBlocksData in dictWithTrackIDsAndDataLists.items():
            assert len(self.trackById(trackId).blocks) == len(listWithBlocksData) #paste and delete selection never delete or create blocks. So the len is always the same. we can zip
            for block, parameterBlockData in zip(self.trackById(trackId).blocks, listWithBlocksData):
                block.data = parameterBlockData

        return [id(track) for track in self.tracks] #TODO: only for debug reasons. the GUI will draw everything with this! Either this function or the api has to figure out the tracks that really changed. we could do a len check of the blocks data.

    def listOfTrackIds(self):
        """Mostly used for the initial callback sweep after loading
        a new file in the api."""
        #return Track.allTracks.keys() #this does contain deleted tracks (in the undo stack) as well. It is only valid when the  program starts up. The undo stack is empty then.
        #assert len (Track.allTracks) == len(self.tracks)  #make sure to only call this function when it is correct. If we ever use this for non-startup reasons change to the list comprehension below.
        #return Track.allTracks.keys()
        return [id(track) for track in self.tracks] #tracks in the score, not in the undo stack.

    def listOfStaticTrackRepresentations(self):
        """A list of tracks"""
        result = []
        for track in self.tracks:
            static = track.staticTrackRepresentation()
            static["hiddenPosition"] = None
            result.append(static)
        return result


    def listOfStaticHiddenTrackRepresentations(self):
        """A list of hidden tracks. Mutually exclusive with
        listOfStaticTrackRepresentations """

        result = []
        for track, originalPosition in self.hiddenTracks.items():
            static = track.staticTrackRepresentation()
            static["hiddenPosition"] = originalPosition
            result.append(static)
        return result


    def graphBlockById(self, blockId):
        for track in self.tracks:
            for cc, graphTrackCC in track.ccGraphTracks.items():
                for graphBlock in graphTrackCC.blocks:
                    if id(graphBlock) == blockId:
                        return id(track), cc, graphBlock
        else:
            raise ValueError("graphBlock with this id not in any track")

    def graphItemById(self, graphItemId):
        for track in self.tracks:
            for cc, graphTrackCC in track.ccGraphTracks.items():
                try:
                    graphBlock, graphItem = graphTrackCC.graphItemById(graphItemId)
                    return id(track), cc, graphBlock, graphItem
                except ValueError: #raised when not in thie cc/track
                    pass
        else:
            raise ValueError("graphItem with this id not in any track")

    def goTo(self, trackIndex, blockindex, localCursorIndexInBlock):
        """Handles even shifting positions in a content link situation"""
        self.trackIndex = trackIndex
        self.currentTrack().toBlockAndLocalCursorIndex(blockindex, localCursorIndexInBlock)
        assert self.where() == (trackIndex, blockindex, localCursorIndexInBlock)

    def where(self):
        """return the data needed by self.goto
        where even keeps the local position if a content linked block inserts items before our position """
        return self.trackIndex, self.currentTrack().state.blockindex, self.currentTrack().currentBlock().localCursorIndex

    def goToItemInCurrentBlock(self, itemInstance):
        """Can also be None for the next block-end/appending """
        self.currentBlock().goToItem(itemInstance)

    #Deprecated
    #def goToItem(self, itemInstance):
    #   """aka search for item. Item instances are unique across a session"""
    #   this is quite expensive. Most of the time we already know the track and block. Use score.goTo with localCUrsorIndexInBlock=0

    def allItems(self, hidden = True, removeContentLinkedData = False):
        seenItems = set()
        if hidden:
            what = list(self.hiddenTracks.keys()) + self.tracks
            #what = Track.allTracks.values() #this includes deleted tracks
        else:
            what = self.tracks

        for track in what:
            for block in track.blocks:
                for item in block.data:
                    if (not removeContentLinkedData) or (not item in seenItems):
                        seenItems.add(item)
                        yield item


    def transposeScore(self, rootPitch, targetPitch):
        """Based on intervalAutomatic"""
        for item in self.allItems(removeContentLinkedData=True):
            item.intervalAutomatic(rootPitch, targetPitch)

    def joinCurrentBlockAndAllContentLinks(self):
        """This is in score and not in track because it works on content links across tracks as well

        Take pairs of blocks current+next and all its content links in the score and join them
        together. The blocks will get the meta-information of the first block.

        a) Every content linked block to the current one is followed by
        a block which is content-linked itself to all other follow-ups.
        b) The current block has no content links. This is really just
        special case of a)

        All content links that are followed by their own link are caught as well.
        Logic dictates that at least one of the content links will have either no follow up
        (end of track) or a different follow up. Both are cases that will prevent a join from
        happening.

        This function does not actually join the blocks but creates new blocks that will replace
        the old pairs. We then return a data structure that api.rearrangeBlocksInMultipleTracks
        can use to create the new order. The old blocks are kept for undo and redo.

        dictOfTrackIdsWithListOfBlockIds is [trackId] = [listOfBlockIds]
        """
        startItem = self.currentTrack().currentItem()
        startBlock = self.currentTrack().currentBlock()

        workingList = [] #pairs of (block, followUpBlock)
        try:
            startFollowUpBlock = startBlock.parentTrack.blocks[startBlock.parentTrack.blocks.index(startBlock) +1] #the follow up for the current block.
        except:
            #current block already without follow up
            return False #already no follow up. Leave.

        #Get the blocks which are actually in the score and not just a reference and compare their lengths.
        if not len([True for strtblk in startBlock.linkedContentBlocks if strtblk._parentTrack]) == len([True for flwupblk in startFollowUpBlock.linkedContentBlocks if flwupblk._parentTrack]):
            #startBlock and FollowUp content link registers do not have same length
            return False

        for block in startBlock.linkedContentBlocks: #all linked blocks in all tracks, including currentBlock. All blocks are unique. The content is not.
            blockIndex = block.parentTrack.blocks.index(block)
            try:
                followUpBlock = block.parentTrack.blocks[blockIndex +1]
            except IndexError: #there is no follow up block. This violates our two requirements.
                #block without follow up found
                return False
            else: #the block is there. continue:
                if not followUpBlock in startFollowUpBlock.linkedContentBlocks:
                    #not all follow up blocks are the same content
                    return False
                else:
                    workingList.append((block, followUpBlock))

        #we now have made it clear that the requirements from the docstring are met.
        dictOfTrackIdsWithListOfBlockIds = {}

        protoBlock = startBlock.copy(newParentTrack = self.currentTrack())
        protoBlock.data += startFollowUpBlock.data

        for firstBlock, secondBlock in workingList:
            parentTrackId = id(firstBlock.parentTrack)
            assert firstBlock.parentTrack in self.tracks, (firstBlock.parentTrack, id(firstBlock.parentTrack), hex(id(firstBlock.parentTrack)))

            if parentTrackId in dictOfTrackIdsWithListOfBlockIds:
                blockOrder = dictOfTrackIdsWithListOfBlockIds[parentTrackId]
            else:
                blockOrder = firstBlock.parentTrack.asListOfBlockIds()

            uniqueBlockForThisPosition = firstBlock.copy(newParentTrack = firstBlock.parentTrack)
            uniqueBlockForThisPosition.name = uniqueBlockForThisPosition.name[:-5] #remove "-copy"
            uniqueBlockForThisPosition.data = protoBlock.data
            uniqueBlockForThisPosition.linkedContentBlocks = protoBlock.linkedContentBlocks
            uniqueBlockForThisPosition.linkedContentBlocks.add(uniqueBlockForThisPosition)

            assert id(uniqueBlockForThisPosition) in Block.allBlocks
            blockOrder[blockOrder.index(id(firstBlock))] = id(uniqueBlockForThisPosition)
            assert not id(firstBlock) in blockOrder

            #We don't need to actually delete the blocks here, this will be done indirectly by rearrangeBlocks through the api later.
            blockOrder.remove(id(secondBlock))
            assert not id(secondBlock) in blockOrder

            dictOfTrackIdsWithListOfBlockIds[parentTrackId] = blockOrder #eventhough it is mutable and therefore changes in the list are reflected in the dict-value we did not make sure that the list is in the dict in the first place above.

        protoBlock.linkedContentBlocks.remove(protoBlock)
        del(protoBlock)

        return dictOfTrackIdsWithListOfBlockIds


    def splitCurrentBlockAndAllContentLinks(self):
        """split the current block around the cursor position.
        If used in an appending position it just creates a new block.

        The new block gets inserted directly right of
        each original content-linked block.

        For the user this looks like all contentLinked blocks got
        splitted in half, but they remain content linked.
        """

        track = self.currentTrack()
        startBlock = track.currentBlock() #this is the only unique thing we can rely on.

        #startBlock will get deleted, but we don't remove the blocks as parent because we need them for undo
        #They will be finally cleared on save.
        #NO. This works for splitting, but not for undo. for item in startBlock.data:
        #    item.parentBlocks.remove(startBlock)

        dictOfTrackIdsWithListOfBlockIds = {}  # [trackId] = [listOfBlockIds]

        protoBlockLeft = startBlock.copy(track)
        protoBlockLeft.data = startBlock.data[:startBlock.localCursorIndex] #everything left of the cursor. The local cursor index of the current block is guaranteed to be correct. The one from other blocks is not.

        protoBlockRight = startBlock.copy(track)
        protoBlockRight.data = startBlock.data[startBlock.localCursorIndex:] #everything right of the cursor. The local cursor index of the current block is guaranteed to be correct. The one from other blocks is not.

        for block in startBlock.linkedContentBlocksInScore(): #all linked blocks in all tracks, including currentBlock. All blocks are unique. The content is not.
            parentTrackId = id(block.parentTrack)
            if parentTrackId in dictOfTrackIdsWithListOfBlockIds:
                blockOrder = dictOfTrackIdsWithListOfBlockIds[parentTrackId]
            else:
                blockOrder = block.parentTrack.asListOfBlockIds()

            newLeft = protoBlockLeft.contentLink()
            newLeftId = id(newLeft)
            newLeft.name = block.name
            newLeft.minimumInTicks = block.minimumInTicks / 2
            assert newLeftId in Block.allBlocks
            assert newLeft.parentTrack is block.parentTrack
            assert newLeft in protoBlockLeft.linkedContentBlocks

            newRight = protoBlockRight.contentLink()
            newRightId = id(newRight)
            newRight.name = block.name
            newRight.minimumInTicks = newLeft.minimumInTicks
            assert newRightId in Block.allBlocks
            assert newRight.parentTrack is block.parentTrack
            assert newRight in protoBlockRight.linkedContentBlocks

            positionToInsert = blockOrder.index(id(block))

            blockOrder[positionToInsert] = newLeftId #replaces the original block which will be kept intact for undo
            blockOrder.insert(positionToInsert + 1, newRightId)

            dictOfTrackIdsWithListOfBlockIds[parentTrackId] = blockOrder #eventhough it is mutable and therefore changes in the list are reflected in the dict-value we did not make sure that the list is in the dict in the first place above.

        protoBlockLeft.linkedContentBlocks.remove(protoBlockLeft) #probably redundant. But makes the intention clear.
        protoBlockRight.linkedContentBlocks.remove(protoBlockRight)
        del(protoBlockLeft)
        del(protoBlockRight)

        return dictOfTrackIdsWithListOfBlockIds

    def removeEmptyBlocks(self):
        dictOfTrackIdsWithListOfBlockIds = {}  # [trackId] = [listOfBlockIds]

        #for trId, track in Track.allTracks.items():
        for track in list(self.hiddenTracks.keys()) + self.tracks:
            if len(track.blocks) <= 1:
                continue #next track

            #Get all blocks and then remove those with no content.
            trId = id(track)
            listOfBlockIds = track.asListOfBlockIds()
            for block in track.blocks:
                if not block.data:
                    listOfBlockIds.remove(id(block))
            #Maybe the track was empty. In this case we add one of the blocks again. Otherwise follow up functions will not act.
            if not listOfBlockIds:
                listOfBlockIds.append(id(block))
            dictOfTrackIdsWithListOfBlockIds[trId] = listOfBlockIds
        return dictOfTrackIdsWithListOfBlockIds

    def lilypond(self):
        """Entry point for converting the score into lilypond.
        Called by session.saveAsLilypond(), returns a string, which is the file content.

        What is happening here?
        Only visible tracks get exported.
        Each object controls what it exports. Overrides are possible at every level.
        """
        tempoStaff = self.tempoTrack.lilypond()
        data = {track:track.lilypond() for track in self.tracks} #processed in the lilypond module
        return fromTemplate(session = self.parentSession, templateFile = "default.ly", data = data, meta = self.metaData, tempoStaff = tempoStaff)

    #Save / Load / Export
    def serialize(self)->dict:
        dictionary = super().serialize()
        dictionary["class"] = self.__class__.__name__
        dictionary["tracks"] = [track.serialize() for track in self.tracks]
        #we can't save hiddenTracks as dict because the serialized track is a dict itself, which is not hashable and therefore not a dict-key.
        dictionary["hiddenTracks"] = [[track.serialize(), originalIndex] for track, originalIndex in self.hiddenTracks.items()]
        dictionary["tempoTrack"] = self.tempoTrack.serialize()
        dictionary["metaData"] = self.metaData
        dictionary["currentMetronomeTrackIndex"] = self.tracks.index(self.currentMetronomeTrack)
        return dictionary

    @classmethod
    def instanceFromSerializedData(cls, parentSession, serializedData):
        assert cls.__name__ == serializedData["class"]
        self = cls.__new__(cls)
        super().copyFromSerializedData(parentSession, serializedData, self) #Tracks, parentSession and tempoMap
        self.parentSession = parentSession
        self.hiddenTracks = {Track.instanceFromSerializedData(parentData=self, serializedData=track):originalIndex for track, originalIndex in serializedData["hiddenTracks"]}
        self.tempoTrack = TempoTrack.instanceFromSerializedData(serializedData["tempoTrack"], parentData = self)
        self.metaData = serializedData["metaData"]
        self.currentMetronomeTrack = self.tracks[serializedData["currentMetronomeTrackIndex"]]
        self._processAfterInit()
        return self

    def export(self)->dict:
        dictionary = super().export()

        return {
            "numberOfTracks" : len(self.tracks),
            "howManyUnits" : self.howManyUnits,
            "whatTypeOfUnit" : self.whatTypeOfUnit,
            "numberOfMeasures" : self.numberOfMeasures,
            "measuresPerGroup" : self.measuresPerGroup,
            "subdivisions" : self.subdivisions,
            "isTransportMaster" : self.tempoMap.export()["isTransportMaster"],
            }

