# -*- coding: ISO-8859-1 -*-
""" capellaScript -- Bernd Jungmann
>>> Capella-Datei anhängen.

Vor dem Anhängen werden die Mustersysteme 
der Basispartitur und der anzuhängenden 
Datei in einer Dialogbox einander 
gegenübergestellt und können interaktiv 
aufeinander abgestimmt werden.|
|
Version 3: Bernd Jungmann 29.4.05|
<<<
Version 3: Bernd Jungmann 29.4.05
Unterstützung für Nicht-ASCII-Zeichen (äöüßé...) in der Mustersystem-Beschreibung
Version 2: Bernd Jungmann 20.4.05:
Unterstützung für metafile-Objekte

Version 1: Bernd Jungmann 18.4.05|
"""

import zipfile, tempfile, os, xml.dom.minidom

# Falls capella mal lernt, bei den Mustersystembezeichnungen auch
# Türkisch oder Kyrillisch zu akzeptieren, kann man 'Latin-1'
# entsprechend ändern. Bis dahin bleibts konstant.
def encode(u):
    return u.encode('Latin-1')
def decode(u):
    return u.decode('Latin-1')

def getFirstChildElement(el, childTag):
    for n in el.childNodes:
        if n.nodeType == n.ELEMENT_NODE and n.tagName == childTag:
            return n
    return None

def mkNameUnique(staves, desc1, ItemNo0, dlgList1):
    StaffLayouts = staves.getElementsByTagName('staffLayout')
    doagain = True
    changed = False
    while doagain:
        # zunächst die bisher angelegten StaffLayouts überprüfen
        # (die können von der Wunschliste abweichen, weil schon Namensänderungen
        # vorgenommen werden mussten)
        doagain = False
        for StaffLayout in StaffLayouts:
            descr = encode(StaffLayout.getAttribute('description'))
            if descr == desc1[0]:
                desc1[0] = desc1[0] + "1"
                doagain = True
                changed = True
                break
        # dann die verbliebenen Items in der Namenswunschliste überprüfen
        ItemMax = len(dlgList1)
        ItemNo = ItemNo0
        while ItemNo < ItemMax:
            hbox = dlgList1[ItemNo]
            ItemNo += 1
            if hbox != ' ':
                # descriptor-String für Basisdatei (ggfls. geändert!)
                ed,lab, edDesc = hbox['content']
                desc = edDesc['value']
                if desc == desc1[0]:
                    desc1[0] += "1"
                    doagain = True
                    changed = True
                    break
    return changed
                        
# ändere das Mustersystem entsprechend den Vorgaben in NewLayoutlist
def UpdateLayout(text, Layoutlist, dlgList1, text2, dlgList2, mapDescr1, mapDescr2):
    layout = getFirstChildElement(text, 'layout')
    layout2 = getFirstChildElement(text2, 'layout')
    if layout != None:
        staves = getFirstChildElement(layout, 'staves')
        staffLayouts = staves.getElementsByTagName('staffLayout')
        mapIndex1 = len(staffLayouts)*[-1]
        staves2 = getFirstChildElement(layout2, 'staves')
        staffLayouts2 = staves2.getElementsByTagName('staffLayout')
        mapIndex2 = len(staffLayouts2)*[-1]
        newstaves = text.parentNode.createElement('staves')
        # dlgList1 durchgehen
        ItemNo = 0
        ItemCount = len(dlgList1)
        newindex = 0
        while ItemNo < ItemCount:
            # ' ' war das letzte Element der Liste, damit sie ins Fenster passt!
            hbox = dlgList1[ItemNo]
            if hbox != ' ':
                # descriptor-String für Basisdatei (ggfls. geändert!)
                ed,lab, edDesc = hbox['content']
                edval = int(ed.value())
                desc1 = edDesc['value']
                # descriptor-String für anzuhängende Datei
                hbox2 = dlgList2[ItemNo]
                ed2,lab2,edDesc2 = hbox2['content']
                descstr2 = lab2['text']
                desc2 = descstr2[descstr2.rfind('|')+2:]
                oldindexstr2 = descstr2[0:descstr2.find('.')]
                if desc1 != "--":
                    # das staffLayout für diese Zeile von der Basis-Datei übernehmen, ggfls. Namen ändern
                    oldindexstr = lab['text']
                    oldindex = int(oldindexstr[0:oldindexstr.find('.')])
                    staffLayoutIndex = oldindex-1
                    newStaffLayout = staffLayouts[staffLayoutIndex].cloneNode(True)
                    descr = encode(newStaffLayout.getAttribute('description'))
                    if descr != desc1:
                        # Eindeutigkeit des geänderten descriptor-Strings sicherstellen!
                        arr = [desc1]
                        if mkNameUnique(newstaves, arr, len(dlgList1), dlgList1):
                            messageBox("Manuell geänderter Name nicht eindeutig","automatisch umbenannt "+desc1+" -> "+arr[0])
                            desc1 = arr[0]
                        newStaffLayout.setAttribute('description',decode(desc1))
                    newstaves.appendChild(newStaffLayout)
                    mapIndex1[staffLayoutIndex] = newindex
                    mapDescr1[descr] = desc1
                    if descstr2 != "---":
                        mapDescr2[desc2] = desc1
                        staffLayoutIndex2 = int(oldindexstr2)-1
                        mapIndex2[staffLayoutIndex2] = newindex
                    newindex += 1
                elif descstr2 != "---":
                    # das staffLayout für diese Zeile von der anzuhängenden Datei übernehmen
                    staffLayoutIndex2 = int(oldindexstr2)-1
                    newStaffLayout = staffLayouts2[staffLayoutIndex2].cloneNode(True)
                    arr = [desc2]
                    if mkNameUnique(newstaves, arr, ItemNo, dlgList1):
                        # der Name muss modifiziert werden
                        messageBox("Name aus anzuhängender Datei nicht eindeutig","automatisch umbenannt "+desc2+" -> "+arr[0])
                        desc1 = arr[0]
                        newStaffLayout.setAttribute('description', decode(desc1))
                    else:
                        # der Name kann unverändert übernommen werden
                        desc1 = desc2
                    mapDescr2[desc2] = desc1
                    newstaves.appendChild(newStaffLayout)
                    mapIndex2[staffLayoutIndex2] = newindex
                    newindex += 1
                else:
                    # in beiden Dateien gibt's keinen Eintrag für diese Zeile.
                    # Also nicht ins Mustersystem übernehmen.
                    pass
            ItemNo += 1
        layout.replaceChild(newstaves, staves)
        staves.unlink()
        # Falls Klammern existieren, Indices den ggfls. verschobenen Zeilennummern anpassen
        brackets2 = layout2.getElementsByTagName('bracket')
        brackets = layout.getElementsByTagName('bracket')
        if len(brackets2) > 0:
            # die anzuhängende Datei hat Klammern.
            bracketsNode2 = getFirstChildElement(layout2, 'brackets')
            if len(brackets) == 0:
                # Die Basispartitur hat keine Klammern
                # Klone das brackets-Element
                newbracketsNode = bracketsNode2.cloneNode(True)
                # newbracketsNode soll hinter staves reingehängt werden.
                # es gibt aber nur appendChild (hängt ans Ende) oder insertBefore
                # deshalb hänge ich's vor spacing rein.
                spacing = getFirstChildElement(layout, 'spacing')
                layout.insertBefore(newbracketsNode, spacing)
                brackets = layout.getElementsByTagName('bracket')
                # jetzt noch für die Klammern der Basispartitur die Indices anpassen
                for bracket in brackets:
                    old = int(bracket.getAttribute('from'))
                    if old < len(mapIndex2):
                        bracket.setAttribute('from',str(mapIndex2[old]))
                    old = int(bracket.getAttribute('to'))
                    if old < len(mapIndex2):
                        bracket.setAttribute('to',str(mapIndex2[old]))
                return
            else:
                # wir versuchen, die Klammern der anzuhängenden Datei in der
                # Basispartitur wiederzufinden. Wenn das nicht gelingt, muss
                # eine neue Klammer angelegt werden.
                bracketsNode = getFirstChildElement(layout, 'brackets')
                for bracket2 in brackets2:
                    oldfrom2 = int(bracket2.getAttribute('from'))
                    oldto2 = int(bracket2.getAttribute('to'))
                    oldcurly2 = bracket2.getAttribute('curly')
                    found = False
                    for brack in brackets:
                        oldfrom = int(brack.getAttribute('from'))
                        oldto = int(brack.getAttribute('to'))
                        oldcurly = brack.getAttribute('curly')
                        if mapIndex2[oldfrom2] == mapIndex1[oldfrom]\
                           and mapIndex2[oldto2] == mapIndex1[oldto]\
                           and oldcurly2 == oldcurly:
                               found = True
                    if not found:
                        newbracket = bracket2.cloneNode(True)
                        newbracket.setAttribute('from',str(mapIndex2[oldfrom2]))
                        newbracket.setAttribute('to',str(mapIndex2[oldto2]))
                        bracketsNode.appendChild(newbracket)

        # jetzt noch für die Klammern der Basispartitur die Indices anpassen
        for bracket in brackets:
            old = int(bracket.getAttribute('from'))
            if old < len(mapIndex1):
                bracket.setAttribute('from',str(mapIndex1[old]))
            old = int(bracket.getAttribute('to'))
            if old < len(mapIndex1):
                bracket.setAttribute('to',str(mapIndex1[old]))

# passe die Verweise in den Zeilen an die geänderten Layoutdeskriptornamen an
def UpdateStaffLayoutDescr(system, mapDescr):
    staves = system.getElementsByTagName('staff')
    for staff in staves:
        descr = encode(staff.getAttribute("layout"))
        if descr in mapDescr:
            mappedDescr = mapDescr[descr]
            if mappedDescr != descr:
                staff.setAttribute("layout", decode(mappedDescr))
    
# hänge die Systeme der anzuhängenden Datei an die Basisdatei an - das Mustersystem passt schon!
def AppendSystems(text1, text2, mapDescr1, mapDescr2, mapOtherFiles):
    systems1 = getFirstChildElement(text1, 'systems')
    # layout-Name der einzelnen Zeilen in Basis-Datei ggfls. anpassen
    for sys1 in systems1.childNodes:
        if sys1.nodeType == sys1.ELEMENT_NODE:
            UpdateStaffLayoutDescr(sys1, mapDescr1)
    systems2 = getFirstChildElement(text2, 'systems')
    for system2 in systems2.childNodes:
        system1 = system2.cloneNode(True)
        if system1.nodeType == system1.ELEMENT_NODE:
            # layout-Name in angehängter Datei ggfls. anpassen
            UpdateStaffLayoutDescr(system1, mapDescr2)
            # Namen der RTF- und WMF- Dateien ggfls. anpassen
            RTFobjs = system1.getElementsByTagName('richText')
            for RTFobj in RTFobjs:
                oldfilnam = RTFobj.getAttribute('file')
                RTFobj.setAttribute('file',mapOtherFiles[oldfilnam])
            WMFobjs = system1.getElementsByTagName('metafile')
            for WMFobj in WMFobjs:
                oldfilnam = WMFobj.getAttribute('file')
                WMFobj.setAttribute('file',mapOtherFiles[oldfilnam])
        systems1.appendChild(system1)
    
    

def ClefToString(clef):
    if clef == 'treble':
        return 'G '
    if clef == 'bass':
        return 'F '
    if clef == 'alto':
        return 'C3'
    if clef == 'tenor':
        return 'C4'
    if clef == 'G2-':
        return 'G8'
    return str(clef)
# Vergleichsrelevante Daten aus dem Mustersystem holen
# Liste mit z.B. folgendem Inhalt:
#     G Singstimme Konzertflügel,80
# {   G Piano r.H. Konzertflügel,80
# {   F Piano l.H. Konzertflügel,80
def getLayoutDescriptor(text):
    layout = getFirstChildElement(text, 'layout')
    if layout != None:
        List = []
        staves = getFirstChildElement(layout, 'staves')
        staffLayouts = staves.getElementsByTagName('staffLayout')
        for staffLayout in staffLayouts:
            FullDescriptor = ''
            descr = encode(staffLayout.getAttribute('description'))
            notation = getFirstChildElement(staffLayout, 'notation')
            defClef = notation.getAttribute('defaultClef')
            notelines = str(notation.getAttribute('notelines'))
            if notelines != '':
                FullDescriptor += notelines + ' | '
            else:
                FullDescriptor += '   '
            FullDescriptor +=  ClefToString(defClef) + ' | '
            FullDescriptor += str(descr)
            List.append(FullDescriptor)
        brackets = layout.getElementsByTagName('bracket')
        ListLength = len(List)
        for bracket in brackets:
            indexFrom = int(bracket.getAttribute('from'))
            indexTo = int(bracket.getAttribute('to'))
            Klammerform = ''
            if bracket.getAttribute('curly'):
                Klammerform = '{ '
            else:
                Klammerform = '[ '
            index = 0
            while index < ListLength:
                if index >= indexFrom and index <= indexTo:
                    List[index] = Klammerform + List[index]
                else:
                    List[index] = '  ' + List[index]
                index += 1
        index = 0
        while index < ListLength:
            List[index] = str(index+1) + '. ' + '  ' + List[index]
            index += 1
        return List

# dialog-Hilfsprogramm
def mkLayoutColumn(layoutlist, descEditable):
    LabelList = []
    ItemNo = 0
    maxItemNo = len(layoutlist)
    for item in layoutlist:
        indexdesc = item.rfind('|')+1
        strdesc = item[indexdesc + 1:]
        editdesc = Edit(strdesc, width=10)
        label = Label(item[0:indexdesc-1], width=6)
        if not descEditable:
            label = Label(item, width=10)
            editdesc = Label(' ')
        ItemNo += 1
        edit = Edit(str(ItemNo), min=1,max=maxItemNo,width = 4)
        LabelList.append(HBox([edit,label,editdesc], padding=10))
#    LabelList.append(' ')
    return LabelList

def reorderLayoutColumn(dlgList, NewLayoutlist):
    ItemNo = 0
    ItemCount = len(dlgList)
    changed = False
    while ItemNo < ItemCount:
        # ' ' war das letzte Element der Liste, damit sie ins Fenster passt!
        hbox = dlgList[ItemNo]
        ItemNo += 1
        if hbox != ' ':
            ed,lab, edDesc = hbox['content']
            edval = int(ed.value())
            if edval != ItemNo:
                changed = True
# falls die Zielposition keine geänderte Positionsnummer hat, soll der
# dort wegbewegte Eintrag die Positionsnummer seiner neuen Stelle übernehmen
                hboxDest = dlgList[edval-1]
                NewLayoutItem = NewLayoutlist[edval-1]
                edDest, labDest, edDescDest = hboxDest['content']
                if int(edDest.value()) == edval:
                    edDest['value'] = str(ItemNo)
                    hboxDest['content'] = [edDest, labDest, edDescDest]
                dlgList[ItemNo-1] = dlgList[edval-1]
                NewLayoutlist[ItemNo-1] = NewLayoutlist[edval-1]
                dlgList[edval-1] = hbox
                NewLayoutlist[edval-1] = NewLayoutItem
    return changed

def SaveChanges(el, file):
    file.write('<' + el.tagName)
    if el.hasAttributes():
        a = el.attributes
        for i in range(a.length):
            file.write(' %s="%s"'
                % (encode(a.item(i).name),
                   encode(el.getAttribute(a.item(i).name))))
    if el.hasChildNodes() or el.tagName == 'textarea':
        file.write('>')                          # Start-Tag schließen
        for n in el.childNodes:
            if n.nodeType == n.TEXT_NODE:
                n.data = n.data.replace('<', '&lt;')
                file.write(encode(n.data))
            elif n.nodeType == n.ELEMENT_NODE:
                SaveChanges(n, file)
        file.write('</' + el.tagName + '>')
    else:
        file.write('/>')                         # Element ohne Inhalt

# transferiere die RichText-Dateien aus File nach FileDest.
# Achte auf Namensgleichheiten!
def CopyOtherFiles(File1, File2, zw, MapOtherFiles):
    CopyOtherFilesSimple(File1, zw)
    CopyOtherFilesCheck(File2, zw, MapOtherFiles)
    
def CopyOtherFilesSimple(File, zw):
    zr = zipfile.ZipFile(File,'r')
    for name in zr.namelist():
        if name != 'score.xml':
            t = zr.read(name)
            info = zipfile.ZipInfo(name)
            info.compress_type = zipfile.ZIP_DEFLATED
            zw.writestr(info, t)
            
def CopyOtherFilesCheck(File, zw, MapOtherFiles):
    zr = zipfile.ZipFile(File,'r')
    newnamelist = zw.namelist()
    for name in zr.namelist():
        if name != 'score.xml':
            strExt = name[name.rfind('.'):]
            newname = name
            while newname in newnamelist:
                newname = newname[:newname.rfind('.')]+"1"+strExt
            t = zr.read(name)
            info = zipfile.ZipInfo(newname)
            info.compress_type = zipfile.ZIP_DEFLATED
            zw.writestr(info, t)
            MapOtherFiles[name] = newname
            newnamelist.append(newname)
            

# hole aus dem ZipFile den CapXML-Text
def getText(file):
    zr = zipfile.ZipFile(file,'r')
    for name in zr.namelist():
        t = zr.read(name)
        if name == 'score.xml':
            doc = xml.dom.minidom.parseString(t)
            return doc.documentElement
        
# hänge capx-Datei File2 an capx-Datei File1 an
def capxAppend(File1, File2, FileDest, zw):
    text1 = getText(File1)
    text2 = getText(File2)
# Kurzdarstellung der Mustersysteme produzieren
    Layoutlist1 = getLayoutDescriptor(text1)
    Layoutlist2 = getLayoutDescriptor(text2)
# Passende Zeilen aufeinander abbilden
    Len1 = len(Layoutlist1)
    Len2 = len(Layoutlist2)
    NewLayoutlist1 = []
    NewLayoutlist2 = []
    index1 = 0
    index2 = 0
    i1lastfound = 0
    while index2 < Len2:
        index1 = i1lastfound
        found = False
        while index1 < Len1:
            if Layoutlist1[index1] == Layoutlist2[index2]:
                found = True
                while i1lastfound < index1:
                    NewLayoutlist1.append(Layoutlist1[i1lastfound])
                    NewLayoutlist2.append('---')
                    i1lastfound += 1
                NewLayoutlist1.append(Layoutlist1[index1])
                NewLayoutlist2.append(Layoutlist2[index2])
                i1lastfound = index1 + 1
            index1 += 1
        if not found:
            NewLayoutlist1.append('---')
            NewLayoutlist2.append(Layoutlist2[index2])
        index2 += 1
    while i1lastfound < Len1:
        NewLayoutlist1.append(Layoutlist1[i1lastfound])
        NewLayoutlist2.append('---')
        i1lastfound += 1

    dlgList1 = mkLayoutColumn(NewLayoutlist1, True)
    vBox1 = VBox(dlgList1, padding = 10, text='Basis')
    dlgList2 = mkLayoutColumn(NewLayoutlist2, False)
    vBox2 = VBox(dlgList2, padding = 10, text='anzuhängen')
    hBox = HBox([vBox1,vBox2], padding = 30)
    helptext = "Stimmen Sie Position und Beschreibung der Mustersystemzeilen in beiden Dateien aufeinander ab!\n\
Folgende Attribute der Mustersystemzeilen werden angezeigt:\n\
[ : Zeile hat eckige Klammer\n\
{ : Zeile hat geschweifte Klammer\n\
G : Zeile hat Violinschlüssel als Standardschlüssel\n\
F : Zeile hat Bassschlüssel als Standardschlüssel\n\
G8 : Zeile hat Violinschlüssel mit Oktavierung nach unten als Standardschlüssel\n\
C3 : Zeile hat C-Schlüssel (Alt) als Standardschlüssel\n\
C4 : Zeile hat C-Schlüssel (Tenor) als Standardschlüssel\n\
\n\
Ok zeigt die Zeilen nach Umordnung in der neuen Position.\n\
Weiteres Ok hängt die anzuhängende Datei in der angezeigten Weise an."
    dlg = Dialog(File2[appendfile.rfind('\\')+1:]+" anhängen: Mustersysteme kombinieren", hBox, helptext)
    changed = True
    doit = False
    dlgList1Save = str(dlgList1)
    while changed and dlg.run():
        doit = True
        changed = False
        while reorderLayoutColumn(dlgList1, NewLayoutlist1):
            changed = True
        while reorderLayoutColumn(dlgList2, NewLayoutlist1):
            changed = True
        if changed:
            dlg = Dialog("Mustersysteme kombiniert", hBox, "noch kein Hilfetext!")
            doit = False
        dlgList1Save = str(dlgList1)
    if doit:
        activeScore().registerUndo("Datei anhängen")
        mapDescr1 = {}
        mapDescr2 = {}
        mapOtherFiles = {}
        CopyOtherFiles(File1, File2, zw, mapOtherFiles)
        UpdateLayout(text1, Layoutlist1, dlgList1, text2, dlgList2, mapDescr1, mapDescr2)
        AppendSystems(text1, text2, mapDescr1, mapDescr2, mapOtherFiles)
        SaveChanges(text1, FileDest)
        return True
    return False

# Hauptprogramm
as = activeScore()
if as:
# Name der anzuhängenden Datei erfragen
    fdlg = FileDialog()
    fdlg.setDefaultExt("capx")
    fdlg.addFilter("capella-Dateien (*.cap, *.capx)","*.cap;*.capx")
    fdlg.addFilter("alle Dateien (*.*)","*.*")
    fdlg.setTitle("Capella-Datei anhängen")
    if fdlg.run():
        appendfile = fdlg.filePath()
        if appendfile.endswith(".capx"):
            tempFile1 = tempfile.mktemp('.capx')
            tempFile2 = tempFile1[:tempFile1.rfind('.')] + '~.capx'
            zw = zipfile.ZipFile(tempFile2, 'w', zipfile.ZIP_DEFLATED)
            tempFile = tempfile.mktemp('.xml')
            fd = file(tempFile, 'wt')
            fd.write('<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>\n')
            as.write(tempFile1)
            if capxAppend(tempFile1, appendfile, fd, zw):
                fd.close()
                zw.write(tempFile, 'score.xml')
                zw.close()
                as.read(tempFile2)
            else:
                fd.close()
                zw.close()
            os.remove(tempFile)
            os.remove(tempFile1)
            os.remove(tempFile2)
        elif appendfile.endswith(".cap"):
            messageBox("Datei anhängen", "keine capx-Datei \""+appendfile[appendfile.rfind('\\')+1:]+"\" - geht noch nicht.")
            pass
        else:
            messageBox("Datei anhängen", "keine capx-Datei  \""+appendfile[appendfile.rfind('\\')+1:]+"\" - Nur für Capella-Dateien!")

