# GNU Solfege - free ear training software
# Copyright (C) 2006, 2007  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., 51 Franklin ST, Fifth Floor, Boston, MA  02110-1301  USA

import time
import gtk
import gu
import abstract
import lessonfile
import mpd
import const

class Teacher(abstract.Teacher):
    OK = 0
    ERR_PICKY = 1
    ERR_NO_ELEMS = 2
    def __init__(self, exname, app):
        abstract.Teacher.__init__(self, exname, app)
        self.lessonfileclass = lessonfile.ElembuilderLessonfile
    def new_question(self):
        self.m_P.select_random_question()
        return self.OK
    def get_timedelta_list(self):
        """
        Return a list of the number of seconds between it should be between
        each tap. Ignore "count-in" tones.
        """
        if 'rhythm' in self.m_P.get_question():
            qvar = 'rhythm'
        else:
            qvar = 'music'
        is_rhythm = isinstance(self.m_P.get_question()['music'],
                               lessonfile.Rhythm)
        lexer = mpd.parser.Lexer(self.m_P.get_question()[qvar].m_musicdata)
        x = len(lexer.m_string)
        retval = []
        while lexer.m_idx < x:
            toc, toc_data = lexer.get()
            if is_rhythm == 'rhythm':
                if toc_data.m_pitch.get_octave_notename() == 'd':
                    continue
                if toc_data.m_pitch.get_octave_notename() != 'c':
                    print "warning: Use only c and d for rhythm music objects"
            retval.append(float(toc_data.m_duration.get_rat_value()) * self.m_P.get_tempo()[1] / self.m_P.get_tempo()[0] * 60)
        return retval
    def start_tapping(self):
        self.m_taps = []
    def tap(self):
        self.m_taps.append(time.time())
    def is_tap_complete(self):
        """
        Return True if the user has tapped as many times as the
        question requires.
        """
        # A little abuse of get_timedelta_list, but this makes it simple.
        return len(self.m_taps) == len(self.get_timedelta_list())
    def get_score(self):
        """
        Return a list of floats telling us how close the users answer was.
        Each float is the timedelta of the question divided by the
        timedelta of the answer.

        If at_question_start == show, then the time between the first
        and the second tap will set the tempo, and all the timedeltas will
        be compared
        """
        retval = []
        question = self.get_timedelta_list()
        if self.m_P.header.at_question_start == 'show':
            # The user can tap in any tempo since he will only se the music.
            # First we change the lists so that the time between the
            # first two taps are 1.0, and the other proportionally to this.
            question = [q/question[0] for q in question]
            answer = []
            for x, i in enumerate(self.m_taps[1:]):
                answer.append(self.m_taps[x+1] - self.m_taps[x])
            answer = [a/answer[0] for a in answer]
            for idx in range(len(answer)):
                retval.append(question[idx] / answer[idx])
        else:
            # Has to tap in the same tempo as the music played.
            for x, i in enumerate(self.m_taps[1:]):
                a = self.m_taps[x+1] - self.m_taps[x]
                retval.append(a / question[x])
        return retval
    def get_answer_status(self):
        """
        Will return a tuple (bool, string) where the bool is True if the
        exercises is answered correctly enough. The string is a message
        to the user describing how acourately the rhythm was tapped.
        """
        score = self.get_score()
        print "rhythmtapping score:", " ".join(["%.2f" % x for x in score])
        s = ""
        max_diff = 0.0
        for f in score:
            s = s + " %.2f" % f
            max_diff = max(max_diff, abs(1.0-f))
        if self.m_P.header.at_question_start == 'show':
            # It is much easier to tap in your own tempo, so we have to
            # require more aquracy.
            limit = 0.05
        else:
            limit = 0.09
        if max_diff < limit:
            s = "OK: %.2f < %.2f" % (max_diff, limit)
        else:
            s = "Not good enough: %.2f > %.2f" % (max_diff, limit)
        return (max_diff < limit, s)

class Gui(abstract.LessonbasedGui):
    please_tap_str = _("Please tap the rhythm.")
    def __init__(self, teacher, window):
        abstract.Gui.__init__(self, teacher, window)
        #
        self.g_music_displayer = mpd.musicdisplayer.MusicDisplayer(None)
        self.practise_box.pack_start(self.g_music_displayer, False)
        #
        self.g_tap = gu.bButton(self.practise_box, "Tap here", self.on_tap)
        self.g_new = gu.bButton(self.action_area, _("_New"),
            self.on_new_question)
        self.g_play_music = gu.bButton(self.action_area, _("P_lay music"),
            lambda w: self.run_exception_handled(self.m_t.m_P.play_question))
        self.g_display_music = gu.bButton(self.action_area, _("_Display music"),
            self.show_answer)
        self.g_repeat = gu.bButton(self.action_area, _("_Repeat"),
            self.on_repeat)
        self.g_give_up = gu.bButton(self.action_area, _("_Give up"), self.on_give_up)
        # Flashbar
        self.g_flashbar = gu.FlashBar()
        self.g_flashbar.show()
        self.practise_box.pack_start(self.g_flashbar, False)
    def on_new_question(self, widget=None):
        def exception_cleanup():
            self.g_repeat.set_sensitive(False)
            self.g_give_up.set_sensitive(False)
        g = self.m_t.new_question()
        if g == self.m_t.OK:
            if self.m_t.m_P.header.have_music_displayer:
                self.g_music_displayer.clear()
            try:
                self.do_at_question_start_show_play()
            except Exception, e:
                e.m_mpd_varname = 'music'
                e.m_mpd_badcode = self.m_t.m_P.get_question()['music'].m_musicdata
                if not self.standard_exception_handler(e, __file__,
                        exception_cleanup):
                    raise
            else:
                self.m_start_time = time.time()
                self.g_flashbar.push(self.please_tap_str)
                self.m_t.start_tapping()
                self.g_new.set_sensitive(
                   not self.get_bool('config/picky_on_new_question'))
                self.g_repeat.set_sensitive(True)
                self.g_give_up.set_sensitive(True)
                self.g_tap.set_sensitive(True)
                self.g_tap.grab_focus()
                self.g_play_music.set_sensitive(True)
                self.g_display_music.set_sensitive(True)
        elif g == self.m_t.ERR_PICKY:
            self.g_flashbar.flash(_("You have to solve this question first."))
        else:
            assert g == self.m_t.ERR_NO_ELEMS
            self.g_repeat.set_sensitive(False)
            self.g_flashbar.flash(_("You have to configure this exercise properly"))
    def on_give_up(self, widget):
        self.g_new.set_sensitive(True)
        self.g_new.grab_focus()
        self.g_repeat.set_sensitive(False)
        self.g_give_up.set_sensitive(False)
        self.g_flashbar.clear()
        self.g_tap.set_sensitive(False)
        self.m_t.q_status = const.QSTATUS_GIVE_UP
    def on_repeat(self, widget):
        self.m_t.m_P.play_question()
        self.g_tap.grab_focus()
    def on_tap(self, widget=None):
        self.g_flashbar.set(_("Tapping in progress..."))
        self.m_t.tap()
        try:
            if self.m_t.is_tap_complete():
                solved, msg = self.m_t.get_answer_status()
                self.g_flashbar.pop()
                if solved:
                    self.g_new.set_sensitive(True)
                    self.g_new.grab_focus()
                    self.g_tap.set_sensitive(False)
                    self.m_t.q_status = const.QSTATUS_SOLVED
                else:
                    self.g_tap.grab_focus()
                    self.m_t.start_tapping()
                    self.g_flashbar.set(self.please_tap_str)
                self.g_flashbar.flash(msg)
        except mpd.MpdException, e:
            # If the 'rhythm' variable is defined in a question, Solfege will
            # use this variable when comparing the rhythm. But it will still
            # use the 'music' variable when playing and evt. displaying the
            # rhythm.
            if 'rhythm' in self.m_t.m_P.get_question():
                qvar = 'rhythm'
            else:
                qvar = 'music'
            e.m_mpd_varname = qvar
            e.m_mpd_badcode = self.m_t.m_P.get_question()[qvar].m_musicdata
            if not self.standard_exception_handler(e, __file__):
                raise
    def on_start_practise(self):
        self.g_new.set_sensitive(True)
        self.g_tap.set_sensitive(False)
        self.g_repeat.set_sensitive(False)
        self.g_give_up.set_sensitive(False)
        self.show_hide_at_question_start_buttons()
        self.g_play_music.set_sensitive(False)
        self.g_display_music.set_sensitive(False)
        if self.m_t.m_P.header.have_music_displayer:
            self.g_music_displayer.show()
            self.g_music_displayer.clear()
        else:
            self.g_music_displayer.hide()
        self.g_flashbar.require_size([
         self.please_tap_str,
         ])
