"""Generate executable scripts, on various platforms."""

import io
import shlex
import zipfile
from importlib.resources import read_binary
from typing import TYPE_CHECKING, Mapping, Optional, Tuple

from installer import _scripts

if TYPE_CHECKING:
    from typing import Literal

    LauncherKind = Literal["posix", "win-ia32", "win-amd64", "win-arm", "win-arm64"]
    ScriptSection = Literal["console", "gui"]


__all__ = ["InvalidScript", "Script"]


_ALLOWED_LAUNCHERS: Mapping[Tuple["ScriptSection", "LauncherKind"], str] = {
    ("console", "win-ia32"): "t32.exe",
    ("console", "win-amd64"): "t64.exe",
    ("console", "win-arm"): "t_arm.exe",
    ("console", "win-arm64"): "t64-arm.exe",
    ("gui", "win-ia32"): "w32.exe",
    ("gui", "win-amd64"): "w64.exe",
    ("gui", "win-arm"): "w_arm.exe",
    ("gui", "win-arm64"): "w64-arm.exe",
}

_SCRIPT_TEMPLATE = """\
# -*- coding: utf-8 -*-
import re
import sys
from {module} import {import_name}
if __name__ == "__main__":
    sys.argv[0] = re.sub(r"(-script\\.pyw|\\.exe)?$", "", sys.argv[0])
    sys.exit({func_path}())
"""


def _is_executable_simple(executable: bytes) -> bool:
    if b" " in executable:
        return False
    shebang_length = len(executable) + 3  # Prefix #! and newline after.
    # According to distlib, Darwin can handle up to 512 characters. But I want
    # to avoid platform sniffing to make this as platform agnostic as possible.
    # The "complex" script isn't that bad anyway.
    return shebang_length <= 127


def _build_shebang(executable: str, forlauncher: bool) -> bytes:
    """Build a shebang line.

    The non-launcher cases are taken directly from distlib's implementation,
    which tries its best to account for command length, spaces in path, etc.

    https://bitbucket.org/pypa/distlib/src/58cd5c6/distlib/scripts.py#lines-124
    """
    executable_bytes = executable.encode("utf-8")
    return b"#!" + executable_bytes


class InvalidScript(ValueError):
    """Raised if the user provides incorrect script section or kind."""


class Script:
    """Describes a script based on an entry point declaration."""

    __slots__ = ("name", "module", "attr", "section")

    def __init__(
        self, name: str, module: str, attr: str, section: "ScriptSection"
    ) -> None:
        """Construct a Script object.

        :param name: name of the script
        :param module: module path, to load the entry point from
        :param attr: final attribute access, for the entry point
        :param section: Denotes the "entry point section" where this was specified.
            Valid values are ``"gui"`` and ``"console"``.
        :type section: str

        """
        self.name = name
        self.module = module
        self.attr = attr
        self.section = section

    def __repr__(self) -> str:
        return "Script(name={!r}, module={!r}, attr={!r}".format(
            self.name,
            self.module,
            self.attr,
        )

    def _get_launcher_data(self, kind: "LauncherKind") -> Optional[bytes]:
        if kind == "posix":
            return None
        key = (self.section, kind)
        try:
            name = _ALLOWED_LAUNCHERS[key]
        except KeyError:
            error = f"{key!r} not in {sorted(_ALLOWED_LAUNCHERS)!r}"
            raise InvalidScript(error)
        return read_binary(_scripts, name)

    def generate(self, executable: str, kind: "LauncherKind") -> Tuple[str, bytes]:
        """Generate a launcher for this script.

        :param executable: Path to the executable to invoke.
        :param kind: Which launcher template should be used.
            Valid values are ``"posix"``, ``"win-ia32"``, ``"win-amd64"`` and
            ``"win-arm"``.
        :type kind: str

        :raises InvalidScript: if no appropriate template is available.
        :return: The name and contents of the launcher file.
        """
        launcher = self._get_launcher_data(kind)
        shebang = _build_shebang(executable, forlauncher=bool(launcher))
        code = _SCRIPT_TEMPLATE.format(
            module=self.module,
            import_name=self.attr.split(".")[0],
            func_path=self.attr,
        ).encode("utf-8")

        if launcher is None:
            return (self.name, shebang + b"\n" + code)

        stream = io.BytesIO()
        with zipfile.ZipFile(stream, "w") as zf:
            zf.writestr("__main__.py", code)
        name = f"{self.name}.exe"
        data = launcher + shebang + b"\n" + stream.getvalue()
        return (name, data)
