use ide_db::{EditionedFileId, defs::Definition, search::FileReference};
use syntax::{
    AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, T, TextRange,
    algo::{find_node_at_range, least_common_ancestor_element},
    ast::{self, HasArgList},
    syntax_editor::Element,
};

use SyntaxKind::WHITESPACE;

use crate::{
    AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder, utils::next_prev,
};

// Assist: remove_unused_param
//
// Removes unused function parameter.
//
// ```
// fn frobnicate(x: i32$0) {}
//
// fn main() {
//     frobnicate(92);
// }
// ```
// ->
// ```
// fn frobnicate() {}
//
// fn main() {
//     frobnicate();
// }
// ```
pub(crate) fn remove_unused_param(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
    let param: ast::Param = ctx.find_node_at_offset()?;
    let ident_pat = match param.pat()? {
        ast::Pat::IdentPat(it) => it,
        _ => return None,
    };
    let func = param.syntax().ancestors().find_map(ast::Fn::cast)?;
    let is_self_present =
        param.syntax().parent()?.children().find_map(ast::SelfParam::cast).is_some();

    // check if fn is in impl Trait for ..
    if func
        .syntax()
        .parent() // AssocItemList
        .and_then(|x| x.parent())
        .and_then(ast::Impl::cast)
        .is_some_and(|imp| imp.trait_().is_some())
    {
        cov_mark::hit!(trait_impl);
        return None;
    }

    let mut param_position = func.param_list()?.params().position(|it| it == param)?;
    // param_list() does not take the self param into consideration, hence this additional check
    // is required. For associated functions, param_position is incremented here. For inherent
    // calls we revet the increment below, in process_usage, as those calls will not have an
    // explicit self parameter.
    if is_self_present {
        param_position += 1;
    }
    let fn_def = {
        let func = ctx.sema.to_def(&func)?;
        Definition::Function(func)
    };

    let param_def = {
        let local = ctx.sema.to_def(&ident_pat)?;
        Definition::Local(local)
    };
    if param_def.usages(&ctx.sema).at_least_one() {
        cov_mark::hit!(keep_used);
        return None;
    }
    let parent = param.syntax().parent()?;
    acc.add(
        AssistId::refactor("remove_unused_param"),
        "Remove unused parameter",
        param.syntax().text_range(),
        |builder| {
            let editor = builder.make_editor(&parent);
            let elements = elements_to_remove(param.syntax());
            for element in elements {
                editor.delete(element);
            }
            for (file_id, references) in fn_def.usages(&ctx.sema).all() {
                process_usages(ctx, builder, file_id, references, param_position, is_self_present);
            }
            builder.add_file_edits(ctx.vfs_file_id(), editor);
        },
    )
}

fn process_usages(
    ctx: &AssistContext<'_, '_>,
    builder: &mut SourceChangeBuilder,
    editioned_file_id: EditionedFileId,
    references: Vec<FileReference>,
    arg_to_remove: usize,
    is_self_present: bool,
) {
    let source_file = ctx.sema.parse(editioned_file_id);
    let file_id = editioned_file_id.file_id(ctx.db());
    builder.edit_file(file_id);
    let possible_ranges = references
        .into_iter()
        .filter_map(|usage| process_usage(&source_file, usage, arg_to_remove, is_self_present));

    for element_range in possible_ranges {
        let Some(SyntaxElement::Node(parent)) = element_range
            .iter()
            .cloned()
            .reduce(|a, b| least_common_ancestor_element(&a, &b).unwrap().syntax_element())
        else {
            continue;
        };
        let editor = builder.make_editor(&parent);
        for element in element_range {
            editor.delete(element);
        }

        builder.add_file_edits(file_id, editor);
    }
}

fn process_usage(
    source_file: &SourceFile,
    FileReference { range, .. }: FileReference,
    mut arg_to_remove: usize,
    is_self_present: bool,
) -> Option<Vec<SyntaxElement>> {
    let call_expr_opt: Option<ast::CallExpr> = find_node_at_range(source_file.syntax(), range);
    if let Some(call_expr) = call_expr_opt {
        let call_expr_range = call_expr.expr()?.syntax().text_range();
        if !call_expr_range.contains_range(range) {
            return None;
        }

        let arg = call_expr.arg_list()?.args().nth(arg_to_remove)?;
        return Some(elements_to_remove(arg.syntax()));
    }

    let method_call_expr_opt: Option<ast::MethodCallExpr> =
        find_node_at_range(source_file.syntax(), range);
    if let Some(method_call_expr) = method_call_expr_opt {
        let method_call_expr_range = method_call_expr.name_ref()?.syntax().text_range();
        if !method_call_expr_range.contains_range(range) {
            return None;
        }

        if is_self_present {
            arg_to_remove -= 1;
        }

        let arg = method_call_expr.arg_list()?.args().nth(arg_to_remove)?;
        return Some(elements_to_remove(arg.syntax()));
    }

    None
}

pub(crate) fn range_to_remove(node: &SyntaxNode) -> TextRange {
    let up_to_comma = next_prev().find_map(|dir| {
        node.siblings_with_tokens(dir)
            .filter_map(|it| it.into_token())
            .find(|it| it.kind() == T![,])
            .map(|it| (dir, it))
    });
    if let Some((dir, token)) = up_to_comma {
        if node.next_sibling().is_some() {
            let up_to_space = token
                .siblings_with_tokens(dir)
                .skip(1)
                .take_while(|it| it.kind() == WHITESPACE)
                .last()
                .and_then(|it| it.into_token());
            return node
                .text_range()
                .cover(up_to_space.map_or(token.text_range(), |it| it.text_range()));
        }
        node.text_range().cover(token.text_range())
    } else {
        node.text_range()
    }
}

pub(crate) fn elements_to_remove(node: &SyntaxNode) -> Vec<SyntaxElement> {
    let up_to_comma = next_prev().find_map(|dir| {
        node.siblings_with_tokens(dir)
            .filter_map(|it| it.into_token())
            .find(|it| it.kind() == T![,])
            .map(|it| (dir, it))
    });
    if let Some((dir, token)) = up_to_comma {
        let after = token.siblings_with_tokens(dir).nth(1).unwrap();
        let mut result: Vec<_> =
            node.siblings_with_tokens(dir).take_while(|it| it != &after).collect();
        if node.next_sibling().is_some() {
            result.extend(
                token.siblings_with_tokens(dir).skip(1).take_while(|it| it.kind() == WHITESPACE),
            );
        }

        result
    } else {
        vec![node.syntax_element()]
    }
}

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

    use super::*;

    #[test]
    fn remove_unused() {
        check_assist(
            remove_unused_param,
            r#"
fn a() { foo(9, 2) }
fn foo(x: i32, $0y: i32) { x; }
fn b() { foo(9, 2,) }
"#,
            r#"
fn a() { foo(9) }
fn foo(x: i32) { x; }
fn b() { foo(9, ) }
"#,
        );
    }

    #[test]
    fn remove_unused_first_param() {
        check_assist(
            remove_unused_param,
            r#"
fn foo($0x: i32, y: i32) { y; }
fn a() { foo(1, 2) }
fn b() { foo(1, 2,) }
"#,
            r#"
fn foo(y: i32) { y; }
fn a() { foo(2) }
fn b() { foo(2,) }
"#,
        );
    }

    #[test]
    fn remove_unused_single_param() {
        check_assist(
            remove_unused_param,
            r#"
fn foo($0x: i32) { 0; }
fn a() { foo(1) }
fn b() { foo(1, ) }
"#,
            r#"
fn foo() { 0; }
fn a() { foo() }
fn b() { foo( ) }
"#,
        );
    }

    #[test]
    fn remove_unused_surrounded_by_params() {
        check_assist(
            remove_unused_param,
            r#"
fn foo(x: i32, $0y: i32, z: i32) { x; }
fn a() { foo(1, 2, 3) }
fn b() { foo(1, 2, 3,) }
"#,
            r#"
fn foo(x: i32, z: i32) { x; }
fn a() { foo(1, 3) }
fn b() { foo(1, 3,) }
"#,
        );
    }

    #[test]
    fn remove_unused_qualified_call() {
        check_assist(
            remove_unused_param,
            r#"
mod bar { pub fn foo(x: i32, $0y: i32) { x; } }
fn b() { bar::foo(9, 2) }
"#,
            r#"
mod bar { pub fn foo(x: i32) { x; } }
fn b() { bar::foo(9) }
"#,
        );
    }

    #[test]
    fn remove_unused_turbofished_func() {
        check_assist(
            remove_unused_param,
            r#"
pub fn foo<T>(x: T, $0y: i32) { x; }
fn b() { foo::<i32>(9, 2) }
"#,
            r#"
pub fn foo<T>(x: T) { x; }
fn b() { foo::<i32>(9) }
"#,
        );
    }

    #[test]
    fn remove_unused_generic_unused_param_func() {
        check_assist(
            remove_unused_param,
            r#"
pub fn foo<T>(x: i32, $0y: T) { x; }
fn b() { foo::<i32>(9, 2) }
fn b2() { foo(9, 2) }
"#,
            r#"
pub fn foo<T>(x: i32) { x; }
fn b() { foo::<i32>(9) }
fn b2() { foo(9) }
"#,
        );
    }

    #[test]
    fn keep_used() {
        cov_mark::check!(keep_used);
        check_assist_not_applicable(
            remove_unused_param,
            r#"
fn foo(x: i32, $0y: i32) { y; }
fn main() { foo(9, 2) }
"#,
        );
    }

    #[test]
    fn trait_impl() {
        cov_mark::check!(trait_impl);
        check_assist_not_applicable(
            remove_unused_param,
            r#"
trait Trait {
    fn foo(x: i32);
}
impl Trait for () {
    fn foo($0x: i32) {}
}
"#,
        );
    }

    #[test]
    fn remove_across_files() {
        check_assist(
            remove_unused_param,
            r#"
//- /main.rs
fn foo(x: i32, $0y: i32) { x; }

mod foo;

//- /foo.rs
use super::foo;

fn bar() {
    let _ = foo(1, 2);
}
"#,
            r#"
//- /main.rs
fn foo(x: i32) { x; }

mod foo;

//- /foo.rs
use super::foo;

fn bar() {
    let _ = foo(1);
}
"#,
        )
    }

    #[test]
    fn test_remove_method_param() {
        check_assist(
            remove_unused_param,
            r#"
struct S;
impl S { fn f(&self, $0_unused: i32) {} }
fn main() {
    S.f(92);
    S.f();
    S.f(93, 92);
    S::f(&S, 92);
}
"#,
            r#"
struct S;
impl S { fn f(&self) {} }
fn main() {
    S.f();
    S.f();
    S.f(92);
    S::f(&S);
}
"#,
        )
    }

    #[test]
    fn nested_call() {
        check_assist(
            remove_unused_param,
            r#"
fn foo(x: i32, $0y: i32) -> i32 {
    x
}

fn bar() {
    foo(1, foo(2, 3));
}
"#,
            r#"
fn foo(x: i32) -> i32 {
    x
}

fn bar() {
    foo(1);
}
"#,
        )
    }
}
