# -*- mode: python -*-
# -*- coding: utf-8 -*-

"""essai: ceci serait un module scodoc/sco_xxx.py
"""

import time
import six.moves._thread

from scodoc_manager import sco_mgr
import app.scodoc.sco_utils as scu
from app.scodoc.notes_log import log
from app.scodoc.sco_exceptions import NoteProcessError
from app.scodoc import sco_cache


#
# Cache global: chaque instance, repérée par sa connexion db, a un cache
# qui est recréé à la demande
#
NOTES_CACHE_INST = {}  # { db_cnx_string : CacheNotesTable instance }

CACHE_formsemestre_inscription = {}
CACHE_evaluations = {}

# cache notes evaluations
def get_evaluations_cache(context):
    """returns cache for evaluations"""
    u = sco_mgr.get_db_uri()
    if u in CACHE_evaluations:
        return CACHE_evaluations[u]
    else:
        log("get_evaluations_cache: new simpleCache")
        CACHE_evaluations[u] = sco_cache.simpleCache()
        return CACHE_evaluations[u]


class CacheNotesTable:
    """gestion rudimentaire de cache pour les NotesTables"""

    def __init__(self):
        log("new CacheTable (id=%s)" % id(self))
        #
        self.lock = six.moves._thread.allocate_lock()
        self.owner_thread = None  # thread owning this cache
        self.nref = 0
        # Cache des NotesTables
        self.cache = {}  # { formsemestre_id : NoteTable instance }
        # Cache des classeur PDF (bulletins)
        self.pdfcache = {}  # { formsemestre_id : (filename, pdfdoc) }
        # Listeners:
        self.listeners = scu.DictDefault(
            defaultvalue={}
        )  # {formsemestre_id : {listener_id : callback }}

    def acquire(self):
        "If this thread does not own the cache, acquire the lock"
        if six.moves._thread.get_ident() != self.owner_thread:
            if self.lock.locked():
                log(
                    "acquire: ident=%s waiting for lock" % six.moves._thread.get_ident()
                )  # XXX debug
            self.lock.acquire()
            self.owner_thread = six.moves._thread.get_ident()
            if self.owner_thread is None:  # bug catching
                log("WARNING: None thread id !")
        self.nref += 1
        # log('nref=%d' % self.nref)

    def release(self):
        "Release the lock"
        cur_owner_thread = self.owner_thread
        # log('release: ident=%s (nref=%d)' % (thread.get_ident(), self.nref))
        self.nref -= 1
        if self.nref == 0:
            self.lock.release()
            self.owner_thread = None
        # Debug:
        if six.moves._thread.get_ident() != cur_owner_thread:
            log(
                "WARNING: release: ident=%s != owner=%s nref=%d"
                % (six.moves._thread.get_ident(), cur_owner_thread, self.nref)
            )
            raise NoteProcessError("problem with notes cache")

    def get_NotesTable(self, context, formsemestre_id):  # >
        import notes_table

        try:
            self.acquire()
            if formsemestre_id in self.cache:
                # log('cache hit %s (id=%s, thread=%s)'
                #    % (formsemestre_id, id(self), thread.get_ident()))
                return self.cache[formsemestre_id]
            else:
                t0 = time.time()
                nt = notes_table.NotesTable(context, formsemestre_id)
                dt = time.time() - t0
                self.cache[formsemestre_id] = nt
                log(
                    "caching formsemestre_id=%s (id=%s) (%gs)"
                    % (formsemestre_id, id(self), dt)
                )
                return nt
        finally:
            self.release()

    def get_cached_formsemestre_ids(self):
        "List of currently cached formsemestre_id"
        return list(self.cache.keys())

    def inval_cache(self, context, formsemestre_id=None, pdfonly=False):  # >
        "expire cache pour un semestre (ou tous si pas d'argument)"
        from app.scodoc import sco_parcours_dut

        log(
            "inval_cache, formsemestre_id=%s pdfonly=%s (id=%s)"
            % (formsemestre_id, pdfonly, id(self))  # >
        )
        try:
            self.acquire()
            if not hasattr(self, "pdfcache"):
                self.pdfcache = {}  # fix for old zope instances...
            if formsemestre_id is None:
                # clear all caches
                log("----- inval_cache: clearing all caches -----")
                # log('cache was containing ' + str(self.cache.keys()))
                # logCallStack() # >>> DEBUG <<<
                if not pdfonly:
                    self.cache = {}
                self.pdfcache = {}
                self._call_all_listeners()
                get_evaluations_cache(
                    context,
                ).inval_cache()
            else:
                # formsemestre_id modifié:
                # on doit virer formsemestre_id et tous les semestres
                # susceptibles d'utiliser des UE capitalisées de ce semestre.
                to_trash = [
                    formsemestre_id
                ] + sco_parcours_dut.list_formsemestre_utilisateurs_uecap(
                    context, formsemestre_id
                )
                if not pdfonly:
                    for formsemestre_id in to_trash:
                        if formsemestre_id in self.cache:
                            log(
                                "delete %s from cache (id=%s)"
                                % (formsemestre_id, id(self))
                            )
                            del self.cache[formsemestre_id]
                            self._call_listeners(formsemestre_id)
                    get_evaluations_cache(
                        context,
                    ).inval_cache()

                for formsemestre_id in to_trash:
                    for (
                        cached_formsemestre_id,
                        cached_version,
                    ) in self.pdfcache.keys():
                        if cached_formsemestre_id == formsemestre_id:
                            log(
                                "delete pdfcache[(%s,%s)]"
                                % (formsemestre_id, cached_version)
                            )
                            del self.pdfcache[(formsemestre_id, cached_version)]
        finally:
            self.release()

    def store_bulletins_pdf(self, formsemestre_id, version, filename, pdfdoc):
        "cache pdf data"
        log(
            "caching PDF formsemestre_id=%s version=%s (id=%s)"
            % (formsemestre_id, version, id(self))
        )
        try:
            self.acquire()
            self.pdfcache[(formsemestre_id, version)] = (filename, pdfdoc)
        finally:
            self.release()

    def get_bulletins_pdf(self, formsemestre_id, version):
        "returns cached PDF, or None if not in the cache"
        try:
            self.acquire()
            if not hasattr(self, "pdfcache"):
                self.pdfcache = {}  # fix for old zope instances...
            r = self.pdfcache.get((formsemestre_id, version), None)
            if r:
                log(
                    "get_bulletins_pdf(%s): cache hit %s (id=%s, thread=%s)"
                    % (version, formsemestre_id, id(self), six.moves._thread.get_ident())
                )
            return r
        finally:
            self.release()

    def add_listener(self, callback, formsemestre_id, listener_id):
        """Add a "listener": a function called each time a formsemestre is modified"""
        self.listeners[formsemestre_id][listener_id] = callback

    def remove_listener(self, formsemestre_id, listener_id):
        """Remove a listener.
        May raise exception if does not exists.
        """
        del self.listeners[formsemestre_id][listener_id]

    def _call_listeners(self, formsemestre_id):
        for listener_id, callback in self.listeners[formsemestre_id].items():
            callback(listener_id)

    def _call_all_listeners(self):
        for formsemestre_id in self.listeners:
            self._call_listeners(formsemestre_id)


def get_notes_cache(context):
    "returns CacheNotesTable instance for us"
    u = sco_mgr.get_db_uri()  # identifie le dept de facon unique
    if u not in NOTES_CACHE_INST:
        log("getNotesCache: creating cache for %s" % u)
        NOTES_CACHE_INST[u] = CacheNotesTable()
    return NOTES_CACHE_INST[u]


def inval_cache(
    context, formsemestre_id=None, pdfonly=False, formsemestre_id_list=None
):  # >
    "expire cache pour un semestre (ou tous si pas d'argument)"
    if formsemestre_id_list:
        for formsemestre_id in formsemestre_id_list:
            get_notes_cache(context).inval_cache(
                context, formsemestre_id=formsemestre_id, pdfonly=pdfonly
            )
            # Affecte aussi cache inscriptions
            get_formsemestre_inscription_cache(context).inval_cache(key=formsemestre_id)
    else:
        get_notes_cache(context).inval_cache(
            context, formsemestre_id=formsemestre_id, pdfonly=pdfonly
        )
        # Affecte aussi cache inscriptions
        get_formsemestre_inscription_cache(context).inval_cache(key=formsemestre_id)


# Cache inscriptions semestres
def get_formsemestre_inscription_cache(context, format=None):
    u = sco_mgr.get_db_uri()
    if u in CACHE_formsemestre_inscription:
        return CACHE_formsemestre_inscription[u]
    else:
        log("get_formsemestre_inscription_cache: new simpleCache")
        CACHE_formsemestre_inscription[u] = sco_cache.simpleCache()
        return CACHE_formsemestre_inscription[u]