"""A package for handling imports

This package provides tools for modifying module imports after
refactorings or as a separate task.

"""
import rope.base.evaluate
from rope.base import libutils
from rope.base.change import ChangeSet, ChangeContents
from rope.refactor import occurrences, rename
from rope.refactor.importutils import module_imports, actions
from rope.refactor.importutils.importinfo import NormalImport, FromImport
import rope.base.codeanalyze


class ImportOrganizer:
    """Perform some import-related commands

    Each method returns a `rope.base.change.Change` object.

    """

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

    def organize_imports(self, resource, offset=None):
        return self._perform_command_on_import_tools(
            self.import_tools.organize_imports, resource, offset
        )

    def expand_star_imports(self, resource, offset=None):
        return self._perform_command_on_import_tools(
            self.import_tools.expand_stars, resource, offset
        )

    def froms_to_imports(self, resource, offset=None):
        return self._perform_command_on_import_tools(
            self.import_tools.froms_to_imports, resource, offset
        )

    def relatives_to_absolutes(self, resource, offset=None):
        return self._perform_command_on_import_tools(
            self.import_tools.relatives_to_absolutes, resource, offset
        )

    def handle_long_imports(self, resource, offset=None):
        return self._perform_command_on_import_tools(
            self.import_tools.handle_long_imports, resource, offset
        )

    def _perform_command_on_import_tools(self, method, resource, offset):
        pymodule = self.project.get_pymodule(resource)
        before_performing = pymodule.source_code
        import_filter = None
        if offset is not None:
            import_filter = self._line_filter(pymodule.lines.get_line_number(offset))
        result = method(pymodule, import_filter=import_filter)
        if result is not None and result != before_performing:
            changes = ChangeSet(
                method.__name__.replace("_", " ") + " in <%s>" % resource.path
            )
            changes.add_change(ChangeContents(resource, result))
            return changes

    def _line_filter(self, lineno):
        def import_filter(import_stmt):
            return import_stmt.start_line <= lineno < import_stmt.end_line

        return import_filter


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

    def get_import(self, resource):
        """The import statement for `resource`"""
        module_name = libutils.modname(resource)
        return NormalImport(((module_name, None),))

    def get_from_import(self, resource, name):
        """The from import statement for `name` in `resource`"""
        module_name = libutils.modname(resource)
        names = []
        if isinstance(name, list):
            names = [(imported, None) for imported in name]
        else:
            names = [
                (name, None),
            ]
        return FromImport(module_name, 0, tuple(names))

    def module_imports(self, module, imports_filter=None):
        return module_imports.ModuleImports(self.project, module, imports_filter)

    def froms_to_imports(self, pymodule, import_filter=None):
        pymodule = self._clean_up_imports(pymodule, import_filter)
        module_imports = self.module_imports(pymodule, import_filter)
        for import_stmt in module_imports.imports:
            if import_stmt.readonly or not self._is_transformable_to_normal(
                import_stmt.import_info
            ):
                continue
            pymodule = self._from_to_normal(pymodule, import_stmt)

        # Adding normal imports in place of froms
        module_imports = self.module_imports(pymodule, import_filter)
        for import_stmt in module_imports.imports:
            if not import_stmt.readonly and self._is_transformable_to_normal(
                import_stmt.import_info
            ):
                import_stmt.import_info = NormalImport(
                    ((import_stmt.import_info.module_name, None),)
                )
        module_imports.remove_duplicates()
        return module_imports.get_changed_source()

    def expand_stars(self, pymodule, import_filter=None):
        module_imports = self.module_imports(pymodule, import_filter)
        module_imports.expand_stars()
        return module_imports.get_changed_source()

    def _from_to_normal(self, pymodule, import_stmt):
        resource = pymodule.get_resource()
        from_import = import_stmt.import_info
        module_name = from_import.module_name
        for name, alias in from_import.names_and_aliases:
            imported = name
            if alias is not None:
                imported = alias
            occurrence_finder = occurrences.create_finder(
                self.project, imported, pymodule[imported], imports=False
            )
            source = rename.rename_in_module(
                occurrence_finder,
                module_name + "." + name,
                pymodule=pymodule,
                replace_primary=True,
            )
            if source is not None:
                pymodule = libutils.get_string_module(self.project, source, resource)
        return pymodule

    def _clean_up_imports(self, pymodule, import_filter):
        resource = pymodule.get_resource()
        module_with_imports = self.module_imports(pymodule, import_filter)
        module_with_imports.expand_stars()
        source = module_with_imports.get_changed_source()
        if source is not None:
            pymodule = libutils.get_string_module(self.project, source, resource)
        source = self.relatives_to_absolutes(pymodule)
        if source is not None:
            pymodule = libutils.get_string_module(self.project, source, resource)

        module_with_imports = self.module_imports(pymodule, import_filter)
        module_with_imports.remove_duplicates()
        module_with_imports.remove_unused_imports()
        source = module_with_imports.get_changed_source()
        if source is not None:
            pymodule = libutils.get_string_module(self.project, source, resource)
        return pymodule

    def relatives_to_absolutes(self, pymodule, import_filter=None):
        module_imports = self.module_imports(pymodule, import_filter)
        to_be_absolute_list = module_imports.get_relative_to_absolute_list()
        for name, absolute_name in to_be_absolute_list:
            pymodule = self._rename_in_module(pymodule, name, absolute_name)
        module_imports = self.module_imports(pymodule, import_filter)
        module_imports.get_relative_to_absolute_list()
        source = module_imports.get_changed_source()
        if source is None:
            source = pymodule.source_code
        return source

    def _is_transformable_to_normal(self, import_info):
        return isinstance(import_info, FromImport)

    def organize_imports(
        self,
        pymodule,
        unused=True,
        duplicates=True,
        selfs=True,
        sort=True,
        import_filter=None,
    ):
        if unused or duplicates:
            module_imports = self.module_imports(pymodule, import_filter)
            if unused:
                module_imports.remove_unused_imports()
            if self.project.prefs.get("split_imports"):
                module_imports.force_single_imports()
            if duplicates:
                module_imports.remove_duplicates()
            source = module_imports.get_changed_source()
            if source is not None:
                pymodule = libutils.get_string_module(
                    self.project, source, pymodule.get_resource()
                )
        if selfs:
            pymodule = self._remove_self_imports(pymodule, import_filter)
        if sort:
            return self.sort_imports(pymodule, import_filter)
        else:
            return pymodule.source_code

    def _remove_self_imports(self, pymodule, import_filter=None):
        module_imports = self.module_imports(pymodule, import_filter)
        (
            to_be_fixed,
            to_be_renamed,
        ) = module_imports.get_self_import_fix_and_rename_list()
        for name in to_be_fixed:
            try:
                pymodule = self._rename_in_module(pymodule, name, "", till_dot=True)
            except ValueError:
                # There is a self import with direct access to it
                return pymodule
        for name, new_name in to_be_renamed:
            pymodule = self._rename_in_module(pymodule, name, new_name)
        module_imports = self.module_imports(pymodule, import_filter)
        module_imports.get_self_import_fix_and_rename_list()
        source = module_imports.get_changed_source()
        if source is not None:
            pymodule = libutils.get_string_module(
                self.project, source, pymodule.get_resource()
            )
        return pymodule

    def _rename_in_module(self, pymodule, name, new_name, till_dot=False):
        old_name = name.split(".")[-1]
        old_pyname = rope.base.evaluate.eval_str(pymodule.get_scope(), name)
        occurrence_finder = occurrences.create_finder(
            self.project, old_name, old_pyname, imports=False
        )
        changes = rope.base.codeanalyze.ChangeCollector(pymodule.source_code)
        for occurrence in occurrence_finder.find_occurrences(pymodule=pymodule):
            start, end = occurrence.get_primary_range()
            if till_dot:
                new_end = pymodule.source_code.index(".", end) + 1
                space = pymodule.source_code[end : new_end - 1].strip()
                if not space == "":
                    for c in space:
                        if not c.isspace() and c not in "\\":
                            raise ValueError()
                end = new_end
            changes.add_change(start, end, new_name)
        source = changes.get_changed()
        if source is not None:
            pymodule = libutils.get_string_module(
                self.project, source, pymodule.get_resource()
            )
        return pymodule

    def sort_imports(self, pymodule, import_filter=None):
        module_imports = self.module_imports(pymodule, import_filter)
        module_imports.sort_imports()
        return module_imports.get_changed_source()

    def handle_long_imports(
        self, pymodule, maxdots=2, maxlength=27, import_filter=None
    ):
        # IDEA: `maxdots` and `maxlength` can be specified in project config
        # adding new from imports
        module_imports = self.module_imports(pymodule, import_filter)
        to_be_fixed = module_imports.handle_long_imports(maxdots, maxlength)
        # performing the renaming
        pymodule = libutils.get_string_module(
            self.project,
            module_imports.get_changed_source(),
            resource=pymodule.get_resource(),
        )
        for name in to_be_fixed:
            pymodule = self._rename_in_module(pymodule, name, name.split(".")[-1])
        # organizing imports
        return self.organize_imports(
            pymodule, selfs=False, sort=False, import_filter=import_filter
        )


def get_imports(project, pydefined):
    """A shortcut for getting the `ImportInfo` used in a scope"""
    pymodule = pydefined.get_module()
    module = module_imports.ModuleImports(project, pymodule)
    if pymodule == pydefined:
        return [stmt.import_info for stmt in module.imports]
    return module.get_used_imports(pydefined)


def get_module_imports(project, pymodule):
    """A shortcut for creating a `module_imports.ModuleImports` object"""
    return module_imports.ModuleImports(project, pymodule)


def add_import(project, pymodule, module_name, name=None):
    imports = get_module_imports(project, pymodule)
    candidates = []
    names = []
    selected_import = None
    # from mod import name
    if name is not None:
        from_import = FromImport(module_name, 0, [(name, None)])
        names.append(name)
        candidates.append(from_import)
    # from pkg import mod
    if "." in module_name:
        pkg, mod = module_name.rsplit(".", 1)
        from_import = FromImport(pkg, 0, [(mod, None)])
        if project.prefs.get("prefer_module_from_imports"):
            selected_import = from_import
        candidates.append(from_import)
        if name:
            names.append(mod + "." + name)
        else:
            names.append(mod)
    # import mod
    normal_import = NormalImport([(module_name, None)])
    if name:
        names.append(module_name + "." + name)
    else:
        names.append(module_name)

    candidates.append(normal_import)

    visitor = actions.AddingVisitor(project, candidates)
    if selected_import is None:
        selected_import = normal_import
    for import_statement in imports.imports:
        if import_statement.accept(visitor):
            selected_import = visitor.import_info
            break
    imports.add_import(selected_import)
    imported_name = names[candidates.index(selected_import)]
    return imports.get_changed_source(), imported_name
