#
#  smconv_XML.py <Felix.Engel@fcenet.de>, <Peter.Bienstman@UGent.be>
#

import os
import math
import time
from xml.etree import cElementTree

from mnemosyne.libmnemosyne.translator import _
from mnemosyne.libmnemosyne.file_format import FileFormat
from mnemosyne.libmnemosyne.file_formats.media_preprocessor \
    import MediaPreprocessor

HOUR = 60 * 60 # Seconds in an hour.
DAY = 24 * HOUR # Seconds in a day.


class Smconv_XML(FileFormat, MediaPreprocessor):

    """Import the xml file created by the smconv.pl script to Mnemosyne.
    smconv.pl is available at http://smconvpl.sourceforge.net and reads
    SuperMemo for Palm databases and exports them to XML.

    In order to import the generated XML into mnemosyne, care must be taken
    to ensure the correct charset encoding of the input file. In my case,
    the palm databases are "windows-1252". The xml file generated by smconv.pl
    was set to "us-ascii". This makes the XML parser fail. For me, changing
    the xml header to <?xml version="1.0" encoding="windows-1252"?>  worked
    well. However, your mileage may vary.

    Restrictions:

    - SM for Palm has six fields for each card. Templates can be used to
      format these fields and to control whether they are part of  the
      question or of the answer. However this class assumes that the first
      field is the question and the second field is the answer.

    """

    description = _("Supermemo for Palm through smconv.pl")
    extension = ".xml"
    filename_filter = _("Supermemo for Palm XML files (*.xml *.XML)")
    import_possible = True
    export_possible = False

    def __init__(self, component_manager):
        FileFormat.__init__(self, component_manager)
        MediaPreprocessor.__init__(self, component_manager)

    def do_import(self, filename, extra_tag_names=None):
        FileFormat.do_import(self, filename, extra_tag_names)
        w = self.main_widget()
        try:
            tree = cElementTree.parse(filename)
        except cElementTree.ParseError, e:
            w.show_error(_("Unable to parse file:") + str(e))
            return
        card_type = self.card_type_with_id("1")
        tag_names = []
        if extra_tag_names:
            tag_names += [tag_name.strip() for tag_name \
                in extra_tag_names.split(",")]
        for element in tree.find("cards").findall("card"):
            category = element.attrib["category"]
            commit = not (element.attrib["commit"] == "0")
            for field in element.find("card_fields").findall("card_field"):
                if field.attrib["idx"] == "1":
                    question = field.text
                else:
                    answer = field.text
            card_other = element.find("card_other")
            if card_other is None:
                difficulty = 40
                difficulty_prev = 40
            else:
                difficulty = int(card_other.attrib["difficulty"])
                difficulty_prev = int(card_other.attrib["difficulty_prev"])
            # Grades are 0-5. In SM for Palm there are commited and uncommited
            # cards. Uncommited cards go to grade -1.
            # Otherwise try to extrapolate something from difficulty in SM
            # I have implemented guess_grade such, that the distribution of
            # grades looks reasonable for my test database of 4000 entries.
            # By "reasonable" I mean than most of the entries should be
            # at grade 4. I've been learning that database for 4 years, so the
            # cards should have converged by now.
            if commit == False:
                grade = -1
            # Very easy items are scarce in SM and must be easiest grade.
            elif difficulty < 10:
                grade = 5
            # Assign passing grades, based upon whether the difficulty has
            # changed.
            elif difficulty > difficulty_prev:
                grade = 2
            elif difficulty == difficulty_prev:
                grade = 3
            elif difficulty < difficulty_prev:
                grade = 4
            # If the interval becomes shorter, it must have been a failure.
            if card_other is None:
                interval = 0
                interval_prev = 0
            else:
                interval = int(card_other.attrib["interval"]) * DAY
                interval_prev = int(card_other.attrib["interval_prev"]) * DAY
            if interval < interval_prev:
                grade = 0
            # Construct card.
            fact_data = {"f": question, "b": answer}
            self.preprocess_media(fact_data, tag_names)
            card = self.controller().create_new_cards(fact_data, card_type,
                grade=grade, tag_names=tag_names + [category],
                check_for_duplicates=False, save=False)[0]
            if _("MISSING_MEDIA") in tag_names:
                tag_names.remove(_("MISSING_MEDIA"))
            if card_other is not None:
                card.creation_time = int(time.mktime(time.strptime(\
                    card_other.attrib["datecreate"], "%Y-%m-%d")))
                card.modification_time = int(time.mktime(time.strptime(\
                    card_other.attrib["datecommit"], "%Y-%m-%d")))
                card.next_rep = self.scheduler().midnight_UTC(int(time.mktime(\
                    time.strptime(card_other.attrib["datenexttest"],
                    "%Y-%m-%d"))))
                card.last_rep = card.next_rep - interval
                card.lapses = int(card_other.attrib["lapses"])
                # Try to fill acquisiton reps and retention reps.
                # Since SM statistics are only available for commited
                # cards, I take acq_reps = 0 and ret_reps = lapses + recalls.
                card.ret_reps = card.lapses + int(card_other.attrib["recalls"])
                # Try to derive an easines factor EF from [1.3 .. 3.2] from
                # difficulty d from [1% .. 100%].
                # The math below is set to translate
                # difficulty=100% --> easiness = 1.3
                # difficulty=40% --> easiness = 2.5
                # difficulty=1% --> easiness = 3.2
                dp = difficulty * 0.01
                # Small values should be easy, large ones hard.
                if dp > 0.4:
                    card.easiness = 1.28 - 1.32 * math.log(dp)
                else:
                    card.easiness = 4.2 - 1.139 * math.exp(dp)
                self.database().update_card(card)
        self.warned_about_missing_media = False