# -*- coding: ISO-8859-1 -*-
""" capellaScript -- Copyright (c) 2004-2008 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/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)
"""

german = ("de", {
    "size"        : "&Größe",
    "vPos"        : "&Vertikale Lage",
    "descr1"      : "&Bezeichnung",
    "point"       : "Punkt",
    "vGaps2"      : "/2 Zw. über Mittellinie",
    "individ"     : "&individuell:",
    "major"       : "für Dur:",
    "minor"       : "für Moll:",
    "appog"       : "Vorschläge:     ",
    "descr2"      : "Bezeichnung",
    "sansSerif"   : "serifenlose Schrift",
    "chordSymbol" : "Akkordsymbol",
    "undoDescr"   : "Transponierbares Akkordsymbol",
    "error"       : "Fehler",
    "noScore"     : "keine aktive Partitur",
    "badCursor1"  : "Markierung ist nicht leer\nDer Cursor muss vor einem Akkord stehen!",
    "badCursor2"  : "Der Cursor steht nicht vor einem Akkord"})

try:
    from chordSymbols_tr import translations
    translations.append(german)
    setLanguages(translations)
except:
    def tr(s):
        return german[1].get(s, "???")
#-------------------------------------------------------------------

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()
        """
        while s.find("b")>=0 or s.find("#")>=0 :
            # Korrektur JJvB min statt max findet alle Vorzeichen
            posb = s.find("b")
            posh = s.find("#")
            if posb>=0 and posh>=0 :
                i = min(posb,posh)
            else :
                i = max(posb,posh)
            left = s[0:i]
            mid = s[i]
            right = s[i+1:]
            if left != "":  # Korrektur Villiger
                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 Villiger
                    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(tr("size"), width=12)
    labelY = Label(tr("vPos"), width=12)
    labelChord = Label(tr("descr1"), width=12)
    hBox1 = HBox([labelSize, comboSize, Label(tr("point"))], padding=8)
    hBox2 = HBox([labelY, comboY, Label(tr("vGaps2"))], padding=8)
    editSym  = Edit (major[0], width=14)
    if multipleChoice:
        radioDef   = Radio([tr("individ")], value=0)
        radioMajor = Radio(major, value=-1, ext=True)
        radioMinor = Radio(minor, value=-1, ext=True)
        v1 = VBox([Label(tr("major")),  radioMajor], padding=8)
        v2 = VBox([Label(tr("minor")), radioMinor], padding=8)
        h1 = HBox([radioDef, editSym, Label(" ")])
        h2 = HBox([Label(tr("appog")), v1, v2])
        chordBox = VBox([h1, h2], padding=12, text=tr("descr2"))
    else:
        chordBox = HBox([labelChord, editSym], padding=8)

    check = CheckBox (tr("sansSerif"), value=sansSerif)

    vBox   = VBox([chordBox, hBox1, hBox2, check], padding=16)
    title = tr("chordSymbol")
    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(tr("error"), tr("noScore"))
        return result
    if sel[0] != sel[1]:
        messageBox(tr("error"), tr("badCursor1"))
        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", tr("badCursor2"))
    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(tr("undoDescr"))
            # 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()

