use hir::HirDisplay;
use syntax::{
    AstNode, TextRange,
    ast::{Expr, GenericArg, GenericArgList, HasGenericArgs, LetStmt, Type::InferType},
};

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

// Assist: replace_turbofish_with_explicit_type
//
// Converts `::<_>` to an explicit type assignment.
//
// ```
// fn make<T>() -> T { ) }
// fn main() {
//     let a = make$0::<i32>();
// }
// ```
// ->
// ```
// fn make<T>() -> T { ) }
// fn main() {
//     let a: i32 = make();
// }
// ```
pub(crate) fn replace_turbofish_with_explicit_type(
    acc: &mut Assists,
    ctx: &AssistContext<'_>,
) -> Option<()> {
    let let_stmt = ctx.find_node_at_offset::<LetStmt>()?;

    let initializer = let_stmt.initializer()?;

    let generic_args = generic_arg_list(&initializer)?;

    // Find range of ::<_>
    let colon2 = generic_args.coloncolon_token()?;
    let r_angle = generic_args.r_angle_token()?;
    let turbofish_range = TextRange::new(colon2.text_range().start(), r_angle.text_range().end());

    let turbofish_args: Vec<GenericArg> = generic_args.generic_args().collect();

    // Find type of ::<_>
    if turbofish_args.len() != 1 {
        cov_mark::hit!(not_applicable_if_not_single_arg);
        return None;
    }

    // An improvement would be to check that this is correctly part of the return value of the
    // function call, or sub in the actual return type.
    let returned_type = match ctx.sema.type_of_expr(&initializer) {
        Some(returned_type) if !returned_type.original.contains_unknown() => {
            let module = ctx.sema.scope(let_stmt.syntax())?.module();
            returned_type.original.display_source_code(ctx.db(), module.into(), false).ok()?
        }
        _ => {
            cov_mark::hit!(fallback_to_turbofish_type_if_type_info_not_available);
            turbofish_args[0].to_string()
        }
    };

    let initializer_start = initializer.syntax().text_range().start();
    if ctx.offset() > turbofish_range.end() || ctx.offset() < initializer_start {
        cov_mark::hit!(not_applicable_outside_turbofish);
        return None;
    }

    if let_stmt.colon_token().is_none() {
        // If there's no colon in a let statement, then there is no explicit type.
        // let x = fn::<...>();
        let ident_range = let_stmt.pat()?.syntax().text_range();

        return acc.add(
            AssistId::refactor_rewrite("replace_turbofish_with_explicit_type"),
            "Replace turbofish with explicit type",
            TextRange::new(initializer_start, turbofish_range.end()),
            |builder| {
                builder.insert(ident_range.end(), format!(": {returned_type}"));
                builder.delete(turbofish_range);
            },
        );
    } else if let Some(InferType(t)) = let_stmt.ty() {
        // If there's a type inference underscore, we can offer to replace it with the type in
        // the turbofish.
        // let x: _ = fn::<...>();
        let underscore_range = t.syntax().text_range();

        return acc.add(
            AssistId::refactor_rewrite("replace_turbofish_with_explicit_type"),
            "Replace `_` with turbofish type",
            turbofish_range,
            |builder| {
                builder.replace(underscore_range, returned_type);
                builder.delete(turbofish_range);
            },
        );
    }

    None
}

fn generic_arg_list(expr: &Expr) -> Option<GenericArgList> {
    match expr {
        Expr::MethodCallExpr(expr) => expr.generic_arg_list(),
        Expr::CallExpr(expr) => {
            if let Expr::PathExpr(pe) = expr.expr()? {
                pe.path()?.segment()?.generic_arg_list()
            } else {
                cov_mark::hit!(not_applicable_if_non_path_function_call);
                None
            }
        }
        Expr::AwaitExpr(expr) => generic_arg_list(&expr.expr()?),
        Expr::TryExpr(expr) => generic_arg_list(&expr.expr()?),
        _ => {
            cov_mark::hit!(not_applicable_if_non_function_call_initializer);
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};

    #[test]
    fn replaces_turbofish_for_vec_string() {
        cov_mark::check!(fallback_to_turbofish_type_if_type_info_not_available);
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a = make$0::<Vec<String>>();
}
"#,
            r#"
fn make<T>() -> T {}
fn main() {
    let a: Vec<String> = make();
}
"#,
        );
    }

    #[test]
    fn replaces_method_calls() {
        // foo.make() is a method call which uses a different expr in the let initializer
        cov_mark::check!(fallback_to_turbofish_type_if_type_info_not_available);
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a = foo.make$0::<Vec<String>>();
}
"#,
            r#"
fn make<T>() -> T {}
fn main() {
    let a: Vec<String> = foo.make();
}
"#,
        );
    }

    #[test]
    fn replace_turbofish_target() {
        check_assist_target(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a = $0make::<Vec<String>>();
}
"#,
            r#"make::<Vec<String>>"#,
        );
    }

    #[test]
    fn not_applicable_outside_turbofish() {
        cov_mark::check!(not_applicable_outside_turbofish);
        check_assist_not_applicable(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let $0a = make::<Vec<String>>();
}
"#,
        );
    }

    #[test]
    fn replace_inferred_type_placeholder() {
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a: _ = make$0::<Vec<String>>();
}
"#,
            r#"
fn make<T>() -> T {}
fn main() {
    let a: Vec<String> = make();
}
"#,
        );
    }

    #[test]
    fn not_applicable_constant_initializer() {
        cov_mark::check!(not_applicable_if_non_function_call_initializer);
        check_assist_not_applicable(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a = "foo"$0;
}
"#,
        );
    }

    #[test]
    fn not_applicable_non_path_function_call() {
        cov_mark::check!(not_applicable_if_non_path_function_call);
        check_assist_not_applicable(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    $0let a = (|| {})();
}
"#,
        );
    }

    #[test]
    fn non_applicable_multiple_generic_args() {
        cov_mark::check!(not_applicable_if_not_single_arg);
        check_assist_not_applicable(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a = make$0::<Vec<String>, i32>();
}
"#,
        );
    }

    #[test]
    fn replaces_turbofish_for_known_type() {
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
fn make<T>() -> T {}
fn main() {
    let a = make$0::<i32>();
}
"#,
            r#"
fn make<T>() -> T {}
fn main() {
    let a: i32 = make();
}
"#,
        );
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
//- minicore: option
fn make<T>() -> T {}
fn main() {
    let a = make$0::<Option<bool>>();
}
"#,
            r#"
fn make<T>() -> T {}
fn main() {
    let a: Option<bool> = make();
}
"#,
        );
    }

    #[test]
    fn replaces_turbofish_not_same_type() {
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
//- minicore: option
fn make<T>() -> Option<T> {}
fn main() {
    let a = make$0::<u128>();
}
"#,
            r#"
fn make<T>() -> Option<T> {}
fn main() {
    let a: Option<u128> = make();
}
"#,
        );
    }

    #[test]
    fn replaces_turbofish_for_type_with_defaulted_generic_param() {
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
struct HasDefault<T, U = i32>(T, U);
fn make<T>() -> HasDefault<T> {}
fn main() {
    let a = make$0::<bool>();
}
"#,
            r#"
struct HasDefault<T, U = i32>(T, U);
fn make<T>() -> HasDefault<T> {}
fn main() {
    let a: HasDefault<bool> = make();
}
"#,
        );
    }

    #[test]
    fn replaces_turbofish_try_await() {
        check_assist(
            replace_turbofish_with_explicit_type,
            r#"
//- minicore: option, future, try
struct Fut<T>(T);
impl<T> core::future::Future for Fut<T> {
    type Output = Option<T>;
}
fn make<T>() -> Fut<T> {}
fn main() {
    let a = make$0::<bool>().await?;
}
"#,
            r#"
struct Fut<T>(T);
impl<T> core::future::Future for Fut<T> {
    type Output = Option<T>;
}
fn make<T>() -> Fut<T> {}
fn main() {
    let a: bool = make().await?;
}
"#,
        );
    }
}
