//! Parser for the `-l` command-line option, which links the generated crate to
//! a native library.
//!
//! (There is also a similar but separate syntax for `#[link]` attributes,
//! which have their own parser in `rustc_metadata`.)

use rustc_feature::UnstableFeatures;
use rustc_hir::attrs::NativeLibKind;

use crate::EarlyDiagCtxt;
use crate::config::UnstableOptions;
use crate::utils::NativeLib;

#[cfg(test)]
mod tests;

/// Parses all `-l` options.
pub(crate) fn parse_native_libs(
    early_dcx: &EarlyDiagCtxt,
    unstable_opts: &UnstableOptions,
    unstable_features: UnstableFeatures,
    matches: &getopts::Matches,
) -> Vec<NativeLib> {
    let cx = ParseNativeLibCx {
        early_dcx,
        unstable_options_enabled: unstable_opts.unstable_options,
        is_nightly: unstable_features.is_nightly_build(),
    };
    matches.opt_strs("l").into_iter().map(|value| parse_native_lib(&cx, &value)).collect()
}

struct ParseNativeLibCx<'a> {
    early_dcx: &'a EarlyDiagCtxt,
    unstable_options_enabled: bool,
    is_nightly: bool,
}

impl ParseNativeLibCx<'_> {
    /// If unstable values are not permitted, exits with a fatal error made by
    /// combining the given strings.
    fn on_unstable_value(&self, message: &str, if_nightly: &str, if_stable: &str) {
        if self.unstable_options_enabled {
            return;
        }

        let suffix = if self.is_nightly { if_nightly } else { if_stable };
        self.early_dcx.early_fatal(format!("{message}{suffix}"));
    }
}

/// Parses the value of a single `-l` option.
fn parse_native_lib(cx: &ParseNativeLibCx<'_>, value: &str) -> NativeLib {
    let NativeLibParts { kind, modifiers, name, new_name } = split_native_lib_value(value);

    let kind = kind.map_or(NativeLibKind::Unspecified, |kind| match kind {
        "static" => {
            NativeLibKind::Static { bundle: None, whole_archive: None, export_symbols: None }
        }
        "dylib" => NativeLibKind::Dylib { as_needed: None },
        "framework" => NativeLibKind::Framework { as_needed: None },
        "link-arg" => {
            cx.on_unstable_value(
                "library kind `link-arg` is unstable",
                ", the `-Z unstable-options` flag must also be passed to use it",
                " and only accepted on the nightly compiler",
            );
            NativeLibKind::LinkArg
        }
        _ => cx.early_dcx.early_fatal(format!(
            "unknown library kind `{kind}`, expected one of: static, dylib, framework, link-arg"
        )),
    });

    // Provisionally create the result, so that modifiers can modify it.
    let mut native_lib = NativeLib {
        name: name.to_owned(),
        new_name: new_name.map(str::to_owned),
        kind,
        verbatim: None,
    };

    if let Some(modifiers) = modifiers {
        // If multiple modifiers are present, they are separated by commas.
        for modifier in modifiers.split(',') {
            parse_and_apply_modifier(cx, modifier, &mut native_lib);
        }
    }

    if native_lib.name.is_empty() {
        cx.early_dcx.early_fatal("library name must not be empty");
    }

    native_lib
}

/// Parses one of the comma-separated modifiers (prefixed by `+` or `-`), and
/// modifies `native_lib` appropriately.
///
/// Exits with a fatal error if a malformed/unknown/inappropriate modifier is
/// found.
fn parse_and_apply_modifier(cx: &ParseNativeLibCx<'_>, modifier: &str, native_lib: &mut NativeLib) {
    let early_dcx = cx.early_dcx;

    // Split off the leading `+` or `-` into a boolean value.
    let (modifier, value) = match modifier.split_at_checked(1) {
        Some(("+", m)) => (m, true),
        Some(("-", m)) => (m, false),
        _ => cx.early_dcx.early_fatal(
            "invalid linking modifier syntax, expected '+' or '-' prefix \
             before one of: bundle, verbatim, whole-archive, as-needed, export-symbols",
        ),
    };

    // Assigns the value (from `+` or `-`) to an empty `Option<bool>`, or emits
    // a fatal error if the option has already been set.
    let assign_modifier = |opt_bool: &mut Option<bool>| {
        if opt_bool.is_some() {
            let msg = format!("multiple `{modifier}` modifiers in a single `-l` option");
            early_dcx.early_fatal(msg)
        }
        *opt_bool = Some(value);
    };

    // Check that the modifier is applicable to the native lib kind, and apply it.
    match (modifier, &mut native_lib.kind) {
        ("bundle", NativeLibKind::Static { bundle, .. }) => assign_modifier(bundle),
        ("bundle", _) => early_dcx
            .early_fatal("linking modifier `bundle` is only compatible with `static` linking kind"),

        ("export-symbols", NativeLibKind::Static { export_symbols, .. }) => {
            assign_modifier(export_symbols)
        }
        ("export-symbols", _) => early_dcx.early_fatal(
            "linking modifier `export-symbols` is only compatible with `static` linking kind",
        ),

        ("verbatim", _) => assign_modifier(&mut native_lib.verbatim),

        ("whole-archive", NativeLibKind::Static { whole_archive, .. }) => {
            assign_modifier(whole_archive)
        }
        ("whole-archive", _) => early_dcx.early_fatal(
            "linking modifier `whole-archive` is only compatible with `static` linking kind",
        ),

        ("as-needed", NativeLibKind::Dylib { as_needed })
        | ("as-needed", NativeLibKind::Framework { as_needed })
        | ("as-needed", NativeLibKind::RawDylib { as_needed }) => {
            cx.on_unstable_value(
                "linking modifier `as-needed` is unstable",
                ", the `-Z unstable-options` flag must also be passed to use it",
                " and only accepted on the nightly compiler",
            );
            assign_modifier(as_needed)
        }
        ("as-needed", _) => early_dcx.early_fatal(
            "linking modifier `as-needed` is only compatible with \
             `dylib` and `framework` linking kinds",
        ),

        _ => early_dcx.early_fatal(format!(
            "unknown linking modifier `{modifier}`, expected one \
             of: bundle, verbatim, whole-archive, as-needed, export-symbols"
        )),
    }
}

#[derive(Debug, PartialEq, Eq)]
struct NativeLibParts<'a> {
    kind: Option<&'a str>,
    modifiers: Option<&'a str>,
    name: &'a str,
    new_name: Option<&'a str>,
}

/// Splits a string of the form `[KIND[:MODIFIERS]=]NAME[:NEW_NAME]` into those
/// individual parts. This cannot fail, but the resulting strings require
/// further validation.
fn split_native_lib_value(value: &str) -> NativeLibParts<'_> {
    // Split the initial value into `[KIND=]NAME`.
    let name = value;
    let (kind, name) = match name.split_once('=') {
        Some((prefix, name)) => (Some(prefix), name),
        None => (None, name),
    };

    // Split the kind part, if present, into `KIND[:MODIFIERS]`.
    let (kind, modifiers) = match kind {
        Some(kind) => match kind.split_once(':') {
            Some((kind, modifiers)) => (Some(kind), Some(modifiers)),
            None => (Some(kind), None),
        },
        None => (None, None),
    };

    // Split the name part into `NAME[:NEW_NAME]`.
    let (name, new_name) = match name.split_once(':') {
        Some((name, new_name)) => (name, Some(new_name)),
        None => (name, None),
    };

    NativeLibParts { kind, modifiers, name, new_name }
}
