use editor::movement;
use gpui::{actions, ViewContext, WindowContext};
use language::Point;
use workspace::Workspace;

use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};

actions!(vim, [Substitute, SubstituteLine]);

pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
    workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| {
        Vim::update(cx, |vim, cx| {
            vim.start_recording(cx);
            let count = vim.take_count(cx);
            substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
        })
    });

    workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
        Vim::update(cx, |vim, cx| {
            vim.start_recording(cx);
            if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
                vim.switch_mode(Mode::VisualLine, false, cx)
            }
            let count = vim.take_count(cx);
            substitute(vim, count, true, cx)
        })
    });
}

pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
    vim.update_active_editor(cx, |editor, cx| {
        editor.set_clip_at_line_ends(false, cx);
        editor.transact(cx, |editor, cx| {
            let text_layout_details = editor.text_layout_details(cx);
            editor.change_selections(None, cx, |s| {
                s.move_with(|map, selection| {
                    if selection.start == selection.end {
                        Motion::Right.expand_selection(
                            map,
                            selection,
                            count,
                            true,
                            &text_layout_details,
                        );
                    }
                    if line_mode {
                        // in Visual mode when the selection contains the newline at the end
                        // of the line, we should exclude it.
                        if !selection.is_empty() && selection.end.column() == 0 {
                            selection.end = movement::left(map, selection.end);
                        }
                        Motion::CurrentLine.expand_selection(
                            map,
                            selection,
                            None,
                            false,
                            &text_layout_details,
                        );
                        if let Some((point, _)) = (Motion::FirstNonWhitespace {
                            display_lines: false,
                        })
                        .move_point(
                            map,
                            selection.start,
                            selection.goal,
                            None,
                            &text_layout_details,
                        ) {
                            selection.start = point;
                        }
                    }
                })
            });
            copy_selections_content(editor, line_mode, cx);
            let selections = editor.selections.all::<Point>(cx).into_iter();
            let edits = selections.map(|selection| (selection.start..selection.end, ""));
            editor.edit(edits, cx);
        });
    });
    vim.switch_mode(Mode::Insert, true, cx);
}

#[cfg(test)]
mod test {
    use crate::{
        state::Mode,
        test::{NeovimBackedTestContext, VimTestContext},
    };
    use indoc::indoc;

    #[gpui::test]
    async fn test_substitute(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // supports a single cursor
        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
        cx.simulate_keystrokes(["s", "x"]);
        cx.assert_editor_state("xˇbc\n");

        // supports a selection
        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
        cx.assert_editor_state("a«bcˇ»\n");
        cx.simulate_keystrokes(["s", "x"]);
        cx.assert_editor_state("axˇ\n");

        // supports counts
        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
        cx.simulate_keystrokes(["2", "s", "x"]);
        cx.assert_editor_state("xˇc\n");

        // supports multiple cursors
        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
        cx.simulate_keystrokes(["2", "s", "x"]);
        cx.assert_editor_state("axˇdexˇg\n");

        // does not read beyond end of line
        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
        cx.simulate_keystrokes(["5", "s", "x"]);
        cx.assert_editor_state("xˇ\n");

        // it handles multibyte characters
        cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
        cx.simulate_keystrokes(["4", "s"]);
        cx.assert_editor_state("ˇ\n");

        // should transactionally undo selection changes
        cx.simulate_keystrokes(["escape", "u"]);
        cx.assert_editor_state("ˇcàfé\n");

        // it handles visual line mode
        cx.set_state(
            indoc! {"
            alpha
              beˇta
            gamma"},
            Mode::Normal,
        );
        cx.simulate_keystrokes(["shift-v", "s"]);
        cx.assert_editor_state(indoc! {"
            alpha
              ˇ
            gamma"});
    }

    #[gpui::test]
    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("The quick ˇbrown").await;
        cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
        cx.assert_shared_state("The quick ˇ").await;

        cx.set_shared_state(indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog"})
            .await;
        cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
        cx.assert_shared_state(indoc! {"
            The ˇver
            the lazy dog"})
            .await;

        let cases = cx.each_marked_position(indoc! {"
            The ˇquick brown
            fox jumps ˇover
            the ˇlazy dog"});
        for initial_state in cases {
            cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
                .await;
            cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
                .await;
        }
    }

    #[gpui::test]
    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx)
            .await
            .binding(["shift-v", "c"]);
        cx.assert(indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog"})
            .await;
        // Test pasting code copied on change
        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
        cx.assert_state_matches().await;

        cx.assert_all(indoc! {"
            The quick brown
            fox juˇmps over
            the laˇzy dog"})
            .await;
        let mut cx = cx.binding(["shift-v", "j", "c"]);
        cx.assert(indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog"})
            .await;
        // Test pasting code copied on delete
        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
        cx.assert_state_matches().await;

        cx.assert_all(indoc! {"
            The quick brown
            fox juˇmps over
            the laˇzy dog"})
            .await;
    }

    #[gpui::test]
    async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        let initial_state = indoc! {"
                    The quick brown
                    fox juˇmps over
                    the lazy dog
                    "};

        // normal mode
        cx.set_shared_state(initial_state).await;
        cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
        cx.assert_shared_state(indoc! {"
            The quick brown
            oˇ
            the lazy dog
            "})
            .await;

        // visual mode
        cx.set_shared_state(initial_state).await;
        cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
            .await;
        cx.assert_shared_state(indoc! {"
            oˇ
            the lazy dog
            "})
            .await;

        // visual block mode
        cx.set_shared_state(initial_state).await;
        cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
            .await;
        cx.assert_shared_state(indoc! {"
            The quick brown
            oˇ
            "})
            .await;

        // visual mode including newline
        cx.set_shared_state(initial_state).await;
        cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
            .await;
        cx.assert_shared_state(indoc! {"
            The quick brown
            oˇ
            the lazy dog
            "})
            .await;

        // indentation
        cx.set_neovim_option("shiftwidth=4").await;
        cx.set_shared_state(initial_state).await;
        cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
            .await;
        cx.assert_shared_state(indoc! {"
            The quick brown
                oˇ
            the lazy dog
            "})
            .await;
    }
}
