# Copyright 2021-2022 Timo Röhling <roehling@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
from configparser import ConfigParser
from os import environ
from pathlib import Path
from re import sub
from typing import List, Dict, Tuple, Optional
from catkin_pkg.package import Package, parse_package


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


def find_packages(srcdir: Path, depth: int) -> List[Tuple[Path, Package]]:
    try:
        package = parse_package(srcdir)
        package.evaluate_conditions(environ)
        return [(srcdir, package)]
    except Exception:
        pass
    if depth == 0:
        return []
    packages = []
    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 = {}
    loop = set()

    def visit(node, depth):
        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=True
) -> List[Tuple[Path, Package]]:
    lookup = {}
    graph = {}
    for path, package in packages:
        lookup[package.name] = (path, package)
        depends = 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 [])
        )
        graph[package.name] = list(depends)
    sorted_names = topological_sort(graph)
    return [lookup[name] for name in sorted_names]


# Proudly stolen from dh-python
def cpython_versions(major: int):
    result = [None, 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:
            result[0] = tuple(int(i) for i in default.split("."))
        except Exception as err:
            pass
    if supported:
        try:
            result[1] = tuple(
                tuple(int(j) for j in i.strip().split("."))
                for i in supported.split(",")
            )
        except Exception as err:
            pass
    return result


DEFAULT_PYTHON_VERSION = None
SUPPORTED_PYTHON_VERSIONS = None


def compute_python_package_path(prefix: Path) -> str:
    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)

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

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

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

    return ":".join(result)
