# GNU Solfege - eartraining for GNOME
# Copyright (C) 2000, 2001, 2002, 2003  Tom Cato Amundsen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


# We have the following to make module testing work.
try:
    if app_running == 'solfege':
        pass
except:
    import sys; sys.path.append(".")
    import src.i18n
    import pygtk
    pygtk.require('2.0')


import re, string, types, os, sys
import const
import random
import mpd, cfg, utils
from soundcard.rat import Rat

import dataparser

# The keys in the dict say how many steps up or down in the
# circle of fifths we go if we transpose.
_keys_to_interval = {
              -10: '-M6', #c -> eses
               -9: '-a2', #c -> beses
               -8: '-a5', #c -> fes
               -7: '-au', #c -> ces
               -6: '-a4', #c -> ges
               -5: 'm2', # c -> des
               -4: '-M3',# c -> as
               -3: 'm3', # c -> es
               -2: '-M2', # c -> bes
               -1: '4', # c -> f
                0: 'u',
                1: '-4', # c -> g,
                2: 'M2', # c -> d
                3: '-m3',# c -> a
                4: 'M3', # c -> e
                5: '-m2', # c -> b
                6: 'a4', # c -> fis
                7: 'au', #c -> cis
                8: 'a5', # c -> gis
                9: 'a2', # c -> dis
                10: 'M6', # c -> ais
            }

predef = {'dictation': 'dictation',
          'progression': 'progression',
          'harmony': 'harmony',
          'sing-chord': 'sing-chord',
          'chord-voicing': 'chord-voicing',
          'chord': 'chord',
          'id-by-name': 'id-by-name',
          'satb': 'satb',
          'horiz': 'horiz',
          'vertic': 'vertic',
          'yes': 1,
          'no': 0,
          'accidentals': 'accidentals',
          'key': 'key',
          'semitones': 'semitones',
          'tempo': (60, 4)}

lessonfile_functions = {
        '_': _,
        '_i': _i,
}

class _Header:
    def __init__(self, headerdict):
        self.m_headerdict = headerdict
        for s in ('description', 'labelformat', 'title', 'version'):
            if not self.m_headerdict.has_key(s):
                self.m_headerdict[s] = ""
    def __getattr__(self, name):
        if self.m_headerdict.has_key(name):
            return dataparser.get_translated_string(self.m_headerdict, name)
        if name == 'filldir':
            return 'vertic'


class LessonfileCommon:
    def __init__(self):
        self.m_prev_question = None
    def _parse_file(self, filename):
        parser = dataparser.Dataparser(predef, lessonfile_functions, ('tempo',))
        parser.parse_file(filename)
        self._idx = 0
        self.m_transpose = mpd.MusicalPitch.new_from_notename("c'")
        self.header = _Header(parser.header)
        self.m_questions = parser.questions
        if utils.compare_version_strings(self.header.version, "1.1.3") == -1:
            def convert(q):
                for n in ('music', 'clue_music'):
                    if not q.has_key(n):
                        continue
                    re_old_times = re.compile(r"\\time\s+(\d+)\s*/\s*(\d+);")
                    r = re_old_times.search(q[n])
                    while r:
                        q[n] = q[n][:r.start()]+r"\time %s/%s" % (r.group(1), r.group(2))+q[n][r.end():]
                        r = re_old_times.search(q[n])
                    re_old_key = re.compile(r"\s*\\key\s+([a-z]+)\s*\\(major|minor);")
                    r = re_old_key.search(q[n])
                    while r:
                        q[n] = q[n][:r.start()]+r"\key %s \%s" % (r.group(1), r.group(2))+q[n][r.end():]
                        r = re_old_key.search(q[n])
                    re_old_clef = re.compile(r"\s*\\clef\s+(\w*);")
                    r = re_old_clef.search(q[n])
                    while r:
                        q[n] = q[n][:r.start()]+r" \clef %s" % r.group(1) + q[n][r.end():]
                        r = re_old_clef.search(q[n])
                return q
            map(convert, self.m_questions)
    def select_random_question(self):
        """
        Select a new question by random.
        """
        count = 0
        while 1:
            count += 1
            self._idx = random.randint(0, len(self.m_questions) - 1)
            if self.header.random_transpose:
                if self.header.random_transpose == 1:
                    self.handle_random_transpose_is_yes()
                # to handle old style random_transpose = -4, 5
                elif len(self.header.random_transpose) == 2:
                    self.header.random_transpose \
                        = ['semitones'] + self.header.random_transpose
                if self.m_questions[self._idx].has_key('key'):
                    key = self.m_questions[self._idx]['key']
                else:
                    key = "c \major"
                if self.header.random_transpose[0] == 'semitones':
                    self._semitone_find_random_transpose()
                else:
                    self._find_random_transpose(key)
            if count == 10:
                break
            if self.m_prev_question == self.get_music() \
                and (len(self.m_questions) > 1 or self.header.random_transpose):
                continue
            break
        self.m_prev_question = self.get_music()
    def _semitone_find_random_transpose(self):
        if self.header.random_transpose == 1:
            self.m_transpose = mpd.MusicalPitch().randomize("g", "fis'")
        elif type(self.header.random_transpose) == type([]):
            self.m_transpose = mpd.MusicalPitch().randomize(
              mpd.transpose_notename("c'", self.header.random_transpose[1]),
              mpd.transpose_notename("c'", self.header.random_transpose[2]))
    def _find_random_transpose(self, key):
        """
        Pick a random interval to transpose the question.
        Set the variable self.m_transpose .
        This function transposes in "accidentals" mode.
        """
        low, high = self.header.random_transpose[1:3]
        tone, minmaj = string.split(key, " ")
        k = mpd.MusicalPitch.new_from_notename(tone).get_octave_notename()
        #FIXME this list say what key signatures are allowed in sing-chord
        # lesson files. Get the correct values and document them.
        kv = ['des', 'aes', 'ees', 'bes', 'f', 'c',
              'g', 'd', 'a', 'e', 'b', 'fis', 'cis', 'gis']
        # na tell the number of accidentals (# is positive, b is negative)
        # the question has from the lessonfile before anything is transpose.
        na = kv.index(k) - 5
        if minmaj == '\\minor':
            na -= 3
        if self.header.random_transpose[0] == 'accidentals':
            # the number of steps down the circle of fifths we can go
            n_down = low - na
            # the number of steps up the circle of fifths we can go
            n_up = high - na
        else:
            assert self.header.random_transpose[0] == 'key'
            n_down = low
            n_up = high
        interv = mpd.Interval()
        interv.set_from_string(_keys_to_interval[random.choice(range(n_down, n_up+1))])
        self.m_transpose = mpd.MusicalPitch.new_from_notename("c'") + interv
    def handle_random_transpose_is_yes(self):
        self.header.random_transpose = ['semitones', -5, 6]
    def get_tempo(self):
        return self.m_questions[self._idx]['tempo']
    def get_name(self):
        if self.m_questions[self._idx].has_key('name'):
            return self.m_questions[self._idx]['name']
        return ""
    def get_music(self):
        """
        Some of the lessonfile classes can use this. They override it
        if they have more special needs.
        """
        if self.header.musicformat == 'chord':
            return r"\staff\transpose %s{< %s > }" \
              % (self.m_transpose.get_octave_notename(), self.m_questions[self._idx]['music'])
        s = self.m_questions[self._idx]['music']
        if self.header.random_transpose:
            s = string.replace(s, r'\staff',
               r'\staff\transpose %s' % self.m_transpose.get_octave_notename())
            s = string.replace(s, r'\addvoice',
               r'\addvoice\transpose %s' % self.m_transpose.get_octave_notename())
        return s


class DictationLessonfile(LessonfileCommon):
    def __init__(self, filename):
        LessonfileCommon.__init__(self)
        self._parse_file(filename)
    def get_breakpoints(self):
        r = []
        if self.m_questions[self._idx].has_key('breakpoints'):
            r = self.m_questions[self._idx]['breakpoints']
            if not type(r) == type([]):
                r = [r]
        r = map(lambda e: Rat(e[0], e[1]), r)
        return r
    def get_clue_end(self):
        if self.m_questions[self._idx].has_key('clue_end'):
            return apply(Rat, self.m_questions[self._idx]['clue_end'])
    def get_clue_music(self):
        if self.m_questions[self._idx].has_key('clue_music'):
            return self.m_questions[self._idx]['clue_music']
    def select_previous(self):
        if self._idx > 0:
            self._idx = self._idx - 1
    def select_next(self):
        if self._idx < len(self.m_questions) -1:
            self._idx = self._idx + 1

class SingChordLessonfile(LessonfileCommon):
    def __init__(self, filename):
        LessonfileCommon.__init__(self)
        self._parse_file(filename)
    def get_satb_vector(self):
        """
        return transposed notenames using self.m_transpose
        """
        if self.header.random_transpose:
            v = []
            for x in string.split(self.m_questions[self._idx]['music'], '|'):
                v.append((mpd.MusicalPitch.new_from_notename(x).transpose_by_musicalpitch(self.m_transpose)).get_octave_notename())
            return v
        return string.split(self.m_questions[self._idx]['music'], '|')
    def get_music(self):
        v = string.split(self.m_questions[self._idx]['music'], '|')
        if self.m_questions[self._idx].has_key('key'):
            k = self.m_questions[self._idx]['key']
        else:
            k = "c \major"
        music = r"""
                \staff{ \key %s\stemUp <%s> }
                \addvoice{ \stemDown <%s> }
                \staff{ \key %s\clef bass \stemUp <%s>}
                \addvoice{ \stemDown <%s>}
                """ % (k, v[0], v[1], k, v[2], v[3])
        if self.header.random_transpose:
            music = string.replace(music, r"\staff",
                      r"\staff\transpose %s" % self.m_transpose.get_octave_notename())
            music = string.replace(music, r"\addvoice",
                      r"\addvoice\transpose %s" % self.m_transpose.get_octave_notename())
        return music

class IdByNameLessonfile(LessonfileCommon):
    def __init__(self, filename):
        LessonfileCommon.__init__(self)
        self.parse_file(filename)
    def parse_file(self, filename):
        self._parse_file(filename)
        self.m_names = {}
        self.m_names_in_file_order = []
        for question in self.m_questions:
            if not question.has_key('name'):
                print >> sys.stderr, "IdByNameLessonfile.parse_file: discarding question in the lessonfile '%s'\nbecause of missing 'name' variable" % filename
                continue
            if not self.m_names.has_key(question['name']):
                self.m_names[question['name']] = \
                        dataparser.get_translated_string(question, 'name')
                self.m_names_in_file_order.append(question['name'])
    def get_untranslated_name(self):
        """
        Return the untranslated name for the selected question.
        """
        return self.m_questions[self._idx]['name']

class ChordLessonfile(LessonfileCommon):
    def __init__(self, filename):
        LessonfileCommon.__init__(self)
        self.parse_file(filename)
    def parse_file(self, filename):
        self._parse_file(filename)
        # C-name = key, translated-name = value
        self.m_chord_types = {}
        self.m_inversions = []
        self.m_toptones = []
        for question in self.m_questions:
            self.m_chord_types[question['name']] = dataparser.get_translated_string(question, 'name')

            # inversion
            if question.has_key('inversion') \
                    and question['inversion'] not in self.m_inversions:
                self.m_inversions.append(question['inversion'])
            # toptone
            if question.has_key('toptone') \
                    and question['toptone'] not in self.m_toptones:
                self.m_toptones.append(question['toptone'])
    def get_music(self):
        """ Returns the music for the currently selected question.
        The function will transpose the question if the lessonfile
        header say random_transpose = 1
        """
        if not self.header.random_transpose:
            return self.m_questions[self._idx]['music']
        v = []
        for n in string.split(self.m_questions[self._idx]['music']):
            v.append(mpd.MusicalPitch.new_from_notename(n).transpose_by_musicalpitch(
                 self.m_transpose).get_octave_notename())
        return string.join(v)
    def get_inversion(self):
        """
        Return 1 for root position, 3 for first inversion etc.
        Return -1 if no inversion info is set in question.
        """
        if self.m_questions[self._idx].has_key('inversion'):
             return self.m_questions[self._idx]['inversion']
        return -1
    def get_toptone(self):
        if self.m_questions[self._idx].has_key('toptone'):
            return self.m_questions[self._idx]['toptone']
        return -1
    def get_chordtype(self):
        return self.m_questions[self._idx]['name']

class LessonFileManager:
    def __init__(self):
        self.parse()
    def parse(self):
        self.m_db = {}
        self.m_invalid_files = []
        vim_tmpfile_re = re.compile("\..*\.swp")
        for collection in string.split(cfg.get_string("config/lessoncollections")):
            self.m_db[collection] = {}
            if not os.path.isdir(os.path.expanduser(cfg.get_string("lessoncollections/%s" % collection))):
                continue
            # filter out backup files
            v = filter(lambda x, r=vim_tmpfile_re: x[-1:] != '~' and not r.match(x), os.listdir(os.path.expanduser(cfg.get_string("lessoncollections/%s" % collection))))
            for filename in v:
                # since I usually run solfege from the source dir:
                if filename in ('CVS', 'Makefile', 'Makefile.in') \
                  or not os.path.isfile(self.get_complete_filename(collection, filename)):
                    continue
                try:
                    p = dataparser.Dataparser(predef, lessonfile_functions)
                    p.parse_file(self.get_complete_filename(collection, filename))
                except dataparser.DataparserException, e:
                    self.m_invalid_files.append((collection, filename))
                    print >> sys.stderr, "Discarding lessonfile '%s/%s' because the file contain errors:" % (collection, filename)
                    print >> sys.stderr, str(e)
                    continue
                c = p.header['content']
                if type(c) in types.StringTypes:
                    c = [c]
                for u in c:
                    if not self.m_db[collection].has_key(u):
                        self.m_db[collection][u] = []
                    self.m_db[collection][u].append(filename)
        for collection in self.m_db.keys():
            for content in self.m_db[collection].keys():
                self.m_db[collection][content].sort()
        self.m_htmldoc = """<html><body><p>This list is updated each time
                you start Solfege. As you can see, Solfege need more
                lessonfiles. <a href="lessonfiles.html">Write one!</a>"""
        if self.m_invalid_files:
            self.m_htmldoc += "<h1>Files we failed to parse</h1><ul>"
            for collection, filename in self.m_invalid_files:
                self.m_htmldoc += "<li>%(filename)s in collection %(collection)s</li>" % {'collection': collection, 'filename': filename}
            self.m_htmldoc += "</ul>"
        for collection in self.m_db.keys():
            self.m_htmldoc = self.m_htmldoc + "<h1>collection: %s</h1>" % collection
            for use in self.m_db[collection].keys():
                self.m_htmldoc = self.m_htmldoc + "<h2>%s</h2><p>" % use
                for fn in self.m_db[collection][use]:
                    P = {const.USE_CHORD: 'chord',
                         const.USE_CHORD_VOICING: 'chord-voicing',
                         const.USE_HARMONY: 'harmonic-progression-dictation',
                         const.USE_ID_BY_NAME: 'id-by-name',
                         const.USE_DICTATION: 'dictation',
                         const.USE_SING_CHORD: 'sing-chord'}[use]
                    self.m_htmldoc = self.m_htmldoc + \
                      "<a href=\"solfege:practise/%s/%s/%s\">%s</a>\n" % (P, collection, fn, fn)
                self.m_htmldoc += "</p>"
        self.m_htmldoc = self.m_htmldoc + "</body></html>"
    def get_collections(self):
        """
        Return a list of available collections. 
        """
        return self.m_db.keys()
    def get_complete_filename(self, catalog, fn):
        """
        Return the absolute path to a file in a given collection
        """
        if not catalog:
            catalog = 'solfege'
        return os.path.expanduser(os.path.join(
         cfg.get_string('lessoncollections/%s' % catalog), fn))
    def get_filenames(self, collection, usetype):
        if self.m_db[collection].has_key(usetype):
            return self.m_db[collection][usetype]
        else:
            return []

if __name__ == '__main__':
    import src, src.i18n
    import time
    cfg.initialise(None, "default.config",
        "~/.solfegerc1.9")
    t1=time.clock()
    for n in range(10):
        #p = LessonfileCommon()
        #p._parse_file(sys.argv[1])
        p = LessonFileManager()
    t2 = time.clock()
    print t2-t1
