import argparse
import os
import platform
import shutil
import subprocess
import sys
from typing import Tuple
from colorama import Fore, Back, Style

from util import *

sys.stdout.reconfigure(encoding='utf-8') # for windows gh runner

expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../test/expected/")

def get_c_compiler_counterpart(compiler: str) -> str:
    return compiler.replace("clang++", "clang").replace("g++", "gcc")

MAX_LINE_DIFF = 2

def similarity(name: str, target: List[str]) -> int:
    parts = name.split(".txt")[0].split(".")
    c = 0
    for part in parts:
        if part in target:
            c += 1
        else:
            return -1
    return c

def output_matches(raw_output: str, params: Tuple[str]):
    target = []

    if params[0].startswith("gcc") or params[0].startswith("g++"):
        target.append("gcc")
    elif params[0].startswith("clang"):
        target.append("clang")
    elif params[0].startswith("cl"):
        target.append("msvc")

    if platform.system() == "Windows":
        target.append("windows")
    elif platform.system() == "Darwin":
        target.append("macos")
    else:
        target.append("linux")

    other_configs = params[1:]
    for config in other_configs:
        assert "WITH_" in config
        target.append(config.split("WITH_")[1].lower())

    print(f"Searching for expected file best matching {target}")

    files = [f for f in os.listdir(expected_dir) if os.path.isfile(os.path.join(expected_dir, f))]
    if len(files) == 0:
        print(f"Error: No expected files to use (searching {expected_dir})")
        sys.exit(1)
    files = list(map(lambda f: (f, similarity(f, target)), files))
    m = max(files, key=lambda entry: entry[1])[1]
    if m <= 0:
        print(f"Error: Could not find match for {target} in {files}")
        sys.exit(1)
    files = [entry[0] for entry in files if entry[1] == m]
    if len(files) > 1:
        print(f"Error: Ambiguous expected file to use ({files})")
        sys.exit(1)

    file = files[0]
    print(f"Reading from {file}")

    with open(os.path.join(expected_dir, file), "r") as f:
        raw_expected = f.read()

    if raw_output.strip() == "":
        print(f"Error: No output from test")
        return False

    expected = [line.strip().split("||") for line in raw_expected.split("\n")]
    output = [line.strip().split("||") for line in raw_output.split("\n")]

    max_line_diff = 0

    errored = False

    try:
        for i, ((output_file, output_line, output_symbol), (expected_file, expected_line, expected_symbol)) in enumerate(zip(output, expected)):
            if output_file != expected_file:
                print(f"Error: File name mismatch on line {i + 1}, found \"{output_file}\" expected \"{expected_file}\"")
                errored = True
            if abs(int(output_line) - int(expected_line)) > max_line_diff:
                print(f"Error: File line mismatch on line {i + 1}, found {output_line} expected {expected_line}")
                errored = True
            if output_symbol != expected_symbol:
                print(f"Error: File symbol mismatch on line {i + 1}, found \"{output_symbol}\" expected \"{expected_symbol}\"")
                errored = True
            if expected_symbol == "main" or expected_symbol == "main()":
                break
    except ValueError:
        print("ValueError during output checking")
        errored = True

    if errored:
        print("Output:")
        print(raw_output)
        print("Expected:")
        print(raw_expected)

    return not errored

def run_test(runner: MatrixRunner, test_binary, params: Tuple[str]):
    def output_matcher(output: str):
        return output_matches(output, params)
    return runner.run_command(test_binary, output_matcher=output_matcher)

def build(runner: MatrixRunner):
    matrix = runner.current_config()
    if platform.system() != "Windows":
        args = [
            "cmake",
            "..",
            "-GNinja",
            f"-DCMAKE_BUILD_TYPE={matrix['target']}",
            f"-DCMAKE_CXX_COMPILER={matrix['compiler']}",
            f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}",
            f"-DCMAKE_CXX_STANDARD={matrix['std']}",
            "-DCPPTRACE_DISABLE_CXX_20_MODULES=ON",
            f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On",
            f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On",
            f"-DCPPTRACE_WERROR_BUILD=On",
            f"-D{matrix['unwind']}=On",
            f"-D{matrix['symbols']}=On",
            f"-D{matrix['demangle']}=On",
            "-DCPPTRACE_BACKTRACE_PATH=/usr/lib/gcc/x86_64-linux-gnu/10/include/backtrace.h",
            "-DCPPTRACE_BUILD_TESTING=On",
            "-DCPPTRACE_SKIP_UNIT=On",
            f"-DBUILD_SHARED_LIBS={matrix['shared']}"
        ]
        if matrix['symbols'] == "CPPTRACE_GET_SYMBOLS_WITH_LIBDL":
           args.append("-DCPPTRACE_BUILD_TEST_RDYNAMIC=On")
        succeeded = runner.run_command(*args)
        if succeeded:
            return runner.run_command("ninja")
    else:
        args = [
            "cmake",
            "..",
            f"-DCMAKE_BUILD_TYPE={matrix['target']}",
            f"-DCMAKE_CXX_COMPILER={matrix['compiler']}",
            f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}",
            f"-DCMAKE_CXX_STANDARD={matrix['std']}",
            "-DCPPTRACE_DISABLE_CXX_20_MODULES=ON",
            f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On",
            f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On",
            f"-DCPPTRACE_WERROR_BUILD=On",
            f"-D{matrix['unwind']}=On",
            f"-D{matrix['symbols']}=On",
            f"-D{matrix['demangle']}=On",
            "-DCPPTRACE_BUILD_TESTING=On",
            "-DCPPTRACE_SKIP_UNIT=On",
            f"-DBUILD_SHARED_LIBS={matrix['shared']}"
        ]
        if matrix["compiler"] == "g++":
            args.append("-GNinja")
        succeeded = runner.run_command(*args)
        if succeeded:
            if matrix["compiler"] == "g++":
                return runner.run_command("ninja")
            else:
                return runner.run_command("msbuild", "cpptrace.sln")
    return False

def build_full_or_auto(runner: MatrixRunner):
    matrix = runner.current_config()
    if platform.system() != "Windows":
        args = [
            "cmake",
            "..",
            "-GNinja",
            f"-DCMAKE_BUILD_TYPE={matrix['target']}",
            f"-DCMAKE_CXX_COMPILER={matrix['compiler']}",
            f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}",
            f"-DCMAKE_CXX_STANDARD={matrix['std']}",
            "-DCPPTRACE_DISABLE_CXX_20_MODULES=ON",
            f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On",
            f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On",
            f"-DCPPTRACE_WERROR_BUILD=On",
            f"-DCPPTRACE_BACKTRACE_PATH=/usr/lib/gcc/x86_64-linux-gnu/10/include/backtrace.h",
            "-DCPPTRACE_BUILD_TESTING=On",
            "-DCPPTRACE_SKIP_UNIT=On",
            f"-DBUILD_SHARED_LIBS={matrix['shared']}"
        ]
        if matrix["config"] != "":
            args.append(f"{matrix['config']}")
        succeeded = runner.run_command(*args)
        if succeeded:
            return runner.run_command("ninja")
    else:
        args = [
            "cmake",
            "..",
            f"-DCMAKE_BUILD_TYPE={matrix['target']}",
            f"-DCMAKE_CXX_COMPILER={matrix['compiler']}",
            f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}",
            f"-DCMAKE_CXX_STANDARD={matrix['std']}",
            "-DCPPTRACE_DISABLE_CXX_20_MODULES=ON",
            f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On",
            f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On",
            f"-DCPPTRACE_WERROR_BUILD=On",
            "-DCPPTRACE_BUILD_TESTING=On",
            "-DCPPTRACE_SKIP_UNIT=On",
            f"-DBUILD_SHARED_LIBS={matrix['shared']}"
        ]
        if matrix["config"] != "":
            args.append(f"{matrix['config']}")
        if matrix["compiler"] == "g++":
            args.append("-GNinja")
        succeeded = runner.run_command(*args)
        if succeeded:
            if matrix["compiler"] == "g++":
                return runner.run_command("ninja")
            else:
                return runner.run_command("msbuild", "cpptrace.sln")
    return False

def test(runner: MatrixRunner):
    matrix = runner.current_config()
    if platform.system() != "Windows":
        return run_test(
            runner,
            "./integration",
            (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"])
        )
    else:
        if matrix["compiler"] == "g++":
            return run_test(
                runner,
                f".\\integration.exe",
                (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"])
            )
        else:
            return run_test(
                runner,
                f".\\{matrix['target']}\\integration.exe",
                (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"])
            )

def test_full_or_auto(runner: MatrixRunner):
    matrix = runner.current_config()
    if platform.system() != "Windows":
        return run_test(
            runner,
            "./integration",
            (matrix["compiler"],)
        )
    else:
        if matrix["compiler"] == "g++":
            return run_test(
                runner,
                f".\\integration.exe",
                (matrix["compiler"],)
            )
        else:
            return run_test(
                runner,
                f".\\{matrix['target']}\\integration.exe",
                (matrix["compiler"],)
            )

def build_and_test(runner: MatrixRunner):
    matrix = runner.current_config()

    if os.path.exists("build"):
        shutil.rmtree("build", ignore_errors=True)

    if not os.path.exists("build"):
        os.mkdir("build")
    os.chdir("build")

    good = False
    if build(runner):
        good = test(runner)

    os.chdir("..")
    print()

    return good

def build_and_test_full_or_auto(runner: MatrixRunner):
    matrix = runner.current_config()

    if os.path.exists("build"):
        shutil.rmtree("build", ignore_errors=True)

    if not os.path.exists("build"):
        os.mkdir("build")
    os.chdir("build")

    good = False
    if build_full_or_auto(runner):
        good = test_full_or_auto(runner)

    os.chdir("..")
    print()

    return good

def run_linux_matrix(compilers: list, shared: bool):
    MatrixRunner(
        matrix = {
            "compiler": compilers,
            "target": ["Debug"],
            "std": ["11", "20"],
            "unwind": [
                "CPPTRACE_UNWIND_WITH_EXECINFO",
                "CPPTRACE_UNWIND_WITH_UNWIND",
                "CPPTRACE_UNWIND_WITH_LIBUNWIND",
                #"CPPTRACE_UNWIND_WITH_NOTHING",
            ],
            "symbols": [
                # Disabled due to libbacktrace bug
                # "CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE",
                "CPPTRACE_GET_SYMBOLS_WITH_LIBDL",
                "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE",
                "CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
                #"CPPTRACE_GET_SYMBOLS_WITH_NOTHING",
            ],
            "demangle": [
                "CPPTRACE_DEMANGLE_WITH_CXXABI",
                #"CPPTRACE_DEMANGLE_WITH_NOTHING",
            ],
            "shared": ["On" if shared else "Off"]
        },
        exclude = []
    ).run(build_and_test)

def run_linux_default(compilers: list, shared: bool):
    MatrixRunner(
        matrix = {
            "compiler": compilers,
            "target": ["Debug"],
            "std": ["11", "20"],
            "config": [""],
            "shared": ["On" if shared else "Off"]
        },
        exclude = []
    ).run(build_and_test_full_or_auto)

def run_macos_matrix(compilers: list, shared: bool):
    MatrixRunner(
        matrix = {
            "compiler": compilers,
            "target": ["Debug"],
            "std": ["11", "20"],
            "unwind": [
                "CPPTRACE_UNWIND_WITH_EXECINFO",
                "CPPTRACE_UNWIND_WITH_UNWIND",
                #"CPPTRACE_UNWIND_WITH_NOTHING",
            ],
            "symbols": [
                #"CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE",
                "CPPTRACE_GET_SYMBOLS_WITH_LIBDL",
                "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE",
                "CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
                #"CPPTRACE_GET_SYMBOLS_WITH_NOTHING",
            ],
            "demangle": [
                "CPPTRACE_DEMANGLE_WITH_CXXABI",
                #"CPPTRACE_DEMANGLE_WITH_NOTHING",
            ],
            "shared": ["On" if shared else "Off"]
        },
        exclude = []
    ).run(build_and_test)

def run_macos_default(compilers: list, shared: bool):
    MatrixRunner(
        matrix = {
            "compiler": compilers,
            "target": ["Debug"],
            "std": ["11", "20"],
            "config": [""],
            "shared": ["On" if shared else "Off"]
        },
        exclude = []
    ).run(build_and_test_full_or_auto)

def run_windows_matrix(compilers: list, shared: bool):
    MatrixRunner(
        matrix = {
            "compiler": compilers,
            "target": ["Debug"],
            "std": ["11", "20"],
            "unwind": [
                "CPPTRACE_UNWIND_WITH_WINAPI",
                "CPPTRACE_UNWIND_WITH_DBGHELP",
                "CPPTRACE_UNWIND_WITH_UNWIND", # Broken on github actions for some reason
                #"CPPTRACE_UNWIND_WITH_NOTHING",
            ],
            "symbols": [
                "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP",
                "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE",
                "CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
                #"CPPTRACE_GET_SYMBOLS_WITH_NOTHING",
            ],
            "demangle": [
                "CPPTRACE_DEMANGLE_WITH_CXXABI",
                "CPPTRACE_DEMANGLE_WITH_NOTHING",
            ],
            "shared": ["On" if shared else "Off"]
        },
        exclude = [
            {
                "demangle": "CPPTRACE_DEMANGLE_WITH_CXXABI",
                "compiler": "cl"
            },
            {
                "unwind": "CPPTRACE_UNWIND_WITH_UNWIND",
                "compiler": "cl"
            },
            {
                "unwind": "CPPTRACE_UNWIND_WITH_UNWIND",
                "compiler": "clang++"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE",
                "compiler": "cl"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE",
                "compiler": "clang++"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
                "compiler": "cl"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
                "compiler": "clang++"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP",
                "compiler": "g++"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP",
                "demangle": "CPPTRACE_DEMANGLE_WITH_CXXABI"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
                "demangle": "CPPTRACE_DEMANGLE_WITH_NOTHING"
            },
            {
                "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE",
                "demangle": "CPPTRACE_DEMANGLE_WITH_NOTHING"
            }
        ]
    ).run(build_and_test)

def run_windows_default(compilers: list, shared: bool):
    MatrixRunner(
        matrix = {
            "compiler": compilers,
            "target": ["Debug"],
            "std": ["11", "20"],
            "config": [""],
            "shared": ["On" if shared else "Off"]
        },
        exclude = []
    ).run(build_and_test_full_or_auto)

def main():
    parser = argparse.ArgumentParser(
        prog="Build in all configs",
        description="Try building the library in all possible configurations for the current host"
    )
    parser.add_argument(
        "--clang",
        action="store_true"
    )
    parser.add_argument(
        "--gcc",
        action="store_true"
    )
    parser.add_argument(
        "--msvc",
        action="store_true"
    )
    parser.add_argument(
        "--all",
        action="store_true"
    )
    parser.add_argument(
        "--shared",
        action="store_true"
    )
    parser.add_argument(
        "--default-config",
        action="store_true"
    )
    args = parser.parse_args()

    if platform.system() == "Linux":
        compilers = []
        if args.clang or args.all:
            compilers.append("clang++-14")
        if args.gcc or args.all:
            compilers.append("g++-10")
        if args.default_config:
            run_linux_default(compilers, args.shared)
        else:
            run_linux_matrix(compilers, args.shared)
    if platform.system() == "Darwin":
        compilers = []
        if args.clang or args.all:
            compilers.append("clang++")
        if args.gcc or args.all:
            compilers.append("g++-12")
        if args.default_config:
            run_macos_default(compilers, args.shared)
        else:
            run_macos_matrix(compilers, args.shared)
    if platform.system() == "Windows":
        compilers = []
        if args.clang or args.all:
            compilers.append("clang++")
        if args.msvc or args.all:
            compilers.append("cl")
        if args.gcc or args.all:
            compilers.append("g++")
        if args.default_config:
            run_windows_default(compilers, args.shared)
        else:
            run_windows_matrix(compilers, args.shared)

main()
