//! Implementation of "enum variant discriminant" inlay hints:
//! ```no_run
//! enum Foo {
//!    Bar/* = 0*/,
//! }
//! ```
use hir::Semantics;
use ide_db::text_edit::TextEdit;
use ide_db::{famous_defs::FamousDefs, RootDatabase};
use span::EditionedFileId;
use syntax::ast::{self, AstNode, HasName};

use crate::{
    DiscriminantHints, InlayHint, InlayHintLabel, InlayHintPosition, InlayHintsConfig, InlayKind,
    InlayTooltip,
};

pub(super) fn enum_hints(
    acc: &mut Vec<InlayHint>,
    FamousDefs(sema, _): &FamousDefs<'_, '_>,
    config: &InlayHintsConfig,
    _: EditionedFileId,
    enum_: ast::Enum,
) -> Option<()> {
    if let DiscriminantHints::Never = config.discriminant_hints {
        return None;
    }

    let def = sema.to_def(&enum_)?;
    let data_carrying = def.is_data_carrying(sema.db);
    if matches!(config.discriminant_hints, DiscriminantHints::Fieldless) && data_carrying {
        return None;
    }
    // data carrying enums without a primitive repr have no stable discriminants
    if data_carrying && def.repr(sema.db).map_or(true, |r| r.int.is_none()) {
        return None;
    }
    for variant in enum_.variant_list()?.variants() {
        variant_hints(acc, sema, &enum_, &variant);
    }
    Some(())
}

fn variant_hints(
    acc: &mut Vec<InlayHint>,
    sema: &Semantics<'_, RootDatabase>,
    enum_: &ast::Enum,
    variant: &ast::Variant,
) -> Option<()> {
    if variant.expr().is_some() {
        return None;
    }

    let eq_token = variant.eq_token();
    let name = variant.name()?;

    let descended = sema.descend_node_into_attributes(variant.clone()).pop();
    let desc_pat = descended.as_ref().unwrap_or(variant);
    let v = sema.to_def(desc_pat)?;
    let d = v.eval(sema.db);

    let range = match variant.field_list() {
        Some(field_list) => name.syntax().text_range().cover(field_list.syntax().text_range()),
        None => name.syntax().text_range(),
    };
    let eq_ = if eq_token.is_none() { " =" } else { "" };
    let label = InlayHintLabel::simple(
        match d {
            Ok(val) => {
                if val >= 10 {
                    format!("{eq_} {val} ({val:#X})")
                } else {
                    format!("{eq_} {val}")
                }
            }
            Err(_) => format!("{eq_} ?"),
        },
        Some(InlayTooltip::String(match &d {
            Ok(_) => "enum variant discriminant".into(),
            Err(e) => format!("{e:?}"),
        })),
        None,
    );
    acc.push(InlayHint {
        range: match eq_token {
            Some(t) => range.cover(t.text_range()),
            _ => range,
        },
        kind: InlayKind::Discriminant,
        label,
        text_edit: d.ok().map(|val| TextEdit::insert(range.start(), format!("{eq_} {val}"))),
        position: InlayHintPosition::After,
        pad_left: false,
        pad_right: false,
        resolve_parent: Some(enum_.syntax().text_range()),
    });

    Some(())
}
#[cfg(test)]
mod tests {
    use crate::inlay_hints::{
        tests::{check_with_config, DISABLED_CONFIG},
        DiscriminantHints, InlayHintsConfig,
    };

    #[track_caller]
    fn check_discriminants(ra_fixture: &str) {
        check_with_config(
            InlayHintsConfig { discriminant_hints: DiscriminantHints::Always, ..DISABLED_CONFIG },
            ra_fixture,
        );
    }

    #[track_caller]
    fn check_discriminants_fieldless(ra_fixture: &str) {
        check_with_config(
            InlayHintsConfig {
                discriminant_hints: DiscriminantHints::Fieldless,
                ..DISABLED_CONFIG
            },
            ra_fixture,
        );
    }

    #[test]
    fn fieldless() {
        check_discriminants(
            r#"
enum Enum {
    Variant,
//  ^^^^^^^ = 0$
    Variant1,
//  ^^^^^^^^ = 1$
    Variant2,
//  ^^^^^^^^ = 2$
    Variant5 = 5,
    Variant6,
//  ^^^^^^^^ = 6$
}
"#,
        );
        check_discriminants_fieldless(
            r#"
enum Enum {
    Variant,
//  ^^^^^^^ = 0
    Variant1,
//  ^^^^^^^^ = 1
    Variant2,
//  ^^^^^^^^ = 2
    Variant5 = 5,
    Variant6,
//  ^^^^^^^^ = 6
}
"#,
        );
    }

    #[test]
    fn datacarrying_mixed() {
        check_discriminants(
            r#"
#[repr(u8)]
enum Enum {
    Variant(),
//  ^^^^^^^^^ = 0
    Variant1,
//  ^^^^^^^^ = 1
    Variant2 {},
//  ^^^^^^^^^^^ = 2
    Variant3,
//  ^^^^^^^^ = 3
    Variant5 = 5,
    Variant6,
//  ^^^^^^^^ = 6
}
"#,
        );
        check_discriminants(
            r#"
enum Enum {
    Variant(),
    Variant1,
    Variant2 {},
    Variant3,
    Variant5,
    Variant6,
}
"#,
        );
    }

    #[test]
    fn datacarrying_mixed_fieldless_set() {
        check_discriminants_fieldless(
            r#"
#[repr(u8)]
enum Enum {
    Variant(),
    Variant1,
    Variant2 {},
    Variant3,
    Variant5,
    Variant6,
}
"#,
        );
    }
}
