# -*- coding: ISO-8859-1 -*-
""" capellaScript -- 12.10.2005, Hartmut Lemmel
>>> Abbreviator

    Noten mit Abbreviaturbalken können in Einzelnoten zerlegt oder
    aus Einzelnoten erzeugt werden. Für die Erzeugung können 
    im Dialogfenster Notenanzahl und -wert der Einzelnoten eingestellt werden.
    Wahlweise wird nur der markierte Bereich verändert bzw.
    die Notengruppe hinter der Cursorposition. 
    
<<<

History:    12.10.2005 Erstausgabe
            15.03.2008 Berücksichtigung von 3-fach Punktierung (Vlg)
            03.11.2010 Fremdsprachen-Unterstützung B. Jungmann ohne 15.03.2008
            10.09.2017 Übersetzungen ausgebreitet und den Niederl. Text ein wenig geändert (WW)

"""

english = {
    'allSingle':'Split all tremolo bars into individual notes',
    'allAbbrev':'Combine individual notes to tremolo bars:',
    'groupsAt':'Groups of ',
    'join':'combine',
    'notes':'Notes',
    'allowRests':'Group may begin or end with rests',
    'allowNotes':'Also convert smaller groups',
    'markedOnly':'Only within the marked area',
    'indication':'Indication', ##(WW) zugefügt
    'pleaseDelete':'Please delete the superfluous rests at the end of the system.\n\n\
They had to be placed there in order to prevent capella from crashing (capella \
expects the marked area not to exceed the end of the system).' ##(WW) zugefügt
}
german = {
    'allSingle':'alle Abbreviaturen in Einzelnoten auflösen',
    'allAbbrev':'Einzelnoten zu Abbreviaturen zusammenfassen und zwar:',
    'groupsAt':'Gruppen zu ',
    'join':'zusammenfassen',
    'notes':'Noten im Werte von:',
    'allowRests':'Gruppe darf mit Pause beginnen oder enden',
    'allowNotes':'auch kleinere Gruppen umwandeln',
    'markedOnly':'nur im markierten Bereich',
    'indication':'Hinweis', ##(WW) zugefügt
    'pleaseDelete':'Bitte löschen Sie die überflüssigen Pausen am Zeilenende.\n\n\
Sie mussten erzeugt werden, um einen capella-Absturz zu verhindern (capella \
erwartet, dass der markierte Bereich nicht über das Zeilenende hinausragt).' ##(WW) zugefügt
}
french = {
    'allSingle':'Fractionner les barres de trémolos en notes individuelles',
    'allAbbrev':'Combiner des notes individuelles en trémolos:',
    'groupsAt':'Grouper par ',
    'join':'combiner',
    'notes':'Notes',
    'allowRests':'Groupe pouvant débuter ou se terminer avec un silence',
    'allowNotes':'Également convertir des petits groupes',
    'markedOnly':'Seulement dans la zone sélectionnée', 
    'indication':'Indication', ##(WW) zugefügt
    'pleaseDelete':'S.v.p. enlever les silences superflus à la fin du système.\n\n\
Elles devaient être placées là pour prévenir la chute de capella (capella ne \
permet pas que la zone sélectionnée dépasse la fin du système).' ##(WW) zugefügt
}
dutch = {
    'allSingle':'alle abbreviaturen in afzonderlijke noten omzetten',
    'allAbbrev':'afzonderlijke noten tot abbreviaturen combineren in :',
    'groupsAt':'groepen van ',
    'join':'combineren',
    'notes':'noten :',
    'allowRests':'groep mag met rust beginnen of eindigen',
    'allowNotes':'ook kleinere groepen omzetten',
    'markedOnly':'alleen in gemarkeerde gebied',
    'indication':'Aanwijzing', ##(WW) zugefügt
    'pleaseDelete':'Verwijder s.v.p. de overbodige rusten aan het eind van het systeem.\n\n\
Deze moesten daar geplaatst worden om te voorkomen dat capella crasht (capella verwacht \
dat het gemarkeerde blok het einde van het systeem niet zal overschrijden.)' ##(WW) zugefügt
}

try:
    setStringTable(
        ("en", english),
        ("de", german),
        ("fr", french),
        ("nl", dutch))
except:
    def tr(s):
        return german[s]


from caplib.capDOM import ScoreChange
import tempfile
from xml.dom.minidom import NodeList, Node, Element


def latin1_e(u):
    return u.encode('Latin-1')
def latin1_d(u):
    return u.decode('Latin-1')


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 another node 'tagName' to el
    newChild = doc.createElement(tagName)
    el.appendChild(newChild)
    return newChild    

def getElementNode(el,tagName):
    # find existing node 'tagName' in el
    # create new node if it doesn't exist yet
    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]
    return addElementNode(el,tagName)



def getAlteration(head):
    alt=0
    altelements=head.getElementsByTagName('alter')
    if len(altelements)>0:
      if altelements[0].hasAttribute('step'):
        alt=int(altelements[0].getAttribute('step'))
    return alt

def sameHeadPitch(head1,head2):
    return head1.getAttribute('pitch') == head2.getAttribute('pitch') and getAlteration(head1) == getAlteration(head2)

def sameChordPitch(chord1,chord2):
    heads1=chord1.getElementsByTagName('heads')[0].getElementsByTagName('head')
    heads2=chord2.getElementsByTagName('heads')[0].getElementsByTagName('head')
    if len(heads1) != len(heads2):
        return 0
    for h1 in heads1:
        found = 0
        for h2 in heads2:
            if sameHeadPitch(h1,h2):
                found=1
                break
        if found==0:
            return 0
    return 1

def getBaseDuration(chord):
    if chord.getElementsByTagName('duration').length == 0:
        return Rational('0')
    dur = chord.getElementsByTagName('duration')[0]
    return(Rational(str(dur.getAttribute('base'))))

def getTime(noteObj, attr):
    timestr=str(noteObj.getAttribute(attr))
    if timestr=='C':
        timestr='4/4'
    elif timestr=='allaBreve':
        timestr='4/4'
    return(Rational(timestr))

def getDuration(chord):
    if chord.getElementsByTagName('duration').length == 0:
        return Rational('0')
    dur = chord.getElementsByTagName('duration')[0]
    if dur.getAttribute('noDuration') == 'true':
        return Rational('0')        
    duration = Rational(str(dur.getAttribute('base')))
    for tuplet in dur.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
        duration = duration * factor
        
    if dur.hasAttribute('dots'):
        d = dur.getAttribute('dots')
        if d == '1':
            duration = duration * Rational('3/2')
        elif d == '2':
            duration = duration * Rational('7/4')
        elif d == '3':
            duration = duration * Rational('15/8')
    return duration

def abbreviate(chordlist,chords,deleteMe): # Noten in chordlist[] zu einer Note zusammenfassen
    global noteDuration,tremoloBars,noteCount,groupDuration,markedOnly,allowRests,allowNotes,newDuration,newDots, doc
    #        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
    mult = [0,1,2,2,4,0,4,4,8,0,0,0,8,0,8,0,16]
    dots = [0,0,0,1,0,0,1,2,0,0,0,0,1,0,2,0,0]
    dur=chordlist[0].getElementsByTagName('duration')[0]
    dur.setAttribute('base',str(noteDuration*mult[chords]))
    dur.setAttribute('dots',str(dots[chords]))
    stem=getElementNode(chordlist[0],'stem')
    stem.setAttribute('tremoloBars',tremoloBars)
    for c in chordlist[1:len(chordlist)]:
        drawObjects=c.getElementsByTagName('drawObjects')
        if drawObjects.length:
          for drawObj in drawObjects[0].getElementsByTagName('drawObj'):
            drawObjects=getElementNode(chordlist[0],'drawObjects')
            drawObjects.appendChild(drawObj.cloneNode(True))
        deleteMe.append(c)

def abbrev2notes(voice,ob1,ob2):
    global doc
    noteObjects=voice.getElementsByTagName('noteObjects')[0]
    ob = 0
    node = 0
    while node<len(noteObjects.childNodes):
      noteObj=noteObjects.childNodes[node]
      if noteObj.nodeType == noteObjects.ELEMENT_NODE:
        if ob>=ob1 and ob<ob2 and noteObj.nodeName == 'chord':
            stems=noteObj.getElementsByTagName('stem')
            if len(stems):
              abbr=0
              if stems[0].hasAttribute('tremoloBars'):
                  abbr=int(stems[0].getAttribute('tremoloBars'))
              if abbr!=0:
                  stems[0].removeAttribute('tremoloBars')
                  noteDurationStr='1/'+str(4 << abbr)
                  totalDuration=getDuration(noteObj)
                  noteDuration=Rational(noteDurationStr)
                  dur=noteObj.getElementsByTagName('duration')[0]
                  dur.setAttribute('base',noteDurationStr)
                  if dur.hasAttribute('dots'):
                      dur.removeAttribute('dots')
                  if dur.hasAttribute('tuplet'):
                      dur.removeAttribute('tuplet')
                  Duration=noteDuration
                  while Duration<totalDuration:
                      Duration+=noteDuration
                      newNoteObj = doc.createElement('chord')
                      if node+1<len(noteObjects.childNodes):
                          noteObjects.insertBefore(newNoteObj,noteObjects.childNodes[node+1])
                      else:
                          noteObjects.appendChild(newNoteObj)
                      dur=getElementNode(newNoteObj,'duration')
                      dur.setAttribute('base',noteDurationStr)
                      heads=getElementNode(noteObj,'heads')
                      newNoteObj.appendChild(heads.cloneNode(True))
                      ob2+=1
        ob += 1
      node += 1 


def notes2abbrev(voice,ob1,ob2,minobj):
    global noteDuration,tremoloBars,noteCount,groupDuration,allowRests,allowNotes,doc,msgshown
    measureDuration = getTime(voice.parentNode.parentNode,'defaultTime')
    noteObjects=voice.getElementsByTagName('noteObjects')[0]
    # messageBox('','measure='+str(measureDuration))
    # Notenobjekte durchsehen und in Frage kommende Objekte speichern. Am Ende einer
    # Notengruppe der verlangten Länge (groupDuration) prüfen, ob Umwandlung möglich ist.
    chordlist = NodeList() # Akkorde mit gefordertem Notenwert, alle gleich hoch
    chords = 0             # Anzahl der Akkorde in chordlist
    chordsDuration = 0     # Gesamtnotenwert der Akkorde in chordlist
    rest1Duration = 0      # Notenwert der vorangehenden Pausen
    rest2Duration = 0      # Notenwert der nachfolgenden Pausen
    totalDuration = 0      # Summe der vorigen drei
    deleteMe = NodeList()  # Liste der zu löschenden Objekte
    clear=0                # wird auf 1 gesetzt, sobald klar ist, dass die aktuelle Notengruppe nicht zusammengefasst werden kann

    ob = 0
    for noteObj in noteObjects.childNodes:
      if noteObj.nodeType == noteObjects.ELEMENT_NODE:                  
        if noteObj.nodeName == 'timeSign':
            measureDuration = getTime(noteObj,'time')
            totalDuration = 0
            clear=1
        elif noteObj.nodeName in ['barline', 'keySign']:
            totalDuration = 0
            clear=1
        elif noteObj.nodeName == 'rest':
            dur = getDuration(noteObj)
            if totalDuration==rest1Duration:
                rest1Duration += dur
            else:
                rest2Duration += dur
            totalDuration += dur
        elif noteObj.nodeName == 'chord':
            dur = getDuration(noteObj)
            if ob>=ob1 and ob<ob2 and clear==0:
                chordok=0
                if rest2Duration==0 and dur==noteDuration and len(noteObj.getElementsByTagName('tie'))==0:
                    if chords==0 or sameChordPitch(chordlist[0],noteObj):
                        chordok=1
                    elif allowNotes:
                        if chords>1:
                            abbreviate(chordlist,chords,deleteMe)
                        del chordlist[0:len(chordlist)]
                        chords = 0
                        chordsDuration = 0
                        rest1Duration = totalDuration
                        chordok=1
                if chordok:
                    chordlist.append(noteObj)                        
                    chords+=1
                    chordsDuration+=dur
                else:
                    if allowNotes==0:
                        clear=1
                    elif rest2Duration:
                        rest2Duration+=dur
                    else:
                        rest1Duration=totalDuration+dur
            totalDuration += dur
        if totalDuration>=groupDuration: # Gruppenende erreicht
            # messageBox('Found','noteDuration='+str(noteDuration)+'\ntotalDur='+str(totalDuration)+'\nchordDur='+str(chordsDuration)+'\nrest1Dur='+str(rest1Duration)+'\nrest2Dur='+str(rest2Duration)+'\nclear='+str(clear)+'\nchords='+str(chords))
            if chords>1 and chords<=16: 
              if clear==0:
               if allowRests or (rest1Duration+rest2Duration)==0:
                if totalDuration==groupDuration or rest2Duration!=0:
                  abbreviate(chordlist,chords,deleteMe)

            while totalDuration>=groupDuration:
                totalDuration-=groupDuration
                clear=1
        if clear:
                if len(chordlist):
                    del chordlist[0:len(chordlist)]
                chords = 0
                chordsDuration = 0
                rest1Duration = totalDuration
                rest2Duration = 0
                clear=0
        ob += 1
    for d in deleteMe:  # Noten löschen, die gelöscht werden müssen
            old = d.parentNode.removeChild(d)
            old.unlink()
            ob -= 1
    if ob < minobj:     # Mindestanzahl der Objekte herstellen, die der aktuellen Markierung entspricht
        while ob < minobj:                          # (sonst capella-Absturz)
            rest=addElementNode(noteObjects,'rest')
            dur =getElementNode(rest,'duration')
            dur.setAttribute('base',str(noteDuration))
            ob += 1
        if msgshown==0:
            messageBox(tr('indication'),tr('pleaseDelete')) 
            msgshown=1
    

class ScoreChange(ScoreChange):
  def changeScore(self, score):
    global markedOnly,action, doc, msgshown
    doc = score.parentNode
    msgshown=0

    (sy1, st1, vo1, ob1),(sy2, st2, vo2, ob2) = (0,0,0,0),(999,999,999,999)  # ganze Partitur

    minobj=0
    if markedOnly==1:
            sel = curSelection()
            if sel <> 0:
                (sy1,st1,vo1,ob1),(sy2,st2,vo2,ob2) = sel
                sel = (min(sy1,sy2), min(st1,st2),min(vo1,vo2),min(ob1,ob2)),(max(sy1,sy2), max(st1,st2),max(vo1,vo2),max(ob1,ob2))
                (sy1,st1,vo1,ob1),(sy2,st2,vo2,ob2) = sel                
                #messageBox('Markierung','sy = '+str(sy1)+' bis '+str(sy2)+'\nst = '+str(st1)+' bis '+str(st2)+'\nvo = '+str(vo1)+' bis '+str(vo2)+'\nob = '+str(ob1)+' bis '+str(ob2))
                if sy1!=sy2:  # mehrere Systeme markiert -> immer alle Zeilen pro System nehmen
                    st1=0
                    st2=999
                if st1!=st2:  # mehrere Zeilen markiert -> immer alle Stimmen einer Zeile nehmen
                    vo1=0
                    vo2=999
                if vo1!=vo2:  # mehrere Stimmen markiert -> immer alle Objekte einer Stimme nehmen
                    ob1=0
                    ob2=999
                    minobj=0
                if ob1!=ob2:  # Noten innerhalb einer Zeile markiert
                    minobj=ob2  # -> sicherstellen, dass minobj Objekt übrigbleiben, sonst capella-Absturz
                else: # nichts markiert, sel=Cursorposition 
                    if action==0:
                        ob2+=1  # Abbreviaturen -> Einzelnoten
                    else:
                        ob2 += noteCount # Einzelnoten -> Abbreviaturen 


                #messageBox('Markierung','sy = '+str(sy1)+' bis '+str(sy2)+'\nst = '+str(st1)+' bis '+str(st2)+'\nvo = '+str(vo1)+' bis '+str(vo2)+'\nob = '+str(ob1)+' bis '+str(ob2))
    sy = 0
    systems=score.getElementsByTagName('system')
    for system in systems:
     if sy1 <= sy <= sy2:
      st = 0
      for staff in system.getElementsByTagName('staff'):
       if st1 <= st <= st2:
        vo = 0
        for voice in staff.getElementsByTagName('voice'):
         if vo1 <= vo <= vo2:
             if action==0:
                 abbrev2notes(voice,ob1,ob2)
             else:
                 notes2abbrev(voice,ob1,ob2,minobj)               
         vo += 1
       st += 1
     sy += 1
         


        
            
def scriptDialog():
    global noteDuration,tremoloBars,noteCount,groupDuration,allowRests,allowNotes,markedOnly,action

    options = ScriptOptions() 
    opt = options.get()  

    radioAction = Radio([tr('allSingle'),tr('allAbbrev')], value=int(opt.get('Action','1')), width=60 )
    lableer=Label('           ')
    lab1=Label(tr('groupsAt'))
    listCount = ['2','3','4','6','8']
    listValue = ['1/8','1/16','1/32','1/64']
    radioCount = Radio(listCount,               value=int(opt.get('Count','0')), width=16 )
    radioValue = Radio(listValue, text=tr('notes'), value=int(opt.get('Value','0')), width=16 )
    lab2=Label(tr('join'))
    chxAllowRests = CheckBox(tr('allowRests') , value = int(opt.get('allowRests','1')))
    chxAllowNotes = CheckBox(tr('allowNotes') , value = int(opt.get('allowNotes','1')))
    chxMarkedOnly = CheckBox(tr('markedOnly') , value = int(opt.get('markedOnly','1')))
    
    dlg = Dialog('  -- Abbreviator --  ', VBox([chxMarkedOnly, radioAction, HBox([lableer,VBox([HBox([lab1,radioCount,radioValue],padding=8) , chxAllowRests, chxAllowNotes])])],padding=8) )

    if dlg.run():
        action = radioAction.value()
        noteDuration = Rational(listValue[radioValue.value()])
        tremoloBars=str(1+radioValue.value())
        noteCount = int(listCount[radioCount.value()])
        groupDuration= noteDuration * noteCount;
        allowNotes = chxAllowNotes.value()
        allowRests = chxAllowRests.value() or allowNotes
        markedOnly = chxMarkedOnly.value()
#        messageBox('Hinweis',str(noteCount)+' Noten zu je '+str(noteValue)+' = '+str(groupValue)+' mit '+str(tremoloBars)+' Tremolostrichen')
        opt.update(dict(Count=str(radioCount.value()), Value=str(radioValue.value()), allowRests=str(allowRests), allowNotes=str(allowNotes), markedOnly=str(markedOnly), Action=str(action)))
        options.set(opt)
        return True
    else:
        return False
        
# Hauptprogramm:

if activeScore():

    if scriptDialog():
    
        activeScore().registerUndo("Abbreviator")
        tempInput = tempfile.mktemp('.capx')
        tempOutput = tempfile.mktemp('.capx')
        activeScore().write(tempInput)
    
        ScoreChange(tempInput, tempOutput)
    
        activeScore().read(tempOutput)
        os.remove(tempInput)
        os.remove(tempOutput)

