# GNU Solfege - free ear training software
# vim: set fileencoding=utf-8 :
# Copyright (C) 2006, 2007, 2008, 2009 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 3 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, see <http://www.gnu.org/licenses/>.

# Lesson file related GUI code.

############################
# Python Standard Library
############################
import os
import gettext
import warnings

###################
import gtk

###################
# Solfege modules
###################
import lessonfile
import gu
import mpd
import statistics

class CustomConfirmationAlert(gtk.Dialog):
    WIDTH_CHARS=70
    def __init__(self, buttons):
        gtk.Dialog.__init__(self, "", None, buttons=buttons)
        self.set_has_separator(False)
        self.set_resizable(False)
        self.set_border_width(6)
        self.vbox.set_spacing(12)
        hbox = gtk.HBox()
        hbox.set_spacing(12)
        hbox.set_border_width(6)
        self.vbox.pack_start(hbox, True)
        im = gtk.Image()
        im.set_alignment(0.5, 0.0)
        im.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG)
        hbox.pack_start(im)
        # cvbox = contents vbox
        self.cvbox = gtk.VBox()
        hbox.pack_start(self.cvbox)
        self.g_primary = None
        self.g_secondary = None
    def set_markup(self, txt):
        self.m_primary = txt
        if not self.g_primary:
            self.g_primary = gtk.Label()
            self.g_primary.set_line_wrap(True)
            self.g_primary.set_width_chars(self.WIDTH_CHARS)
            self.g_primary.set_alignment(0.0, 0.5)
            self.cvbox.pack_start(self.g_primary, False)
        self.g_primary.set_markup("<b>%s</b>" % txt)
    def add_label(self, txt):
        label = gtk.Label(txt)
        label.set_width_chars(self.WIDTH_CHARS)
        label.set_line_wrap(True)
        label.set_alignment(0.0, 0.5)
        self.cvbox.pack_start(label, False)

class ConflictInfoDialog(CustomConfirmationAlert):
    def __init__(self, lesson_id, solfege_file, user_files):
        CustomConfirmationAlert.__init__(self,
                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                     gtk.STOCK_OK, gtk.RESPONSE_OK))
        self.set_default_response(gtk.RESPONSE_OK)
        # translators: 'lesson_id' is a source code variable name that should
        # not be translated.
        self.set_markup(_("The 'lesson_id' variable has to be unique for each lesson file. Solfege will now generate new 'lesson_id' for the conflicting files. One of the conflicting files must keep its 'lesson_id' because it belongs to GNU Solfege.") + " " + gettext.ngettext("Add new 'lesson_id' to the other file?", "Add new 'lesson_id' to the other files?", len(user_files)))
        self.add_label("")
        self.add_label(_("Press 'Cancel' to postpone the decision."))
        self.add_label("")
        self.add_label(u"Files to modify:")
        file_list = []
        for fn in user_files:
            self.add_label("    %s (%s)" % (fn['filename'], fn['timestr']))
        self.add_label("")
        self.add_label(_("Leave unmodified:"))
        self.add_label("    %s (%s)" % (
            os.path.abspath(solfege_file['filename']),
            solfege_file['timestr']))
        self.add_label("")
        self.show_all()


class ConflictResolveDialog(CustomConfirmationAlert):
    def __init__(self, lesson_id, selectable_files, info_files):
        CustomConfirmationAlert.__init__(self,
                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                     gtk.STOCK_OK, gtk.RESPONSE_OK))
        self.set_markup(_("The 'lesson_id' variable has to be unique for each lesson file. Solfege will now generate new 'lesson_id' for the conflicting files. Please select which lesson file shall keep the current 'lesson_id'."))
        self.add_label("")
        self.add_label(_("The preselected file is Solfege's educated guess."))
        self.add_label(_("Press 'Cancel' to postpone the decision."))
        self.add_label("")
        first_btn = None
        oldest = sorted(selectable_files, key=lambda k: k['mtime'])[0]
        self.selected_filename = None
        for fileinfo in selectable_files:
            rdb = gtk.RadioButton(first_btn, "%s (%s)" % (
                os.path.abspath(fileinfo['filename']), fileinfo['timestr']))
            def ff(w, f):
                self.selected_filename = f
            rdb.connect('clicked', ff, fileinfo['filename'])
            if first_btn is None:
                first_btn = rdb
            if fileinfo == oldest:
                rdb.set_active(True)
                self.selected_filename = fileinfo['filename']
            self.cvbox.pack_start(rdb, False)
        if info_files:
            self.add_label("")
            self.add_label(gettext.ngettext("This file is part of the same conflict, and will also get a new 'lesson_id':", "These files are part of the same conflict, and will also get a new 'lesson_id':", len(info_files)))
            for fileinfo in info_files:
                label = gtk.Label(u"    %(filename)s (%(timestr)s)" % fileinfo)
                label.set_alignment(0.0, 0.5)
                self.cvbox.pack_start(label, False)
        self.show_all()


def handle_lesson_id_crash(lessonfile_manager):
    """
    All lesson files that are part of Solfege, and distributed with the
    program are accessed by relative file name. In contrast are all the
    user contributed files in $HOME/lessonfiles accessed by absolute path.
    We call the solfege-file and user-file.

    A solfege-file will always keep its id if a user-file is in conflict.
    But with two solfege-files, or two user-files, Solfege will suggest
    to keep the oldest file, but let the user descide.
    """
    def is_write_protected(filename):
        return not (os.path.isfile(filename) and os.access(filename, os.W_OK))
    for lesson_id in lessonfile_manager.iterate_duplicated_lesson_id():
        infolist = list(lessonfile_manager.get_lesson_file_info(lesson_id))
        solfege_files = [d for d in infolist if not os.path.isabs(d['filename'])]
        user_files = [d for d in infolist if os.path.isabs(d['filename'])]
        del infolist
        if len(solfege_files) == 1:
            dlg = ConflictInfoDialog(lesson_id, solfege_files[0], user_files)
            ret = dlg.run()
            if ret == gtk.RESPONSE_OK:
                lessonfile_manager.duplicate_id_keep_this(lesson_id,
                        solfege_files[0]['filename'])
            elif ret in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT):
                    lessonfile_manager.ignore_duplicates_with_lesson_id(lesson_id)
            dlg.destroy()
        else:
            if len(solfege_files) > 1:
                dlg = ConflictResolveDialog(lesson_id, solfege_files, user_files)
            else:
                dlg = ConflictResolveDialog(lesson_id, user_files, [])
            ret = dlg.run()
            if ret == gtk.RESPONSE_OK:
                lessonfile_manager.duplicate_id_keep_this(lesson_id,
                            dlg.selected_filename)
            else:
                assert ret in (gtk.RESPONSE_CANCEL,
                               gtk.RESPONSE_DELETE_EVENT)
                lessonfile_manager.ignore_duplicates_with_lesson_id(lesson_id)
            dlg.destroy()


def rn_markup(s):
    v = []
    for p1, p2, p3, sep in lessonfile.rnc_markup_tokenizer(s):
        v.append('<span font_family="serif"><span size="xx-large">%s</span><span size="small">%s</span><span size="x-large">%s%s</span></span>' % (p1, p2, p3, sep))
    return "".join(v)

def chordname_markup(s):
    v = []
    for nn, mod, sup, bass in lessonfile.chordname_markup_tokenizer(s):
        if bass:
            bass = u"/%s" % mpd.MusicalPitch.new_from_notename(bass).get_user_notename()
        nn = mpd.MusicalPitch.new_from_notename(nn).get_user_notename()
        nn = nn[0].upper() + nn[1:]
        v.append('%s%s<span size="large" rise="11000">%s</span>%s' % (nn, mod, sup, bass))
    return u'<span font_family="serif" size="xx-large">%s</span>' % u" ".join(v)

def new_labelobject(label):
    """
    label can be one of the following types:
    * unicode string
    * lessonfile.LabelObject instance

    Return a gtk widget of some sort that displays the label.
    FIXME:
        If we in the future want some labels, for example chordlabel,
        to be transposable, then new_labelobject should return objects
        that are derived from gtk.Label, and they should have a
        a is_transposable method. Then Gui.on_new must call
        QuestionNameButtonTable and make it transpose the labels.
        But I don't think translated labels should have high priority.
    """
    if isinstance(label, basestring):
        # We hope all strings are unicode, but check for basestring just
        # in case some modules are wrong.
        l = gtk.Label(label)
        l.show()
    else:
        if isinstance(label, tuple):
            # I think only old code
            # that has  been removed. But let us keep the code until
            # we have a chanse to review.
            warnings.warn("lessonfilegui.new_labelobject: label is a tuple.",
                          DeprecationWarning)
            labeltype, labeldata = label
        else:
            labeltype = label.m_labeltype
            labeldata = label.m_labeldata
        if labeltype == 'progressionlabel':
            l = gu.HarmonicProgressionLabel(labeldata)
            l.show_all()
        else:
            l = gtk.Label()
            if labeltype == 'rnc':
                l.set_markup(rn_markup(labeldata))
            elif labeltype == 'chordname':
                l.set_markup(chordname_markup(labeldata))
            elif labeltype == 'plabel':
                l.set_markup("""<span font_family="serif"><span size="xx-large">%s</span><span size="small">%s</span><span size="x-large">%s</span></span>""" % labeldata)

            elif labeltype == 'pangomarkup':
                l.set_markup(labeldata)
            l.show()
    return l

class ExercisesMenuAddIn(object):
    def create_learning_tree_menu(self):
        """
        Create and return a gtk.Menu object that has submenus that
        let us select all lessons on the learning tree.
        """
        def create_submenu2(collection, fpdata):
            menu = gtk.Menu()
            if isinstance(fpdata['collections'][collection]['children'][0], dict):
                id_list = []
                for l in fpdata['collections'][collection]['children']:
                    id_list.extend(l['children'])
            else:
                id_list = fpdata['collections'][collection]['children']
            for lesson_id in id_list:
                if statistics.db.get_field(lesson_id, 'module') not in ('melodicinterval', 'harmonicinterval', 'idbyname'):
                    continue
                # We don't want to add these lesson files because we know
                # that they cannot be exported. It would be better
                # to catch these with a more generit algorithm, but
                # then we would have to parse all the files, and that
                # would be too slow.
                if lesson_id in (
                        # melodic-interval-self-config
                        "f62929dc-7122-4173-aad1-4d4eef8779af",
                        # harmonic-interval-self-config
                        "466409e7-9086-4623-aff0-7c27f7dfd13b",
                        # the csound-fifth-* files:
                        "b465c807-d7bf-4e3a-a6da-54c78d5b59a1",
                        "aa5c3b18-664b-4e3d-b42d-2f06582f4135",
                        "5098fb96-c362-45b9-bbb3-703db149a079",
                        "3b1f57e8-2983-4a74-96da-468aa5414e5e",
                        "a06b5531-7422-4ea3-8711-ec57e2a4ce22",
                        "e67c5bd2-a275-4d9a-96a8-52e43a1e8987",
                        "1cadef8c-859e-4482-a6c4-31bd715b4787",
                        ):
                    continue
                i = gtk.MenuItem(statistics.db.get_field(lesson_id, 'title'))
                i.set_data('lesson_id', lesson_id)
                i.connect('activate', self.on_select_exercise)
                menu.append(i)
            return menu
        def create_submenu(data, fpdata):
            item = gtk.MenuItem(data['heading'])
            menu = gtk.Menu()
            for m in data['collection-ids']:
                i = gtk.MenuItem(fpdata['collections'][m]['name'])
                menu.append(i)
                i.set_submenu(create_submenu2(m, fpdata))
            item.set_submenu(menu)
            return item
        menu = gtk.Menu()
        for column in self.m_app.m_frontpage_data['columns']:
            for x in column:
                menu.append(create_submenu(x, self.m_app.m_frontpage_data))

        menu.show_all()
        self._menu_hide_stuff(menu)
        return menu
    def _menu_hide_stuff(self, menu):
        """
        Hide the menu if it has no menu items, or all menu items are hidden.
        """
        for sub in menu.get_children():
            assert isinstance(sub, gtk.MenuItem)
            if sub.get_submenu():
                self._menu_hide_stuff(sub.get_submenu())
                if not [c for c in sub.get_submenu().get_children() if c.get_property('visible')]:
                    sub.hide()

