# 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, widgets
import soundcard, mpd, mpd.musicdisplayer
import abstract, const, lessonfile
import string
import utils
import soundcard
import dataparser

class Teacher(abstract.LessonbasedTeacher):
    OK = 0
    ERR_PICKY = 1
    # valid values for self.q_status:
    # QSTATUS_NO       at program startup
    # QSTATUS_NEW      after the new button has been pressed
    # QSTATUS_SOLVED   when all three questions have been answered
    # QSTATUS_GIVE_UP  after 'Give Up' has been pressed.
    CORRECT = 1
    ALL_CORRECT = 2
    def __init__(self, exname, app):
        abstract.LessonbasedTeacher.__init__(self, exname, app, const.USE_CHORD)
        self.lessonfileclass = lessonfile.ChordLessonfile
        try: # the lessonfile might contain errors.
            self.parse_lessonfile()
        except dataparser.DataparserException, e:
            self.m_app.m_ui.display_error_message(str(e))
    def new_question(self):
        """
        return OK or ERR_PICKY
        UI will never call this function unless we have a usable lessonfile.
        """
        assert self.m_P
        if self.get_bool('config/picky_on_new_question') \
           and (not self.q_status in (const.QSTATUS_NO, const.QSTATUS_SOLVED)):
           return self.ERR_PICKY
        self.m_P.select_random_question()
        self.m_correct = [self.m_P.get_chordtype() == -1,
                          self.m_P.get_inversion() == -1,
                          self.m_P.get_toptone() == -1]
        self.q_status = const.QSTATUS_NEW
        return self.OK
    def _generate_question_vector(self):
        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')
        m = map(string.strip, string.split(self.m_P.get_music()))
        v = []
        for x in m:
            v.append([x, instr_middle, instr_middle_vel])
        v[0][1] = instr_low
        v[0][2] = instr_low_vel
        v[-1][1] = instr_high
        v[-1][2] = instr_high_vel
        return v
    def give_up(self):
        self.q_status = const.QSTATUS_SOLVED
    def play_question(self):
        if self.q_status == const.QSTATUS_NO:
            return
        v = self._generate_question_vector()
        m = soundcard.Track()
        m.set_bpm(self.get_int('config/default_bpm'))
        for x in range(len(v)):
            m.set_patch(x, v[x][1])
            m.start_note(x, mpd.notename_to_int(v[x][0]), v[x][2])
        for x in range(len(v)):
            if x == 0:
                m.notelen_time(4)
                m.stop_note(x, mpd.notename_to_int(v[x][0]), v[x][2])
            else:
                m.stop_note(x, mpd.notename_to_int(v[x][0]), v[x][2])
        soundcard.synth.play_track(m)
    def play_question_arpeggio(self):
        if self.q_status == const.QSTATUS_NO:
            return
        v = self._generate_question_vector()
        m = soundcard.Track()
        m.set_bpm(self.get_int('config/arpeggio_bpm'))
        for x in range(len(v)):
            m.set_patch(0, v[x][1])
            m.note(4, 0, mpd.notename_to_int(v[x][0]), v[x][2])
        soundcard.synth.play_track(m)
    def guess_type(self, t):
        """
        GUI guarantees that this method will not be called after it has
        been guessed correct once.
        
        return 0 if this was wrong guess.
        return CORRECT if this question is correct.
        return ALL_CORRECT if all parts of the question is correct.
        """
        assert self.q_status == const.QSTATUS_NEW
        if t == self.m_P.get_chordtype():
            self.m_correct[0] = 1
            if self.m_correct == [1, 1, 1]:
               self.q_status = const.QSTATUS_SOLVED
               return self.ALL_CORRECT
            return self.CORRECT
        else:
            return 0
    def guess_inversion(self, t):
        """
        GUI guarantees that this method will not be called after it has
        been guessed correct once.
        
        return 0 if this was wrong guess.
        return CORRECT if this question is correct.
        return ALL_CORRECT if all parts of the question is correct.
        """
        assert self.q_status == const.QSTATUS_NEW
        if t == self.m_P.get_inversion():
            self.m_correct[1] = 1
            if self.m_correct == [1, 1, 1]:
                self.q_status = const.QSTATUS_SOLVED
                return self.ALL_CORRECT
            return self.CORRECT
        else:
            return 0
        self.m_correct[1] = 1
        return t == self.m_P.get_inversion()
    def guess_toptone(self, t):
        """
        GUI guarantees that this method will not be called after it has
        been guessed correct once.
        
        return 0 if this was wrong guess.
        return CORRECT if this question is correct.
        return ALL_CORRECT if all parts of the question is correct.
        """
        assert self.q_status == const.QSTATUS_NEW
        if t == self.m_P.get_toptone():
            self.m_correct[2] = 1
            if self.m_correct == [1, 1, 1]:
                self.q_status = const.QSTATUS_SOLVED
                return self.ALL_CORRECT
            return self.CORRECT
        else:
            return 0
        self.m_correct[2] = 1
        return t == self.m_P.get_toptone()

class Gui(abstract.LessonbasedGui):
    def __init__(self, teacher, window):
        abstract.LessonbasedGui.__init__(self, teacher, window)
        ################
        # practise_box #
        ################
        hbox = gu.bHBox(self.practise_box)
        hbox.set_spacing(gnome.ui.PAD)

        table = gtk.Table(5, 5, 0)
        hbox.pack_start(table, gtk.FALSE)
        hsep = gtk.HSeparator()
        table.attach(hsep, 0, 5, 1, 2, xoptions=gtk.FILL,
                     yoptions=0, ypadding=gnome.ui.PAD_SMALL)
        hsep = gtk.HSeparator()
        table.attach(hsep, 0, 5, 3, 4, xoptions=gtk.FILL,
                     yoptions=0, ypadding=gnome.ui.PAD_SMALL)
        vsep = gtk.VSeparator()
        table.attach(vsep, 1, 2, 0, 5,
                     xoptions=0, xpadding=gnome.ui.PAD_SMALL)
        vsep = gtk.VSeparator()
        table.attach(vsep, 3, 4, 0, 5,
                     xoptions=0, xpadding=gnome.ui.PAD_SMALL)

        table.attach(gtk.Label(_("Chord type")), 0, 1, 0, 1,
                     xoptions=gtk.FILL, yoptions=0)
        self.g_chordtype_box = gtk.VBox()
        table.attach(self.g_chordtype_box, 0, 1, 2, 3, xoptions=gtk.FILL)
        self.g_type_status = gtk.Label("")
        table.attach(self.g_type_status, 0, 1, 4, 5,
                     xoptions=gtk.FILL, yoptions=0)

        table.attach(gtk.Label(_("Inversion")), 2, 3, 0, 1,
                     xoptions=gtk.FILL, yoptions=0)
        self.g_inversion_box = gtk.VBox()
        table.attach(self.g_inversion_box, 2, 3, 2, 3, xoptions=gtk.FILL)
        self.g_inversion_status = gtk.Label("")
        table.attach(self.g_inversion_status, 2, 3, 4, 5,
                     xoptions=gtk.FILL, yoptions=0)

        table.attach(gtk.Label(_("Toptone")), 4, 5, 0, 1,
                     xoptions=gtk.FILL, yoptions=0)
        self.g_toptone_box = gtk.VBox()
        table.attach(self.g_toptone_box, 4, 5, 2, 3, xoptions=gtk.FILL)
        self.g_toptone_status = gtk.Label("")
        table.attach(self.g_toptone_status, 4, 5, 4, 5,
                     xoptions=gtk.FILL, yoptions=0)

        self.g_music_displayer = mpd.musicdisplayer.MusicDisplayer(utils.play_tone)
        self.g_music_displayer.set_size_request(150, -1)
        hbox.pack_start(self.g_music_displayer)

        self.g_new = gu.bButton(self.action_area, _("_New chord"),
                                self.new_question)
        self.g_repeat = gu.bButton(self.action_area, _("_Repeat"),
                                lambda _o, self=self: self.m_t.play_question())
        self.g_repeat_arpeggio = gu.bButton(self.action_area,
              _("Repeat _arpeggio"),
              lambda _o, self=self: self.m_t.play_question_arpeggio())
        self.g_give_up = gu.bButton(self.action_area, _("_Give up"),
                                    self.give_up)
        self.practise_box.show_all()
        ##############
        # config_box #
        ##############
        self.config_box.set_spacing(gnome.ui.PAD_SMALL)
        self.g_lessonfile_selector = widgets.SelectLessonfileWidget(
            self.m_t, self.g_win, self.update_gui_after_lessonfile_change)
        self.g_lessonfile_selector.show()
        self.config_box.pack_start(self.g_lessonfile_selector)
        self.g_instrument_configurator  \
              = widgets.InstrumentConfigurator(self.m_exname, 3)
        self.config_box.pack_start(self.g_instrument_configurator, gtk.FALSE)
        self.g_instrument_configurator.show()
        self.update_gui_after_lessonfile_change()
    def update_answer_buttons(self, obj=None):
        for x in self.g_chordtype_box.get_children() \
              + self.g_inversion_box.get_children() \
              + self.g_toptone_box.get_children():
            x.destroy()
        if self.m_t.m_P:
            for t in self.m_t.m_P.m_chord_types.keys():
                b = gtk.Button(self.m_t.m_P.m_chord_types[t])
                b.connect('clicked', self.on_type, t)
                self.g_chordtype_box.pack_start(b, gtk.FALSE)
                b.show()
            v = self.m_t.m_P.m_inversions
            v.sort()
            for x in v:
                if x == 0:
                    s = _("root position")
                else:
                    s = _("%i. inversion") % x
                b = gtk.Button(s)
                b.connect('clicked', self.on_inversion, x)
                self.g_inversion_box.pack_start(b, gtk.FALSE)
                b.show()
            v = self.m_t.m_P.m_toptones
            v.sort()
            for x in v:
                b = gtk.Button(str(x))
                b.connect('clicked', self.on_toptone, x)
                self.g_toptone_box.pack_start(b, gtk.FALSE)
                b.show()
    def update_gui_after_lessonfile_change(self):
        self.g_music_displayer.clear()
        if self.m_t.m_P:
            self.g_new.set_sensitive(gtk.TRUE)
        else:
            self.g_new.set_sensitive(gtk.FALSE)
        self.g_repeat.set_sensitive(gtk.FALSE)
        self.g_repeat_arpeggio.set_sensitive(gtk.FALSE)
        self.g_give_up.set_sensitive(gtk.FALSE)
        self.update_answer_buttons()
        self.g_win.set_title("Solfege - " + self.get_pretty_name())
    def on_type(self, button, t):
        if self.m_t.q_status in (const.QSTATUS_NO, const.QSTATUS_GIVE_UP)\
          or self.m_t.m_correct[0]:
            return
        g = self.m_t.guess_type(t)
        if g:
            self.g_type_status.set_text(_("Correct"))
            for b in self.g_chordtype_box.get_children():
                if b != button:
                    b.set_sensitive(gtk.FALSE)
            if g == self.m_t.ALL_CORRECT:
                self.all_guessed_correct()
        else:
            self.g_type_status.set_text(_("Wrong"))
        if self.m_t.m_correct == [1, 1, 1]:
            self.show_answer()
            self.g_new.set_sensitive(gtk.TRUE)
            self.g_new.grab_focus()
            self.g_give_up.set_sensitive(gtk.FALSE)
    def on_inversion(self, button, inversion):
        if self.m_t.q_status in (const.QSTATUS_NO, const.QSTATUS_GIVE_UP) \
          or self.m_t.m_correct[1]:
            return
        if self.m_t.guess_inversion(inversion):
            self.g_inversion_status.set_text(_("Correct"))
            for b in self.g_inversion_box.get_children():
                if b != button:
                    b.set_sensitive(gtk.FALSE)
        else:
            self.g_inversion_status.set_text(_("Wrong"))
        if self.m_t.m_correct == [1, 1, 1]:
            self.show_answer()
            self.g_new.set_sensitive(gtk.TRUE)
            self.g_new.grab_focus()
            self.g_give_up.set_sensitive(gtk.FALSE)
    def on_toptone(self, button, toptone):
        if self.m_t.q_status in (const.QSTATUS_NO, const.QSTATUS_GIVE_UP) \
           or self.m_t.m_correct[2]:
            return
        if self.m_t.guess_toptone(toptone):
            self.g_toptone_status.set_text(_("Correct"))
            for b in self.g_toptone_box.get_children():
                if b != button:
                    b.set_sensitive(gtk.FALSE)
        else:
            self.g_toptone_status.set_text(_("Wrong"))
        if self.m_t.m_correct == [1, 1, 1]:
            self.show_answer()
            self.g_new.set_sensitive(gtk.TRUE)
            self.g_new.grab_focus()
            self.g_give_up.set_sensitive(gtk.FALSE)
    def all_guessed_correct(self):
        self.g_new.set_sensitive(gtk.TRUE)
        self.g_give_up.set_sensitive(gtk.FALSE)
    def new_question(self, widget=None):
        # if we have no lessonfile, then we have to questions.
        if not self.m_t.m_P:
            return
        # make sure all buttons are sensitive.
        for button in self.g_chordtype_box.get_children() \
                    + self.g_inversion_box.get_children() \
                    + self.g_toptone_box.get_children():
            button.set_sensitive(gtk.TRUE)
        ##
        try:
            n = self.m_t.new_question()
        except mpd.MpdException, e:
            soundcard.synth.stop()
            self.g_win.display_question_music_error_message(
               self.m_t.m_P._idx, self.get_string('lessonfile'), e)
            self.g_give_up.set_sensitive(gtk.FALSE)
            self.g_repeat.set_sensitive(gtk.FALSE)
            self.g_repeat_arpeggio.set_sensitive(gtk.FALSE)
            self.m_t.q_status = const.QSTATUS_NO
        else:
            if n == Teacher.ERR_PICKY:
                print "picky!"
            else:
                self.g_music_displayer.clear()
                self.m_t.play_question()
                self.g_give_up.set_sensitive(gtk.TRUE)
                self.g_repeat.set_sensitive(gtk.TRUE)
                if self.get_bool('config/picky_on_new_question'):
                    self.g_new.set_sensitive(gtk.FALSE)
                self.g_repeat_arpeggio.set_sensitive(gtk.TRUE)
                self.g_type_status.set_text('')
                self.g_inversion_status.set_text('')
                self.g_toptone_status.set_text('')
                self.g_inversion_box.set_sensitive(self.m_t.m_P.get_inversion() != -1)
                self.g_toptone_box.set_sensitive(self.m_t.m_P.get_toptone() != -1)
                self.g_chordtype_box.get_children()[0].grab_focus()
    def on_start_practise(self):
        self.g_new.grab_focus()
    def on_end_practise(self):
        self.m_t.end_practise()
        self.g_music_displayer.clear()
        self.g_new.set_sensitive(gtk.TRUE)
        self.g_repeat.set_sensitive(gtk.FALSE)
        self.g_repeat_arpeggio.set_sensitive(gtk.FALSE)
        self.g_give_up.set_sensitive(gtk.FALSE)
        self.g_type_status.set_text('')
        self.g_inversion_status.set_text('')
        self.g_toptone_status.set_text('')
    def give_up(self, widget=None):
        if self.m_t.q_status == const.QSTATUS_NEW:
            self.m_t.give_up()
            self.show_answer()
            self.g_type_status.set_text(self.m_t.m_P.get_chordtype())
            i = self.m_t.m_P.get_inversion()
            if i == 0:
                self.g_inversion_status.set_text(_("root position"))
            elif i > 0:
                self.g_inversion_status.set_text(_("%i. inversion") % i)
            i = self.m_t.m_P.get_toptone()
            if i != -1:
                self.g_toptone_status.set_text("%i" % i)
            self.g_new.set_sensitive(gtk.TRUE)
            self.g_give_up.set_sensitive(gtk.FALSE)
            for b in self.g_chordtype_box.get_children() \
                   + self.g_inversion_box.get_children() \
                   + self.g_toptone_box.get_children():
                b.set_sensitive(gtk.FALSE)
    def show_answer(self):
        clef = mpd.select_clef(self.m_t.m_P.get_music())
        fontsize = self.get_int('config/feta_font_size=20')
        self.g_music_displayer.display(r"\staff{\clef %s <%s>}" % (clef, self.m_t.m_P.get_music()), fontsize)

