import copy
from itertools import chain

from jedi.parser import tree


def deep_ast_copy(obj, parent=None, new_elements=None):
    """Much, much faster than copy.deepcopy, but just for Parser elements (Doesn't
    copy parents).
    """
    if new_elements is None:
        new_elements = {}

    def copy_node(obj):
        # If it's already in the cache, just return it.
        try:
            return new_elements[obj]
        except KeyError:
            # Actually copy and set attributes.
            new_obj = copy.copy(obj)
            new_elements[obj] = new_obj

        # Copy children
        new_children = []
        for child in obj.children:
            typ = child.type
            if typ in ('whitespace', 'operator', 'keyword', 'number', 'string'):
                # At the moment we're not actually copying those primitive
                # elements, because there's really no need to. The parents are
                # obviously wrong, but that's not an issue.
                new_child = child
            elif typ == 'name':
                new_elements[child] = new_child = copy.copy(child)
                new_child.parent = new_obj
            else:  # Is a BaseNode.
                new_child = copy_node(child)
                new_child.parent = new_obj
            new_children.append(new_child)
        new_obj.children = new_children

        # Copy the names_dict (if there is one).
        try:
            names_dict = obj.names_dict
        except AttributeError:
            pass
        else:
            try:
                new_obj.names_dict = new_names_dict = {}
            except AttributeError:  # Impossible to set CompFor.names_dict
                pass
            else:
                for string, names in names_dict.items():
                    new_names_dict[string] = [new_elements[n] for n in names]
        return new_obj

    if obj.type == 'name':
        # Special case of a Name object.
        new_elements[obj] = new_obj = copy.copy(obj)
        if parent is not None:
            new_obj.parent = parent
    elif isinstance(obj, tree.BaseNode):
        new_obj = copy_node(obj)
        if parent is not None:
            for child in new_obj.children:
                if isinstance(child, tree.Name | tree.BaseNode):
                    child.parent = parent
    else:  # String literals and so on.
        new_obj = obj  # Good enough, don't need to copy anything.
    return new_obj


def call_of_name(name, cut_own_trailer=False):
    """Creates a "call" node that consist of all ``trailer`` and ``power``
    objects.  E.g. if you call it with ``append``::

        list([]).append(3) or None

    You would get a node with the content ``list([]).append`` back.

    This generates a copy of the original ast node.
    """
    par = name
    if tree.is_node(par.parent, 'trailer'):
        par = par.parent

    power = par.parent
    if tree.is_node(power, 'power') and power.children[0] != name \
            and not (power.children[-2] == '**' and
                     name.start_pos > power.children[-1].start_pos):
        par = power
        # Now the name must be part of a trailer
        index = par.children.index(name.parent)
        if index != len(par.children) - 1 or cut_own_trailer:
            # Now we have to cut the other trailers away.
            par = deep_ast_copy(par)
            if not cut_own_trailer:
                # Normally we would remove just the stuff after the index, but
                # if the option is set remove the index as well. (for goto)
                index = index + 1
            par.children[index:] = []

    return par


def get_module_names(module, all_scopes):
    """Returns a dictionary with name parts as keys and their call paths as
    values.
    """
    dct = module.used_names if all_scopes else module.names_dict
    return chain.from_iterable(dct.values())


class FakeImport(tree.ImportName):
    def __init__(self, name, parent, level=0):
        super().__init__([])
        self.parent = parent
        self._level = level
        self.name = name

    def get_defined_names(self):
        return [self.name]

    def aliases(self):
        return {}

    @property
    def level(self):
        return self._level

    @property
    def start_pos(self):
        return 0, 0

    def paths(self):
        return [[self.name]]

    def is_definition(self) -> bool:
        return True


class FakeName(tree.Name):
    def __init__(self, name_str, parent=None, start_pos=(0, 0), is_definition=None):
        """In case is_definition is defined (not None), that bool value will be
        returned.
        """
        super().__init__(tree.zero_position_modifier, name_str, start_pos)
        self.parent = parent
        self._is_definition = is_definition

    def get_definition(self):
        return self.parent

    def is_definition(self):
        if self._is_definition is None:
            return super().is_definition()
        return self._is_definition


class LazyName(FakeName):
    def __init__(self, name, parent_callback, is_definition=None):
        super().__init__(name, is_definition=is_definition)
        self._parent_callback = parent_callback

    @property
    def parent(self):
        return self._parent_callback()

    @parent.setter
    def parent(self, value):
        pass  # Do nothing, super classes can try to set the parent.
