# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006, 2007, 2008  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/>.

# IMPORTANT:
# Remember to increase statistics_version and add code to handle
# old versions if incompatabilities are added.

import sys
import textwrap
import time, os, pickle
import utils
import filesystem
import mpd
import hashlib

# version 1, the first version was introduced with Solfege 2.0
# version 2 was introduced in the newxercisesetup changes
statistics_version = 2

STATISTICS_SAVE_DIR = os.path.join(filesystem.app_data(), 'statistics')

if not os.path.exists(STATISTICS_SAVE_DIR):
    if not os.path.exists(STATISTICS_SAVE_DIR):
        os.makedirs(STATISTICS_SAVE_DIR)

if not os.path.exists(os.path.join(STATISTICS_SAVE_DIR, 'statistics_version')):
    f = open(os.path.join(STATISTICS_SAVE_DIR, 'statistics_version'), 'w')
    print >> f, statistics_version
    f.close()

def get_statistics_version():
    """
    Return as an int, the version number the statistics stored in
    STATISTICS_SAVE_DIR has.
    """
    try:
        f = open(os.path.join(STATISTICS_SAVE_DIR, 'statistics_version'), 'r')
    except IOError, e:
        print >> sys.stderr, "While opening the file '%s':" % os.path.join(STATISTICS_SAVE_DIR, 'statistics_version')
        print >> sys.stderr, e
        return statistics_version
    s = f.read()
    f.close()
    try:
        return int(s)
    except ValueError, e:
        print >> sys.stderr, "While parsing the content of '%s':" % os.path.join(STATISTICS_SAVE_DIR, 'statistics_version')
        print >> sys.stderr, e
        return 0

statistics_on_disk_version = get_statistics_version()
if statistics_on_disk_version > statistics_version:
    usable_statistics = None
    print >> sys.stderr, """The statistics stored in %(d)s have been created by a newer version of
Solfege, and that release has changed the way statistics are saved. If you want
to record statistics, you must either use that newer version of Solfege, or
delete (or rename) the directory %(d)s\n""" % {'d':STATISTICS_SAVE_DIR}
elif statistics_on_disk_version == 0:
    usable_statistics = None
    print >> sys.stderr, """
Something is strange with the statistics stored in %s.
""" % STATISTICS_SAVE_DIR
else:
    usable_statistics = 1
YEAR = 0
MONTH = 1
DAY = 2
HOURS = 3
MINUTES = 4
SECONDS = 5
WEEKDAY = 6
JULIANDAY = 7

class AbstractStatistics:
    def __init__(self, teacher):
        self.m_t = teacher
        self.m_session_stat = {}
        self.m_today_stat = {}
        self.m_last7_stat = {}
        self.m_total_stat = {}
        self.m_dicts = {'session' : self.m_session_stat,
                        'today'   : self.m_today_stat,
                        'last7'   : self.m_last7_stat,
                        'total'   : self.m_total_stat}
    def _add(self, question, answer):
        for D in [self.m_session_stat, self.m_today_stat,
                  self.m_last7_stat, self.m_total_stat]:
            if question not in D:
                D[question] = {}
            if answer not in D[question]:
                D[question][answer] = 0
            D[question][answer] = D[question][answer] + 1
    def add_correct(self, answer):
        self._add(answer, answer)
    def add_wrong(self, question, answer):
        self._add(question, answer)
    def merge_stat_dicts(self, A, B):
        "Add content of B to A"
        for k in B:
            if k not in A:
                A[k] = {}
            for n in B[k]:
                if n not in A[k]:
                    A[k][n] = 0
                A[k][n] = A[k][n] + B[k][n]
        return A
    def load_statistics(self, datadir):
        """
        datadir is the directory where the statistics files are stored.
        """
        if not usable_statistics:
            return
        for filename in os.listdir(datadir):
            t = int(filename)
            lt = time.localtime(t)
            try:
                f = open(os.path.join(datadir, filename), 'r')
                D = pickle.load(f)
                f.close()
            except Exception, e:
                print >> sys.stderr, "Error loading statistics from the file\n", os.path.join(datadir, filename)
                print >> sys.stderr, "Exception:", (e,), e
                print >> sys.stderr, "\n".join(textwrap.wrap("There is probably nothing to do except to delete the file. Don't bother reporting this as a bug, unless you can describe how to corrupt the file."))
                continue
            now = int(time.time())
            lt_today = time.localtime(time.time())
            if lt[YEAR] == lt_today[YEAR] and \
                    lt[JULIANDAY] == lt_today[JULIANDAY]:
                self.m_today_stat = self.merge_stat_dicts(self.m_today_stat, D)
            if now - t < 60*60*24*7: # 7 days
                self.m_last7_stat = self.merge_stat_dicts(self.m_last7_stat, D)
            self.m_total_stat = self.merge_stat_dicts(self.m_total_stat, D)
    def get(self):
        """Will return a 0 <= value <= 1.0 that say how many percent is
        correct in this session
        """
        c = t = 0
        for k in self.m_session_stat:
            if k in self.m_session_stat[k]:
                c = c + self.m_session_stat[k][k]
            t = t + self.get_num_guess(self.m_session_stat, k)
        if t > 0:
            return 1.0 * c / t
        else:
            return 0.0
    def display(self):
        print
        v = self.m_session_stat.keys()
        v.sort()
        for x in v:
            print x, self.m_session_stat[x]
    def get_keys(self, all=0):
        """
        by default it returns the keys for all questions that have
        been asked. If 'all' is true, it also includes the keys for
        all the wrong answers.
        """
        keys = []
        for st in self.m_session_stat, self.m_today_stat, self.m_last7_stat, self.m_total_stat:
            for k in st:
                if k not in keys:
                    keys.append(k)
                if all:
                    for wk in self.m_total_stat[k]:
                        if wk not in keys:
                            keys.append(wk)
        keys.sort()
        return keys
    def get_num_guess(self, st, key):
        if key not in st:
            return 0
        t = 0
        for i in st[key]:
            t = t + st[key][i]
        return t
    def get_percentage_correct(self, st, key):
        """
        This was added to be used for harmonic-interval.
        """
        if key not in st:
            return 0.0
        if key in st[key]:
            num_correct = st[key][key]
        else:
            num_correct = 0
        total = 0
        for n in st[key]:
            total = total + st[key][n]
        return num_correct * 100.0 / total
    def key_to_pretty_name(self, k):
        return k
    def reset_session(self):
        self.m_session_stat = {}

id_list = [
 '5b884e34-b8c3-4ec4-9257-3607a319143f',
 '45215ce6-0dbd-46cc-9c9c-abcc195f5584',
 'bc508d74-e92f-4c9e-bfc4-d408a9eb69cf',
 'bbf2aef2-0fe8-4a15-946f-0f6aa743ef01',
 '8aba8bbe-7676-4032-b45e-7aacf9aad43c',
 '9b556a77-db69-4173-8b09-3fd8fe7dae58',
 '2e2b0922-4281-4850-91e2-1e8626876136',
 'df92bab5-d40c-41a2-826e-412228d519c3',
 '9efa6347-69f4-4039-b2d1-6a03a1b7feb3',
 '6517b2f2-f649-4e03-8d21-b1e7194846be',
 '1de9a081-033f-4b1a-abc5-d84e60cd2f14',
 'a6cb517d-d738-4cb4-abe0-e8157d6d060c',
 '7e9eb30b-23bf-4079-a54f-414e3dbe0033',
 '52858a6b-3a8f-45e4-bc4a-cc443636fe57',
 'e2dd4da5-6014-4be1-84aa-956ec8501973',
 '25dffb17-f234-4b16-a111-74467b3df6c7',
 '559aa178-a64b-42be-b89f-3346bd3657c5',
 '3aad0be2-3d75-4c3e-94e1-c13c429aeadb',
 '26cb145f-d712-40f4-adc3-274c47e561f4',
 'cd12a7ce-bb34-4945-abf3-3f6033285563',
 '1b72fd17-fd3c-472d-995f-42366e207d04',
 '7954749b-307d-43e9-b16f-77a942cfdd86',
 '0776bd7c-e641-4644-99ca-d35c05ca95eb',
 '466409e7-9086-4623-aff0-7c27f7dfd13b',
 'b2bb9c1f-1734-4e25-82cb-cce63563f565',
 'solfege.org-all-harmonic-intervals',
 'ec7951fc-21a2-4dbb-87fc-2df34ed09df1',
 'eb76c2c3-414d-4682-8a16-b4d3c5475574',
 '324bb3b3-643a-43bd-a7b4-f679e286b631',
 '7f576b58-e1dc-41a5-bb88-defcf2bd5a67',
 'e2214f81-6e62-4e6f-9457-0f5b71d84b86',
 'a37bf068-c972-4b47-90a4-df36e044f90b',
 '90f50e69-6c29-469b-8833-8cd6fe3149a4',
 '30439e2b-911e-42b5-9e9d-3e79cae2658d',
 'a5c20150-4efc-4d81-933d-933221dc920a',
 '2649307e-c919-48da-8f26-ea68f1167ef3',
 '6a329978-af6d-41ab-942a-f6e9da0c0948',
 '763aaac6-3101-4985-9404-a4ff4e86fe51',
 '6da568be-826b-48ad-bcc2-8efae9736334',
 'f62929dc-7122-4173-aad1-4d4eef8779af',
 'e9201967-d700-47d5-8cf5-0925acd95dda',
 '9b2c4563-35f1-4966-8768-bb62b5f9805f',
 'a9659cbe-d9f9-4ae2-9709-63101f38d3e0',
 '3ac10629-fd95-42f6-88f0-ba6e60ecd0b2',
 'eddd8c3f-fa29-4206-94da-306d2f6ab3fd',
 '40fd22c9-4653-4509-af19-9bbd284dd2ec',
 '055bf664-7f29-440c-b220-cbfab6369228',
 '7ea3d1e5-71c5-4da2-b362-8da0e34df778',
 '06841261-d699-42be-8b47-e50a05696626',
 '4130363a-4701-4d44-9057-e41c565b752a',
 'b519146f-6409-4dc7-a205-6be692a18fd3',
 'b0588df6-5b7a-4ff7-9f32-81c92d135bde',
 '1bf61c7d-2852-455a-aba2-0c26524f8043',
 'a652c131-03da-4c9d-bdc5-df6f5309080a',
 '34f408be-03d1-470e-a233-ca89495a1e8f',
 'bc92bed8-0239-48f1-b95a-7fc149ae33c8',
 '7bb26e78-cc6d-457a-aa48-347e8956b1d2',
 '08ef6cf8-fc50-4eaf-87e3-73a7145ac611',
 '9f830e12-1f50-4fa9-8688-1e04469692fa',
 '954cd554-1697-4e3e-873e-983ef93de398',
 '88a9da8e-077f-423e-8a85-c548eda4e505',
 '4a099719-2b60-4927-8984-c01e249eb6bf',
 'c452a14a-9272-43ff-b3cb-489154ea645c',
 'solfege.org-all-melodic-intervals',
 'd4bb2fb7-ba8d-4d0f-b5bd-387c1545dcc3',
 '7fa8b234-b925-48d1-8fde-33b60106b199',
 '939e2bc2-f5d8-4c4a-857c-7a7ae23c429a',
 'e56b8435-20e2-4de2-96ca-4755779cfbd1',
 '344bb3b3-643a-43bd-a7b4-f679e386b631',
 '8b2174e3-5e44-4453-a989-12867c5aefed',
 '142997b9-8330-4dd4-959f-fdcf309ac31a',
 '3d2d8d7a-24bd-4cdb-8389-182e7437537c',
# csound files
 'csound-intonation-harmonic-maj7-8cent',
 '5098fb96-c362-45b9-bbb3-703db149a079',
 'csound-intonation-maj6-40cent',
 'csound-intonation-harmonic-min3-8cent',
 'csound-intonation-harmonic-maj2-8cent',
 'csound-intonation-harmonic-min6-6cent',
 'csound-intonation-p5-5cent',
 'csound-intonation-harmonic-min2-20cent',
 'csound-intonation-harmonic-maj7-30cent',
 'csound-intonation-min7-6cent',
 'csound-intonation-harmonic-min7-30cent',
 'csound-intonation-harmonic-maj7-5cent',
 'csound-intonation-harmonic-min3-6cent',
 'csound-intonation-min6-20cent',
 'csound-intonation-min7-40cent',
 'csound-intonation-maj6-30cent',
 'csound-intonation-maj2-6cent',
 'csound-intonation-harmonic-maj3-6cent',
 'csound-intonation-harmonic-maj6-40cent',
 'csound-intonation-min2-8cent',
 'csound-intonation-min2-5cent',
 'csound-intonation-p5-6cent',
 'csound-intonation-maj6-8cent',
 'csound-intonation-harmonic-min7-8cent',
 'csound-intonation-maj6-5cent',
 'csound-intonation-harmonic-min2-5cent',
 '1cadef8c-859e-4482-a6c4-31bd715b4787',
 'csound-intonation-harmonic-p4-5cent',
 'csound-intonation-min7-5cent',
 'csound-intonation-min2-10cent',
 'csound-intonation-harmonic-min3-30cent',
 'csound-intonation-min7-8cent',
 'csound-intonation-maj7-30cent',
 'csound-intonation-min6-30cent',
 'csound-intonation-harmonic-maj3-40cent',
 'csound-intonation-p4-20cent',
 'csound-intonation-harmonic-min6-30cent',
 'csound-intonation-harmonic-maj3-15cent',
 'csound-intonation-p4-40cent',
 'csound-intonation-harmonic-maj6-8cent',
 'csound-intonation-maj2-10cent',
 'csound-intonation-harmonic-maj3-8cent',
 'csound-intonation-harmonic-maj3-5cent',
 'csound-intonation-maj3-8cent',
 'csound-intonation-harmonic-maj6-6cent',
 'csound-intonation-min2-15cent',
 'csound-intonation-harmonic-p5-30cent',
 'csound-intonation-min3-10cent',
 'csound-intonation-min7-30cent',
 'csound-intonation-harmonic-maj2-5cent',
 'csound-intonation-maj3-15cent',
 'csound-intonation-harmonic-p4-6cent',
 'csound-intonation-harmonic-p5-5cent',
 'csound-intonation-harmonic-maj6-10cent',
 'csound-intonation-min3-5cent',
 'csound-intonation-p5-8cent',
 'csound-intonation-harmonic-min2-30cent',
 'e67c5bd2-a275-4d9a-96a8-52e43a1e8987',
 'csound-intonation-min3-15cent',
 'csound-intonation-harmonic-p4-8cent',
 'csound-intonation-harmonic-min7-40cent',
 'csound-intonation-maj7-8cent',
 'csound-intonation-min6-40cent',
 'csound-intonation-harmonic-p5-10cent',
 'csound-intonation-harmonic-min7-20cent',
 'csound-intonation-harmonic-min2-10cent',
 'csound-intonation-maj2-30cent',
 'csound-intonation-p5-40cent',
 'csound-intonation-maj3-5cent',
 'csound-intonation-min3-30cent',
 'csound-intonation-maj3-40cent',
 'csound-intonation-harmonic-maj3-10cent',
 'csound-intonation-harmonic-p5-20cent',
 'aa5c3b18-664b-4e3d-b42d-2f06582f4135',
 'csound-intonation-harmonic-p5-15cent',
 'csound-intonation-harmonic-min6-5cent',
 'csound-intonation-harmonic-p5-40cent',
 'csound-intonation-p4-10cent',
 'csound-intonation-p4-15cent',
 'csound-intonation-harmonic-min6-40cent',
 'csound-intonation-harmonic-min6-15cent',
 'csound-intonation-p4-30cent',
 'csound-intonation-maj7-10cent',
 'csound-intonation-harmonic-p5-6cent',
 'csound-intonation-harmonic-min7-15cent',
 'csound-intonation-harmonic-min2-8cent',
 'csound-intonation-maj7-5cent',
 'csound-intonation-min7-20cent',
 'csound-intonation-harmonic-min6-8cent',
 'csound-intonation-maj3-30cent',
 'csound-intonation-p5-10cent',
 'csound-intonation-harmonic-maj2-6cent',
 'csound-intonation-harmonic-min6-20cent',
 'csound-intonation-min6-6cent',
 'csound-intonation-harmonic-maj2-15cent',
 '3b1f57e8-2983-4a74-96da-468aa5414e5e',
 'csound-intonation-min3-8cent',
 'csound-intonation-maj6-6cent',
 'csound-intonation-harmonic-maj3-20cent',
 'csound-intonation-maj3-10cent',
 'csound-intonation-harmonic-p4-15cent',
 'csound-intonation-min2-30cent',
 'csound-intonation-p5-30cent',
 'csound-intonation-maj7-20cent',
 'csound-intonation-min6-15cent',
 'csound-intonation-p5-20cent',
 'csound-intonation-harmonic-min7-5cent',
 'csound-intonation-harmonic-min7-6cent',
 'csound-intonation-maj7-6cent',
 'csound-intonation-p5-15cent',
 'csound-intonation-maj6-20cent',
 'csound-intonation-harmonic-maj3-30cent',
 'csound-intonation-harmonic-p4-10cent',
 'csound-intonation-min2-20cent',
 'csound-intonation-harmonic-maj7-10cent',
 'csound-intonation-harmonic-maj7-15cent',
 'csound-intonation-p4-5cent',
 'csound-intonation-min3-40cent',
 'csound-intonation-maj2-8cent',
 'csound-intonation-harmonic-maj7-20cent',
 'csound-intonation-p4-8cent',
 'csound-intonation-min6-10cent',
 'csound-intonation-harmonic-min3-15cent',
 'csound-intonation-harmonic-maj2-10cent',
 'csound-intonation-harmonic-p5-8cent',
 'csound-intonation-min3-20cent',
 'csound-intonation-harmonic-p4-20cent',
 'csound-intonation-min3-6cent',
 'csound-intonation-maj2-5cent',
 'csound-intonation-maj7-15cent',
 'a06b5531-7422-4ea3-8711-ec57e2a4ce22',
 'csound-intonation-min6-8cent',
 'csound-intonation-harmonic-maj6-30cent',
 'csound-intonation-harmonic-maj2-20cent',
 'csound-intonation-maj2-40cent',
 'csound-intonation-maj2-15cent',
 'csound-intonation-harmonic-maj2-30cent',
 'csound-intonation-min2-6cent',
 'csound-intonation-harmonic-min2-15cent',
 'csound-intonation-min7-10cent',
 'csound-intonation-min6-5cent',
 'csound-intonation-maj3-20cent',
 'csound-intonation-harmonic-maj7-40cent',
 'csound-intonation-harmonic-p4-40cent',
 'csound-intonation-harmonic-min3-20cent',
 'csound-intonation-harmonic-min3-5cent',
 'csound-intonation-harmonic-min3-10cent',
 'csound-intonation-harmonic-maj6-20cent',
 'csound-intonation-harmonic-min6-10cent',
 'csound-intonation-harmonic-maj6-5cent',
 'csound-intonation-harmonic-min3-40cent',
 'csound-intonation-min2-40cent',
 'csound-intonation-harmonic-min2-6cent',
 'csound-intonation-min7-15cent',
 'csound-intonation-p4-6cent',
 'csound-intonation-harmonic-min2-40cent',
 'csound-intonation-harmonic-p4-30cent',
 'csound-intonation-maj6-15cent',
 'csound-intonation-harmonic-maj6-15cent',
 'csound-intonation-harmonic-maj7-6cent',
 'b465c807-d7bf-4e3a-a6da-54c78d5b59a1',
 'csound-intonation-harmonic-maj2-40cent',
 'csound-intonation-maj2-20cent',
 'csound-intonation-maj6-10cent',
 'csound-intonation-maj7-40cent',
 'csound-intonation-maj3-6cent',
 'csound-intonation-harmonic-min7-10cent']

class LessonStatistics(AbstractStatistics):
    def __init__(self, teacher):
        AbstractStatistics.__init__(self, teacher)
        self.m_cur_file = None
    def enter_test_mode(self):
        """
        We assume that the program has saved and emptied the data dict
        before calling this function.
        """
        #FIXME nuke STATISTICS_SAVE_DIR??
        self.m_savepath = os.path.join(filesystem.app_data(),
                'testresults', self.m_t.m_P.header.lesson_id)
    def exit_test_mode(self):
        """
        We assume that the program has saved and emptied the data dict
        before calling this function.
        """
        self.m_savepath = os.path.join(filesystem.app_data(),
                'statistics', self.m_t.m_P.header.lesson_id)
    def lessonfile_changed(self, new_file):
        if not new_file:
            return

        self.m_session_stat = {}
        self.m_today_stat = {}
        self.m_last7_stat = {}
        self.m_total_stat = {}
        self.m_cur_file = new_file
        if not usable_statistics:
            return
        self.m_savepath = os.path.join(STATISTICS_SAVE_DIR,
                self.m_t.m_P.header.lesson_id)
        self.create_statistics_dir()

        # if the lessonfile has changed, we have to
        # delete all statistics just to be save.
        def bugfix_hash_file(filename):
            d, f = os.path.split(filename)
            if d == "lesson-files":
                filename = os.path.join("hash-bug-workaround", f)
            return hash(open(filename, 'r').read())

        if self.get_hash_of_statistics() != self.get_hash_of_lessonfile():
            if not (self.m_t.m_P.header.lesson_id in id_list
                    and self.get_hash_of_statistics() == bugfix_hash_file(self.m_t.m_P.m_filename)):
                if os.path.isfile("%s_hash" % self.m_savepath):
                    os.remove("%s_hash" % self.m_savepath)
                for f in os.listdir(self.m_savepath):
                    os.remove(os.path.join(self.m_savepath, f))
        self.load_statistics(self.m_savepath)
    def store_test_passed(self):
        f = open(os.path.join(self.m_savepath, "passed"), 'w')
        f.write(str(self.get()))
        f.close()
    def save_data(self):
        if not usable_statistics:
            print "Warning: not saving statistics because usable_statistics is False"
            return
        if self.m_session_stat == {}:
            return
        if not os.path.isdir(self.m_savepath):
            os.makedirs(self.m_savepath)
        #save the hashvalue for the lessonfile this statistics was made with
        f = open(os.path.join(self.m_savepath, "..",
            '%s_hash' % self.m_t.m_P.header.lesson_id), 'w')
        f.write(str(self.get_hash_of_lessonfile()))
        f.close()
        f = open(os.path.join(self.m_savepath,
                              str(int(time.time()))), 'w')
        pickle.dump(self.m_session_stat, f)
        f.close()
    def create_statistics_dir(self):
        if not os.path.exists(self.m_savepath):
            os.mkdir(self.m_savepath)
    def get_hash_of_statistics(self):
        # FIXME !wrong function name!
        # get the hash for the content of the lessonfile that was used last
        # time statistics was saved
        if os.path.isfile(os.path.join(self.m_savepath, "..",
            '%s_hash' % self.m_t.m_P.header.lesson_id)):
            s = open(os.path.join(self.m_savepath, "..",
                  '%s_hash' % self.m_t.m_P.header.lesson_id)).read()
            try:
                return int(s)
            except:
                return 0
        else:
            return 0
    def get_hash_of_lessonfile(self):
        return hash(open(self.m_cur_file).read())
    def key_to_pretty_name(self, key):
        for question in self.m_t.m_P.m_questions:
            if question.name.cval == key:
                return question.name
        return key

class IntervalStatistics(LessonStatistics):
    def key_to_pretty_name(self, key):
        return utils.int_to_intervalname(key, 1, 1)

class HarmonicIntervalStatistics(LessonStatistics):
    def key_to_pretty_name(self, key):
        return utils.int_to_intervalname(key, 1, 0)

class IdToneStatistics(LessonStatistics):
    def key_to_pretty_name(self, key):
        return mpd.MusicalPitch.new_from_notename(key).get_user_notename()

