use either::Either;
use syntax::{
    AstNode, T,
    ast::{self, edit::AstNodeEdit},
    match_ast,
};

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

// Assist: add_braces
//
// Adds braces to closure bodies, match arm expressions and assignment bodies.
//
// ```
// fn foo(n: i32) -> i32 {
//     match n {
//         1 =>$0 n + 1,
//         _ => 0
//     }
// }
// ```
// ->
// ```
// fn foo(n: i32) -> i32 {
//     match n {
//         1 => {
//             n + 1
//         },
//         _ => 0
//     }
// }
// ```
// ---
// ```
// fn foo(n: i32) -> i32 {
//     let x =$0 n + 2;
// }
// ```
// ->
// ```
// fn foo(n: i32) -> i32 {
//     let x = {
//         n + 2
//     };
// }
// ```
pub(crate) fn add_braces(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
    let (expr_type, expr) = get_replacement_node(ctx)?;

    acc.add(
        AssistId::refactor_rewrite("add_braces"),
        match expr_type {
            ParentType::ClosureExpr => "Add braces to this closure body",
            ParentType::MatchArmExpr => "Add braces to this match arm expression",
            ParentType::Assignment => "Add braces to this assignment expression",
        },
        expr.syntax().text_range(),
        |builder| {
            let editor = builder.make_editor(expr.syntax());
            let make = editor.make();

            let new_expr = expr.reset_indent().indent(1.into());
            let block_expr = make.block_expr(None, Some(new_expr));

            editor.replace(expr.syntax(), block_expr.indent(expr.indent_level()).syntax());
            builder.add_file_edits(ctx.vfs_file_id(), editor);
        },
    )
}

enum ParentType {
    MatchArmExpr,
    ClosureExpr,
    Assignment,
}

fn get_replacement_node(ctx: &AssistContext<'_, '_>) -> Option<(ParentType, ast::Expr)> {
    let node = ctx.find_node_at_offset::<Either<ast::MatchArm, ast::ClosureExpr>>();
    let (parent_type, body) = if let Some(eq_token) = ctx.find_token_syntax_at_offset(T![=]) {
        let parent = eq_token.parent()?;
        let body = match_ast! {
            match parent {
                ast::LetStmt(it) => it.initializer()?,
                ast::LetExpr(it) => it.expr()?,
                ast::BinExpr(it) => it.rhs()?,
                ast::Static(it) => it.body()?,
                ast::Const(it) => it.body()?,
                _ => return None,
            }
        };
        (ParentType::Assignment, body)
    } else if let Some(Either::Left(match_arm)) = &node {
        let match_arm_expr = match_arm.expr()?;
        (ParentType::MatchArmExpr, match_arm_expr)
    } else if let Some(Either::Right(closure_expr)) = &node {
        let body = closure_expr.body()?;
        (ParentType::ClosureExpr, body)
    } else {
        return None;
    };

    if matches!(body, ast::Expr::BlockExpr(_)) {
        return None;
    }

    Some((parent_type, body))
}

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

    use super::*;

    #[test]
    fn suggest_add_braces_for_closure() {
        check_assist(
            add_braces,
            r#"
fn foo() {
    t(|n|$0 n + 100);
}
"#,
            r#"
fn foo() {
    t(|n| {
        n + 100
    });
}
"#,
        );
    }

    #[test]
    fn suggest_add_braces_for_closure_in_match() {
        check_assist(
            add_braces,
            r#"
fn foo() {
    match () {
        () => {
            t(|n|$0 n + 100);
        }
    }
}
"#,
            r#"
fn foo() {
    match () {
        () => {
            t(|n| {
                n + 100
            });
        }
    }
}
"#,
        );
    }

    #[test]
    fn suggest_add_braces_for_assignment() {
        check_assist(
            add_braces,
            r#"
fn foo() {
    let x =$0 n + 100;
}
"#,
            r#"
fn foo() {
    let x = {
        n + 100
    };
}
"#,
        );

        check_assist(
            add_braces,
            r#"
fn foo() {
    let x;
    x =$0 n + 100;
}
"#,
            r#"
fn foo() {
    let x;
    x = {
        n + 100
    };
}
"#,
        );

        check_assist(
            add_braces,
            r#"
fn foo() {
    if let x =$0 n + 100 {}
}
"#,
            r#"
fn foo() {
    if let x = {
        n + 100
    } {}
}
"#,
        );
    }

    #[test]
    fn suggest_add_braces_for_const_initializer() {
        check_assist(
            add_braces,
            r#"
const X: i32 =$0 1 + 2;
"#,
            r#"
const X: i32 = {
    1 + 2
};
"#,
        );
    }

    #[test]
    fn suggest_add_braces_for_static_initializer() {
        check_assist(
            add_braces,
            r#"
static X: i32 $0= 1 + 2;
"#,
            r#"
static X: i32 = {
    1 + 2
};
"#,
        );
    }

    #[test]
    fn no_assist_for_closures_with_braces() {
        check_assist_not_applicable(
            add_braces,
            r#"
fn foo() {
    t(|n|$0 { n + 100 });
}
"#,
        );
    }

    #[test]
    fn suggest_add_braces_for_match() {
        check_assist(
            add_braces,
            r#"
fn foo() {
    match n {
        Some(n) $0=> 29,
        _ => ()
    };
}
"#,
            r#"
fn foo() {
    match n {
        Some(n) => {
            29
        },
        _ => ()
    };
}
"#,
        );
    }

    #[test]
    fn multiple_indent() {
        check_assist(
            add_braces,
            r#"
fn foo() {
    {
        match n {
            Some(n) $0=> foo(
                29,
                30,
            ),
            _ => ()
        };
    }
}
"#,
            r#"
fn foo() {
    {
        match n {
            Some(n) => {
                foo(
                    29,
                    30,
                )
            },
            _ => ()
        };
    }
}
"#,
        );
    }

    #[test]
    fn no_assist_for_match_with_braces() {
        check_assist_not_applicable(
            add_braces,
            r#"
fn foo() {
    match n {
        Some(n) $0=> { return 29; },
        _ => ()
    };
}
"#,
        );
    }
}
