# coding: utf-8

from __future__ import unicode_literals

import abc
import collections
import inspect
import json
import numbers
import datetime

import six


if six.PY2:
    basestring_type = basestring
else:
    basestring_type = str


try:
    from idm.core.workflow.exceptions import WorkflowError  # если импортируем в коде IDM
except ImportError:
    from workflow_exceptions import WorkflowError  # если импортируем внутри контейнера

try:
    from idm.core.workflow import exceptions as idm_exceptions
except ImportError:
    import workflow_exceptions as idm_exceptions


workflow_exceptions = {
    name: value
    for (name, value)
    in inspect.getmembers(idm_exceptions)
    if isinstance(value, type) and issubclass(value, WorkflowError)
}

SERIALIZABLE_TYPES_REGISTRY = dict(**workflow_exceptions)


class SerializableMetaclass(abc.ABCMeta):
    def __new__(typ, name, *args, **kwargs):
        new_class = super(SerializableMetaclass, typ).__new__(typ, name, *args, **kwargs)
        SERIALIZABLE_TYPES_REGISTRY[name] = new_class
        return new_class


@six.add_metaclass(SerializableMetaclass)
class Serializable(object):
    @classmethod
    @abc.abstractmethod
    def from_dict(cls, data, context=None):
        pass

    @abc.abstractmethod
    def as_dict(self):
        pass


SERIALIZABLE_TYPES = [Serializable] + list(workflow_exceptions.values())


def is_serializable_plain_type(obj):
    if isinstance(obj, (basestring_type, numbers.Number)):
        return True
    if obj in (True, False, None):
        return True
    return False


def serialize(obj, skip_functions=False):
    if isinstance(obj, tuple(SERIALIZABLE_TYPES)):
        return {'type': type(obj).__name__, 'value': obj.as_dict()}
    elif isinstance(obj, (list, tuple, set)):
        return [serialize(x) for x in obj if not (skip_functions and (callable(x) or isinstance(x, type)))]
    elif isinstance(obj, datetime.date):
        return {'type': type(obj).__name__, 'value': obj.isoformat()}
    elif isinstance(obj, dict):
        obj.pop('__builtins__', None)
        child_dict = {}
        for k, v in obj.items():
            if skip_functions and (callable(v) or isinstance(v, type) or inspect.ismodule(v)):
                continue
            if not isinstance(k, basestring_type):
                raise ValueError('Serializable dict keys must be strings')
            child_dict[k] = serialize(v)
        return {'type': 'dict', 'value': child_dict}
    elif is_serializable_plain_type(obj):
        return obj
    else:
        raise ValueError('Type cannot be serialized: {}'.format(type(obj).__name__))


def deserialize(obj, context=None, cached_objects=None):
    cached_objects = cached_objects if cached_objects is not None else collections.defaultdict(dict)

    if isinstance(obj, dict):
        if sorted(obj.keys()) != ['type', 'value']:
            raise ValueError('Invalid object structure, should be {"type": ..., "value": {...}}')
        if obj['type'] == 'dict':
            new_dict = {}
            for k, v in obj['value'].items():
                if not isinstance(k, basestring_type):
                    raise ValueError('Serializable dict keys must be strings')
                new_dict[k] = deserialize(v, context=context)
            return new_dict
        elif obj['type'] == 'date':
            return datetime.datetime.strptime(obj['value'], "%Y-%m-%d").date()
        else:
            serializable_type = SERIALIZABLE_TYPES_REGISTRY.get(obj['type'])
            if not serializable_type:
                raise ValueError('Object {} is not serializable'.format(repr(obj)))

            cache_key = json.dumps(obj['value'], sort_keys=True)
            cached_obj = cached_objects.get(obj['type'], {}).get(cache_key)
            if cached_obj is None:
                cached_objects[obj['type']][cache_key] = serializable_type.from_dict(obj['value'], context=context)

            return cached_objects[obj['type']][cache_key]

    elif isinstance(obj, list):
        return [deserialize(x, context=context) for x in obj]
    elif is_serializable_plain_type(obj):
        return obj
    else:
        raise ValueError('Type cannot be serialized: {}'.format(type(obj).__name__))
