python3 << EndPython3
import collections
import os
import sys
import vim


def strtobool(text):
    if text.lower() in ["y", "yes", "t", "true", "on", "1"]:
        return True
    if text.lower() in ["n", "no", "f", "false", "off", "0"]:
        return False
    raise ValueError(f"{text} is not convertible to boolean")


class Flag(collections.namedtuple("FlagBase", "name, cast")):
    @property
    def var_name(self):
        return self.name.replace("-", "_")

    @property
    def vim_rc_name(self):
        name = self.var_name
        if name == "line_length":
            name = name.replace("_", "")
        return "g:black_" + name


FLAGS = [
    Flag(name="line_length", cast=int),
    Flag(name="fast", cast=strtobool),
    Flag(name="skip_string_normalization", cast=strtobool),
    Flag(name="quiet", cast=strtobool),
    Flag(name="skip_magic_trailing_comma", cast=strtobool),
    Flag(name="preview", cast=strtobool),
]


def _get_python_binary(exec_prefix, pyver):
    try:
        default = vim.eval("g:pymode_python").strip()
    except vim.error:
        default = ""
    if default and os.path.exists(default):
        return default
    if sys.platform[:3] == "win":
        return exec_prefix / "python.exe"
    bin_path = exec_prefix / "bin"
    exec_path = (bin_path / f"python{pyver[0]}.{pyver[1]}").resolve()
    if exec_path.exists():
        return exec_path
    # It is possible that some environments may only have python3
    exec_path = (bin_path / "python3").resolve()
    if exec_path.exists():
        return exec_path
    raise ValueError("python executable not found")


def _get_pip(venv_path):
    if sys.platform[:3] == "win":
        return venv_path / "Scripts" / "pip.exe"
    return venv_path / "bin" / "pip"


def _get_virtualenv_site_packages(venv_path, pyver):
    if sys.platform[:3] == "win":
        return venv_path / "Lib" / "site-packages"
    if (
        venv_path.exists()
        and not (venv_path / "lib" / f"python{pyver[0]}.{pyver[1]}").exists()
    ):
        # The virtualenv already exists but it doesn't seem to have the expected
        # Python version, so we disregard the requested `pyver` and
        # discover the real Python interpreter from this virtualenv
        import subprocess

        result = subprocess.run(
            [_get_python_binary(venv_path, pyver), "--version"],
            stdout=subprocess.PIPE,
            text=True,
        )
        venv_version = result.stdout.split(" ")[1].strip().split(".")
        return (
            venv_path
            / "lib"
            / f"python{venv_version[0]}.{venv_version[1]}"
            / "site-packages"
        )
    else:
        return venv_path / "lib" / f"python{pyver[0]}.{pyver[1]}" / "site-packages"


def _initialize_black_env(upgrade=False):
    if vim.eval("g:black_use_virtualenv ? 'true' : 'false'") == "false":
        if upgrade:
            print("Upgrade disabled due to g:black_use_virtualenv being disabled.")
            print(
                "Either use your system package manager (or pip) to upgrade black"
                " separately,"
            )
            print("or modify your vimrc to have 'let g:black_use_virtualenv = 1'.")
            return False
        else:
            # Nothing needed to be done.
            return True

    pyver = sys.version_info[:3]
    if pyver < (3, 10):
        print("Sorry, Black requires Python 3.10+ to run.")
        return False

    from pathlib import Path
    import subprocess
    import venv

    virtualenv_path = Path(vim.eval("g:black_virtualenv")).expanduser()
    virtualenv_site_packages = str(
        _get_virtualenv_site_packages(virtualenv_path, pyver)
    )
    first_install = False
    if not virtualenv_path.is_dir():
        print("Please wait, one time setup for Black.")
        _executable = sys.executable
        _base_executable = getattr(sys, "_base_executable", _executable)
        try:
            executable = str(_get_python_binary(Path(sys.exec_prefix), pyver))
            sys.executable = executable
            sys._base_executable = executable
            print(f"Creating a virtualenv in {virtualenv_path}...")
            print(
                "(this path can be customized in .vimrc by setting g:black_virtualenv)"
            )
            venv.create(virtualenv_path, with_pip=True)
        except Exception:
            print(
                "Encountered exception while creating virtualenv (see traceback below)."
            )
            print(f"Removing {virtualenv_path}...")
            import shutil

            shutil.rmtree(virtualenv_path)
            raise
        finally:
            sys.executable = _executable
            sys._base_executable = _base_executable
        first_install = True
    if first_install:
        print("Installing Black with pip...")
    if upgrade:
        print("Upgrading Black with pip...")
    if first_install or upgrade:
        subprocess.run(
            [str(_get_pip(virtualenv_path)), "install", "-U", "black"],
            stdout=subprocess.PIPE,
        )
        print("DONE! You are all set, thanks for waiting ✨ 🍰 ✨")
    if first_install:
        print(
            "Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and"
            " restart Vim.\n"
        )
    if virtualenv_site_packages not in sys.path:
        sys.path.insert(0, virtualenv_site_packages)
    return True


if _initialize_black_env():
    try:
        import black
    except ImportError:
        print(f"Could not import black from any of: {', '.join(sys.path)}.")
    import time


def get_target_version(tv):
    if isinstance(tv, black.TargetVersion):
        return tv
    ret = None
    try:
        ret = black.TargetVersion[tv.upper()]
    except KeyError:
        print(
            f"WARNING: Target version {tv!r} not recognized by Black, using default"
            " target"
        )
    return ret


def Black(**kwargs):
    """
    kwargs allows you to override ``target_versions`` argument of
    ``black.FileMode``.

    ``target_version`` needs to be cleaned because ``black.FileMode``
    expects the ``target_versions`` argument to be a set of TargetVersion enums.

    Allow kwargs["target_version"] to be a string to allow
    to type it more quickly.

    Using also target_version instead of target_versions to remain
    consistent to Black's documentation of the structure of pyproject.toml.
    """
    start = time.time()
    configs = get_configs()

    black_kwargs = {}
    if "target_version" in kwargs:
        target_version = kwargs["target_version"]

        if not isinstance(target_version, (list, set)):
            target_version = [target_version]
        target_version = set(
            filter(lambda x: x, map(lambda tv: get_target_version(tv), target_version))
        )
        black_kwargs["target_versions"] = target_version

    mode = black.FileMode(
        line_length=configs["line_length"],
        string_normalization=not configs["skip_string_normalization"],
        is_pyi=vim.current.buffer.name.endswith(".pyi"),
        magic_trailing_comma=not configs["skip_magic_trailing_comma"],
        preview=configs["preview"],
        **black_kwargs,
    )
    quiet = configs["quiet"]

    buffer_str = "\n".join(vim.current.buffer) + "\n"
    try:
        new_buffer_str = black.format_file_contents(
            buffer_str,
            fast=configs["fast"],
            mode=mode,
        )
    except black.NothingChanged:
        if not quiet:
            print(
                "Black: already well formatted, good job. (took"
                f" {time.time() - start:.4f}s)"
            )
    except Exception as exc:
        print(f"Black: {exc}")
    else:
        current_buffer = vim.current.window.buffer
        cursors = []
        for i, tabpage in enumerate(vim.tabpages):
            if tabpage.valid:
                for j, window in enumerate(tabpage.windows):
                    if window.valid and window.buffer == current_buffer:
                        cursors.append((i, j, window.cursor))
        vim.current.buffer[:] = new_buffer_str.split("\n")[:-1]
        for i, j, cursor in cursors:
            window = vim.tabpages[i].windows[j]
            try:
                window.cursor = cursor
            except vim.error:
                window.cursor = (len(window.buffer), 0)
        if not quiet:
            print(f"Black: reformatted in {time.time() - start:.4f}s.")


def get_configs():
    filename = vim.eval("@%")
    path_pyproject_toml = black.find_pyproject_toml((filename,))
    if path_pyproject_toml:
        toml_config = black.parse_pyproject_toml(path_pyproject_toml)
    else:
        toml_config = {}

    return {
        flag.var_name: toml_config.get(flag.name, flag.cast(vim.eval(flag.vim_rc_name)))
        for flag in FLAGS
    }


def BlackUpgrade():
    _initialize_black_env(upgrade=True)


def BlackVersion():
    print(f"Black, version {black.__version__} on Python {sys.version}.")
EndPython3

function black#Black(...)
    let kwargs = {}
    for arg in a:000
        let arg_list = split(arg, '=')
        let kwargs[arg_list[0]] = arg_list[1]
    endfor
python3 << EOF
import vim

kwargs = vim.eval("kwargs")
EOF
  :py3 Black(**kwargs)
endfunction

function black#BlackUpgrade()
  :py3 BlackUpgrade()
endfunction

function black#BlackVersion()
  :py3 BlackVersion()
endfunction
