from abc import abstractmethod, ABC
from typing import List, Tuple, Optional, Protocol


class ImportStatement:
    """Represent an import in a module

    `readonly` attribute controls whether this import can be changed
    by import actions or not.

    """

    def __init__(
        self, import_info, start_line, end_line, main_statement=None, blank_lines=0
    ):
        self.start_line = start_line
        self.end_line = end_line
        self.readonly = False
        self.main_statement = main_statement
        self._import_info = None
        self.import_info = import_info
        self._is_changed = False
        self.new_start = None
        self.blank_lines = blank_lines

    def _get_import_info(self):
        return self._import_info

    def _set_import_info(self, new_import):
        if (
            not self.readonly
            and new_import is not None
            and not new_import == self._import_info
        ):
            self._is_changed = True
            self._import_info = new_import

    import_info = property(_get_import_info, _set_import_info)

    def get_import_statement(self):
        if self._is_changed or self.main_statement is None:
            return self.import_info.get_import_statement()
        else:
            return self.main_statement

    def empty_import(self):
        self.import_info = ImportInfo.get_empty_import()

    def move(self, lineno, blank_lines=0):
        self.new_start = lineno
        self.blank_lines = blank_lines

    def get_old_location(self):
        return self.start_line, self.end_line

    def get_new_start(self):
        return self.new_start

    def is_changed(self):
        return self._is_changed or (
            self.new_start is not None or self.new_start != self.start_line
        )

    def accept(self, visitor):
        return visitor.dispatch(self)


class ImportInfo(ABC):
    names_and_aliases: List[Tuple[str, Optional[str]]]

    @abstractmethod
    def get_imported_primaries(self, context) -> List[str]: ...

    def get_imported_names(self, context):
        return [
            primary.split(".")[0] for primary in self.get_imported_primaries(context)
        ]

    def __repr__(self):
        return f'<{self.__class__.__name__} "{self.get_import_statement()}">'

    @abstractmethod
    def get_import_statement(self) -> str: ...

    def is_empty(self):
        pass

    def __hash__(self):
        return hash(self.get_import_statement())

    def _are_name_and_alias_lists_equal(self, list1, list2):
        if len(list1) != len(list2):
            return False
        for pair1, pair2 in zip(list1, list2):
            if pair1 != pair2:
                return False
        return True

    def __eq__(self, obj):
        return (
            isinstance(obj, self.__class__)
            and self.get_import_statement() == obj.get_import_statement()
        )

    def __ne__(self, obj):
        return not self.__eq__(obj)

    @staticmethod
    def get_empty_import():
        return EmptyImport()


class NormalImport(ImportInfo):
    def __init__(
        self,
        names_and_aliases: List[Tuple[str, Optional[str]]],
    ) -> None:
        self.names_and_aliases = names_and_aliases

    def get_imported_primaries(self, context) -> List[str]:
        result = []
        for name, alias in self.names_and_aliases:
            if alias:
                result.append(alias)
            else:
                result.append(name)
        return result

    def get_import_statement(self) -> str:
        result = "import "
        for name, alias in self.names_and_aliases:
            result += name
            if alias:
                result += " as " + alias
            result += ", "
        return result[:-2]

    def is_empty(self):
        return len(self.names_and_aliases) == 0


class FromImport(ImportInfo):
    module_name: str
    level: int

    def __init__(
        self,
        module_name: str,
        level: int,
        names_and_aliases: List[Tuple[str, Optional[str]]],
    ):
        self.module_name = module_name
        self.level = level
        self.names_and_aliases = names_and_aliases

    def get_imported_primaries(self, context) -> List[str]:
        if self.names_and_aliases[0][0] == "*":
            module = self.get_imported_module(context)
            return [name for name in module if not name.startswith("_")]
        result = []
        for name, alias in self.names_and_aliases:
            if alias:
                result.append(alias)
            else:
                result.append(name)
        return result

    def get_imported_resource(self, context):
        """Get the imported resource

        Returns `None` if module was not found.
        """
        if self.level == 0:
            return context.project.find_module(self.module_name, folder=context.folder)
        else:
            return context.project.find_relative_module(
                self.module_name, context.folder, self.level
            )

    def get_imported_module(self, context):
        """Get the imported `PyModule`

        Raises `rope.base.exceptions.ModuleNotFoundError` if module
        could not be found.
        """
        if self.level == 0:
            return context.project.get_module(self.module_name, context.folder)
        else:
            return context.project.get_relative_module(
                self.module_name, context.folder, self.level
            )

    def get_import_statement(self) -> str:
        result = "from " + "." * self.level + self.module_name + " import "
        for name, alias in self.names_and_aliases:
            result += name
            if alias:
                result += " as " + alias
            result += ", "
        return result[:-2]

    def is_empty(self):
        return len(self.names_and_aliases) == 0

    def is_star_import(self):
        return len(self.names_and_aliases) > 0 and self.names_and_aliases[0][0] == "*"


class EmptyImport(ImportInfo):
    names_and_aliases = []

    def is_empty(self):
        return True

    def get_imported_primaries(self, context) -> List[str]:
        return []

    def get_import_statement(self) -> str:
        raise NotImplementedError()


class ImportContext:
    def __init__(self, project, folder):
        self.project = project
        self.folder = folder
