# Copyright 2021-2022 Timo Röhling <roehling@debian.org>
# SPDX-License-Identifier: MIT
from configparser import ConfigParser
from os import environ
from pathlib import Path
from re import sub
from typing import cast, overload, List, Dict, Set, Tuple, Optional
from catkin_pkg.package import Package, parse_package  # type: ignore[reportUnknownVariableType]
import subprocess


@overload
def get_build_var(
    package: Optional[str],
    name: str,
    default: str,
    env: Optional[Dict[str, str]] = None,
) -> str: ...


@overload
def get_build_var(
    package: Optional[str],
    name: str,
    default: None = None,
    env: Optional[Dict[str, str]] = None,
) -> Optional[str]: ...


def get_build_var(
    package: Optional[str],
    name: str,
    default: Optional[str] = None,
    env: Optional[Dict[str, str]] = None,
) -> Optional[str]:
    if env is None:
        env = cast(Dict[str, str], environ)
    if package is not None:
        sanitized_package = sub(r"[^A-Za-z0-9]", "_", package)
        if f"{name}_{sanitized_package}" in env:
            return env.get(f"{name}_{sanitized_package}")
    return env.get(name, default)


def find_packages(srcdir: Path, depth: int) -> List[Tuple[Path, Package]]:
    try:
        package = cast(Package, parse_package(srcdir))
        package.evaluate_conditions(environ)  # type: ignore[reportUnknownMemberType]
        return [(srcdir, package)]
    except Exception:
        pass
    if depth == 0:
        return []
    packages: List[Tuple[Path, Package]] = []
    for p in srcdir.iterdir():
        if p.is_dir():
            packages += find_packages(p, max(-1, depth - 1))
    return packages


def topological_sort(graph: Dict[str, List[str]]) -> List[str]:
    order: Dict[str, int] = {}
    loop: Set[str] = set()

    def visit(node: str, depth: int) -> None:
        if node in loop:
            raise RuntimeError(f"Dependency loop involving {node!r} detected!")
        if order.get(node, -1) < depth:
            order[node] = depth
            for dep in graph[node]:
                if dep in graph:
                    loop.add(node)
                    visit(dep, depth + 1)
                    loop.remove(node)

    for node in graph.keys():
        visit(node, 0)

    flattened = [(-depth, node) for node, depth in order.items()]
    flattened.sort()
    return [node for _, node in flattened]


def sort_packages(
    packages: List[Tuple[Path, Package]], test_depends: bool = True
) -> List[Tuple[Path, Package]]:
    lookup = {}
    graph: Dict[str, List[str]] = {}
    for path, package in packages:
        lookup[package.name] = (path, package)
        depends: Set[str] = set(
            d.name
            for d in package.build_export_depends
            + package.buildtool_export_depends
            + package.build_depends
            + package.buildtool_depends
            + (package.exec_depends if test_depends else [])
            + (package.test_depends if test_depends else [])
        )
        tweaks = get_build_var(package.name, "ROS_DEPENDS", "").split()
        if test_depends:
            tweaks += get_build_var(package.name, "ROS_TEST_DEPENDS", "").split()
        tweaks = [t for t in tweaks if t]
        for tweak in tweaks:
            if tweak[0] == "+":
                depends.add(tweak[1:])
            elif tweak[0] == "-":
                depends.discard(tweak[1:])
        graph[package.name] = list(depends)
    sorted_names = topological_sort(graph)
    return [lookup[name] for name in sorted_names]


PyVerTuple = Tuple[int, ...]
PyVerTuples = List[Tuple[int, ...]]


# Proudly stolen from dh-python
def cpython_versions(major: int) -> Tuple[Optional[PyVerTuple], Optional[PyVerTuples]]:
    default_tuple: Optional[PyVerTuple] = None
    supported_tuples: Optional[PyVerTuples] = None
    ver = "" if major == 2 else "3"
    supported = environ.get("DEBPYTHON{}_SUPPORTED".format(ver))
    default = environ.get("DEBPYTHON{}_DEFAULT".format(ver))
    if not supported or not default:
        config = ConfigParser()
        config.read("/usr/share/python{}/debian_defaults".format(ver))
        if not default:
            default = config.get("DEFAULT", "default-version", fallback="")[6:]
        if not supported:
            supported = config.get(
                "DEFAULT", "supported-versions", fallback=""
            ).replace("python", "")
    if default:
        try:
            default_tuple = tuple(int(i) for i in default.split("."))
        except Exception:
            pass
    if supported:
        try:
            supported_tuples = [
                tuple(int(j) for j in i.strip().split("."))
                for i in supported.split(",")
            ]
        except Exception:
            pass
    return default_tuple, supported_tuples


_default_python_version = None
_supported_python_versions = None


def compute_ament_cmake_python_path(builddir: Path) -> List[str]:
    ament_cmake_python_path = (builddir / "ament_cmake_python").resolve()
    if ament_cmake_python_path.is_dir():
        return [str(d) for d in ament_cmake_python_path.iterdir() if d.is_dir()]
    return []


def get_python_versions():
    global _supported_python_versions, _default_python_version

    if _default_python_version is None or _supported_python_versions is None:
        try:
            major = int(environ.get("ROS_PYTHON_VERSION", "3"))
        except ValueError:
            major = 3
        _default_python_version, _supported_python_versions = cpython_versions(major)
    return _default_python_version, _supported_python_versions


def is_default_python(version: PyVerTuple) -> bool:
    default_python, _ = get_python_versions()
    return version == default_python


def compute_python_package_path(prefix: Path) -> List[str]:
    default_python, supported_python = get_python_versions()

    def append_sitedir(path: Path):
        if path.is_dir():
            result.append(str(path))

    result: List[str] = []
    if default_python is not None:
        if default_python[0] == 3:
            append_sitedir(prefix / "lib" / "python3" / "dist-packages")

    if supported_python is not None:
        for version in supported_python:
            append_sitedir(
                prefix / "lib" / "python{}.{}".format(*version[:2]) / "dist-packages"
            )
            append_sitedir(
                prefix / "lib" / "python{}.{}".format(*version[:2]) / "site-packages"
            )

    return result


def query_dpkg_architecture() -> Dict[str, str]:
    query = subprocess.run(["dpkg-architecture"], capture_output=True, text=True)
    return dict(L.strip().split("=", 1) for L in query.stdout.split("\n") if "=" in L)
