use std::{env, mem};

use crate::{target::TargetInfo, utilities::OnceLock, Error, ErrorKind};

#[derive(Debug)]
struct TargetInfoParserInner {
    full_arch: Box<str>,
    arch: Box<str>,
    vendor: Box<str>,
    os: Box<str>,
    env: Box<str>,
    abi: Box<str>,
}

impl TargetInfoParserInner {
    fn from_cargo_environment_variables() -> Result<Self, Error> {
        // `TARGET` must be present.
        //
        // No need to emit `rerun-if-env-changed` for this,
        // as it is controlled by Cargo itself.
        #[allow(clippy::disallowed_methods)]
        let target_name = env::var("TARGET").map_err(|err| {
            Error::new(
                ErrorKind::EnvVarNotFound,
                format!("failed reading TARGET: {err}"),
            )
        })?;

        // Parse the full architecture name from the target name.
        let (full_arch, _rest) = target_name.split_once('-').ok_or(Error::new(
            ErrorKind::InvalidTarget,
            format!("target `{target_name}` only had a single component (at least two required)"),
        ))?;

        let cargo_env = |name, fallback: Option<&str>| -> Result<Box<str>, Error> {
            // No need to emit `rerun-if-env-changed` for these,
            // as they are controlled by Cargo itself.
            #[allow(clippy::disallowed_methods)]
            match env::var(name) {
                Ok(var) => Ok(var.into_boxed_str()),
                Err(err) => match fallback {
                    Some(fallback) => Ok(fallback.into()),
                    None => Err(Error::new(
                        ErrorKind::EnvVarNotFound,
                        format!("did not find fallback information for target `{target_name}`, and failed reading {name}: {err}"),
                    )),
                },
            }
        };

        // Prefer to use `CARGO_ENV_*` if set, since these contain the most
        // correct information relative to the current `rustc`, and makes it
        // possible to support custom target JSON specs unknown to `rustc`.
        //
        // NOTE: If the user is using an older `rustc`, that data may be older
        // than our pre-generated data, but we still prefer Cargo's view of
        // the world, since at least `cc` won't differ from `rustc` in that
        // case.
        //
        // These may not be set in case the user depended on being able to
        // just set `TARGET` outside of build scripts; in those cases, fall
        // back back to data from the known set of target names instead.
        //
        // See discussion in #1225 for further details.
        let fallback_target = TargetInfo::from_rustc_target(&target_name).ok();
        let ft = fallback_target.as_ref();
        let arch = cargo_env("CARGO_CFG_TARGET_ARCH", ft.map(|t| t.arch))?;
        let vendor = cargo_env("CARGO_CFG_TARGET_VENDOR", ft.map(|t| t.vendor))?;
        let os = cargo_env("CARGO_CFG_TARGET_OS", ft.map(|t| t.os))?;
        let mut env = cargo_env("CARGO_CFG_TARGET_ENV", ft.map(|t| t.env))?;
        // `target_abi` was stabilized in Rust 1.78, which is higher than our
        // MSRV, so it may not always be available; In that case, fall back to
        // `""`, which is _probably_ correct for unknown target names.
        let mut abi = cargo_env("CARGO_CFG_TARGET_ABI", ft.map(|t| t.abi))
            .unwrap_or_else(|_| String::default().into_boxed_str());

        // Remove `macabi` and `sim` from `target_abi` (if present), it's been moved to `target_env`.
        // TODO: Remove once MSRV is bumped to 1.91 and `rustc` removes these from `target_abi`.
        if matches!(&*abi, "macabi" | "sim") {
            debug_assert!(
                matches!(&*env, "" | "macabi" | "sim"),
                "env/abi mismatch: {:?}, {:?}",
                env,
                abi,
            );
            env = mem::replace(&mut abi, String::default().into_boxed_str());
        }

        Ok(Self {
            full_arch: full_arch.to_string().into_boxed_str(),
            arch,
            vendor,
            os,
            env,
            abi,
        })
    }
}

/// Parser for [`TargetInfo`], contains cached information.
#[derive(Default, Debug)]
pub(crate) struct TargetInfoParser(OnceLock<Result<TargetInfoParserInner, Error>>);

impl TargetInfoParser {
    pub fn parse_from_cargo_environment_variables(&self) -> Result<TargetInfo<'_>, Error> {
        match self
            .0
            .get_or_init(TargetInfoParserInner::from_cargo_environment_variables)
        {
            Ok(TargetInfoParserInner {
                full_arch,
                arch,
                vendor,
                os,
                env,
                abi,
            }) => Ok(TargetInfo {
                full_arch,
                arch,
                vendor,
                os,
                env,
                abi,
            }),
            Err(e) => Err(e.clone()),
        }
    }
}

/// Parse the full architecture in the target name into the simpler
/// `cfg(target_arch = "...")` that `rustc` exposes.
fn parse_arch(full_arch: &str) -> Option<&str> {
    // NOTE: Some of these don't necessarily match an existing target in
    // `rustc`. They're parsed anyhow to be as forward-compatible as possible,
    // while still being correct.
    //
    // See also:
    // https://docs.rs/cfg-expr/0.18.0/cfg_expr/targets/index.html
    // https://docs.rs/target-lexicon/0.13.2/target_lexicon/enum.Architecture.html
    // https://gcc.gnu.org/onlinedocs/gcc/Submodel-Options.html
    // `clang -print-targets`
    Some(match full_arch {
        arch if arch.starts_with("mipsisa32r6") => "mips32r6", // mipsisa32r6 | mipsisa32r6el
        arch if arch.starts_with("mipsisa64r6") => "mips64r6", // mipsisa64r6 | mipsisa64r6el

        arch if arch.starts_with("mips64") => "mips64", // mips64 | mips64el
        arch if arch.starts_with("mips") => "mips",     // mips | mipsel

        arch if arch.starts_with("loongarch64") => "loongarch64",
        arch if arch.starts_with("loongarch32") => "loongarch32",

        arch if arch.starts_with("powerpc64") => "powerpc64", // powerpc64 | powerpc64le
        arch if arch.starts_with("powerpc") => "powerpc",
        arch if arch.starts_with("ppc64") => "powerpc64",
        arch if arch.starts_with("ppc") => "powerpc",

        arch if arch.starts_with("x86_64") => "x86_64", // x86_64 | x86_64h
        arch if arch.starts_with("i") && arch.ends_with("86") => "x86", // i386 | i586 | i686

        "arm64ec" => "arm64ec", // https://github.com/rust-lang/rust/issues/131172
        arch if arch.starts_with("aarch64") => "aarch64", // arm64e | arm64_32
        arch if arch.starts_with("arm64") => "aarch64", // aarch64 | aarch64_be

        arch if arch.starts_with("arm") => "arm", // arm | armv7s | armeb | ...
        arch if arch.starts_with("thumb") => "arm", // thumbv4t | thumbv7a | thumbv8m | ...

        arch if arch.starts_with("riscv64") => "riscv64",
        arch if arch.starts_with("riscv32") => "riscv32",

        arch if arch.starts_with("wasm64") => "wasm64",
        arch if arch.starts_with("wasm32") => "wasm32", // wasm32 | wasm32v1
        "asmjs" => "wasm32",

        arch if arch.starts_with("nvptx64") => "nvptx64",
        arch if arch.starts_with("nvptx") => "nvptx",

        arch if arch.starts_with("bpf") => "bpf", // bpfeb | bpfel

        // https://github.com/bytecodealliance/wasmtime/tree/v30.0.1/pulley
        arch if arch.starts_with("pulley64") => "pulley64",
        arch if arch.starts_with("pulley32") => "pulley32",

        // https://github.com/Clever-ISA/Clever-ISA
        arch if arch.starts_with("clever") => "clever",

        "sparc" | "sparcv7" | "sparcv8" => "sparc",
        "sparc64" | "sparcv9" => "sparc64",

        "amdgcn" => "amdgpu",
        "avr" => "avr",
        "csky" => "csky",
        "hexagon" => "hexagon",
        "m68k" => "m68k",
        "msp430" => "msp430",
        "r600" => "r600",
        "s390x" => "s390x",
        "xtensa" => "xtensa",

        // Arches supported by gcc, but not LLVM.
        arch if arch.starts_with("alpha") => "alpha", // DEC Alpha
        "hppa" => "hppa", // https://en.wikipedia.org/wiki/PA-RISC, also known as HPPA
        arch if arch.starts_with("sh") => "sh", // SuperH
        _ => return None,
    })
}

/// Parse environment and ABI from the last component of the target name.
fn parse_envabi(last_component: &str) -> Option<(&str, &str)> {
    let (env, abi) = match last_component {
        // Combined environment and ABI

        // gnullvm | gnueabi | gnueabihf | gnuabiv2 | gnuabi64 | gnuspe | gnux32 | gnu_ilp32
        env_and_abi if env_and_abi.starts_with("gnu") => {
            let abi = env_and_abi.strip_prefix("gnu").unwrap();
            let abi = abi.strip_prefix("_").unwrap_or(abi);
            ("gnu", abi)
        }
        // musl | musleabi | musleabihf | muslabi64 | muslspe
        env_and_abi if env_and_abi.starts_with("musl") => {
            ("musl", env_and_abi.strip_prefix("musl").unwrap())
        }
        // uclibc | uclibceabi | uclibceabihf
        env_and_abi if env_and_abi.starts_with("uclibc") => {
            ("uclibc", env_and_abi.strip_prefix("uclibc").unwrap())
        }
        // newlib | newlibeabihf
        env_and_abi if env_and_abi.starts_with("newlib") => {
            ("newlib", env_and_abi.strip_prefix("newlib").unwrap())
        }

        // Environments
        "msvc" => ("msvc", ""),
        "ohos" => ("ohos", ""),
        "qnx700" => ("nto70", ""),
        "qnx710_iosock" => ("nto71_iosock", ""),
        "qnx710" => ("nto71", ""),
        "qnx800" => ("nto80", ""),
        "sgx" => ("sgx", ""),
        "threads" => ("threads", ""),
        "mlibc" => ("mlibc", ""),

        // ABIs
        "abi64" => ("", "abi64"),
        "abiv2" => ("", "spe"),
        "eabi" => ("", "eabi"),
        "eabihf" => ("", "eabihf"),
        "macabi" => ("macabi", ""),
        "sim" => ("sim", ""),
        "softfloat" => ("", "softfloat"),
        "spe" => ("", "spe"),
        "x32" => ("", "x32"),

        // Badly named targets, ELF is already known from target OS.
        // Probably too late to fix now though.
        "elf" => ("", ""),
        // Undesirable to expose to user code (yet):
        // https://github.com/rust-lang/rust/pull/131166#issuecomment-2389541917
        "freestanding" => ("", ""),

        _ => return None,
    };
    Some((env, abi))
}

impl<'a> TargetInfo<'a> {
    pub(crate) fn from_rustc_target(target: &'a str) -> Result<Self, Error> {
        // FIXME(madsmtm): This target should be renamed, cannot be parsed
        // with the means we do below (since `none` must not be interpreted
        // as an env/ABI).
        if target == "x86_64-unknown-linux-none" {
            return Ok(Self {
                full_arch: "x86_64",
                arch: "x86_64",
                vendor: "unknown",
                os: "linux",
                env: "",
                abi: "",
            });
        }

        if target == "armv7a-vex-v5" {
            return Ok(Self {
                full_arch: "armv7a",
                arch: "arm",
                vendor: "vex",
                os: "vexos",
                env: "v5",
                abi: "eabihf",
            });
        }

        let mut components = target.split('-');

        // Insist that the target name contains at least a valid architecture.
        let full_arch = components.next().ok_or(Error::new(
            ErrorKind::InvalidTarget,
            "target was empty".to_string(),
        ))?;
        let arch = parse_arch(full_arch).ok_or_else(|| {
            Error::new(
                ErrorKind::UnknownTarget,
                format!("target `{target}` had an unknown architecture"),
            )
        })?;

        // Newer target names have begun omitting the vendor, so the only
        // component we know must be there is the OS name.
        let components: Vec<_> = components.collect();
        let (vendor, os, mut env, mut abi) = match &*components {
            [] => {
                return Err(Error::new(
                    ErrorKind::InvalidTarget,
                    format!("target `{target}` must have at least two components"),
                ))
            }
            // Two components; format is `arch-os`.
            [os] => ("unknown", *os, "", ""),
            // The three-component case is a bit tricky to handle, it could
            // either have the format `arch-vendor-os` or `arch-os-env+abi`.
            [vendor_or_os, os_or_envabi] => {
                // We differentiate between these by checking if the last
                // component is an env/ABI; if it isn't, then it's probably
                // an OS instead.
                if let Some((env, abi)) = parse_envabi(os_or_envabi) {
                    ("unknown", *vendor_or_os, env, abi)
                } else {
                    (*vendor_or_os, *os_or_envabi, "", "")
                }
            }
            // Four components; format is `arch-vendor-os-env+abi`.
            [vendor, os, envabi] => {
                let (env, abi) = parse_envabi(envabi).ok_or_else(|| {
                    Error::new(
                        ErrorKind::UnknownTarget,
                        format!("unknown environment/ABI `{envabi}` in target `{target}`"),
                    )
                })?;
                (*vendor, *os, env, abi)
            }
            _ => {
                return Err(Error::new(
                    ErrorKind::InvalidTarget,
                    format!("too many components in target `{target}`"),
                ))
            }
        };

        // Part of the architecture name is carried over into the ABI.
        match full_arch {
            // https://github.com/rust-lang/compiler-team/issues/830
            arch if arch.starts_with("riscv32e") => {
                abi = "ilp32e";
            }
            _ => {}
        }

        // Various environment/ABIs are determined based on OS name.
        match os {
            "3ds" | "rtems" | "espidf" => env = "newlib",
            "vxworks" => env = "gnu",
            "redox" => env = "relibc",
            "aix" => abi = "vec-extabi",
            _ => {}
        }

        // Extra overrides for badly named targets.
        match target {
            // Actually simulator targets.
            "i386-apple-ios" | "x86_64-apple-ios" | "x86_64-apple-tvos" => {
                env = "sim";
            }
            // Name should've contained `muslabi64`.
            "mips64-openwrt-linux-musl" => {
                abi = "abi64";
            }
            // Specifies abi even though not in name.
            "armv6-unknown-freebsd" | "armv6k-nintendo-3ds" | "armv7-unknown-freebsd" => {
                abi = "eabihf";
            }
            // Specifies abi even though not in name.
            "armv7-unknown-linux-ohos" | "armv7-unknown-trusty" => {
                abi = "eabi";
            }
            _ => {}
        }

        let os = match os {
            // Horizon is the common/internal OS name for 3DS and the Switch.
            "3ds" | "switch" => "horizon",
            // FIXME(madsmtm): macOS targets are badly named.
            "darwin" => "macos",

            // WASI targets contain the preview version in them too. Should've
            // been `wasi-p1`/`wasi-p2`, but that's probably too late now.
            os if os.starts_with("wasi") => {
                env = os.strip_prefix("wasi").unwrap();
                "wasi"
            }
            // FIXME(madsmtm): Badly named targets `*-linux-androideabi`,
            // should be `*-android-eabi`.
            "androideabi" => {
                abi = "eabi";
                "android"
            }

            os => os,
        };

        let vendor = match vendor {
            // esp, esp32, esp32s2 etc.
            vendor if vendor.starts_with("esp") => "espressif",
            // FIXME(madsmtm): Badly named targets `*-linux-android*`,
            // "linux" makes no sense as the vendor name.
            "linux" if os == "android" || os == "androideabi" => "unknown",
            // FIXME(madsmtm): Fix in `rustc` after
            // https://github.com/rust-lang/compiler-team/issues/850.
            "wali" => "unknown",
            "lynx" => "unknown",
            // Some Linux distributions set their name as the target vendor,
            // so we have to assume that it can be an arbitary string.
            vendor => vendor,
        };

        // Intentionally also marked as an ABI:
        // https://github.com/rust-lang/rust/pull/86922
        if vendor == "fortanix" {
            abi = "fortanix";
        }
        if vendor == "uwp" {
            abi = "uwp";
        }
        if ["powerpc64-unknown-linux-gnu", "powerpc64-wrs-vxworks"].contains(&target) {
            abi = "elfv1";
        }
        if [
            "powerpc64-unknown-freebsd",
            "powerpc64-unknown-linux-musl",
            "powerpc64-unknown-openbsd",
            "powerpc64le-unknown-freebsd",
            "powerpc64le-unknown-linux-gnu",
            "powerpc64le-unknown-linux-musl",
        ]
        .contains(&target)
        {
            abi = "elfv2";
        }

        Ok(Self {
            full_arch,
            arch,
            vendor,
            os,
            env,
            abi,
        })
    }
}

#[cfg(test)]
#[allow(unexpected_cfgs)]
mod tests {
    use std::process::Command;

    use super::TargetInfo;
    use crate::ErrorKind;

    // Test tier 1 targets.
    #[test]
    fn tier1() {
        let targets = [
            "aarch64-unknown-linux-gnu",
            "aarch64-apple-darwin",
            "i686-pc-windows-gnu",
            "i686-pc-windows-msvc",
            "i686-unknown-linux-gnu",
            "x86_64-apple-darwin",
            "x86_64-pc-windows-gnu",
            "x86_64-pc-windows-msvc",
            "x86_64-unknown-linux-gnu",
        ];

        for target in targets {
            // Check that they parse.
            let _ = TargetInfo::from_rustc_target(target).unwrap();
        }
    }

    // Various custom target names not (or no longer) known by `rustc`.
    #[test]
    fn parse_extra() {
        let targets = [
            "aarch64-unknown-none-gnu",
            "aarch64-uwp-windows-gnu",
            "arm-frc-linux-gnueabi",
            "arm-unknown-netbsd-eabi",
            "armv7neon-unknown-linux-gnueabihf",
            "armv7neon-unknown-linux-musleabihf",
            "thumbv7-unknown-linux-gnueabihf",
            "thumbv7-unknown-linux-musleabihf",
            "armv7-apple-ios",
            "wasm32-wasi",
            "x86_64-rumprun-netbsd",
            "x86_64-unknown-linux",
            "x86_64-alpine-linux-musl",
            "x86_64-chimera-linux-musl",
            "x86_64-foxkit-linux-musl",
            "arm-poky-linux-gnueabi",
            "x86_64-unknown-moturus",
            "x86_64-unknown-managarm-mlibc",
        ];

        for target in targets {
            // Check that they parse.
            let _ = TargetInfo::from_rustc_target(target).unwrap();
        }
    }

    fn target_from_rustc_cfgs<'a>(target: &'a str, cfgs: &'a str) -> TargetInfo<'a> {
        // Cannot determine full architecture from cfgs.
        let (full_arch, _rest) = target.split_once('-').expect("target to have arch");

        let mut target = TargetInfo {
            full_arch,
            arch: "invalid-none-set",
            vendor: "invalid-none-set",
            os: "invalid-none-set",
            env: "invalid-none-set",
            // Not set in older Rust versions
            abi: "",
        };

        for cfg in cfgs.lines() {
            if let Some((name, value)) = cfg.split_once('=') {
                // Remove whitespace, if `rustc` decided to insert any.
                let name = name.trim();
                let value = value.trim();

                // Remove quotes around value.
                let value = value.strip_prefix('"').unwrap_or(value);
                let value = value.strip_suffix('"').unwrap_or(value);

                match name {
                    "target_arch" => target.arch = value,
                    "target_vendor" => target.vendor = value,
                    "target_os" => target.os = value,
                    "target_env" => target.env = value,
                    "target_abi" => target.abi = value,
                    _ => {}
                }
            } else {
                // Skip cfgs like `debug_assertions` and `unix`.
            }
        }

        if matches!(target.abi, "macabi" | "sim") {
            assert_eq!(target.env, target.abi);
            target.abi = "";
        }

        target
    }

    #[test]
    fn unknown_env_determined_as_unknown() {
        let err = TargetInfo::from_rustc_target("aarch64-unknown-linux-bogus").unwrap_err();
        assert!(matches!(err.kind, ErrorKind::UnknownTarget));
    }

    // Used in .github/workflows/test-rustc-targets.yml
    #[test]
    #[cfg_attr(
        not(rustc_target_test),
        ignore = "must enable explicitly with --cfg=rustc_target_test"
    )]
    fn parse_rustc_targets() {
        let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());

        let target_list = Command::new(&rustc)
            .arg("--print=target-list")
            .output()
            .unwrap()
            .stdout;
        let target_list = String::from_utf8(target_list).unwrap();

        let mut has_failure = false;
        for target in target_list.lines() {
            let cfgs = Command::new(&rustc)
                .arg("--target")
                .arg(target)
                .arg("--print=cfg")
                .output()
                .unwrap()
                .stdout;
            let cfgs = String::from_utf8(cfgs).unwrap();

            let expected = target_from_rustc_cfgs(target, &cfgs);
            let actual = TargetInfo::from_rustc_target(target);

            if Some(&expected) != actual.as_ref().ok() {
                eprintln!("failed comparing {target}:");
                eprintln!("  expected: Ok({expected:?})");
                eprintln!("    actual: {actual:?}");
                eprintln!();
                has_failure = true;
            }
        }

        if has_failure {
            panic!("failed comparing targets");
        }
    }

    #[test]
    fn parses_apple_envs_correctly() {
        assert_eq!(
            TargetInfo::from_rustc_target("aarch64-apple-ios-macabi").unwrap(),
            TargetInfo {
                full_arch: "aarch64",
                arch: "aarch64",
                vendor: "apple",
                os: "ios",
                env: "macabi",
                abi: "",
            }
        );
        assert_eq!(
            TargetInfo::from_rustc_target("aarch64-apple-ios-sim").unwrap(),
            TargetInfo {
                full_arch: "aarch64",
                arch: "aarch64",
                vendor: "apple",
                os: "ios",
                env: "sim",
                abi: "",
            }
        );
    }
}
