# 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

import gtk, gnome
import gu
import inputwidgets
import soundcard, mpd
import dataparser

import const, utils, string
import types
import cfg

class Teacher(cfg.ConfigUtils):
    def __init__(self, exname, app):
        cfg.ConfigUtils.__init__(self, exname)
        self.m_app = app
        self.q_status = const.QSTATUS_NO
        self.m_statistics = None
        self.m_timeout_handle = None
    def get_instrument_config(self, num):
        if self.get_bool('override_default_instrument'):
            instr_low = self.get_int('lowest_instrument')
            instr_low_vel = self.get_int('lowest_instrument_velocity')
            instr_middle = self.get_int('middle_instrument')
            instr_middle_vel = self.get_int('middle_instrument_velocity')
            instr_high = self.get_int('highest_instrument')
            instr_high_vel = self.get_int('highest_instrument_velocity')
        else:
            instr_low = instr_middle = instr_high \
                        = self.get_int('config/preferred_instrument')
            instr_low_vel = instr_middle_vel = instr_high_vel \
                         = self.get_int('config/preferred_instrument_velocity')
        if num == 2:
            return instr_low, instr_low_vel, instr_high, instr_high_vel
        else:
            return instr_low, instr_low_vel, \
                instr_middle, instr_middle_vel, instr_high, instr_high_vel
    def maybe_auto_new_question(self):
        if self.get_bool('new_question_automatically'):
            if self.m_timeout_handle == None:
                def remove_timeout(self=self):
                    self.m_timeout_handle = None
                    self.g_view.new_question()
                self.m_timeout_handle = gtk.timeout_add(int(self.get_float('seconds_before_new_question')*1000),  remove_timeout)
    def end_practise(self):
        if self.m_timeout_handle:
            gtk.timeout_remove(self.m_timeout_handle)
            self.m_timeout_handle = None
        self.q_status = const.QSTATUS_NO
        soundcard.synth.stop()
    def configure_exercise(self, collection, lessonfile, D):
        for k in D.keys():
            self.set_string(k, D[k])

class MelodicIntervalTeacher(Teacher):
    """
    Base class for interval exercises where
    you can have more than one interval.
    When this class was created it was used by melodic-intevall
    and sing-interval.
    """
    OK = 0
    ERR_PICKY = 1
    ERR_CONFIGURE = 2
    def __init__(self, exname, app):
        Teacher.__init__(self, exname, app)
        self.m_tonika = None
        self.m_question = []
    def new_question(self, L, H):
        assert type(L) in types.StringTypes
        assert type(H) in types.StringTypes
        if self.get_list('ask_for_intervals_0') == []:
            return self.ERR_CONFIGURE
        L, H = utils.adjust_low_high_to_irange(L, H,
                     self.get_list('ask_for_intervals_0'))

        if self.m_timeout_handle:
            gtk.timeout_remove(self.m_timeout_handle)
            self.m_timeout_handle = None

        if self.get_bool('config/picky_on_new_question') \
              and self.q_status in [const.QSTATUS_NEW, const.QSTATUS_WRONG]:
            return self.ERR_PICKY

        self.q_status = const.QSTATUS_NO
        last_tonika = self.m_tonika
        last_question = self.m_question
        for x in range(10):# we try max 10 times to get a question that
                           # is different from the last one.
            self.m_tonika, i = utils.random_tonika_and_interval(L, H,
                            self.get_list('ask_for_intervals_0'))
            self.m_question = [i]
            t = self.m_tonika + i
            for x in range(1, self.get_int('number_of_intervals=1')):
                if not self.get_list('ask_for_intervals_%i' % x):
                    return self.ERR_CONFIGURE
                i = utils.random_interval(t, L, H,
                               self.get_list('ask_for_intervals_%i' % x))
                if not i:
                    # if we can't find an interval that is with the range
                    # we, find the interval that is closest to the range
                    # of notes the user want. This mean that the questions
                    # are not necessarily that random.
                    low = mpd.MusicalPitch(L)
                    high = mpd.MusicalPitch(H)
                    off = 1000
                    best = None
                    for interval in self.get_list('ask_for_intervals_%i'%x):
                        if t + interval > high:
                            if t + interval - high < off:
                                off = t + interval - high
                                best = interval
                        if t + interval < low:
                            if low - (t + interval) < off:
                                off = low - (t + interval)
                                best = interval
                    i = best
                self.m_question.append(i)
                t = t + i
            if last_tonika is not None \
                    and last_tonika == self.m_tonika \
                    and last_question == self.m_question:
                continue
            break
        self.q_status = const.QSTATUS_NEW
        return self.OK
    def play_question(self):
        if self.q_status == const.QSTATUS_NO:
            return
        t = self.m_tonika
        m = soundcard.Track()
        m.set_bpm(self.get_int('config/default_bpm'))
        m.set_patch(0, self.get_int('config/preferred_instrument'))
        m.note(4, 0, self.m_tonika.semitone_pitch(),
               self.get_int('config/preferred_instrument_velocity'))
        for i in self.m_question:
            t = t + i
            m.note(4, 0, t.semitone_pitch(),
                   self.get_int('config/preferred_instrument_velocity'))
        soundcard.synth.play_track(m)

class LessonbasedTeacher(Teacher):
    def __init__(self, exname, app, content_type):
        Teacher.__init__(self, exname, app)
        self.m_P = None
        self.m_content_type = content_type
        self.m_question = None
        self.__make_sure_existing_lessonfile_is_selected()
    def set_lessoncollection(self, collection):
        self.set_string('lessoncollection', collection)
        self.__make_sure_existing_lessonfile_is_selected()
        self.parse_lessonfile()
        if self.m_statistics:
            self.m_statistics.lessonfile_changed(
             self.get_string('lessoncollection'), self.get_string('lessonfile'))
    def set_lessonfile(self, file):
        self.set_string('lessonfile', file)
        self.parse_lessonfile()
        if self.m_statistics:
            self.m_statistics.lessonfile_changed(
             self.get_string('lessoncollection'), self.get_string('lessonfile'))
    def __make_sure_existing_lessonfile_is_selected(self):
        if not self.get_string('lessoncollection') or \
               self.get_string('lessoncollection') \
                 not in self.m_app.lessonfile_manager.get_collections():
            self.set_string('lessoncollection', 'solfege')
        F = self.m_app.lessonfile_manager.get_filenames(
                 self.get_string('lessoncollection'), self.m_content_type)
        if F:
            if self.get_string('lessonfile') not in F:
                self.set_string('lessonfile', F[0])
        else:
            self.set_string('lessonfile','')
    def parse_lessonfile(self):
        self.m_question = None
        self.q_status = const.QSTATUS_NO
        if not self.get_string('lessonfile'):
            self.m_P = None
            return
        fn = self.m_app.lessonfile_manager.get_complete_filename(
             self.get_string('lessoncollection'),
             self.get_string('lessonfile'))
        try:
            self.m_P = self.lessonfileclass(fn)
        except IOError:
            self.m_app.m_ui.display_error_message(
"""There was an IOError while trying to parse a lesson file. The cause was probably you deleting or renaming a lesson file. Please don't do this while Solfege is running. The program is still a little fragile. 

You really should reastart the program now.""")
    def play_question(self):
        if self.q_status == const.QSTATUS_NO:
            return
        mpd.play_music(self.m_P.get_music(),
                           self.m_P.get_tempo(),
                           self.get_int('config/preferred_instrument'),
                           self.get_int('config/preferred_instrument_velocity'))
    def configure_exercise(self, lessoncollection, lessonfile, dict):
        if lessoncollection or lessonfile:
            if lessoncollection:
                self.set_string('lessoncollection', lessoncollection)
            if lessonfile:
                self.set_string('lessonfile', lessonfile)
        for k in dict.keys():
            self.set_string(k, dict[k])

class Gui(gtk.VBox, cfg.ConfigUtils):
    """Important members:
         - practise_box
         - action_area
         - config_box
    
    """
    def __init__(self, teacher, window, no_notebook=0):
        gtk.VBox.__init__(self)
        cfg.ConfigUtils.__init__(self, teacher.m_exname)
        #cfg.ConfigUtils.__dict__['__init__'](self, teacher.m_exname)
        self.m_key_bindings = {}
        self.m_t = teacher
        self.g_win = window

        vbox = gtk.VBox()
        vbox.set_spacing(gnome.ui.PAD)
        vbox.set_border_width(gnome.ui.PAD)
        vbox.show()

        self.practise_box = gtk.VBox()
        self.practise_box.show()
        vbox.pack_start(self.practise_box)

        self.action_area = gtk.HBox()
        self.action_area.show()
        vbox.pack_end(self.action_area, gtk.FALSE)

        self.config_box = gtk.VBox()
        self.config_box.set_border_width(gnome.ui.PAD)
        self.config_box.show()
        if no_notebook:
            self.pack_start(vbox)
            self.pack_start(self.config_box, gtk.FALSE)
            self.g_notebook = None
        else:
            self.g_notebook = gtk.Notebook()
            self.pack_start(self.g_notebook)

            self.g_notebook.append_page(vbox, gtk.Label(_("Practise")))
            self.g_notebook.append_page(self.config_box, gtk.Label(_("Config")))
            self.g_notebook.show()
        self.m_exercise_menu = []
    def on_start_practise(self):
        """Dummy function that child classes can override.
        """
        print "dummy abstract.Gui.on_start_practise", self.m_exname
    def on_end_practise(self):
        print "dummy abstract.Gui.on_end_practise", self.m_exname
    def on_key_press_event(self, widget, event):
        if (self.g_notebook == None or self.g_notebook.get_current_page() == 0) \
           and event.type == gtk.gdk.KEY_PRESS:
            for s in self.m_key_bindings.keys():
                if self.keymatch(event, s):
                    self.m_key_bindings[s]()
                    return 1
    def keymatch(self, event, cfname):
        a, b = utils.parse_key_string(self.get_string(cfname))
        return ((event.state & (gtk.gdk.CONTROL_MASK|gtk.gdk.SHIFT_MASK|gtk.gdk.MOD1_MASK)) == a) and (event.keyval == b)
    def setup_statisticsviewer(self, viewclass, heading):
        self.g_statview = viewclass(self.m_t.m_statistics, heading)
        self.g_statview.show()
        self.g_notebook.append_page(self.g_statview, gtk.Label(_("Statistics")))
        self.g_notebook.connect('switch_page', self.on_switch_page)
    def on_switch_page(self, notebook, obj, pagenum):
        if pagenum == 2:
            if isinstance(self.m_t, LessonbasedTeacher):
                if self.m_t.m_P:
                    self.g_statview.update()
                else:
                    self.g_statview.clear()
            else:
                self.g_statview.update()
    def get_pretty_name(self):
        return string.replace(const.exercisedata[self.m_exname][0], '_', '')
    def _add_auto_new_question_gui(self, box):
        hbox = gu.bHBox(box, gtk.FALSE)
        hbox.set_spacing(gnome.ui.PAD_SMALL)
        adj = gtk.Adjustment(0, 0, 10, 0.1, 1, 1)
        spin = gu.nSpinButton(self.m_exname, 'seconds_before_new_question',
                       adj)
        spin.set_digits(1)
        label = gtk.Label(_("Delay (seconds):"))
        label.show()
        def f(button, spin=spin, label=label):
            spin.set_sensitive(button.get_active())
            label.set_sensitive(button.get_active())
        b = gu.nCheckButton(self.m_exname, 'new_question_automatically',
                            _("_New question automatically."), callback=f)
        hbox.pack_start(b, gtk.FALSE)
        label.set_sensitive(b.get_active())
        hbox.pack_start(label, gtk.FALSE)
        spin.set_sensitive(b.get_active())
        hbox.pack_start(spin, gtk.FALSE)

class IntervalGui(Gui):
    """
    Creates 'New interval' and 'Repeat' buttons in the action_area.
    """
    def __init__(self, teacher, window):
        Gui.__init__(self, teacher, window)

        self.g_input = None

        self.g_flashbar = gu.FlashBar()
        self.g_flashbar.show()
        self.practise_box.pack_start(self.g_flashbar, gtk.FALSE)
        self.practise_box.set_spacing(gnome.ui.PAD)

        self.g_new_interval = gu.bButton(self.action_area, _("_New interval"),
                                          self.new_question)
        self.g_repeat = gu.bButton(self.action_area, _("_Repeat"), self.repeat_question)
        self.g_repeat.set_sensitive(gtk.FALSE)
        self.setup_key_bindings()
    def select_inputwidget(self):
        """
        This will be called by HarmonicInterval and MelodicInterval
        constructor
        """
        i = self.get_int('inputwidget')
        if i >= len(inputwidgets.inputwidget_names):
            i = 0
        self.use_inputwidget(i)
    def use_inputwidget(self, i):
        self.set_int('inputwidget', i)
        if self.g_input:
            self.g_input.destroy()
        # FIXME UGH ugly ugly ugly, I'm lazy lazy lazy
        import harmonicinterval
        if isinstance(self, harmonicinterval.Gui):
            v = ['intervals']
        else:
            v = []
            for x in range(self.get_int('maximum_number_of_intervals')):
                v.append('ask_for_intervals_%i' % x)
        if i == 0:#new_widget_name == _("Buttons"):
            self.g_input = inputwidgets.IntervalButtonsWidget(self.m_exname,
                  'intervals', self.click_on_interval, self.get_interval_input_list, v)
        else:
            self.g_input = inputwidgets.name_to_inputwidget(
                                 inputwidgets.inputwidget_names[i], #new_widget_name,
                                 self.click_on_interval)
        self.practise_box.pack_start(self.g_input)
        self.practise_box.reorder_child(self.g_input, 0)
        self.g_input.show()
        self.m_notenamerange.set_range(self.g_input.m_lowest_tone,
                                       self.g_input.m_highest_tone)
        self.on_end_practise()
        self.g_disable_unused_buttons.set_sensitive(self.get_int('inputwidget')==0)
    def setup_key_bindings(self):
        keys = ['minor2', 'major2', 'minor3', 'major3',
                'perfect4', 'diminished5', 'perfect5', 'minor6',
                'major6', 'minor7', 'major7', 'perfect8',
                'minor9', 'major9', 'minor10', 'major10']
        self.m_key_bindings = {}
        for idx in range(len(keys)):
            self.m_key_bindings['interval_input/'+keys[idx]] = lambda idx=idx, self=self: self.click_on_interval(1, idx+1, None)
    def repeat_question(self, *w):
        self.m_t.play_question()
        self.g_input.grab_focus_first_sensitive_button()


class LessonbasedGui(Gui):
    def __init__(self, teacher, window):
        Gui.__init__(self, teacher, window)
    def get_pretty_name(self):
        s = string.replace(const.exercisedata[self.m_exname][0], '_', '')
        if self.get_string('lessoncollection') == 'solfege':
            return "%s - %s" % (s, self.get_string('lessonfile'))
        else:
            return "%s - %s/%s" % (s,
             self.get_string('lessoncollection'), self.get_string('lessonfile'))

