# 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.

import argparse
import logging
import os
from pathlib import Path
import re
import subprocess
import shlex
import shutil
import sys

from ..common import (
    find_packages,
    compute_python_package_path,
    sort_packages,
    get_build_var,
)

destdirs = set()


def prepare_argparse():
    p = argparse.ArgumentParser()
    p.add_argument(
        "--sourcedir",
        default=Path("."),
        type=Path,
        help="root directory of the source package",
    )
    g = p.add_mutually_exclusive_group()
    g.add_argument(
        "--search-depth",
        default=2,
        type=int,
        help="limit maximum depth when searching for packages recursively",
    )
    g.add_argument(
        "--unlimited-search-depth",
        action="store_const",
        dest="search_depth",
        const=-1,
        help="do not limit maximum depth when searching for packages",
    )
    p.add_argument(
        "--builddir", default=Path(".rosbuild"), type=Path, help="build directory"
    )
    p.add_argument(
        "--destdir",
        default=Path("debian/tmp"),
        type=Path,
        help="installation directory",
    )
    p.add_argument("--verbose", action="store_true", help="make verbose output")
    g = p.add_mutually_exclusive_group()
    g.add_argument(
        "--detect", action="store_true", help="detect if ROS packages are to be built"
    )
    g.add_argument(
        "--build-types", action="store_true", help="list detected build types"
    )
    g.add_argument(
        "--build-order", action="store_true", help="list packages in build order"
    )
    return p


def run():
    # initialize script
    logging.basicConfig(
        format="%(levelname).1s: dh_ros %(module)s:%(lineno)d: %(message)s"
    )
    log = logging.getLogger("dhros")
    p = prepare_argparse()
    args = p.parse_args()

    if args.verbose or os.environ.get("DH_VERBOSE") == "1":
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)

    packages = find_packages(args.sourcedir, depth=args.search_depth)

    if args.detect:
        return 0 if packages else 1

    if args.build_types:
        sys.stdout.write(
            "\n".join(sorted(set(p.get_build_type() for _, p in packages)))
        )
        return 0

    buildopts = re.split(r"[ ,\t\r\n]+", os.environ.get("DEB_BUILD_OPTIONS", ""))

    packages = sort_packages(packages, test_depends="nocheck" not in buildopts)
    skipped_packages = get_build_var(None, "ROS_SKIP_PACKAGES")
    skipped_packages = set(shlex.split(skipped_packages) if skipped_packages else [])

    if args.build_order:
        sys.stdout.write("\n".join(p.name for _, p in packages))
        return 0

    sys.stdout.write(
        "\n"
        "#############################################################################\n"
        "## Detected ROS packages (in build order):\n"
        "##\n"
    )
    for path, package in packages:
        sys.stdout.write(f"## - {package.name} [{package.get_build_type()}]\n")
    sys.stdout.write(
        "#############################################################################\n"
        "\n"
    )

    try:
        global destdirs
        destdirs = set()
        for path, package in packages:
            sys.stdout.write(
                "\n"
                "#############################################################################\n"
                f"## ROS Package {package.name:<59} ##\n"
                "#############################################################################\n"
            )

            if package.name in skipped_packages:
                sys.stdout.write("Skipping package\n")
                continue

            sys.stdout.write(
                "\n"
                "-----------------------------------------------------------------------------\n"
                f" Configuring {package.name}\n"
                "-----------------------------------------------------------------------------\n"
            )
            do_action("configure", args, path, package, buildopts)
            sys.stdout.write(
                "\n"
                "-----------------------------------------------------------------------------\n"
                f" Building {package.name}\n"
                "-----------------------------------------------------------------------------\n"
            )
            do_action("build", args, path, package, buildopts)
            sys.stdout.write(
                "\n"
                "-----------------------------------------------------------------------------\n"
                f" Testing {package.name}\n"
                "-----------------------------------------------------------------------------\n"
            )
            if "nocheck" not in buildopts:
                ignore_test_results = get_build_var(package.name, "ROS_IGNORE_TEST_RESULTS")
                try:
                    do_action("test", args, path, package, buildopts)
                except subprocess.CalledProcessError as e:
                    if ignore_test_results == "1":
                        sys.stdout.write("Ignoring test results as instructed\n")
                    else:
                        raise
            else:
                sys.stdout.write("Tests are skipped\n")
            sys.stdout.write(
                "\n"
                "-----------------------------------------------------------------------------\n"
                f" Installing {package.name}\n"
                "-----------------------------------------------------------------------------\n"
            )
            do_action("install", args, path, package, buildopts)
        sys.stdout.write(
            "\n"
            "#############################################################################\n"
            "## All ROS packages have been built successfully                           ##\n"
            "#############################################################################\n"
        )
    except KeyError as e:
        sys.stderr.write(f"ERROR: Invalid substitution {{{str(e)}}} in command hook\n")
        return 1
    except subprocess.CalledProcessError as e:
        return e.returncode or 1
    return 0


def do_action(action, args, path, package, buildopts):
    global destdirs
    log = logging.getLogger("dhros")
    destdir = Path(get_build_var(package.name, "ROS_DESTDIR", args.destdir)).resolve()
    log.debug(f"ROS_DESTDIR={str(destdir)!r}")
    destdirs.add(destdir)
    package_suffix = re.sub(r"[^A-Za-z0-9]", "_", package.name)
    build_type = package.get_build_type()
    hook_vars = {
        "dir": shlex.quote(str(path.resolve())),
        "builddir": shlex.quote(str((args.builddir / package.name).resolve())),
        "destdir": shlex.quote(str(destdir)),
        "package": shlex.quote(package.name),
        "package_suffix": package_suffix,
        "version": shlex.quote(package.version),
        "build_type": shlex.quote(build_type),
    }
    env = os.environ.copy()
    pybuild_per_package_keys = [
        key
        for key in env
        if key.startswith("PYBUILD_") and key.endswith(f"_{package_suffix}")
    ]
    for key in pybuild_per_package_keys:
        env[key[: -len(package_suffix) - 1]] = env[key]
        del env[key]

    if action == "configure":
        prefixes = env.get("CMAKE_PREFIX_PATH", None)
        prefixes = prefixes.split(":") if prefixes is not None else []
        env["DEB_PYTHON_INSTALL_LAYOUT"] = "deb_system"
        env["CMAKE_PREFIX_PATH"] = ":".join(
            [str(d / "usr") for d in destdirs] + prefixes
        )
        log.debug(f"CMAKE_PREFIX_PATH={env['CMAKE_PREFIX_PATH']!r}")
        if build_type == "ament_python":
            shutil.rmtree(args.sourcedir / ".pybuild", ignore_errors=True)

    env["PYTHONPATH"] = ":".join(
        compute_python_package_path(d / "usr") for d in destdirs
    )
    log.debug(f"PYTHONPATH={env['PYTHONPATH']!r}")

    execute_hook(f"before_{action}", package, hook_vars, env)

    if not execute_hook(f"custom_{action}", package, hook_vars, env):
        cmdline = [f"dh_auto_{action}", f"--sourcedir={path}"]
        if build_type in ["ament_python"]:
            cmdline += ["--buildsystem=pybuild"]
        elif build_type in ["ament_cmake", "catkin", "cmake"]:
            cmdline += [
                f"--builddir={args.builddir / package.name}",
                "--buildsystem=cmake",
            ]
        if action == "install":
            cmdline += [f"--destdir={str(destdir)}"]

        cmdline += ["--"]

        if action == "configure":
            if build_type in ["ament_cmake", "catkin", "cmake"]:
                build_testing = "OFF" if "nocheck" in buildopts else "ON"
                cmdline += ["--no-warn-unused-cli", f"-DBUILD_TESTING={build_testing}"]
            if build_type == "catkin":
                cmdline += ["-DCATKIN_BUILD_BINARY_PACKAGE=ON"]

        extra_args = get_build_var(
            package.name,
            f"ROS_{action.upper()}_{build_type.upper()}_ARGS",
            get_build_var(package.name, f"ROS_{action.upper()}_ARGS"),
        )
        if extra_args:
            cmdline += shlex.split(extra_args)

        sys.stdout.write(shlex.join(cmdline))
        sys.stdout.write("\n")
        sys.stdout.flush()
        subprocess.check_call(cmdline, env=env)

    execute_hook(f"after_{action}", package, hook_vars, env)


def execute_hook(hook, package, vars, env):
    cmdline = get_build_var(package.name, f"ROS_EXECUTE_{hook.upper()}")
    if cmdline is None:
        return False

    cmdline = cmdline.format(vars)

    sys.stdout.write(cmdline)
    sys.stdout.write("\n")
    sys.stdout.flush()
    subprocess.check_call(["/bin/sh", "-c", cmdline], env=env)
    return True
