import cPickle as pickle
import re
import tempfile
import shutil
import os

from mudsling import utils
import mudsling.utils.modules
from mudsling.utils.migrate import module_name_map, class_name_map
from mudsling.utils.migrate import class_move_map

_ext_id_re = re.compile(r"^(.+)~(.+)~(.+)$")
_external_types = {}


def register_external_type(cls, persistent_id, persistent_load):
    """
    Register a type to be considered 'external' along with the functions to
    return the persistent ID during pickling and instance during unpickling.

    External objects are handy if you have code-controlled instances that you
    do not wish to store in the database. This could be a strategy for reducing
    memory usage, or for enforcing code-based instance data only.

    @see U{http://docs.python.org/2/library/pickle.html#pickle-protocol}

    @param cls: The type these id/load functions apply to. Instances of this
        type will never be pickled directly, and instance persistence is the
        responsibility of the implementor.
    @param persistent_id: A function returning an ASCII persistent identifier
        when passed an object of the specified type.
    @param persistent_load: A callable returning an instance of the specified
        type when given a persistent identifier (as generated by the companion
        persistent_id function).
    """
    _external_types[cls] = (persistent_id, persistent_load)


def _persistent_id(obj):
    cls = getattr(obj, '__class__', None)
    if cls in _external_types:
        f = _external_types[cls][0]
        return "%s~%s~%s" % (cls.__module__, cls.__name__, f(obj))
    else:
        return None  # Pickle as usual.


def _persistent_load(id):
    m = _ext_id_re.match(id)
    if not m:
        raise pickle.UnpickleableError("Cannot unpickle external id: %s" % id)
    module_name, class_name, id = m.groups()
    cls = utils.modules.variable_from_module(module_name, class_name)
    if cls in _external_types:
        return _external_types[cls][1](id)
    else:
        raise pickle.UnpickleableError("No external factory for %s.%s#%s"
                                       % (module_name, class_name, id))


def dump(filepath, obj):
    dir, base = os.path.split(filepath)
    tmp = tempfile.NamedTemporaryFile(prefix=base, dir=dir, delete=False)
    p = pickle.Pickler(tmp, pickle.HIGHEST_PROTOCOL)
    p.persistent_id = _persistent_id
    try:
        p.dump(obj)
        tmp.close()
        if os.name != 'posix' and os.path.isfile(filepath):
            os.remove(filepath)
        os.rename(tmp.name, filepath)
    finally:
        tmp.close()
        if os.path.isfile(tmp.name):
            os.remove(tmp.name)


def load(filepath):
    # Use more flexible Python version for loading.
    import pickle as _pickle
    with open(filepath, 'rb') as file:
        p = _pickle.Unpickler(file)
        p.persistent_load = _persistent_load
        p.dispatch[_pickle.GLOBAL] = mapped_load_global
        return p.load()


def mapped_load_global(self):
    _module = self.readline()[:-1]
    parts = _module.split('.')
    suffix = []
    module = None
    while len(parts):
        search = '.'.join(parts)
        if search in module_name_map:
            module = module_name_map[search]
            if len(suffix):
                module += '.' + '.'.join(suffix)
            break
        suffix.append(parts.pop())
    if module is None:
        module = _module
    name = self.readline()[:-1]
    _full = '%s.%s' % (_module, name)
    _mapped = '%s.%s' % (module, name)
    if _full in class_move_map:
        module, _, name = class_move_map[_full].rpartition('.')
    elif _mapped in class_move_map:
        name = class_move_map[_mapped].split('.')[-1]
    else:
        name_options = ('%s.%s' % (module, name), '%s.%s' % (_module, name))
        for opt in name_options:
            if opt in class_name_map:
                name = opt.split('.')[-1]
                break
    klass = self.find_class(module, name)
    self.append(klass)
