# -*- coding: ISO-8859-1 -*-
""" capellaScript -- (c) Paul Villiger
>>> Brillenschlangen

    Mit diesem Script lassen sich Tremoli oder sg. Brillenschlangen realisieren.
    Eingabemöglichkeiten:|
     - Anzahl der Balken|
     - Anzahle der langen Balken|
     - Halsrichtung nach oben/unten
     - Balken mit oder ohne Abstand zum Hals|
     - Balkenabstand zum Hals|
     - Faktor zwischen langen und kurzen Balken|
     - Alle Tremoli justieren|
     - Tremoli für Vorspiel auflösen
    Anwendung: von der ersten bis zur letzten Note markieren und Skript aufrufen.
    Wenn sich das Notenbild ändert, das Skript nochmals aufrufen.
    Die erste Note bestimmt die Dauer, alle folgenden Noten sind ohne Wert. Die Noten in der Mitte
    sind unsichtbar und dienen als Platzhalter. Die Dialogwerte werden gespeichert. Die Tremolibalken
    hängen am Halsende und können mit n + auf/ab verschoben werden. 
    
<<<

History:  21.04.04 - Erste Ausgabe
          22.04.04 - Anpassung über ganze Partitur möglich
          25.04.04 - neu Halsrichtung oben/unten implementiert
          25.05.04 - Capella Stepup 05, posX enthält jetzt auch shiftX
          25.06.04 - Fehler wenn Noten von rechts nach links markiert wurden
          29.08.07 - Script-Internationalisierung 
          15.09.09 - Brillenschlangen auflösen
          31.01.11 - geänderte Halslängen werden berücksichtigt
                     Halslängen bei Akkorden werden richtig berechnet
          01.02.11 - Einstellung maximale Balkensteigung
                   - Automatische Anpassung der Halslänge
                   - Halslängen zurücksetzen
          03.02.11 - Balken horizontal justiert

"""

german = ("de", {
    'beamCount'         :   'Anzahl Balken',
    'longBeamCount'     :   'Anzahl lange Balken',
    'beamAtStem'        :   'Balken am Notenhals',
    'stemDirUp'         :   'Halsrichtung nach oben',
    'distToStem'        :   'Abstand vom Notenhals',
    'factorBeam'        :   'Faktor lange/kurze Balken',
    'recalc1'           :   'Alle Elemente der ganzen Partitur\n\rneu berechnen',
    'recalc2'           :   'Wenn Noten verschoben werden, Skript erneut aufrufen',
    'dialogHeader'      :   '--- Brillenschlangen ---',
    'error'             :   'Fehler',
    'noActiveScore'     :   'keine aktive Partitur',
    'regUndo'           :   'Brillenschlangen',
    'expandAll'			:	'Brillenschlangen expandieren',
    'noLimit'           :   'keine Anpassung',
    'resetStem'         :   'Halslänge zurücksetzen',
    'maxInclination'    :   'Maximale Balkensteigung \n\r  (Notenzeilen/2)'
    })

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
import tempfile, string, new

# START SETTINGS ################

class settings:
    def __init__(self):
        self.stemDirUp = True
        self.atStem    = True 
        self.numBars   = 2
        self.numLongBars = 2
        self.factShortBars = 0.5
        self.stemDist    = 0.5
        self.changeAll   = False
        self.expandAll   = False
        self.maxInclination = 3
        
     
defaults = settings()
dlgSet = settings()

options = ScriptOptions() 
opt = options.get()


def getOptions():
    global dlgSet
    dlgSet.stemDirUp      = eval(opt.get('Brillenschlange_stemDirUp',    str(dlgSet.stemDirUp)))
    dlgSet.atStem         = eval(opt.get('Brillenschlange_atStem',       str(dlgSet.atStem)))
    dlgSet.stemDist       = eval(opt.get('Brillenschlange_stemDist',     str(dlgSet.stemDist)))
    dlgSet.numBars        = eval(opt.get('Brillenschlange_numBars',      str(dlgSet.numBars)))
    dlgSet.numLongBars    = eval(opt.get('Brillenschlange_numLongBars',  str(dlgSet.numLongBars)))
    dlgSet.factShortBars  = eval(opt.get('Brillenschlange_factShortBars',str(dlgSet.factShortBars)))
    dlgSet.maxInclination = eval(opt.get('Brillenschlange_maxInclination',str(dlgSet.maxInclination)))

def setOptions():
    global dlgSet
    opt.update(dict(Brillenschlange_stemDirUp    = str(dlgSet.stemDirUp),
                    Brillenschlange_atStem       = str(dlgSet.atStem),
                    Brillenschlange_stemDist     = str(dlgSet.stemDist),
                    Brillenschlange_numBars      = str(dlgSet.numBars),
                    Brillenschlange_numLongBars  = str(dlgSet.numLongBars),
                    Brillenschlange_factShortBars= str(dlgSet.factShortBars),
                    Brillenschlange_maxInclination= str(dlgSet.maxInclination)
                    ))
    options.set(opt)

# END SETTINGS ################

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 scriptDialog():
    global dlgSet

    getOptions()
    
    labelLeerzeile=Label('', width = 20)
    cboxBalken = ComboBox (['1','2','3','4'], value = dlgSet.numBars - 1 )
    cboxBalkenLang = ComboBox (['1','2','3','4'], value = dlgSet.numLongBars -1 )
    cboxAmHals = CheckBox('', value = dlgSet.atStem)
    cboxStemUp = CheckBox('', value = dlgSet.stemDirUp)
    editAbstand = Edit(str(dlgSet.stemDist))
    editFaktor = Edit(str(dlgSet.factShortBars))
    maxInclination = ComboBox([tr('resetStem'),tr('noLimit'), '0','1','2','3','4','5'], width = 17, value = dlgSet.maxInclination + 2)
    cboxAll = CheckBox('', value = False)
    expandAll = CheckBox('', value = False)    

    labelLeer  = Label('  ')    
    
    
    vbox = VBox([HBox([Label(tr('beamCount'), width = 25),          cboxBalken]),
                 HBox([Label(tr('longBeamCount'), width = 25),     cboxBalkenLang]),
                 HBox([Label(tr('stemDirUp'), width = 25), cboxStemUp]),
                 HBox([Label(tr('beamAtStem'), width = 25),    cboxAmHals]),
                 HBox([Label(tr('distToStem'),width= 25),    editAbstand]),
                 HBox([Label(tr('factorBeam'),width= 25), editFaktor]),
                 HBox([Label(tr('maxInclination'), width = 25), maxInclination ]),
                 labelLeer,
                 HBox([Label(tr('recalc1'), width = 25), cboxAll]),
                 labelLeer,
                 HBox([Label(tr('expandAll'), width= 25), expandAll]),
                 labelLeer,
                 Label(tr('recalc2')), 
                 labelLeer])
    
    dlg = Dialog(tr('dialogHeader'), vbox)
    if dlg.run():
        dlgSet.numBars     = cboxBalken.value() + 1
        dlgSet.numLongBars = cboxBalkenLang.value() + 1
        dlgSet.atStem      = cboxAmHals.value()
        dlgSet.stemDirUp   = cboxStemUp.value()
        dlgSet.stemDist    = eval(editAbstand.value())
        dlgSet.factShortBars = eval(editFaktor.value())
        dlgSet.changeAll   = cboxAll.value()
        dlgSet.expandAll   = expandAll.value()
        dlgSet.maxInclination = maxInclination.value() - 2

        if dlgSet.numLongBars > dlgSet.numBars:
            dlgSet.numLongBars = dlgSet.numBars
                
    setOptions()


def getCursor():
    sel = curSelection()
    result = None
    if sel == 0:
        messageBox( tr('error'), tr('noActiveScore') )
        return result
    result = sel
    return result

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 addElementNode(el,tagName):
    # add new Node to el if Node "tagName" does not exist
    # otherwise return the existing Node
    global doc
    childs = el.childNodes
    for n in range(childs.length):
        if childs[n].nodeType ==childs[n].ELEMENT_NODE and childs[n].tagName == tagName:
            return childs[n]
    newChild = doc.createElement(tagName)
    el.appendChild(newChild)
    return newChild
    
def addNewElementNode(el,tagName):
    # add new Node with tagName "tagName" to el 
    global doc
    newChild = doc.createElement(tagName)
    el.appendChild(newChild)
    return newChild

def getPositions(sel):
    [sy,st,vo,ob1],[sy2,st2,vo2, ob2] = sel
    system = activeScore().system(sy)
    staff = system.staff(st)
    voice = staff.voice(vo)
    pos1 = voice.noteObj(ob1).posX()
    pos2 = voice.noteObj(ob2-1).posX()

    return (pos1, pos2)    
    
def addBarLine(obj, x1, y1, dx, dy, d, e1, e2):
    # d = Distanz x zum Hals
    # e1/2 = Extradistanz x Hals1/2
    s = dy / dx   # Steigung
    px1 = x1 + d + e1
    py1 = y1 + px1 * s
    px2 = x1 + dx - d - e2
    py2 = y1 + px2 * s
    
    drawObj = addNewElementNode(obj,'drawObj')

    polygon = addNewElementNode(drawObj,'polygon')
    polygon.setAttribute('filled','true')
    polygon.setAttribute('lineWidth','0')

    basic = addNewElementNode(drawObj,'basic')
    basic.setAttribute('horizAlign','1')
    basic.setAttribute('vertAlign','3')

    points = addNewElementNode(polygon,'points')
            
    point = addNewElementNode(points,'point')
    point.setAttribute('x',str(px1))
    point.setAttribute('y',str(py1))
            
    point = addNewElementNode(points,'point')
    point.setAttribute('x',str(px1))
    point.setAttribute('y',str(py1 +0.5))
            
    point = addNewElementNode(points,'point')
    point.setAttribute('x',str(px2))
    point.setAttribute('y',str(py2+0.5))
            
    point = addNewElementNode(points,'point')
    point.setAttribute('x',str(px2))
    point.setAttribute('y',str(py2))

def setNoteAttributes(score, sel, stemUp):
    [sy,st,vo,ob1],[sy2,st2,vo2, ob2] = sel
    system = score.getElementsByTagName('system')[sy]
    staff = system.getElementsByTagName('staff')[st]
    voice = staff.getElementsByTagName('voice')[vo]
    noteObject = voice.getElementsByTagName('noteObjects')[0]
    objList = getElementObjects(noteObject.childNodes)
    dur1 = '1/1'
    for no in objList[ob1:ob2]:
        if no.tagName == 'chord':
            stem = addElementNode(no,'stem')
            if stemUp:
                stem.setAttribute('dir','up')
            else:
                stem.setAttribute('dir','down')

            duration = addElementNode(no,'duration')
            display = addElementNode(no,'display')
            if no == objList[ob1]:
                dur1 = duration.getAttribute('base')                
            else:
                duration.setAttribute('noDuration','true')
                display.setAttribute('postGrace','true')
                if no == objList[ob2-1]:
                    pass
                else:
                    display.setAttribute('invisible','true')
    return dur1

def getLengthening(chord):
    lengthening = 0.0
    stem = chord.getElementsByTagName('stem')
    if stem:
        if stem[0].hasAttribute('lengthening'):
            lengthening = float(stem[0].getAttribute('lengthening'))
    return lengthening

def addLengthening(chord, leng):
    stem = chord.gotoChild('stem')
    if stem.hasAttribute('lengthening'):
        lengthening = float(stem.getAttribute('lengthening'))
    else:
        lengthening = 0.0
    lengthening += leng
    stem.setAttribute('lengthening', str(lengthening))

def clearLengthening(chord):
    stem = chord.getElementsByTagName('stem')
    if stem:
        if stem[0].hasAttribute('lengthening'):
            stem[0].removeAttribute('lengthening')
    
def addDiatonic(pitch, offset):
    dp = string.find('CDEFGAB', pitch[0]) + 7 * int(pitch[1])
    dp += offset
    dpNew = 'CDEFGAB'[dp % 7] + str(dp / 7)
    return dpNew


def clef2CenterPitch(clef):
    clefs = {'bass':'F4','treble':'G2','alto':'C3','tenor':'C4'}
    cl = clefs.get(clef, clef)    
    clef2Pitch = {'G':'F6', 'C': 'B5', 'F': 'E5'}
    pitch = clef2Pitch[cl[0]]
    pitch = addDiatonic(pitch, - 2 * int(cl[1]))
    if len(cl) > 2:
        if cl[2] == '-':
            pitch[1] -= 1
        elif cl[2] == '+':
            pitch[1] += 1
    return pitch            

def setBarLines(score,sel):
    notenString='CDEFGAB'
    [sy,st,vo,ob1],[sy2,st2,vo2, ob2] = sel
    ob2 = ob2 - 1
    system = score.getElementsByTagName('system')[sy]
    staff = system.getElementsByTagName('staff')[st]
    voice = staff.getElementsByTagName('voice')[vo]
    noteObject = voice.getElementsByTagName('noteObjects')[0]
    objList = getElementObjects(noteObject.childNodes)
    obj1 = objList[ob1]
    obj2 = objList[ob2]

    # get clef offset
    clef1 = clef2 = 0
    for i in range(len(objList)):
        if objList[i].tagName == 'clefSign':
            cl = clef2CenterPitch( objList[i].getAttribute('clef'))
            off = notenString.find(cl[0]) + 7 * int(cl[1])
            if i < ob1:
                clef1 = off
            if i < ob2:
                clef2 = off
                
    if ob1 < ob2 and obj1.tagName == 'chord' and obj2.tagName == 'chord':
        (pos1, pos2) = getPositions(sel)
        heads1 = obj1.getElementsByTagName('head')
        heads2 = obj2.getElementsByTagName('head')
        if dlgSet.stemDirUp:
            pitch1 = heads1[heads1.length -1].getAttribute('pitch')
            pitch2 = heads2[heads2.length -1].getAttribute('pitch')
            lenDir = +1
        else:
            pitch1 = heads1[0].getAttribute('pitch')
            pitch2 = heads2[0].getAttribute('pitch')
            lenDir = -1
        off1 = notenString.find(pitch1[0]) + 7 * int(pitch1[1])
        off2 = notenString.find(pitch2[0]) + 7 * int(pitch2[1])

        # adjust to center line
        if dlgSet.stemDirUp:
            if clef1 - off1 > 7 :
                off1 = clef1 - 7
            if clef2 - off2 > 7 :
                off2 = clef2 - 7
        else:
            if off1 - clef1 > 7 :
                off1 = clef1 + 7
            if off2 - clef2 > 7 :
                off2 = clef2 + 7

        if dlgSet.maxInclination == -2:
            clearLengthening(obj1)
            clearLengthening(obj2)

        off1 += getLengthening(obj1) * 2.0 * lenDir
        off2 += getLengthening(obj2) * 2.0 * lenDir

        if dlgSet.maxInclination >= 0:
            maxDy = dlgSet.maxInclination
            if abs(off1 - off2) > maxDy:
                if dlgSet.stemDirUp:
                    if off1 > off2:
                        addLengthening(obj2, (off1 - off2 - maxDy) / 2.0 )
                        off2 = off1 - maxDy
                    elif off2 > off1:
                        addLengthening(obj1, (off2 - off1 - maxDy) / 2.0 )
                        off1 = off2 - maxDy
                else:
                    if off1 > off2:
                        addLengthening(obj1, (off1 - off2 - maxDy) / 2.0 )
                        off1 = off2 + maxDy
                    elif off2 > off1:
                        addLengthening(obj2, (off2 - off1 - maxDy) / 2.0 )
                        off2 = off1 + maxDy
                    
        dy = (off1 - off2) / 2.0
        dx = pos2 - pos1
    
        dur1= setNoteAttributes(score,sel,dlgSet.stemDirUp)
        if dur1 == '2/1':
            extraDist = 2.0
        elif dur1 =='1/1':
            extraDist = 1.4
        else:
            extraDist = 0
    
        if dlgSet.atStem:
            stemDist = 0
        else:
            stemDist = dlgSet.stemDist
        
        drawObjects = addElementNode(obj1,'drawObjects')
        for basic in drawObjects.getElementsByTagName('basic'):
            if basic.hasAttribute('tag') and '1793-' in basic.getAttribute('tag'):
                basic.parentNode.parentNode.removeChild(basic.parentNode)
        drawObj = addNewElementNode(drawObjects,'drawObj')
        group = addNewElementNode(drawObj,'group')
        basic = addNewElementNode(drawObj,'basic')

        # Die Formatinformation wird im zweiten Teil des Tags abgespeichert:
        # Einer Bit 0,1 = Anzahl Balken -1, Bit 2 = atStem
        # Zehner Bit 0,1 = Anzahl Langer Balken -1, Bit 2 = stemDirUp
        # Hunderter = Anzahl Noten
        tag = (dlgSet.numBars-1) + 4 * int(dlgSet.atStem) + 10 * (dlgSet.numLongBars-1) + 40 * int(dlgSet.stemDirUp) + 100 * (ob2 - ob1)
        
        basic.setAttribute('tag','1793-'+str(tag))

        if dlgSet.stemDirUp:
            for n in range(dlgSet.numBars):
                if n < dlgSet.numLongBars:
                    addBarLine(group, -0.05, n * 0.75, dx, dy, stemDist, 0, extraDist)
                else:
                    addDist = (dx - extraDist - 2.0 * stemDist) * (1-dlgSet.factShortBars) / 2.0
                    addBarLine(group, -0.05, n * 0.75, dx, dy, stemDist+addDist, 0, extraDist)
        else:    
            for n in range(dlgSet.numBars):
                if n < dlgSet.numLongBars:
                    addBarLine(group, +0.05, - n * 0.75 - 0.5, dx, dy, stemDist, extraDist, 0)
                else:
                    addDist = (dx - extraDist - 2.0 * stemDist) * (1-dlgSet.factShortBars) / 2.0
                    addBarLine(group, +0.05, - n * 0.75 - 0.5, dx, dy, stemDist+addDist, extraDist, 0)


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 expandBarLines(score,sel):
    notenString='CDEFGAB'
    [sy,st,vo,ob1],[sy2,st2,vo2, ob2] = sel
    ob2 = ob2 - 1
    system = score.getElementsByTagName('system')[sy]
    staff = system.getElementsByTagName('staff')[st]
    voice = staff.getElementsByTagName('voice')[vo]
    noteObject = voice.getElementsByTagName('noteObjects')[0]
    objList = getElementObjects(noteObject.childNodes)
    obj1 = objList[ob1]
    obj2 = objList[ob2]
    if ob1 < ob2 and obj1.tagName == 'chord' and obj2.tagName == 'chord':
        noteDuration = getDuration(obj1)
        heads1 = obj1.getElementsByTagName('heads')[0]
        heads2 = obj2.getElementsByTagName('heads')[0]
        singleDuration = Rational('1/4') / 2**dlgSet.numBars

        while noteDuration > 0:
            el1 = doc.createElement('chord')
            duration = el1.gotoChild('duration')
            duration.setAttribute('base',str(singleDuration))
            newHead = heads1.cloneNode(True)
            el1.appendChild(newHead)
            obj1.parentNode.insertBefore(el1,obj1)

            el1 = doc.createElement('chord')
            duration = el1.gotoChild('duration')
            duration.setAttribute('base',str(singleDuration))
            newHead = heads2.cloneNode(True)
            el1.appendChild(newHead)
            obj1.parentNode.insertBefore(el1,obj1)

            noteDuration -= 2 * singleDuration

        objects = objList[ob1:ob2+1]
        for ob in objects:
            ob.parentNode.removeChild(ob)



def changeDoc(score):
    global fonts, dlgSet

    scriptDialog()
    
    if not (dlgSet.changeAll or dlgSet.expandAll):
        sel = getCursor()
        if sel == None or sel[0] == sel[1] or sel[0][0:3] <> sel[1][0:3]:
            return
        if sel[0][3] > sel[1][3]:
            [a,b,c,d],[e,f,g,h] = sel
            sel = [e,f,g,h],[a,b,c,d]
        setBarLines(score,sel)

    elif dlgSet.expandAll:
        systems = score.getElementsByTagName('system')
        for sy in range(systems.length):
            staves = systems[sy].getElementsByTagName('staff')
            for st in range(staves.length):
                voices = staves[st].getElementsByTagName('voice')
                for vo in range(voices.length):
                    noteObject = voices[vo].getElementsByTagName('noteObjects')[0]
                    found = True
                    while found:
                        found = False
                        objList = getElementObjects(noteObject.childNodes)
                        for ob in range(objList.length):
                            sel = None
                            for basic in objList[ob].getElementsByTagName('basic'):
                                if basic.hasAttribute('tag') and '1793-' in basic.getAttribute('tag'):
                                    found = True
                                    num = eval(basic.getAttribute('tag')[5:])
                                    ob2 = ob + num / 100 + 1
                                    dlgSet.numBars = 1 + (num % 10) % 4
                                    dlgSet.numLongBars = 1 + ((num %100) / 10 ) % 4
                                    dlgSet.stemDirUp = num % 100 >= 40
                                    dlgSet.atStem = num % 10 >= 4
                                    sel = [sy, st, vo, ob],[sy, st, vo, ob2]
                                    expandBarLines(score,sel)
                                    
                                if found: break
                            if found: break
                                

            
    elif dlgSet.changeAll :
        systems = score.getElementsByTagName('system')
        for sy in range(systems.length):
            staves = systems[sy].getElementsByTagName('staff')
            for st in range(staves.length):
                voices = staves[st].getElementsByTagName('voice')
                for vo in range(voices.length):
                    noteObject = voices[vo].getElementsByTagName('noteObjects')[0]
                    objList = getElementObjects(noteObject.childNodes)
                    for ob in range(objList.length):
                        sel = None
                        for basic in objList[ob].getElementsByTagName('basic'):
                            if basic.hasAttribute('tag') and '1793-' in basic.getAttribute('tag'):
                                num = eval(basic.getAttribute('tag')[5:])
                                ob2 = ob + num / 100 + 1
                                dlgSet.numBars = 1 + (num % 10) % 4
                                dlgSet.numLongBars = 1 + ((num %100) / 10 ) % 4
                                dlgSet.stemDirUp = num % 100 >= 40
                                dlgSet.atStem = num % 10 >= 4
                                sel = [sy, st, vo, ob],[sy, st, vo, ob2]
                                setBarLines(score,sel)
            

            
# Hauptprogramm:

from caplib.capDOM import ScoreChange
import tempfile

class ScoreChange(ScoreChange):
    def changeScore(self, score):
        global 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)

    activeScore().read(tempOutput)
    os.remove(tempInput)
    os.remove(tempOutput)
            

