use rustc_errors::{Diag, EmissionGuarantee, IntoDiagArg, Subdiagnostic, msg};
use rustc_hir::def_id::LocalDefId;
use rustc_middle::bug;
use rustc_middle::ty::{self, TyCtxt};
use rustc_span::{Span, kw};

use crate::error_reporting::infer::nice_region_error::find_anon_type;

struct DescriptionCtx<'a> {
    span: Option<Span>,
    kind: &'a str,
    arg: String,
}

impl<'a> DescriptionCtx<'a> {
    fn new<'tcx>(
        tcx: TyCtxt<'tcx>,
        generic_param_scope: LocalDefId,
        region: ty::Region<'tcx>,
        alt_span: Option<Span>,
    ) -> Option<Self> {
        let (span, kind, arg) = match region.kind() {
            ty::ReEarlyParam(br) => {
                let scope = tcx
                    .parent(tcx.generics_of(generic_param_scope).region_param(br, tcx).def_id)
                    .expect_local();
                let span = if let Some(param) =
                    tcx.hir_get_generics(scope).and_then(|generics| generics.get_named(br.name))
                {
                    param.span
                } else {
                    tcx.def_span(scope)
                };
                if br.is_named() {
                    (Some(span), "as_defined", br.name.to_string())
                } else {
                    (Some(span), "as_defined_anon", String::new())
                }
            }
            ty::ReLateParam(ref fr) => {
                if !fr.kind.is_named(tcx)
                    && let Some((ty, _)) = find_anon_type(tcx, generic_param_scope, region)
                {
                    (Some(ty.span), "defined_here", String::new())
                } else {
                    let scope = fr.scope.expect_local();
                    match fr.kind {
                        ty::LateParamRegionKind::Named(def_id) => {
                            let name = tcx.item_name(def_id);
                            let span = if let Some(param) = tcx
                                .hir_get_generics(scope)
                                .and_then(|generics| generics.get_named(name))
                            {
                                param.span
                            } else {
                                tcx.def_span(scope)
                            };
                            if name == kw::UnderscoreLifetime {
                                (Some(span), "as_defined_anon", String::new())
                            } else {
                                (Some(span), "as_defined", name.to_string())
                            }
                        }
                        ty::LateParamRegionKind::Anon(_) => {
                            let span = Some(tcx.def_span(scope));
                            (span, "defined_here", String::new())
                        }
                        _ => (Some(tcx.def_span(scope)), "defined_here_reg", region.to_string()),
                    }
                }
            }

            ty::ReStatic => (alt_span, "restatic", String::new()),

            ty::RePlaceholder(_) | ty::ReError(_) => return None,

            ty::ReVar(_) | ty::ReBound(..) | ty::ReErased => {
                bug!("unexpected region for DescriptionCtx: {:?}", region);
            }
        };
        Some(DescriptionCtx { span, kind, arg })
    }
}

pub enum PrefixKind {
    Empty,
    RefValidFor,
    ContentValidFor,
    TypeObjValidFor,
    SourcePointerValidFor,
    TypeSatisfy,
    TypeOutlive,
    LfParamInstantiatedWith,
    LfParamMustOutlive,
    LfInstantiatedWith,
    LfMustOutlive,
    PointerValidFor,
    DataValidFor,
}

pub enum SuffixKind {
    Empty,
    Continues,
    ReqByBinding,
}

impl IntoDiagArg for PrefixKind {
    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> rustc_errors::DiagArgValue {
        let kind = match self {
            Self::Empty => "empty",
            Self::RefValidFor => "ref_valid_for",
            Self::ContentValidFor => "content_valid_for",
            Self::TypeObjValidFor => "type_obj_valid_for",
            Self::SourcePointerValidFor => "source_pointer_valid_for",
            Self::TypeSatisfy => "type_satisfy",
            Self::TypeOutlive => "type_outlive",
            Self::LfParamInstantiatedWith => "lf_param_instantiated_with",
            Self::LfParamMustOutlive => "lf_param_must_outlive",
            Self::LfInstantiatedWith => "lf_instantiated_with",
            Self::LfMustOutlive => "lf_must_outlive",
            Self::PointerValidFor => "pointer_valid_for",
            Self::DataValidFor => "data_valid_for",
        }
        .into();
        rustc_errors::DiagArgValue::Str(kind)
    }
}

impl IntoDiagArg for SuffixKind {
    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> rustc_errors::DiagArgValue {
        let kind = match self {
            Self::Empty => "empty",
            Self::Continues => "continues",
            Self::ReqByBinding => "req_by_binding",
        }
        .into();
        rustc_errors::DiagArgValue::Str(kind)
    }
}

pub struct RegionExplanation<'a> {
    desc: DescriptionCtx<'a>,
    prefix: PrefixKind,
    suffix: SuffixKind,
}

impl RegionExplanation<'_> {
    pub fn new<'tcx>(
        tcx: TyCtxt<'tcx>,
        generic_param_scope: LocalDefId,
        region: ty::Region<'tcx>,
        alt_span: Option<Span>,
        prefix: PrefixKind,
        suffix: SuffixKind,
    ) -> Option<Self> {
        Some(Self {
            desc: DescriptionCtx::new(tcx, generic_param_scope, region, alt_span)?,
            prefix,
            suffix,
        })
    }
}

impl Subdiagnostic for RegionExplanation<'_> {
    fn add_to_diag<G: EmissionGuarantee>(self, diag: &mut Diag<'_, G>) {
        diag.store_args();
        diag.arg("pref_kind", self.prefix);
        diag.arg("suff_kind", self.suffix);
        diag.arg("desc_kind", self.desc.kind);
        diag.arg("desc_arg", self.desc.arg);

        let msg = diag.eagerly_translate(msg!(
            "{$pref_kind ->
                *[should_not_happen] [{$pref_kind}]
                [ref_valid_for] ...the reference is valid for
                [content_valid_for] ...but the borrowed content is only valid for
                [type_obj_valid_for] object type is valid for
                [source_pointer_valid_for] source pointer is only valid for
                [type_satisfy] type must satisfy
                [type_outlive] type must outlive
                [lf_param_instantiated_with] lifetime parameter instantiated with
                [lf_param_must_outlive] but lifetime parameter must outlive
                [lf_instantiated_with] lifetime instantiated with
                [lf_must_outlive] but lifetime must outlive
                [pointer_valid_for] the pointer is valid for
                [data_valid_for] but the referenced data is only valid for
                [empty] {\"\"}
            }{$pref_kind ->
                [empty] {\"\"}
                *[other] {\" \"}
            }{$desc_kind ->
                *[should_not_happen] [{$desc_kind}]
                [restatic] the static lifetime
                [revar] lifetime {$desc_arg}
                [as_defined] the lifetime `{$desc_arg}` as defined here
                [as_defined_anon] the anonymous lifetime as defined here
                [defined_here] the anonymous lifetime defined here
                [defined_here_reg] the lifetime `{$desc_arg}` as defined here
                }{$suff_kind ->
                *[should_not_happen] [{$suff_kind}]
                [empty]{\"\"}
                [continues] ...
                [req_by_binding] {\" \"}as required by this binding
            }"
        ));
        diag.restore_args();
        if let Some(span) = self.desc.span {
            diag.span_note(span, msg);
        } else {
            diag.note(msg);
        }
    }
}
