use crate::snippet::Style;
use crate::{DiagnosticArg, DiagnosticMessage, FluentBundle};
use rustc_data_structures::sync::Lrc;
use rustc_error_messages::{
    fluent_bundle::resolver::errors::{ReferenceKind, ResolverError},
    FluentArgs, FluentError,
};
use std::borrow::Cow;

/// Convert diagnostic arguments (a rustc internal type that exists to implement
/// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation.
///
/// Typically performed once for each diagnostic at the start of `emit_diagnostic` and then
/// passed around as a reference thereafter.
pub fn to_fluent_args<'iter, 'arg: 'iter>(
    iter: impl Iterator<Item = DiagnosticArg<'iter, 'arg>>,
) -> FluentArgs<'arg> {
    let mut args = if let Some(size) = iter.size_hint().1 {
        FluentArgs::with_capacity(size)
    } else {
        FluentArgs::new()
    };

    for (k, v) in iter {
        args.set(k.clone(), v.clone());
    }

    args
}

pub trait Translate {
    /// Return `FluentBundle` with localized diagnostics for the locale requested by the user. If no
    /// language was requested by the user then this will be `None` and `fallback_fluent_bundle`
    /// should be used.
    fn fluent_bundle(&self) -> Option<&Lrc<FluentBundle>>;

    /// Return `FluentBundle` with localized diagnostics for the default locale of the compiler.
    /// Used when the user has not requested a specific language or when a localized diagnostic is
    /// unavailable for the requested locale.
    fn fallback_fluent_bundle(&self) -> &FluentBundle;

    /// Convert `DiagnosticMessage`s to a string, performing translation if necessary.
    fn translate_messages(
        &self,
        messages: &[(DiagnosticMessage, Style)],
        args: &FluentArgs<'_>,
    ) -> Cow<'_, str> {
        Cow::Owned(
            messages.iter().map(|(m, _)| self.translate_message(m, args)).collect::<String>(),
        )
    }

    /// Convert a `DiagnosticMessage` to a string, performing translation if necessary.
    fn translate_message<'a>(
        &'a self,
        message: &'a DiagnosticMessage,
        args: &'a FluentArgs<'_>,
    ) -> Cow<'_, str> {
        trace!(?message, ?args);
        let (identifier, attr) = match message {
            DiagnosticMessage::Str(msg) | DiagnosticMessage::Eager(msg) => {
                return Cow::Borrowed(msg);
            }
            DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr),
        };

        let translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> {
            let message = bundle.get_message(identifier)?;
            let value = match attr {
                Some(attr) => message.get_attribute(attr)?.value(),
                None => message.value()?,
            };
            debug!(?message, ?value);

            let mut errs = vec![];
            let translated = bundle.format_pattern(value, Some(args), &mut errs);
            debug!(?translated, ?errs);
            Some((translated, errs))
        };

        self.fluent_bundle()
            .and_then(|bundle| translate_with_bundle(bundle))
            // If `translate_with_bundle` returns `None` with the primary bundle, this is likely
            // just that the primary bundle doesn't contain the message being translated, so
            // proceed to the fallback bundle.
            //
            // However, when errors are produced from translation, then that means the translation
            // is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided).
            //
            // In debug builds, assert so that compiler devs can spot the broken translation and
            // fix it..
            .inspect(|(_, errs)| {
                debug_assert!(
                    errs.is_empty(),
                    "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}",
                    identifier,
                    attr,
                    args,
                    errs
                );
            })
            // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so
            // just hide it and try with the fallback bundle.
            .filter(|(_, errs)| errs.is_empty())
            .or_else(|| translate_with_bundle(self.fallback_fluent_bundle()))
            .map(|(translated, errs)| {
                // Always bail out for errors with the fallback bundle.

                let mut help_messages = vec![];

                if !errs.is_empty() {
                    for error in &errs {
                        match error {
                            FluentError::ResolverError(ResolverError::Reference(
                                ReferenceKind::Message { id, .. },
                            )) if args.iter().any(|(arg_id, _)| arg_id == id) => {
                                help_messages.push(format!("Argument `{id}` exists but was not referenced correctly. Try using `{{${id}}}` instead"));
                            }
                            _ => {}
                        }
                    }

                    panic!(
                        "Encountered errors while formatting message for `{identifier}`\n\
                        help: {}\n\
                        attr: `{attr:?}`\n\
                        args: `{args:?}`\n\
                        errors: `{errs:?}`",
                        help_messages.join("\nhelp: ")
                    );
                }

                translated
            })
            .expect("failed to find message in primary or fallback fluent bundles")
    }
}
