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, sub_paths):
    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", "main"], cwd=path)
                return
            else:
                print("Didn't understand answer...")
    print("Cloning {} repository...".format(repo_name))
    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", "--dump-json"]
    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()
    content = json.loads(convert_to_string(output))
    for intrinsic in content:
        data = content[intrinsic]
        if not isinstance(data, dict):
            continue
        current_arch = data.get("TargetPrefix")
        builtin_name = data.get("ClangBuiltinName")
        if current_arch is None or current_arch == "" or builtin_name is None:
            continue
        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, builtin_name)

    return intrinsics


def update_intrinsics(llvm_path):
    intrinsics = extract_intrinsics_from_llvm(llvm_path)

    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`
// DO NOT EDIT IT!
/// Translate a given LLVM intrinsic name to an equivalent GCC one.
fn map_arch_intrinsic(full_name:&str)-> &'static str {
    let Some(name) = full_name.strip_prefix("llvm.") else { unimplemented!("***** unsupported LLVM intrinsic {}", full_name) };
    let Some((arch, name)) = name.split_once('.') else { unimplemented!("***** unsupported LLVM intrinsic llvm.{}", name) };
    let old_arch_res = old_archs(arch, name);
    if let ArchCheckResult::Ok(res) = old_arch_res {
        return res;
    }
match arch {""")
        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[1]))
            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 "_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("""    _ => {
        match old_arch_res {
            ArchCheckResult::UnknownIntrinsic => unimplemented!("***** unsupported LLVM intrinsic {full_name}"),
            ArchCheckResult::UnknownArch => unimplemented!("***** unsupported LLVM architecture {arch}, intrinsic: {full_name}"),
            ArchCheckResult::Ok(_) => unreachable!(),
        }
    }""")
        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",
    )

    # First, we clone the LLVM repository if it's not already here.
    clone_repository(
        "llvm-project",
        llvm_path,
        "https://github.com/llvm/llvm-project",
        ["llvm/include/llvm/IR", "llvm/include/llvm/CodeGen/"],
    )
    update_intrinsics(llvm_path)

# llvm-tblgen can be built with:
#
# mkdir llvm-tblgen-build && cd llvm-tblgen-build
# cmake -G Ninja  -DLLVM_ENABLE_PROJECTS="llvm"  -DCMAKE_BUILD_TYPE=Release  ../llvm
# ninja llvm-tblgen

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