use std::{iter::once, mem};

use hir::Semantics;
use ide_db::{base_db::FileRange, RootDatabase};
use itertools::Itertools;
use syntax::{
    algo, ast, match_ast, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange,
    TokenAtOffset,
};
use text_edit::{TextEdit, TextEditBuilder};

#[derive(Copy, Clone, Debug)]
pub enum Direction {
    Up,
    Down,
}

// Feature: Move Item
//
// Move item under cursor or selection up and down.
//
// |===
// | Editor  | Action Name
//
// | VS Code | **Rust Analyzer: Move item up**
// | VS Code | **Rust Analyzer: Move item down**
// |===
//
// image::https://user-images.githubusercontent.com/48062697/113065576-04298180-91b1-11eb-91ce-4505e99ed598.gif[]
pub(crate) fn move_item(
    db: &RootDatabase,
    range: FileRange,
    direction: Direction,
) -> Option<TextEdit> {
    let sema = Semantics::new(db);
    let file = sema.parse(range.file_id);

    let item = if range.range.is_empty() {
        SyntaxElement::Token(pick_best(file.syntax().token_at_offset(range.range.start()))?)
    } else {
        file.syntax().covering_element(range.range)
    };

    find_ancestors(item, direction, range.range)
}

fn find_ancestors(item: SyntaxElement, direction: Direction, range: TextRange) -> Option<TextEdit> {
    let root = match item {
        SyntaxElement::Node(node) => node,
        SyntaxElement::Token(token) => token.parent()?,
    };

    let movable = [
        SyntaxKind::ARG_LIST,
        SyntaxKind::GENERIC_PARAM_LIST,
        SyntaxKind::GENERIC_ARG_LIST,
        SyntaxKind::VARIANT_LIST,
        SyntaxKind::TYPE_BOUND_LIST,
        SyntaxKind::MATCH_ARM,
        SyntaxKind::PARAM,
        SyntaxKind::LET_STMT,
        SyntaxKind::EXPR_STMT,
        SyntaxKind::IF_EXPR,
        SyntaxKind::FOR_EXPR,
        SyntaxKind::LOOP_EXPR,
        SyntaxKind::WHILE_EXPR,
        SyntaxKind::RETURN_EXPR,
        SyntaxKind::MATCH_EXPR,
        SyntaxKind::MACRO_CALL,
        SyntaxKind::TYPE_ALIAS,
        SyntaxKind::TRAIT,
        SyntaxKind::IMPL,
        SyntaxKind::MACRO_DEF,
        SyntaxKind::STRUCT,
        SyntaxKind::UNION,
        SyntaxKind::ENUM,
        SyntaxKind::FN,
        SyntaxKind::MODULE,
        SyntaxKind::USE,
        SyntaxKind::STATIC,
        SyntaxKind::CONST,
        SyntaxKind::MACRO_RULES,
        SyntaxKind::MACRO_DEF,
    ];

    let ancestor = once(root.clone())
        .chain(root.ancestors())
        .find(|ancestor| movable.contains(&ancestor.kind()))?;

    move_in_direction(&ancestor, direction, range)
}

fn move_in_direction(
    node: &SyntaxNode,
    direction: Direction,
    range: TextRange,
) -> Option<TextEdit> {
    match_ast! {
        match node {
            ast::ArgList(it) => swap_sibling_in_list(node, it.args(), range, direction),
            ast::GenericParamList(it) => swap_sibling_in_list(node, it.generic_params(), range, direction),
            ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction),
            ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction),
            ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction),
            _ => Some(replace_nodes(range, node, &match direction {
                Direction::Up => node.prev_sibling(),
                Direction::Down => node.next_sibling(),
            }?))
        }
    }
}

fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
    node: &SyntaxNode,
    list: I,
    range: TextRange,
    direction: Direction,
) -> Option<TextEdit> {
    let list_lookup = list
        .tuple_windows()
        .filter(|(l, r)| match direction {
            Direction::Up => r.syntax().text_range().contains_range(range),
            Direction::Down => l.syntax().text_range().contains_range(range),
        })
        .next();

    if let Some((l, r)) = list_lookup {
        Some(replace_nodes(range, l.syntax(), r.syntax()))
    } else {
        // Cursor is beyond any movable list item (for example, on curly brace in enum).
        // It's not necessary, that parent of list is movable (arg list's parent is not, for example),
        // and we have to continue tree traversal to find suitable node.
        find_ancestors(SyntaxElement::Node(node.parent()?), direction, range)
    }
}

fn replace_nodes<'a>(
    range: TextRange,
    mut first: &'a SyntaxNode,
    mut second: &'a SyntaxNode,
) -> TextEdit {
    let cursor_offset = if range.is_empty() {
        // FIXME: `applySnippetTextEdits` does not support non-empty selection ranges
        if first.text_range().contains_range(range) {
            Some(range.start() - first.text_range().start())
        } else if second.text_range().contains_range(range) {
            mem::swap(&mut first, &mut second);
            Some(range.start() - first.text_range().start())
        } else {
            None
        }
    } else {
        None
    };

    let first_with_cursor = match cursor_offset {
        Some(offset) => {
            let mut item_text = first.text().to_string();
            item_text.insert_str(offset.into(), "$0");
            item_text
        }
        None => first.text().to_string(),
    };

    let mut edit = TextEditBuilder::default();

    algo::diff(first, second).into_text_edit(&mut edit);
    edit.replace(second.text_range(), first_with_cursor);

    edit.finish()
}

fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
    return tokens.max_by_key(priority);

    fn priority(n: &SyntaxToken) -> usize {
        match n.kind() {
            SyntaxKind::IDENT | SyntaxKind::LIFETIME_IDENT => 2,
            kind if kind.is_trivia() => 0,
            _ => 1,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::fixture;
    use expect_test::{expect, Expect};

    use crate::Direction;

    fn check(ra_fixture: &str, expect: Expect, direction: Direction) {
        let (analysis, range) = fixture::range(ra_fixture);
        let edit = analysis.move_item(range, direction).unwrap().unwrap_or_default();
        let mut file = analysis.file_text(range.file_id).unwrap().to_string();
        edit.apply(&mut file);
        expect.assert_eq(&file);
    }

    #[test]
    fn test_moves_match_arm_up() {
        check(
            r#"
fn main() {
    match true {
        true => {
            println!("Hello, world");
        },
        false =>$0$0 {
            println!("Test");
        }
    };
}
            "#,
            expect![[r#"
fn main() {
    match true {
        false =>$0 {
            println!("Test");
        },
        true => {
            println!("Hello, world");
        }
    };
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_match_arm_down() {
        check(
            r#"
fn main() {
    match true {
        true =>$0$0 {
            println!("Hello, world");
        },
        false => {
            println!("Test");
        }
    };
}
            "#,
            expect![[r#"
fn main() {
    match true {
        false => {
            println!("Test");
        },
        true =>$0 {
            println!("Hello, world");
        }
    };
}
            "#]],
            Direction::Down,
        );
    }

    #[test]
    fn test_nowhere_to_move() {
        check(
            r#"
fn main() {
    match true {
        true =>$0$0 {
            println!("Hello, world");
        },
        false => {
            println!("Test");
        }
    };
}
            "#,
            expect![[r#"
fn main() {
    match true {
        true => {
            println!("Hello, world");
        },
        false => {
            println!("Test");
        }
    };
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_let_stmt_up() {
        check(
            r#"
fn main() {
    let test = 123;
    let test2$0$0 = 456;
}
            "#,
            expect![[r#"
fn main() {
    let test2$0 = 456;
    let test = 123;
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_expr_up() {
        check(
            r#"
fn main() {
    println!("Hello, world");
    println!("All I want to say is...");$0$0
}
            "#,
            expect![[r#"
fn main() {
    println!("All I want to say is...");$0
    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
        check(
            r#"
fn main() {
    println!("Hello, world");

    if true {
        println!("Test");
    }$0$0
}
            "#,
            expect![[r#"
fn main() {
    if true {
        println!("Test");
    }$0

    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
        check(
            r#"
fn main() {
    println!("Hello, world");

    for i in 0..10 {
        println!("Test");
    }$0$0
}
            "#,
            expect![[r#"
fn main() {
    for i in 0..10 {
        println!("Test");
    }$0

    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
        check(
            r#"
fn main() {
    println!("Hello, world");

    loop {
        println!("Test");
    }$0$0
}
            "#,
            expect![[r#"
fn main() {
    loop {
        println!("Test");
    }$0

    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
        check(
            r#"
fn main() {
    println!("Hello, world");

    while true {
        println!("Test");
    }$0$0
}
            "#,
            expect![[r#"
fn main() {
    while true {
        println!("Test");
    }$0

    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
        check(
            r#"
fn main() {
    println!("Hello, world");

    return 123;$0$0
}
            "#,
            expect![[r#"
fn main() {
    return 123;$0

    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_nowhere_to_move_stmt() {
        check(
            r#"
fn main() {
    println!("All I want to say is...");$0$0
    println!("Hello, world");
}
            "#,
            expect![[r#"
fn main() {
    println!("All I want to say is...");
    println!("Hello, world");
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_move_item() {
        check(
            r#"
fn main() {}

fn foo() {}$0$0
            "#,
            expect![[r#"
fn foo() {}$0

fn main() {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_move_impl_up() {
        check(
            r#"
struct Yay;

trait Wow {}

impl Wow for Yay $0$0{}
            "#,
            expect![[r#"
struct Yay;

impl Wow for Yay $0{}

trait Wow {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_move_use_up() {
        check(
            r#"
use std::vec::Vec;
use std::collections::HashMap$0$0;
            "#,
            expect![[r#"
use std::collections::HashMap$0;
use std::vec::Vec;
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_match_expr_up() {
        check(
            r#"
fn main() {
    let test = 123;

    $0match test {
        456 => {},
        _ => {}
    };$0
}
            "#,
            expect![[r#"
fn main() {
    match test {
        456 => {},
        _ => {}
    };

    let test = 123;
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_param() {
        check(
            r#"
fn test(one: i32, two$0$0: u32) {}

fn main() {
    test(123, 456);
}
            "#,
            expect![[r#"
fn test(two$0: u32, one: i32) {}

fn main() {
    test(123, 456);
}
            "#]],
            Direction::Up,
        );
        check(
            r#"
fn f($0$0arg: u8, arg2: u16) {}
            "#,
            expect![[r#"
fn f(arg2: u16, $0arg: u8) {}
            "#]],
            Direction::Down,
        );
    }

    #[test]
    fn test_moves_arg_up() {
        check(
            r#"
fn test(one: i32, two: u32) {}

fn main() {
    test(123, 456$0$0);
}
            "#,
            expect![[r#"
fn test(one: i32, two: u32) {}

fn main() {
    test(456$0, 123);
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_arg_down() {
        check(
            r#"
fn test(one: i32, two: u32) {}

fn main() {
    test(123$0$0, 456);
}
            "#,
            expect![[r#"
fn test(one: i32, two: u32) {}

fn main() {
    test(456, 123$0);
}
            "#]],
            Direction::Down,
        );
    }

    #[test]
    fn test_nowhere_to_move_arg() {
        check(
            r#"
fn test(one: i32, two: u32) {}

fn main() {
    test(123$0$0, 456);
}
            "#,
            expect![[r#"
fn test(one: i32, two: u32) {}

fn main() {
    test(123, 456);
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_generic_param_up() {
        check(
            r#"
struct Test<A, B$0$0>(A, B);

fn main() {}
            "#,
            expect![[r#"
struct Test<B$0, A>(A, B);

fn main() {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_generic_arg_up() {
        check(
            r#"
struct Test<A, B>(A, B);

fn main() {
    let t = Test::<i32, &str$0$0>(123, "yay");
}
            "#,
            expect![[r#"
struct Test<A, B>(A, B);

fn main() {
    let t = Test::<&str$0, i32>(123, "yay");
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_variant_up() {
        check(
            r#"
enum Hello {
    One,
    Two$0$0
}

fn main() {}
            "#,
            expect![[r#"
enum Hello {
    Two$0,
    One
}

fn main() {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_moves_type_bound_up() {
        check(
            r#"
trait One {}

trait Two {}

fn test<T: One + Two$0$0>(t: T) {}

fn main() {}
            "#,
            expect![[r#"
trait One {}

trait Two {}

fn test<T: Two$0 + One>(t: T) {}

fn main() {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_prioritizes_trait_items() {
        check(
            r#"
struct Test;

trait Yay {
    type One;

    type Two;

    fn inner();
}

impl Yay for Test {
    type One = i32;

    type Two = u32;

    fn inner() {$0$0
        println!("Mmmm");
    }
}
            "#,
            expect![[r#"
struct Test;

trait Yay {
    type One;

    type Two;

    fn inner();
}

impl Yay for Test {
    type One = i32;

    fn inner() {$0
        println!("Mmmm");
    }

    type Two = u32;
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_weird_nesting() {
        check(
            r#"
fn test() {
    mod hello {
        fn inner() {}
    }

    mod hi {$0$0
        fn inner() {}
    }
}
            "#,
            expect![[r#"
fn test() {
    mod hi {$0
        fn inner() {}
    }

    mod hello {
        fn inner() {}
    }
}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_cursor_at_item_start() {
        check(
            r#"
$0$0#[derive(Debug)]
enum FooBar {
    Foo,
    Bar,
}

fn main() {}
            "#,
            expect![[r#"
fn main() {}

$0#[derive(Debug)]
enum FooBar {
    Foo,
    Bar,
}
            "#]],
            Direction::Down,
        );
        check(
            r#"
$0$0enum FooBar {
    Foo,
    Bar,
}

fn main() {}
            "#,
            expect![[r#"
fn main() {}

$0enum FooBar {
    Foo,
    Bar,
}
            "#]],
            Direction::Down,
        );
        check(
            r#"
struct Test;

trait SomeTrait {}

$0$0impl SomeTrait for Test {}

fn main() {}
            "#,
            expect![[r#"
struct Test;

$0impl SomeTrait for Test {}

trait SomeTrait {}

fn main() {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn test_cursor_at_item_end() {
        check(
            r#"
enum FooBar {
    Foo,
    Bar,
}$0$0

fn main() {}
            "#,
            expect![[r#"
fn main() {}

enum FooBar {
    Foo,
    Bar,
}$0
            "#]],
            Direction::Down,
        );
        check(
            r#"
struct Test;

trait SomeTrait {}

impl SomeTrait for Test {}$0$0

fn main() {}
            "#,
            expect![[r#"
struct Test;

impl SomeTrait for Test {}$0

trait SomeTrait {}

fn main() {}
            "#]],
            Direction::Up,
        );
    }

    #[test]
    fn handles_empty_file() {
        check(r#"$0$0"#, expect![[r#""#]], Direction::Up);
    }
}