# GNU Solfege - eartraining for GNOME
# Copyright (C) 2000, 2001, 2002  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

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

import dataparser


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

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 _parse_file(self, filename):
        parser = dataparser.Dataparser(predef, ('tempo',))
        parser.parse_file(filename)
        self._idx = 0
        self.m_transpose = mpd.MusicalPitch("c'")
        H = filter(lambda e: e['blocktype'] == 'header', parser.m_blocks)
        self.header = _Header(H[0])
        self.m_questions = filter(lambda e: e['blocktype'] == 'question',
                                  parser.m_blocks)
        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 question by random. Set the self.m_transpose variable if
        the lessonfile say the question should be transposed.
        """
        old_music = self.get_music()
        while 1:
            self._idx = random.randint(0, len(self.m_questions) - 1)
            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[0]),
                  mpd.transpose_notename("c'", self.header.random_transpose[1]))
            if old_music == self.get_music() \
                and (len(self.m_questions) > 1 or self.header.random_transpose):
                continue
            break
    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.str(), 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.str())
            s = string.replace(s, r'\addvoice',
               r'\addvoice\transpose %s' % self.m_transpose.str())
        return s


class DictationLessonfile(LessonfileCommon):
    def __init__(self, filename):
        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):
        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(x).transpose_by_musicalpitch(self.m_transpose)).str())
            return v
        return string.split(self.m_questions[self._idx]['music'], '|')
    def get_music(self):
        v = string.split(self.m_questions[self._idx]['music'], '|')
        k = "c \major"
        if self.m_questions[self._idx].has_key('key'):
            k = self.m_questions[self._idx]['key']
        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.str())
            music = string.replace(music, r"\addvoice",
                      r"\addvoice\transpose %s" % self.m_transpose.str())
        return music
    def select_random_question(self):
        key = "c \major"
        if self.m_questions[self._idx].has_key('key'):
            key = self.m_questions[self._idx]['key']
        v = string.split(key, " ")
        k = mpd.MusicalPitch(v[0]).str()
        n = ['des', 'aes', 'ees', 'bes', 'f', 'c',
             'g', 'd', 'a', 'e', 'b', 'fis', 'cis', 'gis']
        if k not in n:
            raise "bad key signature in sing-chord lessonfile"
        i = n.index(k) - 5
        if v[1] == '\minor':
            i = i - 3
        d = {
              0: (1, 0, 0, 0),
             -1: (1, 3, 0, 0),
             -2: (-1, 1, 0, 0),
             -3: ( 1, 2, -1, 0),
             -4: (-1, 2, 0, 0),
             -5: ( 1, 1, -1, 0),
             -6: (-1, 3, 1, 0),
             -7: ( 1, 0, -1, 0),
             -8: ( 1, 3, -1, 0),
             -9: (-1, 1, 1, 0),
             -10: (1, 2, -2, 0),
             }
        for x in range(-10, -0):
            d[-x] = (-d[x][0], d[x][1], d[x][2], d[x][3])

        old_music = self.get_music()
        while 1:
            self._idx = random.randint(0, len(self.m_questions) - 1)
            if self.header.random_transpose:
                self.m_transpose = mpd.MusicalPitch("c'") \
                    + apply(mpd.Intervall()._set, d[random.randint(-5-i, 5-i)])
            if old_music == self.get_music() \
                and (len(self.m_questions) > 1 or self.header.random_transpose):
                continue
            break


class IdByNameLessonfile(LessonfileCommon):
    def __init__(self, filename):
        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 "discarding question in the lessonfile '%s'" % filename
                print "because of missing 'name' variable"
                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']
    def get_name(self):
        """
        Return the translated name for the selected question.
        """
        return self.m_names[self.m_questions[self._idx]['name']]


class ChordLessonfile(LessonfileCommon):
    def __init__(self, filename):
        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 curretly 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(n).transpose_by_musicalpitch(
                 self.m_transpose).str())
        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)
                    p.parse_file(self.get_complete_filename(collection, filename), 1)
                except dataparser.DataparserException:
                    self.m_invalid_files.append(filename)
                    continue
                c = p.m_blocks[0]['content']
                if type(c) == types.StringType:
                    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>"""
        for collection in self.m_db.keys():
            self.m_htmldoc = self.m_htmldoc + "<h1>%s</h1>" % collection
            for use in self.m_db[collection].keys():
                self.m_htmldoc = self.m_htmldoc + "<h2>%s</h2>" % use
                for file in self.m_db[collection][use]:
                    P = {const.USE_CHORD: 'chord',
                         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> " % (P, collection, file, file)
        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 []

