import json
import os
import re
import sys
import subprocess


def run_command(command, cwd=None):
    p = subprocess.Popen(command, cwd=cwd)
    if p.wait() != 0:
        print("command `{}` failed...".format(" ".join(command)))
        sys.exit(1)


def clone_repository(repo_name, path, repo_url, branch="master", sub_paths=None):
    if os.path.exists(path):
        while True:
            choice = input("There is already a `{}` folder, do you want to update it? [y/N]".format(path))
            if choice == "" or choice.lower() == "n":
                print("Skipping repository update.")
                return
            elif choice.lower() == "y":
                print("Updating repository...")
                run_command(["git", "pull", "origin", branch], cwd=path)
                return
            else:
                print("Didn't understand answer...")
    print("Cloning {} repository...".format(repo_name))
    if sub_paths is None:
        run_command(["git", "clone", repo_url, "--depth", "1", path])
    else:
        run_command(["git", "clone", repo_url, "--filter=tree:0", "--no-checkout", path])
        run_command(["git", "sparse-checkout", "init"], cwd=path)
        run_command(["git", "sparse-checkout", "set", *sub_paths], cwd=path)
        run_command(["git", "checkout"], cwd=path)


def append_intrinsic(array, intrinsic_name, translation):
    array.append((intrinsic_name, translation))


def convert_to_string(content):
    if content.__class__.__name__ == 'bytes':
        return content.decode('utf-8')
    return content


def extract_intrinsics_from_llvm(llvm_path, intrinsics):
    command = ["llvm-tblgen", "llvm/IR/Intrinsics.td"]
    cwd = os.path.join(llvm_path, "llvm/include")
    print("=> Running command `{}` from `{}`".format(command, cwd))
    p = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE)
    output, err = p.communicate()
    lines = convert_to_string(output).splitlines()
    pos = 0
    while pos < len(lines):
        line = lines[pos]
        if not line.startswith("def "):
            pos += 1
            continue
        intrinsic = line.split(" ")[1].strip()
        content = line
        while pos < len(lines):
            line = lines[pos].split(" // ")[0].strip()
            content += line
            pos += 1
            if line == "}":
                break
        entries = re.findall('string ClangBuiltinName = "(\\w+)";', content)
        current_arch = re.findall('string TargetPrefix = "(\\w+)";', content)
        if len(entries) == 1 and len(current_arch) == 1:
            current_arch = current_arch[0]
            intrinsic = intrinsic.split("_")
            if len(intrinsic) < 2 or intrinsic[0] != "int":
                continue
            intrinsic[0] = "llvm"
            intrinsic = ".".join(intrinsic)
            if current_arch not in intrinsics:
                intrinsics[current_arch] = []
            append_intrinsic(intrinsics[current_arch], intrinsic, entries[0])


def append_translation(json_data, p, array):
    it = json_data["index"][p]
    content = it["docs"].split('`')
    if len(content) != 5:
        return
    append_intrinsic(array, content[1], content[3])


def extract_intrinsics_from_llvmint(llvmint, intrinsics):
    archs = [
        "AMDGPU",
        "aarch64",
        "arm",
        "cuda",
        "hexagon",
        "mips",
        "nvvm",
        "ppc",
        "ptx",
        "x86",
        "xcore",
    ]

    json_file = os.path.join(llvmint, "target/doc/llvmint.json")
    # We need to regenerate the documentation!
    run_command(
        ["cargo", "rustdoc", "--", "-Zunstable-options", "--output-format", "json"],
        cwd=llvmint,
    )
    with open(json_file, "r", encoding="utf8") as f:
        json_data = json.loads(f.read())
    for p in json_data["paths"]:
        it = json_data["paths"][p]
        if it["crate_id"] != 0:
            # This is from an external crate.
            continue
        if it["kind"] != "function":
            # We're only looking for functions.
            continue
        # if len(it["path"]) == 2:
        #   # This is a "general" intrinsic, not bound to a specific arch.
        #   append_translation(json_data, p, general)
        #   continue
        if len(it["path"]) != 3 or it["path"][1] not in archs:
            continue
        arch = it["path"][1]
        if arch not in intrinsics:
            intrinsics[arch] = []
        append_translation(json_data, p, intrinsics[arch])


def fill_intrinsics(intrinsics, from_intrinsics, all_intrinsics):
    for arch in from_intrinsics:
        if arch not in intrinsics:
            intrinsics[arch] = []
        for entry in from_intrinsics[arch]:
            if entry[0] in all_intrinsics:
                if all_intrinsics[entry[0]] == entry[1]:
                    # This is a "full" duplicate, both the LLVM instruction and the GCC
                    # translation are the same.
                    continue
                intrinsics[arch].append((entry[0], entry[1], True))
            else:
                intrinsics[arch].append((entry[0], entry[1], False))
                all_intrinsics[entry[0]] = entry[1]


def update_intrinsics(llvm_path, llvmint, llvmint2):
    intrinsics_llvm = {}
    intrinsics_llvmint = {}
    all_intrinsics = {}

    extract_intrinsics_from_llvm(llvm_path, intrinsics_llvm)
    extract_intrinsics_from_llvmint(llvmint, intrinsics_llvmint)
    extract_intrinsics_from_llvmint(llvmint2, intrinsics_llvmint)

    intrinsics = {}
    # We give priority to translations from LLVM over the ones from llvmint.
    fill_intrinsics(intrinsics, intrinsics_llvm, all_intrinsics)
    fill_intrinsics(intrinsics, intrinsics_llvmint, all_intrinsics)

    archs = [arch for arch in intrinsics]
    archs.sort()

    output_file = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "../src/intrinsic/archs.rs",
    )
    # A hashmap of all architectures. This allows us to first match on the architecture, and then on the intrinsics.
    # This speeds up the comparison, and makes our code considerably smaller.
    # Since all intrinsic names start with "llvm.", we skip that prefix.
    print("Updating content of `{}`...".format(output_file))
    with open(output_file, "w", encoding="utf8") as out:
        out.write("// File generated by `rustc_codegen_gcc/tools/generate_intrinsics.py`\n")
        out.write("// DO NOT EDIT IT!\n")
        out.write("/// Translate a given LLVM intrinsic name to an equivalent GCC one.\n")
        out.write("fn map_arch_intrinsic(full_name:&str)->&'static str{\n")
        out.write('let Some(name) = full_name.strip_prefix("llvm.") else { unimplemented!("***** unsupported LLVM intrinsic {}", full_name) };\n')
        out.write('let Some((arch, name)) = name.split_once(\'.\') else { unimplemented!("***** unsupported LLVM intrinsic {}", name) };\n')
        out.write("match arch {\n")
        for arch in archs:
            if len(intrinsics[arch]) == 0:
                continue
            attribute = "#[expect(non_snake_case)]" if arch[0].isupper() else ""
            out.write("\"{}\" => {{ {} fn {}(name: &str,full_name:&str) -> &'static str {{ match name {{".format(arch, attribute, arch))
            intrinsics[arch].sort(key=lambda x: (x[0], x[2]))
            out.write('    // {}\n'.format(arch))
            for entry in intrinsics[arch]:
                llvm_name = entry[0].removeprefix("llvm.");
                llvm_name = llvm_name.removeprefix(arch);
                llvm_name = llvm_name.removeprefix(".");
                if entry[2] is True: # if it is a duplicate
                    out.write('    // [DUPLICATE]: "{}" => "{}",\n'.format(llvm_name, entry[1]))
                elif "_round_mask" in entry[1]:
                    out.write('    // [INVALID CONVERSION]: "{}" => "{}",\n'.format(llvm_name, entry[1]))
                else:
                    out.write('    "{}" => "{}",\n'.format(llvm_name, entry[1]))
            out.write('    _ => unimplemented!("***** unsupported LLVM intrinsic {full_name}"),\n')
            out.write("}} }} {}(name,full_name) }}\n,".format(arch))
        out.write('    _ => unimplemented!("***** unsupported LLVM architecture {arch}, intrinsic:{full_name}"),\n')
        out.write("}\n}")
    subprocess.call(["rustfmt", output_file])
    print("Done!")


def main():
    llvm_path = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "llvm-project",
    )
    llvmint_path = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "llvmint",
    )
    llvmint2_path = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "llvmint-2",
    )

    # First, we clone the LLVM repository if it's not already here.
    clone_repository(
        "llvm-project",
        llvm_path,
        "https://github.com/llvm/llvm-project",
        branch="main",
        sub_paths=["llvm/include/llvm/IR", "llvm/include/llvm/CodeGen/"],
    )
    clone_repository(
        "llvmint",
        llvmint_path,
        "https://github.com/GuillaumeGomez/llvmint",
    )
    clone_repository(
        "llvmint2",
        llvmint2_path,
        "https://github.com/antoyo/llvmint",
    )
    update_intrinsics(llvm_path, llvmint_path, llvmint2_path)


if __name__ == "__main__":
    sys.exit(main())
