# Generates "version_constants.h" config header file containing feature flags generated by checking for the availability of certain compiler features.
# This script is invoked by the Bazel build system to generate the "version_constants.h" file automatically as part of the build.
import json
import os
import platform
import re
import subprocess
import threading
from typing import Dict


# This function gets the running OS as identified by Python
# It should only be used to set up defaults for options/variables, because
# its value could potentially be overridden by setting TARGET_OS on the
# command-line. Treat this output as the value of HOST_OS
def get_running_os_name():
    running_os = os.sys.platform
    if running_os.startswith("linux"):
        running_os = "linux"
    elif running_os.startswith("freebsd"):
        running_os = "freebsd"
    elif running_os.startswith("openbsd"):
        running_os = "openbsd"
    elif running_os == "sunos5":
        running_os = "solaris"
    elif running_os == "win32":
        running_os = "windows"
    elif running_os == "darwin":
        running_os = "macOS"
    else:
        running_os = "unknown"
    return running_os


def tool_to_path(tool, compiler_path):
    if tool == "CC":
        return compiler_path
    elif tool == "CXX":
        if os.path.basename(compiler_path) == "gcc":
            return os.path.join(os.path.dirname(compiler_path), "g++")
        elif os.path.basename(compiler_path) == "clang":
            return os.path.join(os.path.dirname(compiler_path), "clang++")
        else:
            return


def get_toolchain_ver(tool, compiler_path):
    # By default we don't know the version of each tool, and only report what
    # command gets executed (gcc vs /opt/mongodbtoolchain/bin/gcc).
    verstr = "version unknown"
    proc = None
    tool = tool_to_path(tool, compiler_path)
    if compiler_path.endswith("gcc") or compiler_path.endswith("clang"):
        proc = subprocess.run(
            f"{tool} --version",
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            stdin=subprocess.DEVNULL,
            universal_newlines=True,
            check=True,
            shell=True,
            text=True,
        )
        verstr = proc.stdout
    elif compiler_path.endswith("cl"):
        proc = subprocess.run(
            tool,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            stdin=subprocess.DEVNULL,
            universal_newlines=True,
            check=True,
            shell=True,
            text=True,
        )
        verstr = proc.stderr

    return f"{tool}: {verstr}"


# This is the key/value mapping that will be returned by the buildInfo command and
# printed by the --version command-line option to mongod.
# Each mapped value is in turn a dict consisting of:
#   key: <string>
#   value: <string>
#   inBuildInfo: <bool> : should it be included in buildInfo output
#   inVersion: <bool> : should it be included in --version output
# The `value` field will be passed through env.subst, so you can use any SCons variables you
# want to define them.
def default_buildinfo_environment_data(compiler_path, extra_definitions):
    data = (
        (
            "distmod",
            extra_definitions["MONGO_DISTMOD"],
            True,
            True,
        ),
        (
            "distarch",
            platform.machine().lower(),
            True,
            True,
        ),
        (
            "cc",
            get_toolchain_ver("CC", compiler_path),
            True,
            False,
        ),
        (
            "ccflags",
            extra_definitions["compile_variables"],
            True,
            False,
        ),
        (
            "cxx",
            get_toolchain_ver("CXX", compiler_path),
            True,
            False,
        ),
        (
            "cxxflags",
            extra_definitions["compile_variables"],
            True,
            False,
        ),
        (
            "linkflags",
            extra_definitions["linkflags"],
            True,
            False,
        ),
        (
            "target_arch",
            platform.machine().lower(),
            True,
            True,
        ),
        (
            "target_os",
            get_running_os_name(),
            True,
            False,
        ),
        (
            "cppdefines",
            extra_definitions["cpp_defines"],
            True,
            False,
        ),
    )
    return {
        k: {"key": k, "value": v, "inBuildInfo": ibi, "inVersion": iv} for k, v, ibi, iv in data
    }


# Render the MONGO_BUILDINFO_ENVIRONMENT_DATA dict into an initializer for a
# `std::vector<VersionInfoInterface::BuildInfoField>`.
def fmt_build_info(data):
    def fmt_bool(val):
        return "true" if val else "false"

    def fmt_str(val):
        return 'R"({0})"_sd'.format(val.replace("\\", r"\\"))

    def fmt_obj(obj):
        return "{{{}, {}, {}, {}}}".format(
            fmt_str(obj["key"]),
            fmt_str(obj["value"]),
            fmt_bool(obj["inBuildInfo"]),
            fmt_bool(obj["inVersion"]),
        )

    return ",\n".join([fmt_obj(obj) for _, obj in data.items()])


logfile_path: str = ""
loglock = threading.Lock()


def log_check(message):
    global loglock, logfile_path
    with loglock:
        with open(logfile_path, "a") as f:
            f.write(message + "\n")


def generate_config_header(
    compiler_path, compiler_args, env_vars, logpath, additional_inputs, extra_definitions={}
) -> Dict[str, str]:
    global logfile_path
    logfile_path = logpath

    log_check("")
    extra_definitions_dict = json.loads(extra_definitions)
    buildInfoInitializer = fmt_build_info(
        default_buildinfo_environment_data(compiler_path, extra_definitions_dict)
    )

    # This generates a numeric representation of the version string so that
    # you can easily compare versions of MongoDB without having to parse
    # the version string.
    #
    # Examples:
    # 5.1.1-123 =>        ['5', '1', '1', '123', None, None] =>          [5, 1, 2, -100]
    # 5.1.1-rc2 =>        ['5', '1', '1', 'rc2', 'rc', '2'] =>           [5, 1, 1, -23]
    # 5.1.1-rc2-123 =>    ['5', '1', '1', 'rc2-123', 'rc', '2'] =>       [5, 1, 1, -23]
    # 5.1.0-alpha-123 =>  ['5', '1', '0', 'alpha-123', 'alpha', ''] =>   [5, 1, 0, -50]
    # 5.1.0-alpha1-123 => ['5', '1', '0', 'alpha1-123', 'alpha', '1'] => [5, 1, 0, -49]
    # 5.1.1 =>            ['5', '1', '1', '', None, None] =>             [5, 1, 1, 0]

    version_parts = [
        x
        for x in re.match(
            r"^(\d+)\.(\d+)\.(\d+)-?((?:(rc|alpha)(\d?))?.*)?",
            extra_definitions_dict["MONGO_VERSION"],
        ).groups()
    ]
    version_extra = version_parts[3] if version_parts[3] else ""
    if version_parts[4] == "rc":
        version_parts[3] = int(version_parts[5]) + -25
    elif version_parts[4] == "alpha":
        if version_parts[5] == "":
            version_parts[3] = -50
        else:
            version_parts[3] = int(version_parts[5]) + -50
    elif version_parts[3]:
        version_parts[2] = int(version_parts[2]) + 1
        version_parts[3] = -100
    else:
        version_parts[3] = 0
    version_parts = [int(x) for x in version_parts[:4]]

    modules = ["enterprise"] if "build_enterprise_enabled" in extra_definitions_dict else []
    module_list = ",\n".join(['"{0}"_sd'.format(x) for x in modules])

    replacements = {
        "@mongo_version@": extra_definitions_dict["MONGO_VERSION"],
        "@mongo_version_major@": str(version_parts[0]),
        "@mongo_version_minor@": str(version_parts[1]),
        "@mongo_version_patch@": str(version_parts[2]),
        "@mongo_version_extra@": str(version_parts[3]),
        "@mongo_version_extra_str@": version_extra,
        "@mongo_git_hash@": extra_definitions_dict["GIT_COMMIT_HASH"],
        "@buildinfo_js_engine@": extra_definitions_dict["js_engine_ver"],
        "@buildinfo_allocator@": extra_definitions_dict["MONGO_ALLOCATOR"],
        "@buildinfo_modules@": module_list,
        "@buildinfo_environment_data@": buildInfoInitializer,
    }

    return replacements
