from rope.base import ast, evaluate, builtins, pyobjects
from rope.refactor import patchedast, occurrences


class Wildcard(object):

    def get_name(self):
        """Return the name of this wildcard"""

    def matches(self, suspect, arg):
        """Return `True` if `suspect` matches this wildcard"""


class Suspect(object):

    def __init__(self, pymodule, node, name):
        self.name = name
        self.pymodule = pymodule
        self.node = node


class DefaultWildcard(object):
    """The default restructuring wildcard

    The argument passed to this wildcard is in the
    ``key1=value1,key2=value2,...`` format.  Possible keys are:

    * name - for checking the reference
    * type - for checking the type
    * object - for checking the object
    * instance - for checking types but similar to builtin isinstance
    * exact - matching only occurrences with the same name as the wildcard
    * unsure - matching unsure occurrences

    """

    def __init__(self, project):
        self.project = project

    def get_name(self):
        return 'default'

    def matches(self, suspect, arg=''):
        args = parse_arg(arg)

        if not self._check_exact(args, suspect):
            return False
        if not self._check_object(args, suspect):
            return False
        return True

    def _check_object(self, args, suspect):
        kind = None
        expected = None
        unsure = args.get('unsure', False)
        for check in ['name', 'object', 'type', 'instance']:
            if check in args:
                kind = check
                expected = args[check]
            if expected is not None:
                checker = _CheckObject(self.project, expected,
                                       kind, unsure=unsure)
                return checker(suspect.pymodule, suspect.node)
        return True

    def _check_exact(self, args, suspect):
        node = suspect.node
        if args.get('exact'):
            if not isinstance(node, ast.Name) or not node.id == suspect.name:
                return False
        else:
            if not isinstance(node, ast.expr):
                return False
        return True


def parse_arg(arg):
    if isinstance(arg, dict):
        return arg
    result = {}
    tokens = arg.split(',')
    for token in tokens:
        if '=' in token:
            parts = token.split('=', 1)
            result[parts[0].strip()] = parts[1].strip()
        else:
            result[token.strip()] = True
    return result


class _CheckObject(object):

    def __init__(self, project, expected, kind='object', unsure=False):
        self.project = project
        self.kind = kind
        self.unsure = unsure
        self.expected = self._evaluate(expected)

    def __call__(self, pymodule, node):
        pyname = self._evaluate_node(pymodule, node)
        if pyname is None or self.expected is None:
            return self.unsure
        if self._unsure_pyname(pyname, unbound=self.kind == 'name'):
            return True
        if self.kind == 'name':
            return self._same_pyname(self.expected, pyname)
        else:
            pyobject = pyname.get_object()
            if self.kind == 'object':
                objects = [pyobject]
            if self.kind == 'type':
                objects = [pyobject.get_type()]
            if self.kind == 'instance':
                objects = [pyobject]
                objects.extend(self._get_super_classes(pyobject))
                objects.extend(self._get_super_classes(pyobject.get_type()))
            for pyobject in objects:
                if self._same_pyobject(self.expected.get_object(), pyobject):
                    return True
            return False

    def _get_super_classes(self, pyobject):
        result = []
        if isinstance(pyobject, pyobjects.AbstractClass):
            for superclass in pyobject.get_superclasses():
                result.append(superclass)
                result.extend(self._get_super_classes(superclass))
        return result

    def _same_pyobject(self, expected, pyobject):
        return expected == pyobject

    def _same_pyname(self, expected, pyname):
        return occurrences.same_pyname(expected, pyname)

    def _unsure_pyname(self, pyname, unbound=True):
        return self.unsure and occurrences.unsure_pyname(pyname, unbound)

    def _split_name(self, name):
        parts = name.split('.')
        expression, kind = parts[0], parts[-1]
        if len(parts) == 1:
            kind = 'name'
        return expression, kind

    def _evaluate_node(self, pymodule, node):
        scope = pymodule.get_scope().get_inner_scope_for_line(node.lineno)
        expression = node
        if isinstance(expression, ast.Name) and \
           isinstance(expression.ctx, ast.Store):
            start, end = patchedast.node_region(expression)
            text = pymodule.source_code[start:end]
            return evaluate.eval_str(scope, text)
        else:
            return evaluate.eval_node(scope, expression)

    def _evaluate(self, code):
        attributes = code.split('.')
        pyname = None
        if attributes[0] in ('__builtin__', '__builtins__'):
            class _BuiltinsStub(object):
                def get_attribute(self, name):
                    return builtins.builtins[name]

                def __getitem__(self, name):
                    return builtins.builtins[name]

                def __contains__(self, name):
                    return name in builtins.builtins
            pyobject = _BuiltinsStub()
        else:
            pyobject = self.project.get_module(attributes[0])
        for attribute in attributes[1:]:
            pyname = pyobject[attribute]
            if pyname is None:
                return None
            pyobject = pyname.get_object()
        return pyname
