use std::{ops::Range, time::Duration};

use crate::{
    motion::Motion,
    object::Object,
    state::{Mode, Register},
    Vim, VimSettings,
};
use collections::HashMap;
use editor::{ClipboardSelection, Editor};
use gpui::Context;
use gpui::Window;
use language::Point;
use multi_buffer::MultiBufferRow;
use settings::Settings;

struct HighlightOnYank;

impl Vim {
    pub fn yank_motion(
        &mut self,
        motion: Motion,
        times: Option<usize>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.update_editor(window, cx, |vim, editor, window, cx| {
            let text_layout_details = editor.text_layout_details(window);
            editor.transact(window, cx, |editor, window, cx| {
                editor.set_clip_at_line_ends(false, cx);
                let mut original_positions: HashMap<_, _> = Default::default();
                editor.change_selections(None, window, cx, |s| {
                    s.move_with(|map, selection| {
                        let original_position = (selection.head(), selection.goal);
                        original_positions.insert(selection.id, original_position);
                        motion.expand_selection(map, selection, times, true, &text_layout_details);
                    });
                });
                vim.yank_selections_content(editor, motion.linewise(), window, cx);
                editor.change_selections(None, window, cx, |s| {
                    s.move_with(|_, selection| {
                        let (head, goal) = original_positions.remove(&selection.id).unwrap();
                        selection.collapse_to(head, goal);
                    });
                });
            });
        });
        self.exit_temporary_normal(window, cx);
    }

    pub fn yank_object(
        &mut self,
        object: Object,
        around: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.update_editor(window, cx, |vim, editor, window, cx| {
            editor.transact(window, cx, |editor, window, cx| {
                editor.set_clip_at_line_ends(false, cx);
                let mut start_positions: HashMap<_, _> = Default::default();
                editor.change_selections(None, window, cx, |s| {
                    s.move_with(|map, selection| {
                        object.expand_selection(map, selection, around);
                        let start_position = (selection.start, selection.goal);
                        start_positions.insert(selection.id, start_position);
                    });
                });
                vim.yank_selections_content(editor, false, window, cx);
                editor.change_selections(None, window, cx, |s| {
                    s.move_with(|_, selection| {
                        let (head, goal) = start_positions.remove(&selection.id).unwrap();
                        selection.collapse_to(head, goal);
                    });
                });
            });
        });
        self.exit_temporary_normal(window, cx);
    }

    pub fn yank_selections_content(
        &mut self,
        editor: &mut Editor,
        linewise: bool,
        window: &mut Window,
        cx: &mut Context<Editor>,
    ) {
        self.copy_ranges(
            editor,
            linewise,
            true,
            editor
                .selections
                .all_adjusted(cx)
                .iter()
                .map(|s| s.range())
                .collect(),
            window,
            cx,
        )
    }

    pub fn copy_selections_content(
        &mut self,
        editor: &mut Editor,
        linewise: bool,
        window: &mut Window,
        cx: &mut Context<Editor>,
    ) {
        self.copy_ranges(
            editor,
            linewise,
            false,
            editor
                .selections
                .all_adjusted(cx)
                .iter()
                .map(|s| s.range())
                .collect(),
            window,
            cx,
        )
    }

    pub(crate) fn copy_ranges(
        &mut self,
        editor: &mut Editor,
        linewise: bool,
        is_yank: bool,
        selections: Vec<Range<Point>>,
        window: &mut Window,
        cx: &mut Context<Editor>,
    ) {
        let buffer = editor.buffer().read(cx).snapshot(cx);
        self.set_mark(
            "[".to_string(),
            selections
                .iter()
                .map(|s| buffer.anchor_before(s.start))
                .collect(),
            editor.buffer(),
            window,
            cx,
        );
        self.set_mark(
            "]".to_string(),
            selections
                .iter()
                .map(|s| buffer.anchor_after(s.end))
                .collect(),
            editor.buffer(),
            window,
            cx,
        );

        let mut text = String::new();
        let mut clipboard_selections = Vec::with_capacity(selections.len());
        let mut ranges_to_highlight = Vec::new();

        {
            let mut is_first = true;
            for selection in selections.iter() {
                let mut start = selection.start;
                let end = selection.end;
                if is_first {
                    is_first = false;
                } else {
                    text.push('\n');
                }
                let initial_len = text.len();

                // if the file does not end with \n, and our line-mode selection ends on
                // that line, we will have expanded the start of the selection to ensure it
                // contains a newline (so that delete works as expected). We undo that change
                // here.
                let max_point = buffer.max_point();
                let should_adjust_start = linewise
                    && end.row == max_point.row
                    && max_point.column > 0
                    && start.row < max_point.row
                    && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
                let should_add_newline =
                    should_adjust_start || (end == max_point && max_point.column > 0 && linewise);

                if should_adjust_start {
                    start = Point::new(start.row + 1, 0);
                }

                let start_anchor = buffer.anchor_after(start);
                let end_anchor = buffer.anchor_before(end);
                ranges_to_highlight.push(start_anchor..end_anchor);

                for chunk in buffer.text_for_range(start..end) {
                    text.push_str(chunk);
                }
                if should_add_newline {
                    text.push('\n');
                }
                clipboard_selections.push(ClipboardSelection {
                    len: text.len() - initial_len,
                    is_entire_line: linewise,
                    first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
                });
            }
        }

        let selected_register = self.selected_register.take();
        Vim::update_globals(cx, |globals, cx| {
            globals.write_registers(
                Register {
                    text: text.into(),
                    clipboard_selections: Some(clipboard_selections),
                },
                selected_register,
                is_yank,
                linewise,
                cx,
            )
        });

        let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration;
        if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 {
            return;
        }

        editor.highlight_background::<HighlightOnYank>(
            &ranges_to_highlight,
            |colors| colors.editor_document_highlight_read_background,
            cx,
        );
        cx.spawn(async move |this, cx| {
            cx.background_executor()
                .timer(Duration::from_millis(highlight_duration))
                .await;
            this.update(cx, |editor, cx| {
                editor.clear_background_highlights::<HighlightOnYank>(cx)
            })
            .ok();
        })
        .detach();
    }
}
