# -*- coding: ISO-8859-1 -*-
""" capellaScript -- Copyright (c) 2004 Hartmut Ring
>>> Transponierbares Akkordsymbol
    Über dem Akkord an der Cursorposition
    ein Akkordsymbol einfügen.||

    Das Symbol wird in einem Dialog in ein Textfeld eingetippt
    und automatisch formatiert und transponierbar gemacht.
<<<
    Fehlerkorrekturen (Paul Villiger, mit vlg. markiert):
    (1) Bei Akkordsymbolen mit z.B. G/F werden nicht alle Intervalle
        richtig berechnet.
    (2) Bei Akkordsymbolen mit # oder b am Anfang des Textes wird ein
        leeres Textelement eingefügt, was bei externen Skripts falsch
        umgesetzt und mit Inhalt gefüllt wird.

    Fehlerkorrekturen/Erweiterungen (J.Jørgen von Bargen, mit JJvB markiert)
    (3) Bei Eingabe von C# 7b9#13 wurde nur das #13 aber nicht das b9
        mit Vorzeichen in Capella-Font ersetzt
    (4) Akkorde können jetzt auch über Pausen eingegeben werden.
        (Halt ohne Vorschläge)

"""

helpText = """Geben Sie die Akkordbezeichnung ohne Rücksicht auf die Formatierung in das Textfeld ein.

Am Anfang muss ein Akkord stehen:
Buchstabe A bis G, danach kann # oder b und evtl. ein m stehen).

Der Rest (falls vorhanden) muss durch ein Leerzeichen abgetrennt werden.

Es wird ein (mit automatischer Hoch- und Tiefstellung) passend formatiertes transponierbares Akkordsymbol an der Cursorposition eingefügt!

Beispiele für die Eingabe:
C      \t(C-Dur)
Bb     \t(B-Dur)
F#m    \t(fis-Moll)
Cm 7b5 \t(c-Moll mit kl. Sept., verm. Quinte)
F# maj7\t(Fis-Dur mit großer Septime)
C/E    \t(C-Dur mit E im Bass)
Csus   \t(C-Dur oder -Moll mit Quartvorhalt)"""


class ChordSymbol(object):
    def __init__(self, obj, s, y=3.5, relSize=1.0, sansSerif=False):
        self.s = s
        self.x = 0
        self.relSize = relSize
        self.factorSmall = 0.6
        normalPitch   = 10.0 * relSize
        slashPitch    = 18.0 * relSize
        self.capPitch = 12.0 * relSize
        self.yBase  = -y                              # Basislinie: normale Schrift
        self.dyBaseSharp = -0.8 * relSize
        self.dyBaseBemol = -0.4 * relSize
        self.yHigh      = self.yBase - 1.0 * relSize  # Basislinie: hochgestellt
        self.ySlash     = self.yBase + 1.0 * relSize  # Basislinie: Schrägstrich
        self.yLow       = self.yBase + 1.0 * relSize  # Basislinie: tief (nach Schrägstrich)
        self.obj = obj
        if sansSerif:
            self.normalFace = 'Arial'
        else:
            self.normalFace = 'Times New Roman'
        self.normalFont = dict(face=self.normalFace, height=normalPitch)
        self.smallFont  = dict(face=self.normalFace, height=self.factorSmall*normalPitch)
        self.slashFont  = dict(face=self.normalFace, height=slashPitch)
        self.notes = ['Fb', 'Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb',
                      'F',  'C',  'G',  'D',  'A',  'E',  'B',
                      'F#', 'C#', 'G#', 'D#', 'A#', 'E#', 'B#']

    def appendAlteration(self, y, sharp=True, small=False):
        """ Behandlung der Alterationssymbole # und b mit dem capella-Font.
            Hilfsmethode für appendString()
        """
        dx = 0.1*self.relSize
        if sharp:
            dy = self.dyBaseSharp
            symb = 'S' # das Symbol # liegt im capella-Font bei S
        else:
            dy = self.dyBaseBemol
            symb = 'Q' # das Symbol b liegt im capella-Font bei Q
        height = self.capPitch
        if small:
            height *= self.factorSmall
            dy     *= self.factorSmall
            dx *= 2 # damit näher an der rechten Ziffer
        capFont = dict(face = 'capella3',
                       pitchAndFamily = 2, # FF_DONTCARE | VARIABLE_PITCH
                       charSet = 2,        # SYMBOL_CHARSET
                       height = height)
        t = dict(type='text', x=self.x+dx, y=y+dy, content=symb, font=capFont)
        # Die Breite der capella-Zeichen enthält zu viel Leerraum.
        # Deshalb Breitenbestimmung mit einem passenden Buchstaben ('-'):
        if small:
            test = dict(type='text', x=self.x, y=0, content='c', font=self.smallFont)
        else:
            test = dict(type='text', x=self.x, y=0, content='c', font=self.normalFont)
        size = self.obj.textSize(test)
        self.x += size[0]
        self.groupItems.append(t)

    def appendText(self, s, y, small=False):
        """ Behandlung von einfachem Text,
            Hilfsmethode für appendString()
        """
        font = self.normalFont
        if small:
            font = self.smallFont
        o = dict(type='text', x=self.x, y=y, content=s, font=font)
        size = self.obj.textSize(o)
        self.x += size[0]
        self.groupItems.append(o)

    def appendString(self, s, y, small=False):
        """ Da das Akkordsymbol aus unterschiedlichen Fonts (Text/capella) besteht,
            wird es als Gruppe aus verschiedenen Einfachtexten zusammengesetzt
            Hilfsmethode für createSymbol()
        """
        # Korrektur JJvB min statt max findet alle Vorzeichen
        while min(s.find('b'),s.find('#')) >= 0:
            i = min(s.find('b'),s.find('#'))
            left = s[0:i]
            mid = s[i]
            right = s[i+1:]
            if left != '':            # Korrektur vlg, leere Textelemente erzeugen Fehler. Bsp. C # (C mit # hochgestellt)
                self.appendText(left, y, small)
            self.appendAlteration(y, mid == '#', small)
            s = right
        if s != '':
            self.appendText(s, y, small)

    def createSymbol(self):
        """ Zusammensetzung eines kompletten Akkordsymbols
            aus den Komponenten base, high, low
            als Gruppe von einfachen Textelementen
        """
        self.x = 0
        self.groupItems = []
        self.appendString(self.base, self.yBase)
        if self.high != '':
            self.appendString(self.high, self.yHigh, True)
        if self.low != '':
            slash = dict(type='text', x=self.x-0.7*self.relSize, y=self.ySlash,
                         content='/', font=self.slashFont)
            self.x += 0.1*self.relSize
            self.groupItems.append(slash)
            self.appendString(self.low, self.yLow)
        return dict(type='group', items=self.groupItems)

    def splitChordSymbolString(self):
        # self.s aufteilen und Groß-/Kleinschreibung anpassen (high und low sind optional):
        #        +------+
        # +------| high | /
        # | base |------+/
        # +------+      /+-----+
        #              / | low |
        #             /  +-----+
        self.low = ''
        if '/' in self.s:         # low ist durch / abgetrennt
            i = self.s.find('/')
            self.low = self.s[i+1:].strip()
            self.s = self.s[0:i]
        self.high = ''            # high ist durch Leerzeichen abgetrennt
        if ' ' in self.s:
            i = self.s.find(' ')
            self.high = self.s[i+1:].strip().lower().replace('0', 'o')
            self.s = self.s[0:i]
        self.base = self.s.strip()
        self.base = self.s[0].upper() + self.s[1:].lower()
        if self.low != '':
            self.low = self.low[0].upper() + self.low[1:].lower()
        try:
            if len(self.base) > 1 and self.base[1] in '#b':
                self.n0 = self.notes.index(self.base[0:2])
            else:
                self.n0 = self.notes.index(self.base[0])
        except:
            self.n0 = -1
        try:
            if len(self.low) > 1 and self.low[1] in '#b':
                self.n1 = self.notes.index(self.low[0:2])
            else:
                self.n1 = self.notes.index(self.low[0])
        except:
            self.n1 = -1

    def create(self):
        """ Erzeugung des transponierbaren Akkordsymbols
        """
        self.splitChordSymbolString()
        if self.n0 < 0:
            return self.createSymbol()
        else:
            base1 = self.notes[self.n0]
            base2 = self.base[len(base1):]
            transpBase = self.n0 - 8
            if self.n1 >= 0:
                low1 = self.notes[self.n1]
                low2 = self.low[len(low1):]
            itemList = []
            for i, note in enumerate(self.notes):
                self.base = note + base2
                if self.n1 >= 0:
                    iLow = i + (self.n1-self.n0)
                    iLow = iLow % 21 # korrektur vlg, Falsche Intervallberechnung G/F führt zu F#/E
                    self.low = self.notes[iLow] + low2
                itemList.append(self.createSymbol())
            return dict(type='transposable', nRefNote=transpBase, items=itemList)

def getOptions(major, minor):
    multipleChoice = len(major) + len(minor) > 1 # Mehrfachauswahl mit Radiobuttons
    options = ScriptOptions()
    opt = options.get()
    size = int(opt.get('size', '10'))
    sizeStep = size - 6
    y = int(opt.get('y', '7'))
    yStep = y - 6
    sansSerif = int(opt.get('sansSerif', '0'))

    comboSize  = ComboBox([str(i) for i in range(6,15)], value=sizeStep)
    comboY  = ComboBox([str(i) for i in range(6,13)], value=yStep)
    labelSize = Label('&Größe', width=12)
    labelY = Label('&Vertikale Lage', width=12)
    labelChord = Label('&Bezeichnung', width=12)
    hBox1 = HBox([labelSize, comboSize, Label('Punkt')], padding=8)
    hBox2 = HBox([labelY, comboY, Label('/2 Zw. über Mittellinie')], padding=8)
    editSym  = Edit (major[0], width=14)
    if multipleChoice:
        radioDef   = Radio(['&individuell:'], value=0)
        radioMajor = Radio(major, value=-1, ext=True)
        radioMinor = Radio(minor, value=-1, ext=True)
        v1 = VBox([Label('für Dur:'),  radioMajor], padding=8)
        v2 = VBox([Label('für Moll:'), radioMinor], padding=8)
        h1 = HBox([radioDef, editSym, Label(' ')])
        h2 = HBox([Label('Vorschläge:     '), v1, v2])
        chordBox = VBox([h1, h2], padding=12, text='Bezeichnung')
    else:
        chordBox = HBox([labelChord, editSym], padding=8)

    check = CheckBox ('serifenlose Schrift', value=sansSerif)

    vBox   = VBox([chordBox, hBox1, hBox2, check], padding=16)
    title = 'Akkordsymbol'
    dlg = Dialog(title, vBox) #, helpText)
    result = False
    if dlg.run():
        opt = dict(size=6+comboSize.value(), y=6+comboY.value(), sansSerif=str(check.value()))
        if multipleChoice:
            if radioMajor.value() >= 0:
                resultString = major[radioMajor.value()]
            elif radioMinor.value() >= 0:
                resultString = minor[radioMinor.value()]
            else:
                resultString = editSym.value()
        else:
            resultString = editSym.value()
        opt['sym'] = resultString
        options.set(opt)
        result = (resultString, 0.5*(6+comboY.value()), 0.1*(6+comboSize.value()), check.value())
    return result

def getCur():
    sel = curSelection()
    result = (False, False, False, False)
    if sel == 0:
        messageBox('Fehler', 'keine aktive Partitur')
        return result
    if sel[0] != sel[1]:
        messageBox('Fehler', 'Markierung ist nicht leer')
        return result
    sel = sel[0]
    sys = activeScore().system(sel[0])
    staff = sys.staff(sel[1])
    voice = staff.voice(sel[2])
    obj = 0
    if sel[3] < voice.nNoteObjs():
        obj = voice.noteObj(sel[3])
        # Änderung JJvB auch Pause erlauben
        #   if obj.isChord(): # and obj.nHeads() > 1:
        return (sys, staff, voice, obj)
    messageBox('Fehler', 'Der Cursor steht nicht vor einem Akkord')
    return result

def analyzeChord(pitches):
    base = pitches[0]
    chordString = 'CDEFGAB'[base[0]%7] + ('bb','b','','#','+')[base[1]+2]
    no3 = False
    I = intervals(pitches)
    if ((2,-1) in I                           # kleine Terz
            and ((4,-2) in I or (3,2) in I)   # verm. Quinte oder überm. Quarte
            and ((6,-2) in I or (5,1) in I)): # verm. Sept. oder gr. Sexte
        return chordString + ' 0'             # "Nullakkord" (wird in kleines o umgewandelt)
    if (3,0) in I:                            # reine Quarte
        chordString += 'sus'
    elif (2,-1) in I:                         # kleine Terz: moll
        chordString += 'm'
    if (2, 1) not in I and (2,-1) not in I:   # keine Terz
        no3 = True

    # Intervallziffern:
    # mod7 | Interv.| verm.| klein| rein | groß |überm.| fehlend
    #------+--------+------+------+------+------+------+--------
    #  1   | None   | ---  |  b9  | ---  | 9    | #9   |
    #  2   | Terz   | ---  |   m  | ---  |(Std.)| ---  | no3
    #  3   | Quarte | ---  | ---  | sus  | ---  | ---  |
    #  4   | Quinte | b5   | ---  |(Std.)| ---  | #5   |
    #  5   | Sexte  | ---  |   6  | ---  | 6    | ---  |
    #  6   | Septime| ---  |   7  | ---  | maj7 | ---  | ggf. "add9" statt 9
    #                                                         "addb9" statt b9
    highString = ''
    sept = False
    if (5,1) in I or (5,-1) in I: # kleine und große Sexte bekommen beide "6"
        highString += '6'
    if (6,-1) in I:       # kleine Septime
        highString += '7'
        sept = True
    elif (6,1) in I:      # große Septime
        highString += 'maj7'
        sept = True
    if (4,-2) in I:       # verminderte Quinte
        highString += 'b5'
    if (1,-1) in I or (1,1) in I: # None (bzw. Sekunde)
        if not sept:
            highString += 'add'
        if (1,-1) in I:   # kleine None
            highString += 'b'
        highString += '9'
    if no3:
        highString += 'no3'

    if highString != '':
        chordString += ' ' + highString
    return [chordString]

def analyzeSingleNote(pitch, keyStep):
    # Schlüssel: Note
    # Werte    : Akkordvorschläge, durch '|' getrennt

    cMajor = {                  'C': 'C|F|Am',      'C#': 'A|C#|G 0',
        'Db': 'Eb 7|Fm #5|G 0', 'D': 'G|D|Dm',      'D#': 'B|C 0|E maj7',
        'Eb': 'Cm|C 0|Eb',      'E': 'C|A|E',       'E#': 'C#',
        'Fb': 'Gb 7',           'F': 'F|G 7|Dm',    'F#': 'D|G maj7|Bm',
        'Gb': 'C 0|Eb|Ab 7',    'G': 'G|C|E',       'G#': 'E|F 0|A maj7',
        'Ab': 'Fm|Fm #5|B 7',   'A': 'F|A|C 0',     'A#': 'F#',
        'Bb': 'C 7|G 0|Gm',     'B': 'G|G 7|C maj7','B#': 'G#'}

    cMinor = {                  'C': 'A|C|F',       'C#': 'A|G 0|D maj7',
        'Db': 'Eb 7|E 0|Db',    'D': 'Dm|E 7|F 6',  'D#': 'BE maj7|C 0',
        'Eb': 'A 0|Cm|F 7',     'E': 'Am|E|C',      'E#': 'C#',
        'Fb': 'Db 0',           'F': 'Dm|G 7|F',    'F#': 'D|G maj7|A 0',
        'Gb': 'A 0|Ab 7|Gb',    'G': 'G|Em|C',      'G#': 'E|A maj7|D 0',
        'Ab': 'F 0|Bb 7|Ab',    'A': 'Am|Dm|F',     'A#': 'F#',
        'Bb': 'Gm|Bb|C 7',      'B': 'E|Em|G',      'B#': 'A 0'}

    keyNote = RelDiatonicNote.fromCircleOfFifth(keyStep)
    assert pitch[1] in (-1, 0, 1) # TODO: abfangen
    relNote = RelDiatonicNote.fromStepAlter(pitch[0]%7, pitch[1]) - keyNote
    p = str(relNote)

    choicesMajor = []
    choicesMinor = []

    def splitNoteExtra(s):
        if len(s) < 2 or s[1] not in '#b':
            return (s[0], s[1:])
        else:
            return (s[:2], s[2:])

    for a in cMajor[p].split('|'):
        base, extra = splitNoteExtra(a)
        baseNote = RelDiatonicNote.fromSymbolic(base) + keyNote
        choicesMajor.append(str(baseNote) + extra)
    for a in cMinor[p].split('|'):
        base, extra = splitNoteExtra(a)
        baseNote = RelDiatonicNote.fromSymbolic(base) + keyNote
        choicesMinor.append(str(baseNote) + extra)
    return (choicesMajor, choicesMinor)

def main():
    sys, staff, voice, obj = getCur()
    if obj:
        time = obj.time()
        key = obj.curKey()
        pitches = sys.pitches(time, True)
        # Änderung JJvB auch Pause erlauben
        #        assert len(pitches) > 0
        minor = []
        major = ['']
        if len(pitches) > 0:
            if len(pitches) == 1:
                major, minor = analyzeSingleNote(pitches[0], key)
            else:
                major = analyzeChord(pitches)
        opt = getOptions(major, minor)
        if opt:
            (s, y, size, sansSerif) = opt
            cs = ChordSymbol(obj, s, y, size, sansSerif)
            sym = cs.create()
            activeScore().registerUndo("Transponierbares Akkordsymbol")
            # WICHTIG: registerUndo() ersetzt die Partitur durch ein Duplikat
            # und speichert das Original zum Rückgängigmachen.
            # Deshalb muss das Cursorobjekt neu ermittelt werden (geänderte Adresse)!
            sys, staff, voice, obj = getCur()
            obj.addDrawObj(sym)

main()

