//! Utilities for formatting macro expanded nodes until we get a proper formatter.
use syntax::{
    NodeOrToken,
    SyntaxKind::{self, *},
    SyntaxNode, SyntaxToken, T, WalkEvent,
    ast::syntax_factory::SyntaxFactory,
    syntax_editor::{Position, SyntaxEditor},
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrettifyWsKind {
    Space,
    Indent(usize),
    Newline,
}

/// Renders a [`SyntaxNode`] with whitespace inserted between tokens that require them.
///
/// This is an internal API that is only exported because `mbe` needs it for tests and cannot depend
/// on `hir-expand`. For any purpose other than tests, you are supposed to use the `prettify_macro_expansion`
/// from `hir-expand` that handles `$crate` for you.
#[deprecated = "use `hir_expand::prettify_macro_expansion()` instead"]
pub fn prettify_macro_expansion(
    syn: SyntaxNode,
    dollar_crate_replacement: &mut dyn FnMut(&SyntaxToken, &SyntaxFactory) -> Option<SyntaxToken>,
    inspect_mods: impl FnOnce(&[(Position, PrettifyWsKind)]),
) -> SyntaxNode {
    let mut indent = 0;
    let mut last: Option<SyntaxKind> = None;
    let mut mods = Vec::new();
    let mut dollar_crate_replacements = Vec::new();
    let (editor, syn) = SyntaxEditor::new(syn);

    let before = Position::before;
    let after = Position::after;

    let do_indent = |pos: fn(_) -> Position, token: &SyntaxToken, indent| {
        (pos(token.clone()), PrettifyWsKind::Indent(indent))
    };
    let do_ws =
        |pos: fn(_) -> Position, token: &SyntaxToken| (pos(token.clone()), PrettifyWsKind::Space);
    let do_nl =
        |pos: fn(_) -> Position, token: &SyntaxToken| (pos(token.clone()), PrettifyWsKind::Newline);

    for event in syn.preorder_with_tokens() {
        let token = match event {
            WalkEvent::Enter(NodeOrToken::Token(token)) => token,
            WalkEvent::Leave(NodeOrToken::Node(node)) => {
                let is_last_child =
                    node.parent().is_some_and(|parent| parent.last_child().as_ref() == Some(&node));
                let is_always_newline = matches!(node.kind(), ATTR);
                let is_non_last_newline = match node.kind() {
                    MATCH_ARM | STRUCT | ENUM | UNION | FN | IMPL | MACRO_RULES | EXTERN_BLOCK
                    | EXTERN_CRATE | MODULE => true,
                    EXPR_STMT if Some(R_CURLY) == node.last_token().map(|it| it.kind()) => true,
                    _ => false,
                };
                if (!is_last_child && is_non_last_newline) || is_always_newline {
                    mods.push((Position::after(node.clone()), PrettifyWsKind::Indent(indent)));
                    if node.parent().is_some() {
                        mods.push((Position::after(node), PrettifyWsKind::Newline));
                    }
                }
                continue;
            }
            _ => continue,
        };
        if token.kind() == SyntaxKind::IDENT
            && token.text() == "$crate"
            && let Some(replacement) = dollar_crate_replacement(&token, editor.make())
        {
            dollar_crate_replacements.push((token.clone(), replacement));
        }
        let tok = &token;

        let is_next = |f: fn(SyntaxKind) -> bool, default| -> bool {
            tok.next_token().map(|it| f(it.kind())).unwrap_or(default)
        };
        let is_last =
            |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) };

        match tok.kind() {
            k if is_text(k)
                && is_next(|it| !it.is_punct() || matches!(it, T![_] | T![#] | L_CURLY), false) =>
            {
                mods.push(do_ws(after, tok));
            }
            L_CURLY if is_next(|it| it != R_CURLY, true) => {
                indent += 1;
                mods.push(do_indent(after, tok, indent));
                mods.push(do_nl(after, tok));
            }
            R_CURLY if is_last(|it| it != L_CURLY, true) => {
                indent = indent.saturating_sub(1);

                mods.push(do_indent(before, tok, indent));
                mods.push(do_nl(before, tok));
            }
            R_CURLY if is_next(|it| it == T![else], false) => {
                mods.push(do_indent(before, tok, indent));
                mods.push(do_nl(before, tok));
            }
            LIFETIME_IDENT if is_next(is_text, true) => {
                mods.push(do_ws(after, tok));
            }
            AS_KW | DYN_KW | IMPL_KW | CONST_KW | MUT_KW | LET_KW | MATCH_KW => {
                mods.push(do_ws(after, tok));
            }
            T![;] if is_next(|it| it != R_CURLY, true) => {
                mods.push(do_indent(after, tok, indent));
                if tok.text_range().end() != syn.text_range().end() {
                    mods.push(do_nl(after, tok));
                }
            }
            T![=] if let Some((last, next)) = last.zip(tok.next_token()) => {
                // FIXME: this branch is for `=>` in macro_rules!, which is currently parsed as
                // two separate symbols.
                match (last, next.kind()) {
                    (T![=], _) | (_, T![=]) => (),
                    // catch ..= += etc
                    #[rustfmt::skip]
                    (
                        T![!] | T![%] | T![&] | T![*] | T![+] | T![-] |
                        T![/] | T![<] | T![>] | T![^] | T![|] | T![.],
                        _,
                    ) => (),
                    (_, T![>]) => {
                        mods.push(do_ws(before, tok));
                        mods.push(do_ws(after, &next));
                    }
                    _ => {
                        mods.push(do_ws(before, tok));
                        mods.push(do_ws(after, tok));
                    }
                }
            }
            T![->] | T![=>] => {
                mods.push(do_ws(before, tok));
                mods.push(do_ws(after, tok));
            }
            T![:] if is_next(|it| it != T![:], false) && is_last(|it| it != T![:], false) => {
                // XXX: Why input included WHITESPACE?
                if is_next(|it| it != SyntaxKind::WHITESPACE, false) {
                    mods.push(do_ws(after, tok));
                }
            }
            T![!] if is_last(|it| it == MACRO_RULES_KW, false) && is_next(is_text, false) => {
                mods.push(do_ws(after, tok));
            }
            _ => (),
        }

        last = Some(tok.kind());
    }

    inspect_mods(&mods);
    for (pos, insert) in mods {
        editor.insert(
            pos,
            match insert {
                PrettifyWsKind::Space => editor.make().whitespace(" "),
                PrettifyWsKind::Indent(0) => continue,
                PrettifyWsKind::Indent(indent) => editor.make().whitespace(&" ".repeat(4 * indent)),
                PrettifyWsKind::Newline => editor.make().whitespace("\n"),
            },
        );
    }
    for (old, new) in dollar_crate_replacements {
        editor.replace(old, new);
    }

    if let Some(it) = syn.last_token().filter(|it| it.kind() == SyntaxKind::WHITESPACE) {
        editor.delete(it);
    }

    editor.finish().new_root().clone()
}

fn is_text(k: SyntaxKind) -> bool {
    // Consider all keywords in all editions.
    k.is_any_identifier() || k.is_literal() || k == UNDERSCORE
}

#[cfg(test)]
mod tests {
    use super::*;
    use expect_test::{Expect, expect};

    #[expect(deprecated)]
    fn check_pretty(#[rust_analyzer::rust_fixture] ra_fixture: &str, expect: Expect) {
        let ra_fixture = stdx::trim_indent(ra_fixture);
        let source_file = syntax::ast::SourceFile::parse(&ra_fixture, span::Edition::CURRENT);
        let syn = remove_whitespaces(&source_file.syntax_node());

        let pretty = prettify_macro_expansion(syn, &mut |_, _| None, |_| ());
        let mut pretty = pretty.to_string();
        if pretty.contains('\n') {
            pretty.push('\n');
        }
        expect.assert_eq(&pretty);

        fn remove_whitespaces(node: &SyntaxNode) -> SyntaxNode {
            let (editor, node) = SyntaxEditor::new(node.clone());
            node.preorder_with_tokens().for_each(|it| match it {
                WalkEvent::Enter(NodeOrToken::Token(tok)) if tok.kind().is_trivia() => {
                    editor.delete(tok);
                }
                _ => (),
            });
            editor.finish().new_root().clone()
        }
    }

    #[test]
    fn test_in_macro() {
        check_pretty(
            r#"
            const X: i32 = x::y::z;
            macro_rules! foo {
                () => {
                    $crate::foo::bar!();
                    (1..2, 1..=2);
                    (a==b, a!=b, a<=b, a>=b, x+=2, x<<=2);
                };
            }
            "#,
            expect![[r#"
                const X: i32 = x::y::z;
                macro_rules! foo {
                    () => {
                        $crate::foo::bar!();
                        (1..2,1..=2);
                        (a==b,a!=b,a<=b,a>=b,x+=2,x<<=2);
                    };
                }
            "#]],
        );
    }

    #[test]
    fn test_curly_indent() {
        check_pretty(
            r#"
            const _: () = {
                {
                    2;
                    3
                }
            };
            "#,
            expect![[r#"
                const _: () = {
                    {
                        2;
                        3
                    }
                };
            "#]],
        );
    }

    #[test]
    fn test_pats() {
        check_pretty(
            r#"
            const _: () = {
                let x = 2;
                let mut y = 3;
                let ref mut z @ 0..5 = 4;
                let ref mut t @ 0..=5 = 4;
                let (x, ref y) = (5, 6);
                let (Foo { x, y }, Bar(z, t));
                let (&mut x, (y | y));
                match () {}
            };
            "#,
            expect![[r#"
                const _: () = {
                    let x = 2;
                    let mut y = 3;
                    let ref mut z@0..5 = 4;
                    let ref mut t@0..=5 = 4;
                    let (x,ref y) = (5,6);
                    let (Foo {
                        x,y
                    },Bar(z,t));
                    let (&mut x,(y|y));
                    match (){}
                };
            "#]],
        );
    }

    #[test]
    fn test_attrs() {
        check_pretty(
            r#"
            #[attr1]
            #[attr2]
            const _: () = {};
            #[attr1]
            const _: () = {
                #[attr2]
                {}
            };
            "#,
            expect![[r#"
                #[attr1]
                #[attr2]
                const _: () = {};
                #[attr1]
                const _: () = {
                    #[attr2]
                    {}
                };
            "#]],
        );
    }

    #[test]
    fn test_items() {
        check_pretty(
            r#"
            fn foo() {}
            struct Foo {}
            struct Foo;
            enum Foo {}
            impl Foo {}
            const _: () = {};
            static S: () = {};
            extern {}
            mod x {}
            mod x;
            type X = 2;
            use a;
            use b::{c, d};
            macro_rules! foo { () => {}; }
            "#,
            expect![[r#"
                fn foo(){}
                struct Foo {}
                struct Foo;

                enum Foo {}
                impl Foo {}
                const _: () = {};
                static S: () = {};
                extern {}
                mod x {}
                mod x;

                type X = 2;
                use a;
                use b::{
                    c,d
                };
                macro_rules! foo {
                    () => {};
                }
            "#]],
        );
    }

    #[test]
    fn test_exprs() {
        check_pretty(
            r#"
            const _: () = {
                let _ = 1+2;
                let _ = !true && false;
                let _ = foo() + !bar() + dbg!(2) + *x;
                let _ = async move || {};
                let _ = async move {};
                let _ = x.await;
                let _ = (1..2, 1..=2);
                'lab: for _ in 0..5 {
                    loop { }
                    break 'lab expr;
                    if let pat = expr {
                        foo()
                    } else if true {
                        bar()
                    } else {}
                    if true {} else if true {} else {}
                    fun()
                }
            };
            "#,
            expect![[r#"
                const _: () = {
                    let _ = 1+2;
                    let _ = !true&&false;
                    let _ = foo()+!bar()+dbg!(2)+*x;
                    let _ = async move||{};
                    let _ = async move {};
                    let _ = x.await;
                    let _ = (1..2,1..=2);
                    'lab: for _ in 0..5 {
                        loop {}
                        break 'lab expr;
                        if let pat = expr {
                            foo()
                        }else if true {
                            bar()
                        }else {}
                        if true {
                        }else if true {
                        }else {}
                        fun()
                    }
                };
            "#]],
        );
    }

    #[test]
    fn test_match_arm() {
        check_pretty(
            r#"
            const _: () = {
                match 2 {
                    tmp => foo!(),
                };
            };
            "#,
            expect![[r#"
                const _: () = {
                    match 2 {
                        tmp => foo!(),
                    };
                };
            "#]],
        );

        check_pretty(
            r#"
            const _: () = {
                match 2 {
                    tmp => {}
                };
            };
            "#,
            expect![[r#"
                const _: () = {
                    match 2 {
                        tmp => {}
                    };
                };
            "#]],
        );

        check_pretty(
            r#"
            const _: () = {
                match 2 {
                    1 => {}
                    2 => foo(),
                    _ => {},
                };
            };
            "#,
            expect![[r#"
                const _: () = {
                    match 2 {
                        1 => {}
                        2 => foo(),
                        _ => {},
                    };
                };
            "#]],
        );
    }
}
