use std::backtrace::Backtrace;
use std::borrow::Cow;
use std::fmt;
use std::num::ParseIntError;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;

use rustc_abi::TargetDataLayoutErrors;
use rustc_ast::util::parser::ExprPrecedence;
use rustc_ast_pretty::pprust;
use rustc_macros::Subdiagnostic;
use rustc_span::edition::Edition;
use rustc_span::{Ident, MacroRulesNormalizedIdent, Span, Symbol};
use rustc_target::spec::{PanicStrategy, SplitDebuginfo, StackProtector, TargetTuple};
use rustc_type_ir::{ClosureKind, FloatTy};
use {rustc_ast as ast, rustc_hir as hir};

use crate::diagnostic::DiagLocation;
use crate::{
    Diag, DiagArgValue, DiagCtxtHandle, Diagnostic, EmissionGuarantee, ErrCode, IntoDiagArg, Level,
    SubdiagMessageOp, Subdiagnostic, fluent_generated as fluent,
};

pub struct DiagArgFromDisplay<'a>(pub &'a dyn fmt::Display);

impl IntoDiagArg for DiagArgFromDisplay<'_> {
    fn into_diag_arg(self) -> DiagArgValue {
        self.0.to_string().into_diag_arg()
    }
}

impl<'a> From<&'a dyn fmt::Display> for DiagArgFromDisplay<'a> {
    fn from(t: &'a dyn fmt::Display) -> Self {
        DiagArgFromDisplay(t)
    }
}

impl<'a, T: fmt::Display> From<&'a T> for DiagArgFromDisplay<'a> {
    fn from(t: &'a T) -> Self {
        DiagArgFromDisplay(t)
    }
}

impl<'a, T: Clone + IntoDiagArg> IntoDiagArg for &'a T {
    fn into_diag_arg(self) -> DiagArgValue {
        self.clone().into_diag_arg()
    }
}

#[macro_export]
macro_rules! into_diag_arg_using_display {
    ($( $ty:ty ),+ $(,)?) => {
        $(
            impl IntoDiagArg for $ty {
                fn into_diag_arg(self) -> DiagArgValue {
                    self.to_string().into_diag_arg()
                }
            }
        )+
    }
}

macro_rules! into_diag_arg_for_number {
    ($( $ty:ty ),+ $(,)?) => {
        $(
            impl IntoDiagArg for $ty {
                fn into_diag_arg(self) -> DiagArgValue {
                    // Convert to a string if it won't fit into `Number`.
                    #[allow(irrefutable_let_patterns)]
                    if let Ok(n) = TryInto::<i32>::try_into(self) {
                        DiagArgValue::Number(n)
                    } else {
                        self.to_string().into_diag_arg()
                    }
                }
            }
        )+
    }
}

into_diag_arg_using_display!(
    ast::ParamKindOrd,
    std::io::Error,
    Box<dyn std::error::Error>,
    std::num::NonZero<u32>,
    hir::Target,
    Edition,
    Ident,
    MacroRulesNormalizedIdent,
    ParseIntError,
    StackProtector,
    &TargetTuple,
    SplitDebuginfo,
    ExitStatus,
    ErrCode,
);

impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::TraitRef<I> {
    fn into_diag_arg(self) -> DiagArgValue {
        self.to_string().into_diag_arg()
    }
}

impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::ExistentialTraitRef<I> {
    fn into_diag_arg(self) -> DiagArgValue {
        self.to_string().into_diag_arg()
    }
}

impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::UnevaluatedConst<I> {
    fn into_diag_arg(self) -> rustc_errors::DiagArgValue {
        format!("{self:?}").into_diag_arg()
    }
}

impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::FnSig<I> {
    fn into_diag_arg(self) -> rustc_errors::DiagArgValue {
        format!("{self:?}").into_diag_arg()
    }
}

impl<I: rustc_type_ir::Interner, T> IntoDiagArg for rustc_type_ir::Binder<I, T>
where
    T: IntoDiagArg,
{
    fn into_diag_arg(self) -> DiagArgValue {
        self.skip_binder().into_diag_arg()
    }
}

into_diag_arg_for_number!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize);

impl IntoDiagArg for bool {
    fn into_diag_arg(self) -> DiagArgValue {
        if self {
            DiagArgValue::Str(Cow::Borrowed("true"))
        } else {
            DiagArgValue::Str(Cow::Borrowed("false"))
        }
    }
}

impl IntoDiagArg for char {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(format!("{self:?}")))
    }
}

impl IntoDiagArg for Vec<char> {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::StrListSepByAnd(
            self.into_iter().map(|c| Cow::Owned(format!("{c:?}"))).collect(),
        )
    }
}

impl IntoDiagArg for Symbol {
    fn into_diag_arg(self) -> DiagArgValue {
        self.to_ident_string().into_diag_arg()
    }
}

impl<'a> IntoDiagArg for &'a str {
    fn into_diag_arg(self) -> DiagArgValue {
        self.to_string().into_diag_arg()
    }
}

impl IntoDiagArg for String {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self))
    }
}

impl<'a> IntoDiagArg for Cow<'a, str> {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self.into_owned()))
    }
}

impl<'a> IntoDiagArg for &'a Path {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self.display().to_string()))
    }
}

impl IntoDiagArg for PathBuf {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self.display().to_string()))
    }
}

impl IntoDiagArg for PanicStrategy {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self.desc().to_string()))
    }
}

impl IntoDiagArg for hir::ConstContext {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Borrowed(match self {
            hir::ConstContext::ConstFn => "const_fn",
            hir::ConstContext::Static(_) => "static",
            hir::ConstContext::Const { .. } => "const",
        }))
    }
}

impl IntoDiagArg for ast::Expr {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(pprust::expr_to_string(&self)))
    }
}

impl IntoDiagArg for ast::Path {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(pprust::path_to_string(&self)))
    }
}

impl IntoDiagArg for ast::token::Token {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(pprust::token_to_string(&self))
    }
}

impl IntoDiagArg for ast::token::TokenKind {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(pprust::token_kind_to_string(&self))
    }
}

impl IntoDiagArg for FloatTy {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Borrowed(self.name_str()))
    }
}

impl IntoDiagArg for std::ffi::CString {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self.to_string_lossy().into_owned()))
    }
}

impl IntoDiagArg for rustc_data_structures::small_c_str::SmallCStr {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Owned(self.to_string_lossy().into_owned()))
    }
}

impl IntoDiagArg for ast::Visibility {
    fn into_diag_arg(self) -> DiagArgValue {
        let s = pprust::vis_to_string(&self);
        let s = s.trim_end().to_string();
        DiagArgValue::Str(Cow::Owned(s))
    }
}

impl IntoDiagArg for rustc_lint_defs::Level {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Borrowed(self.to_cmd_flag()))
    }
}

impl<Id> IntoDiagArg for hir::def::Res<Id> {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Borrowed(self.descr()))
    }
}

impl IntoDiagArg for DiagLocation {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::from(self.to_string()))
    }
}

impl IntoDiagArg for Backtrace {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::from(self.to_string()))
    }
}

impl IntoDiagArg for Level {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::from(self.to_string()))
    }
}

impl IntoDiagArg for ClosureKind {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(self.as_str().into())
    }
}

impl IntoDiagArg for hir::def::Namespace {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Str(Cow::Borrowed(self.descr()))
    }
}

impl IntoDiagArg for ExprPrecedence {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::Number(self as i32)
    }
}

#[derive(Clone)]
pub struct DiagSymbolList<S = Symbol>(Vec<S>);

impl<S> From<Vec<S>> for DiagSymbolList<S> {
    fn from(v: Vec<S>) -> Self {
        DiagSymbolList(v)
    }
}

impl<S> FromIterator<S> for DiagSymbolList<S> {
    fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self {
        iter.into_iter().collect::<Vec<_>>().into()
    }
}

impl<S: std::fmt::Display> IntoDiagArg for DiagSymbolList<S> {
    fn into_diag_arg(self) -> DiagArgValue {
        DiagArgValue::StrListSepByAnd(
            self.0.into_iter().map(|sym| Cow::Owned(format!("`{sym}`"))).collect(),
        )
    }
}

impl<G: EmissionGuarantee> Diagnostic<'_, G> for TargetDataLayoutErrors<'_> {
    fn into_diag(self, dcx: DiagCtxtHandle<'_>, level: Level) -> Diag<'_, G> {
        match self {
            TargetDataLayoutErrors::InvalidAddressSpace { addr_space, err, cause } => {
                Diag::new(dcx, level, fluent::errors_target_invalid_address_space)
                    .with_arg("addr_space", addr_space)
                    .with_arg("cause", cause)
                    .with_arg("err", err)
            }
            TargetDataLayoutErrors::InvalidBits { kind, bit, cause, err } => {
                Diag::new(dcx, level, fluent::errors_target_invalid_bits)
                    .with_arg("kind", kind)
                    .with_arg("bit", bit)
                    .with_arg("cause", cause)
                    .with_arg("err", err)
            }
            TargetDataLayoutErrors::MissingAlignment { cause } => {
                Diag::new(dcx, level, fluent::errors_target_missing_alignment)
                    .with_arg("cause", cause)
            }
            TargetDataLayoutErrors::InvalidAlignment { cause, err } => {
                Diag::new(dcx, level, fluent::errors_target_invalid_alignment)
                    .with_arg("cause", cause)
                    .with_arg("err_kind", err.diag_ident())
                    .with_arg("align", err.align())
            }
            TargetDataLayoutErrors::InconsistentTargetArchitecture { dl, target } => {
                Diag::new(dcx, level, fluent::errors_target_inconsistent_architecture)
                    .with_arg("dl", dl)
                    .with_arg("target", target)
            }
            TargetDataLayoutErrors::InconsistentTargetPointerWidth { pointer_size, target } => {
                Diag::new(dcx, level, fluent::errors_target_inconsistent_pointer_width)
                    .with_arg("pointer_size", pointer_size)
                    .with_arg("target", target)
            }
            TargetDataLayoutErrors::InvalidBitsSize { err } => {
                Diag::new(dcx, level, fluent::errors_target_invalid_bits_size).with_arg("err", err)
            }
        }
    }
}

/// Utility struct used to apply a single label while highlighting multiple spans
pub struct SingleLabelManySpans {
    pub spans: Vec<Span>,
    pub label: &'static str,
}
impl Subdiagnostic for SingleLabelManySpans {
    fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>(
        self,
        diag: &mut Diag<'_, G>,
        _: &F,
    ) {
        diag.span_labels(self.spans, self.label);
    }
}

#[derive(Subdiagnostic)]
#[label(errors_expected_lifetime_parameter)]
pub struct ExpectedLifetimeParameter {
    #[primary_span]
    pub span: Span,
    pub count: usize,
}

#[derive(Subdiagnostic)]
#[suggestion(errors_indicate_anonymous_lifetime, code = "{suggestion}", style = "verbose")]
pub struct IndicateAnonymousLifetime {
    #[primary_span]
    pub span: Span,
    pub count: usize,
    pub suggestion: String,
}

#[derive(Subdiagnostic)]
pub struct ElidedLifetimeInPathSubdiag {
    #[subdiagnostic]
    pub expected: ExpectedLifetimeParameter,
    #[subdiagnostic]
    pub indicate: Option<IndicateAnonymousLifetime>,
}
