//! Implementation of "chaining" inlay hints.
use ide_db::famous_defs::FamousDefs;
use span::EditionedFileId;
use syntax::{
    ast::{self, AstNode},
    Direction, NodeOrToken, SyntaxKind, T,
};

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

use super::label_of_ty;

pub(super) fn hints(
    acc: &mut Vec<InlayHint>,
    famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
    config: &InlayHintsConfig,
    file_id: EditionedFileId,
    expr: &ast::Expr,
) -> Option<()> {
    if !config.chaining_hints {
        return None;
    }

    if matches!(expr, ast::Expr::RecordExpr(_)) {
        return None;
    }

    let descended = sema.descend_node_into_attributes(expr.clone()).pop();
    let desc_expr = descended.as_ref().unwrap_or(expr);

    let mut tokens = expr
        .syntax()
        .siblings_with_tokens(Direction::Next)
        .filter_map(NodeOrToken::into_token)
        .filter(|t| match t.kind() {
            SyntaxKind::WHITESPACE if !t.text().contains('\n') => false,
            SyntaxKind::COMMENT => false,
            _ => true,
        });

    // Chaining can be defined as an expression whose next sibling tokens are newline and dot
    // Ignoring extra whitespace and comments
    let next = tokens.next()?.kind();
    if next == SyntaxKind::WHITESPACE {
        let mut next_next = tokens.next()?.kind();
        while next_next == SyntaxKind::WHITESPACE {
            next_next = tokens.next()?.kind();
        }
        if next_next == T![.] {
            let ty = sema.type_of_expr(desc_expr)?.original;
            if ty.is_unknown() {
                return None;
            }
            if matches!(expr, ast::Expr::PathExpr(_)) {
                if let Some(hir::Adt::Struct(st)) = ty.as_adt() {
                    if st.fields(sema.db).is_empty() {
                        return None;
                    }
                }
            }
            let label = label_of_ty(famous_defs, config, &ty, file_id.edition())?;
            acc.push(InlayHint {
                range: expr.syntax().text_range(),
                kind: InlayKind::Chaining,
                label,
                text_edit: None,
                position: InlayHintPosition::After,
                pad_left: true,
                pad_right: false,
                resolve_parent: Some(expr.syntax().text_range()),
            });
        }
    }
    Some(())
}

#[cfg(test)]
mod tests {
    use expect_test::{expect, Expect};
    use ide_db::text_edit::{TextRange, TextSize};

    use crate::{
        fixture,
        inlay_hints::tests::{check_with_config, DISABLED_CONFIG, TEST_CONFIG},
        InlayHintsConfig,
    };

    #[track_caller]
    fn check_chains(ra_fixture: &str) {
        check_with_config(InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG }, ra_fixture);
    }

    #[track_caller]
    pub(super) fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
        let (analysis, file_id) = fixture::file(ra_fixture);
        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
        let filtered =
            inlay_hints.into_iter().map(|hint| (hint.range, hint.label)).collect::<Vec<_>>();
        expect.assert_debug_eq(&filtered)
    }

    #[track_caller]
    pub(super) fn check_expect_clear_loc(
        config: InlayHintsConfig,
        ra_fixture: &str,
        expect: Expect,
    ) {
        let (analysis, file_id) = fixture::file(ra_fixture);
        let mut inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
        inlay_hints.iter_mut().flat_map(|hint| &mut hint.label.parts).for_each(|hint| {
            if let Some(loc) = &mut hint.linked_location {
                loc.range = TextRange::empty(TextSize::from(0));
            }
        });
        let filtered =
            inlay_hints.into_iter().map(|hint| (hint.range, hint.label)).collect::<Vec<_>>();
        expect.assert_debug_eq(&filtered)
    }

    #[test]
    fn chaining_hints_ignore_comments() {
        check_expect(
            InlayHintsConfig { type_hints: false, chaining_hints: true, ..DISABLED_CONFIG },
            r#"
struct A(B);
impl A { fn into_b(self) -> B { self.0 } }
struct B(C);
impl B { fn into_c(self) -> C { self.0 } }
struct C;

fn main() {
    let c = A(B(C))
        .into_b() // This is a comment
        // This is another comment
        .into_c();
}
"#,
            expect![[r#"
                [
                    (
                        147..172,
                        [
                            InlayHintLabelPart {
                                text: "B",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 63..64,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                    (
                        147..154,
                        [
                            InlayHintLabelPart {
                                text: "A",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 7..8,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                ]
            "#]],
        );
    }

    #[test]
    fn chaining_hints_without_newlines() {
        check_chains(
            r#"
struct A(B);
impl A { fn into_b(self) -> B { self.0 } }
struct B(C);
impl B { fn into_c(self) -> C { self.0 } }
struct C;

fn main() {
    let c = A(B(C)).into_b().into_c();
}"#,
        );
    }

    #[test]
    fn disabled_location_links() {
        check_expect(
            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
            r#"
    struct A { pub b: B }
    struct B { pub c: C }
    struct C(pub bool);
    struct D;

    impl D {
        fn foo(&self) -> i32 { 42 }
    }

    fn main() {
        let x = A { b: B { c: C(true) } }
            .b
            .c
            .0;
        let x = D
            .foo();
    }"#,
            expect![[r#"
                [
                    (
                        143..190,
                        [
                            InlayHintLabelPart {
                                text: "C",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 51..52,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                    (
                        143..179,
                        [
                            InlayHintLabelPart {
                                text: "B",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 29..30,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                ]
            "#]],
        );
    }

    #[test]
    fn struct_access_chaining_hints() {
        check_expect(
            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
            r#"
struct A { pub b: B }
struct B { pub c: C }
struct C(pub bool);
struct D;

impl D {
    fn foo(&self) -> i32 { 42 }
}

fn main() {
    let x = A { b: B { c: C(true) } }
        .b
        .c
        .0;
    let x = D
        .foo();
}"#,
            expect![[r#"
                [
                    (
                        143..190,
                        [
                            InlayHintLabelPart {
                                text: "C",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 51..52,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                    (
                        143..179,
                        [
                            InlayHintLabelPart {
                                text: "B",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 29..30,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                ]
            "#]],
        );
    }

    #[test]
    fn generic_chaining_hints() {
        check_expect(
            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
            r#"
struct A<T>(T);
struct B<T>(T);
struct C<T>(T);
struct X<T,R>(T, R);

impl<T> A<T> {
    fn new(t: T) -> Self { A(t) }
    fn into_b(self) -> B<T> { B(self.0) }
}
impl<T> B<T> {
    fn into_c(self) -> C<T> { C(self.0) }
}
fn main() {
    let c = A::new(X(42, true))
        .into_b()
        .into_c();
}
"#,
            expect![[r#"
                [
                    (
                        246..283,
                        [
                            InlayHintLabelPart {
                                text: "B",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 23..24,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<",
                            InlayHintLabelPart {
                                text: "X",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 55..56,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<i32, bool>>",
                        ],
                    ),
                    (
                        246..265,
                        [
                            InlayHintLabelPart {
                                text: "A",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 7..8,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<",
                            InlayHintLabelPart {
                                text: "X",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 55..56,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<i32, bool>>",
                        ],
                    ),
                ]
            "#]],
        );
    }

    #[test]
    fn shorten_iterator_chaining_hints() {
        check_expect_clear_loc(
            InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG },
            r#"
//- minicore: iterators
use core::iter;

struct MyIter;

impl Iterator for MyIter {
    type Item = ();
    fn next(&mut self) -> Option<Self::Item> {
        None
    }
}

fn main() {
    let _x = MyIter.by_ref()
        .take(5)
        .by_ref()
        .take(5)
        .by_ref();
}
"#,
            expect![[r#"
                [
                    (
                        174..241,
                        [
                            "impl ",
                            InlayHintLabelPart {
                                text: "Iterator",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            1,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<",
                            InlayHintLabelPart {
                                text: "Item",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            1,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                            " = ()>",
                        ],
                    ),
                    (
                        174..224,
                        [
                            "impl ",
                            InlayHintLabelPart {
                                text: "Iterator",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            1,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<",
                            InlayHintLabelPart {
                                text: "Item",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            1,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                            " = ()>",
                        ],
                    ),
                    (
                        174..206,
                        [
                            "impl ",
                            InlayHintLabelPart {
                                text: "Iterator",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            1,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                            "<",
                            InlayHintLabelPart {
                                text: "Item",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            1,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                            " = ()>",
                        ],
                    ),
                    (
                        174..189,
                        [
                            "&mut ",
                            InlayHintLabelPart {
                                text: "MyIter",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 0..0,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                ]
            "#]],
        );
    }

    #[test]
    fn hints_in_attr_call() {
        check_expect(
            TEST_CONFIG,
            r#"
//- proc_macros: identity, input_replace
struct Struct;
impl Struct {
    fn chain(self) -> Self {
        self
    }
}
#[proc_macros::identity]
fn main() {
    let strukt = Struct;
    strukt
        .chain()
        .chain()
        .chain();
    Struct::chain(strukt);
}
"#,
            expect![[r#"
                [
                    (
                        124..130,
                        [
                            InlayHintLabelPart {
                                text: "Struct",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 7..13,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                    (
                        145..185,
                        [
                            InlayHintLabelPart {
                                text: "Struct",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 7..13,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                    (
                        145..168,
                        [
                            InlayHintLabelPart {
                                text: "Struct",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 7..13,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                    (
                        222..228,
                        [
                            InlayHintLabelPart {
                                text: "self",
                                linked_location: Some(
                                    FileRangeWrapper {
                                        file_id: FileId(
                                            0,
                                        ),
                                        range: 42..46,
                                    },
                                ),
                                tooltip: "",
                            },
                        ],
                    ),
                ]
            "#]],
        );
    }
}
