//! Compiler / archiver identity, version, and taxonomy types.

use std::fmt;

use serde::{Deserialize, Serialize};

/// Recognized C/C++ compiler family.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompilerKind {
    /// LLVM Clang.
    Clang,
    /// Apple-shipped Clang (`Apple clang version …`).  Treated as
    /// Clang-compatible for capability purposes; tracked separately
    /// for diagnostics.
    AppleClang,
    /// LLVM `clang-cl`: Clang's `cl.exe`-compatible driver.  Reports a
    /// `clang version …` banner like Clang, but accepts the MSVC
    /// command line (`/std:c++17`, `/showIncludes`, `/Fo…`), so it is
    /// detected by the invoked name and drives the MSVC dialect.
    ClangCl,
    /// GNU GCC / `g++`.
    Gcc,
    /// Microsoft Visual C++ (`cl.exe`).  Detected so Cabin can
    /// produce a clear unsupported-backend error; the GCC/Clang
    /// command pipeline cannot be used with this compiler.
    Msvc,
    /// Compiler whose `--version` output Cabin does not recognize.
    /// Capability detection treats this conservatively.
    Unknown,
}

impl CompilerKind {
    /// Stable lower-case identifier used in metadata output.
    pub fn as_key(self) -> &'static str {
        match self {
            CompilerKind::Clang => "clang",
            CompilerKind::AppleClang => "apple-clang",
            CompilerKind::ClangCl => "clang-cl",
            CompilerKind::Gcc => "gcc",
            CompilerKind::Msvc => "msvc",
            CompilerKind::Unknown => "unknown",
        }
    }

    /// Inverse of [`Self::as_key`]: parse a stable family id as
    /// used by `cfg(cc = "...")` / `cfg(cxx = "...")` conditions
    /// and `cabin metadata`.  Returns `None` outside the closed set.
    pub fn from_key(key: &str) -> Option<Self> {
        match key {
            "clang" => Some(CompilerKind::Clang),
            "apple-clang" => Some(CompilerKind::AppleClang),
            "clang-cl" => Some(CompilerKind::ClangCl),
            "gcc" => Some(CompilerKind::Gcc),
            "msvc" => Some(CompilerKind::Msvc),
            "unknown" => Some(CompilerKind::Unknown),
            _ => None,
        }
    }

    /// Whether this compiler is part of the Clang family. `clang-cl`
    /// is Clang under the hood, so it shares Clang's diagnostic and
    /// response-file capabilities even though it speaks the MSVC
    /// dialect.
    pub fn is_clang_like(self) -> bool {
        matches!(
            self,
            CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::ClangCl
        )
    }

    /// Whether this compiler accepts the GCC-style command line
    /// the current C++ backend emits (`-O<n>`, `-std=c++NN`,
    /// `-MMD -MF`, `-DNAME`, `-Idir`, …).  Note `clang-cl` is
    /// excluded: it is Clang but parses the MSVC command line.
    pub fn supports_gcc_style_command_line(self) -> bool {
        matches!(
            self,
            CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::Gcc
        )
    }

    /// Whether this compiler drives the MSVC command-line dialect
    /// (`/std:…`, `/Fo…`, `/showIncludes`, `<name>.lib` archives):
    /// `cl.exe` and Clang's `cl`-compatible `clang-cl` driver.
    pub fn speaks_msvc_dialect(self) -> bool {
        matches!(self, CompilerKind::Msvc | CompilerKind::ClangCl)
    }
}

impl fmt::Display for CompilerKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_key())
    }
}

/// Recognized static-library archiver family.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ArchiverKind {
    /// GNU `ar` / BSD `ar`.  Accepts the `crs` mode flags Cabin
    /// emits today.
    Ar,
    /// LLVM `llvm-ar`.  Accepts the same `crs` mode flags.
    LlvmAr,
    /// Microsoft `lib.exe`.  The MSVC dialect's archiver, driven as
    /// `lib /OUT:<lib> <objs>` to produce a `.lib` static library.
    Lib,
    /// Archiver whose `--version` output Cabin does not recognize.
    Unknown,
}

impl ArchiverKind {
    pub fn as_key(self) -> &'static str {
        match self {
            ArchiverKind::Ar => "ar",
            ArchiverKind::LlvmAr => "llvm-ar",
            ArchiverKind::Lib => "lib",
            ArchiverKind::Unknown => "unknown",
        }
    }

    /// Whether this archiver accepts the `crs` mode flags Cabin
    /// emits today.
    pub fn supports_ar_crs(self) -> bool {
        matches!(self, ArchiverKind::Ar | ArchiverKind::LlvmAr)
    }

    /// Whether this archiver can produce a static library in some
    /// dialect Cabin drives: GNU `ar` / `llvm-ar` via `ar crs`, or
    /// MSVC `lib.exe` via `lib /OUT:`.  Distinct from
    /// [`Self::supports_ar_crs`], which is GNU-specific - `lib.exe`
    /// produces a static library but not via `crs` mode flags.
    pub fn produces_static_library(self) -> bool {
        matches!(
            self,
            ArchiverKind::Ar | ArchiverKind::LlvmAr | ArchiverKind::Lib
        )
    }
}

impl fmt::Display for ArchiverKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_key())
    }
}

/// Decomposed compiler / archiver version (`major.minor.patch`).
///
/// `major` is required; `minor` and `patch` are optional because
/// some versions only report two components. `raw` keeps the
/// original substring for diagnostics.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerVersion {
    pub major: u32,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub minor: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub patch: Option<u32>,
    pub raw: String,
}

impl CompilerVersion {
    /// Parse a `major[.minor[.patch]]` substring into a typed
    /// [`CompilerVersion`].  Returns `None` when the leading
    /// component is not a valid `u32`.
    pub fn parse(raw: &str) -> Option<Self> {
        let mut parts = raw.split('.');
        let major: u32 = parts.next()?.parse().ok()?;
        let minor = parts.next().and_then(|s| s.parse().ok());
        let patch = parts.next().and_then(|s| s.parse().ok());
        Some(Self {
            major,
            minor,
            patch,
            raw: raw.to_owned(),
        })
    }

    /// Like [`Self::parse`], but tolerant of a non-numeric suffix
    /// on each dotted component (`"20.1.0git"`, `"10.0.0-4ubuntu1"`,
    /// `"19.1.0-rc2"`): a component contributes its leading ASCII
    /// digits and must start with a digit.  Banner parsers opt into
    /// this where vendors append suffixes; [`Self::parse`] stays
    /// strict for callers that need exact numeric tokens.
    pub fn parse_with_suffix(raw: &str) -> Option<Self> {
        let mut parts = raw.split('.');
        let major = leading_digits(parts.next()?)?;
        let minor = parts.next().and_then(leading_digits);
        let patch = parts.next().and_then(leading_digits);
        Some(Self {
            major,
            minor,
            patch,
            raw: raw.to_owned(),
        })
    }

    /// Formatted `major.minor.patch` view, omitting unset
    /// components.  Used in metadata JSON and `CABIN_*` env vars.
    pub fn to_display_string(&self) -> String {
        match (self.minor, self.patch) {
            (Some(min), Some(pat)) => format!("{}.{}.{}", self.major, min, pat),
            (Some(min), None) => format!("{}.{}", self.major, min),
            _ => self.major.to_string(),
        }
    }
}

impl fmt::Display for CompilerVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.to_display_string())
    }
}

fn leading_digits(part: &str) -> Option<u32> {
    let end = part
        .find(|c: char| !c.is_ascii_digit())
        .unwrap_or(part.len());
    if end == 0 {
        return None;
    }
    part[..end].parse().ok()
}

/// Detected identity of one C/C++ compiler.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerIdentity {
    pub kind: CompilerKind,
    /// Parsed version, when the version-output line was
    /// recognized.  `None` when the compiler emitted output Cabin
    /// could not parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<CompilerVersion>,
    /// Optional default target triple as the compiler reported it
    /// (the "Target: …" line from Clang, or an analogous GCC line).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
    /// First non-empty line of combined `--version` output, kept
    /// for diagnostics.  Truncated to a sensible length.
    pub raw_version_line: String,
}

impl CompilerIdentity {
    /// Convenience: identity for an unknown / unparsable compiler.
    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
        Self {
            kind: CompilerKind::Unknown,
            version: None,
            target: None,
            raw_version_line: raw_version_line.into(),
        }
    }

    /// Compact JSON view used by `cabin metadata`.
    pub fn as_json(&self) -> serde_json::Value {
        identity_json(
            self.kind.as_key(),
            self.version.as_ref(),
            self.target.as_deref(),
            &self.raw_version_line,
        )
    }
}

/// Shared JSON shape for detected tool identities: `kind`, optional
/// `version`, optional `target`, and `raw_version_line`, in that
/// insertion order.
fn identity_json(
    kind: &str,
    version: Option<&CompilerVersion>,
    target: Option<&str>,
    raw_version_line: &str,
) -> serde_json::Value {
    let mut obj = serde_json::Map::new();
    obj.insert(
        "kind".to_owned(),
        serde_json::Value::String(kind.to_owned()),
    );
    if let Some(v) = version {
        obj.insert(
            "version".to_owned(),
            serde_json::Value::String(v.to_display_string()),
        );
    }
    if let Some(t) = target {
        obj.insert("target".to_owned(), serde_json::Value::String(t.to_owned()));
    }
    obj.insert(
        "raw_version_line".to_owned(),
        serde_json::Value::String(raw_version_line.to_owned()),
    );
    serde_json::Value::Object(obj)
}

/// Detected identity of a static-library archiver.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiverIdentity {
    pub kind: ArchiverKind,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<CompilerVersion>,
    pub raw_version_line: String,
}

impl ArchiverIdentity {
    pub fn unknown(raw_version_line: impl Into<String>) -> Self {
        Self {
            kind: ArchiverKind::Unknown,
            version: None,
            raw_version_line: raw_version_line.into(),
        }
    }

    pub fn as_json(&self) -> serde_json::Value {
        identity_json(
            self.kind.as_key(),
            self.version.as_ref(),
            None,
            &self.raw_version_line,
        )
    }
}
