import inspect
import yaml
from parallels.core.utils.common import merge_dicts


def entity_pretty_yaml(cls):
    """Set up YAML library to dump/load entity class as mapping."""
    if cls.yaml_prefix is not None:
        prefix_str = "%s." % cls.yaml_prefix
    else:
        prefix_str = ""

    tag = u'!%s%s' % (prefix_str, cls.__name__)

    def namedtuple_representer(dumper, data):
        return dumper.represent_mapping(tag, data.as_dictionary())

    def namedtuple_constructor(loader, node):
        return cls(**loader.construct_mapping(node))

    yaml.add_representer(cls, namedtuple_representer)
    yaml.add_constructor(tag, namedtuple_constructor)

    return cls


class EntityMetaclass(type):
    def __new__(mcs, class_name, class_parents, class_attr):
        c = super(EntityMetaclass, mcs).__new__(mcs, class_name, class_parents, class_attr)
        entity_pretty_yaml(c)
        return c


class Entity(object):
    yaml_prefix = None

    __metaclass__ = EntityMetaclass

    @property
    def properties_list(self):
        """List names of properties of this entity

        By default all constructor arguments are considered as entity properties.

        :rtype: set[str]
        """
        return set(inspect.getargspec(self.__init__).args) - {'self'}

    def as_dictionary(self, recursive=False):
        if recursive:
            return self._recursive_as_dictionary(self)
        else:
            result = {}

            for property_name in self.properties_list:
                property_value = getattr(self, property_name)
                result[property_name] = property_value

            return result

    @classmethod
    def _recursive_as_dictionary(cls, entity_object):
        if isinstance(entity_object, Entity):
            result = {}

            for property_name in entity_object.properties_list:
                property_value = getattr(entity_object, property_name)
                property_value = cls._recursive_as_dictionary(property_value)
                result[property_name] = property_value

            return result
        elif isinstance(entity_object, list):
            result = []

            for item in entity_object:
                result.append(cls._recursive_as_dictionary(item))

            return result
        elif isinstance(entity_object, dict):
            result = {}

            for k, v in entity_object.items():
                result[k] = cls._recursive_as_dictionary(v)

            return result
        else:
            return entity_object

    def clone(self, **kwargs):
        """Clone entity. Optionally replace attributes with another ones"""
        attributes = self.as_dictionary()
        for k, v in kwargs.items():
            attributes[k] = v
        return self.__class__(**attributes)

    @classmethod
    def create_none_filled(cls, **kwargs):
        properties = merge_dicts(
            {name: None for name in set(inspect.getargspec(cls.__init__).args) - {'self'}},
            kwargs
        )

        return cls(**properties)

    @staticmethod
    def _meta_attributes():
        return {'properties_list', 'as_dictionary', 'yaml_prefix', 'create_none_filled', 'clone'}

    def __str__(self):
        properties = ', '.join([
            "%s=%s" % (p, self._format_property_value(getattr(self, p)))
            for p in self.properties_list
        ])
        return '%s(%s)' % (self.__class__.__name__, properties)

    def __repr__(self):
        return str(self)

    def _format_property_value(self, prop):
        if isinstance(prop, list):
            return '[%s]' % ', '.join([self._format_property_value(i) for i in prop])
        if isinstance(prop, Entity):
            return str(prop)
        else:
            return repr(prop)

    def __eq__(self, other):
        return type(self) == type(other) and all([
            getattr(self, prop) == getattr(other, prop) for prop in self.properties_list
        ])

    def __ne__(self, other):
        return not self.__eq__(other)

    def __iter__(self):
        for field in self.properties_list:
            yield getattr(self, field)
