use editor::{ToOffset, movement};
use gpui::{Action, Context, Window};
use schemars::JsonSchema;
use serde::Deserialize;

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

/// Pastes text from the specified register at the cursor position.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
pub struct HelixPaste {
    #[serde(default)]
    before: bool,
}

impl Vim {
    pub fn helix_paste(
        &mut self,
        action: &HelixPaste,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.record_current_action(cx);
        self.store_visual_marks(window, cx);
        let count = Vim::take_count(cx).unwrap_or(1);
        // TODO: vim paste calls take_forced_motion here, but I don't know what that does
        // (none of the other helix_ methods call it)

        self.update_editor(cx, |vim, editor, cx| {
            if editor.read_only(cx) {
                return;
            }

            editor.transact(window, cx, |editor, window, cx| {
                editor.set_clip_at_line_ends(false, cx);

                let selected_register = vim.selected_register.take();

                let Some(register) = Vim::update_globals(cx, |globals, cx| {
                    globals.read_register(selected_register, Some(editor), cx)
                })
                .filter(|reg| !reg.text.is_empty()) else {
                    return;
                };
                let text = register.text;
                let clipboard_selections = register.clipboard_selections;

                let display_map = editor.display_snapshot(cx);
                let current_selections = editor.selections.all_adjusted_display(&display_map);

                // The clipboard can have multiple selections, and there can
                // be multiple selections. Helix zips them together, so the first
                // clipboard entry gets pasted at the first selection, the second
                // entry gets pasted at the second selection, and so on. If there
                // are more clipboard selections than selections, the extra ones
                // don't get pasted anywhere. If there are more selections than
                // clipboard selections, the last clipboard selection gets
                // pasted at all remaining selections.

                let mut edits = Vec::new();
                let mut new_selections = Vec::new();
                let mut start_offset = 0;

                let mut replacement_texts: Vec<String> = Vec::new();

                for ix in 0..current_selections.len() {
                    let to_insert = if let Some(clip_sel) =
                        clipboard_selections.as_ref().and_then(|s| s.get(ix))
                    {
                        let end_offset = start_offset + clip_sel.len;
                        let text = text[start_offset..end_offset].to_string();
                        start_offset = if clip_sel.is_entire_line {
                            end_offset
                        } else {
                            end_offset + 1
                        };
                        text
                    } else if let Some(last_text) = replacement_texts.last() {
                        // We have more current selections than clipboard selections: repeat the last one.
                        last_text.to_owned()
                    } else {
                        text.to_string()
                    };
                    replacement_texts.push(to_insert);
                }

                let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));

                for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {
                    // Helix doesn't care about the head/tail of the selection.
                    // Pasting before means pasting before the whole selection.
                    let display_point = if line_mode {
                        if action.before {
                            movement::line_beginning(&display_map, sel.start, false)
                        } else {
                            if sel.start == sel.end {
                                movement::right(
                                    &display_map,
                                    movement::line_end(&display_map, sel.end, false),
                                )
                            } else {
                                sel.end
                            }
                        }
                    } else if action.before {
                        sel.start
                    } else if sel.start == sel.end {
                        // In Helix, a single-point cursor is "on top" of a
                        // character, and pasting after means after that character.
                        // At line end this means the next line. But on an empty
                        // line there is no character, so paste at the cursor.
                        let right = movement::right(&display_map, sel.end);
                        if right.row() != sel.end.row() && sel.end.column() == 0 {
                            sel.end
                        } else {
                            right
                        }
                    } else {
                        sel.end
                    };
                    let point = display_point.to_point(&display_map);
                    let anchor = if action.before {
                        display_map.buffer_snapshot().anchor_after(point)
                    } else {
                        display_map.buffer_snapshot().anchor_before(point)
                    };
                    edits.push((point..point, to_insert.repeat(count)));
                    new_selections.push((anchor, to_insert.len() * count));
                }

                editor.edit(edits, cx);

                let snapshot = editor.buffer().read(cx).snapshot(cx);
                editor.change_selections(Default::default(), window, cx, |s| {
                    s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
                        let offset = anchor.to_offset(&snapshot);
                        if action.before {
                            offset.saturating_sub_usize(len)..offset
                        } else {
                            offset..(offset + len)
                        }
                    }));
                })
            });
        });

        self.switch_mode(Mode::HelixNormal, true, window, cx);
    }
}

#[cfg(test)]
mod test {
    use indoc::indoc;

    use gpui::ClipboardItem;

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

    #[gpui::test]
    async fn test_system_clipboard_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.enable_helix();
        cx.set_state(
            indoc! {"
            The quiˇck brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
        cx.simulate_keystrokes("p");
        cx.assert_state(
            indoc! {"
            The quic«clipboardˇ»k brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Multiple cursors with system clipboard (no metadata) pastes
        // the same text at each cursor.
        cx.set_state(
            indoc! {"
            ˇThe quick brown
            fox ˇjumps over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.write_to_clipboard(ClipboardItem::new_string("hi".to_string()));
        cx.simulate_keystrokes("p");
        cx.assert_state(
            indoc! {"
            T«hiˇ»he quick brown
            fox j«hiˇ»umps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Multiple cursors on empty lines should paste on those same lines.
        cx.set_state("ˇ\nˇ\nˇ\nend", Mode::HelixNormal);
        cx.write_to_clipboard(ClipboardItem::new_string("X".to_string()));
        cx.simulate_keystrokes("p");
        cx.assert_state("«Xˇ»\n«Xˇ»\n«Xˇ»\nend", Mode::HelixNormal);
    }

    #[gpui::test]
    async fn test_read_only_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.enable_helix();
        cx.set_state("aˇb", Mode::HelixNormal);
        cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
        cx.update_editor(|editor, _window, _cx| editor.set_read_only(true));

        cx.simulate_keystrokes("p");

        cx.assert_state("aˇb", Mode::HelixNormal);
    }

    #[gpui::test]
    async fn test_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.enable_helix();
        cx.set_state(
            indoc! {"
            The «quiˇ»ck brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        cx.simulate_keystrokes("y w p");

        cx.assert_state(
            indoc! {"
            The quick «quiˇ»brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Pasting before the selection:
        cx.set_state(
            indoc! {"
            The quick brown
            fox «jumpsˇ» over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("shift-p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox «quiˇ»jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );
    }

    #[gpui::test]
    async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.enable_helix();
        cx.set_state(
            indoc! {"
            The quiˇck brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        cx.simulate_keystrokes("y");

        // Pasting before the selection:
        cx.set_state(
            indoc! {"
            The quick brown
            fox jumpsˇ over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("shift-p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox jumps«cˇ» over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Pasting after the selection:
        cx.set_state(
            indoc! {"
            The quick brown
            fox jumpsˇ over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox jumps «cˇ»over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Pasting after the selection at the end of a line:
        cx.set_state(
            indoc! {"
            The quick brown
            fox jumps overˇ
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox jumps over
            «cˇ»the lazy dog."},
            Mode::HelixNormal,
        );
    }

    #[gpui::test]
    async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.enable_helix();
        // Select two blocks of text.
        cx.set_state(
            indoc! {"
            The «quiˇ»ck brown
            fox ju«mpsˇ» over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("y");

        // Only one cursor: only the first block gets pasted.
        cx.set_state(
            indoc! {"
            ˇThe quick brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("shift-p");
        cx.assert_state(
            indoc! {"
            «quiˇ»The quick brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Two cursors: both get pasted.
        cx.set_state(
            indoc! {"
            ˇThe ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("shift-p");
        cx.assert_state(
            indoc! {"
            «quiˇ»The «mpsˇ»quick brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Three cursors: the second yanked block is duplicated.
        cx.set_state(
            indoc! {"
            ˇThe ˇquick brown
            fox jumpsˇ over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("shift-p");
        cx.assert_state(
            indoc! {"
            «quiˇ»The «mpsˇ»quick brown
            fox jumps«mpsˇ» over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // Again with three cursors. All three should be pasted twice.
        cx.set_state(
            indoc! {"
            ˇThe ˇquick brown
            fox jumpsˇ over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("2 shift-p");
        cx.assert_state(
            indoc! {"
            «quiquiˇ»The «mpsmpsˇ»quick brown
            fox jumps«mpsmpsˇ» over
            the lazy dog."},
            Mode::HelixNormal,
        );
    }

    #[gpui::test]
    async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;
        cx.enable_helix();
        cx.set_state(
            indoc! {"
            The quick brow«n
            ˇ»fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        cx.simulate_keystrokes("y shift-p");

        cx.assert_state(
            indoc! {"
            «n
            ˇ»The quick brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // In line mode, if we're in the middle of a line then pasting before pastes on
        // the line before.
        cx.set_state(
            indoc! {"
            The quick brown
            fox jumpsˇ over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("shift-p");
        cx.assert_state(
            indoc! {"
            The quick brown
            «n
            ˇ»fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        // In line mode, if we're in the middle of a line then pasting after pastes on
        // the line after.
        cx.set_state(
            indoc! {"
            The quick brown
            fox jumpsˇ over
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox jumps over
            «n
            ˇ»the lazy dog."},
            Mode::HelixNormal,
        );

        // If we're currently at the end of a line, "the line after"
        // means right after the cursor.
        cx.set_state(
            indoc! {"
            The quick brown
            fox jumps overˇ
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox jumps over
            «n
            ˇ»the lazy dog."},
            Mode::HelixNormal,
        );

        cx.set_state(
            indoc! {"

            The quick brown
            fox jumps overˇ
            the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("x y up up p");
        cx.assert_state(
            indoc! {"

            «fox jumps over
            ˇ»The quick brown
            fox jumps over
            the lazy dog."},
            Mode::HelixNormal,
        );

        cx.set_state(
            indoc! {"
            «The quick brown
            fox jumps over
            ˇ»the lazy dog."},
            Mode::HelixNormal,
        );
        cx.simulate_keystrokes("y p p");
        cx.assert_state(
            indoc! {"
            The quick brown
            fox jumps over
            The quick brown
            fox jumps over
            «The quick brown
            fox jumps over
            ˇ»the lazy dog."},
            Mode::HelixNormal,
        );
    }
}
