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::{motion::Motion, normal::move_cursor, 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,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct FindCommand {
    pub query: String,
    pub backwards: bool,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ReplaceCommand {
    pub query: String,
}

#[derive(Debug, Default)]
struct Replacement {
    search: String,
    replacement: String,
    should_replace_all: bool,
    is_case_sensitive: bool,
}

impl_actions!(
    vim,
    [MoveToNext, MoveToPrev, Search, FindCommand, ReplaceCommand]
);
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);

    cx.add_action(find_command);
    cx.add_action(replace_command);
}

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.take_count(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_replacement(None, cx);
                        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.take_count(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);
    });
}

fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
    let pane = workspace.active_pane().clone();
    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| {
                if !search_bar.show(cx) {
                    return None;
                }
                let mut query = action.query.clone();
                if query == "" {
                    query = search_bar.query(cx);
                };

                search_bar.activate_search_mode(SearchMode::Regex, cx);
                Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
            });
            let Some(search) = search else { return };
            let search_bar = search_bar.downgrade();
            let direction = if action.backwards {
                Direction::Prev
            } else {
                Direction::Next
            };
            cx.spawn(|_, mut cx| async move {
                search.await?;
                search_bar.update(&mut cx, |search_bar, cx| {
                    search_bar.select_match(direction, 1, cx)
                })?;
                anyhow::Ok(())
            })
            .detach_and_log_err(cx);
        }
    })
}

fn replace_command(
    workspace: &mut Workspace,
    action: &ReplaceCommand,
    cx: &mut ViewContext<Workspace>,
) {
    let replacement = parse_replace_all(&action.query);
    let pane = workspace.active_pane().clone();
    pane.update(cx, |pane, cx| {
        let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
            return;
        };
        let search = search_bar.update(cx, |search_bar, cx| {
            if !search_bar.show(cx) {
                return None;
            }

            let mut options = SearchOptions::default();
            if replacement.is_case_sensitive {
                options.set(SearchOptions::CASE_SENSITIVE, true)
            }
            let search = if replacement.search == "" {
                search_bar.query(cx)
            } else {
                replacement.search
            };

            search_bar.set_replacement(Some(&replacement.replacement), cx);
            search_bar.activate_search_mode(SearchMode::Regex, cx);
            Some(search_bar.search(&search, Some(options), cx))
        });
        let Some(search) = search else { return };
        let search_bar = search_bar.downgrade();
        cx.spawn(|_, mut cx| async move {
            search.await?;
            search_bar.update(&mut cx, |search_bar, cx| {
                if replacement.should_replace_all {
                    search_bar.select_last_match(cx);
                    search_bar.replace_all(&Default::default(), cx);
                    Vim::update(cx, |vim, cx| {
                        move_cursor(
                            vim,
                            Motion::StartOfLine {
                                display_lines: false,
                            },
                            None,
                            cx,
                        )
                    })
                }
            })?;
            anyhow::Ok(())
        })
        .detach_and_log_err(cx);
    })
}

// convert a vim query into something more usable by zed.
// we don't attempt to fully convert between the two regex syntaxes,
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
fn parse_replace_all(query: &str) -> Replacement {
    let mut chars = query.chars();
    if Some('%') != chars.next() || Some('s') != chars.next() {
        return Replacement::default();
    }

    let Some(delimeter) = chars.next() else {
        return Replacement::default();
    };

    let mut search = String::new();
    let mut replacement = String::new();
    let mut flags = String::new();

    let mut buffer = &mut search;

    let mut escaped = false;
    // 0 - parsing search
    // 1 - parsing replacement
    // 2 - parsing flags
    let mut phase = 0;

    for c in chars {
        if escaped {
            escaped = false;
            if phase == 1 && c.is_digit(10) {
                buffer.push('$')
            // unescape escaped parens
            } else if phase == 0 && c == '(' || c == ')' {
            } else if c != delimeter {
                buffer.push('\\')
            }
            buffer.push(c)
        } else if c == '\\' {
            escaped = true;
        } else if c == delimeter {
            if phase == 0 {
                buffer = &mut replacement;
                phase = 1;
            } else if phase == 1 {
                buffer = &mut flags;
                phase = 2;
            } else {
                break;
            }
        } else {
            // escape unescaped parens
            if phase == 0 && c == '(' || c == ')' {
                buffer.push('\\')
            }
            buffer.push(c)
        }
    }

    let mut replacement = Replacement {
        search,
        replacement,
        should_replace_all: true,
        is_case_sensitive: true,
    };

    for c in flags.chars() {
        match c {
            'g' | 'I' => {}
            'c' | 'n' => replacement.should_replace_all = false,
            'i' => replacement.is_case_sensitive = false,
            _ => {}
        }
    }

    replacement
}

#[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_text_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");
    }
}
