"""Rope preferences."""
from enum import Enum
from dataclasses import asdict, dataclass
from textwrap import dedent
from typing import Any, Callable, Dict, List, Optional, Tuple

from packaging.requirements import Requirement
from pytoolconfig import PyToolConfig, UniversalKey, field
from pytoolconfig.sources import Source

from rope.base.resources import Folder


@dataclass
class AutoimportPrefs:
    # underlined: bool = field(
    #     default=False, description="Cache underlined (private) modules")
    # memory: bool = field(default=None, description="Cache in memory instead of disk")
    # parallel: bool = field(default=True, description="Use multiple processes to parse")

    aliases: List[Tuple[str, str]] = field(
        default_factory=lambda : [
            ("np", "numpy"),
            ("pd", "pandas"),
            ("plt", "matplotlib.pyplot"),
            ("sns", "seaborn"),
            ("tf", "tensorflow"),
            ("sk", "sklearn"),
            ("sm", "statsmodels"),
        ],
        description=dedent("""
            Aliases for module names.  For example, `[('np', 'numpy')]` makes rope recommend
            ``import numpy as np``.
        """),
    )


@dataclass
class ImportPrefs:
    preferred_import_style: str = field(
        default="default",
        description=dedent("""
            Controls how rope inserts new import statements. If set to
            ``"normal-import"`` (default) will insert ``import <package>``; if
            set to ``"from-module"`` will insert ``from <package> import
            <module>``; if set to ``"from-global"`` rope will insert ``from
            <package>.<module> import <object>``.
        """),
    )


@dataclass
class Prefs:
    """Class to store rope preferences."""

    ignored_resources: List[str] = field(
        default_factory=lambda: [
            "*.pyc",
            "*~",
            ".ropeproject",
            ".hg",
            ".svn",
            "_svn",
            ".git",
            ".tox",
            ".venv",
            "venv",
            ".mypy_cache",
            ".pytest_cache",
        ],
        description=dedent("""
            Specify which files and folders to ignore in the project.
            Changes to ignored resources are not added to the history and
            VCSs.  Also they are not returned in ``Project.get_files()``.
            Note that ``?`` and ``*`` match all characters but slashes.
            ``*.pyc``: matches ``test.pyc`` and ``pkg/test.pyc``
            ``mod*.pyc``: matches ``test/mod1.pyc`` but not ``mod/1.pyc``
            ``.svn``: matches ``pkg/.svn`` and all of its children
            ``build/*.o``: matches ``build/lib.o`` but not ``build/sub/lib.o``
            ``build//*.o``: matches ``build/lib.o`` and ``build/sub/lib.o``
        """),
    )
    python_files: List[str] = field(
        default_factory=lambda: ["*.py"],
        description=dedent("""
            Specifies which files should be considered python files.  It is
            useful when you have scripts inside your project.  Only files
            ending with ``.py`` are considered to be python files by
            default.
        """),
    )
    source_folders: List[str] = field(
        description=dedent("""
            Custom source folders:  By default rope searches the project
            for finding source folders (folders that should be searched
            for finding modules).  You can add paths to that list.  Note
            that rope guesses project source folders correctly most of the
            time; use this if you have any problems.
            The folders should be relative to project root and use ``/`` for
            separating folders regardless of the platform rope is running on.
            ``src/my_source_folder`` for instance.
        """),
        default_factory=lambda: [],
    )
    python_path: List[str] = field(
        default_factory=lambda: [],
        description="You can extend python path for looking up modules.",
    )
    save_objectdb: bool = field(
        default=False, description="Should rope save object information or not."
    )
    compress_objectdb: bool = field(
        default=False,
        description="**Deprecated**. This has no effect",
    )
    automatic_soa: bool = field(
        True, "If ``True``, rope analyzes each module when it is being saved."
    )
    soa_followed_calls: int = field(
        default=0, description="The depth of calls to follow in static object analysis"
    )
    perform_doa: bool = field(
        default=True,
        description=dedent("""
            If ``False`` when running modules or unit tests 'dynamic object
            analysis' is turned off. This makes them much faster.
        """),
    )
    validate_objectdb: bool = field(
        default=False,
        description="Rope can check the validity of its object DB when running.",
    )

    max_history_items: int = field(default=32, description="How many undos to hold?")
    save_history: bool = field(
        default=True, description="Shows whether to save history across sessions."
    )
    compress_history: bool = field(
        default=False,
        description="**Deprecated**. This has no effect",
    )

    indent_size: int = field(
        default=4,
        description=dedent("""
            Set the number spaces used for indenting.  According to
            :PEP:`8`, it is best to use 4 spaces.  Since most of rope's
            unit-tests use 4 spaces it is more reliable, too.
        """),
    )

    extension_modules: List[str] = field(
        default_factory=list,
        description="""
Builtin and c-extension modules that are allowed to be imported and inspected by rope.
        """,
    )

    import_dynload_stdmods: bool = field(
        default=True,
        description="Add all standard c-extensions to ``extension_modules`` list.",
    )
    ignore_syntax_errors: bool = field(
        default=False,
        description=dedent("""
            If ``True`` modules with syntax errors are considered to be empty.
            The default value is ``False``; when ``False`` syntax errors raise
            ``rope.base.exceptions.ModuleSyntaxError`` exception.
        """),
    )

    ignore_bad_imports: bool = field(
        default=False,
        description=dedent("""
            If ``True``, rope ignores unresolvable imports.  Otherwise, they
            appear in the importing namespace.
        """),
    )
    prefer_module_from_imports: bool = field(
        default=False,
        description=dedent("""
            **Deprecated**. ``imports.preferred_import_style`` takes
            precedence over ``prefer_module_from_imports``.
        """),
    )

    split_imports: bool = field(
        default=False,
        description=dedent("""
            If ``True``, rope will transform a comma list of imports into
            multiple separate import statements when organizing
            imports.
        """),
    )

    pull_imports_to_top: bool = field(
        default=True,
        description=dedent("""
            If ``True``, rope will remove all top-level import statements and
            reinsert them at the top of the module when making changes.
        """),
    )

    sort_imports_alphabetically: bool = field(
        default=False,
        description=dedent("""
            If ``True``, rope will sort imports alphabetically by module name instead
            of alphabetically by import statement, with from imports after normal
            imports.
        """),
    )
    type_hinting_factory: str = field(
        "rope.base.oi.type_hinting.factory.default_type_hinting_factory",
        description=dedent("""
            Location of implementation of
            ``rope.base.oi.type_hinting.interfaces.ITypeHintingFactory``

            In general case, you don't have to change this value, unless you're
            an rope expert.  Change this value to inject you own
            implementations of interfaces listed in module
            ``rope.base.oi.type_hinting.providers.interfaces`` 

            For example, you can add you own providers for Django Models, or
            disable the search type-hinting in a class hierarchy, etc.
        """),
    )
    project_opened: Optional[Callable] = field(
        None,
        description=dedent("""
            This function is called after opening the project.
            Can only be set in config.py.
        """),
    )
    py_version: Optional[Tuple[int, int]] = field(
        default=None,
        description="Minimum python version to target",
        universal_config=UniversalKey.min_py_version,
    )
    dependencies: Optional[List[Requirement]] = field(
        default=None, universal_config=UniversalKey.dependencies
    )
    callbacks: Dict[str, Callable[[Any], None]] = field(
        default_factory=lambda: {},
        description=dedent("""
            Callbacks run when configuration values are changed.
            Can only be set in config.py.
        """),
    )
    autoimport: AutoimportPrefs = field(
        default_factory=AutoimportPrefs,
        description="Preferences for Autoimport",
    )
    imports: ImportPrefs = field(
        default_factory=ImportPrefs,
        description="Preferences for Import Organiser",
    )

    def set(self, key: str, value: Any):
        """Set the value of `key` preference to `value`."""
        if key in self.callbacks:
            self.callbacks[key](value)
        else:
            setattr(self, key, value)

    def add(self, key: str, value: Any):
        """Add an entry to a list preference

        Add `value` to the list of entries for the `key` preference.

        """
        if getattr(self, key) is None:
            self[key] = []
        getattr(self, key).append(value)

    def get(self, key: str, default: Any = None):
        """Get the value of the key preference"""
        return getattr(self, key, default)

    def add_callback(self, key: str, callback: Callable):
        """Add `key` preference with `callback` function

        Whenever `key` is set the callback is called with the
        given `value` as parameter.

        """
        self.callbacks[key] = callback

    def __setitem__(self, key: str, value: Any):
        self.set(key, value)

    def __getitem__(self, key: str):
        return self.get(key)


class _RopeConfigSource(Source):
    """Custom source for rope config.py files."""

    name: str = "config.py"
    run_globals: Dict

    def __init__(self, ropefolder: Folder):
        self.ropefolder = ropefolder
        self.run_globals = {}

    def _read(self) -> bool:
        if self.ropefolder is None or not self.ropefolder.has_child("config.py"):
            return False
        config = self.ropefolder.get_child("config.py")
        self.run_globals.update(
            {
                "__name__": "__main__",
                "__builtins__": __builtins__,
                "__file__": config.real_path,
            }
        )
        with open(config.real_path) as f:
            code = compile(f.read(), config.real_path, "exec")
            exec(code, self.run_globals)
        return True

    def parse(self) -> Optional[Dict]:
        prefs = Prefs()
        if not self._read():
            return None
        if "set_prefs" in self.run_globals:
            self.run_globals["set_prefs"](prefs)
        if "project_opened" in self.run_globals:
            prefs["project_opened"] = self.run_globals["project_opened"]
        return asdict(prefs)


def get_config(root: Folder, ropefolder: Folder) -> PyToolConfig:
    custom_sources = [_RopeConfigSource(ropefolder)]
    config = PyToolConfig(
        "rope",
        root.pathlib,
        Prefs,
        custom_sources=custom_sources,
        bases=[".ropefolder"],
        recursive=False,
        global_config=True,
    )
    return config


class ImportStyle(Enum):  # FIXME: Use StrEnum once we're on minimum Python 3.11
    normal_import = "normal-import"
    from_module = "from-module"
    from_global = "from-global"


DEFAULT_IMPORT_STYLE = ImportStyle.normal_import


def get_preferred_import_style(prefs: Prefs) -> ImportStyle:
    try:
        return ImportStyle(prefs.imports.preferred_import_style)
    except ValueError:
        if prefs.imports.preferred_import_style == "default" and prefs.prefer_module_from_imports:
            return ImportStyle.from_module
        return DEFAULT_IMPORT_STYLE
