//! Renderer for function calls.

use hir::{AsAssocItem, HirDisplay, db::HirDatabase};
use ide_db::{SnippetCap, SymbolKind};
use itertools::Itertools;
use stdx::{format_to, to_lower_snake_case};
use syntax::{AstNode, SmolStr, ToSmolStr, format_smolstr};

use crate::{
    CallableSnippets,
    context::{
        CompleteSemicolon, CompletionContext, DotAccess, DotAccessKind, PathCompletionCtx, PathKind,
    },
    item::{
        Builder, CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevanceFn,
        CompletionRelevanceReturnType, CompletionRelevanceTraitInfo,
    },
    render::{
        RenderContext, compute_exact_name_match, compute_ref_match, compute_type_match, match_types,
    },
};

#[derive(Debug)]
enum FuncKind<'ctx> {
    Function(&'ctx PathCompletionCtx<'ctx>),
    Method(&'ctx DotAccess<'ctx>, Option<SmolStr>),
}

pub(crate) fn render_fn(
    ctx: RenderContext<'_, '_>,
    path_ctx: &PathCompletionCtx<'_>,
    local_name: Option<hir::Name>,
    func: hir::Function,
) -> Builder {
    let _p = tracing::info_span!("render_fn").entered();
    render(ctx, local_name, func, FuncKind::Function(path_ctx))
}

pub(crate) fn render_method(
    ctx: RenderContext<'_, '_>,
    dot_access: &DotAccess<'_>,
    receiver: Option<SmolStr>,
    local_name: Option<hir::Name>,
    func: hir::Function,
) -> Builder {
    let _p = tracing::info_span!("render_method").entered();
    render(ctx, local_name, func, FuncKind::Method(dot_access, receiver))
}

fn render(
    ctx @ RenderContext { completion, .. }: RenderContext<'_, '_>,
    local_name: Option<hir::Name>,
    func: hir::Function,
    func_kind: FuncKind<'_>,
) -> Builder {
    let db = completion.db;

    let name = local_name.unwrap_or_else(|| func.name(db));

    let (call, escaped_call) = match &func_kind {
        FuncKind::Method(_, Some(receiver)) => (
            format_smolstr!("{}.{}", receiver, name.as_str()),
            format_smolstr!("{}.{}", receiver, name.display(ctx.db(), completion.edition)),
        ),
        _ => (name.as_str().to_smolstr(), name.display(db, completion.edition).to_smolstr()),
    };
    let has_self_param = func.self_param(db).is_some();
    let mut item = CompletionItem::new(
        CompletionItemKind::SymbolKind(if has_self_param {
            SymbolKind::Method
        } else {
            SymbolKind::Function
        }),
        ctx.source_range(),
        call.clone(),
        completion.edition,
    );

    let ret_type = func.ret_type(db);
    let assoc_item = func.as_assoc_item(db);

    let trait_info =
        assoc_item.and_then(|trait_| trait_.container_or_implemented_trait(db)).map(|trait_| {
            CompletionRelevanceTraitInfo {
                notable_trait: completion.is_doc_notable_trait(trait_),
                is_op_method: completion.is_ops_trait(trait_),
            }
        });

    let (has_dot_receiver, has_call_parens, cap) = match func_kind {
        FuncKind::Function(&PathCompletionCtx {
            kind: PathKind::Expr { .. },
            has_call_parens,
            ..
        }) => (false, has_call_parens, ctx.completion.config.snippet_cap),
        FuncKind::Method(&DotAccess { kind: DotAccessKind::Method, .. }, _) => {
            (true, true, ctx.completion.config.snippet_cap)
        }
        FuncKind::Method(DotAccess { kind: DotAccessKind::Field { .. }, .. }, _) => {
            (true, false, ctx.completion.config.snippet_cap)
        }
        _ => (false, false, None),
    };
    let complete_call_parens = cap
        .filter(|_| !has_call_parens)
        .and_then(|cap| Some((cap, params(ctx.completion, func, &func_kind, has_dot_receiver)?)));

    let function = assoc_item
        .and_then(|assoc_item| assoc_item.implementing_ty(db))
        .map(|self_type| compute_return_type_match(db, &ctx, self_type, &ret_type))
        .map(|return_type| CompletionRelevanceFn {
            has_params: has_self_param || func.num_params(db) > 0,
            has_self_param,
            return_type,
        });

    item.set_relevance(CompletionRelevance {
        type_match: if has_call_parens || complete_call_parens.is_some() {
            compute_type_match(completion, &ret_type)
        } else {
            compute_type_match(completion, &func.ty(db))
        },
        exact_name_match: compute_exact_name_match(completion, &call),
        function,
        trait_: trait_info,
        is_skipping_completion: matches!(func_kind, FuncKind::Method(_, Some(_))),
        ..ctx.completion_relevance()
    });

    match func_kind {
        FuncKind::Function(path_ctx) => {
            super::path_ref_match(completion, path_ctx, &ret_type, &mut item);
        }
        FuncKind::Method(DotAccess { receiver: Some(receiver), .. }, _) => {
            if let Some(original_expr) = completion.sema.original_ast_node(receiver.clone())
                && let Some(ref_mode) = compute_ref_match(completion, &ret_type)
            {
                item.ref_match(ref_mode, original_expr.syntax().text_range().start());
            }
        }
        _ => (),
    }

    let detail = if ctx.completion.config.full_function_signatures {
        detail_full(ctx.completion, func)
    } else {
        detail(ctx.completion, func)
    };
    item.set_documentation(ctx.docs(func))
        .set_deprecated(ctx.is_deprecated(func, func.as_assoc_item(db)))
        .detail(detail)
        .lookup_by(name.as_str().to_smolstr());

    if let Some((cap, (self_param, params))) = complete_call_parens {
        add_call_parens(
            &mut item,
            completion,
            cap,
            call,
            escaped_call,
            self_param,
            params,
            &ret_type,
        );
    }

    match ctx.import_to_add {
        Some(import_to_add) => {
            item.add_import(import_to_add);
        }
        None => {
            if let Some(actm) = assoc_item
                && let Some(trt) = actm.container_or_implemented_trait(db)
            {
                item.trait_name(trt.name(db).display_no_db(ctx.completion.edition).to_smolstr());
            }
        }
    }

    item.doc_aliases(ctx.doc_aliases);
    item
}

fn compute_return_type_match(
    db: &dyn HirDatabase,
    ctx: &RenderContext<'_, '_>,
    self_type: hir::Type<'_>,
    ret_type: &hir::Type<'_>,
) -> CompletionRelevanceReturnType {
    if match_types(ctx.completion, &self_type, ret_type).is_some() {
        // fn([..]) -> Self
        CompletionRelevanceReturnType::DirectConstructor
    } else if ret_type
        .type_arguments()
        .any(|ret_type_arg| match_types(ctx.completion, &self_type, &ret_type_arg).is_some())
    {
        // fn([..]) -> Result<Self, E> OR Wrapped<Foo, Self>
        CompletionRelevanceReturnType::Constructor
    } else if ret_type
        .as_adt()
        .map(|adt| adt.name(db).as_str().ends_with("Builder"))
        .unwrap_or(false)
    {
        // fn([..]) -> [..]Builder
        CompletionRelevanceReturnType::Builder
    } else {
        CompletionRelevanceReturnType::Other
    }
}

pub(super) fn add_call_parens<'b>(
    builder: &'b mut Builder,
    ctx: &CompletionContext<'_, '_>,
    cap: SnippetCap,
    name: SmolStr,
    escaped_name: SmolStr,
    self_param: Option<hir::SelfParam>,
    params: Vec<hir::Param<'_>>,
    ret_type: &hir::Type<'_>,
) -> &'b mut Builder {
    cov_mark::hit!(inserts_parens_for_function_calls);

    let (mut snippet, label_suffix) = if self_param.is_none() && params.is_empty() {
        (format!("{escaped_name}()$0"), "()")
    } else {
        builder.trigger_call_info();
        let snippet = if let Some(CallableSnippets::FillArguments) = ctx.config.callable {
            let offset = if self_param.is_some() { 2 } else { 1 };
            let function_params_snippet =
                params.iter().enumerate().format_with(", ", |(index, param), f| {
                    match param.name(ctx.db) {
                        Some(n) => {
                            let smol_str = n.display_no_db(ctx.edition).to_smolstr();
                            let text = smol_str.as_str().trim_start_matches('_');
                            let ref_ = ref_of_param(ctx, text, param.ty());
                            f(&format_args!("${{{}:{ref_}{text}}}", index + offset))
                        }
                        None => {
                            let name = match param.ty().as_adt() {
                                None => "_".to_owned(),
                                Some(adt) => to_lower_snake_case(adt.name(ctx.db).as_str()),
                            };
                            f(&format_args!("${{{}:{name}}}", index + offset))
                        }
                    }
                });
            match self_param {
                Some(self_param) => {
                    format!(
                        "{}(${{1:{}}}{}{})$0",
                        escaped_name,
                        self_param.display(ctx.db, ctx.display_target),
                        if params.is_empty() { "" } else { ", " },
                        function_params_snippet
                    )
                }
                None => {
                    format!("{escaped_name}({function_params_snippet})$0")
                }
            }
        } else {
            cov_mark::hit!(suppress_arg_snippets);
            format!("{escaped_name}($0)")
        };

        (snippet, "(…)")
    };
    if ret_type.is_unit() {
        match ctx.complete_semicolon {
            CompleteSemicolon::DoNotComplete => {}
            CompleteSemicolon::CompleteSemi | CompleteSemicolon::CompleteComma => {
                cov_mark::hit!(complete_semicolon);
                let ch = if matches!(ctx.complete_semicolon, CompleteSemicolon::CompleteComma) {
                    ','
                } else {
                    ';'
                };
                if snippet.ends_with("$0") {
                    snippet.insert(snippet.len() - "$0".len(), ch);
                } else {
                    snippet.push(ch);
                }
            }
        }
    }
    builder.label(SmolStr::from_iter([&name, label_suffix])).insert_snippet(cap, snippet)
}

fn ref_of_param(ctx: &CompletionContext<'_, '_>, arg: &str, ty: &hir::Type<'_>) -> &'static str {
    if let Some(derefed_ty) = ty.remove_ref() {
        for (name, local) in ctx.locals.iter().sorted_by_key(|&(k, _)| k.clone()) {
            if name.as_str() == arg {
                return if local.ty(ctx.db) == derefed_ty {
                    if ty.is_mutable_reference() { "&mut " } else { "&" }
                } else {
                    ""
                };
            }
        }
    }
    ""
}

fn detail(ctx: &CompletionContext<'_, '_>, func: hir::Function) -> String {
    let mut ret_ty = func.ret_type(ctx.db);
    let mut detail = String::new();

    if func.is_const(ctx.db) {
        format_to!(detail, "const ");
    }
    if func.is_async(ctx.db) {
        format_to!(detail, "async ");
        if let Some(async_ret) = func.async_ret_type(ctx.db) {
            ret_ty = async_ret;
        }
    }
    if func.is_unsafe_to_call(ctx.db, ctx.containing_function, ctx.edition) {
        format_to!(detail, "unsafe ");
    }

    detail.push_str("fn(");
    params_display(ctx, &mut detail, func);
    detail.push(')');
    if !ret_ty.is_unit() {
        format_to!(detail, " -> {}", ret_ty.display(ctx.db, ctx.display_target));
    }
    detail
}

fn detail_full(ctx: &CompletionContext<'_, '_>, func: hir::Function) -> String {
    let signature = format!("{}", func.display(ctx.db, ctx.display_target));
    let mut detail = String::with_capacity(signature.len());

    for segment in signature.split_whitespace() {
        if !detail.is_empty() {
            detail.push(' ');
        }

        detail.push_str(segment);
    }

    detail
}

fn params_display(ctx: &CompletionContext<'_, '_>, detail: &mut String, func: hir::Function) {
    if let Some(self_param) = func.self_param(ctx.db) {
        format_to!(detail, "{}", self_param.display(ctx.db, ctx.display_target));
        let assoc_fn_params = func.assoc_fn_params(ctx.db);
        let params = assoc_fn_params
            .iter()
            .skip(1) // skip the self param because we are manually handling that
            .map(|p| p.ty().display(ctx.db, ctx.display_target));
        for param in params {
            format_to!(detail, ", {}", param);
        }
    } else {
        let assoc_fn_params = func.assoc_fn_params(ctx.db);
        format_to!(
            detail,
            "{}",
            assoc_fn_params.iter().map(|p| p.ty().display(ctx.db, ctx.display_target)).format(", ")
        );
    }

    if func.is_varargs(ctx.db) {
        detail.push_str(", ...");
    }
}

fn params<'db>(
    ctx: &CompletionContext<'_, 'db>,
    func: hir::Function,
    func_kind: &FuncKind<'_>,
    has_dot_receiver: bool,
) -> Option<(Option<hir::SelfParam>, Vec<hir::Param<'db>>)> {
    ctx.config.callable.as_ref()?;

    // Don't add parentheses if the expected type is a function reference with the same signature.
    if let Some(expected) = ctx.expected_type.as_ref().filter(|e| e.is_fn())
        && let Some(expected) = expected.as_callable(ctx.db)
        && let Some(completed) = func.ty(ctx.db).as_callable(ctx.db)
        && expected.sig() == completed.sig()
    {
        cov_mark::hit!(no_call_parens_if_fn_ptr_needed);
        return None;
    }

    let self_param = if has_dot_receiver || matches!(func_kind, FuncKind::Method(_, Some(_))) {
        None
    } else {
        func.self_param(ctx.db)
    };
    Some((self_param, func.params_without_self(ctx.db)))
}

#[cfg(test)]
mod tests {
    use crate::{
        CallableSnippets, CompletionConfig,
        tests::{TEST_CONFIG, check_edit, check_edit_with_config},
    };

    #[test]
    fn inserts_parens_for_function_calls() {
        cov_mark::check!(inserts_parens_for_function_calls);
        check_edit(
            "no_args",
            r#"
fn no_args() {}
fn main() { no_$0 }
"#,
            r#"
fn no_args() {}
fn main() { no_args();$0 }
"#,
        );

        check_edit(
            "with_args",
            r#"
fn with_args(x: i32, y: String) {}
fn main() { with_$0 }
"#,
            r#"
fn with_args(x: i32, y: String) {}
fn main() { with_args(${1:x}, ${2:y});$0 }
"#,
        );

        check_edit(
            "foo",
            r#"
struct S;
impl S {
    fn foo(&self) -> i32 { 0 }
}
fn bar(s: &S) { s.f$0 }
"#,
            r#"
struct S;
impl S {
    fn foo(&self) -> i32 { 0 }
}
fn bar(s: &S) { s.foo()$0 }
"#,
        );

        check_edit(
            "foo",
            r#"
struct S {}
impl S {
    fn foo(&self, x: i32) {}
}
fn bar(s: &S) {
    s.f$0
}
"#,
            r#"
struct S {}
impl S {
    fn foo(&self, x: i32) {}
}
fn bar(s: &S) {
    s.foo(${1:x});$0
}
"#,
        );

        check_edit(
            "foo",
            r#"
struct S {}
impl S {
    fn foo(&self, x: i32) {
        $0
    }
}
"#,
            r#"
struct S {}
impl S {
    fn foo(&self, x: i32) {
        self.foo(${1:x});$0
    }
}
"#,
        );
    }

    #[test]
    fn parens_for_method_call_as_assoc_fn() {
        check_edit(
            "foo",
            r#"
struct S;
impl S {
    fn foo(&self) {}
}
fn main() { S::f$0 }
"#,
            r#"
struct S;
impl S {
    fn foo(&self) {}
}
fn main() { S::foo(${1:&self});$0 }
"#,
        );
    }

    #[test]
    fn suppress_arg_snippets() {
        cov_mark::check!(suppress_arg_snippets);
        check_edit_with_config(
            CompletionConfig { callable: Some(CallableSnippets::AddParentheses), ..TEST_CONFIG },
            "with_args",
            r#"
fn with_args(x: i32, y: String) {}
fn main() { with_$0 }
"#,
            r#"
fn with_args(x: i32, y: String) {}
fn main() { with_args($0); }
"#,
        );
    }

    #[test]
    fn strips_underscores_from_args() {
        check_edit(
            "foo",
            r#"
fn foo(_foo: i32, ___bar: bool, ho_ge_: String) {}
fn main() { f$0 }
"#,
            r#"
fn foo(_foo: i32, ___bar: bool, ho_ge_: String) {}
fn main() { foo(${1:foo}, ${2:bar}, ${3:ho_ge_});$0 }
"#,
        );
    }

    #[test]
    fn insert_ref_when_matching_local_in_scope() {
        check_edit(
            "ref_arg",
            r#"
struct Foo {}
fn ref_arg(x: &Foo) {}
fn main() {
    let x = Foo {};
    ref_ar$0
}
"#,
            r#"
struct Foo {}
fn ref_arg(x: &Foo) {}
fn main() {
    let x = Foo {};
    ref_arg(${1:&x});$0
}
"#,
        );
    }

    #[test]
    fn insert_mut_ref_when_matching_local_in_scope() {
        check_edit(
            "ref_arg",
            r#"
struct Foo {}
fn ref_arg(x: &mut Foo) {}
fn main() {
    let x = Foo {};
    ref_ar$0
}
"#,
            r#"
struct Foo {}
fn ref_arg(x: &mut Foo) {}
fn main() {
    let x = Foo {};
    ref_arg(${1:&mut x});$0
}
"#,
        );
    }

    #[test]
    fn insert_ref_when_matching_local_in_scope_for_method() {
        check_edit(
            "apply_foo",
            r#"
struct Foo {}
struct Bar {}
impl Bar {
    fn apply_foo(&self, x: &Foo) {}
}

fn main() {
    let x = Foo {};
    let y = Bar {};
    y.$0
}
"#,
            r#"
struct Foo {}
struct Bar {}
impl Bar {
    fn apply_foo(&self, x: &Foo) {}
}

fn main() {
    let x = Foo {};
    let y = Bar {};
    y.apply_foo(${1:&x});$0
}
"#,
        );
    }

    #[test]
    fn trim_mut_keyword_in_func_completion() {
        check_edit(
            "take_mutably",
            r#"
fn take_mutably(mut x: &i32) {}

fn main() {
    take_m$0
}
"#,
            r#"
fn take_mutably(mut x: &i32) {}

fn main() {
    take_mutably(${1:x});$0
}
"#,
        );
    }

    #[test]
    fn complete_pattern_args_with_type_name_if_adt() {
        check_edit(
            "qux",
            r#"
struct Foo {
    bar: i32
}

fn qux(Foo { bar }: Foo) {
    println!("{}", bar);
}

fn main() {
  qu$0
}
"#,
            r#"
struct Foo {
    bar: i32
}

fn qux(Foo { bar }: Foo) {
    println!("{}", bar);
}

fn main() {
  qux(${1:foo});$0
}
"#,
        );
    }

    #[test]
    fn complete_fn_param() {
        // has mut kw
        check_edit(
            "bar: u32",
            r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), mut ba$0)
"#,
            r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), mut bar: u32)
"#,
        );

        // has unmatched mut kw
        check_edit(
            "bar: u32",
            r#"
fn f(foo: (), bar: u32) {}
fn g(foo: (), mut ba$0)
"#,
            r#"
fn f(foo: (), bar: u32) {}
fn g(foo: (), mut bar: u32)
"#,
        );

        check_edit(
            "mut bar: u32",
            r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), ba$0)
"#,
            r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), mut bar: u32)
"#,
        );

        // has type param
        check_edit(
            "bar: u32",
            r#"
fn g(foo: (), mut ba$0: u32)
fn f(foo: (), mut bar: u32) {}
"#,
            r#"
fn g(foo: (), mut bar: u32)
fn f(foo: (), mut bar: u32) {}
"#,
        );
    }

    #[test]
    fn complete_fn_mut_param_add_comma() {
        // add leading and trailing comma
        check_edit(
            "bar: u32",
            r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: ()mut ba$0 baz: ())
"#,
            r#"
fn f(foo: (), mut bar: u32) {}
fn g(foo: (), mut bar: u32, baz: ())
"#,
        );
    }

    #[test]
    fn complete_fn_mut_param_has_attribute() {
        check_edit(
            r#"#[baz = "qux"] mut bar: u32"#,
            r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: (), mut ba$0)
"#,
            r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: (), #[baz = "qux"] mut bar: u32)
"#,
        );

        check_edit(
            r#"#[baz = "qux"] mut bar: u32"#,
            r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: (), #[baz = "qux"] mut ba$0)
"#,
            r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: (), #[baz = "qux"] mut bar: u32)
"#,
        );

        check_edit(
            r#"#[baz = "qux"] mut bar: u32"#,
            r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: ()#[baz = "qux"] mut ba$0)
"#,
            r#"
fn f(foo: (), #[baz = "qux"] mut bar: u32) {}
fn g(foo: (), #[baz = "qux"] mut bar: u32)
"#,
        );
    }

    #[test]
    fn complete_semicolon_for_unit() {
        cov_mark::check!(complete_semicolon);
        check_edit(
            r#"foo"#,
            r#"
fn foo() {}
fn bar() {
    foo$0
}
"#,
            r#"
fn foo() {}
fn bar() {
    foo();$0
}
"#,
        );
        check_edit(
            r#"foo"#,
            r#"
fn foo(a: i32) {}
fn bar() {
    foo$0
}
"#,
            r#"
fn foo(a: i32) {}
fn bar() {
    foo(${1:a});$0
}
"#,
        );
        check_edit(
            r#"foo"#,
            r#"
fn foo(a: i32) {}
fn bar() {
    foo$0;
}
"#,
            r#"
fn foo(a: i32) {}
fn bar() {
    foo(${1:a})$0;
}
"#,
        );
        check_edit_with_config(
            CompletionConfig { add_semicolon_to_unit: false, ..TEST_CONFIG },
            r#"foo"#,
            r#"
fn foo(a: i32) {}
fn bar() {
    foo$0
}
"#,
            r#"
fn foo(a: i32) {}
fn bar() {
    foo(${1:a})$0
}
"#,
        );
    }

    #[test]
    fn complete_comma_for_unit_match_arm() {
        cov_mark::check!(complete_semicolon);
        check_edit(
            r#"foo"#,
            r#"
fn foo() {}
fn bar() {
    match Some(false) {
        v => fo$0
    }
}
"#,
            r#"
fn foo() {}
fn bar() {
    match Some(false) {
        v => foo(),$0
    }
}
"#,
        );
        check_edit(
            r#"foo"#,
            r#"
fn foo() {}
fn bar() {
    match Some(false) {
        v => fo$0,
    }
}
"#,
            r#"
fn foo() {}
fn bar() {
    match Some(false) {
        v => foo()$0,
    }
}
"#,
        );
    }

    #[test]
    fn no_semicolon_in_closure_ret() {
        check_edit(
            r#"foo"#,
            r#"
fn foo() {}
fn baz(_: impl FnOnce()) {}
fn bar() {
    baz(|| fo$0);
}
"#,
            r#"
fn foo() {}
fn baz(_: impl FnOnce()) {}
fn bar() {
    baz(|| foo()$0);
}
"#,
        );
    }

    #[test]
    fn no_semicolon_in_arg_list() {
        check_edit(
            r#"foo"#,
            r#"
fn foo() {}
fn baz(_: impl FnOnce()) {}
fn bar() {
    baz(fo$0);
}
"#,
            r#"
fn foo() {}
fn baz(_: impl FnOnce()) {}
fn bar() {
    baz(foo()$0);
}
"#,
        );
    }

    #[test]
    fn no_semicolon_in_array() {
        check_edit(
            r#"foo"#,
            r#"
fn foo() {}
fn bar() {
    let _ = [fo$0];
}
"#,
            r#"
fn foo() {}
fn bar() {
    let _ = [foo()$0];
}
"#,
        );
    }
}
