# Copyright 2015 Rackspace
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from cafe.common.reporting import cclogging
import six


class CommonToolsMixin(object):
    """Methods used to make building data models easier, common to all types"""

    @staticmethod
    def _bool_to_string(value, true_string='true', false_string='false'):
        """Returns a string representation of a boolean value, or the value
        provided if the value is not an instance of bool
        """

        if isinstance(value, bool):
            return true_string if value is True else false_string
        return value

    @staticmethod
    def _string_to_bool(boolean_string):
        """Returns a boolean value of a boolean value string representation
        """
        if boolean_string.lower() == "true":
            return True
        elif boolean_string.lower() == "false":
            return False
        else:
            raise ValueError(
                msg="Passed in boolean string was neither True or False: {0}"
                .format(boolean_string))

    @staticmethod
    def _remove_empty_values(dictionary):
        """Returns a new dictionary based on 'dictionary', minus any keys with
        values that are None
        """

        return dict(
            (k, v) for k, v in six.iteritems(dictionary) if v is not None)

    @staticmethod
    def _replace_dict_key(dictionary, old_key, new_key, recursion=False):
        """Replaces key names in a dictionary, by default only first level keys
        will be replaced, recursion needs to be set to True for replacing keys
        in nested dicts and/or lists
        """
        if old_key in dictionary:
            dictionary[new_key] = dictionary.pop(old_key)

        # Recursion for nested dicts and lists if flag set to True
        if recursion:
            for key, value in dictionary.items():
                if isinstance(value, dict):
                    CommonToolsMixin._replace_dict_key(value, old_key, new_key,
                                                       recursion=True)
                elif isinstance(value, list):
                    dictionaries = (
                        item for item in value if isinstance(item, dict))
                    for x in dictionaries:
                        CommonToolsMixin._replace_dict_key(x, old_key, new_key,
                                                           recursion=True)
        return dictionary


class JSON_ToolsMixin(object):
    """Methods used to make building json data models easier"""

    pass


class XML_ToolsMixin(object):
    """Methods used to make building xml data models easier"""

    _XML_VERSION = '1.0'
    _ENCODING = 'UTF-8'

    @property
    def xml_header(self):
        return "<?xml version='{version}' encoding='{encoding}'?>".format(
            version=self._XML_VERSION, encoding=self._ENCODING)

    @staticmethod
    def _set_xml_etree_element(
            xml_etree, property_dict, exclude_empty_properties=True):
        '''Sets a dictionary of keys and values as properties of the xml etree
        element if value is not None. Optionally, add all keys and values as
        properties if only if exclude_empty_properties == False.
        '''
        if exclude_empty_properties:
            property_dict = CommonToolsMixin._remove_empty_values(
                property_dict)

        for key in property_dict:
            xml_etree.set(key, property_dict[key])
        return xml_etree

    @staticmethod
    def _remove_xml_etree_namespace(doc, namespace):
        """Remove namespace in the passed document in place."""

        ns = six.u("{{{0}}}".format(namespace))
        nsl = len(ns)
        for elem in list(doc.iter()):
            for key in elem.attrib:
                if key.startswith(ns):
                    new_key = key[nsl:]
                    elem.attrib[new_key] = elem.attrib[key]
                    del elem.attrib[key]
            if elem.tag.startswith(ns):
                elem.tag = elem.tag[nsl:]
        return doc


class BaseModel(object):
    __REPR_SEPARATOR__ = '\n'

    def __init__(self):
        self._log = cclogging.getLogger(
            cclogging.get_object_namespace(self.__class__))

    def __eq__(self, obj):
        try:
            if vars(obj) == vars(self):
                return True
        except:
            pass

        return False

    def __ne__(self, obj):
        if obj is None:
            return True

        if vars(obj) == vars(self):
            return False
        else:
            return True

    def __str__(self):
        strng = '<{0} object> {1}'.format(
            type(self).__name__, self.__REPR_SEPARATOR__)
        for key in list(vars(self).keys()):
            val = getattr(self, key)
            if isinstance(val, cclogging.logging.Logger):
                continue
            elif isinstance(val, six.text_type):
                strng = '{0}{1} = {2}{3}'.format(
                    strng, key, val.encode("utf-8"), self.__REPR_SEPARATOR__)
            else:
                strng = '{0}{1} = {2}{3}'.format(
                    strng, key, val, self.__REPR_SEPARATOR__)
        return '{0}'.format(strng)

    def __repr__(self):
        return self.__str__()


# Splitting the xml and json stuff into mixins cleans up the code but still
# muddies the AutoMarshallingModel namespace.  We could create
# tool objects in the AutoMarshallingModel, which would just act as
# sub-namespaces, to keep it clean. --Jose
class AutoMarshallingModel(
        BaseModel, CommonToolsMixin, JSON_ToolsMixin, XML_ToolsMixin):
    """
    @summary: A class used as a base to build and contain the logic necessary
             to automatically create serialized requests and automatically
             deserialize responses in a format-agnostic way.
    """

    _log = cclogging.getLogger(__name__)

    def __init__(self):
        super(AutoMarshallingModel, self).__init__()
        self._log = cclogging.getLogger(
            cclogging.get_object_namespace(self.__class__))

    def serialize(self, format_type):
        serialization_exception = None
        try:
            serialize_method = '_obj_to_{0}'.format(format_type)
            return getattr(self, serialize_method)()
        except Exception as serialization_exception:
            pass

        if serialization_exception:
            try:
                self._log.error(
                    'Error occured during serialization of a data model into'
                    'the "{0}: \n{1}" format'.format(
                        format_type, serialization_exception))
                self._log.exception(serialization_exception)
            except Exception as exception:
                self._log.exception(exception)
                self._log.debug(
                    "Unable to log information regarding the "
                    "deserialization exception due to '{0}'".format(
                        serialization_exception))
        return None

    @classmethod
    def deserialize(cls, serialized_str, format_type):
        cls._log = cclogging.getLogger(
            cclogging.get_object_namespace(cls))

        model_object = None
        deserialization_exception = None
        if serialized_str and len(serialized_str) > 0:
            try:
                deserialize_method = '_{0}_to_obj'.format(format_type)
                model_object = getattr(cls, deserialize_method)(serialized_str)
            except Exception as deserialization_exception:
                cls._log.exception(deserialization_exception)

        # Try to log string and format_type if deserialization broke
        if deserialization_exception is not None:
            try:
                cls._log.debug(
                    "Deserialization Error: Attempted to deserialize type"
                    " using type: {0}".format(format_type.decode(
                        encoding='UTF-8', errors='ignore')))
                cls._log.debug(
                    "Deserialization Error: Unble to deserialize the "
                    "following:\n{0}".format(serialized_str.decode(
                        encoding='UTF-8', errors='ignore')))
            except Exception as exception:
                cls._log.exception(exception)
                cls._log.debug(
                    "Unable to log information regarding the "
                    "deserialization exception")

        return model_object

    # Serialization Functions
    def _obj_to_json(self):
        raise NotImplementedError

    def _obj_to_xml(self):
        raise NotImplementedError

    # Deserialization Functions
    @classmethod
    def _xml_to_obj(cls, serialized_str):
        raise NotImplementedError

    @classmethod
    def _json_to_obj(cls, serialized_str):
        raise NotImplementedError


class AutoMarshallingListModel(list, AutoMarshallingModel):
    """List-like AutoMarshallingModel used for some special cases"""

    def __str__(self):
        return list.__str__(self)


class AutoMarshallingDictModel(dict, AutoMarshallingModel):
    """Dict-like AutoMarshallingModel used for some special cases"""

    def __str__(self):
        return dict.__str__(self)