//! Implementation of trait bound hints.
//!
//! Currently this renders the implied `Sized` bound.
use either::Either;
use ide_db::{famous_defs::FamousDefs, text_edit::TextEdit};

use syntax::ast::{self, AstNode};

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

pub(super) fn hints(
    acc: &mut Vec<InlayHint>,
    FamousDefs(sema, _): &FamousDefs<'_, '_>,
    config: &InlayHintsConfig<'_>,
    path: Either<ast::PathType, ast::DynTraitType>,
) -> Option<()> {
    if !config.implied_dyn_trait_hints {
        return None;
    }

    let parent = path.syntax().parent()?;
    let range = match path {
        Either::Left(path) => {
            let paren = parent
                .ancestors()
                .take_while(|it| {
                    ast::ParenType::can_cast(it.kind()) || ast::ForType::can_cast(it.kind())
                })
                .last();
            let parent = paren.as_ref().and_then(|it| it.parent()).unwrap_or(parent);
            if ast::TypeBound::can_cast(parent.kind())
                || ast::TypeAnchor::can_cast(parent.kind())
                || ast::Impl::cast(parent).is_some_and(|it| {
                    it.trait_().map_or(
                        // only show it for impl type if the impl is not incomplete, otherwise we
                        // are likely typing a trait impl
                        it.assoc_item_list().is_none_or(|it| it.l_curly_token().is_none()),
                        |trait_| trait_.syntax() == path.syntax(),
                    )
                })
            {
                return None;
            }
            sema.resolve_trait(&path.path()?)?;
            path.syntax().text_range()
        }
        Either::Right(dyn_) => {
            if dyn_.dyn_token().is_some() {
                return None;
            }

            dyn_.syntax().text_range()
        }
    };

    acc.push(InlayHint {
        range,
        kind: InlayKind::Dyn,
        label: InlayHintLabel::simple("dyn", None, None),
        text_edit: Some(
            config.lazy_text_edit(|| TextEdit::insert(range.start(), "dyn ".to_owned())),
        ),
        position: InlayHintPosition::Before,
        pad_left: false,
        pad_right: true,
        resolve_parent: Some(range),
    });

    Some(())
}

#[cfg(test)]
mod tests {

    use expect_test::expect;

    use crate::inlay_hints::InlayHintsConfig;

    use crate::inlay_hints::tests::{DISABLED_CONFIG, check_edit, check_with_config};

    #[track_caller]
    fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
        check_with_config(
            InlayHintsConfig {
                sized_bound: true,
                implied_dyn_trait_hints: true,
                ..DISABLED_CONFIG
            },
            ra_fixture,
        );
    }

    #[test]
    fn path_works() {
        check(
            r#"
struct S {}
trait T {}
fn foo(_: T,  _: dyn T, _: S) {}
       // ^ dyn
fn foo(_: &T,  _: for<'a> T) {}
        // ^ dyn
                       // ^ dyn
impl T {}
  // ^ dyn
impl T for (T) {}
         // ^ dyn
impl T for {}
impl T
"#,
        );
    }

    #[test]
    fn missing_dyn_bounds() {
        check(
            r#"
trait T {}
fn foo(
    _: T + T,
    // ^^^^^ dyn
    _: T + 'a,
    // ^^^^^^ dyn
    _: 'a + T,
    // ^^^^^^ dyn
    _: &(T + T)
    //   ^^^^^ dyn
    _: &mut (T + T)
    //       ^^^^^ dyn
    _: *mut (T),
    //       ^ dyn
) {}
"#,
        );
    }

    #[test]
    fn edit() {
        check_edit(
            InlayHintsConfig { implied_dyn_trait_hints: true, ..DISABLED_CONFIG },
            r#"
trait T {}
fn foo(
    _: &mut T
) {}
"#,
            expect![[r#"
                trait T {}
                fn foo(
                    _: &mut dyn T
                ) {}
            "#]],
        );
    }

    #[test]
    fn hrtb_bound_does_not_add_dyn() {
        check(
            r#"
//- minicore: fn
fn test<F>(f: F) where F: for<'a> FnOnce(&'a i32) {}
     // ^: Sized
        "#,
        );
    }

    #[test]
    fn with_parentheses() {
        check(
            r#"
trait T {}
fn foo(v: &(T)) {}
         // ^ dyn
        "#,
        );
    }
}
