# Copyright 2014-2015 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

"""
Define few unrelated algorithms that work on declarations.

"""


def declaration_path(decl, with_defaults=True):
    """
    Returns a list of parent declarations names.

    :param decl: declaration for which declaration path should be calculated
    :type decl: :class:`declaration_t`

    :rtype: [names], where first item contains top parent name and last item
             contains the `decl` name

    """

    if not decl:
        return []
    if not decl.cache.declaration_path:
        result = [decl.name]
        parent = decl.parent
        while parent:
            if parent.cache.declaration_path:
                result.reverse()
                decl.cache.declaration_path = parent.cache.declaration_path + \
                    result
                return decl.cache.declaration_path
            else:
                result.append(parent.name)
                parent = parent.parent
        result.reverse()
        decl.cache.declaration_path = result
        return result
    else:
        return decl.cache.declaration_path


def partial_declaration_path(decl):
    """
    Returns a list of parent declarations names without template arguments that
    have default value.

    :param decl: declaration for which declaration path should be calculated
    :type decl: :class:`declaration_t`

    :rtype: [names], where first item contains top parent name and last item
             contains the `decl` name

    """

    # TODO:
    # If parent declaration cache already has declaration_path, reuse it for
    # calculation.
    if not decl:
        return []
    if not decl.cache.partial_declaration_path:
        result = [decl.partial_name]
        parent = decl.parent
        while parent:
            if parent.cache.partial_declaration_path:
                result.reverse()
                decl.cache.partial_declaration_path \
                    = parent.cache.partial_declaration_path + result
                return decl.cache.partial_declaration_path
            else:
                result.append(parent.partial_name)
                parent = parent.parent
        result.reverse()
        decl.cache.partial_declaration_path = result
        return result
    else:
        return decl.cache.partial_declaration_path


def get_named_parent(decl):
    """
    Returns a reference to a named parent declaration.

    :param decl: the child declaration
    :type decl: :class:`declaration_t`

    :rtype: reference to :class:`declaration_t` or None if not found

    """

    if not decl:
        return None

    parent = decl.parent
    while parent and (not parent.name or parent.name == '::'):
        parent = parent.parent
    return parent


def full_name_from_declaration_path(dpath):
    # Here I have lack of knowledge:
    # TODO: "What is the full name of declaration declared in unnamed
    # namespace?"
    result = [_f for _f in dpath if _f]
    result = result[0] + '::'.join(result[1:])
    return result


def full_name(decl, with_defaults=True):
    """
    Returns declaration full qualified name.

    If `decl` belongs to anonymous namespace or class, the function will return
    C++ illegal qualified name.
    :param decl: :class:`declaration_t`
    :type decl: :class:`declaration_t`
    :rtype: full name of declarations.

    """

    if None is decl:
        raise RuntimeError("Unable to generate full name for None object!")
    if with_defaults:
        if not decl.cache.full_name:
            decl.cache.full_name = full_name_from_declaration_path(
                declaration_path(decl))
        return decl.cache.full_name
    else:
        if not decl.cache.full_partial_name:
            decl.cache.full_partial_name = full_name_from_declaration_path(
                partial_declaration_path(decl))
        return decl.cache.full_partial_name


def make_flatten(decl_or_decls):
    """
    Converts tree representation of declarations to flatten one.

    :param decl_or_decls: reference to list of declaration's or single
        declaration
    :type decl_or_decls: :class:`declaration_t` or [ :class:`declaration_t` ]
    :rtype: [ all internal declarations ]

    """

    import pygccxml.declarations  # prevent cyclic import

    def proceed_single(decl):
        answer = [decl]
        if not isinstance(decl, pygccxml.declarations.scopedef_t):
            return answer
        for elem in decl.declarations:
            if isinstance(elem, pygccxml.declarations.scopedef_t):
                answer.extend(proceed_single(elem))
            else:
                answer.append(elem)
        return answer

    decls = []
    if isinstance(decl_or_decls, list):
        decls.extend(decl_or_decls)
    else:
        decls.append(decl_or_decls)
    answer = []
    for decl in decls:
        answer.extend(proceed_single(decl))
    return answer


def __make_flatten_generator(decl_or_decls):
    """
    Converts tree representation of declarations to flatten one.

    :param decl_or_decls: reference to list of declaration's or single
    declaration
    :type decl_or_decls: :class:`declaration_t` or [ :class:`declaration_t` ]

    :rtype: [ all internal declarations ]

    """

    import pygccxml.declarations

    def proceed_single(decl):
        yield decl
        if not isinstance(decl, pygccxml.declarations.scopedef_t):
            return
        for internal in decl.declarations:
            if isinstance(internal, pygccxml.declarations.scopedef_t):
                for internal_internal in proceed_single(internal):
                    yield internal_internal
            else:
                yield internal

    if isinstance(decl_or_decls, list):
        for creator in decl_or_decls:
            for internal in proceed_single(creator):
                yield internal
    else:
        for internal in proceed_single(decl_or_decls):
            yield internal


def get_global_namespace(decls):
    import pygccxml.declarations
    found = [
        decl for decl in make_flatten(decls) if decl.name == '::' and
        isinstance(decl, pygccxml.declarations.namespace_t)]
    if len(found) == 1:
        return found[0]
    raise RuntimeError("Unable to find global namespace.")


class match_declaration_t(object):
    """
    Helper class for different search algorithms.

    This class will help developer to match declaration by:
        - declaration type, for example :class:`class_t` or
            :class:`operator_t`.
        - declaration name
        - declaration full name
        - reference to parent declaration

    """

    def __init__(self, type=None, name=None, fullname=None, parent=None):
        self.type = type
        self.name = name
        self.fullname = fullname
        self.parent = parent

    def does_match_exist(self, inst):
        """
        Returns True if inst does match one of specified criteria.

        :param inst: declaration instance
        :type inst: :class:`declaration_t`

        :rtype: bool

        """

        answer = True
        if self.type is not None:
            answer &= isinstance(inst, self.type)
        if self.name is not None:
            answer &= inst.name == self.name
        if self.parent is not None:
            answer &= self.parent is inst.parent
        if self.fullname is not None:
            if inst.name:
                answer &= self.fullname == full_name(inst)
            else:
                answer = False
        return answer

    def __call__(self, inst):
        """
        .. code-block:: python

           return self.does_match_exist(inst)

        """

        return self.does_match_exist(inst)


def find_all_declarations(
        declarations,
        type=None,
        name=None,
        parent=None,
        recursive=True,
        fullname=None):
    """
    Returns a list of all declarations that match criteria, defined by
    developer.

    For more information about arguments see :class:`match_declaration_t`
    class.

    :rtype: [ matched declarations ]

    """

    if recursive:
        decls = make_flatten(declarations)
    else:
        decls = declarations

    return list(
        filter(
            match_declaration_t(
                type,
                name,
                fullname,
                parent),
            decls))


def find_declaration(
        declarations,
        type=None,
        name=None,
        parent=None,
        recursive=True,
        fullname=None):
    """
    Returns single declaration that match criteria, defined by developer.
    If more the one declaration was found None will be returned.

    For more information about arguments see :class:`match_declaration_t`
    class.

    :rtype: matched declaration :class:`declaration_t` or None

    """

    decl = find_all_declarations(
        declarations,
        type=type,
        name=name,
        parent=parent,
        recursive=recursive,
        fullname=fullname)
    if len(decl) == 1:
        return decl[0]


def find_first_declaration(
        declarations,
        type=None,
        name=None,
        parent=None,
        recursive=True,
        fullname=None):
    """
    Returns first declaration that match criteria, defined by developer.

    For more information about arguments see :class:`match_declaration_t`
    class.

    :rtype: matched declaration :class:`declaration_t` or None

    """

    matcher = match_declaration_t(type, name, fullname, parent)
    if recursive:
        decls = make_flatten(declarations)
    else:
        decls = declarations
    for decl in decls:
        if matcher(decl):
            return decl
    return None


def declaration_files(decl_or_decls):
    """
    Returns set of files

    Every declaration is declared in some file. This function returns set, that
    contains all file names of declarations.

    :param decl_or_decls: reference to list of declaration's or single
        declaration
    :type decl_or_decls: :class:`declaration_t` or [:class:`declaration_t`]
    :rtype: set(declaration file names)

    """

    files = set()
    decls = make_flatten(decl_or_decls)
    for decl in decls:
        if decl.location:
            files.add(decl.location.file_name)
    return files


class visit_function_has_not_been_found_t(RuntimeError):
    """
    Exception that is raised, from :func:`apply_visitor`, when a visitor could
    not be applied.

    """

    def __init__(self, visitor, decl_inst):
        RuntimeError.__init__(self)
        self.__msg = (
            "Unable to find visit function. Visitor class: %s. " +
            "Declaration instance class: %s'") \
            % (visitor.__class__.__name__, decl_inst.__class__.__name__)

    def __str__(self):
        return self.__msg


def apply_visitor(visitor, decl_inst):
    """
    Applies a visitor on declaration instance.

    :param visitor: instance
    :type visitor: :class:`type_visitor_t` or :class:`decl_visitor_t`

    """

    fname = 'visit_' + \
        decl_inst.__class__.__name__[:-2]  # removing '_t' from class name
    if not hasattr(visitor, fname):
        raise visit_function_has_not_been_found_t(visitor, decl_inst)
    return getattr(visitor, fname)()
