# Copyright 2014 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 all "built-in" classes that implement declarations compare
functionality according to some criteria
"""

import os
import re
from . import algorithm
from . import variable
from . import namespace
from . import calldef
from . import cpptypes
from . import templates
from . import class_declaration
from pygccxml import utils


class matcher_base_t(object):

    """matcher_base_t class defines interface for classes that will implement
       compare functionality according to some criteria.
    """

    def __init__(self):
        object.__init__(self)

    def __call__(self, decl):
        raise NotImplementedError(
            "matcher must always implement the __call__() method.")

    def __invert__(self):
        """not-operator (~)"""
        return not_matcher_t(self)

    def __and__(self, other):
        """and-operator (&)"""
        return and_matcher_t([self, other])

    def __or__(self, other):
        """or-operator (|)"""
        return or_matcher_t([self, other])

    def __str__(self):
        return "base class for all matchers"


class and_matcher_t(matcher_base_t):

    """
    Combine several other matchers with "&" (and) operator.

    For example: find all private functions with name XXX

    .. code-block:: python

       matcher = access_type_matcher_t( 'private' ) & \
           calldef_matcher_t( name='XXX' )
    """

    def __init__(self, matchers):
        matcher_base_t.__init__(self)
        self.matchers = matchers

    def __call__(self, decl):
        for matcher in self.matchers:
            if not matcher(decl):
                return False
        return True

    def __str__(self):
        return " & ".join(["(%s)" % str(x) for x in self.matchers])


class or_matcher_t(matcher_base_t):

    """Combine several other matchers with "|" (or) operator.

    For example: find all functions and variables with name 'XXX'

    .. code-block:: python

       matcher = variable_matcher_t( name='XXX' ) | \
           calldef_matcher_t( name='XXX' )

    """

    def __init__(self, matchers):
        matcher_base_t.__init__(self)
        self.matchers = matchers

    def __call__(self, decl):
        for matcher in self.matchers:
            if matcher(decl):
                return True
        return False

    def __str__(self):
        return " | ".join(["(%s)" % str(x) for x in self.matchers])


class not_matcher_t(matcher_base_t):

    """
    return the inverse result of a matcher

    For example: find all public and protected declarations

    .. code-block:: python

       matcher = ~access_type_matcher_t( 'private' )
    """

    def __init__(self, matcher):
        matcher_base_t.__init__(self)
        self.matcher = matcher

    def __call__(self, decl):
        return not self.matcher(decl)

    def __str__(self):
        return "~(%s)" % str(self.matcher)


class declaration_matcher_t(matcher_base_t):

    """
    Instance of this class will match declarations by next criteria:
          - declaration name, also could be fully qualified name
            Example: `wstring` or `::std::wstring`
          - declaration type
            Example: :class:`class_t`, :class:`namespace_t`,
            :class:`enumeration_t`
          - location within file system ( file or directory )
    """

    def __init__(
            self,
            name=None,
            decl_type=None,
            header_dir=None,
            header_file=None):
        """
        :param decl_type: declaration type to match by. For example
        :class:`enumeration_t`.
        :type decl_type: any class that derives from :class:`declaration_t`
        class

        :param name: declaration name, could be full name.
        :type name: str

        :param header_dir: absolute directory path
        :type header_dir: str

        :param header_file: absolute file path
        :type header_file: str

        """
        # An other option is that pygccxml will create absolute path using
        # os.path.abspath function. But I think this is just wrong, because
        # abspath builds path using current working directory. This behavior
        # is fragile and very difficult to find a bug.
        matcher_base_t.__init__(self)
        self.decl_type = decl_type
        self.__name = None
        self.__opt_is_tmpl_inst = None
        self.__opt_tmpl_name = None
        self.__opt_is_full_name = None
        self.__decl_name_only = None

        # Set the name through the setter.
        self.name = name

        self.header_dir = header_dir
        self.header_file = header_file

        if self.header_dir:
            self.header_dir = utils.normalize_path(self.header_dir)
            if not os.path.isabs(self.header_dir):
                raise RuntimeError(
                    "Path to header directory should be absolute!")

        if self.header_file:
            self.header_file = utils.normalize_path(self.header_file)
            if not os.path.isabs(self.header_file):
                raise RuntimeError("Path to header file should be absolute!")

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, name):
        self.__name = name
        if not self.__name:
            self.__opt_is_tmpl_inst = None
            self.__opt_tmpl_name = None
            self.__opt_is_full_name = None
            self.__decl_name_only = None
        else:
            self.__opt_is_tmpl_inst = templates.is_instantiation(self.__name)
            self.__opt_tmpl_name = templates.name(self.__name)
            if self.__opt_is_tmpl_inst:
                if '::' in self.__opt_tmpl_name:
                    self.__opt_is_full_name = True
                    self.__decl_name_only = \
                        self.__opt_tmpl_name.split('::')[-1]
                else:
                    self.__opt_is_full_name = False
                    self.__decl_name_only = self.__opt_tmpl_name
                self.__name = templates.normalize(name)
            else:
                if '::' in self.__name:
                    self.__opt_is_full_name = True
                    self.__decl_name_only = self.__name.split('::')[-1]
                else:
                    self.__opt_is_full_name = False
                    self.__decl_name_only = self.__name

    def __str__(self):
        msg = []
        if self.decl_type is not None:
            msg.append('(decl type==%s)' % self.decl_type.__name__)
        if self.name is not None:
            msg.append('(name==%s)' % self.name)
        if self.header_dir is not None:
            msg.append('(header dir==%s)' % self.header_dir)
        if self.header_file is not None:
            msg.append('(header file==%s)' % self.header_file)
        if not msg:
            msg.append('any')
        return ' and '.join(msg)

    def __call__(self, decl):
        if self.decl_type is not None:
            if not isinstance(decl, self.decl_type):
                return False
        if self.name is not None:
            if not self.check_name(decl):
                return False
        if self.header_dir is not None:
            if decl.location:
                decl_dir = os.path.abspath(
                    os.path.dirname(decl.location.file_name))
                decl_dir = utils.normalize_path(decl_dir)
                if decl_dir[:len(self.header_dir)] != self.header_dir:
                    return False
            else:
                return False
        if self.header_file is not None:
            if decl.location:
                decl_file = os.path.abspath(decl.location.file_name)
                decl_file = utils.normalize_path(decl_file)
                if decl_file != self.header_file:
                    return False
            else:
                return False
        return True

    def check_name(self, decl):
        assert self.name is not None
        if self.__opt_is_tmpl_inst:
            if not self.__opt_is_full_name:
                if self.name != templates.normalize(decl.name) \
                   and self.name != templates.normalize(decl.partial_name):
                    return False
            else:
                if self.name != templates.normalize(
                        algorithm.full_name(decl, with_defaults=True)) \
                    and self.name != templates.normalize(
                        algorithm.full_name(decl, with_defaults=False)):
                    return False
        else:
            if not self.__opt_is_full_name:
                if self.name != decl.name and self.name != decl.partial_name:
                    return False
            else:
                if self.name != algorithm.full_name(decl, with_defaults=True) \
                   and self.name != algorithm.full_name(
                        decl, with_defaults=False):
                    return False
        return True

    def is_full_name(self):
        return self.__opt_is_full_name

    @property
    def decl_name_only(self):
        return self.__decl_name_only


class variable_matcher_t(declaration_matcher_t):

    """
    Instance of this class will match variables by next criteria:
        - :class:`declaration_matcher_t` criteria
        - variable type. Example: :class:`int_t` or 'int'
    """

    def __init__(
            self,
            name=None,
            type=None,
            header_dir=None,
            header_file=None):
        """
        :param type: variable type
        :type type: string or instance of :class:`type_t` derived class
        """
        declaration_matcher_t.__init__(
            self,
            name=name,
            decl_type=variable.variable_t,
            header_dir=header_dir,
            header_file=header_file)
        self.type = type

    def __call__(self, decl):
        if not super(variable_matcher_t, self).__call__(decl):
            return False
        if self.type is not None:
            if isinstance(self.type, cpptypes.type_t):
                if self.type != decl.type:
                    return False
            else:
                if self.type != decl.type.decl_string:
                    return False
        return True

    def __str__(self):
        msg = [super(variable_matcher_t, self).__str__()]
        if msg == ['any']:
            msg = []
        if self.type is not None:
            msg.append('(value type==%s)' % str(self.type))
        if not msg:
            msg.append('any')
        return ' and '.join(msg)


class namespace_matcher_t(declaration_matcher_t):

    """Instance of this class will match namespaces by name."""

    def __init__(self, name=None):
        declaration_matcher_t.__init__(
            self,
            name=name,
            decl_type=namespace.namespace_t)

    def __call__(self, decl):
        if self.name and decl.name == '':
            # unnamed namespace have same name as thier parent, we should
            # prevent this happens. The price is: user should search for
            # unnamed namespace directly.
            return False
        return super(namespace_matcher_t, self).__call__(decl)


class calldef_matcher_t(declaration_matcher_t):

    """
    Instance of this class will match callable by the following criteria:
       * :class:`declaration_matcher_t` criteria
       * return type. For example: :class:`int_t` or 'int'
       * argument types

    """

    def __init__(
            self,
            name=None,
            return_type=None,
            arg_types=None,
            decl_type=None,
            header_dir=None,
            header_file=None):
        """
        :param return_type: callable return type
        :type return_type: string or instance of :class:`type_t` derived class

        :type arg_types: list
        :param arg_types: list of function argument types. `arg_types` can
                          contain.
                          Any item within the list could be string or instance
                          of :class:`type_t` derived class. If you don't want
                          some argument to participate in match you can put
                          None.

        For example:

          .. code-block:: python

             calldef_matcher_t( arg_types=[ 'int &', None ] )

        will match all functions that takes 2 arguments, where the first one is
        reference to integer and second any
        """
        if None is decl_type:
            decl_type = calldef.calldef_t
        declaration_matcher_t.__init__(
            self,
            name=name,
            decl_type=decl_type,
            header_dir=header_dir,
            header_file=header_file)

        self.return_type = return_type
        self.arg_types = arg_types

    def __call__(self, decl):
        if not super(calldef_matcher_t, self).__call__(decl):
            return False
        if self.return_type is not None \
           and not self.__compare_types(self.return_type, decl.return_type):
            return False
        if self.arg_types:
            if isinstance(self.arg_types, (list, tuple)):
                if len(self.arg_types) != len(decl.arguments):
                    return False
                for type_or_str, arg in zip(self.arg_types, decl.arguments):
                    if None == type_or_str:
                        continue
                    else:
                        if not self.__compare_types(type_or_str, arg.type):
                            return False
        return True

    def __compare_types(self, type_or_str, type):
        assert type_or_str
        if type is None:
            return False
        if isinstance(type_or_str, cpptypes.type_t):
            if type_or_str != type:
                return False
        else:
            if type_or_str != type.decl_string:
                return False
        return True

    def __str__(self):
        msg = [super(calldef_matcher_t, self).__str__()]
        if msg == ['any']:
            msg = []
        if self.return_type is not None:
            msg.append('(return type==%s)' % str(self.return_type))
        if self.arg_types:
            for i in range(len(self.arg_types)):
                if self.arg_types[i] is None:
                    msg.append('(arg %d type==any)' % i)
                else:
                    msg.append(
                        '(arg %d type==%s)' %
                        (i, str(self.arg_types[i])))
        if not msg:
            msg.append('any')
        return ' and '.join(msg)


class operator_matcher_t(calldef_matcher_t):

    """
    Instance of this class will match operators by next criteria:
        * :class:`calldef_matcher_t` criteria
        * operator symbol: =, !=, (), [] and etc
    """

    def __init__(
            self,
            name=None,
            symbol=None,
            return_type=None,
            arg_types=None,
            decl_type=None,
            header_dir=None,
            header_file=None):
        """
        :param symbol: operator symbol
        :type symbol: str
        """
        if None is decl_type:
            decl_type = calldef.operator_t
        calldef_matcher_t.__init__(
            self,
            name=name,
            return_type=return_type,
            arg_types=arg_types,
            decl_type=decl_type,
            header_dir=header_dir,
            header_file=header_file)
        self.symbol = symbol

    def __call__(self, decl):
        if not super(operator_matcher_t, self).__call__(decl):
            return False
        if self.symbol is not None:
            if self.symbol != decl.symbol:
                return False
        return True

    def __str__(self):
        msg = [super(operator_matcher_t, self).__str__()]
        if msg == ['any']:
            msg = []
        if self.symbol is not None:
            msg.append('(symbol==%s)' % str(self.symbol))
        if not msg:
            msg.append('any')
        return ' and '.join(msg)


class regex_matcher_t(matcher_base_t):

    """
    Instance of this class will match declaration using regular expression.
    User should supply a function that will extract from declaration desired
    information as string. Later, this matcher will match that string using
    user regular expression.
    """

    def __init__(self, regex, function=None):
        """
        :param regex: regular expression
        :type regex: string, an instance of this class will compile it for you

        :param function: function that will be called to get an information
                         from declaration as string. As input this function
                         takes single argument - reference to a declaration.
                         Return value should be string. If function is None,
                         then the matcher will use declaration name.

        """
        matcher_base_t.__init__(self)
        self.regex = re.compile(regex)
        self.function = function
        if None is self.function:
            self.function = lambda decl: decl.name

    def __call__(self, decl):
        text = self.function(decl)
        return bool(self.regex.match(text))

    def __str__(self):
        return '(regex=%s)' % self.regex


class access_type_matcher_t(matcher_base_t):

    """
    Instance of this class will match declaration by its access type: public,
    private or protected. If declarations does not have access type, for
    example free function, then `False` will be returned.
    """

    def __init__(self, access_type):
        """
        :param access_type: declaration access type, could be "public",
        "private", "protected"
        :type access_type: :class: `str`
        """
        matcher_base_t.__init__(self)
        self.access_type = access_type

    def __call__(self, decl):
        if not isinstance(decl.parent, class_declaration.class_t):
            return False
        return (
            self.access_type == decl.parent.find_out_member_access_type(decl)
        )

    def __str__(self):
        return '(access type=%s)' % self.access_type


class virtuality_type_matcher_t(matcher_base_t):

    """
    Instance of this class will match declaration by its virtual type: not
    virtual, virtual or pure virtual. If declarations does not have "virtual"
    property, for example free function, then `False` will be returned.
    """

    def __init__(self, virtuality_type):
        """
        :param access_type: declaration access type
        :type access_type: :class:VIRTUALITY_TYPES defines few constants for
        your convenience.
        """
        matcher_base_t.__init__(self)
        self.virtuality_type = virtuality_type

    def __call__(self, decl):
        if not isinstance(decl.parent, class_declaration.class_t):
            return False
        return self.virtuality_type == decl.virtuality

    def __str__(self):
        return '(virtuality type=%s)' % self.virtuality_type


class custom_matcher_t(matcher_base_t):

    """
    Instance of this class will match declaration by user custom criteria.
    """

    def __init__(self, function):
        """
        :param function: callable, that takes single argument -
                         declaration instance should return True or False
        """
        matcher_base_t.__init__(self)
        self.function = function

    def __call__(self, decl):
        return bool(self.function(decl))

    def __str__(self):
        return '(user criteria)'
