# -*- coding: ISO-8859-1 -*-
""" capellaScript -- (c) Paul Villiger
>>> Metronom

Erstellt eine Metronomzeile
    
<<<
<?xml version="1.0" encoding="ISO-8859-1"?>
<info>
  <lang id="en">
    <title>Metronome</title>
    <descr>
      <p>Generates a metronome staff</p>
    </descr>
  </lang>

  <lang id="de">
    <title>Metronom</title>
    <descr>
      <p>Erstellt eine Metronomzeile</p>
    </descr>
  </lang>

  <lang id="nl">
    <title>Metronoom</title>
    <descr>
      <p>Genereert een metronoombalk</p>
    </descr>
  </lang>
</info>

History:  09.04.19 - Erste Ausgabe
          03.02.20 - Tonhöhe für ersten Ton und Folgeton
          06.02.20 - Internationalisierung (en-de-nl) und Hilfe zugefügt (WW)
                    
"""

english = {
            'textTitle'                 : '-- Metronome --',
            'help'                      : "This script generates a staff with time beating.\n\n\
\nS E T T I N G S :\n\
\n- I n s t r u m e n t - Here the corresponding MIDI number can be entered, 115 equals woodblock.\n\
\n- V o l u m e - Value from 0 to 127.\n\
\n- P i t c h - For the first and the next beats the pitch can be defined. \
The description corresponds to the internal designation in the capx file. \
C5 equals c', C6 equals c'', etc. . If b or # is put in front of them, \
the tones are alterated. Examples: 'E5,C5'; '#D5,bB4'.\n\
\n- The metronome staff can also be removed.\n\
\n- If you change the settings, an existing staff will be overwritten.\n\n\
\nR E M A R K S :\n\
\nIrregular times as 5/8, 7/8 or 8/8 are treated in a special way:\n\
\n     5/8 - 2+3\
\n     7/8 - 2+2+3\
\n     8/8 - 3+3+2",
            'textInstrument'            : 'Instrument (1-127)',
            'textVolume'                : 'Volume (0-127)',
            'textRemove'                : 'Remove metronome staff',
            'textPitch'                 : 'Pitch 1st, next (D6,B5)',
            'textDescr1'                : "Pitch C5 == c', C6 == c'', b and # allowed",
            'textDescr2'                : 'Example: #D6,B5 - Instrument equals Midi number'
}
german = {
            'textTitle'                 : '-- Metronom --',
            'help'                      : "Dieses Skript erzeugt eine Notenzeile mit dem Taktschlag.\n\n\
\nE I N S T E L L U N G E N :\n\
\n- I n s t r u m e n t - hier kann die entsprechende MIDI Nummer eingegeben werden, 115 entspricht Klangholz.\n\
\n- L a u t s t ä r k e - Wert von 0 bis 127\n\
\n- T o n - Für den ersten und die folgenden Schläge kann die Tonhöhe definiert werden. \
Die Bezeichnung entspricht der internen Darstellung in der capx Datei. \
C5 entspricht c', C6 entpricht c'', usw. Wird ein b oder # vorangesetzt, \
so werden die Töne alteriert. Beispiele: 'E5,C5'; '#D5,bB4'.\n\
\n- Die Metronomzeile kann auch wieder gelöscht werden.\n\
\n- Ändert man die Einstellungen, so wird eine bestehende Zeile überschrieben.\n\n\
\nB E M E R K U N G E N :\n\
\nUnregelmässige Taktarten wie 5/8, 7/8 oder 8/8 werden speziell behandelt:\n\
\n     5/8 - 2+3 \
\n     7/8 - 2+2+3 \
\n     8/8 - 3+3+2",
            'textInstrument'            : 'Instrument (1-127)',
            'textVolume'                : 'Volumen (0-127)',
            'textRemove'                : 'Metronomzeile entfernen',
            'textPitch'                 : '1ster, folge Ton (D6,B5)',
            'textDescr1'                : "Ton C5 == c', C6 == c'', b und # erlaubt",
            'textDescr2'                : 'Beispiel: #D6,B5 - Instrument entspricht Midinummer'
}


dutch = {
            'textTitle'                 : '-- Metronoom --',
            'help'                      : "Dit script genereert een notenbalk met maatslag.\n\n\
\nI N S T E L L I N G E N :\n\
\n- I n s t r u m e n t - hier kan het betreffende MIDI-nummer worden ingevoerd, 115 = woodblock.\n\
\n- V o l u m e - waarde van 0 tot 127\n\
\n- T o o n - voor de eerste en de volgende tikken kan de toonhoogte worden ingesteld. \
De omschrijving komt overeen met de interne aanduiding in het capx-bestand. \
C5 staat voor c', C6 voor c'', enz. Als er een b of een # vóór wordt geplaatst, \
dan worden de tonen verlaagd, resp. verhoogd. Voorbeelden: 'E5,C5'; '#D5,bB4'.\n\
\n- De metronoombalk kan ook weer verwijderd werden.\n\
\n- Wanneer men de instellingen verandert, wordt een al aanwezige metronoombalk overschreven.\n\n\
\nO P M E R K I N G E N :\n\
\nOnregelmatige maatsoorten zoals 5/8, 7/8 oder 8/8 worden speciaal behandeld:\n\
\n     5/8 - 2+3 \
\n     7/8 - 2+2+3 \
\n     8/8 - 3+3+2",
            'textInstrument'            : 'Instrument (1-127)',
            'textVolume'                : 'Volume (0-127)',
            'textRemove'                : 'Metronoombalk verwijderen',
            'textPitch'                 : '1ste, volgende toon (D6,B5)',
            'textDescr1'                : "Toon C5 == c', C6 == c'', b en # toegestaan",
            'textDescr2'                : 'Voorbeeld: #D6,B5 - Instrument komt overeen met MIDI-nummer'
}


try:
    setStringTable(
        ("en", english),
        ("de", german),
        ("nl", dutch)
        )
except:
    def tr(s):
        return german[s]

import new
from xml.dom.minidom import parseString, NodeList, Node, Element
from caplib.rational import Rational

layoutNameMetronome = '_Metronom_'

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 getDuration(note, defaultTime):
    """ 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 = str(duration.getAttribute('base'))
    if '/' in n:
        n = Rational(n)
    else:
        n = defaultTime * int(n)

    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 addMetronomeStaffLayout(score):
    global glbInstrument, glbVolume
    layout = score.getElementsByTagName('layout')
    staves = layout[0].gotoChild('staves')

    for staffLayout in staves.getElementsByTagName('staffLayout'):
        if staffLayout.getAttribute('description') == layoutNameMetronome:
            return      # bereits vorhanden
        
    staffLayout = staves.gotoChild('staffLayout', True)
    staffLayout.setAttribute('description', layoutNameMetronome)

    notation = staffLayout.gotoChild('notation')
    notation.setAttribute('defaultClef','P3')
    notation.setAttribute('notelines','1')

    barlines = notation.gotoChild('barlines')
    barlines.setAttribute('mode','internal')
    barlines.setAttribute('from','3')
    barlines.setAttribute('to','7')
    
    distances = staffLayout.gotoChild('distances')
    distances.setAttribute('top','4')
    distances.setAttribute('bottom','4')

    instrument = staffLayout.gotoChild('instrument')
    
    sound = staffLayout.gotoChild('sound')
    sound.setAttribute('instr', glbInstrument)
    sound.setAttribute('volume', glbVolume)
    # sound.setAttribute('sample','Celesta')
    # sound.setAttribute('genericSound','percussion.metal.celesta')

def removeMetronome(score):
    for layout in score.getElementsByTagName('layout'):
        for staffLayout in layout.getElementsByTagName('staffLayout'):
            if staffLayout.getAttribute('description') == layoutNameMetronome:
                staffLayout.parentNode.removeChild(staffLayout)
                break

    for system in score.getElementsByTagName('system'):
        for staff in system.getElementsByTagName('staff'):
            if staff.getAttribute('layout') == layoutNameMetronome:
                staff.parentNode.removeChild(staff)


def getNoteObjects(noteObjects):  # returns a List
    objList = noteObjects.childNodes
    newList = NodeList()
    for n in range(objList.length):
        if objList[n].nodeType == objList[n].ELEMENT_NODE:
            newList.append(objList[n])
    return newList


def addMetronomeStaff(staves):
    firstStaff = staves.gotoChild('staff')
    newStaff = staves.gotoChild('staff', True)
    newStaff.setAttribute('layout', layoutNameMetronome)
    
    if firstStaff.hasAttribute('defaultTime'):
        newStaff.setAttribute('defaultTime', firstStaff.getAttribute('defaultTime') )
        currentDefaultTime = firstStaff.getAttribute('defaultTime')
    else:
        currentDefaultTime = '4/4'

    if currentDefaultTime == 'C':
        currentDefaultTime = '4/4'
    elif currentDefaultTime == 'AllaBreve':
        currentDefaultTime = '2/2'
        
        
    firstVoices = firstStaff.gotoChild('voices')
    newVoices = newStaff.gotoChild('voices')

    firstVoice = firstVoices.gotoChild('voice')
    newVoice = newVoices.gotoChild('voice')

    lyricsSettings = firstVoice.gotoChild('lyricsSettings')
    newVoice.appendChild(lyricsSettings.cloneNode(True))

    firstNoteObjects = firstVoice.gotoChild('noteObjects')
    newNoteObjects = newVoice.gotoChild('noteObjects')


beatStroke = {'count':0, 'beatCount':'4', 'beat':'4', 'rythm': 'bbbb', 'currentTime': Rational('4/4') }

def handleTime(defaultTime):
    global beatStroke
    if defaultTime == 'infinite':
        beatCount = '1'
        beat = '4'
    elif defaultTime == 'C':
        beatCount = '4'
        beat = '4'
    elif defaultTime == 'allaBreve':
        beatCount = '2'
        beat = '2'
    else:
        beatCount, beat = defaultTime.split('/')

    currentTime = Rational( str('%s/%s' % (beatCount, beat) ) )

    beatStroke['count'] = 0
    beatStroke['beatCount'] = beatCount
    beatStroke['beat'] = beat
    beatStroke['currentTime'] = currentTime
    if beat in ['1','2','4']:
        beatStroke['rythm'] = 'b' * int(beatCount)
    elif beat == '8':
        #             0   1   2    3     4      5       6        7         8          9
        rythmList = ['b','b','bp','bpp','bpbp','bpbpp','bppbpp','bpbpbpp','bpbppbpp','bppbppbpp']
        if int(beatCount) <= len(rythmList):
            beatStroke['rythm'] = rythmList[int(beatCount)]
        else:
            rythm = 'bpp' * int(beatCount)
            rythm = rythm[:int(beatCount)]
            beatStroke['rythm'] = rythm
            
def fillBeat(noteObjects, diff):
    global beatStroke, firstPitch, nextPitch
    fPitch = firstPitch
    nPitch = nextPitch
    fAlter = nAlter = ''
    
    if fPitch[0] in 'b#':
        fAlter = fPitch[0]
        fPitch = fPitch[1:]
    if nPitch[0] in 'b#':
        nAlter = nPitch[0]
        nPitch = nPitch[1:]

    beat = beatStroke['beat']
    beatTime = Rational(str('1/%s' % beat))
    d = diff
    while d >= beatTime:
        stroke = beatStroke['rythm'][beatStroke['count']]
        if stroke == 'b':
            chord = noteObjects.gotoChild('chord', True)
            duration = chord.gotoChild('duration', True)
            duration.setAttribute('base', str(beatTime))
            heads = chord.gotoChild('heads', True)
            head =  heads.gotoChild('head', True)
            if beatStroke['count'] == 0:
                head.setAttribute('pitch',fPitch)
                if fAlter == 'b':
                    alter = head.gotoChild('alter')
                    alter.setAttribute('step','-1')
                elif fAlter == '#':
                    alter = head.gotoChild('alter')
                    alter.setAttribute('step','1')
            else:                
                head.setAttribute('pitch',nPitch)
                if nAlter == 'b':
                    alter = head.gotoChild('alter')
                    alter.setAttribute('step','-1')
                elif nAlter == '#':
                    alter = head.gotoChild('alter')
                    alter.setAttribute('step','1')
        else:
            rest = noteObjects.gotoChild('rest', True)
            duration = rest.gotoChild('duration', True)
            duration.setAttribute('base', str(beatTime))

        beatStroke['count'] += 1            
        beatStroke['count'] = beatStroke['count'] % int(beatStroke['beatCount'])

        d = d - beatTime

    return d

def fillRest(noteObjects, diff):
    if diff == 0:
        return
    
    for d in [32,16, 8,4,2,1]:
        base = '1/%s' % d
        if diff <= Rational( base ):
            break
    rest = noteObjects.gotoChild('rest', True)
    duration = rest.gotoChild('duration', True)
    duration.setAttribute('base', base)
    
    dots = 0
    if diff == 1.5 * Rational(base):
        dots = 1
    elif diff == 1.75 * Rational(base):
        dots = 2
    elif diff == 1.875 * Rational(base):
        dots = 3

    if dots:
        duration.setAttribute('dots', str(dots))
        return
    
             

def fillMetronomeStaff(system, restCount):
    global BeatStroke
    staves = system.gotoChild('staves')
    firstStaff = staves.gotoChild('staff')
    for staff in staves.getElementsByTagName('staff'):
        if staff.getAttribute('layout') == layoutNameMetronome:
            metronomeStaff = staff
            break

    handleTime(staff.getAttribute('defaultTime'))
    currentTime = beatStroke['currentTime']
    beat = beatStroke['beat']
    

    noteObjects = firstStaff.getElementsByTagName('noteObjects')[0]
    firstStaffObjects = getNoteObjects(noteObjects)

    metronomeNoteObjects = metronomeStaff.getElementsByTagName('noteObjects')[0]

    firstClef = False
    firstKeySign = False
    measureTime = Rational(0)
    fillTime = Rational(0)
    measureEnd = False

    beatStroke['count'] = 0

    for noteObj in firstStaffObjects:
        if noteObj.nodeName == 'clefSign':
            if not firstClef:
                newObj = metronomeNoteObjects.gotoChild('clefSign',True)
                newObj.setAttribute('clef','P3')
                firstClef = True

        elif noteObj.nodeName == 'barline':
            diffTime = measureTime - fillTime
            fillRest(metronomeNoteObjects, diffTime)
            metronomeNoteObjects.appendChild(noteObj.cloneNode(True) )
            beatStroke['count'] = 0
        elif noteObj.nodeName == 'timeSign':
            metronomeNoteObjects.appendChild(noteObj.cloneNode(True) )
            handleTime(noteObj.getAttribute('time'))
            currentTime = beatStroke['currentTime']
            beat = beatStroke['beat']

            measureEnd = True
        elif noteObj.nodeName == 'keySign':
            metronomeNoteObjects.appendChild(noteObj.cloneNode(True) )
            firstKeySign = True
            measureEnd = True
            beatStroke['count'] = 0

        elif noteObj.nodeName in ['chord','rest']:
            # metronomeNoteObjects.appendChild(noteObj.cloneNode(True) )
            measureTime += getDuration(noteObj, currentTime)
        else:
            pass

        diffTime = measureTime - fillTime
        restTime = fillBeat(metronomeNoteObjects, diffTime)
        fillTime = measureTime - restTime
        

def getDialogValues():
    options = ScriptOptions() 
    opt = options.get()

    editInstrument = Edit(opt.get('editInstrument', '115'), width = 8)
    editVolume     = Edit(opt.get('editVolume', '127'),     width = 8)
    editPitch      = Edit(opt.get('editPitch', 'D6,B5'), width = 8)
    cBoxUndo       =CheckBox( tr('textRemove') )
    dlg = Dialog(tr('textTitle'),
                 VBox([HBox([Label(tr('textInstrument'), width = 20), editInstrument, Label(' ', width = 5)]),
                       HBox([Label(tr('textVolume'),    width = 20), editVolume]),
                       HBox([Label(tr('textPitch'), width = 20), editPitch]),
                       cBoxUndo,
                       Label(''),
                       Label(tr('textDescr1')),
                       Label(tr('textDescr2'))
                       ]                    
                     ),
                 help = tr('help'))

    if dlg.run():
        editInstrument = editInstrument.value()
        if editInstrument.isdigit() and int(editInstrument) < 128:
            opt['editInstrument'] = editInstrument
        else:
            editInstrument = opt.get('editInstrument', '115')

        editVolume = editVolume.value()
        if editVolume.isdigit() and int(editVolume) < 128:
            opt['editVolume'] = editVolume
        else:
            editVolume = opt.get('editVolume', '127')

        editPitch = editPitch.value()
        if ',' in editPitch:
            firstPitch, nextPitch = editPitch.split(',')
        else:
            firstPitch = nextPitch = editPitch

        firstPitch = firstPitch.upper().strip()
        nextPitch  = nextPitch.upper().strip()
        if len(firstPitch) == 3:
            if firstPitch[0] in '#B' and firstPitch[1] in 'CDEFGAB' and firstPitch[2] in '2345678':
                if firstPitch[0] == 'B':
                    firstPitch = 'b' + firstPitch[1:]
            else:
                firstPitch = 'D6'
        elif len(firstPitch) == 2:
            if firstPitch[0] in 'CDEFGAB' and firstPitch[1] in '2345678':
                pass
            else:
                firstPitch = 'D6'
        else:
            firstPitch = 'D6'
        if len(nextPitch) == 3:
            if nextPitch[0] in '#B' and nextPitch[1] in 'CDEFGAB' and nextPitch[2] in '2345678':
                if nextPitch[0] == 'B':
                    nextPitch = 'b' + nextPitch[1:]
            else:
                nextPitch = 'B5'
                
        elif len(nextPitch) == 2:
            if nextPitch[0] in 'CDEFGAB' and nextPitch[1] in '2345678':
                pass
            else:
                nextPitch = 'B5'
        else:
            nextPitch = 'B5'

        opt['editPitch'] = '%s,%s' % (firstPitch, nextPitch)


        cBoxUndo = cBoxUndo.value() == 1

        options.set(opt)
        return (True, editInstrument, editVolume, cBoxUndo, firstPitch, nextPitch)

    else:
        return (False, '0', '0', False, 'D6', 'B5')

    

def changeDoc(score):
    global undo

    removeMetronome(score)      # Metronomzeile wird gelöscht oder ersetzt
    
    if not undo:
        addMetronomeStaffLayout(score)
        
        for system in score.getElementsByTagName('system'):
            for staves in system.getElementsByTagName('staves'):
                addMetronomeStaff(staves)
                
            restCount = 0
            fillMetronomeStaff(system, restCount)


# Hauptprogramm:

from caplib.capDOM import ScoreChange
import tempfile

class ScoreChange(ScoreChange):
    def changeScore(self, score):
        changeDoc(score)
        

(ok, glbInstrument, glbVolume, undo, firstPitch, nextPitch) = getDialogValues()


if activeScore() and ok:
    activeScore().registerUndo("Voltenklammern ausrichten")
    tempInput = tempfile.mktemp('.capx')
    tempOutput = tempfile.mktemp('.capx')
    activeScore().write(tempInput)
    
    ScoreChange(tempInput, tempOutput)

    activeScore().read(tempOutput)
    os.remove(tempInput)
    os.remove(tempOutput)
