# Released under the MIT License. See LICENSE for details.
#
"""Functionality for wrangling tool config files.

This lets us auto-populate values such as python-paths or versions
into tool config files automatically instead of having to update
everything everywhere manually. It also provides a centralized location
for some tool defaults across all my projects.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from efro.terminal import Clr

if TYPE_CHECKING:
    from pathlib import Path


def install_tool_config(projroot: Path, src: Path, dst: Path) -> None:
    """Install a config."""
    print(f'Creating tool config: {Clr.BLD}{dst}{Clr.RST}')

    # Special case: if we've got a src .yaml and a dst .json, convert.
    # This can be handy to add annotations/etc. in the src which isn't
    # possible with json.
    if src.suffix == '.yaml' and dst.suffix == '.json':
        import yaml
        import json

        with src.open(encoding='utf-8') as infile:
            contents = yaml.safe_load(infile.read())
        cfg = json.dumps(contents, indent=2, sort_keys=True)

    # In normal cases we just push the source file straight through.
    else:
        with src.open(encoding='utf-8') as infile:
            cfg = infile.read()

    # Some substitutions, etc.
    cfg = _filter_tool_config(projroot, cfg)

    # Add an auto-generated notice.
    comment = None
    if dst.name in ['.dir-locals.el']:
        comment = ';;'
    elif dst.name in [
        '.mypy.ini',
        '.pylintrc',
        '.style.yapf',
        '.clang-format',
        '.editorconfig',
    ]:
        comment = '#'
    if comment is not None:
        cfg = (
            f'{comment} THIS FILE WAS AUTOGENERATED; DO NOT EDIT.\n'
            f'{comment} Source: {src}.\n\n' + cfg
        )

    with dst.open('w', encoding='utf-8') as outfile:
        outfile.write(cfg)


def _filter_tool_config(projroot: Path, cfg: str) -> str:
    # pylint: disable=too-many-locals
    import textwrap

    from efrotools.project import getprojectconfig
    from efrotools.pyver import PYVER

    # Emacs dir-locals defaults. Note that these contain other
    # replacements so need to be at the top.

    name = '__EFRO_EMACS_STANDARD_CPP_LSP_SETUP__'
    if name in cfg:
        cfg = cfg.replace(
            name,
            ';; Set up clangd as our C++ language server.\n'
            ' (c++-ts-mode . ((eglot-server-programs'
            ' . ((c++-ts-mode . ("clangd"'
            ' "--compile-commands-dir=.cache/compile_commands_db"))))))',
        )

    name = '__EFRO_EMACS_STANDARD_PYTHON_LSP_SETUP__'
    if name in cfg:
        cfg = cfg.replace(
            name,
            ';; Set up python-lsp-server as our Python language server.\n'
            ' (python-ts-mode . (\n'
            '     (eglot-server-programs . (\n'
            '         (python-ts-mode . ("__EFRO_PY_BIN__" "-m" "pylsp"))))\n'
            '     (python-shell-interpreter . "__EFRO_PY_BIN__")\n'
            '     (eglot-workspace-configuration . (\n'
            '         (:pylsp . (:plugins (\n'
            '             :pylint (:enabled t)\n'
            '             :flake8 (:enabled :json-false)\n'
            '             :pycodestyle (:enabled :json-false)\n'
            '             :mccabe (:enabled :json-false)\n'
            '             :autopep8 (:enabled :json-false)\n'
            '             :pyflakes (:enabled :json-false)\n'
            '             :rope_autoimport (:enabled :json-false)\n'
            '             :rope_completion (:enabled :json-false)\n'
            '             :rope_rename (:enabled :json-false)\n'
            '             :yapf (:enabled :json-false)\n'
            '             :black (:enabled t\n'
            '                     :skip_string_normalization t\n'
            '                     :line_length 80\n'
            '                     :cache_config t)\n'
            '             :jedi (:extra_paths'
            ' [__EFRO_PYTHON_PATHS_Q_REL_STR__])\n'
            '             :pylsp_mypy (:enabled t\n'
            '                          :live_mode :json-false\n'
            '                          :report_progress t\n'
            '                          :dmypy :json-false))))))))\n',
        )

    # Stick project-root wherever they want.
    cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(projroot))

    # Project Python version; '3.12', etc.
    name = '__EFRO_PY_VER__'
    if name in cfg:
        cfg = cfg.replace(name, PYVER)

    # Project Python version as a binary name; 'python3.12', etc.
    name = '__EFRO_PY_BIN__'
    if name in cfg:
        cfg = cfg.replace(name, str(Path(projroot, '.venv', 'bin', 'python')))

    # Colon-separated list of project Python paths.
    name = '__EFRO_PYTHON_PATHS__'
    if name in cfg:
        pypaths = getprojectconfig(projroot).get('python_paths')
        if pypaths is None:
            raise RuntimeError('python_paths not set in project config')
        assert not any(' ' in p for p in pypaths)
        cfg = cfg.replace(name, ':'.join(f'{projroot}/{p}' for p in pypaths))

    # Quoted relative space-separated list of project Python paths.
    name = '__EFRO_PYTHON_PATHS_Q_REL_STR__'
    if name in cfg:
        pypaths = getprojectconfig(projroot).get('python_paths')
        if pypaths is None:
            raise RuntimeError('python_paths not set in project config')
        assert not any(' ' in p for p in pypaths)
        cfg = cfg.replace(name, ' '.join(f'"{p}"' for p in pypaths))

    # Short project name.
    short_names = {
        'ballistica-internal': 'ba-i',
        'ballistica': 'ba',
        'ballistica-master-server': 'bmas',
        'ballistica-master-server-legacy': 'bmasl',
        'ballistica-server-node': 'basn',
    }
    shortname = short_names.get(projroot.name, projroot.name)
    cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname)

    mypy_standard_settings = textwrap.dedent(
        """
    # We don't want all of our plain scripts complaining
    # about __main__ being redefined.
    scripts_are_modules = True

    # Try to be as strict as we can about using types everywhere.
    no_implicit_optional = True
    warn_unused_ignores = True
    warn_no_return = True
    warn_return_any = True
    warn_redundant_casts = True
    warn_unreachable = True
    warn_unused_configs = True
    disallow_incomplete_defs = True
    disallow_untyped_defs = True
    disallow_untyped_decorators = True
    disallow_untyped_calls = True
    disallow_any_unimported = True
    disallow_subclassing_any = True
    strict_equality = True
    local_partial_types = True
    no_implicit_reexport = True

    enable_error_code = redundant-expr, truthy-bool, \
truthy-function, unused-awaitable, explicit-override
    """
    ).strip()

    cfg = cfg.replace('__EFRO_MYPY_STANDARD_SETTINGS__', mypy_standard_settings)

    name = '__PYTHON_BLACK_EXTRA_ARGS__'
    if name in cfg:
        from efrotools.code import black_base_args

        bargs = black_base_args(projroot)
        assert bargs[2] == 'black'
        cfg = cfg.replace(
            name, '(' + ' '.join(f'"{b}"' for b in bargs[3:]) + ')'
        )

    # Gen a pylint init hook that sets up our Python paths.
    pylint_init_tag = '__EFRO_PYLINT_INIT__'
    if pylint_init_tag in cfg:
        pypaths = getprojectconfig(projroot).get('python_paths')
        if pypaths is None:
            raise RuntimeError('python_paths not set in project config')
        cstr = 'init-hook=import sys;'
        # Stuff our paths in at the beginning in the order they appear
        # in our projectconfig.
        for i, path in enumerate(pypaths):
            cstr += f" sys.path.insert({i}, '{projroot}/{path}');"
        cfg = cfg.replace(pylint_init_tag, cstr)
    return cfg
