# -*- coding: ISO-8859-1 -*-
""" capellaScript -- (c) Paul Villiger
>>> Tempomacher

    Mit diesem Script werden Tempoübergänge und Fermaten automatisch umgesetzt.
    Während der Fermate wird das Tempo um den gewünschten Faktor abgesenkt.||
    Für Tempoübergänge werden folgende Einfachtexte berücksichtigt:
    ritardando, ralentando, accelerando, sostenuto, stentnado, stentato, allargando, a tempo, atempo und poco, molto.||
    Nur Tempoangaben aus der obersten Stimme eines Systems werden berücksichtigt.
    ||    
    Rückmeldungen bitte an villpaul(a)bluewin.ch
<<<

History: 03.11.2009 - Erste Ausgabe
         06.11.2009 - ral/acc at end of score
                    - molto/poco tempo
         10.11.2009 - Kein Absturz bei mehrfach Tempi
                    - rit vor Fermate: Fermate wie a tempo behandeln, ohne Tempo stzem
                    - generell 2fach Tempo, damit auch bei Wiederholung wirksam
         14.12.2009 - Absturz bei leeren Textelementen
                    - Behandlung von stringendo und poco a poco
         29.03.2010 - Tempo nach Fermate wird nicht überschrieben
         31.03.2010 - ral acc vor Tempowechsel wird richtig behandelt
         02.04.2010 - beginTempo not initialized
         17.06.2010 - sost - sostenuto
         18.02.2011 - allargando
         14.11.2011 - Bei Tempozeichen mit Tempo wird interpoliert
         19.11.2011 - sten (stentato, stentando) wie ralentando
         20.01.2012 - Auswahl Notenzeile
         11.05.2012 - Einführung von Tempolabels
                    - Tempo global zurücksetzen
         15.05.2012 - Mehrfach Tempi bei Interpolationen, Fermaten
                    - Workaround: capella kann mehrere Systemtempis nicht behandeln
                    

"""
german = ("de", {
    'dialogHeader'      :   ' Tempomacher ',
    'regUndo'           :   'Tempomacher',
    'layoutSelect'      :   'Notenzeile:'
    } )

try:
    exec('from %s import translations' % ( translationModule() ))
    translations.append(german)
    setLanguages(translations)
except:
    def tr(s):
        return german[1].get(s, "???")
#-------------------------------------------------------------------
from xml.dom.minidom import parseString, NodeList, Node, Element
from caplib.rational import Rational
from caplib.capDOM import ScoreChange
import tempfile, string, new

def latin1(u):
    return u.encode('Latin-1')

doc = parseString('<score/>')

def gotoChild(self, name, new=False):
    newEl = None
    if new:
        pass
    else:
        for child in self.childNodes:
            if child.nodeType == child.ELEMENT_NODE and child.tagName == name:
                newEl = child
                break
    if newEl == None:
        newEl = doc.createElement(name)
        self.appendChild(newEl)
    return newEl
Node.gotoChild = new.instancemethod(gotoChild,None,Node)

def getElementObjects(objList):  # returns a List
    newList = NodeList()
    for n in range(objList.length):
        if objList[n].nodeType == objList[n].ELEMENT_NODE:
            newList.append(objList[n])
    return newList

def getDuration(note):
    """ Dauer einer Note oder Pause als rationale Zahl in ganzen Noten
        <note> muss ein 'chord'- oder 'rest'-Knoten sein
        (kopiert aus capDOM.py und angepasst)
    """
    duration = note.gotoChild('duration')
    if duration.getAttribute('noDuration') == 'true':
        return 0
    n = Rational(str(duration.getAttribute('base')))

    dots = duration.getAttribute('dots')
    if dots != '':
        if int(dots) == 1: n = (3 * n) / 2
        elif int(dots) == 2: n = (7 * n) / 4
        else: n = (15 * n) / 8

    tuplet = duration.gotoChild('tuplet')
    if tuplet.hasAttribute('count'):
        denominator = Rational(int(tuplet.getAttribute('count')))
        numerator = Rational(1)
        if tuplet.getAttribute('tripartite') == 'true':
           numerator = Rational(3,2)
        while numerator < denominator: numerator *= 2
        if tuplet.getAttribute('prolong') != 'true':
           numerator /= 2
        n = (n * numerator) / denominator

    return n

def getAllLayouts(score):
    # Gibt eine Liste zurueck mit allen Eintraegen aus dem Mustersystem,
    staffLayoutList = []
    for staffLayout in score.getElementsByTagName('staffLayout'):
        staffLayoutList.append(latin1(staffLayout.getAttribute('description')))

    return staffLayoutList

def hasFermate(noteObject):
    for drawObjects in noteObject.getElementsByTagName('drawObjects'):
        drwObjs = getElementObjects(drawObjects.childNodes)
        for drawObj in drwObjs:
            if drawObj.tagName == 'drawObj':
                for obj in getElementObjects(drawObj.childNodes):
                    if obj.tagName == 'text':
                        font = obj.gotoChild('font')
                        if 'capella' in font.getAttribute('face'):
                            for content in obj.getElementsByTagName('content'):
                                if latin1(content.firstChild.nodeValue) in ('u','k'):
                                    return True
                        else:
                            for content in obj.getElementsByTagName('content'):
                                text = latin1(content.firstChild.nodeValue).lower().strip()
                                if text[:3] == '@tm':
                                    text = text[3:].strip()
                                    if text[:4] == 'ferm':
                                        return True
                            
    return False


    
def handleFermate(score, factorFermate, staffLayoutName):

    for system in score.getElementsByTagName('system'):
        if system.hasAttribute('tempo'):
            staffTempo = system.getAttribute('tempo')
            currentTempo = staffTempo
        else:
            currentTempo = stafftempo = '120'

        returnToCurrentTempo = False

        for staff in system.getElementsByTagName('staff'):
            if staffLayoutName == staff.getAttribute('layout'):
                pass            # handle selected staff
            elif not staffLayoutName:
                pass            # only handle 1st staff
            else:
                continue        # check for next staff
            
            for voice in staff.getElementsByTagName('voice'):
                for no in voice.getElementsByTagName('noteObjects'):
                    child = no.firstChild
                    while child:
                        if child.nodeType == child.ELEMENT_NODE:
                            if child.tagName in ['chord','rest']:
                                if child.hasAttribute('tempo'):
                                    currentTempo = child.getAttribute('tempo')
                                    returnToCurrentTempo = False
                                elif hasFermate(child):
                                    t = multiplyTempo(currentTempo, str(100.0/factorFermate))
                                    child.setAttribute('tempo', t)
                                    returnToCurrentTempo = True
                                elif returnToCurrentTempo:
                                    returnToCurrentTempo = False
                                    child.setAttribute('tempo', currentTempo )
                            elif child.tagName in ['barLine']:
                                if child.hasAttribute('type'):
                                    # not simple barline
                                    pass
                        child = child.nextSibling
                        
                break   # handle only 1st voice
            break       # handle only 1st voice

def multiplyTempo(tempo, tempoFactor):
    tempoList = tempo.split()
    for i in range(len(tempoList)):
        tempoList[i] = str(eval(tempoList[i]) * eval(tempoFactor) / 100)
    newTempo = ''
    for t in tempoList:
        newTempo += ' ' + t
    newTempo = newTempo.strip()
    return newTempo

def handleTempoAssignement(score, staffLayoutName, tempoFactor):
    currentTempo = ''
    for system in score.getElementsByTagName('system'):
        if system.hasAttribute('tempo'):
            staffTempo = system.getAttribute('tempo')
        else:
            staffTempo = '120'
            
        firstObject = True
        if currentTempo:
            system.setAttribute('tempo', currentTempo)

        for staff in system.getElementsByTagName('staff'):
            if staffLayoutName == staff.getAttribute('layout'):
                pass            # handle selected staff
            elif not staffLayoutName:
                pass            # only handle 1st staff
            else:
                continue        # check for next staff

            for voice in staff.getElementsByTagName('voice'):
                for no in voice.getElementsByTagName('noteObjects'):
                    child = no.firstChild
                    while child:
                        if child.nodeType == child.ELEMENT_NODE:
                            if child.tagName in ['chord','rest']:
                                for drawObjects in child.getElementsByTagName('drawObjects'):
                                    drwObjs = getElementObjects(drawObjects.childNodes)
                                    for drawObj in drwObjs:
                                        if drawObj.tagName == 'drawObj':
                                            for obj in getElementObjects(drawObj.childNodes):
                                                if obj.tagName == 'text':
                                                    for content in obj.getElementsByTagName('content'):
                                                        text = latin1(content.firstChild.nodeValue).lower().strip()
                                                        if text[:3] == '@tm':
                                                            text = text[3:].strip()
                                                            valid = True
                                                            for c in text:
                                                                if c.isdigit():
                                                                    continue
                                                                if c == ' ':
                                                                    continue
                                                                valid = False

                                                            if valid:
                                                                text = multiplyTempo(text, tempoFactor)
                                                                child.setAttribute('tempo', text)
                                                                currentTempo = text
                                                                
                                                                if firstObject:
                                                                    system.setAttribute('tempo', currentTempo)

                                # workaround: set system tempo to 1st chord or rest
                                if firstObject and not child.hasAttribute('tempo') and currentTempo:
                                    child.setAttribute('tempo', currentTempo)
                                if firstObject and not currentTempo:
                                    child.setAttribute('tempo', staffTempo)
                                    currentTempo = staffTempo
                                    
                                    
                                firstObject = False
                                                                    
                                                            
                        child = child.nextSibling
                        
                break   # handle only 1st voice
            break       # handle only 1st voice
            

noteObjectList = []
collectNoteObjects = False

def interpolateTempo(beginTempo, endTempo):
    # messageBox('', '%s %s %s' % (beginTempo, endTempo, str(len(noteObjectList))))
    beginTempo = beginTempo.split()
    endTempo   = endTempo.split()

    endTempo += beginTempo[len(endTempo):]  # handle length diff
    
    totalDuration = Rational(0)
    for noteObject in noteObjectList:
        totalDuration += getDuration(noteObject)
    durationCount = Rational(0)
    for noteObject in noteObjectList:
        t = ''
        for i in range(len(beginTempo)):
            tmp = str( float(durationCount/totalDuration) * (float(endTempo[i]) - float(beginTempo[i])) + float(beginTempo[i]) )
            t += tmp + ' '
        t = t.strip()
        noteObject.setAttribute('tempo', t )
        durationCount += getDuration(noteObject)
        # messageBox('', '%s %s %s' % (t, str(durationCount), str(totalDuration)))
            

def getTempoSign(noteObject):
    for drawObjects in noteObject.getElementsByTagName('drawObjects'):
        drwObjs = getElementObjects(drawObjects.childNodes)
        for drawObj in drwObjs:
            if drawObj.tagName == 'drawObj':
                for obj in getElementObjects(drawObj.childNodes):
                    if obj.tagName == 'text':
                        for content in obj.getElementsByTagName('content'):
                            if not content.firstChild:
                                continue
                            text = latin1(content.firstChild.nodeValue).lower().strip()
                            if text[:3] == '@tm':
                                text = text[3:].strip()
                            if text[:3] in ['ral', 'rit']:
                                return 'ral', 1.0
                            if text[:4] in ['sost']:
                                return 'ral', 1.0
                            if text[:6] in ['allarg']:
                                return 'ral', 1.0
                            if text[:3] in ['acc']:
                                return 'acc', 1.0
                            if text[:6] in ['string']:
                                return 'acc', 1.0
                            if text[:4] in ['sten']:
                                return 'ral', 1.0
                            
                            if text[:3] in ['a t', 'ate']:  # a tempo
                                return 'atempo', 1.0
                            

                            if text[:5] == 'molto':
                                text = text[5:].strip()
                                if text[:3] in ['ral', 'rit']:
                                    return 'ral', 1.5
                                if text[:4] in ['sost']:
                                    return 'ral', 1.5
                                if text[:6] in ['allarg']:
                                    return 'ral', 1.5
                                if text[:3] in ['acc']:
                                    return 'acc', 1.5
                                if text[:6] in ['string']:
                                    return 'acc', 1.5
                                if text[:4] in ['sten']:
                                    return 'ral', 1.5

                                
                            if text[:11] == 'poco a poco':
                                text = text[7:]
                            if text[:4] == 'poco':
                                text = text[4:].strip()
                                if text[:3] in ['ral', 'rit']:
                                    return 'ral', 0.5
                                if text[:6] in ['allarg']:
                                    return 'ral', 0.5
                                if text[:4] in ['sost']:
                                    return 'ral', 0.5
                                if text[:3] in ['acc']:
                                    return 'acc', 0.5
                                if text[:6] in ['string']:
                                    return 'acc', 0.5
                                if text[:4] in ['sten']:
                                    return 'ral', 0.5
                                

    return '', 1.0

def resetTempo(score, tempo):
    for system in score.getElementsByTagName('system'):
        system.setAttribute('tempo', tempo)

    for chord in score.getElementsByTagName('chord'):
        if chord.hasAttribute('tempo'):
            chord.removeAttribute('tempo')
    for rest in score.getElementsByTagName('rest'):
        if rest.hasAttribute('tempo'):
            rest.removeAttribute('tempo')
        

def handleTempoMove(score, factorATempo, staffLayoutName):
    global noteObjectList, collectNoteObjects

    currentTempo = beginTempo = endTempo = '120'  # default

    aTempoFactor = -factorATempo   # default
    molto = 1.0
    
    returnToCurrentTempo = False

    for system in score.getElementsByTagName('system'):
        if system.hasAttribute('tempo'):
            staffTempo = system.getAttribute('tempo')
        else:
            staffTempo = '120'
            
        """
        # If the tempo changes at the beginning of a staff, the tempo interpolation is terminated
        if currentTempo <> staffTempo:
            endTempo = staffTempo
            if collectNoteObjects:
                interpolateTempo(beginTempo,endTempo)
                noteObjectList = []
                collectNoteObjects = False
                
        currentTempo = staffTempo
        """
        
        for staff in system.getElementsByTagName('staff'):
            if staffLayoutName == staff.getAttribute('layout'):
                pass            # handle selected staff
            elif not staffLayoutName:
                pass            # only handle 1st staff
            else:
                continue        # check for next staff
            
            for voice in staff.getElementsByTagName('voice'):
                for no in voice.getElementsByTagName('noteObjects'):
                    child = no.firstChild
                    while child:
                        if child.nodeType == child.ELEMENT_NODE:
                            if child.tagName in ['chord','rest']:
                                ts , molto = getTempoSign(child)
                                if ts in ['ral','acc']:
                                    if collectNoteObjects:
                                        endTempo = multiplyTempo(beginTempo, str(100 + aTempoFactor))
                                        interpolateTempo(beginTempo,endTempo)
                                        beginTempo = endTempo
                                    else:
                                        beginTempo = endTempo = currentTempo

                                    collectNoteObjects = True
                                    noteObjectList = []
                                    if ts == 'ral':
                                        aTempoFactor = -factorATempo * molto
                                    else:
                                        aTempoFactor = +factorATempo * molto
                                    returnToCurrentTempo = False

                                # Fermate is handled like a tempo
                                if hasFermate(child):
                                    if collectNoteObjects:
                                        endTempo = multiplyTempo(beginTempo, str(100 + aTempoFactor))
                                        interpolateTempo(beginTempo,endTempo)
                                        noteObjectList = []
                                        collectNoteObjects = False
                                        beginTempo = endTempo = currentTempo
                                        # t = str(currentTempo)
                                        # child.setAttribute('tempo', '%s %s' % (t,t) )
                                    returnToCurrentTempo = False


                                # The tempo interpolation terminates when a new tempo is assigned
                                if child.hasAttribute('tempo') and noteObjectList and currentTempo <> child.getAttribute('tempo'):
                                    currentTempo = child.getAttribute('tempo')
                                    _endTempo = _currentTempo = currentTempo.split()
                                    _beginTempo = beginTempo.split()
                                    _beginTempo = _beginTempo + _currentTempo[len(_beginTempo):]
                                    
                                    endTempo = ''
                                    for i in range(len(_currentTempo)):
                                        if (float(_currentTempo[i]) * 1.05 >= float(_beginTempo[i])) and (aTempoFactor > 0):
                                            _endTempo[i] = _currentTempo[i]
                                        elif (float(_currentTempo[i]) <= float(_beginTempo[i]) * 1.05) and (aTempoFactor < 0):
                                            _endTempo[i] = _currentTempo[i]
                                        else:
                                            _endTempo[i] = str(float(_beginTempo[i]) * (100 + aTempoFactor) / 100 )
                                        endTempo += _endTempo[i] + ' '
                                    endTempo = endTempo.strip()

                                    if collectNoteObjects:
                                        interpolateTempo(beginTempo,endTempo)
                                        noteObjectList = []
                                        collectNoteObjects = False
                                    returnToCurrentTempo = False
                                elif child.hasAttribute('tempo'):
                                    currentTempo = child.getAttribute('tempo')

                                # If "a tempo" found return to current tempo
                                if ts in ['atempo']:
                                    if collectNoteObjects:
                                        endTempo = multiplyTempo(beginTempo, str(100 + aTempoFactor))
                                        interpolateTempo(beginTempo,endTempo)
                                        noteObjectList = []
                                        collectNoteObjects = False
                                        beginTempo = endTempo = currentTempo
                                        child.setAttribute('tempo', currentTempo)
                                    returnToCurrentTempo = False
                                        
                                if returnToCurrentTempo:
                                    if child.hasAttribute('tempo'):
                                        pass
                                    else:
                                        child.setAttribute('tempo', currentTempo)
                                    returnToCurrentTempo = False

                                    

                                if collectNoteObjects:
                                    noteObjectList.append(child)
                                    
                            elif child.tagName in ['barline']:
                                if child.getAttribute('type') in ['repEnd','end','repEndBegin']:
                                    if collectNoteObjects:
                                        endTempo = multiplyTempo(beginTempo, str(100 + aTempoFactor))
                                        interpolateTempo(beginTempo,endTempo)
                                        noteObjectList = []
                                        collectNoteObjects = False
                                        returnToCurrentTempo = True

                        child = child.nextSibling
                        
                break   # handle only 1st voice
            break       # handle only 1st staff

    # check at end of score for open tempo sign
    if collectNoteObjects:
        endTempo = beginTempo * (100 + aTempoFactor) / 100
        interpolateTempo(beginTempo,endTempo)
        beginTempo = endTempo


def setTempoLabelVisibility(score, flag):
    for textNode in score.getElementsByTagName('text'):
        for content in textNode.getElementsByTagName('content'):
            text = latin1(content.firstChild.nodeValue).lower().strip()
            if text[:3] == '@tm':
                basic = textNode.parentNode.gotoChild('basic')
                if flag:
                    basic.setAttribute('visibilityFlags',flag)
                else:
                    if basic.hasAttribute('visibilityFlags'):
                        basic.removeAttribute('visibilityFlags')

                

def userInput(staffLayoutList):
    options = ScriptOptions() 
    opt = options.get()
    optCheckFermate = eval(opt.get('checkFermate','0'))
    optCheckTempo   = eval(opt.get('checkTempo','0'))
    optFactorFermate = opt.get('factorFermate','2.0')
    optFactorATempo = opt.get('factorATempo','20')
    optNewTempo = opt.get('newTempo', '120')
    optCheckTempoReset = eval(opt.get('checkTempoReset','0'))
    optCheckTempoAssign = eval(opt.get('checkTempoAssign', '0'))
    optTempoFactor = opt.get('tempoFactor', '100')

    checkTempoReset = CheckBox('Tempi global zurücksetzen', value = optCheckTempoReset)
    checkTempoAssign = CheckBox('Tempi aus Text übernehmen (@tm...)', value = optCheckTempoAssign)
    tempoFactor = Edit(optTempoFactor, width = 10)
    checkShowTempoLabels = CheckBox('Tempolabels anzeigen (exclusiv)', value = 0)
    checkHideTempoLabels = CheckBox('Tempolabels verbergen (exclusiv)', value = 0)
    
    
    newTempo = Edit(optNewTempo, width = 10)
    checkFermate = CheckBox('Fermaten verlängern', value = optCheckFermate)
    checkTempo   = CheckBox('Acc./Ral. interpolieren', value = optCheckTempo)
    factorFermate = Edit(optFactorFermate, width = 10)
    factorATempo = Edit(optFactorATempo, width = 10)
    layoutFilter = ComboBox(['Oberste Notenzeile'] + staffLayoutList, value = 0, width = 20)
    
    
    dlg = Dialog(tr('dialogHeader'),
                 VBox([checkTempoReset,
                       HBox([Label('Neues Tempo ganze Partitur:', width = 20), newTempo]),
                       checkTempoAssign,
                       HBox([Label('Faktor für Tempo in %:', width = 20), tempoFactor]),
                       checkFermate,
                       HBox([Label('Fermatenverlängerung:', width = 20), factorFermate]),
                       checkTempo,
                       HBox([Label('"a tempo" Variation in %:', width = 20), factorATempo]),
                       Label(' '),
                       HBox([Label('Auswahl Notenzeile:', width = 20), layoutFilter]),
                       Label(' '),
                       checkShowTempoLabels,
                       checkHideTempoLabels,
                       Label(' ')
                       ]))
    
    if dlg.run():
        ok = True
        checkTempoReset = checkTempoReset.value()
        checkTempoAssign = checkTempoAssign.value()
        tempoFactor = tempoFactor.value()
        checkShowTempoLabels = checkShowTempoLabels.value()
        checkHideTempoLabels = checkHideTempoLabels.value()
        
        newTempo = newTempo.value()
        checkFermate = checkFermate.value()
        checkTempo   = checkTempo.value()
        factorFermate = factorFermate.value()
        try:
            factorFermate = float(factorFermate)
        except:
            factorFermate = 2.0

        factorATempo = factorATempo.value()
        try:
            factorATempo = float(factorATempo)
        except:
            factorATempo = 20
        layoutFilter = layoutFilter.value()
        if layoutFilter == 0:
            staffLayoutName = False
        else:
            staffLayoutName = staffLayoutList[layoutFilter - 1]

        opt['checkFermate'] = checkFermate
        opt['checkTempo'] = checkTempo
        opt['factorFermate'] = factorFermate
        opt['factorATempo'] = factorATempo
        opt['newTempo'] = newTempo
        opt['checkTempoReset'] = checkTempoReset
        opt['checkTempoAssign'] = checkTempoAssign
        opt['tempoFactor'] = tempoFactor
        
        options.set(opt)

        return ok, checkFermate, checkTempo, factorFermate, factorATempo, staffLayoutName, checkTempoReset, newTempo, checkTempoAssign, tempoFactor, checkShowTempoLabels, checkHideTempoLabels
        
    else:
        return False, '', '', '', '', '', '', '', '', '', '', ''


handlingOk = True

def changeDoc(score):
    global handlingOk
    staffLayoutList = getAllLayouts(score)
    (ok, checkFermate, checkTempo, factorFermate, factorATempo, staffLayoutName, checkTempoReset, newTempo, checkTempoAssign, tempoFactor, checkShowTempoLabels, checkHideTempoLabels) = userInput(staffLayoutList)
    if ok:
        if checkShowTempoLabels:
            setTempoLabelVisibility(score, '')
            return
        if checkHideTempoLabels:
            setTempoLabelVisibility(score, '31')
            return
        
        if checkTempoReset:
            resetTempo(score, newTempo)
        if checkTempoAssign:
            handleTempoAssignement(score, staffLayoutName, tempoFactor)
        if checkFermate:
            handleFermate(score, factorFermate, staffLayoutName)
        if checkTempo:
            handleTempoMove(score, factorATempo, staffLayoutName)
    else:
        handlingOK = False

class ScoreChange(ScoreChange):

    def changeScore(self, score):
        global scriptAction, doc
        doc = score.parentNode
        changeDoc(score)
        
if activeScore():

    activeScore().registerUndo( tr('regUndo') )
    tempInput = tempfile.mktemp('.capx')
    tempOutput = tempfile.mktemp('.capx')
    activeScore().write(tempInput)

    ScoreChange(tempInput, tempOutput)

    if handlingOk:
        activeScore().read(tempOutput)
        
    os.remove(tempInput)
    os.remove(tempOutput)


