use std::borrow::Cow;
use std::fmt::{Display, from_fn};
use std::num::ParseIntError;
use std::str::FromStr;

use crate::spec::{
    BinaryFormat, Cc, CfgAbi, DebuginfoKind, Env, FloatAbi, FramePointer, LinkerFlavor, Lld, Os,
    RustcAbi, SplitDebuginfo, StackProbeType, StaticCow, Target, TargetOptions, cvs,
};

#[cfg(test)]
mod tests;

#[allow(non_camel_case_types)]
#[derive(Copy, Clone, PartialEq)]
pub(crate) enum Arch {
    Armv7k,
    Armv7s,
    Arm64,
    Arm64e,
    Arm64_32,
    I386,
    I686,
    X86_64,
    X86_64h,
}

impl Arch {
    fn target_name(self) -> &'static str {
        match self {
            Self::Armv7k => "armv7k",
            Self::Armv7s => "armv7s",
            Self::Arm64 => "arm64",
            Self::Arm64e => "arm64e",
            Self::Arm64_32 => "arm64_32",
            Self::I386 => "i386",
            Self::I686 => "i686",
            Self::X86_64 => "x86_64",
            Self::X86_64h => "x86_64h",
        }
    }

    pub(crate) fn target_arch(self) -> crate::spec::Arch {
        match self {
            Self::Armv7k | Self::Armv7s => crate::spec::Arch::Arm,
            Self::Arm64 | Self::Arm64e | Self::Arm64_32 => crate::spec::Arch::AArch64,
            Self::I386 | Self::I686 => crate::spec::Arch::X86,
            Self::X86_64 | Self::X86_64h => crate::spec::Arch::X86_64,
        }
    }

    fn target_cpu(self, env: TargetEnv) -> &'static str {
        match self {
            Self::Armv7k => "cortex-a8",
            Self::Armv7s => "swift", // iOS 10 is only supported on iPhone 5 or higher.
            Self::Arm64 => match env {
                TargetEnv::Normal => "apple-a7",
                TargetEnv::Simulator => "apple-a12",
                TargetEnv::MacCatalyst => "apple-a12",
            },
            Self::Arm64e => "apple-a12",
            Self::Arm64_32 => "apple-s4",
            // Only macOS 10.12+ is supported, which means
            // all x86_64/x86 CPUs must be running at least penryn
            // https://github.com/llvm/llvm-project/blob/01f924d0e37a5deae51df0d77e10a15b63aa0c0f/clang/lib/Driver/ToolChains/Arch/X86.cpp#L79-L82
            Self::I386 | Self::I686 => "penryn",
            Self::X86_64 => "penryn",
            // Note: `core-avx2` is slightly more advanced than `x86_64h`, see
            // comments (and disabled features) in `x86_64h_apple_darwin` for
            // details. It is a higher baseline then `penryn` however.
            Self::X86_64h => "core-avx2",
        }
    }

    fn stack_probes(self) -> StackProbeType {
        match self {
            Self::Armv7k | Self::Armv7s => StackProbeType::None,
            Self::Arm64
            | Self::Arm64e
            | Self::Arm64_32
            | Self::I386
            | Self::I686
            | Self::X86_64
            | Self::X86_64h => StackProbeType::Inline,
        }
    }
}

#[derive(Copy, Clone, PartialEq)]
pub(crate) enum TargetEnv {
    Normal,
    Simulator,
    MacCatalyst,
}

impl TargetEnv {
    fn target_env(self) -> Env {
        match self {
            Self::Normal => Env::Unspecified,
            Self::MacCatalyst => Env::MacAbi,
            Self::Simulator => Env::Sim,
        }
    }

    // NOTE: We originally set `cfg(target_abi = "macabi")` / `cfg(target_abi = "sim")`,
    // before it was discovered that those are actually environments:
    // https://github.com/rust-lang/rust/issues/133331
    //
    // But let's continue setting them for backwards compatibility.
    // FIXME(madsmtm): Warn about using these in the future.
    fn target_abi(self) -> CfgAbi {
        match self {
            Self::Normal => CfgAbi::Unspecified,
            Self::MacCatalyst => CfgAbi::MacAbi,
            Self::Simulator => CfgAbi::Sim,
        }
    }
}

/// Get the base target options, unversioned LLVM target and `target_arch` from the three
/// things that uniquely identify Rust's Apple targets: The OS, the architecture, and the ABI.
pub(crate) fn base(
    os: Os,
    arch: Arch,
    env: TargetEnv,
) -> (TargetOptions, StaticCow<str>, crate::spec::Arch) {
    let link_env_remove = link_env_remove(&os);
    let unversioned_llvm_target = unversioned_llvm_target(&os, arch, env);
    let mut opts = TargetOptions {
        llvm_floatabi: if arch.target_arch() == crate::spec::Arch::Arm {
            Some(FloatAbi::Hard)
        } else {
            // `llvm_floatabi` makes no sense on x86 and aarch64.
            None
        },
        os,
        env: env.target_env(),
        cfg_abi: env.target_abi(),
        cpu: arch.target_cpu(env).into(),
        link_env_remove,
        vendor: "apple".into(),
        linker_flavor: LinkerFlavor::Darwin(Cc::Yes, Lld::No),
        // macOS has -dead_strip, which doesn't rely on function_sections
        function_sections: false,
        dynamic_linking: true,
        families: cvs!["unix"],
        is_like_darwin: true,
        binary_format: BinaryFormat::MachO,
        // LLVM notes that macOS 10.11+ and iOS 9+ default
        // to v4, so we do the same.
        // https://github.com/llvm/llvm-project/blob/378778a0d10c2f8d5df8ceff81f95b6002984a4b/clang/lib/Driver/ToolChains/Darwin.cpp#L1203
        default_dwarf_version: 4,
        frame_pointer: match arch {
            // clang ignores `-fomit-frame-pointer` for Armv7, it only accepts `-momit-leaf-frame-pointer`
            Arch::Armv7k | Arch::Armv7s => FramePointer::Always,
            // clang supports omitting frame pointers for the rest, but... don't?
            Arch::Arm64 | Arch::Arm64e | Arch::Arm64_32 => FramePointer::NonLeaf,
            Arch::I386 | Arch::I686 | Arch::X86_64 | Arch::X86_64h => FramePointer::Always,
        },
        has_rpath: true,
        dll_suffix: ".dylib".into(),
        archive_format: "darwin".into(),
        // Thread locals became available with iOS 8 and macOS 10.7,
        // and both are far below our minimum.
        has_thread_local: true,
        abi_return_struct_as_int: true,
        emit_debug_gdb_scripts: false,
        eh_frame_header: false,
        stack_probes: arch.stack_probes(),

        debuginfo_kind: DebuginfoKind::DwarfDsym,
        // The historical default for macOS targets is to run `dsymutil` which
        // generates a packed version of debuginfo split from the main file.
        split_debuginfo: SplitDebuginfo::Packed,
        supported_split_debuginfo: Cow::Borrowed(&[
            SplitDebuginfo::Packed,
            SplitDebuginfo::Unpacked,
            SplitDebuginfo::Off,
        ]),

        // Tell the linker that we would like it to avoid irreproducible binaries.
        //
        // This environment variable is pretty magical but is intended for
        // producing deterministic builds. This was first discovered to be used
        // by the `ar` tool as a way to control whether or not mtime entries in
        // the archive headers were set to zero or not.
        //
        // In `ld64-351.8`, shipped with Xcode 9.3, the linker was updated to
        // read this flag too. Linker versions that don't support this flag
        // may embed modification timestamps in binaries (especially in debug
        // information).
        //
        // A cleaner alternative would be to pass the `-reproducible` flag,
        // though that is only supported since `ld64-819.6` shipped with Xcode
        // 14, which is too new for our minimum supported version:
        // https://doc.rust-lang.org/rustc/platform-support/apple-darwin.html#host-tooling
        //
        // For some more info see the commentary on #47086
        link_env: Cow::Borrowed(&[(Cow::Borrowed("ZERO_AR_DATE"), Cow::Borrowed("1"))]),

        ..Default::default()
    };
    if matches!(arch, Arch::I386 | Arch::I686) {
        // All Apple x86-32 targets have SSE2.
        opts.rustc_abi = Some(RustcAbi::X86Sse2);
    }
    (opts, unversioned_llvm_target, arch.target_arch())
}

/// Generate part of the LLVM target triple.
///
/// See `rustc_codegen_ssa::back::versioned_llvm_target` for the full triple passed to LLVM and
/// Clang.
fn unversioned_llvm_target(os: &Os, arch: Arch, env: TargetEnv) -> StaticCow<str> {
    let arch = arch.target_name();
    // Convert to the "canonical" OS name used by LLVM:
    // https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/TargetParser/Triple.cpp#L236-L282
    let os = match os {
        Os::MacOs => "macosx",
        Os::IOs => "ios",
        Os::WatchOs => "watchos",
        Os::TvOs => "tvos",
        Os::VisionOs => "xros",
        _ => unreachable!("tried to get LLVM target OS for non-Apple platform"),
    };
    let environment = match env {
        TargetEnv::Normal => "",
        TargetEnv::MacCatalyst => "-macabi",
        TargetEnv::Simulator => "-simulator",
    };
    format!("{arch}-apple-{os}{environment}").into()
}

fn link_env_remove(os: &Os) -> StaticCow<[StaticCow<str>]> {
    // Apple platforms only officially support macOS as a host for any compilation.
    //
    // If building for macOS, we go ahead and remove any erroneous environment state
    // that's only applicable to cross-OS compilation. Always leave anything for the
    // host OS alone though.
    if *os == Os::MacOs {
        // `IPHONEOS_DEPLOYMENT_TARGET` must not be set when using the Xcode linker at
        // "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld",
        // although this is apparently ignored when using the linker at "/usr/bin/ld".
        cvs!["IPHONEOS_DEPLOYMENT_TARGET", "TVOS_DEPLOYMENT_TARGET", "XROS_DEPLOYMENT_TARGET"]
    } else {
        // Otherwise if cross-compiling for a different OS/SDK (including Mac Catalyst), remove any part
        // of the linking environment that's wrong and reversed.
        cvs!["MACOSX_DEPLOYMENT_TARGET"]
    }
}

/// Deployment target or SDK version.
///
/// The size of the numbers in here are limited by Mach-O's `LC_BUILD_VERSION`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OSVersion {
    pub major: u16,
    pub minor: u8,
    pub patch: u8,
}

impl FromStr for OSVersion {
    type Err = ParseIntError;

    /// Parse an OS version triple (SDK version or deployment target).
    fn from_str(version: &str) -> Result<Self, ParseIntError> {
        if let Some((major, minor)) = version.split_once('.') {
            let major = major.parse()?;
            if let Some((minor, patch)) = minor.split_once('.') {
                Ok(Self { major, minor: minor.parse()?, patch: patch.parse()? })
            } else {
                Ok(Self { major, minor: minor.parse()?, patch: 0 })
            }
        } else {
            Ok(Self { major: version.parse()?, minor: 0, patch: 0 })
        }
    }
}

impl OSVersion {
    pub fn new(major: u16, minor: u8, patch: u8) -> Self {
        Self { major, minor, patch }
    }

    pub fn fmt_pretty(self) -> impl Display {
        let Self { major, minor, patch } = self;
        from_fn(move |f| {
            write!(f, "{major}.{minor}")?;
            if patch != 0 {
                write!(f, ".{patch}")?;
            }
            Ok(())
        })
    }

    pub fn fmt_full(self) -> impl Display {
        let Self { major, minor, patch } = self;
        from_fn(move |f| write!(f, "{major}.{minor}.{patch}"))
    }

    /// Minimum operating system versions currently supported by `rustc`.
    pub fn os_minimum_deployment_target(os: &Os) -> Self {
        // When bumping a version in here, remember to update the platform-support docs too.
        //
        // NOTE: The defaults may change in future `rustc` versions, so if you are looking for the
        // default deployment target, prefer:
        // ```
        // $ rustc --print deployment-target
        // ```
        let (major, minor, patch) = match os {
            Os::MacOs => (10, 12, 0),
            Os::IOs => (10, 0, 0),
            Os::TvOs => (10, 0, 0),
            Os::WatchOs => (5, 0, 0),
            Os::VisionOs => (1, 0, 0),
            other => {
                unreachable!("tried to get deployment target for non-Apple platform: {:?}", other)
            }
        };
        Self { major, minor, patch }
    }

    /// The deployment target for the given target.
    ///
    /// This is similar to `os_minimum_deployment_target`, except that on certain targets it makes sense
    /// to raise the minimum OS version.
    ///
    /// This matches what LLVM does, see in part:
    /// <https://github.com/llvm/llvm-project/blob/llvmorg-21.1.3/llvm/lib/TargetParser/Triple.cpp#L2140-L2175>
    pub fn minimum_deployment_target(target: &Target) -> Self {
        let (major, minor, patch) = match (&target.os, &target.arch, &target.env) {
            (Os::MacOs, crate::spec::Arch::AArch64, _) => (11, 0, 0),
            (Os::IOs, crate::spec::Arch::AArch64, Env::MacAbi) => (14, 0, 0),
            (Os::IOs, crate::spec::Arch::AArch64, Env::Sim) => (14, 0, 0),
            (Os::IOs, _, _) if target.llvm_target.starts_with("arm64e") => (14, 0, 0),
            // Mac Catalyst defaults to 13.1 in Clang.
            (Os::IOs, _, Env::MacAbi) => (13, 1, 0),
            (Os::TvOs, crate::spec::Arch::AArch64, Env::Sim) => (14, 0, 0),
            (Os::WatchOs, crate::spec::Arch::AArch64, Env::Sim) => (7, 0, 0),
            // True Aarch64 on watchOS (instead of their Aarch64 Ilp32 called `arm64_32`) has been
            // available since Xcode 14, but it's only actually used more recently in watchOS 26.
            (Os::WatchOs, crate::spec::Arch::AArch64, Env::Unspecified)
                if !target.llvm_target.starts_with("arm64_32") =>
            {
                (26, 0, 0)
            }
            _ => return Self::os_minimum_deployment_target(&target.os),
        };
        Self { major, minor, patch }
    }
}

/// Name of the environment variable used to fetch the deployment target on the given OS.
pub fn deployment_target_env_var(os: &Os) -> &'static str {
    match os {
        Os::MacOs => "MACOSX_DEPLOYMENT_TARGET",
        Os::IOs => "IPHONEOS_DEPLOYMENT_TARGET",
        Os::WatchOs => "WATCHOS_DEPLOYMENT_TARGET",
        Os::TvOs => "TVOS_DEPLOYMENT_TARGET",
        Os::VisionOs => "XROS_DEPLOYMENT_TARGET",
        _ => unreachable!("tried to get deployment target env var for non-Apple platform"),
    }
}
