# ------------------------------------------------------------------------------
# Name:          MeasureData.py
# Purpose:       MeasureData is an object somewhere between an m21 Measure
#                and a GridMeasure.
#
# Authors:       Greg Chapman <gregc@mac.com>
#                Humdrum code derived/translated from humlib (authored by
#                       Craig Stuart Sapp <craig@ccrma.stanford.edu>)
#
# Copyright:     (c) 2021-2022 Greg Chapman
# License:       MIT, see LICENSE
# ------------------------------------------------------------------------------
import sys
from typing import Union, List
#from fractions import Fraction

import music21 as m21
# from converter21.humdrum import HumdrumExportError
from converter21.humdrum import HumNum
from converter21.humdrum import MeasureStyle
from converter21.humdrum import EventData
from converter21.humdrum import Convert
from converter21.humdrum import M21Utilities
from converter21.humdrum import M21Convert

### For debug or unit test print, a simple way to get a string which is the current function name
### with a colon appended.
# for current func name, specify 0 or no argument.
# for name of caller of current func, specify 1.
# for name of caller of caller of current func, specify 2. etc.
# pylint: disable=protected-access
funcName = lambda n=0: sys._getframe(n + 1).f_code.co_name + ':'  #pragma no cover
# pylint: enable=protected-access

class SimultaneousEvents:
    def __init__(self):
        self.startTime : HumNum = HumNum(-1) # start time of events
        self.duration  : HumNum = HumNum(-1) # max duration of events?
        self.zeroDur   : [EventData] = []    # zero-duration events at this time
        self.nonZeroDur: [EventData] = []    # non-zero-duration events at this time

class MeasureData:
    def __init__(self, measure: m21.stream.Measure,
                       ownerStaff,
                       measureIndex: int,
                       prevMeasData): # prevMeasData: Optional[MeasureData]
        from converter21.humdrum import StaffData
        ownerStaff: StaffData
        self.m21Measure: m21.stream.Measure = measure
        self.ownerStaff: StaffData = ownerStaff
        self.spannerBundle = ownerStaff.spannerBundle # inherited from ownerScore, ultimately
        self._prevMeasData = prevMeasData
        self._measureIndex: int = measureIndex
        self._startTime: HumNum = HumNum(-1)
        self._duration: HumNum = HumNum(-1)
        self._timeSigDur: HumNum = HumNum(-1)
        # leftBarlineStyle describes the left barline of this measure
        self.leftBarlineStyle: MeasureStyle = MeasureStyle.Regular
        # _rightBarlineStyle describes the right barline of this measure
        self.rightBarlineStyle: MeasureStyle = MeasureStyle.Regular
        # _measureStyle is a combination of this measure's leftBarlineStyle and
        # the previous measure's rightBarlineStyle.  It's the style we use when
        # writing a barline ('=') token.
        self.measureStyle: MeasureStyle = MeasureStyle.Regular
        self._measureNumberString: str = ''
        self.events: [EventData] = []
        self.sortedEvents: [SimultaneousEvents] = [] # public list of startTime-binned events

        self._parseMeasure() # generates _events and then sortedEvents (also barlines)

    @property
    def measureIndex(self) -> int:
        return self._measureIndex

    @property
    def measureNumberString(self) -> str:
        return self._measureNumberString

    @property
    def staffIndex(self) -> int:
        return self.ownerStaff.staffIndex

    @property
    def staffNumber(self) -> int:
        return self.staffIndex + 1

    @property
    def partIndex(self) -> int:
        return self.ownerStaff.partIndex

    @property
    def partNumber(self) -> int:
        return self.partIndex + 1

    '''
    //////////////////////////////
    //
    // MxmlMeasure::getStartTime --
    '''
    @property
    def startTime(self) -> HumNum:
        return self._startTime

    '''
    //////////////////////////////
    //
    // MxmlMeasure::getDuration --
    '''
    @property
    def duration(self) -> HumNum:
        return self._duration

    '''
    //////////////////////////////
    //
    // MxmlMeasure::getTimeSigDur --
    '''
    @property
    def timeSigDur(self) -> HumNum:
        return self._timeSigDur

    '''
    //////////////////////////////
    //
    // MxmlMeasure::parseMeasure -- Reads music21 data for one staff's measure.
    '''
    def _parseMeasure(self):
        self._setStartTimeOfMeasure()
        self._duration = HumNum(self.m21Measure.duration.quarterLength)

        # m21 tracks timesigdur for us in barDuration, which is always the latest timeSig duration
        # But it can be ridiculously slow if it has to search back in the score for a timesignature,
        # so we only call it if the measure has a timesignature. If it doesn't have one, we
        # just use the _timeSigDur of the previous measure.
        if self.m21Measure.timeSignature is None and self._prevMeasData is not None:
            self._timeSigDur = self._prevMeasData.timeSigDur
        else:
            self._timeSigDur = HumNum(self.m21Measure.barDuration.quarterLength)

        self._measureNumberString = self.m21Measure.measureNumberWithSuffix()
        if self._measureNumberString == '0':
            self._measureNumberString = ''

        # In humdrum, measure style is a combination of this measure's left barline and the
        # previous measure's right barline.  (For example, ':|!|:' in a humdrum barline comes
        # from the previous right barline being an end-repeat (light-heavy) and the current
        # left barline being a start repeat (heavy-light)).
        # The very last humdrum barline (last measure's right barline) is handled as a special
        # case at a higher level, not here.

        # Compute our left and right barline style
        self.leftBarlineStyle = M21Convert.measureStyleFromM21Barline(self.m21Measure.leftBarline)
        self.rightBarlineStyle = M21Convert.measureStyleFromM21Barline(self.m21Measure.rightBarline)

        # Grab the previous measure's right barline style (if there is one) and
        # combine it with our left barline style, giving our measureStyle.
        prevRightMeasureStyle: MeasureStyle = MeasureStyle.Regular
        if self._prevMeasData is not None:
            prevRightMeasureStyle = self._prevMeasData.rightBarlineStyle
        self.measureStyle = M21Convert.combineTwoMeasureStyles(self.leftBarlineStyle,
                                                               prevRightMeasureStyle)

        # measure index 0 only: add events for any Instruments found in ownerStaff.m21PartStaff
        if self.measureIndex == 0:
            for elementIndex, inst in enumerate(
                    self.ownerStaff.m21PartStaff.getElementsByClass(m21.instrument.Instrument)):
                event: EventData = EventData(inst, elementIndex, -1, self)
                if event is not None:
                    self.events.append(event)

        if len(list(self.m21Measure.voices)) == 0:
            # treat the measure itself as voice 0
            self._parseEventsIn(self.m21Measure, 0)
        else:
            # first parse the voices...
            for voiceIndex, voice in enumerate(self.m21Measure.voices):
                emptyStartDuration: HumNum = HumNum(voice.offset)
                emptyEndDuration: HumNum = HumNum(self.duration - (HumNum(voice.offset) + HumNum(voice.duration.quarterLength)))
                self._parseEventsIn(voice, voiceIndex, emptyStartDuration, emptyEndDuration)

            # ... then parse the non-streams last, so that the ending barline lands after
            # any objects in the voices that are also at the last offset in the measure
            # (e.g. TextExpressions after the last note in the measure).
            self._parseEventsAtTopLevelOf(self.m21Measure)

        self._sortEvents()

    def _parseEventsIn(self, m21Stream: Union[m21.stream.Voice, m21.stream.Measure],
                             voiceIndex: int,
                             emptyStartDuration: HumNum = HumNum(0),
                             emptyEndDuration: HumNum = HumNum(0)):
        if emptyStartDuration > 0:
            # make m21 hidden rests totalling this duration, and pretend they
            # were at the beginning of m21Stream
            durations: List[HumNum] = Convert.getPowerOfTwoDurationsWithDotsAddingTo(emptyStartDuration)
            startTime: HumNum = self.startTime
            for duration in durations:
                m21StartRest: m21.note.Rest = m21.note.Rest(duration=m21.duration.Duration(duration))
                m21StartRest.style.hideObjectOnPrint = True
                event: EventData = EventData(m21StartRest, -1, voiceIndex, self, offsetInScore=startTime)
                if event is not None:
                    self.events.append(event)
                startTime += duration

        for elementIndex, element in enumerate(m21Stream.recurse()
                                                    .getElementsNotOfClass(m21.stream.Stream)):
            event: EventData = EventData(element, elementIndex, voiceIndex, self)
            if event is not None:
                self.events.append(event)
                # Make a separate event for any DynamicWedge (in score's
                #   spannerBundle) that starts with this element.
                #   Why?
                #       1. So we don't put the end of the wedge in the same slice as the
                #           endNote (wedges end at the end time of the endNote, not at
                #           the start time of the endNote).
                #       2. So wedge starts/ends will go in their own slice if necessary (e.g.
                #           if we choose not to export the voice-with-only-invisible-rests we
                #           may have made to position them correctly.
                extraEvents: List[EventData] = self._parseDynamicWedgesStartedOrStoppedAt(event)
                if extraEvents:
                    self.events += extraEvents

        if emptyEndDuration > 0:
            # make m21 hidden rests totalling this duration, and pretend they
            # were at the end of m21Stream
            durations: List[HumNum] = Convert.getPowerOfTwoDurationsWithDotsAddingTo(emptyEndDuration)
            startTime: HumNum = self.startTime + self.duration - emptyEndDuration
            for duration in durations:
                m21EndRest: m21.note.Rest = m21.note.Rest(duration=m21.duration.Duration(duration))
                m21EndRest.style.hideObjectOnPrint = True
                event: EventData = EventData(m21EndRest, -1, voiceIndex, self, offsetInScore=startTime)
                if event is not None:
                    self.events.append(event)
                startTime += duration

    def _parseDynamicWedgesStartedOrStoppedAt(self, event: EventData) -> List[EventData]:
        output: List[EventData] = []
        wedges: List[m21.dynamics.DynamicWedge] = (
                    M21Utilities.getDynamicWedgesStartedOrStoppedWithGeneralNote(
                        event.m21Object,
                        self.spannerBundle)
                )
        wedge: m21.dynamics.DynamicWedge
        for wedge in wedges:
            ownerScore = self.ownerStaff.ownerPart.ownerScore
            score: m21.stream.Score = ownerScore.m21Score
            startNote: m21.note.GeneralNote = wedge.getFirst()
            endNote: m21.note.GeneralNote = wedge.getLast()
            thisEventIsStart: bool = startNote is event.m21Object
            thisEventIsEnd: bool = endNote is event.m21Object
            wedgeStartTime: HumNum = None
            wedgeDuration: HumNum = None
            wedgeEndTime: HumNum = (HumNum(endNote.getOffsetInHierarchy(score))
                                        + HumNum(endNote.duration.quarterLength))
            if thisEventIsStart:
                wedgeStartTime = HumNum(startNote.getOffsetInHierarchy(score))
                wedgeDuration = HumNum(wedgeEndTime - wedgeStartTime)

            if thisEventIsStart and thisEventIsEnd:
                # print(f'wedgeStartStopEvent: {event}', file=sys.stderr)
                # one note for the wedge? let's make one event, so it can become '>]' or whatever
                wedgeStartStopEvent: EventData = EventData(wedge, -1, event.voiceIndex, self,
                                                           offsetInScore=wedgeStartTime,
                                                           duration=wedgeDuration)
                output.append(wedgeStartStopEvent)
            elif thisEventIsStart:
                # add the start event, with duration
                # print(f'wedgeStartEvent: {event}', file=sys.stderr)
                wedgeStartEvent: EventData = EventData(wedge, -1, event.voiceIndex, self,
                                                       offsetInScore=wedgeStartTime,
                                                       duration=wedgeDuration)
                output.append(wedgeStartEvent)
            elif thisEventIsEnd:
                # add the end event (duration == 0)
                # but add it to the same measure/voice as the wedge start event
                # print(f'wedgeStopEvent: {event}', file=sys.stderr)
                wedgeStartEvent: EventData = ownerScore.eventFromM21Object.get(wedge.id, None)
                if wedgeStartEvent is None:
                    # print('wedgeStop with no wedgeStart, putting it in its own measure/voice', file=sys.stderr)
                    endVoiceIndex: int = event.voiceIndex
                    endSelf = self
                else:
                    endVoiceIndex: int = wedgeStartEvent.voiceIndex
                    endSelf = wedgeStartEvent.ownerMeasure
                wedgeEndEvent: EventData = EventData(wedge, -1, endVoiceIndex, endSelf,
                                                     offsetInScore=wedgeEndTime,
                                                     duration=0)
                output.append(wedgeEndEvent)

        return output

    def _parseEventsAtTopLevelOf(self, m21Stream: m21.stream.Measure):
        for elementIndex, element in enumerate(m21Stream):
            if 'Stream' in element.classes:
                # skip substreams, just parse the top-level objects
                continue

            event: EventData = EventData(element, elementIndex, -1, self)
            if event is not None:
                self.events.append(event)

                extraEvents: List[EventData] = self._parseDynamicWedgesStartedOrStoppedAt(event)
                if extraEvents:
                    self.events += extraEvents

    '''
    //////////////////////////////
    //
    // MxmlMeasure::sortEvents -- Sorts events for the measure into
    //   time order.  They are split into zero-duration events and
    //   non-zero events.  mevent_floating type are placed into the
    //   non-zero events even though they have zero duration (this is
    //   for harmony not attached to a note attack, and will be
    //   eventually including basso continuo figuration having the
    //   same situation).
    '''
    def _sortEvents(self):
        self.events.sort(key=lambda event: event.startTime)
        times: [HumNum] = [] # sorted same as self.events (by time)
        for event in self.events:
            if not times or event.startTime != times[-1]: # don't add duplicated entries (like a set)
                times.append(event.startTime)

        self.sortedEvents = []
        for val in times:
            self.sortedEvents.append(SimultaneousEvents())
            self.sortedEvents[-1].startTime = val

        # setup sorted access:
        mapping: dict = {}
        for sortedEvent in self.sortedEvents:
            mapping[sortedEvent.startTime] = sortedEvent

        for event in self.events:
            startTime: HumNum = event.startTime
            duration: HumNum = event.duration
            if duration != 0 or event.isDynamicWedgeStartOrStop:
                # We treat dynamicWedge start/stop events as having duration even though
                # the stop events do not.  This is so that they can go in the same
                # slice with notes/rests, or on their own slice if they have a unique
                # timestamp.
                mapping[startTime].nonZeroDur.append(event)
            else:
                mapping[startTime].zeroDur.append(event)

        # debugging
        '''
        for e in self.sortedEvents:
            if len(e.zeroDur) > 0:
                print(e.startTime, 'z\t', end='', file=sys.stderr)
                for z in e.zeroDur:
                    print(' ', z.name, end='', file=sys.stderr)
                    print('(', z.partIndex, end='', file=sys.stderr)
                    print(',', z.staffIndex, end='', file=sys.stderr)
                    print(',', z.voiceIndex, end='', file=sys.stderr)
                    print(')', end='', file=sys.stderr)
                print('', file=sys.stderr)

            if len(e.nonZeroDur) > 0:
                print(e.startTime, 'n\t', end='', file=sys.stderr)
                for nz in e.nonZeroDur:
                    print(' ', nz.name, end='', file=sys.stderr)
                    print('(', nz.partIndex, end='', file=sys.stderr)
                    print(',', nz.staffIndex, end='', file=sys.stderr)
                    print(',', nz.voiceIndex, end='', file=sys.stderr)
                    print(')', end='', file=sys.stderr)
                print('', file=sys.stderr)
        '''
    '''
    //////////////////////////////
    //
    // MxmlMeasure::setStartTimeOfMeasure --
    '''
    def _setStartTimeOfMeasure(self):
        if self.ownerStaff is None:
            self._startTime = HumNum(0)
            return

        if self._prevMeasData is None:
            self._startTime = HumNum(0)
            return

        self._startTime = self._prevMeasData.startTime + self._prevMeasData.duration

    '''
    //////////////////////////////
    //
    // MxmlMeasure::reportEditorialAccidentalToOwner --
        The RDF signifier is returned, so we know what to put in the token
    //
    '''
    def reportEditorialAccidentalToOwner(self, editorialStyle: str) -> str:
        return self.ownerStaff.reportEditorialAccidentalToOwner(editorialStyle)

    '''
    //////////////////////////////
    //
    // MxmlMeasure::reportCaesuraToOwner --
        The RDF signifier is returned, so we know what to put in the token
    '''
    def reportCaesuraToOwner(self) -> str:
        return self.ownerStaff.reportCaesuraToOwner()

    def reportLinkedSlurToOwner(self) -> str:
        return self.ownerStaff.reportLinkedSlurToOwner()

    '''
    //////////////////////////////
    //
    // MxmlMeasure::reportDynamicToOwner --
    '''
    def reportDynamicToOwner(self):
        self.ownerStaff.receiveDynamic()
