# Copyright 2014-2016 Insight Software Consortium.
# Copyright 2004-2008 Roman Yakovenko.
# Distributed under the Boost Software License, Version 1.0.
# See http://www.boost.org/LICENSE_1_0.txt

"""
defines classes, that describes C++ classes

This modules contains definition for next C++ declarations:
    - class definition
    - class declaration
    - small helper class for describing C++ class hierarchy
"""

import warnings
from . import scopedef
from . import declaration_utils
from . import declaration
from . import templates
from . import cpptypes
from .. import utils


class ACCESS_TYPES(object):

    """class that defines "access" constants"""
    PUBLIC = "public"
    PRIVATE = "private"
    PROTECTED = "protected"
    ALL = [PUBLIC, PRIVATE, PROTECTED]


class CLASS_TYPES(object):

    """class that defines "class" type constants"""
    CLASS = "class"
    STRUCT = "struct"
    UNION = "union"
    ALL = [CLASS, STRUCT, UNION]


def get_partial_name(name):
    from . import container_traits  # prevent cyclic dependencies
    ct = container_traits.find_container_traits(name)
    if ct:
        return ct.remove_defaults(name)
    elif templates.is_instantiation(name):
        tmpl_name, args = templates.split(name)
        for i, arg_name in enumerate(args):
            args[i] = get_partial_name(arg_name.strip())
        return templates.join(tmpl_name, args)
    else:
        return name


class hierarchy_info_t(object):

    """describes class relationship"""

    def __init__(self, related_class=None, access=None, is_virtual=False):
        """creates class that contains partial information about class
        relationship"""
        if related_class:
            assert(isinstance(related_class, class_t))
        self._related_class = related_class
        if access:
            assert(access in ACCESS_TYPES.ALL)
        self._access = access
        self._is_virtual = is_virtual
        self._declaration_path = None
        self._declaration_path_hash = None

    def __eq__(self, other):
        if not isinstance(other, hierarchy_info_t):
            return False
        return (self.declaration_path_hash ==
                other.declaration_path_hash) \
            and self._declaration_path == other._declaration_path \
            and self._access == other._access \
            and self._is_virtual == other._is_virtual

    def __hash__(self):
        return self.declaration_path_hash

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

    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            return self.__class__.__name__ < other.__class__.__name__
        return (self.declaration_path, self.access, self.is_virtual) < \
            (other.declaration_path, other.access, other.is_virtual)

    @property
    def related_class(self):
        """reference to base or derived :class:`class <class_t>`"""
        return self._related_class

    @related_class.setter
    def related_class(self, new_related_class):
        if new_related_class:
            assert(isinstance(new_related_class, class_t))
        self._related_class = new_related_class
        self._declaration_path = None
        self._declaration_path_hash = None

    @property
    def access(self):
        return self._access

    @access.setter
    def access(self, new_access):
        assert(new_access in ACCESS_TYPES.ALL)
        self._access = new_access

    # TODO: Why is there an access_type / access which are the same ?
    @property
    def access_type(self):
        """describes :class:`hierarchy type <ACCESS_TYPES>`"""
        return self.access

    @access_type.setter
    def access_type(self, new_access_type):
        self.access = new_access_type

    # TODO: check whether GCC XML support this and if so parser this
    # information
    @property
    def is_virtual(self):
        """indicates whether the inheritance is virtual or not"""
        return self._is_virtual

    @is_virtual.setter
    def is_virtual(self, new_is_virtual):
        self._is_virtual = new_is_virtual

    @property
    def declaration_path(self):
        if self._declaration_path is None:
            self._declaration_path = declaration_utils.declaration_path(
                self.related_class)
        return self._declaration_path

    @property
    def declaration_path_hash(self):
        if self._declaration_path_hash is None:
            self._declaration_path_hash = hash(tuple(self.declaration_path))
        return self._declaration_path_hash


class class_declaration_t(declaration.declaration_t):

    """describes class declaration"""

    def __init__(self, name=''):
        """creates class that describes C++ class declaration
        ( and not definition )"""
        declaration.declaration_t.__init__(self, name)
        self._aliases = []
        self._container_traits = None  # Deprecated
        self._container_traits_set = False  # Deprecated
        self._container_traits_cache = None

    def _get__cmp__items(self):
        """implementation details"""
        return []

    def i_depend_on_them(self, recursive=True):
        return []

    @property
    def aliases(self):
        """List of :class:`aliases <typedef_t>` to this instance"""
        return self._aliases

    @aliases.setter
    def aliases(self, new_aliases):
        self._aliases = new_aliases

    @property
    def container_traits(self):
        """reference to :class:`container_traits_impl_t` or None"""

        # Deprecated since 1.8.0. Will be removed in 1.9.0
        warnings.warn(
            "The container_traits attribute is deprecated. \n" +
            "Please use the find_container_traits function from the"
            "declarations module instead.",
            DeprecationWarning)

        if self._container_traits_set is False:
            from . import container_traits  # prevent cyclic dependencies
            self._container_traits_set = True
            self._container_traits = container_traits.find_container_traits(
                self)
            self._container_traits_cache = self._container_traits
        return self._container_traits

    def _get_partial_name_impl(self):
        return get_partial_name(self.name)


class class_t(scopedef.scopedef_t):

    """describes class definition"""

    # Can be set from outside
    USE_DEMANGLED_AS_NAME = True

    def __init__(
            self,
            name='',
            class_type=CLASS_TYPES.CLASS,
            is_abstract=False):
        """creates class that describes C++ class definition"""
        scopedef.scopedef_t.__init__(self, name)
        if class_type:
            assert(class_type in CLASS_TYPES.ALL)
        self._class_type = class_type
        self._bases = []
        self._derived = []
        self._is_abstract = is_abstract
        self._public_members = []
        self._private_members = []
        self._protected_members = []
        self._aliases = []
        self._byte_size = 0
        self._byte_align = 0
        self._container_traits_cache = None
        self._container_traits = None  # Deprecated
        self._container_traits_set = False  # Deprecated
        self._recursive_bases = None
        self._recursive_derived = None
        self._use_demangled_as_name = False

    @property
    def use_demangled_as_name(self):
        if "GCC" in utils.xml_generator:
            return class_t.USE_DEMANGLED_AS_NAME
        elif "CastXML" in utils.xml_generator:
            return False

    @use_demangled_as_name.setter
    def use_demangled_as_name(self, use_demangled_as_name):
        self._use_demangled_as_name = use_demangled_as_name

    def _get_name_impl(self):
        if not self._name:  # class with empty name
            return self._name
        elif self.use_demangled_as_name and self.demangled:

            if not self.cache.demangled_name:
                fname = declaration_utils.full_name(self.parent)
                if fname.startswith('::') and \
                        not self.demangled.startswith('::'):
                    fname = fname[2:]
                if self.demangled.startswith(fname):
                    tmp = self.demangled[len(fname):]  # demangled::name
                    if tmp.startswith('::'):
                        tmp = tmp[2:]
                    if '<' not in tmp and '<' in self._name:
                        # we have template class, but for some reason demangled
                        # name doesn't contain any template
                        # This happens for std::string class, but this breaks
                        # other cases, because this behaviour is not consistent
                        self.cache.demangled_name = self._name
                        return self.cache.demangled_name
                    else:
                        self.cache.demangled_name = tmp
                        return tmp
                else:
                    self.cache.demangled_name = self._name
                    return self._name
            else:
                return self.cache.demangled_name
        else:
            return self._name

    def __str__(self):
        name = declaration_utils.full_name(self)
        if name[:2] == "::":
            name = name[2:]
        return "%s [%s]" % (name, self.class_type)

    def _get__cmp__scope_items(self):
        """implementation details"""
        return [
                self.class_type,
                [declaration_utils.declaration_path(base.related_class) for
                 base in self.bases].sort(),
                [declaration_utils.declaration_path(derive.related_class) for
                 derive in self.derived].sort(),
                self.is_abstract,
                self.public_members.sort(),
                self.private_members.sort(),
                self.protected_members.sort()]

    def __eq__(self, other):
        if not scopedef.scopedef_t.__eq__(self, other):
            return False
        return self.class_type == other.class_type \
            and [declaration_utils.declaration_path(base.related_class) for
                 base in self.bases].sort() \
            == [declaration_utils.declaration_path(base.related_class) for
                base in other.bases].sort() \
            and [declaration_utils.declaration_path(derive.related_class) for
                 derive in self.derived].sort() \
            == [declaration_utils.declaration_path(derive.related_class) for
                derive in other.derived].sort() \
            and self.is_abstract == other.is_abstract \
            and self.public_members.sort() \
            == other.public_members.sort() \
            and self.private_members.sort() \
            == other.private_members.sort() \
            and self.protected_members.sort() \
            == other.protected_members.sort()

    def __hash__(self):
        return hash(self.class_type)

    @property
    def class_type(self):
        """describes class :class:`type <CLASS_TYPES>`"""
        return self._class_type

    @class_type.setter
    def class_type(self, new_class_type):
        if new_class_type:
            assert(new_class_type in CLASS_TYPES.ALL)
        self._class_type = new_class_type

    @property
    def bases(self):
        """list of :class:`base classes <hierarchy_info_t>`"""
        return self._bases

    @bases.setter
    def bases(self, new_bases):
        self._bases = new_bases

    @property
    def recursive_bases(self):
        """list of all :class:`base classes <hierarchy_info_t>`"""
        if self._recursive_bases is None:
            to_go = self.bases[:]
            all_bases = []
            while to_go:
                base = to_go.pop()
                if base not in all_bases:
                    all_bases.append(base)
                    to_go.extend(base.related_class.bases)
            self._recursive_bases = all_bases
        return self._recursive_bases

    @property
    def derived(self):
        """list of :class:`derived classes <hierarchy_info_t>`"""
        return self._derived

    @derived.setter
    def derived(self, new_derived):
        self._derived = new_derived

    @property
    def recursive_derived(self):
        """list of all :class:`derive classes <hierarchy_info_t>`"""
        if self._recursive_derived is None:
            to_go = self.derived[:]
            all_derived = []
            while to_go:
                derive = to_go.pop()
                if derive not in all_derived:
                    all_derived.append(derive)
                    to_go.extend(derive.related_class.derived)
            self._recursive_derived = all_derived
        return self._recursive_derived

    @property
    def is_abstract(self):
        """describes whether class abstract or not"""
        return self._is_abstract

    @is_abstract.setter
    def is_abstract(self, is_abstract):
        self._is_abstract = is_abstract

    @property
    def public_members(self):
        """list of all public :class:`members <declarationt_>`"""
        return self._public_members

    @public_members.setter
    def public_members(self, new_public_members):
        self._public_members = new_public_members

    @property
    def private_members(self):
        """list of all private :class:`members <declarationt_>`"""
        return self._private_members

    @private_members.setter
    def private_members(self, new_private_members):
        self._private_members = new_private_members

    @property
    def protected_members(self):
        """list of all protected :class:`members <declarationt_>`"""
        return self._protected_members

    @protected_members.setter
    def protected_members(self, new_protected_members):
        self._protected_members = new_protected_members

    @property
    def aliases(self):
        """List of :class:`aliases <typedef_t>` to this instance"""
        return self._aliases

    @aliases.setter
    def aliases(self, new_aliases):
        self._aliases = new_aliases

    @property
    def byte_size(self):
        """Size of this class in bytes @type: int"""
        return self._byte_size

    @byte_size.setter
    def byte_size(self, new_byte_size):
        self._byte_size = new_byte_size

    @property
    def byte_align(self):
        """Alignment of this class in bytes @type: int"""
        return self._byte_align

    @byte_align.setter
    def byte_align(self, new_byte_align):
        self._byte_align = new_byte_align

    def _get_declarations_impl(self):
        return self.get_members()

    def get_members(self, access=None):
        """
        returns list of members according to access type

        If access equals to None, then returned list will contain all members.
        You should not modify the list content, otherwise different
        optimization data will stop work and may to give you wrong results.

        :param access: describes desired members
        :type access: :class:ACCESS_TYPES

        :rtype: [ members ]
        """
        if access == ACCESS_TYPES.PUBLIC:
            return self.public_members
        elif access == ACCESS_TYPES.PROTECTED:
            return self.protected_members
        elif access == ACCESS_TYPES.PRIVATE:
            return self.private_members
        else:
            all_members = []
            all_members.extend(self.public_members)
            all_members.extend(self.protected_members)
            all_members.extend(self.private_members)
            return all_members

    def adopt_declaration(self, decl, access):
        """adds new declaration to the class

        :param decl: reference to a :class:`declaration_t`

        :param access: member access type
        :type access: :class:ACCESS_TYPES
        """
        if access == ACCESS_TYPES.PUBLIC:
            self.public_members.append(decl)
        elif access == ACCESS_TYPES.PROTECTED:
            self.protected_members.append(decl)
        elif access == ACCESS_TYPES.PRIVATE:
            self.private_members.append(decl)
        else:
            raise RuntimeError("Invalid access type: %s." % access)
        decl.parent = self
        decl.cache.reset()
        decl.cache.access_type = access

    def remove_declaration(self, decl):
        """
        removes decl from  members list

        :param decl: declaration to be removed
        :type decl: :class:`declaration_t`
        """

        access_type = self.find_out_member_access_type(decl)
        if access_type == ACCESS_TYPES.PUBLIC:
            container = self.public_members
        elif access_type == ACCESS_TYPES.PROTECTED:
            container = self.protected_members
        else:  # decl.cache.access_type == ACCESS_TYPES.PRVATE
            container = self.private_members
        del container[container.index(decl)]
        decl.cache.reset()

    def find_out_member_access_type(self, member):
        """
        returns member access type

        :param member: member of the class
        :type member: :class:`declaration_t`

        :rtype: :class:ACCESS_TYPES
        """
        assert member.parent is self
        if not member.cache.access_type:
            if member in self.public_members:
                access_type = ACCESS_TYPES.PUBLIC
            elif member in self.protected_members:
                access_type = ACCESS_TYPES.PROTECTED
            elif member in self.private_members:
                access_type = ACCESS_TYPES.PRIVATE
            else:
                raise RuntimeError(
                    "Unable to find member within internal members list.")
            member.cache.access_type = access_type
            return access_type
        else:
            return member.cache.access_type

    def __find_out_member_dependencies(self, access_type):
        members = self.get_members(access_type)
        answer = []
        for mem in members:
            answer.extend(mem.i_depend_on_them(recursive=True))
        member_ids = set([id(m) for m in members])
        for dependency in answer:
            if id(dependency.declaration) in member_ids:
                dependency.access_type = access_type
        return answer

    def i_depend_on_them(self, recursive=True):

        answer = []

        for base in self.bases:
            answer.append(
                dependency_info_t(
                    self,
                    base.related_class,
                    base.access_type,
                    "base class"))

        if recursive:
            for access_type in ACCESS_TYPES.ALL:
                answer.extend(self.__find_out_member_dependencies(access_type))

        return answer

    @property
    def container_traits(self):
        """reference to :class:`container_traits_impl_t` or None"""

        # Deprecated since 1.8.0. Will be removed in 1.9.0
        warnings.warn(
            "The container_traits attribute is deprecated. \n" +
            "Please use the find_container_traits function from the"
            "declarations module instead.",
            DeprecationWarning)

        if self._container_traits_set is False:
            from . import container_traits  # prevent cyclic dependencies
            self._container_traits_set = True
            self._container_traits = container_traits.find_container_traits(
                self)
            self._container_traits_cache = self.container_traits
        return self._container_traits

    def find_copy_constructor(self):

        # Deprecated since 1.8.0. Will be removed in 1.9.0
        warnings.warn(
            "The find_copy_constructor method is deprecated. \n" +
            "Please use the find_copy_constructor function from the"
            "declarations module instead.",
            DeprecationWarning)

        from . import type_traits_classes  # prevent cyclic dependencies
        return type_traits_classes.find_copy_constructor(self)

    def find_trivial_constructor(self):

        # Deprecated since 1.8.0. Will be removed in 1.9.0
        warnings.warn(
            "The find_trivial_constructor method is deprecated. \n" +
            "Please use the find_trivial_constructor function from the"
            "declarations module instead.",
            DeprecationWarning)

        from . import type_traits_classes  # prevent cyclic dependencies
        return type_traits_classes.find_trivial_constructor(self)

    def _get_partial_name_impl(self):
        from . import type_traits  # prevent cyclic dependencies
        if type_traits.is_std_string(self):
            return 'string'
        elif type_traits.is_std_wstring(self):
            return 'wstring'
        else:
            return get_partial_name(self.name)

    def find_noncopyable_vars(self):
        """returns list of all `noncopyable` variables"""

        # Deprecated since 1.8.0. Will be removed in 1.9.0
        warnings.warn(
            "The find_noncopyable_vars method is deprecated. \n" +
            "Please use the find_noncopyable_vars function from the"
            "declarations module instead.",
            DeprecationWarning)

        from . import type_traits_classes  # prevent cyclic dependencies
        type_traits_classes.find_noncopyable_vars(self)

    @property
    def has_vtable(self):
        """True, if class has virtual table, False otherwise"""

        # Deprecated since 1.8.0. Will be removed in 1.9.0
        warnings.warn(
            "The has_vtable argument is deprecated. \n" +
            "Please use the has_vtable function from the declarations \n" +
            "module instead.",
            DeprecationWarning)

        # prevent cyclic import
        from . import type_traits_classes
        return type_traits_classes.has_vtable(self)

    @property
    def top_class(self):
        """reference to a parent class, which contains this class and defined
        within a namespace

        if this class is defined under a namespace, self will be returned"""
        curr = self
        parent = self.parent
        while isinstance(parent, class_t):
            curr = parent
            parent = parent.parent
        return curr

class_types = (class_t, class_declaration_t)


class impl_details(object):

    @staticmethod
    def dig_declarations(depend_on_it):

        # FIXME: prevent cyclic imports
        from . import type_traits

        if isinstance(depend_on_it, declaration.declaration_t):
            return [depend_on_it]
        base_type = type_traits.base_type(
            type_traits.remove_alias(depend_on_it))
        if isinstance(base_type, cpptypes.declarated_t):
            return [base_type.declaration]
        elif isinstance(base_type, cpptypes.calldef_type_t):
            result = []
            result.extend(impl_details.dig_declarations(base_type.return_type))
            for argtype in base_type.arguments_types:
                result.extend(impl_details.dig_declarations(argtype))
            if isinstance(base_type, cpptypes.member_function_type_t):
                result.extend(
                    impl_details.dig_declarations(
                        base_type.class_inst))
            return result
        return []


class dependency_info_t(object):

    def __init__(self, declaration, depend_on_it, access_type=None, hint=None):
        object.__init__(self)

        assert isinstance(
            depend_on_it,
            (class_t,
             cpptypes.type_t))
        self._declaration = declaration
        self._depend_on_it = depend_on_it
        self._access_type = access_type
        self._hint = hint

    @property
    def declaration(self):
        return self._declaration
    # short name
    decl = declaration

    @property
    def depend_on_it(self):
        return self._depend_on_it

    @property
    def access_type(self):
        return self._access_type

    @access_type.setter
    def access_type(self, access_type):
        self._access_type = access_type

    def __str__(self):
        return 'declaration "%s" depends( %s ) on "%s" ' \
               % (self.declaration, self.access_type, self.depend_on_it)

    @property
    def hint(self):
        """The declaration, that report dependency can put some additional
        inforamtion about dependency. It can be used later"""
        return self._hint

    def find_out_depend_on_it_declarations(self):
        """If declaration depends on other declaration and not on some type
        this function will return reference to it. Otherwise None will be
        returned
        """
        return impl_details.dig_declarations(self.depend_on_it)

    @staticmethod
    def i_depend_on_them(decl):
        """Returns set of declarations. every item in the returned set,
        depends on a declaration from the input"""

        to_be_included = set()
        for dependency_info in decl.i_depend_on_them():
            for ddecl in dependency_info.find_out_depend_on_it_declarations():
                if ddecl:
                    to_be_included.add(ddecl)

        if isinstance(decl.parent, class_t):
            to_be_included.add(decl.parent)
        return to_be_included

    @staticmethod
    def we_depend_on_them(decls):
        """Returns set of declarations. every item in the returned set,
        depends on a declaration from the input"""
        to_be_included = set()
        for decl in decls:
            to_be_included.update(dependency_info_t.i_depend_on_them(decl))
        return to_be_included
