#!/usr/bin/env python3

import argparse
import subprocess
import sys
import os
import shutil
from typing import List

ACCEPTABLE_COMBINATIONS = (
    ("gcov", "genhtml", "html"),
    ("clang", "llvm-cov", "html"),
    ("gcov", "gcovr", "summary"),
    ("gcov", "gcovr", "html"),
)


def parse_args():
    parser = argparse.ArgumentParser(
        description="Create a coverage report for Dungeon Crawl Stone Soup.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        epilog="Acceptable combinations for coverage/generator/format:\n{combos}.".format(
            combos=acceptable_combinations_description()
        ),
    )
    parser.add_argument(
        "-c",
        "--coverage",
        choices=("gcov", "clang"),
        default=default_coverage(),
        help="Coverage type. Use the same value you passed to the Makefile as COVERAGE. (auto-detected default)",
    )
    parser.add_argument(
        "-g",
        "--generator",
        choices=("genhtml", "llvm-cov", "gcovr"),
        default=default_generator(),
        help="Report generator (auto-detected default)",
    )
    parser.add_argument(
        "-f",
        "--format",
        choices=("html", "summary"),
        default="html",
        help="Report format",
    )
    parser.add_argument(
        "-b",
        "--binary",
        choices=("crawl", "catch2-tests-executable"),
        help="Binary to report on (auto-detected default)",
        default=default_binary(),
    )

    return parser.parse_args()


def acceptable_combinations_description():
    return ", ".join("/".join(combo) for combo in sorted(ACCEPTABLE_COMBINATIONS))


def sanity_check_args(args):
    combo = (args.coverage, args.generator, args.format)
    if combo not in ACCEPTABLE_COMBINATIONS:
        die("Unsupported combination of coverage, generator & format.")

    if not shutil.which(args.generator):
        die(
            "Report generator %s doesn't exist. Do you need to install it?"
            % args.generator
        )


def clean(name: str):
    if os.path.exists(name):
        print("Deleting existing %s" % name)
    else:
        return
    if os.path.isfile(name):
        os.unlink(name)
    elif os.path.isdir(name):
        subprocess.run(["rm", "-r", name])
    else:
        die("Not sure how to do that actually...")


def gcovr_common_args():
    return (
        "gcovr",
        "--exclude=catch2-tests/catch.hpp",
        "--exclude=conftest",
        "--exclude=levcomp",
        "--exclude=catch2-tests/catch.hpp",
        "--exclude=conftest",
        "--exclude=levcomp",
    )


def generate_report(generator, report_format, binary):
    if report_format == "html":
        clean("coverage-html-report")
        os.makedirs("coverage-html-report")

    if generator == "llvm-cov" and report_format == "html":
        run(
            [
                "llvm-cov",
                "show",
                binary,
                "-instr-profile=coverage.profdata",
                "-format=html",
                "-output-dir=coverage-html-report",
            ]
        )
    elif generator == "genhtml":
        run(
            [
                "genhtml",
                "coverage.info",
                "--output-directory",
                "coverage-html-report",
                "--ignore-errors",
                "source",
            ]
        )
    elif generator == "gcovr" and report_format == "summary":
        run([*gcovr_common_args(), "--print-summary"])
    elif generator == "gcovr" and report_format == "html":
        run(
            [
                *gcovr_common_args(),
                "--html=coverage-html-report/index.html",
                "--html-details",
            ]
        )
    else:
        die("Unhandled generator/format combo")

    if report_format == "html" and shutil.which("open"):
        run(["open", "coverage-html-report/index.html"])


def prepare_intermediate_profiling_data(coverage: str, generator: str):
    if coverage == "gcov" and generator != "gcovr":
        clean("coverage.info")
        run(
            [
                "lcov",
                "--capture",
                "--directory",
                ".",
                "--output-file",
                "coverage.info",
                "--ignore-errors",
                "source",
            ]
        )
    elif coverage == "gcov" and generator == "gcovr":
        pass
    elif coverage == "clang":
        clean("coverage.profdata")
        run(
            [
                "llvm-profdata",
                "merge",
                "-sparse",
                "default.profraw",
                "-o",
                "coverage.profdata",
            ]
        )
    else:
        die("Can't prepare profiling data of unknown type (%s)." % coverage)


def profiling_data_exists(coverage: str) -> bool:
    if coverage == "gcov":
        return os.path.isfile("english.gcda")  # file picked arbitrarily
    elif coverage == "clang":
        return os.path.isfile("default.profraw")
    else:
        die("Can't check for unknown type of profiling data (%s)." % coverage)


def run(cmd: List[str]) -> int:
    print("Running: %s" % " ".join(cmd))
    ret = subprocess.call(cmd)
    if ret != 0:
        die("Command failed, exiting %s" % sys.argv[0])
    return ret


def die(msg: str):
    print(msg, file=sys.stderr)
    sys.exit(1)


def default_coverage() -> str:
    # capture_output=True requires Python 3.7+ :(
    p = subprocess.run(["cc", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if b"clang" in p.stderr.split(b"\n")[0]:
        return "clang"
    return "gcov"


def default_generator() -> str:
    if default_coverage() == "gcov":
        if shutil.which("gcovr"):
            return "gcovr"
        return "genhtml"
    return "llvm-cov"


def default_binary() -> str:
    if os.path.isfile("catch2-tests-executable"):
        return "catch2-tests-executable"
    else:
        return "crawl"


def main():
    if sys.version_info < (3, 5):
        # hint for subprocess.run
        raise ImportError("This script requires Python 3.5+")

    args = parse_args()

    print(
        "Generating {format} report with {generator} for '{binary}' (with {coverage} coverage data)".format(
            format=args.format,
            generator=args.generator,
            coverage=args.coverage,
            binary=args.binary,
        )
    )

    sanity_check_args(args)

    if not profiling_data_exists(args.coverage):
        die(
            "There is no profiling data (hint: did you make {bin} with COVERAGE={cov} set and then run the binary?)".format(
                bin=args.binary, cov=args.coverage
            )
        )

    prepare_intermediate_profiling_data(args.coverage, args.generator)

    generate_report(args.generator, args.format, args.binary)


if __name__ == "__main__":
    main()
