# -*- coding: ISO-8859-1 -*-
""" capellaScript -- (c) Paul Villiger
>>> 
    Pausenfueller

    Das Skript füllt zu kurze Notenzeilen mit Pausen. Der Bereich kann gewählt werden: ganze Partitur, System oder Stimme.
    Mit dem Skript kann auch eine neue Stimme, gefüllt mit Pausen, generiert werden. Tonart, Takt und Taktstriche werden
    von einer korrekten Notenzeile übernommen. Die letzte Einstellung wird gespeichert.
    
<<<

History:  15.02.06 - Erste Ausgabe
          21.02.06 - Einzelstimme bei Cursor auffüllen
                   - Schlüssel
          22.02.06 - gleiche Notenlinien mit der gleichen Anzahl Stimmen auffüllen
          23.02.06 - in neuer Stimme Auffüllen bis zum Cursor
          13.09.07 - Script-Internationalisierung
          03.11.07 - Ganztaktpausen und kurze Pausen zuerst
          15.03.08 - Berücksichtigung von 3-fach Punktierung
          12.12.11 - Fehler bei Berechnung der Punktierung

Bemerkung: Das Skript kann Notenzeilen mit fehlerhaften unregelmässigen Teilungen nicht korrekt auffüllen,
           z.B. eine Triole mit nur zwei Noten.

Funktionen:

    Notenzeilen mit Pausen ergänzen ...
    ... in der ganzen Partitur
    ... im aktuellen System
    ... in aktueller Notenzeile
    ... in aktueller Stimme
    ... in neuer Stimme unter Cursor    -->  erzeugt eine neue Stimme unter Cursor und füllt mit Pausen
    ... in neuer Stimme bis Cursor      -->  dto, füllt Pausen nur bis zum Cursor
    ... und Anzahl Stimmen Angleichen   -->  In allen gleichen Notenzeilen werden fehlende Stimmen
                                             mit Pausenstimmen ergänzt + Pausen werden in ganzer Partitur
                                             ergänzt.
"""

german = ("de", {
    'radioWholeScore'   :   '... in der ganzen Partitur',
    'radioCurrentSystem' :   '... im aktuellen System',
    'radioCurrentStaff'  :   '... in aktueller Notenzeile',
    'radioCurrentVoice'  :   '... in aktueller Stimme',
    'radioVoiceBelowCursor':'... in neuer Stimme unter Cursor',
    'radioVoiceUntilCursor':'... in neuer Stimme bis Cursor',
    'radioMissingVoices':   '... und Anzahl Stimmen Angleichen',
    'invisibleRest'     :   'Pausen unsichtbar',
    'dialogHeader'      :   'Mach mal eine Pause!',
    'dialogText'        :   'Notenzeilen mit Pausen ergänzen ...',
    'error'             :   'Fehler',
    'noActiveScore'     :   'keine aktive Partitur',
    'regUndo'           :   'Pausenfueller'
    })

try:
    exec('from %s import translations' % ( translationModule() ))
    translations.append(german)
    setLanguages(translations)
except:
    def tr(s):
        return german[1].get(s, "???")

#-------------------------------------------------------------------

import new
from xml.dom.minidom import NodeList, Node, Element
from xml.dom.minidom import parseString, NodeList, Node, Element
from string import find, replace, strip

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 getMeter(self):
    # bestimmt den Takt, welcher zu einem Notenobjekt gehört
    # self muss ein childNode von "noteObjects" sein
    obj = self
    meter = None
    while obj and meter == None:
        if obj.nodeType == obj.ELEMENT_NODE and obj.tagName == 'timeSign':
            meter = obj.getAttribute('time')
        obj = obj.previousSibling

    if meter == None:
        #            noteOjects voice      voices     staff
        staff = self.parentNode.parentNode.parentNode.parentNode
        meter = staff.getAttribute('defaultTime')

    return str(meter)
Node.getMeter = new.instancemethod(getMeter, None, Node)

def getChordDuration(self):
    # Bestimmt die Notendauer von chord oder rest
    # self muss ein childNode von "noteObjects" sein
    if not self or self.nodeType <> self.ELEMENT_NODE or self.tagName not in ['chord','rest']:
        return Rational('0')
    
    if self.getElementsByTagName('duration').length == 0:
        return Rational('0')
    duration = self.gotoChild('duration')
    if duration.hasAttribute('noDuration') and duration.getAttribute('noDuration') == 'true':
        return Rational('0')

    base = str(duration.getAttribute('base'))
    if '/' not in base:
        meter = self.getMeter()
        if meter in ['C','allaBreve', 'infinite']:
            meter = '4/4'
        dur =  Rational(meter) * Rational(base) 
    else:
        dur = Rational(base)
        
    for tuplet in duration.getElementsByTagName('tuplet'):
        if tuplet.hasAttribute('count'):
            count = tuplet.getAttribute('count')
        else:
            count = '1'
        if tuplet.hasAttribute('tripartite') and tuplet.getAttribute('tripartite') == 'true':
            #                   0     1     2     3     4     5     6     7     8     9     10     11     12      13
            factor = Rational(['1/1','1/1','3/4','2/3','3/4','3/5','3/6','6/7','6/8','6/9','6/10','6/11','12/12','12/13','12/14','12/15'][eval(count)])
        else:
            factor = Rational(['1/1','1/1','2/2','2/3','4/4','4/5','4/6','4/7','8/8','8/9','8/10','8/11','8/12', '8/13', '8/14', '8/15' ][eval(count)])
        if tuplet.hasAttribute('prolong') and tuplet.getAttribute('prolong') == 'true':
            factor = factor * 2
        dur = dur * factor
        
    if duration.hasAttribute('dots'):
        d = duration.getAttribute('dots')
        if d == '1':
            dur = dur * Rational('3/2')
        elif d == '2':
            dur = dur * Rational('7/4')
        elif d == '3':
            dur = dur * Rational('15/8')
    return dur
Node.getChordDuration = new.instancemethod(getChordDuration,None,Node)


def getVoiceDuration(voice):
    dur = Rational(0)
    obj = voice.gotoChild('noteObjects').firstChild
    while obj:
        dur += obj.getChordDuration()
        obj = obj.nextSibling

    return dur

def checkVoiceDuration(system):
    voiceList = []
    maxDur = Rational('0')
    minDur = Rational('9999')
    for voice in system.getElementsByTagName('voice'):
        dur = getVoiceDuration(voice)
        voiceList.append(voice)
        if dur > maxDur:
            maxDur = dur
            maxVoice = voice
        if dur < minDur:
            minDur = dur

    if maxDur <> minDur:
        return [maxVoice] + voiceList
    else:
        return []

def getVoiceCount(score):
    # bestimmt die maximale Anzahl von Stimmen in einem Staff
    voiceCount = {}
    for staff in score.getElementsByTagName('staff'):
        layout = staff.getAttribute('layout')
        voiceList = []
        for voice in staff.getElementsByTagName('voice'):
            voiceList.append(voice.getAttribute('stemDir'))
        if layout in voiceCount:
            if len(voiceCount[layout]) < len(voiceList):
                voiceCount[layout] = voiceList
        else:
            voiceCount[layout] = voiceList
            
    return voiceCount

def addEmptyVoices(score, voiceCount):
    for system in score.getElementsByTagName('system'):
        result = checkVoiceDuration(system)
        for staff in system.getElementsByTagName('staff'):
            layout = staff.getAttribute('layout')
            voices = staff.getElementsByTagName('voice')
            lenMax = len(voiceCount[layout])
            lenAct = len(voices)
            while lenAct < lenMax:
                voice = voices[0].parentNode.gotoChild('voice', new=True)
                stemDir = voiceCount[layout][lenAct-1]
                if stemDir:
                    voice.setAttribute('stemDir', stemDir)
                lenAct += 1
                
                if result:
                    refVoice = result[0]   # erste Stimme ist die längste
                else:
                    refVoice = voices[0]   # alle Stimmen sind gleich lang
                fillVoiceWithRest(refVoice, voice, fromStart = True, invisible = eval(dialogResult['invisible']))
                

def appendRest(voice, dur, currentMeter, invisible = False):
    restList = []
    d = dur
    while d >= currentMeter:
        restList.insert(0, '1')            # base = 1 --> whole rest
        d -= currentMeter

    for i in [2, 4, 8, 16, 32, 64, 128]:
        r = '1/' + str(i)
        if d >= Rational(r):
            restList.append(r)
            d -= Rational(r)
        if d == Rational('0'):
            break
    noteObjects = voice.gotoChild('noteObjects')
    restList.reverse()    # Liste umdrehen
    for r in restList:
        rest = noteObjects.gotoChild('rest', True)
        duration = rest.gotoChild('duration')
        duration.setAttribute('base', r)
        if r == '1':
            display = rest.gotoChild('display', True)
            display.setAttribute('churchStyle', 'true')
        if invisible:
            display = rest.gotoChild('display')
            display.setAttribute('invisible', 'true')


def fillVoiceWithRest(refVoice, voice2, fromStart = False, invisible = False, toObj = 0):
    dur2 = getVoiceDuration(voice2)
    obj = refVoice.gotoChild('noteObjects').firstChild
    dur = barDur = Rational(0)
    meter = obj.getMeter()
    if meter in ['C','allaBreve', 'infinite']:
        meter = '4/4'
    meter = Rational(meter)
    barline = False
    clefSign = False

    objCount = 0
    while obj:
        if obj.nodeType == obj.ELEMENT_NODE:
            d = obj.getChordDuration()             
            dur += d
            barDur += d

            if obj.tagName == 'timeSign':
                meter = obj.getAttribute('time')
                if meter in ['C','allaBreve', 'infinite']:
                    meter = '4/4'
                meter = Rational(str(meter))
                
                barline = True
            elif obj.tagName == 'keySign':
                barline = True
            elif obj.tagName == 'barline':
                barline = True
            elif obj.tagName == 'clefSign':
                clefSign = True

            if barDur >= meter or barline:
                barDur = Rational(0)
                if dur > dur2 and dur > Rational('0'):
                    deltaDur = dur - dur2
                    if deltaDur > Rational('0'):
                        appendRest(voice2, deltaDur, meter, invisible)
                        dur2 = dur

            if clefSign:
                if dur > dur2 and dur > Rational('0'):
                    deltaDur = dur - dur2
                    if deltaDur > Rational('0'):
                        appendRest(voice2, deltaDur, meter, invisible)
                        dur2 = dur
                
            if barline or clefSign:
                barline = clefSign = False
                if dur >= dur2 and (dur > Rational('0')or fromStart):
                    clone = obj.cloneNode(True)
                    voice2.gotoChild('noteObjects').appendChild(clone)

            objCount += 1
            if toObj and toObj <= objCount:  
                break
                    
        obj = obj.nextSibling

    if dur > dur2:
        deltaDur = dur - dur2
        appendRest(voice2, deltaDur, meter, invisible)
        
def getCursor():
    sel = curSelection()
    result = None
    if sel == 0:
        messageBox('Fehler', 'keine aktive Partitur')
        return result
    if sel[0] < sel[1]:
        result = sel[0]
    else:
        result = sel[1]
    return result

def changeDoc(score):
    sy1, st1, vo1, ob1 = getCursor()
                                                # Pausen ergänzen ....
    if dialogResult['selection'] in ['0','6']:          # in ganzer Partitur
        for system in score.getElementsByTagName('system'):
            result = checkVoiceDuration(system)
            if result:
                refVoice = result[0]
                for voice in result[1:]:
                    if voice <> refVoice:
                        fillVoiceWithRest(refVoice, voice, invisible = eval(dialogResult['invisible']))
                        
    elif dialogResult['selection'] == '1':        # in aktuellem System
        count = 0
        for system in score.getElementsByTagName('system'):
            if count == sy1:
                result = checkVoiceDuration(system)
                if result:
                    refVoice = result[0]
                    for voice in result[1:]:
                        if voice <> refVoice:
                            fillVoiceWithRest(refVoice, voice, invisible = eval(dialogResult['invisible']) )
            count += 1

    elif dialogResult['selection'] in ['2','3','4','5']:        # in aktueller Notenzeile oder in Neuer Stimme unter Cursor
        syCount = stCount = voCount = obCount = 0
        for system in score.getElementsByTagName('system'):
            if syCount == sy1:
                for staff in system.getElementsByTagName('staff'):
                    if stCount == st1:
                        for voice in staff.getElementsByTagName('voice'):
                            checkVoice = voice
                            fromStart = False
                            if voCount == vo1:
                                if dialogResult['selection'] in ['4','5']:         # in Neuer Stimme unter Cursor
                                    checkVoice = newVoice = doc.createElement('voice')
                                    next = voice.nextSibling
                                    if next:
                                        next.parentNode.insertBefore(newVoice, next)
                                    else:
                                        voice.parentNode.appendChild(newVoice)
                                    fromStart = True

                                result = checkVoiceDuration(system)
                                if result:
                                    if dialogResult['selection'] == '5':
                                        fillVoiceWithRest(voice, newVoice, fromStart, invisible = eval(dialogResult['invisible']), toObj=ob1 )
                                    else:
                                        refVoice = result[0]
                                        for voice1 in result[1:]:
                                            if voice1 <> refVoice and voice1 == checkVoice:
                                                fillVoiceWithRest(refVoice, voice1, fromStart, invisible = eval(dialogResult['invisible']) )
                                break
                            voCount += 1
                        break
                    stCount += 1
                break
            syCount += 1

    if dialogResult['selection'] == '6':
        voiceCount = getVoiceCount(score)
        addEmptyVoices(score, voiceCount)
                

def getDialog():
    options = ScriptOptions() 
    opt = options.get()

    
    selection = Radio([tr('radioWholeScore'),
                       tr('radioCurrentSystem'),
                       tr('radioCurrentStaff'),
                       tr('radioCurrentVoice'),
                       tr('radioVoiceBelowCursor'),
                       tr('radioVoiceUntilCursor'),
                       tr('radioMissingVoices')], value = eval( opt.setdefault('selection', '0')))
    
    invisible = CheckBox( tr('invisibleRest'), value = eval( opt.setdefault('invisible', 'True') ))
    dlg = Dialog( tr('dialogHeader'),
                 VBox([selection,
                       Label(' '),
                       invisible,
                       Label(' ')
                      ], text = tr('dialogText') )
                 )
    if dlg.run():
        result = {'selection':str(selection.value()),
                  'invisible':str(invisible.value())
                 }
        options.set(result)
                         
    else:
        result = {'selection':'-1',
                  'invisible':'False'
                 }
    return result
        

# Hauptprogramm:

from caplib.capDOM import ScoreChange
import tempfile

class ScoreChange(ScoreChange):
    def changeScore(self, score):
        changeDoc(score)


if activeScore():
    dialogResult = getDialog()
    if dialogResult['selection'] <> '-1':
        activeScore().registerUndo( tr('regUndo') )

        tempInput = tempfile.mktemp('.capx')
        tempOutput = tempfile.mktemp('.capx')
        activeScore().write(tempInput)
        
        ScoreChange(tempInput, tempOutput)

        activeScore().read(tempOutput)
        os.remove(tempInput)
        os.remove(tempOutput)

