# GNU Solfege - ear training for GNOME
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005  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
import gobject
import gnome
import gu

import mpd
import mpd.musicdisplayer
import const
import abstract
import lessonfile
import cfg
import dataparser
import soundcard

class Teacher(abstract.LessonbasedTeacher):
    OK = 0
    ERR_PICKY = 1
    #UGH should we do this here
    def __init__(self, exname, app):
        abstract.LessonbasedTeacher.__init__(self, exname, app)
        self.lessonfileclass = lessonfile.ChordLessonfile
    def new_question(self):
        """
        return 1 if the teacher has a question to ask
        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_VOICING_SOLVED, const.QSTATUS_NO)):
            return Teacher.ERR_PICKY

        if self.m_P:
            self.m_P.select_random_question()
            self.q_status = const.QSTATUS_NEW
            return Teacher.OK
    def guess_chordtype(self, t):
        """
        return 1 if correct, None if not.
        This function will set self.q_status, and will raise an
        exception if the function is called with invalid value of
        q_status.

        Before we start: const.QSTATUS_NO
        After 'New' is clicked: const.QSTATUS_NEW

        if chordtype correct: const.QSTATUS_TYPE_SOLVED
        if chordtype wrong: const.QSTATUS_TYPE_WRONG

        if voicing correct const.QSTATUS_VOICING_SOLVED
        if voicing wrong: const.QSTATUS_VOICING_WRONG
        """
        if self.q_status in (const.QSTATUS_NEW, const.QSTATUS_TYPE_WRONG):
            if t == self.m_P.get_cname():
                self.q_status = const.QSTATUS_TYPE_SOLVED
                return 1
            else:
                self.q_status = const.QSTATUS_TYPE_WRONG
                return
    def guess_voicing(self, tones):
        """
        return 1 if correct, None if not.
        Gui should not call this function if the question is already solved.
        """
        v = self.m_P.get_music_as_notename_list('music')
        v.sort(mpd.compare_notenames)
        question_i = []
        for n in v:
            while n[-1] in ",'":
                n = n[:-1]
            question_i.append(mpd.notename_to_int(n))
        answer_i = []
        for n in tones:
            while n[-1] in ",'":
                n = n[:-1]
            answer_i.append(mpd.notename_to_int(n))
        if answer_i == question_i:
            self.q_status = const.QSTATUS_VOICING_SOLVED
            return 1
        self.q_status = const.QSTATUS_VOICING_WRONG
    def give_up(self):
        self.q_status = const.QSTATUS_VOICING_SOLVED

class Gui(abstract.LessonbasedGui):
    def __init__(self, teacher, window):
        abstract.LessonbasedGui.__init__(self, teacher, window)
        self.m_stacking_frame_min_height = 0

        ###############
        # practise_box
        ###############
        self.practise_box.set_spacing(gnome.ui.PAD)

        hbox = gu.bHBox(self.practise_box, True, True)
        hbox.set_spacing(gnome.ui.PAD)
        ##################
        # chordtype frame 
        ##################
        frame = gtk.Frame(_("Identify chord type"))
        hbox.pack_start(frame)
        self.g_chordtype_box = gtk.VBox()
        self.g_chordtype_box.set_border_width(gnome.ui.PAD_SMALL)
        frame.add(self.g_chordtype_box)

        #################
        # stacking frame
        #################
        self.g_stacking_frame = gtk.Frame(_("Chord voicing"))
        self.g_stacking_frame.set_sensitive(False)
        hbox.pack_start(self.g_stacking_frame)
        vbox = gtk.VBox()
        vbox.set_border_width(gnome.ui.PAD_SMALL)
        self.g_stacking_frame.add(vbox)
        t = gtk.Table(1, 1, 1)
        vbox.pack_start(t)
        self.g_source = gtk.VBox()
        t.attach(self.g_source, 0, 1, 0, 1, gtk.EXPAND|gtk.FILL)
        self.g_answer = gtk.VBox()
        t.attach(self.g_answer, 1, 2, 0, 1, gtk.EXPAND|gtk.FILL)
        self.g_redo = gtk.Button("<<<")
        self.g_redo.connect('clicked', lambda o, self=self: self.fill_stacking_frame())
        vbox.pack_end(self.g_redo, False, False)

        self.g_flashbar = gu.FlashBar()
        self.practise_box.pack_start(self.g_flashbar, False)

        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.m_t.m_P.play_question())
        self.g_repeat_arpeggio = gu.bButton(self.action_area, _("Repeat _arpeggio"), lambda _o: self.m_t.m_P.play_question_arpeggio())
        self.g_give_up = gu.bButton(self.action_area, _("_Give up"), lambda _o, self=self: self.give_up())
        self.practise_box.show_all()
        ##############
        # config_box #
        ##############
        self.config_box.set_spacing(gnome.ui.PAD_SMALL)
        self.add_random_transpose_gui()
    def new_question(self, widget=None):
        def exception_cleanup():
            soundcard.synth.stop()
            self.g_give_up.set_sensitive(False)
            self.g_repeat.set_sensitive(False)
            self.g_repeat_arpeggio.set_sensitive(False)
            self.m_t.q_status = const.QSTATUS_NO
        # if we have no lessonfile, then we have no questions.
        if not self.m_t.m_P:
            return
        try:
            g = self.m_t.new_question()
            if g == Teacher.OK:
                self.g_give_up.set_sensitive(True)
                self.g_repeat.set_sensitive(True)
                self.g_repeat_arpeggio.set_sensitive(True)
                self.g_new.set_sensitive(
                    not self.get_bool('config/picky_on_new_question'))
                self.clear_stacking_frame()
                self.g_stacking_frame.set_sensitive(False)
                for c in self.g_chordtype_box.get_children():
                    c.set_sensitive(True)
                self.m_t.m_P.play_question()
                self.g_flashbar.flash(_("Chord type"))
                self.g_chordtype_box.get_children()[0].grab_focus()
            elif g == Teacher.ERR_PICKY:
                self.g_flashbar.flash(_("You have to solve this question first."))
        except lessonfile.LessonfileException, e:
            exception_cleanup()
            # This exception will be raised if for example the music
            # type for a question is invalid.
            self.g_win.display_exception_message(e, __file__)
        except mpd.MpdException, e:
            exception_cleanup()
            self.g_win.display_question_music_error_message(
               self.m_t.m_P._idx, self.get_string('lessonfile'), e)
    def update_gui_after_lessonfile_change(self, *v):
        self.set_lesson_heading(self.m_t.m_P.header.lesson_heading)
        self.g_random_transpose.set_text(str(self.m_t.m_P.header.random_transpose))
        self.g_new.set_sensitive(True)
        self.fill_chordtype_box()
        self.g_repeat.set_sensitive(False)
        self.g_repeat_arpeggio.set_sensitive(False)
        self.g_give_up.set_sensitive(False)
        self.clear_stacking_frame()
    def clear_stacking_frame(self):
        for c in self.g_source.get_children() + self.g_answer.get_children():
            c.destroy()
    def fill_stacking_frame(self):
        """
        Create the buttons in stacking frame.
        """
        self.g_redo.set_sensitive(True)
        self.g_stacking_frame.set_sensitive(True)
        self.m_answer = []
        self.clear_stacking_frame()
        v = self.m_t.m_P.get_music_as_notename_list('music')
        v.sort()
        for n in v:
            nn = mpd.MusicalPitch.new_from_notename(n)
            b = gtk.Button(nn.get_user_notename())
            b.connect('clicked', self.on_notebutton_clicked, nn.get_notename(), nn.get_user_notename())
            self.g_source.pack_end(b, False, False)
            b.show()
        self.g_source.get_children()[0].grab_focus()
    def give_up(self, v=None):
        self.m_t.give_up()
        self.g_give_up.set_sensitive(False)
        self.g_new.set_sensitive(True)
        self.g_new.grab_focus()
        self.set_chordtype_frame_status_solved(self.m_t.m_P.get_cname())
        self.clear_stacking_frame()
        self.g_stacking_frame.set_sensitive(True)
        self.g_redo.set_sensitive(False)
        self.show_music()
    def on_chordtype_right_clicked(self, button, event, t):
        if self.m_t.m_P and not self.m_t.m_P.header.enable_right_click:
            self.g_flashbar.flash(_("Right click is not allowed for this lesson file."))
            return
        if self.m_t.q_status == const.QSTATUS_NO:
            self.g_flashbar.flash(_("Click 'New chord' to begin."))
            return
        if self.m_t.q_status == const.QSTATUS_NEW:
            self.g_flashbar.flash(_("You should try to guess before right-clicking."))
            return
        if not self.m_t.m_P.has_question():
            return
        try:
            # We try first with the 'set' variable, as this is what is closest
            if 'set' in self.m_t.m_P.get_question():
                for question in self.m_t.m_P.m_questions:
                    if question['set'] == self.m_t.m_P.get_question()['set'] \
                            and question.get_cname() == t:
                        self.m_t.m_P.play_question(question)
                        return
            #
            for question in self.m_t.m_P.m_questions:
                if question.get_cname() == self.m_t.m_P.get_cname():
                    self.m_t.m_P.play_question(question)
                    return
        except lessonfile.LessonfileException, e:
            # This exception will be raised if for example the music
            # type for a question is invalid.
            self.g_win.display_exception_message(e, __file__)
        except mpd.MpdException, e:
            # Invalid key variable or music code goes here.
            self.g_win.display_question_music_error_message(
                self.m_t.m_P._idx, self.get_string('lessonfile'), e)
    def on_chordtype_clicked(self, btn, t):
        if self.m_t.q_status == const.QSTATUS_NO:
            self.g_flashbar.flash(_("Click 'New chord' to begin."))
        elif self.m_t.q_status in (const.QSTATUS_NEW, const.QSTATUS_TYPE_WRONG):
            g = self.m_t.guess_chordtype(t)
            if g:
                self.g_flashbar.flash(_("Correct, now stack the tones"))
                self.set_chordtype_frame_status_solved(t)
                self.fill_stacking_frame()
            else:
                self.g_flashbar.flash(_("Wrong"))
        elif self.m_t.q_status in (const.QSTATUS_TYPE_SOLVED, const.QSTATUS_VOICING_WRONG):
            self.g_flashbar.flash(_("Type is already solved, now specify voicing."))
    def on_notebutton_clicked(self, btn, n, user_notename):
        newb = gtk.Button(user_notename)
        newb.show()
        btn.destroy()
        self.m_answer.append(n)
        self.g_answer.pack_end(newb, False, False)
        if not self.g_source.get_children():
            # no children mean that the user has finished answering
            if self.m_t.guess_voicing(self.m_answer):
                self.g_flashbar.flash(_("Correct"))
                self.show_music()
                self.g_new.set_sensitive(True)
                self.g_new.grab_focus()
                self.g_give_up.set_sensitive(False)
                self.g_redo.set_sensitive(False)
            else:
                self.g_flashbar.flash(_("Wrong"))
                self.g_redo.grab_focus()
        else:
            self.g_source.get_children()[0].grab_focus()
    def show_music(self):
        self.clear_stacking_frame()
        md = mpd.musicdisplayer.MusicDisplayer(None)
        self.g_source.pack_start(md)
        md.show()
        md.display(r"\staff{ \clef %s < %s >}" % (
                mpd.select_clef(self.m_t.m_P.get_music_as_notename_string('music')),
                self.m_t.m_P.get_music_as_notename_string('music')), 20)
        # display the notenames on the buttons with octave info
        v = self.m_t.m_P.get_music_as_notename_list('music')
        v.sort(mpd.compare_notenames)
        for n in v:
            b = gtk.Button(mpd.MusicalPitch.new_from_notename(n).get_user_octave_notename())
            b.get_children()[0].set_use_markup(1)
            b.show()
            self.g_answer.pack_end(b, False, False)
    def fill_chordtype_box(self):
        for x in self.g_chordtype_box.get_children():
            x.destroy()
        if self.m_t.m_P:
            for ctype, translated_type in self.m_t.m_P.m_chord_types.iteritems():
                b = gtk.Button(translated_type)
                b.set_data('chordtype', ctype)
                self.g_chordtype_box.pack_start(b, False)
                b.connect('clicked', self.on_chordtype_clicked, ctype)
                b.connect('button_release_event', self.on_chordtype_right_clicked, ctype)
                b.show()
    def set_chordtype_frame_status_solved(self, t):
        for c in self.g_chordtype_box.get_children():
            c.set_sensitive(c.get_data('chordtype')==t)
    def on_start_practise(self):
        self.update_gui_after_lessonfile_change()
        self.g_flashbar.require_size([
            _("You have to solve this question first."),
            _("Type is already solved, now specify voicing."),
        ])
        self.g_new.grab_focus()
        gobject.timeout_add(const.SHORT_WAIT, lambda self=self:
            self.g_flashbar.flash(_("Click 'New chord' to begin.")))
        self.fill_chordtype_box()
        self.clear_stacking_frame()
        self.g_stacking_frame.set_sensitive(False)
    def on_end_practise(self):
        self.m_t.end_practise()
        self.g_new.set_sensitive(True)
        self.g_repeat.set_sensitive(False)
        self.g_repeat_arpeggio.set_sensitive(False)
        self.g_give_up.set_sensitive(False)
