"""
Central caching module.

"""

from sys import getsizeof
import os
import threading
from collections import defaultdict

from evennia.server.models import ServerConfig
from evennia.utils.utils import uses_database, to_str, get_evennia_pids

_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__

_IS_SUBPROCESS = os.getpid() in get_evennia_pids()
_IS_MAIN_THREAD = threading.currentThread().getName() == "MainThread"

#
# Set up the cache stores
#

_ATTR_CACHE = {}
_PROP_CACHE = defaultdict(dict)

#------------------------------------------------------------
# Cache key hash generation
#------------------------------------------------------------

if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4':
    # mysql <5.6.4 don't support millisecond precision
    _DATESTRING = "%Y:%m:%d-%H:%M:%S:000000"
else:
    _DATESTRING = "%Y:%m:%d-%H:%M:%S:%f"


def hashid(obj, suffix=""):
    """
    Returns a per-class unique hash that combines the object's
    class name with its idnum and creation time. This makes this id unique also
    between different typeclassed entities such as scripts and
    objects (which may still have the same id).
    """
    if not obj:
        return obj
    try:
        hid = _GA(obj, "_hashid")
    except AttributeError:
        try:
            date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id")
        except AttributeError:
            try:
                # maybe a typeclass, try to go to dbobj
                obj = _GA(obj, "dbobj")
                date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id")
            except AttributeError:
                # this happens if hashing something like ndb. We have to
                # rely on memory adressing in this case.
                date, idnum = "InMemory", id(obj)
        if not idnum or not date:
            # this will happen if setting properties on an object which
            # is not yet saved
            return None
        # we have to remove the class-name's space, for eventual use
        # of memcached
        hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum)
        hid = hid.replace(" ", "")
        # we cache the object part of the hashid to avoid too many
        # object lookups
        _SA(obj, "_hashid", hid)
    # build the complete hashid
    hid = "%s%s" % (hid, suffix)
    return to_str(hid)


#------------------------------------------------------------
# Cache callback handlers
#------------------------------------------------------------

# callback to field pre_save signal (connected in evennia.server.server)
#def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
#    """
#    Called at the beginning of the field save operation. The save method
#    must be called with the update_fields keyword in order to be most efficient.
#    This method should NOT save; rather it is the save() that triggers this
#    function. Its main purpose is to allow to plug-in a save handler and oob
#    handlers.
#    """
#    if raw:
#        return
#    if update_fields:
#        # this is a list of strings at this point. We want field objects
#        update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields)
#    else:
#        # meta.fields are already field objects; get them all
#        update_fields = _GA(_GA(instance, "_meta"), "fields")
#    for field in update_fields:
#        fieldname = field.name
#        handlername = "_at_%s_presave" % fieldname
#        handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
#        if callable(handler):
#            handler()


def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
    """
    Called at the beginning of the field save operation. The save method
    must be called with the update_fields keyword in order to be most efficient.
    This method should NOT save; rather it is the save() that triggers this
    function. Its main purpose is to allow to plug-in a save handler and oob
    handlers.
    """
    if raw:
        return
    if update_fields:
        # this is a list of strings at this point. We want field objects
        update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields)
    else:
        # meta.fields are already field objects; get them all
        update_fields = _GA(_GA(instance, "_meta"), "fields")
    for field in update_fields:
        fieldname = field.name
        handlername = "_at_%s_postsave" % fieldname
        handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
        if callable(handler):
            handler()
        trackerhandler = _GA(instance, "_trackerhandler") if "_trackerhandler" in _GA(instance, '__dict__') else None
        if trackerhandler:
            trackerhandler.update(fieldname, _GA(instance, fieldname))

#------------------------------------------------------------
# Attribute lookup cache
#------------------------------------------------------------

def get_attr_cache(obj):
    "Retrieve lookup cache"
    hid = hashid(obj)
    return _ATTR_CACHE.get(hid, None)


def set_attr_cache(obj, store):
    "Set lookup cache"
    global _ATTR_CACHE
    hid = hashid(obj)
    _ATTR_CACHE[hid] = store

#------------------------------------------------------------
# Property cache - this is a generic cache for properties stored on models.
#------------------------------------------------------------

# access methods

def get_prop_cache(obj, propname):
    "retrieve data from cache"
    hid = hashid(obj, "-%s" % propname)
    return _PROP_CACHE[hid].get(propname, None) if hid else None


def set_prop_cache(obj, propname, propvalue):
    "Set property cache"
    hid = hashid(obj, "-%s" % propname)
    if hid:
        _PROP_CACHE[hid][propname] = propvalue


def del_prop_cache(obj, propname):
    "Delete element from property cache"
    hid = hashid(obj, "-%s" % propname)
    if hid:
        if propname in _PROP_CACHE[hid]:
            del _PROP_CACHE[hid][propname]


def flush_prop_cache():
    "Clear property cache"
    global _PROP_CACHE
    _PROP_CACHE = defaultdict(dict)


def get_cache_sizes():
    """
    Get cache sizes, expressed in number of objects and memory size in MB
    """
    global _ATTR_CACHE, _PROP_CACHE
    attr_n = len(_ATTR_CACHE)
    attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0
    prop_n = sum(len(dic) for dic in _PROP_CACHE.values())
    prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0
    return (attr_n, attr_mb), (prop_n, prop_mb)


