use syntax::{
    AstNode, SyntaxElement, SyntaxKind, SyntaxNode, T,
    ast::{
        self,
        edit::{AstNodeEdit, IndentLevel},
        make,
    },
    match_ast,
    syntax_editor::{Element, Position, SyntaxEditor},
};

use crate::{AssistContext, AssistId, Assists};

// Assist: unwrap_block
//
// This assist removes if...else, for, while and loop control statements to just keep the body.
//
// ```
// fn foo() {
//     if true {$0
//         println!("foo");
//     }
// }
// ```
// ->
// ```
// fn foo() {
//     println!("foo");
// }
// ```
pub(crate) fn unwrap_block(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
    let l_curly_token = ctx.find_token_syntax_at_offset(T!['{'])?;
    let block = l_curly_token.parent_ancestors().nth(1).and_then(ast::BlockExpr::cast)?;
    let target = block.syntax().text_range();
    let mut container = block.syntax().clone();
    let mut replacement = block.clone();
    let mut prefer_container = None;

    let from_indent = block.indent_level();
    let into_indent = loop {
        let parent = container.parent()?;
        container = match_ast! {
            match parent {
                ast::ForExpr(it) => it.syntax().clone(),
                ast::LoopExpr(it) => it.syntax().clone(),
                ast::WhileExpr(it) => it.syntax().clone(),
                ast::MatchArm(it) => it.parent_match().syntax().clone(),
                ast::LetElse(it) => it.syntax().parent()?,
                ast::LetStmt(it) => {
                    replacement = wrap_let(&it, replacement);
                    prefer_container = Some(it.syntax().clone());
                    it.syntax().clone()
                },
                ast::IfExpr(it) => {
                    prefer_container.get_or_insert_with(|| {
                        if let Some(else_branch) = it.else_branch()
                            && *else_branch.syntax() == container
                        {
                            else_branch.syntax().clone()
                        } else {
                            it.syntax().clone()
                        }
                    });
                    it.syntax().clone()
                },
                ast::ExprStmt(it) => it.syntax().clone(),
                ast::StmtList(it) => break it.indent_level(),
                _ => return None,
            }
        };
    };
    let replacement = replacement.stmt_list()?;

    acc.add(AssistId::refactor_rewrite("unwrap_block"), "Unwrap block", target, |builder| {
        let editor = builder.make_editor(block.syntax());
        let replacement = replacement.dedent(from_indent).indent(into_indent);
        let container = prefer_container.unwrap_or(container);

        editor.replace_with_many(&container, extract_statements(replacement));
        delete_else_before(container, &editor);

        builder.add_file_edits(ctx.vfs_file_id(), editor);
    })
}

fn delete_else_before(container: SyntaxNode, edit: &SyntaxEditor) {
    let Some(else_token) = container
        .siblings_with_tokens(syntax::Direction::Prev)
        .skip(1)
        .map_while(|it| it.into_token())
        .find(|it| it.kind() == T![else])
    else {
        return;
    };
    itertools::chain(else_token.prev_token(), else_token.next_token())
        .filter(|it| it.kind() == SyntaxKind::WHITESPACE)
        .for_each(|it| edit.delete(it));
    let indent = IndentLevel::from_node(&container);
    let newline = make::tokens::whitespace(&format!("\n{indent}"));
    edit.replace(else_token, newline);
}

fn wrap_let(assign: &ast::LetStmt, replacement: ast::BlockExpr) -> ast::BlockExpr {
    let try_wrap_assign = || {
        let initializer = assign.initializer()?.syntax().syntax_element();
        let (editor, replacement) = SyntaxEditor::with_ast_node(&replacement);
        let tail_expr = replacement.tail_expr()?;
        let before =
            assign.syntax().children_with_tokens().take_while(|it| *it != initializer).collect();
        let after = assign
            .syntax()
            .children_with_tokens()
            .skip_while(|it| *it != initializer)
            .skip(1)
            .collect();

        editor.insert_all(Position::before(tail_expr.syntax()), before);
        editor.insert_all(Position::after(tail_expr.syntax()), after);
        ast::BlockExpr::cast(editor.finish().new_root().clone())
    };
    try_wrap_assign().unwrap_or(replacement)
}

fn extract_statements(stmt_list: ast::StmtList) -> Vec<SyntaxElement> {
    let mut elements = stmt_list
        .syntax()
        .children_with_tokens()
        .filter(|it| !matches!(it.kind(), T!['{'] | T!['}']))
        .skip_while(|it| it.kind() == SyntaxKind::WHITESPACE)
        .collect::<Vec<_>>();
    while elements.pop_if(|it| it.kind() == SyntaxKind::WHITESPACE).is_some() {}
    elements
}

#[cfg(test)]
mod tests {
    use crate::tests::{check_assist, check_assist_not_applicable};

    use super::*;

    #[test]
    fn unwrap_tail_expr_block() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    $0{
        92
    }
}
"#,
            r#"
fn main() {
    92
}
"#,
        )
    }

    #[test]
    fn unwrap_stmt_expr_block() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    $0{
        92;
    }
    ()
}
"#,
            r#"
fn main() {
    92;
    ()
}
"#,
        );
        // Pedantically, we should add an `;` here...
        check_assist(
            unwrap_block,
            r#"
fn main() {
    $0{
        92
    }
    ()
}
"#,
            r#"
fn main() {
    92
    ()
}
"#,
        );
    }

    #[test]
    fn simple_if() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    bar();
    if true {$0
        foo();

        // comment
        bar();
    } else {
        println!("bar");
    }
}
"#,
            r#"
fn main() {
    bar();
    foo();

    // comment
    bar();
}
"#,
        );
    }

    #[test]
    fn simple_if_else() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    bar();
    if true {
        foo();

        // comment
        bar();
    } else {$0
        println!("bar");
    }
}
"#,
            r#"
fn main() {
    bar();
    if true {
        foo();

        // comment
        bar();
    }
    println!("bar");
}
"#,
        );
    }

    #[test]
    fn simple_if_else_if() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {$0
        println!("bar");
    } else {
        println!("foo");
    }
}
"#,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    }
    println!("bar");
}
"#,
        );
    }

    #[test]
    fn simple_if_else_if_nested() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {
        println!("bar");
    } else if true {$0
        println!("foo");
    }
}
"#,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {
        println!("bar");
    }
    println!("foo");
}
"#,
        );
    }

    #[test]
    fn simple_if_else_if_nested_else() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {
        println!("bar");
    } else if true {
        println!("foo");
    } else {$0
        println!("else");
    }
}
"#,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {
        println!("bar");
    } else if true {
        println!("foo");
    }
    println!("else");
}
"#,
        );
    }

    #[test]
    fn simple_if_else_if_nested_middle() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {
        println!("bar");
    } else if true {$0
        println!("foo");
    } else {
        println!("else");
    }
}
"#,
            r#"
fn main() {
    // bar();
    if true {
        println!("true");

        // comment
        // bar();
    } else if false {
        println!("bar");
    }
    println!("foo");
}
"#,
        );
    }

    #[test]
    fn simple_if_bad_cursor_position() {
        check_assist_not_applicable(
            unwrap_block,
            r#"
fn main() {
    bar();$0
    if true {
        foo();

        // comment
        bar();
    } else {
        println!("bar");
    }
}
"#,
        );
    }

    #[test]
    fn simple_for() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    for i in 0..5 {$0
        if true {
            foo();

            // comment
            bar();
        } else {
            println!("bar");
        }
    }
}
"#,
            r#"
fn main() {
    if true {
        foo();

        // comment
        bar();
    } else {
        println!("bar");
    }
}
"#,
        );
    }

    #[test]
    fn simple_if_in_for() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    for i in 0..5 {
        if true {$0
            foo();

            // comment
            bar();
        } else {
            println!("bar");
        }
    }
}
"#,
            r#"
fn main() {
    for i in 0..5 {
        foo();

        // comment
        bar();
    }
}
"#,
        );
    }

    #[test]
    fn simple_loop() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    loop {$0
        if true {
            foo();

            // comment
            bar();
        } else {
            println!("bar");
        }
    }
}
"#,
            r#"
fn main() {
    if true {
        foo();

        // comment
        bar();
    } else {
        println!("bar");
    }
}
"#,
        );
    }

    #[test]
    fn simple_while() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    while true {$0
        if true {
            foo();

            // comment
            bar();
        } else {
            println!("bar");
        }
    }
}
"#,
            r#"
fn main() {
    if true {
        foo();

        // comment
        bar();
    } else {
        println!("bar");
    }
}
"#,
        );
    }

    #[test]
    fn simple_let_else() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    let Some(2) = None else {$0
        return;
    };
}
"#,
            r#"
fn main() {
    return;
}
"#,
        );
        check_assist(
            unwrap_block,
            r#"
fn main() {
    let Some(2) = None else {$0
        return
    };
}
"#,
            r#"
fn main() {
    return
}
"#,
        );
    }

    #[test]
    fn unwrap_match_arm() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    match rel_path {
        Ok(rel_path) => {$0
            let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
            Some((*id, rel_path))
        }
        Err(_) => None,
    }
}
"#,
            r#"
fn main() {
    let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
    Some((*id, rel_path))
}
"#,
        );
    }

    #[test]
    fn unwrap_match_arm_in_let() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    let value = match rel_path {
        Ok(rel_path) => {$0
            let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
            Some((*id, rel_path))
        }
        Err(_) => None,
    };
}
"#,
            r#"
fn main() {
    let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
    let value = Some((*id, rel_path));
}
"#,
        );
    }

    #[test]
    fn simple_if_in_while_bad_cursor_position() {
        check_assist_not_applicable(
            unwrap_block,
            r#"
fn main() {
    while true {
        if true {
            foo();$0

            // comment
            bar();
        } else {
            println!("bar");
        }
    }
}
"#,
        );
    }

    #[test]
    fn simple_single_line() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    {$0 0 }
}
"#,
            r#"
fn main() {
    0
}
"#,
        );
    }

    #[test]
    fn simple_nested_block() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    $0{
        {
            3
        }
    }
}
"#,
            r#"
fn main() {
    {
        3
    }
}
"#,
        );
    }

    #[test]
    fn nested_single_line() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    {$0 { println!("foo"); } }
}
"#,
            r#"
fn main() {
    { println!("foo"); }
}
"#,
        );

        check_assist(
            unwrap_block,
            r#"
fn main() {
    {$0 { 0 } }
}
"#,
            r#"
fn main() {
    { 0 }
}
"#,
        );
    }

    #[test]
    fn simple_if_single_line() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    if true {$0 /* foo */ foo() } else { bar() /* bar */}
}
"#,
            r#"
fn main() {
    /* foo */ foo()
}
"#,
        );
    }

    #[test]
    fn if_single_statement() {
        check_assist(
            unwrap_block,
            r#"
fn main() {
    if true {$0
        return 3;
    }
}
"#,
            r#"
fn main() {
    return 3;
}
"#,
        );
    }

    #[test]
    fn multiple_statements() {
        check_assist(
            unwrap_block,
            r#"
fn main() -> i32 {
    if 2 > 1 {$0
        let a = 5;
        return 3;
    }
    5
}
"#,
            r#"
fn main() -> i32 {
    let a = 5;
    return 3;
    5
}
"#,
        );
    }

    #[test]
    fn unwrap_block_in_let_initializers() {
        // https://github.com/rust-lang/rust-analyzer/issues/13679
        check_assist(
            unwrap_block,
            r#"
fn main() {
    let x = {$0
        bar
    };
}
"#,
            r#"
fn main() {
    let x = bar;
}
"#,
        );
        check_assist(
            unwrap_block,
            r#"
fn main() -> i32 {
    let _ = {$01; 2};
}
"#,
            r#"
fn main() -> i32 {
    1; let _ = 2;
}
"#,
        );
        check_assist(
            unwrap_block,
            r#"
fn main() -> i32 {
    let mut a = {$01; 2};
}
"#,
            r#"
fn main() -> i32 {
    1; let mut a = 2;
}
"#,
        );
        check_assist(
            unwrap_block,
            r#"
fn main() -> i32 {
    let mut a = {$0
        1;
        2;
        3
    };
}
"#,
            r#"
fn main() -> i32 {
    1;
    2;
    let mut a = 3;
}
"#,
        );
    }

    #[test]
    fn unwrap_if_in_let_initializers() {
        // https://github.com/rust-lang/rust-analyzer/issues/13679
        check_assist(
            unwrap_block,
            r#"
fn main() {
    let a = 1;
    let x = if a - 1 == 0 {$0
        foo
    } else {
        bar
    };
}
"#,
            r#"
fn main() {
    let a = 1;
    let x = foo;
}
"#,
        );
    }

    #[test]
    fn unwrap_block_with_modifiers() {
        // https://github.com/rust-lang/rust-analyzer/issues/17964
        check_assist(
            unwrap_block,
            r#"
fn main() {
    unsafe $0{
        bar;
    }
}
"#,
            r#"
fn main() {
    bar;
}
"#,
        );
        check_assist(
            unwrap_block,
            r#"
fn main() {
    async move $0{
        bar;
    }
}
"#,
            r#"
fn main() {
    bar;
}
"#,
        );
        check_assist(
            unwrap_block,
            r#"
fn main() {
    try $0{
        bar;
    }
}
"#,
            r#"
fn main() {
    bar;
}
"#,
        );
    }
}
