#! /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 WeakValueDictionary, WeakSet

#Third Party Modules
from calfbox import cbox

#Template Modules
import template.engine.duration as duration
from template.engine.duration import D1, D4, D1024
from template.helper import pairwise

#Our modules

class GraphItem(object):
    """We use 'CC' here as synonym for whatever int value  between 0
    and 127, typical 7bit midi values"""

    def __init__(self, ccStart):
        self.ccStart = ccStart #int
        self.graphType = "linear" #options: linear, standalone
        self._secondInit(parentBlock = None)
        self.lilypondParameters = {}

    def _secondInit(self, parentBlock):
        """see Score._secondInit"""
        #ignore parentBlock
        pass

    @classmethod
    def instanceFromSerializedData(cls, serializedObject, parentObject):
        """see Score.instanceFromSerializedData"""
        assert cls.__name__ == serializedObject["class"]
        self = cls.__new__(cls)
        self.ccStart = int(serializedObject["ccStart"])
        self.graphType = serializedObject["graphType"]
        self.lilypondParameters = serializedObject["lilypondParameters"]
        self._secondInit(parentBlock = parentObject)
        return self

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

    def copy(self):
        new = GraphItem(self.ccStart)
        new.graphType = self.graphType
        new.lilypondParameters = self.lilypondParameters.copy()
        return new

    def linearRepresentation(self, ccEnd, tickStart, tickEnd):
        """
        the variable is taken from the standard formula: f(x) = m*x + n
        m = (x2-x1) / (y2-y1)
        x1 =  tick start
        y1 =  CC start
        x2 =  tick end
        y2 =  CC end

        tickStart and tickEnd are absolute values for the complete track
        This means we can directly export them to calfbox.

        result is tuples (ccValue, tickPOsitionOfThatCCValue)
        """

        assert 0 <= self.ccStart < 128
        assert 0 <= ccEnd < 128

        result = []
        if ccEnd == self.ccStart: #there is no interpolation. It is just one value.
            result.append((self.ccStart, tickStart))
            return result

        m = (tickEnd - tickStart)  / (ccEnd - self.ccStart) #we need to calculate this after making sure that ccend and start are not the same. Else we get /0
        #From here on: We actually need interpolation. Let the math begin.
        if ccEnd > self.ccStart: #upward slope
            iteratorList = list(range(self.ccStart, ccEnd)) #20 to 70 results in 20....69
        else: #downward slope
            iteratorList = list(range(ccEnd+1, self.ccStart+1)) #70 to 20 results ins 70...21
            iteratorList.reverse()

        #Calculate at which tick a given ccValue will happen. value = m * (i - y1)
        for ccValue in iteratorList:
            result.append((ccValue, tickStart + int(m * (ccValue - self.ccStart))))  # int(m * (ccValue - self.ccStart)) results in 0 for the first item (set by the user). so the first item is just tickStart. that does not mean tickStart for the first item is 0. We just normalize to tickStart as virtual 0 here.
        assert result
        assert result[0][1] == tickStart
        return result

    def staticRepresentation(self, ccEnd, tickStart, tickEnd):
        if self.graphType == "standalone" or tickEnd < 0 or ccEnd < 0:
            result = [(self.ccStart, tickStart)]
        elif self.graphType == "linear":
            result = self.linearRepresentation(ccEnd, tickStart, tickEnd)
        else:
            raise ValueError("Graph Type unknown:", self.graphType)
        assert result
        return result

class GraphBlock(object):
    """Basically a variant of structures.Block, but not for Item type,
    but for GraphItem.

    Relative to the blocks beginning tick position, handled by the
    GraphTrack, in self.data there are:
    key = tick position
    value = GraphItem

    GraphBlocks have a fixed duration. The user has to align them
    manually.
    """

    firstBlockWithNewContentDuringDeserializeToObject = dict() #this is not resetted anywhere since each load is a program start.
    allBlocks = {} #key is the blockId, value is the Block. This is a one way dict. It gets never deleted so undo can recover old blocks. Since blocks are unique this is no problem.

    def __init__(self, parentGraphTrack):

        self.data = {0:GraphItem(0)} #content linked, mutable.
        self.name = str(id(self))
        self._duration = [D1*4] #len is always 1. Duration is content linked, thats why it is a mutable list of len==1.
        #self.duration = 0 #triggers the setter. in reality this is set to a standard minimum value in def duration
        self.linkedContentBlocks = WeakSet() #only new standalone blocks use this empty WeakSet. once you contentLink a block it will be overwritten.
        self._secondInit(parentGraphTrack)

    def _secondInit(self, parentGraphTrack):
        """see Score._secondInit"""
        self.linkedContentBlocks.add(self)
        self.rememberBlock()
        self.parentGraphTrack = parentGraphTrack

    @property
    def duration(self):
        return self._duration[0]

    @duration.setter
    def duration(self, newValue):
        """Keep the mutable list at all cost"""
        
        if newValue <= 0:        
            raise ValueError("duration must be > 1")
        listId = id(self._duration)
        self._duration.pop() #don't replace the data structure, keep the mutable list alive.
        self._duration.append(newValue)
        assert len(self._duration) == 1
        assert listId == id(self._duration)

    def rememberBlock(self):
        oid = id(self)
        GraphBlock.allBlocks[oid] = self #This is on the score level, or a global level. That means we don't need to change this, even if the track gets moved to a new track by the api.
        #weakref_finalize(self, print, "deleted block "+str(oid))
        return oid

    @classmethod
    def instanceFromSerializedData(cls, serializedObject, parentObject):
        """see Score.instanceFromSerializedData"""
        assert cls.__name__ == serializedObject["class"]
        self = cls.__new__(cls)
        self.parentGraphTrack = parentObject

        if serializedObject["data"] is None:
            firstBlock = GraphBlock.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]] #first block with the same contentGroup. This is the one with the real data.
            self.data = firstBlock.data
            self._duration = firstBlock._duration
            self.linkedContentBlocks = firstBlock.linkedContentBlocks #add self to this is in _secondInit
        else: #Standalone or First occurence of a content linked block
            self.data = {int(position):GraphItem.instanceFromSerializedData(item, parentObject = self) for position, item in serializedObject["data"].items()}
            GraphBlock.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]] = self
            self.linkedContentBlocks = WeakSet()
            self._duration = [int(serializedObject["duration"])]  #this is just an int, minimumBlockInTicks or so. Not Item.Duration(). For save and load we drop the list as mutable value.

        self.name = serializedObject["name"]
        self._secondInit(parentObject)
        return self

    def serialize(self):
        result = {}
        result["class"] = self.__class__.__name__
        result["name"] = self.name
        result["duration"] = self.duration #this is just an int, minimumBlockInTicks or so. Not Item.Duration(). For save and load we drop the list as mutable value.

        #We only save the data if this is the first content-linked block in a sequence.
        contentLinkGroupId = id(self.data)
        result["contentLinkGroup"] = contentLinkGroupId

        for block in self.parentGraphTrack.blocks:
            blockId = id(block)
            dataId = id(block.data)
            if dataId == contentLinkGroupId:
                if blockId == id(self): #first block with this dataId found.
                    result["data"] = {int(itemTickPosition):item.serialize() for itemTickPosition, item in self.data.items()}
                else: #content linked, but not the first. Block already serialized.
                    result["data"] = None #we don't need to do anything more. The rest is handled by load and instanceFromSerializedData
                break
        else:
            raise StopIteration("self block not found in graphTrack.blocks")
            #loop ran through. This never happens.

        return result

    def getDataAsDict(self):
        return { "name" : self.name,
                 "duration" : self.duration,
        }

    def putDataFromDict(self, dataDict):
        """modify inplace. Useful for a gui function. Compatible with
        the data from getDataAsDict"""
        self.name = dataDict["name"]
        self.duration = dataDict["duration"]

    def copy(self):
        """Return an independet copy of this block."""
        new = type(self)(parentGraphTrack = self.parentGraphTrack)

        assert len(new.linkedContentBlocks) == 1  #even a copy of a linked block becomes a stand-alone copy

        for itemTickPosition, item in self.data.items():
            new.data[itemTickPosition] = item.copy()
        if self.name.endswith("-copy"):
            new.name = self.name
        else:
            new.name = self.name + "-copy"
        new._duration = self._duration[:] #mutable
        return new

    def contentLink(self):
        """Return a copy where only certain parameters
        like Content are linked. Others can be changed"""
        new = type(self)(parentGraphTrack = self.parentGraphTrack)
        new.linkedContentBlocks = self.linkedContentBlocks
        new.linkedContentBlocks.add(new)
        new.data = self.data #mutable
        new.name = self.name
        new._duration = self._duration #mutable
        return new

    def getUnlinkedData(self):
        """Set and handled for undo/redo by the api.
        This function does not unlink itself but needs
        the api. Instead we return new independent data."""
        newData = {}
        linkedContentBlocks = WeakSet()
        linkedContentBlocks.add(self)

        for itemTickPosition, item in self.data.items():  #deep copy
            copy = item.copy()
            newData[itemTickPosition] = copy            

        return newData, linkedContentBlocks, self._duration[:] #mutable. the api uses the list directly as well because we want to undo/restore the old original list, which may be content linked.

    def linkedContentBlocksInScore(self):
        """Named for compatibility with structures block.
        Added a safety net to make sure only blocks which are actually in the timeline will be
        returned. Not those in the undo buffer.

        The return is sorted to be in the actual track order.
        """
        assert len(self.linkedContentBlocks) >= 1
        blocksInTrack = [block for block in self.linkedContentBlocks if block.parentGraphTrack]
        ordered = sorted(blocksInTrack, key = lambda block: self.parentGraphTrack.blocks.index(block))
        return ordered

    def insert(self, graphItem, tickPositionRelativeToBlockStart):        
        self.data[tickPositionRelativeToBlockStart] = graphItem        
        return True #cannot fail, for now. the api still waits for a positive return code

    def find(self, graphItem):
        """find a relative tick position by graphItem.
        We already know that the graphItem is in this block. Now we
        need to find the position."""
        assert graphItem in self.data.values()
        for position, item in self.data.items():
            if item is graphItem:
                return position
        else:
            raise ValueError("graphItem not found in this block", graphItem, self)

    def remove(self, tickPositionRelativeToBlockStart):
        if not tickPositionRelativeToBlockStart == 0: #don't allow the first item in a block to be deleted. Since the 0 item can't be moved this will always be the 0 item.
            assert tickPositionRelativeToBlockStart != min(self.data.keys()) #this can't be the first item in this block.
            del self.data[tickPositionRelativeToBlockStart]
            return True
        return False

    def move(self, tickPositionRelativeToBlockStart, newPosition):
        """we don't allow the first item to be moved. Makes
        things easier and clearer for the user"""
        if tickPositionRelativeToBlockStart > 0 and not tickPositionRelativeToBlockStart == newPosition: #otherwise it would just delete the complete item because old and new are the same and there would be no duplicate item to delete.
            self.data[newPosition] = self.data[tickPositionRelativeToBlockStart]
            del self.data[tickPositionRelativeToBlockStart]

    def deleteHiddenItems(self):
        if self.getMaxContentPosition() > self.duration:
            toDelete = [pos for pos in self.data.keys() if pos > self.duration] #must be list, not generator, because we need the positions in advance to delete them later. A generator would delete from the dict while it is still generated.
            for delPos in toDelete:
                del self.data[delPos]

    def exportsAllItems(self):
        """Does the current self.duration prevent GraphItems from
        getting exported?"""
        if max(self.data) > self.duration:
            return False
        else:
            return True

    def getMaxContentPosition(self):
        value = max(sorted(self.data.keys()))
        return value

    def staticRepresentation(self):
        """list of (tickPositionRelativeToBlockStart, GraphItem) tuples.
        Only exports items which fit into this blocks duration."""
        sortedListOfTuples = []
        for tickPosition, graphItem in self.data.items():
            if tickPosition <= self.duration:
                sortedListOfTuples.append((tickPosition, graphItem))
        sortedListOfTuples.sort()
        return sortedListOfTuples

    def extendToTrackLength(self, myParentCCTrack):
        """This is a user-called command intended for the last block
        in a track

        Why not extend the last block automatically?
        First these are too many updates so performance goes down.
        Second it confuses the user when he/she attempts to split
        or append. Especially append.
        """
        assert self is myParentCCTrack.blocks[-1]

        plainScoreDuration = myParentCCTrack.score.duration()
        durationWithoutLastBlock = myParentCCTrack.durationWithoutLastBlock()
        #print ("score:", plainScoreDuration, "allOther", TempoBlock.tempoTrack.durationWithoutLastBlock())

        if plainScoreDuration > 0 and plainScoreDuration > durationWithoutLastBlock:
            self.duration = plainScoreDuration - durationWithoutLastBlock

class GraphTrackCC(object):
    """A track for midi Control Changes.
    There is no cursor and no state. Just a sequence of blocks.
    The actual CC value comes from the track."""
    def __init__(self, cc, parentTrack):
        firstBlock = GraphBlock(parentGraphTrack = self)
        self.blocks = [firstBlock] #there is always at least one block.
        self.cc = cc
        self._secondInit(parentTrack = parentTrack)

    def _secondInit(self, parentTrack):
        """see Score._secondInit"""
        self.parentTrack = parentTrack        

    @classmethod
    def instanceFromSerializedData(cls, serializedObject, parentTrack):
        """see Score.instanceFromSerializedData"""
        assert cls.__name__ == serializedObject["class"]
        self = cls.__new__(cls)
        self.cc = int(serializedObject["cc"])
        self.blocks = [GraphBlock.instanceFromSerializedData(block, parentObject = self) for block in serializedObject["blocks"]]
        self._secondInit(parentTrack = parentTrack)
        return self

    def serialize(self):
        result = {}
        result["class"] = self.__class__.__name__
        result["cc"] = self.cc
        result["blocks"] = [block.serialize() for block in self.blocks]
        return result

    def durationWithoutLastBlock(self):
        result = 0
        for block in self.blocks[:-1]: #returns empty list when only one block in self.blocks. So no loop will happen and this function returns 0
            result += block.duration
        return result

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

    def appendGraphBlock(self):
        """A simple method to add a new GraphBlock at the end of the
        current track. Basically you can do the same with split and
        resize, but this is much more easier."""
        new = GraphBlock(parentGraphTrack = self)
        return self.appendExistingGraphBlock(new)

    def appendExistingGraphBlock(self, graphBlock):
        self.blocks.append(graphBlock)
        return graphBlock

    def splitGraphBlock(self, graphBlock, positionInTicksRelativeToBlock):
        """The new block will be right of the original content. If the new block will be empty
        or has no real start value the last prevailing value will be used
        as the blocks start-point"""
        #TODO: refactoring. This is bascially a copy of splitTempoBlock except the block is not an id here. And the name.
        block = graphBlock
        assert block.duration > positionInTicksRelativeToBlock

        hasPointAtZero = False
        toDelete = []
        modelNewBlock = GraphBlock(parentGraphTrack = self) #will not be used directly, but instead content links will be created.
        for pos, item in block.data.items():
            #Determine if a ccPoint gets to move to the new block
            if pos >= positionInTicksRelativeToBlock:
                if pos == positionInTicksRelativeToBlock:
                    hasPointAtZero = True
                modelNewBlock.data[pos - positionInTicksRelativeToBlock] = item
                toDelete.append(pos)
            #else: ccPoint stays in the old block

        for pos in toDelete:
            del block.data[pos]

        #Since every block comes with a value at position 0 we need to check if this was already replaced or if we need to adjust it to the real tempo at this point in time.
        if not hasPointAtZero:
            realStartCCPoint = block.data[block.getMaxContentPosition()] #only the remaining items.
            modelNewBlock.data[0] = realStartCCPoint.copy()

        #Now the original block and all its content links have fewer items than before
        #For every of the content-linked blocks in the tempo track we need to create a new block and insert it right next to it.

        for bl in block.linkedContentBlocksInScore():
            index = self.blocks.index(bl)
            link = modelNewBlock.contentLink()
            self.blocks.insert(index +1, link)

        #duration is mutable. All content links change duration as well.
        link.duration = block.duration - positionInTicksRelativeToBlock
        block.duration = positionInTicksRelativeToBlock

        del GraphBlock.allBlocks[id(modelNewBlock)] #Clean up the model block since it was never intended to go into the TempoTrack

        return True

    def mergeWithNextGraphBlock(self, graphBlock):
        """see structures block (the music block) for an explanation about merging content linked
        blocks.

        Hidden items (after the current duration value) of the original block
        will be deleted. Hidden items of the follow-up block will be merged, but stay hidden.
        """
        #TODO: refactoring. This is bascially a copy of splitTempoBlock except the block is not an id here. And the name.

        block = graphBlock

        if len(self.blocks) == 1:
            logger.info("CC Block merge aborted: only one block in the track")
            return False

        blockIndex = self.blocks.index(block)

        if blockIndex+1 == len(self.blocks):
            logger.info("CC Block merge aborted: not for the last block")
            return False

        nextIndex = blockIndex + 1
        nextBlock = self.blocks[nextIndex]

        firstBlock_endingValue = block.data[block.getMaxContentPosition()].ccStart
        nextBlock_startingValue = nextBlock.data[0].ccStart

        startDurationToReturnForUndo = block.duration

        if len(block.linkedContentBlocksInScore()) == 1 and len(nextBlock.linkedContentBlocksInScore()) == 1: #both blocks are standalone. no content-links. This also prevents that both blocks are content links of each other.
            block.deleteHiddenItems()

            if firstBlock_endingValue == nextBlock_startingValue: #remove redundancy
                del nextBlock.data[0]

            for pos, item in nextBlock.data.items():
                assert not pos + block.duration in block.data
                block.data[pos + block.duration] = item

            block.duration += nextBlock.duration
            self.deleteBlock(nextBlock)

            return startDurationToReturnForUndo

        elif len(block.linkedContentBlocksInScore()) == len(nextBlock.linkedContentBlocksInScore()): #It is maybe possible to build pairs from the current block and all its content links with the follow up block and all its content links.
            #Further testing required:
            for firstBlock, secondBlock in zip(block.linkedContentBlocksInScore(), nextBlock.linkedContentBlocksInScore()):
                if firstBlock.data is secondBlock.data: #content link of itself in succession. Very common usecase, but not compatible with merging.
                    logger.info("CC Block merge aborted: content link follows itself")
                    return False
                elif not self.blocks.index(firstBlock) + 1 == self.blocks.index(secondBlock): #all first blocks must be followed directly by a content link of the second block.  linkedContentBlocksInScore() returns a blocklist in order so we can compare.
                    logger.info("CC Block merge aborted: not all content link blocks-pairs are next to each other")
                    return False
                    
            #Test complete without exit. All blocks can be paired up.

            block.deleteHiddenItems()
            nextBlock.deleteHiddenItems()

            if firstBlock_endingValue == nextBlock_startingValue: #remove redundancy
                del nextBlock.data[0]

            for pos, item in nextBlock.data.items():
                assert not pos + block.duration in block.data  #this includes pos==0, if not deleted above.
                block.data[pos + block.duration] = item
            
            newFirstBlockDuration = block.duration+nextBlock.duration 
            for firstBlock, secondBlock in zip(block.linkedContentBlocksInScore(), nextBlock.linkedContentBlocksInScore()):
                firstBlock._duration = [newFirstBlockDuration,] #Duration is content linked. if we use the setter it will create the wrong sum. Force the new duration instead.
                self.deleteBlock(secondBlock)            

            return startDurationToReturnForUndo

    def deleteBlock(self, graphBlock):
        """at least one block. If you want to delete the track
        use api.deleteGraphTrackCC"""
        if len(self.blocks) > 1:
            #graphBlock.parentGraphTrack = None  #We still need this for undo!
            self.blocks.remove(graphBlock)
            return graphBlock

    def duplicateBlock(self, graphBlock):
        index = self.blocks.index(graphBlock)
        copy = graphBlock.copy()
        self.blocks.insert(index +1, copy)

    def duplicateContentLinkBlock(self, graphBlock):
        index = self.blocks.index(graphBlock)
        linked = graphBlock.contentLink()
        self.blocks.insert(index +1, linked)

    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 rearrangeBlocks(self, listOfBlockIds):
        """Reorder the blocks in this track.
        Achtung! Not including a block will delete this block
        This is not allowed so we check for it."""
        #blocksDict = self.blocksAsDict()
        newBlockArrangement = []

        for idLong in listOfBlockIds:
            #newBlockArrangement.append(blocksDict[idLong])
            newBlockArrangement.append(GraphBlock.allBlocks[idLong]) #all blocks includes deleted blocks in the undo memory.

        #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

    def staticRepresentation(self):
        typeString = ""
        sumOfBlocksDurationsWithoutCurrent = 0
        result = []
        patternBlob = bytes() # Create a binary blob that contains the MIDI events for CC tracks
        self.cleanBlockEdges()
        for blockIndex in range(len(self.blocks)):
            block = self.blocks[blockIndex]
            assert len(block.data) > 0
            l = block.staticRepresentation() #this takes care that only user items which fit in the block.duration are exported.
            for itemIndex in range(len(l)): #range starts from 0 so we can use this as index.
                #l has tuples (tickPositionFromBlockStart, GraphItem)
                thisPosition = l[itemIndex][0]
                thisGraphItem = l[itemIndex][1]

                #Check if we reached the last item in this block.
                if itemIndex is len(l)-1:  #len counts from 1, itemIndex from 0.
                    assert thisGraphItem is l[-1][1] #l[-1] is a tuple (tickPositionFromBlockStart, GraphItem)
                    #Is there another block after this one?
                    if block is self.blocks[-1]: #this was the last block?
                        #there is no nextGraphItem and subsequently no interpolation.
                        typeString = "lastInTrack" # also a switch for later.
                        nextPosition = -1 #doesn't matter
                        #this is checked later in the loop, before we create the exportDict
                    else: #there is still a block left after the current
                        #The nextGraphItem can be found in the next block.
                        nextBlock = self.blocks[blockIndex+1]
                        nextBlockL = nextBlock.staticRepresentation()
                        nextPosition = nextBlockL[0][0] + block.duration #Instead of itemIndex we just need [0] for the first in the next block.
                        nextGraphItem = nextBlockL[0][1] #Instead of itemIndex we just need [0] for the first
                else: #default case. Next item is still in the same block.
                    nextPosition = l[itemIndex+1][0]
                    nextGraphItem = l[itemIndex+1][1]

                #We now generate a chain of items from the current position to the next,
                #at least one, depending on the interpolation type (like linear, none etc.)

                if typeString == "lastInTrack":
                    userItemAndInterpolatedItemsPositions = thisGraphItem.staticRepresentation(-1, thisPosition, -1) #-1 are magic markers that indicate a forced standalone mode. no interpolation, we just get one item back.
                else:
                    assert thisPosition >= 0
                    assert nextPosition >= 0
                    typeString = "user" #interpolated, lastInTrack
                    userItemAndInterpolatedItemsPositions = thisGraphItem.staticRepresentation(nextGraphItem.ccStart, thisPosition, nextPosition)

                for ccValue, generatedTickPosition in userItemAndInterpolatedItemsPositions:  #generatedTickPosition is relative to the block beginning.
                    if typeString == "user" or typeString == "lastInTrack": #interpolated items can be anywhere. We don't care much about them.
                        assert generatedTickPosition <= block.duration

                    assert 127 >= ccValue >= 0
                    assert generatedTickPosition >= 0

                    exportDictItem = {
                        "type" : typeString,
                        "value": -1*ccValue, #minus goes up because it reduces the line position, which starts from top of the screen. For a tempo this is a bpm value for quarter notes.
                        "position" : sumOfBlocksDurationsWithoutCurrent + generatedTickPosition, #generatedTickPosition is relative to the block beginning.
                        "id" : id(thisGraphItem),
                        "blockId": id(block),
                        "minPossibleAbsolutePosition" : sumOfBlocksDurationsWithoutCurrent, #If you want to move this item to the left or right, this is only possible within the current block.
                        "maxPossibleAbsolutePosition" : sumOfBlocksDurationsWithoutCurrent + block.duration, #If you want to move this item to the left or right, this is only possible within the current block.
                        "lastInBlock" : len(block.data) == 1,
                        }
                    result.append(exportDictItem)
                    typeString = "interpolated" #The next items in userItemAndInterpolatedItemsPositions are interpolated items. Reset once we leave the local forLoop.

                    #numbers from 0-15 which represent the midi channels all CCs are sent to. Only replaced by a new tuple by the user directly.                    
                    if self.parentTrack.ccChannels:
                        for channel in self.parentTrack.ccChannels:
                            blob = cbox.Pattern.serialize_event(exportDictItem["position"], 0xB0 + channel, self.cc, ccValue) #position, status byte+channel, controller number, controller value  #TODO use channel of the parent note track at that moment in time.
                            patternBlob += blob
                    else: #If empty then CC uses the tracks initial midi channel.
                        blob = cbox.Pattern.serialize_event(exportDictItem["position"], 0xB0 + self.parentTrack.initialMidiChannel, self.cc, ccValue) #position, status byte+channel, controller number, controller value  #TODO use channel of the parent note track at that moment in time.
                        patternBlob += blob

            #Prepare data for the next block.
            sumOfBlocksDurationsWithoutCurrent += block.duration #the naming is only true until the last iteration. Then all blocks, even the current one, are in the sum and can be used below.
        
        
        sumOfBlockDurations = sumOfBlocksDurationsWithoutCurrent #Choose the correct name. Explicit is better than implicit.
        #if sumOfBlockDurations > 0:
        self.parentTrack.sequencerInterface.setSubtrack(key=self.cc, blobs=[(patternBlob, 0, sumOfBlockDurations),]) #(bytes-blob, position, length)                 
        return result

    def staticGraphBlocksRepresentation(self):
        """Return a sorted list"""
        result = []
        tickCounter = 0
        for block in self.blocks:
            result.append({"type" : "GraphBlock", "id":id(block), "name":block.name, "duration":block.duration, "position":tickCounter, "exportsAllItems":block.exportsAllItems()})
            tickCounter += block.duration
        return result

    def staticTrackRepresentation(self):
        result = {}
        return result

    def graphItemById(self, graphItemId):
        for graphBlock in self.blocks:
            for tickPosition, graphItem in graphBlock.staticRepresentation(): # a list of tuples
                if id(graphItem) == graphItemId:
                    return graphBlock, graphItem
        else:
            raise ValueError("graphItemId not found in this track", graphItemId)

    def cleanBlockEdges(self):
        """If a first block has an item at tick 300 and a second block is resized to begin at
        tick 300 as well then the last item and the start-item are in the same spot.
        The last item needs to go away."""
        for block in self.blocks:
            if block.getMaxContentPosition() == block.duration:
                block.remove(block.duration)

    def otherTracksWithOurLinkedContent(self)->set:
        """returns all tracks that need to be updated"""
        #TODO: A bit wasteful. Optimisation? Did really a content linked block change?
        result = set()
        for block in self.blocks:
            for linkedBlock in block.linkedContentBlocks:                
                result.add(linkedBlock.parentGraphTrack)           
        assert result #at least this very track        
        assert len(result) <= len(self.parentTrack.parentData.tracks)  #For now we assume that blocks cannot be linked across CCs. This can be removed after we tested it for a while and the time comes for cross-CC links        
        return result
