# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Name:         musedata.translate.py
# Purpose:      Translate MuseData into music21 objects
#
# Authors:      Christopher Ariza
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2010-2012 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
**N.B. in Dec. 2014 MuseData access was removed from music21 because the rights conflicted with
access computationally from music21.  This module is retained for anyone who has such access,
however it is completely untested now and errors cannot and will not be fixed.**


Functions for translating music21 objects and
:class:`~music21.musedata.base.MuseDataHandler` instances. Mostly,
these functions are for advanced, low level usage. For basic importing of MuseData
files from a file or URL to a :class:`~music21.stream.Stream`, use the music21
converter module's :func:`~music21.converter.parse` function.
'''
from __future__ import annotations

import unittest

from music21 import clef
from music21 import environment

environLocal = environment.Environment('musedata.translate')


# ------------------------------------------------------------------------------
def _musedataBeamToBeams(beamSymbol):
    '''
    Given a musedata beam symbol, converter to a music21 Beams object representation.

    >>> from music21.musedata import translate
    >>> translate._musedataBeamToBeams('[[')
    <music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>
    >>> translate._musedataBeamToBeams('===')
    <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam
            2/continue>/<music21.beam.Beam 3/continue>>

    >>> translate._musedataBeamToBeams(r']/')  # must escape backslash
    <music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/partial/right>>

    '''
    from music21 import beam

    beamsObj = beam.Beams()

    # assume that we are starting with 8th, even if given as a space
    for char in beamSymbol:
        direction = None
        if char == '[':
            beamType = 'start'
        elif char == ']':
            beamType = 'stop'
        elif char == '=':
            beamType = 'continue'
        elif char == '/':  # forward is right
            beamType = 'partial'
            direction = 'right'
        elif char in ('\\', r'\\'):  # backward is left
            beamType = 'partial'
            direction = 'left'
        else:
            environLocal.printDebug(['cannot interpret beams char:', char])
            continue
        # will automatically increment number
        # note that this does not permit defining 16th and not defining 8th
        beamsObj.append(beamType, direction)

    return beamsObj


def _musedataRecordListToNoteOrChord(records, previousElement=None):
    '''
    Given a list of MuseDataRecord objects, return a configured
    :class:`~music21.note.Note` or :class:`~music21.chord.Chord`.

    Optionally pass a previous element, which may be music21 Note, Chord, or Rest;
    this is used to determine tie status
    '''
    from music21 import note
    from music21 import chord
    from music21 import tie

    if len(records) == 1:
        post = note.Note()
        # directly assign pitch object; will already have accidental
        post.pitch = records[0].getPitchObject()
    else:
        # environLocal.printDebug(['attempting chord creation: records', len(records)])
        # can supply a list of Pitch objects at creation
        post = chord.Chord([r.getPitchObject() for r in records])

    # if a chord, we are assuming that all durations are the same
    post.quarterLength = records[0].getQuarterLength()

    # see if there are nay lyrics; not sure what to do if lyrics are defined
    # for multiple chord tones
    lyricList = records[0].getLyrics()
    if lyricList is not None:
        # cyclically calling addLyric will auto increment lyric number assigned
        for lyric in lyricList:
            post.addLyric(lyric)

    # see if there are any beams; again, get from first record only
    beamsChars = records[0].getBeams()
    if beamsChars is not None:
        post.beams = _musedataBeamToBeams(beamsChars)

    # get accents and expressions; assumes all on first
    # returns an empty list of None

    for a in records[0].getArticulationObjects():
        post.articulations.append(a)
    for e in records[0].getExpressionObjects():
        post.expressions.append(e)

    dynamicObjs = []  # stored in stream, not Note
    for d in records[0].getDynamicObjects():
        dynamicObjs.append(d)

    # presently this sets a single tie for a chord; may be different cases
    if records[0].isTied():
        post.tie = tie.Tie('start')  # can be start or continue;
        if previousElement is not None and previousElement.tie is not None:
            # if previous is a "start" or a "continue"; this has to be a "continue"
            # as musedata does not mark the end of a tie
            if previousElement.tie.type in ['start', 'continue']:
                post.tie = tie.Tie('continue')
    else:  # if no tie indication in the musedata record
        if previousElement is not None and previousElement.tie is not None:
            if previousElement.tie.type in ['start', 'continue']:
                post.tie = tie.Tie('stop')  # can be start, end, continue
    return post, dynamicObjs


def _processPending(hasVoices, pendingRecords, eLast, m, vActive):
    e, dynamicObjs = _musedataRecordListToNoteOrChord(pendingRecords, eLast)
    # place dynamics at same position as element
    if hasVoices:
        vActive.coreAppend(e)
        for d in dynamicObjs:
            vActive.coreInsert(e.getOffsetBySite(vActive), d)
    else:
        m.coreAppend(e)
        for d in dynamicObjs:
            m.coreInsert(e.getOffsetBySite(m), d)
    return e


def musedataPartToStreamPart(museDataPart, inputM21=None):
    '''
    Translate a musedata part to a :class:`~music21.stream.Part`.
    '''
    from music21 import stream
    from music21 import note
    from music21 import tempo

    if inputM21 is None:
        s = stream.Score()
    else:
        s = inputM21

    p = stream.Part()
    p.id = museDataPart.getPartName()
    p.partName = p.id

    # create and store objects
    mdmObjs = museDataPart.getMeasures()

    # environLocal.printDebug(['first measure parent', mdmObjs[0].parent])

    barCount = 0
    # get each measure
    # store last Note/Chord/Rest for tie comparisons; span measures
    eLast = None
    for mIndex, mdm in enumerate(mdmObjs):
        # environLocal.printDebug(['processing:', mdm.src])
        if not mdm.hasNotes():
            continue

        if mdm.hasVoices():
            hasVoices = True
            vActive = stream.Voice()
        else:
            hasVoices = False
            vActive = None

        # m = stream.Measure()
        # get a measure object with a left configured bar line
        if mIndex <= len(mdmObjs) - 2:
            mdmNext = mdmObjs[mIndex + 1]
        else:
            mdmNext = None

        m = mdm.getMeasureObject()

        # conditions for a final measure definition defining the last bar
        if mdmNext is not None and not mdmNext.hasNotes():
            # environLocal.printDebug(['got mdmNext not none and not has notes'])
            # get bar from next measure definition
            m.rightBarline = mdmNext.getBarObject()

        if barCount == 0:  # only for when no bars are defined
            # the parent of the measure is the part
            c = mdm.parent.getClefObject()
            if c is not None:
                m.clef = mdm.parent.getClefObject()
            m.timeSignature = mdm.parent.getTimeSignatureObject()
            m.keySignature = mdm.parent.getKeySignature()
            # look for a tempo indication
            directive = mdm.parent.getDirective()
            if directive is not None:
                tt = tempo.TempoText(directive)
                # if this appears to be a tempo indication, then get metro
                if tt.isCommonTempoText():
                    mm = tt.getMetronomeMark()
                    m.insert(0, mm)

        # get all records; may be notes or note components
        mdrObjs = mdm.getRecords()
        # store pairs of pitches and durations for making chords after a
        # new note has been found
        pendingRecords = []

        # get notes in each record
        for mdr in mdrObjs:
            # environLocal.printDebug(['processing:', mdr.src])

            if mdr.isBack():
                # the current use of back assumes tt back assumes tt we always
                # return to the start of the measure; this may not be the case
                if pendingRecords:
                    eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
                    pendingRecords = []

                # every time we encounter a back, we need to store
                # our existing voice and create a new one
                m.insert(0, vActive)
                vActive = stream.Voice()

            if mdr.isRest():
                # environLocal.printDebug(['got mdr rest, parent:', mdr.parent])
                # check for pending records first
                if pendingRecords:
                    eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
                    pendingRecords = []
                # create rest after clearing pending records
                r = note.Rest()
                r.quarterLength = mdr.getQuarterLength()
                if hasVoices:
                    vActive.coreAppend(r)
                else:
                    m.coreAppend(r)
                eLast = r
                continue
            # a note is note as chord, but may have chord tones
            # attached to it that follow
            elif mdr.isChord():
                # simply append if a chord; do not clear or change pending
                pendingRecords.append(mdr)

            elif mdr.isNote():
                # either this is a note alone, or this is the first
                # note found that is not a chord; if first not a chord
                # need to append immediately
                if pendingRecords:
                    # this could be a Chord or Note
                    eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)
                    pendingRecords = []
                # need to append this record for the current note
                pendingRecords.append(mdr)

        # check for any remaining single notes (if last) or chords
        if pendingRecords:
            eLast = _processPending(hasVoices, pendingRecords, eLast, m, vActive)

        # may be bending elements in a voice to append to a measure
        if vActive is not None and vActive:
            vActive.coreElementsChanged()
            m.coreInsert(0, vActive)

        m.coreElementsChanged()

        if barCount == 0 and m.timeSignature is not None:  # easy case
            # can only do this b/c ts is defined
            if m.barDurationProportion() < 1.0:
                m.padAsAnacrusis()
                # environLocal.printDebug(['incompletely filled Measure found on musedata import; ',
                #   'interpreting as a anacrusis:', 'paddingLeft:', m.paddingLeft])
        p.coreAppend(m)
        barCount += 1

    p.coreElementsChanged()
    # for now, make all imports a c-score on import;
    tInterval = museDataPart.getTranspositionIntervalObject()
    # environLocal.printDebug(['got transposition interval', p.id, tInterval])
    if tInterval is not None:
        p.flatten().transpose(tInterval,
                              classFilterList=['Note', 'Chord', 'KeySignature'],
                              inPlace=True)
        # need to call make accidentals to correct new issues
        p.makeAccidentals(inPlace=True)

    if museDataPart.stage == 1:
        # cannot yet get stage 1 clef data
        p.getElementsByClass(stream.Measure).first().clef = clef.bestClef(p, recurse=True)
        p.makeBeams(inPlace=True)
        # will call overridden method on Part
        p.makeAccidentals(inPlace=True)
    # assume that beams and clefs are defined in all stage 2

    s.insert(0, p)
    return s


def museDataWorkToStreamScore(museDataWork, inputM21=None):
    '''
    Given an museDataWork object, build into a multi-part
    :class:`~music21.stream.Score` with metadata.

    This assumes that this MuseDataHandler defines a single work
    (with 1 or fewer reference numbers).

    if the optional parameter inputM21 is given a music21 Stream subclass,
    it will use that object
    as the outermost object.  However, inner parts will always be
    made :class:`~music21.stream.Part` objects.
    '''
    from music21 import stream
    from music21 import metadata

    if inputM21 is None:
        s = stream.Score()
    else:
        s = inputM21

    # each musedata part has complete metadata, so must get first
    mdpObjs = museDataWork.getParts()

    md = metadata.Metadata()
    s.insert(0, md)

    md.title = mdpObjs[0].getWorkTitle()
    md.movementNumber = mdpObjs[0].getMovementNumber()
    md.movementName = mdpObjs[0].getMovementTitle()

    # not obvious where composer is stored
    # md.composer = mdpObjs[0].getWorkNumber()
    # md.localeOfComposition = mdpObjs[0].getWorkNumber()
    md.number = mdpObjs[0].getWorkNumber()

    for mdPart in mdpObjs:
        musedataPartToStreamPart(mdPart, s)
    return s


# ------------------------------------------------------------------------------
class Test(unittest.TestCase):

    def testBasic(self):
        from music21 import musedata
        from music21 import common

        fp1 = (common.getSourceFilePath()
                           / 'musedata' / 'testPrimitive' / 'test01' / '01.md')
        mdw = musedata.MuseDataWork()
        mdw.addFile(fp1)

        s = museDataWorkToStreamScore(mdw)
        # post = s.musicxml

        # s.show()
        self.assertEqual(len(s.parts), 1)

        self.assertEqual(s.parts[0].id, 'Clarinet in A')

        self.assertEqual(len(s.parts[0].recurse().notesAndRests), 54)


        # # try stage 1
        # mdw = musedata.MuseDataWork()
        # mdw.addString(testFiles.bachContrapunctus1_part1)
        # mdw.addString(testFiles.bachContrapunctus1_part2)
        #
        # s = museDataWorkToStreamScore(mdw)
        # self.assertEqual(len(s.parts[0].recurse().notesAndRests), 291)
        # self.assertEqual(len(s.parts[1].recurse().notesAndRests), 293)



    # def testGetMetaData(self):
    #
    #     from music21 import musedata
    #     from music21.musedata import testFiles


    # def testGetLyrics(self):
    #     from music21 import corpus
    #
    #     s = corpus.parse('hwv56', '1-08')
    #     self.assertEqual(len(s.parts), 2)
    #     self.assertEqual(s.parts[0].id, 'Contr\'alto')
    #     self.assertEqual(s.parts[1].id, 'Bassi')
    #
    #     self.assertEqual(len(s.parts[0].recurse().notesAndRests), 34)
    #     self.assertEqual(len(s.parts[1].recurse().notesAndRests), 9)
    #
    #     # note that hyphens are stripped on import
    #     self.assertEqual(s.parts[0].flatten().notesAndRests[2].lyric, 'Be')
    #     self.assertEqual(s.parts[0].flatten().notesAndRests[3].lyric, 'hold,')
    #     s.show()


    def testGetBeams(self):
        # try single character conversion
        post = _musedataBeamToBeams('=')
        self.assertEqual(str(post),
                         '<music21.beam.Beams <music21.beam.Beam 1/continue>>')

        post = _musedataBeamToBeams(']\\')
        self.assertEqual(
            str(post),
            '<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/partial/left>>')

        post = _musedataBeamToBeams(']/')
        self.assertEqual(
            str(post),
            '<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/partial/right>>')

        # s = corpus.parse('hwv56', '1-18')
        # self.assertEqual(len(s.parts), 5)
        # # the fourth part is vocal, and has no beams defined
        # self.assertEqual(str(s.parts[3].getElementsByClass(
        #     'Measure')[3].notesAndRests[0].beams), '<music21.beam.Beams >')
        # self.assertEqual(str(s.parts[3].getElementsByClass(
        #     'Measure')[3].notesAndRests[0].lyric), 'sud')
        #
        # # the bottom part has 8ths beamed two to a bar
        # self.assertEqual(str(s.parts[4].getElementsByClass(
        #     'Measure')[3].notesAndRests[0].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/start>>')
        # self.assertEqual(str(s.parts[4].getElementsByClass(
        #     'Measure')[3].notesAndRests[1].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/continue>>')
        # self.assertEqual(str(s.parts[4].getElementsByClass(
        #     'Measure')[3].notesAndRests[2].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/continue>>')
        # self.assertEqual(str(s.parts[4].getElementsByClass(
        #     'Measure')[3].notesAndRests[3].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/stop>>')
        #
        # # s.show()
        # # test that stage1 files continue to have makeBeams called
        # s = corpus.parse('bwv1080', '16')
        # # measure two has 9/16 beamed in three beats of 16ths
        # self.assertEqual(len(s.parts), 2)
        #
        # # s.parts[0].getElementsByClass(stream.Measure)[1].show()
        #
        # self.assertEqual(str(s.parts[0].getElementsByClass(
        #     'Measure')[1].notesAndRests[0].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>')
        # self.assertEqual(str(s.parts[0].getElementsByClass(
        #     'Measure')[1].notesAndRests[1].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>')
        # self.assertEqual(str(s.parts[0].getElementsByClass(
        #     'Measure')[1].notesAndRests[2].beams),
        #     '<music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/stop>>')


    def testAccidentals(self):
        '''
        testing a piece with 1 flat to make sure that sharps appear but normal B-flats do not.
        '''
        pass
        # s = corpus.parse('bwv1080', '16')
        # self.assertEqual(len(s.parts[0].getKeySignatures()), 1)
        # self.assertEqual(str(s.parts[0].getKeySignatures()[0]),
        #        '<music21.key.KeySignature of 1 flat>')
        #
        # notes = s.parts[0].flatten().notesAndRests
        # self.assertEqual(str(notes[2].accidental), '<music21.pitch.Accidental sharp>')
        # self.assertTrue(notes[2].accidental.displayStatus)
        #
        # # from key signature
        # # B-, thus no flat should appear.
        # self.assertEqual(str(notes[16].accidental), '<music21.pitch.Accidental flat>')
        # self.assertFalse(notes[16].accidental.displayStatus)
        #
        # # cautionary from within measure, the C follows a C#
        # notes = s.parts[1].measure(13).flatten().notesAndRests
        # self.assertEqual(str(notes[8].accidental), '<music21.pitch.Accidental natural>')
        # self.assertTrue(notes[8].accidental.displayStatus)

        # s.show()


    def testBackBasic(self):
        from music21 import converter
        from music21 import common
        fpDir = common.getSourceFilePath() / 'musedata' / 'testPrimitive' / 'test01'
        s = converter.parse(fpDir)
        # note: this is a multi-staff work, but presently gets encoded
        # as multiple voices
        measures = s.parts[0].measures(1, 5)
        self.assertEqual(len(measures[0].recurse().notesAndRests), 2)
        self.assertEqual(len(measures[1].recurse().notesAndRests), 5)
        self.assertEqual(len(measures[2].recurse().notesAndRests), 5)
        self.assertEqual(len(measures[3].recurse().notesAndRests), 6)
        self.assertEqual(len(measures[4].recurse().notesAndRests), 4)

        # s.show()

        # s.show()


    # def testMuseDataStage1A(self):
    #     from music21 import corpus
    #     s = corpus.parse('k168', 1)
    #
    #     self.assertEqual(len(s.parts), 4)
    #     self.assertEqual(str(s.parts[0][meter.TimeSignature].first()),
    #            '<music21.meter.TimeSignature 4/4>')
    #
    #     self.assertEqual(
    #        [n.offset for n in s.parts[0].getElementsByClass(stream.Measure)[0].notes],
    #        [0.0, 3.0, 3.5, 3.75])
    #
    #     self.assertEqual([n.nameWithOctave for
    #            n in s.parts[0].getElementsByClass(stream.Measure)[0].notes],
    #                ['F5', 'F5', 'E5', 'D5'])
    #
    #     self.assertEqual(
    #         [n.offset for n in s.parts[1].getElementsByClass(stream.Measure)[0].notes],
    #         [1.0, 2.0, 3.0])

    # def testMuseDataStage1B(self):
    #     from music21 import corpus
    #     s = corpus.parse('k169', 3)
    #
    #     self.assertEqual(len(s.parts), 4)
    #     self.assertEqual(str(s.parts[0][meter.TimeSignature].first()),
    #         '<music21.meter.TimeSignature 3/4>')
    #
    #     self.assertEqual(
    #         [n.offset for n in s.parts[0].getElementsByClass(stream.Measure)[0].notes],
    #            [0.0, 2.0])
    #
    #     self.assertEqual([n.nameWithOctave for
    #                n in s.parts[0].getElementsByClass(stream.Measure)[0].notes], ['A4', 'B4'])
    #
    #     self.assertEqual([n.offset
    #                       for n in s.parts[2].getElementsByClass(stream.Measure)[0].notes],
    #        [0.0, 1.0, 2.0])


    # def testMuseDataImportTempoA(self):
    #     from music21 import corpus
    #     # a small file
    #     s = corpus.parse('movement2-09.md')
    #     self.assertEqual(len(s.parts), 5)
    #     # the tempo is found in the 4th part here
    #     self.assertEqual(str(
    #         s.parts[3][tempo.TempoIndication].first()),
    #         '<music21.tempo.MetronomeMark Largo e piano Quarter=46>')
    #     # s.show()
    #
    #     s = corpus.parse('movement2-07.md')
    #     self.assertEqual(str(
    #         s[tempo.TempoIndication].first()),
    #         '<music21.tempo.MetronomeMark Largo Quarter=46>')

    # def testMuseDataImportDynamicsA(self):
    #     # note: this is importing a large work, but this seems to presently
    #     # be the only one with dynamics
    #
    #     # TODO: Turn back on when a smaller work is found
    #     from music21 import corpus
    #     s = corpus.parse('symphony94', 3)
    #     sFlat = s.flatten()
    #     # s.show()
    #     self.assertEqual(len(sFlat.getElementsByClass(dynamics.Dynamic)), 79)

    # def testMuseDataImportErrorA(self):
    #     from music21 import corpus
    #     # this files was crashing in the handling of an error in beam notation
    #     s = corpus.parse('haydn/opus55no1/movement2.md')
    #     self.assertEqual(len(s[note.Note]), 1735)
    #
    #     # s.show('t')

    # def testMuseDataImportErrorB(self):
    #     # this file has a malformed END repeated twice
    #     from music21 import corpus
    #     s = corpus.parse('haydn/opus71no1/movement1.zip')
    #     self.assertEqual(len(s[note.Note]), 2792)

# ------------------------------------------------------------------------------
# define presented order in documentation
_DOC_ORDER = [museDataWorkToStreamScore]

if __name__ == '__main__':
    # sys.arg test options will be used in mainTest()
    import music21
    music21.mainTest(Test)


