use gpui::{actions, impl_actions, AppContext, ViewContext};
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
use serde_derive::Deserialize;
use workspace::{searchable::Direction, Pane, Workspace};

use crate::{state::SearchState, Vim};

#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoveToNext {
    #[serde(default)]
    partial_word: bool,
}

#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoveToPrev {
    #[serde(default)]
    partial_word: bool,
}

#[derive(Clone, Deserialize, PartialEq)]
pub(crate) struct Search {
    #[serde(default)]
    backwards: bool,
}

impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
actions!(vim, [SearchSubmit]);

pub(crate) fn init(cx: &mut AppContext) {
    cx.add_action(move_to_next);
    cx.add_action(move_to_prev);
    cx.add_action(search);
    cx.add_action(search_submit);
    cx.add_action(search_deploy);
}

fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
    move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
}

fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
    move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
}

fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
    let pane = workspace.active_pane().clone();
    let direction = if action.backwards {
        Direction::Prev
    } else {
        Direction::Next
    };
    Vim::update(cx, |vim, cx| {
        let count = vim.pop_number_operator(cx).unwrap_or(1);
        pane.update(cx, |pane, cx| {
            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                search_bar.update(cx, |search_bar, cx| {
                    if !search_bar.show(cx) {
                        return;
                    }
                    let query = search_bar.query(cx);

                    search_bar.select_query(cx);
                    cx.focus_self();

                    if query.is_empty() {
                        search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
                        search_bar.activate_search_mode(SearchMode::Regex, cx);
                    }
                    vim.workspace_state.search = SearchState {
                        direction,
                        count,
                        initial_query: query.clone(),
                    };
                });
            }
        })
    })
}

// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
    Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
    cx.propagate_action();
}

fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
    Vim::update(cx, |vim, cx| {
        let pane = workspace.active_pane().clone();
        pane.update(cx, |pane, cx| {
            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                search_bar.update(cx, |search_bar, cx| {
                    let state = &mut vim.workspace_state.search;
                    let mut count = state.count;
                    let direction = state.direction;

                    // in the case that the query has changed, the search bar
                    // will have selected the next match already.
                    if (search_bar.query(cx) != state.initial_query)
                        && state.direction == Direction::Next
                    {
                        count = count.saturating_sub(1)
                    }
                    state.count = 1;
                    search_bar.select_match(direction, count, cx);
                    search_bar.focus_editor(&Default::default(), cx);
                });
            }
        });
    })
}

pub fn move_to_internal(
    workspace: &mut Workspace,
    direction: Direction,
    whole_word: bool,
    cx: &mut ViewContext<Workspace>,
) {
    Vim::update(cx, |vim, cx| {
        let pane = workspace.active_pane().clone();
        let count = vim.pop_number_operator(cx).unwrap_or(1);
        pane.update(cx, |pane, cx| {
            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                let search = search_bar.update(cx, |search_bar, cx| {
                    let mut options = SearchOptions::CASE_SENSITIVE;
                    options.set(SearchOptions::WHOLE_WORD, whole_word);
                    if search_bar.show(cx) {
                        search_bar
                            .query_suggestion(cx)
                            .map(|query| search_bar.search(&query, Some(options), cx))
                    } else {
                        None
                    }
                });

                if let Some(search) = search {
                    let search_bar = search_bar.downgrade();
                    cx.spawn(|_, mut cx| async move {
                        search.await?;
                        search_bar.update(&mut cx, |search_bar, cx| {
                            search_bar.select_match(direction, count, cx)
                        })?;
                        anyhow::Ok(())
                    })
                    .detach_and_log_err(cx);
                }
            }
        });
        vim.clear_operator(cx);
    });
}

#[cfg(test)]
mod test {
    use std::sync::Arc;

    use editor::DisplayPoint;
    use search::BufferSearchBar;

    use crate::{state::Mode, test::VimTestContext};

    #[gpui::test]
    async fn test_move_to_next(
        cx: &mut gpui::TestAppContext,
        deterministic: Arc<gpui::executor::Deterministic>,
    ) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);

        cx.simulate_keystrokes(["*"]);
        deterministic.run_until_parked();
        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);

        cx.simulate_keystrokes(["*"]);
        deterministic.run_until_parked();
        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);

        cx.simulate_keystrokes(["#"]);
        deterministic.run_until_parked();
        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);

        cx.simulate_keystrokes(["#"]);
        deterministic.run_until_parked();
        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);

        cx.simulate_keystrokes(["2", "*"]);
        deterministic.run_until_parked();
        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);

        cx.simulate_keystrokes(["g", "*"]);
        deterministic.run_until_parked();
        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);

        cx.simulate_keystrokes(["n"]);
        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);

        cx.simulate_keystrokes(["g", "#"]);
        deterministic.run_until_parked();
        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
    }

    #[gpui::test]
    async fn test_search(
        cx: &mut gpui::TestAppContext,
        deterministic: Arc<gpui::executor::Deterministic>,
    ) {
        let mut cx = VimTestContext::new(cx, true).await;

        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
        cx.simulate_keystrokes(["/", "c", "c"]);

        let search_bar = cx.workspace(|workspace, cx| {
            workspace
                .active_pane()
                .read(cx)
                .toolbar()
                .read(cx)
                .item_of_type::<BufferSearchBar>()
                .expect("Buffer search bar should be deployed")
        });

        search_bar.read_with(cx.cx, |bar, cx| {
            assert_eq!(bar.query(cx), "cc");
        });

        deterministic.run_until_parked();

        cx.update_editor(|editor, cx| {
            let highlights = editor.all_background_highlights(cx);
            assert_eq!(3, highlights.len());
            assert_eq!(
                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
                highlights[0].0
            )
        });

        cx.simulate_keystrokes(["enter"]);
        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);

        // n to go to next/N to go to previous
        cx.simulate_keystrokes(["n"]);
        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
        cx.simulate_keystrokes(["shift-n"]);
        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);

        // ?<enter> to go to previous
        cx.simulate_keystrokes(["?", "enter"]);
        deterministic.run_until_parked();
        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
        cx.simulate_keystrokes(["?", "enter"]);
        deterministic.run_until_parked();
        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);

        // /<enter> to go to next
        cx.simulate_keystrokes(["/", "enter"]);
        deterministic.run_until_parked();
        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);

        // ?{search}<enter> to search backwards
        cx.simulate_keystrokes(["?", "b", "enter"]);
        deterministic.run_until_parked();
        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);

        // works with counts
        cx.simulate_keystrokes(["4", "/", "c"]);
        deterministic.run_until_parked();
        cx.simulate_keystrokes(["enter"]);
        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);

        // check that searching resumes from cursor, not previous match
        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
        cx.simulate_keystrokes(["/", "d"]);
        deterministic.run_until_parked();
        cx.simulate_keystrokes(["enter"]);
        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
        cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
        cx.simulate_keystrokes(["/", "b"]);
        deterministic.run_until_parked();
        cx.simulate_keystrokes(["enter"]);
        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
    }

    #[gpui::test]
    async fn test_non_vim_search(
        cx: &mut gpui::TestAppContext,
        deterministic: Arc<gpui::executor::Deterministic>,
    ) {
        let mut cx = VimTestContext::new(cx, false).await;
        cx.set_state("ˇone one one one", Mode::Normal);
        cx.simulate_keystrokes(["cmd-f"]);
        deterministic.run_until_parked();

        cx.assert_editor_state("«oneˇ» one one one");
        cx.simulate_keystrokes(["enter"]);
        cx.assert_editor_state("one «oneˇ» one one");
        cx.simulate_keystrokes(["shift-enter"]);
        cx.assert_editor_state("«oneˇ» one one one");
    }
}
