# -*- coding: iso-8859-1 -*-

###############################################################
# Name:             Transpose.py
# Purpose:     Transposing services
# Author:         Luca Allulli (webmaster@roma21.it)
# Created:     2009-11-18
# Copyright: Luca Allulli (https://www.skeed.it/songpress)
# License:     GNU GPL v2
##############################################################

import re
import math

import wx


_ = wx.GetTranslation


class Notation(object):
    def __init__(self, id, desc, chords, repl, replrev):
        object.__init__(self)
        self.id = id
        self.descv = desc
        self.chords = chords
        self.chordDict = {}
        i = 0
        for k in chords:
            self.chordDict[k.upper()] = i
            i += 1
        self.repl = [(re.compile(x[0]), x[1]) for x in repl]
        self.replrev = [(re.compile(x[0]), x[1]) for x in replrev]

    def GetDesc(self):
        return _(self.descv)

    def SetDesc(self, v):
        pass

    desc = property(GetDesc, SetDesc)

    def Ord2Chord(self, pos):
        return self.chords[pos]

    def Chord2Ord(self, chord):
        return self.chordDict[chord.upper()]

    def __AlterationStandard(self, a, rs):
        for r in rs:
            p = 0
            b = ''
            for m in r[0].finditer(a):
                b += a[p:m.start()] + r[1]
                p = m.end()
            b += a[p:]
            a = b
        return a

    def PostprocessingFromStandard(self, c, a):
        return c, a

    def PreprocessingToStandard(self, chord):
        return chord

    def AlterationFromStandard(self, a):
        return self.__AlterationStandard(a, self.repl)

    def AlterationToStandard(self, a):
        return self.__AlterationStandard(a, self.replrev)


enNotation = Notation(
    "enNotation",
    _("American (C D E... B)"),
    ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
    [],
    []
)

itNotation = Notation(
    "itNotation",
    _("Italian (Do Re Mi... Si)"),
    ['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si'],
    [
        (r'maj7', '7+'),
        (r'sus4', '4'),
        (r'^m', '-')
    ],
    [
        (r'7\+', 'maj7'),
        (r'^4', 'sus4'),
        (r'^-', 'm')
    ]
)


itUcNotation = Notation(
    "itUcNotation",
    _("Uppercase Italian (DO RE MI... SI)"),
    ['DO', 'RE', 'MI', 'FA', 'SOL', 'LA', 'SI'],
    [
        (r'maj7', '7+'),
        (r'sus4', '4'),
        (r'^m', '-')
    ],
    [
        (r'7\+', 'maj7'),
        (r'^4', 'sus4'),
        (r'^-', 'm')
    ]
)

frNotation = Notation(
    "frNotation",
    _(u"French (Do Ré Mi... Si)"),
    ['Do', u'Ré', 'Mi', 'Fa', 'Sol', 'La', 'Si'],
    [
        (r'maj7', '7+'),
        (r'sus4', '4'),
    ],
    [
        (r'7\+', 'maj7'),
        (r'^4', 'sus4'),
    ]
)

ptNotation = Notation(
    "ptNotation",
    _(u"Portuguese (Dó Ré Mi... Si)"),
    [u'Dó', u'Ré', 'Mi', u'Fá', 'Sol', u'Lá', 'Si'],
    [
        (r'maj7', '7+'),
        (r'sus4', '4'),
        (r'^m', '-')
    ],
    [
        (r'7\+', 'maj7'),
        (r'^4', 'sus4'),
        (r'^-', 'm')
    ]
)


easyChords = {
    'basic': (_("Basic chords (A, E, D)"), ["A", "E", "D"], 4),
    'Cprog': (_("50s progr. in C (C, Am, Dm, G7)"), ["C", "Am", "Dm", "G7"], 4),
    'F': (_("F chord"), ["F"], 2),
    'Gprog': (_("50s progr. in G (G, Em, Am, D7)"), ["G", "Em", "Am", "D7"], 4),
    'Dprog': (_("50s progr. in D (D, Bm, Em, A7)"), ["D", "Bm", "Em", "A7"], 1),
    'Aprog': (_("50s progr. in A (A, F#m, Bm, E7)"), ["A", "F#m", "Bm", "E7"], 1),
    'C#(m)(7)': (_("C#, C#m, C#7 chords"), ["C#", "C#m", "C#7"], 0),
    'Fprog': (_("50s progr. in F (F, Dm, Gm, C7)"), ["F", "Dm", "Gm", "C7"], 0),
    'BB7': (_("B and B7 chords"), ["B", "B7"], 0),
}

def getEasyChordsDescription(e):
    return _(e[0])

easyChordsOrder = ['basic', 'Cprog', 'F', 'Gprog', 'Dprog', 'Aprog', 'C#(m)(7)', 'Fprog', 'BB7']


class GermanNotation(Notation):
    def PreprocessingToStandard(self, chord):
        if chord.lower().startswith('bb'):
            return "Hb" + chord[2:]
        return chord

    def PostprocessingFromStandard(self, c, a):
        if c == 'Hb':
            c = 'Bb'
        return c, a


trad_de_map = [
    ('C#', 'Cis'),
    ('Db', 'Des'),
    ('Eb', 'Es'),
    ('F#', 'Fis'),
    ('Gb', 'Ges'),
    ('G#', 'Gis'),
    ('Ab', 'As'),
    ('A#', 'Ais'),
    ('Hb', 'B'),
]

trad_de_from = {a.lower(): b for a, b in trad_de_map}
trad_de_to = [(b.lower(), a) for a, b in trad_de_map]


class TraditionalGermanNotation(Notation):
    def PreprocessingToStandard(self, chord):
        cl = chord.lower()
        for a, b in trad_de_to:
            if cl.startswith(a):
                return b + chord[len(a):]
        return chord

    def PostprocessingFromStandard(self, c, a):
        c = trad_de_from.get(c.lower(), c)
        if a != "" and a[0] == 'm' and not a.startswith('maj'):
            c = c.lower()
        return c, a


deNotation = GermanNotation(
    "deNotation",
    _("German/Sacandinavian (C C#/Db D... Bb H)"),
    ['C', 'D', 'E', 'F', 'G', 'A', 'H'],
    [],
    []
)


tradDeNotation = TraditionalGermanNotation(
    "tradDeNotation",
    _("Traditional German (C Cis/Des D... B H)"),
    ['C', 'D', 'E', 'F', 'G', 'A', 'H'],
    [
        ('C#', 'Cis'),
        ('Db', 'Des'),
    ],
    [
        ('C#', 'Cis'),
        ('Db', 'Des'),
    ]
)


defaultLangNotation = {
    'en': enNotation,
    'it': itNotation,
    'fr': frNotation,
    'de': deNotation,
}


naturalScale = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

tone = {
    'C': 0,
    'D': 2,
    'E': 4,
    'F': 5,
    'G': 7,
    'A': 9,
    'B': 11
}

interval = [
 (0, 0),
 (0, 1),
 (1, 0),
 (1, 1),
 (2, 0),
 (3, 0),
 (3, 1),
 (4, 0),
 (4, 1),
 (5, 0),
 (5, 1),
 (6, 0),
 (6, 1),
 (7, 0)
]

scales = {
    'C': (0, ['C', 'D', 'E', 'F', 'G', 'A', 'B']),
    'Db': (1, ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C']),
    'D': (2, ['D', 'E', 'F#', 'G', 'A', 'B', 'C#']),
    'Eb': (3, ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D']),
    'E': (4, ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#']),
    'F': (5, ['F', 'G', 'A', 'Bb', 'C', 'D', 'E']),
    'Gb': (6, ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F']),
    'G': (7, ['G', 'A', 'B', 'C', 'D', 'E', 'F#']),
    'Ab': (8, ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G']),
    'A': (9, ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#']),
    'Bb': (10, ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A']),
    'B': (11, ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'])
}
orderedKeys = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']

vectorModes = ['', 'm', '7', 'm7']

referenceVector = [0.65713162540630443, 0.0, 0.037841466800806009, 0.0, 0.0, 0.0014554410308002313, 0.0, 0.0, 0.026197938554404162, 0.12444020813341977, 0.0014554410308002313, 0.029108820616004623, 0.0, 0.0, 0.0, 0.0, 0.040024628347006361, 0.12589564916422, 0.030564261646804855, 0.0036386025770005779, 0.46210252727907342, 0.013098969277202081, 0.0, 0.0, 0.0, 0.00072772051540011566, 0.0, 0.0, 0.49703311201827893, 0.0, 0.016009851338802544, 0.0, 0.00072772051540011566, 0.0, 0.0, 0.0, 0.0094603667002015022, 0.2648902676056421, 0.0058217641232009253, 0.0043663230924006939, 0.0014554410308002313, 0.0, 0.00072772051540011566, 0.0, 0.0, 0.0014554410308002313, 0.0, 0.0]


def splitChord(c, locNotation=enNotation):
    for k in locNotation.chords:
        if c.upper().startswith(k.upper()):
            if len(c) != len(k):
                d = c[len(k)]
                if d == "b" or d == "#":
                    return (k+d, c[(len(k) + 1):])
                return (k, c[len(k):])
            return (c, "")
    return ("", c)


def __alteration(chord):
    if len(chord) == 1:
        return (chord, 0)
    elif chord[1] == '#':
        return (chord[0], 1)
    else:
        return (chord[0], -1)


def chord2pos(chord, key="C"):
    c, a = __alteration(chord)
    s, b = __alteration(key)
    return (tone[c.upper()] + a - tone[s.upper()] - b) % 12


def __pos2chord(pos, key):
    n, i = interval[pos]
    # if pos in scale, use it
    if i == 0:
        return scales[key][1][n]
    # else use natural scale
    ref = scales[key][0]
    diff = (pos + ref) % 12
    return naturalScale[diff]


def transpose(s, d, chord, notation=enNotation):
    sl = chord.find("/")
    if sl > -1:
        return "%s/%s" % (transpose(s, d, chord[:sl], notation), transpose(s, d, chord[sl + 1:], notation))
    chord = translateChord(chord, notation, enNotation)
    c, v = splitChord(chord)
    if c == "":
        return chord
    p = chord2pos(c, s)
    return translateChord(__pos2chord(p, d) + v, enNotation, notation)


def translateChord(chord, sNotation=enNotation, dNotation=enNotation):
    #if sNotation == dNotation:
    #    return chord
    sl = chord.find("/")
    if sl > -1:
        return "%s/%s" % (translateChord(chord[:sl], sNotation, dNotation), translateChord(chord[sl + 1:], sNotation, dNotation))
    chord = sNotation.PreprocessingToStandard(chord)
    c, a = splitChord(chord, sNotation)
    if c == "":
        return chord
    alt = c[-1]
    if alt == '#' or alt == 'b':
        c = c[:-1]
    else:
        alt = ""
    if c == "":
        return chord
    d = dNotation.Ord2Chord(sNotation.Chord2Ord(c)) + alt
    b = dNotation.AlterationFromStandard(sNotation.AlterationToStandard(a))
    d, b = dNotation.PostprocessingFromStandard(d, b)
    return d + b


def transposeChordPro(s, d, text, notation=enNotation):
    r = re.compile('\[([^]]*)\]')
    p = 0
    b = ''
    for m in r.finditer(text):
        b += "%s[%s]" % (
            text[p:m.start()],
            transpose(s, d, m.group(1), notation)
        )
        p = m.end()
    return b + text[p:]


def translateChordPro(text, sNotation=enNotation, dNotation=enNotation):
    r = re.compile('\[([^]]*)\]')
    p = 0
    b = ''
    for m in r.finditer(text):
        b += "%s[%s]" % (
            text[p:m.start()],
            translateChord(m.group(1), sNotation, dNotation)
        )
        p = m.end()
    return b + text[p:]


def autodetectNotation(text, notations):
    r = re.compile('\[([^]]*)\]')
    cnt = [0 for x in notations]
    for m in r.finditer(text):
        for i in range(0, len(notations)):
            c, a = splitChord(m.group(1), notations[i])
            if c != "":
                cnt[i] += 1
    return notations[cnt.index(max(cnt))]


def normalize(vector):
    count = 0
    for c in vector:
        count += c**2
    if count != 0:
        count = math.sqrt(count)
        return [x/count for x in vector]
    else:
        return vector


def scalarProduct(v1, v2):
    count = 0
    for i in range(0, len(v1)):
        count += v1[i]*v2[i]
    return count


def vectorizeChords(text, notation=enNotation):
    r = re.compile('\[([^]]*)\]')
    v = [0 for x in range(12 * len(vectorModes))]
    for m in r.finditer(text):
        chord = translateChord(m.group(1), notation, enNotation)
        c, a = splitChord(chord)
        if c != "" and a in vectorModes:
            v[chord2pos(c, "C") * len(vectorModes) + vectorModes.index(a)] += 1
    return normalize(v)


def autodetectKey(text, notation=enNotation):
    v = vectorizeChords(text, notation)
    r = referenceVector
    max = 0
    key = 0
    n = len(vectorModes)
    for k in range(0, 12):
        s = scalarProduct(v, r)
        if s > max:
            max = s
            key = k
        r = r[-n:] + r[:-n]
    return orderedKeys[key]


def integrateChords(chords, text):
    """Integrate chord line in text line, as chordpro"""
    r = re.compile(r'(\S+)')
    i = r.finditer(chords)
    l = [x for x in i]
    l.reverse()
    if l != []:
        lc = l[0].start()
        lt = len(text)
        for i in range(0, lc-lt):
            text += ' '
        for m in l:
            s = m.start()
            text = text[:s] + '[' + m.group() + ']' + text[s:]
    return text


def testChordLine(line, notation=enNotation):
    """Return True iff line contains only chords"""
    # First, tokenize line: if a token is not a chord => False
    r = re.compile(r'(\S+)')
    l = 0
    n = 0
    for m in r.finditer(line):
        c = m.group()
        c = notation.PreprocessingToStandard(c)
        e, a = splitChord(c, notation)
        if e == '':
            return False
        l += len(translateChord(c, notation, enNotation))
        n += 1
    # We don't consider an empty line a chord line
    if n == 0:
        return False
    # Looks okay... if a whitespace is occurring twice => True
    if line.find("  ") != -1:
        return True
    # Uhm, weird... are chord tokens "short"? Yes <=> True
    return l/float(n) <= 2.3


def testTabFormat(text, notations):
    """
    Test whether text is in tab format

    It happens iff at least 3 lines are chord lines.
    Return the best matching notation, or None
    notations is a list of notations to be tested
    """
    lines = text.splitlines()
    max = 0
    maxn = None
    for n in notations:
        nc = 0
        for l in lines:
            if testChordLine(l, n):
                nc += 1
        if nc > max and nc >= 3:
            max = nc
            maxn = n
    return maxn


def tab2ChordPro(text, notation=enNotation):
    """
    Convert text from tab to chordpro format
    """
    l = text.splitlines()
    n = len(l)
    i = 0
    out = []
    while i < n:
        if i + 1 < n and testChordLine(l[i], notation) and not testChordLine(l[i+1], notation):
            out.append(integrateChords(l[i], l[i+1]))
            i += 2
        else:
            out.append(l[i])
            i += 1
    return "\n".join(out)


def testSpuriousLines(text):
    """
    Determine if there are spurious empty lines

    It happens iff at least 1/3 of the lines are empty,
    and there are at least 3 non-empty lines
    """
    c = 0
    ll = text.splitlines()
    for l in ll:
        if l.strip() == '':
            c += 1
    return c >= len(ll)/3.0 and len(ll) - c >= 3


def removeSpuriousLines(text):
    """
    Remove spurious blank lines from text

    If a blank line is isolated, remove it
    If there are several contiguous blank lines, keep only one of them
    """
    out = []
    ll = text.splitlines()
    ln = len(ll)
    i = 0
    def empty(l):
        return l.strip() == ''
    while i < ln:
        if empty(ll[i]):
            if i + 1 < ln and empty(ll[i + 1]):
                out.append('')
                i += 2
                while i < ln and empty(ll[i]):
                    i += 1
            else:
                i += 1
        else:
            out.append(ll[i])
            i += 1
    return "\n".join(out)


def findEasiestKey(text, fav, notation=enNotation):
    """
    Find easiest key for song.

        text: song text
        fav: dictionary of (s)favourite chords, with the form {chord: weight}
             (chords in fav are expressed using enNotation)
        return (chord_count, current_key, current_difficulty, easiest_key, easiest_difficulty)
    """
    current_key = autodetectKey(text, notation)
    ws = dict([(k, 0) for k in scales])
    r = re.compile('\[([^]]*)\]')
    count = 0
    for m in r.finditer(text):
        count += 1
        chord = translateChord(m.group(1), notation)
        for k in ws:
            c = transpose(current_key, k, chord)
            if c in fav:
                ws[k] += fav[c]
    easiest_key = current_key
    m = ws[easiest_key]
    for k in ws:
        if m is None or ws[k] > m:
            easiest_key = k
            m = ws[k]
    difficulty = lambda x: max(0, min(1, (count - x) / float(count)))
    if count > 0:
        return (
                count,
                translateChord(current_key, enNotation, notation),
                difficulty(ws[current_key]),
                translateChord(easiest_key, enNotation, notation),
                difficulty(m),
            )
    return (0, current_key, 0, current_key, 0)


def removeChords(text):
    """
    Remove all chords in (ChordPro) text.

        text: song text
        return: text without chords
    """
    return re.sub('\[([^]]*)\]', "", text)


def removeChordPro(text):
    """
    Remove every ChordPro tag, including chords, from text.

        text: song text
        return: text without ChordPro stuff and chords
    """
    return removeSpuriousLines(re.sub('(\[([^]]*)\]|{[^}]*})', "", text))


def replaceTitles(text, replace_string):
    """
    Replace {t:something} and {tititle:something} commands with a string

    :param text: ChordPro song
    :param replace_string: string to be replaced to titles
    :return:
    """
    return re.sub(r'({t:[^}]*}|{title:[^}]*})', replace_string, text)


def pasteChords(src, dest):
    """
    Paste chords from src to dest

    Remove any existing chords in lines in the first k lines of dest,
    if there are k lines in src.
        src: source chordpro text
        dest: destination text
    """
    ss = src.splitlines()
    sd = dest.splitlines()
    out = []
    m = min(len(ss), len(sd))
    r = re.compile('\[([^]]*)\]')
    for i in range(0, m):
        cd = r.sub("", sd[i])
        for x in r.finditer(ss[i]):
            s = x.start()
            if len(cd) < s:
                cd += "".join([" " for y in range(len(cd), s)])
            cd = cd[:s] + x.group(0) + cd[s:]
        out.append(cd)
    for i in range(m, len(sd)):
        out.append(r.sub("", sd[i]))
    return "\n".join(out)
