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

This file is part of Laborejo ( https://www.laborejo.org )

Laborejo 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")

from weakref import WeakSet
from weakref import ref as weakref_ref
from .items import * #loading from file needs all items.

class Block(object):
    #allBlocks = WeakValueDictionary() #key is the blockId, value is the weak reference to the Block
    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.
    #NEVER!! iteratre over allBlocks if you don't know what you are doing. This contains deleted blocks as well.
    firstBlockWithNewContentDuringDeserializeToObject = dict() #this is not resetted anywhere since each load is a program start.

    def __init__(self, track):
        self.data = list()
        self._name = [ str(id(self)) ]  #list of len==1 so it is mutable for linked blocks. See @property below
        self._minimumInTicks = [0] # if this is bigger than the actual duration-sum of the content this will be used instead. Advice: best used in the form of x*210 (multiple of real base-durations)  #is content linked, thats why it is a mutable list of len==1. See @property below
        self.linkedContentBlocks = WeakSet() #only new standalone blocks use this empty WeakSet. once you contentLink a block it will be overwritten.
        self._secondInit(parentTrack = track)

    def _secondInit(self, parentTrack):
        """see Score._secondInit"""
        self._parentTrack = weakref_ref(parentTrack) #a block can only be in one track.
        self.localCursorIndex = 0
        self.linkedContentBlocks.add(self)
        self.rememberBlock()

    @property
    def name(self):
        """name must be mutable so it is shared between content link blocks"""
        return self._name[0]

    @name.setter
    def name(self, name:str):
        listId = id(self._name) #just a precaution
        self._name.pop()
        self._name.append(name)
        assert len(self._name) == 1
        assert listId == id(self._name)

    @property
    def minimumInTicks(self):
        """minimumInTicks must be mutable so it is shared between content link blocks"""
        return self._minimumInTicks[0]

    @minimumInTicks.setter
    def minimumInTicks(self, newValue:int):
        """Keep the mutable list at all cost"""
        listId = id(self._minimumInTicks) #just a precaution
        self._minimumInTicks.pop()
        self._minimumInTicks.append(newValue)
        assert len(self._minimumInTicks) == 1
        assert listId == id(self._minimumInTicks)

    @classmethod
    def instanceFromSerializedData(cls, serializedObject, parentObject):
        """see Score.instanceFromSerializedData"""
        assert cls.__name__ == serializedObject["class"]
        self = cls.__new__(cls)
        if serializedObject["data"] is None: #Found a content linked block which already has one member of its group in the score
            firstBlock = Block.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]] #block with the same contentGroup. This is the one with the real data.
            self.data = firstBlock.data
            self._minimumInTicks = firstBlock._minimumInTicks #mutable list
            self._name = firstBlock._name #mutable list
            self.linkedContentBlocks = firstBlock.linkedContentBlocks #add self to this is in _secondInit
        else: #found a stand-alone block or the first one of a content link group
            self.linkedContentBlocks = WeakSet()
            self.data = [eval(item["class"]).instanceFromSerializedData(item, parentObject = self) for item in serializedObject["data"]]
            self._name = [ str(serializedObject["name"]) ] #saved as string, used as list
            self._minimumInTicks = [ int(serializedObject["minimumInTicks"]) ]  #saved as int, used as list
            Block.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]] = self
        for item in self.data:
            item.parentBlocks.add(self)
        self._secondInit(parentTrack = parentObject)
        return self

    def serialize(self):
        """Return a serialized data from this instance.
        Used for save and load.

        Can be called in a chain by subclasses with super().serialize

        The main difference between serialize and exportObject is that
        serialize does not compute anything. I just saves the state
        without calulating note on and off or stem directions,
        for example.
        """
        #result = super()._serialize() #call this in child classes
        result = {}
        result["class"] = self.__class__.__name__
        result["name"] =  self.name
        result["minimumInTicks"] = self.minimumInTicks

        #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.parentTrack.parentData.allBlocks():
            blockId = id(block)
            dataId = id(block.data)
            if dataId == contentLinkGroupId:
                if blockId == id(self): #first block with this dataId found.
                    result["data"] = [item.serialize() for item in self.data]
                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:
            #loop ran through. This never happens.
        return result

    def rememberBlock(self):
        oid = id(self)
        Block.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

    @property
    def parentTrack(self):
        if self._parentTrack:
            return self._parentTrack() #return the real parentTrack
        else:
            assert self._parentTrack is None
            return self._parentTrack

    @parentTrack.setter
    def parentTrack(self, newTrack):
        if not self._parentTrack: #because it was None/not in a track
            self._parentTrack = WeakSet()
        if newTrack:
            self._parentTrack = weakref_ref(newTrack)
        else:
            self._parentTrack = None

    def copy(self, newParentTrack):
        """Return an independet copy of this block.
        It will not be inserted into a track here but in the parentTrack

        It is by design only possible that a block will be inserted
        in the same track, next to the original block.
        It can be moved later by the api module which will reset
        the parentTrack. But at first the parentTrack stays the same."""
        new = Block(newParentTrack)
        assert newParentTrack

        #Do not change new.linkedContentBlocks!  Copy creates a stand alone copy.
        assert len(new.linkedContentBlocks) == 1

        for item in self.data:
            copyItem = item.copy()
            new.data.append(copyItem)

            assert not copyItem.parentBlocks #parentBlock was empty until now
            copyItem.parentBlocks.add(new)
            if self in copyItem.parentBlocks: #TODO: investigate
                copyItem.parentBlocks.remove(self)

        if self.name.endswith("-copy"):  #name is mutable, but getter and setter deal with that.
            new.name = self.name
        else:
            new.name = self.name + "-copy"
        new._minimumInTicks = self._minimumInTicks[:] #it is a mutable value, we make a copy of the list (which has an int inside, which is immutable and copied automatically)
        return new

    def getUnlinkedData(self):
        """
        Returns a new list with copies of items.
        Set and handled for undo/redo by the api.
        """
        newData = []
        newParentBlocks = WeakSet()
        newParentBlocks.add(self)

        for item in self.data:
            copyItem = item.copy()
            newData.append(copyItem)
            copyItem.parentBlocks = newParentBlocks

        assert len(newData) == len(self.data)
        return newData


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

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

    def contentLink(self):
        """Return a copy where only certain parameters
        like Content are linked. Others can be changed.
        It will not be inserted into a track here but in the parentTrack

        It is by design only possible that a block will be inserted
        in the same track, next to the original block.
        It can be moved later by the api module which will reset
        the parentTrack. But at first the parentTrack stays the same.
        """
        assert self.parentTrack
        new = Block(self.parentTrack)
        new.linkedContentBlocks = self.linkedContentBlocks
        new.linkedContentBlocks.add(new)
        new.data = self.data  #mutable. Will change in all blocks together.
        new._name = self._name  #mutable as well.
        new._minimumInTicks = self._minimumInTicks #mutable
        #Add the new block to the parentBlocks set but don't delete self from it, as in copy(). The items are now in more than one block simultaniously
        for item in new.data:
            item.parentBlocks.add(new)
        return new

    def linkedContentBlocksInScore(self):
        """filters linkedContentBlocks to only include
        those currently in the score. Not those in the undo repository
        """
        return (block for block in self.linkedContentBlocks if block.parentTrack)

    def duration(self):
        """The first block might have an upbeat. We check that here and not in the track."""

        actualBlockDuration = 0
        for item in self.data:
            actualBlockDuration += item.logicalDuration()

        if  actualBlockDuration >= self.minimumInTicks:
            return actualBlockDuration
        else:
            return self.minimumInTicks

    def staticExportEndMarkerDuration(self):
        actualBlockDuration = 0
        for item in self.data:
            actualBlockDuration += item.logicalDuration()
        if  actualBlockDuration >= self.minimumInTicks: #this also guarantees that the substraction below is > 0.
            return 0
        else:
            return self.minimumInTicks - actualBlockDuration  #this is the difference to self.duration()

    def position(self):
        """the position of the subcursor in this block"""
        return self.localCursorIndex

    def left(self):
        if self.localCursorIndex > 0:
            self.localCursorIndex -= 1
            return True
        else:
            return False #Already at the start.

    def right(self):
        if not self.isAppending():
            self.localCursorIndex += 1
            return True
        else:
            return False #Already at the end.

    def head(self):
        self.localCursorIndex = 0

    def tail(self):
        self.localCursorIndex = len(self.data) #eventhough len counts from 1 and the cursorIndex from 0 we want exactly to be after the last item in data.

    def goToItem(self, itemInstance):
        if itemInstance:
            self.head()
            while self.right():
                item = self.currentItem()
                if item is itemInstance:
                    return True
            else:
                raise ValueError("Item not in this block. ", self, itemInstance)
        elif itemInstance is None:
            self.tail()
        else:
            raise ValueError("You must go to an item or None, not to: ", itemInstance)


    def currentItem(self):
        """Can be used with goToItem"""
        if not self.isAppending():
            return self.data[self.localCursorIndex]
        else:
            return None

    def previousItem(self):
        """Can be used with goToItem"""
        if self.localCursorIndex > 0:
            return self.data[self.localCursorIndex-1]
        else:
            return None

    def nextItem(self):
        """Can be used with goToItem"""
        if self.localCursorIndex+1 >= len(self.data): #one before appending or appending itself
            return None
        else:
            return self.data[self.localCursorIndex+1]

    def insert(self, item):
        self.data.insert(self.localCursorIndex, item) #we do not need to check if appending or not. list.insert appends if the index is higher then len()
        #self.localCursorIndex += 1 #we don't need to go right here because track.insert() is calling its own right() directly after insert, which triggers block.right()
        item.parentBlocks.add(self)

    def delete(self):
        """The commented out is the immediate garbage collector which
        made sure that deleted item are not in the allNotes weakref
        dict any more. However, this does not happen any more since
        we introduced undo which keeps a saved version.
        For consitency reasons we chose not to save a copy in the undo
        register (return ...copy()) but the real item.

        If this ever leads to problems with the weakref dict we must
        reintroduce the copy"""
        if not self.isAppending():
            result = self.data[self.localCursorIndex]
            del self.data[self.localCursorIndex]
            #we don't need to delete self from item.parentBlocks. It is automatically deleted in all contentLinked blocks, of course including its parentBlocks WeakSet()
            return result

    def isAppending(self):
        return self.localCursorIndex == len(self.data) #len counts from 1 and the cursorIndex from 0. So if they are the same we are one cursor position right of last item

    def lilypond(self, carryLilypondRanges):
        """Called by track.lilypond(), returns a string.
        carryLilypondRanges is handed from item to item for ranges
        such as tuplets.
        Can act like a stack or simply remember stuff.
        """
        return " ".join(item.lilypond(carryLilypondRanges) for item in self.data)

