mod cursor;
mod draw_command_batcher;
mod grid;
mod intro;
mod style;
mod window;

use std::{collections::HashMap, sync::Arc, thread};

#[cfg(target_os = "macos")]
use {
    std::collections::HashSet,
    std::time::{Duration, Instant},
};

use log::{error, trace, warn};
use skia_safe::Color4f;
use tokio::sync::mpsc::unbounded_channel;
use winit::event_loop::EventLoopProxy;
use winit::window::Theme;

use crate::{
    bridge::{GridLineCell, GuiOption, NeovimHandler, RedrawEvent, WindowAnchor},
    clipboard::ClipboardHandle,
    profiling::{tracy_named_frame, tracy_zone},
    renderer::{DrawCommand, WindowDrawCommand, rendered_window::BASE_GRID_ID},
    running_tracker::RunningTracker,
    settings::Settings,
    units::{GridRect, GridSize},
    window::{EventPayload, RouteId, UserEvent, WindowCommand, WindowSettings},
};

pub use cursor::{Cursor, CursorMode, CursorShape};
pub use draw_command_batcher::DrawCommandBatcher;
pub use style::{Colors, Style, UnderlineStyle};
pub use window::*;

use intro::{IntroMessageExtender, IntroProcessing};

const MODE_CMDLINE: u64 = 4;
pub const MSG_ZINDEX: u64 = 200; // See the documenation for nvim_open_win

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SortOrder {
    pub z_index: u64,
    composition_order: u64,
}

impl Ord for SortOrder {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        // The windows are sorted primarily by z_index, and inside the z_index by
        // composition_order. The composition_order is the window creation order with the special
        // case that every time a floating window is activated it gets the highest priority for
        // its z_index.
        let a = (self.z_index, (self.composition_order as i64));
        let b = (other.z_index, (other.composition_order as i64));
        a.cmp(&b)
    }
}

impl PartialOrd for SortOrder {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct AnchorInfo {
    pub anchor_grid_id: u64,
    pub anchor_type: WindowAnchor,
    pub anchor_left: f64,
    pub anchor_top: f64,
    pub sort_order: SortOrder,
}

#[cfg(target_os = "macos")]
#[derive(Clone, Debug)]
struct MatchParenCandidate {
    row: u64,
    column: u64,
    text: Option<String>,
    is_cursor: bool,
}

#[cfg(target_os = "macos")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum MatchParenKind {
    Paren,
    Bracket,
    Brace,
    Angle,
}

impl WindowAnchor {
    fn modified_top_left(
        &self,
        grid_left: f64,
        grid_top: f64,
        width: u64,
        height: u64,
    ) -> (f64, f64) {
        match self {
            WindowAnchor::NorthWest => (grid_left, grid_top),
            WindowAnchor::NorthEast => (grid_left - width as f64, grid_top),
            WindowAnchor::SouthWest => (grid_left, grid_top - height as f64),
            WindowAnchor::SouthEast => (grid_left - width as f64, grid_top - height as f64),
            WindowAnchor::Absolute => (grid_left, grid_top),
        }
    }
}

pub struct Editor {
    pub windows: HashMap<u64, Window>,
    pub cursor: Cursor,
    pub defined_styles: HashMap<u64, Arc<Style>>,
    pub mode_list: Vec<CursorMode>,
    pub draw_command_batcher: DrawCommandBatcher,
    pub current_mode_index: Option<u64>,
    pub ui_ready: bool,
    event_loop_proxy: EventLoopProxy<EventPayload>,
    route_id: RouteId,
    #[allow(dead_code)]
    settings: Arc<Settings>,
    composition_order: u64,
    intro_message_extender: IntroMessageExtender,
    #[cfg(target_os = "macos")]
    match_paren_highlight_ids: HashSet<u64>,
    #[cfg(target_os = "macos")]
    last_match_paren_flash: Option<(u64, u64, u64, Instant)>,
    #[cfg(target_os = "macos")]
    match_paren_cache: HashMap<u64, HashMap<(u64, u64), Option<String>>>,
    #[cfg(target_os = "macos")]
    match_paren_dirty: bool,
    #[cfg(target_os = "macos")]
    match_paren_cache_cleared_in_batch: bool,
}

impl Editor {
    pub fn new(
        route_id: RouteId,
        event_loop_proxy: EventLoopProxy<EventPayload>,
        settings: Arc<Settings>,
    ) -> Self {
        Editor {
            windows: HashMap::new(),
            cursor: Cursor::new(),
            defined_styles: HashMap::new(),
            #[cfg(target_os = "macos")]
            match_paren_highlight_ids: HashSet::new(),
            #[cfg(target_os = "macos")]
            last_match_paren_flash: None,
            #[cfg(target_os = "macos")]
            match_paren_cache: HashMap::new(),
            #[cfg(target_os = "macos")]
            match_paren_dirty: false,
            #[cfg(target_os = "macos")]
            match_paren_cache_cleared_in_batch: false,
            mode_list: Vec::new(),
            draw_command_batcher: DrawCommandBatcher::new(),
            current_mode_index: None,
            ui_ready: false,
            settings,
            event_loop_proxy,
            route_id,
            composition_order: 0,
            intro_message_extender: IntroMessageExtender::new(),
        }
    }

    fn send_window_command(&self, command: WindowCommand) {
        let payload = EventPayload::for_route(UserEvent::WindowCommand(command), self.route_id);
        let _ = self.event_loop_proxy.send_event(payload);
    }

    pub fn handle_redraw_event(&mut self, event: RedrawEvent) {
        match event {
            RedrawEvent::SetTitle { mut title } => {
                tracy_zone!("EditorSetTitle");
                if title.is_empty() {
                    title = "Neovide".to_string()
                }
                self.send_window_command(WindowCommand::TitleChanged(title));
            }
            RedrawEvent::ModeInfoSet { cursor_modes } => {
                tracy_zone!("EditorModeInfoSet");
                self.mode_list = cursor_modes;
                if let Some(current_mode_i) = self.current_mode_index {
                    if let Some(current_mode) = self.mode_list.get(current_mode_i as usize) {
                        self.cursor.change_mode(current_mode, &self.defined_styles)
                    }
                }
            }
            RedrawEvent::OptionSet { gui_option } => {
                tracy_zone!("EditorOptionSet");
                self.set_option(gui_option);
            }
            RedrawEvent::ModeChange { mode, mode_index } => {
                tracy_zone!("ModeChange");
                if let Some(cursor_mode) = self.mode_list.get(mode_index as usize) {
                    self.cursor.change_mode(cursor_mode, &self.defined_styles);
                    self.current_mode_index = Some(mode_index)
                } else {
                    self.current_mode_index = None
                }
                self.draw_command_batcher.queue(DrawCommand::ModeChanged(mode));
            }
            RedrawEvent::MouseOn => {
                tracy_zone!("EditorMouseOn");
                self.send_window_command(WindowCommand::SetMouseEnabled(true));
            }
            RedrawEvent::MouseOff => {
                tracy_zone!("EditorMouseOff");
                self.send_window_command(WindowCommand::SetMouseEnabled(false));
            }
            RedrawEvent::BusyStart => {
                tracy_zone!("EditorBusyStart");
                trace!("Cursor off");
                self.cursor.enabled = false;
            }
            RedrawEvent::BusyStop => {
                tracy_zone!("EditorBusyStop");
                trace!("Cursor on");
                self.cursor.enabled = true;
            }
            RedrawEvent::Flush => {
                tracy_zone!("EditorFlush");
                trace!("Image flushed");
                tracy_named_frame!("neovim draw command flush");
                self.send_cursor_info();

                {
                    trace!("send_batch");
                    self.draw_command_batcher.send_batch(self.route_id, &self.event_loop_proxy);
                }

                #[cfg(target_os = "macos")]
                self.maybe_flash_match_paren_from_cache();

                #[cfg(target_os = "macos")]
                {
                    self.match_paren_cache_cleared_in_batch = false;
                }
            }
            RedrawEvent::DefaultColorsSet { colors } => {
                tracy_zone!("EditorDefaultColorsSet");
                self.send_window_command(WindowCommand::ThemeChanged(window_theme_for_background(
                    colors.background,
                )));

                self.draw_command_batcher
                    .queue(DrawCommand::DefaultStyleChanged(Style::new(colors)));
                self.redraw_screen();
                self.draw_command_batcher.send_batch(self.route_id, &self.event_loop_proxy);
            }
            RedrawEvent::HighlightAttributesDefine { id, style, name } => {
                tracy_zone!("EditorHighlightAttributesDefine");
                self.defined_styles.insert(id, Arc::new(style));

                #[cfg(target_os = "macos")]
                self.update_match_paren_highlight(id, name.as_deref());

                #[cfg(not(target_os = "macos"))]
                let _ = name;
            }
            RedrawEvent::HighlightGroupSet { name, id } => {
                tracy_zone!("EditorHighlightGroupSet");
                #[cfg(target_os = "macos")]
                if name.starts_with("MatchParen") {
                    self.register_match_paren_highlight_id(id);
                }

                #[cfg(not(target_os = "macos"))]
                let _ = (name, id);
            }
            RedrawEvent::CursorGoto { grid, column: left, row: top } => {
                tracy_zone!("EditorCursorGoto");
                self.set_cursor_position(grid, left, top);
            }
            RedrawEvent::Resize { grid, width, height } => {
                tracy_zone!("EditorResize");
                self.resize_window(grid, width, height);
            }
            RedrawEvent::GridLine { grid, row, column_start, cells } => {
                tracy_zone!("EditorGridLine");
                self.set_ui_ready();
                self.draw_grid_line(grid, row, column_start, &cells);
                self.handle_intro_banner_for_line(grid, row, &cells);
            }
            RedrawEvent::GridHighlight { grid, row, column_start, column_end, highlight_id } => {
                tracy_zone!("EditorGridHighlight");
                #[cfg(target_os = "macos")]
                self.handle_match_paren_grid_highlight(
                    grid,
                    row,
                    column_start,
                    column_end,
                    highlight_id,
                );

                #[cfg(not(target_os = "macos"))]
                let _ = (grid, row, column_start, column_end, highlight_id);
            }
            RedrawEvent::Clear { grid } => {
                tracy_zone!("EditorClear");
                let window = self.windows.get_mut(&grid);
                if let Some(window) = window {
                    window.clear(&mut self.draw_command_batcher);
                }
                #[cfg(target_os = "macos")]
                {
                    self.match_paren_cache.remove(&grid);
                    self.match_paren_dirty = false;
                    self.match_paren_cache_cleared_in_batch = false;
                }
                self.intro_message_extender.reset(grid);
            }
            RedrawEvent::Destroy { grid } => {
                tracy_zone!("EditorDestroy");
                self.intro_message_extender.reset(grid);
                #[cfg(target_os = "macos")]
                {
                    self.match_paren_cache.remove(&grid);
                    self.match_paren_dirty = false;
                    self.match_paren_cache_cleared_in_batch = false;
                }
                self.close_window(grid)
            }
            RedrawEvent::Scroll { grid, top, bottom, left, right, rows, columns } => {
                tracy_zone!("EditorScroll");
                #[cfg(target_os = "macos")]
                {
                    self.match_paren_cache.remove(&grid);
                    self.match_paren_dirty = false;
                    self.match_paren_cache_cleared_in_batch = false;
                }
                let window = self.windows.get_mut(&grid);
                if let Some(window) = window {
                    window.scroll_region(
                        &mut self.draw_command_batcher,
                        GridRect::from_min_max((left, top), (right, bottom)),
                        GridSize::new(columns, rows),
                    );
                }
            }
            RedrawEvent::WindowPosition { grid, start_row, start_column, width, height } => {
                tracy_zone!("EditorWindowPosition");
                self.set_window_position(grid, start_column, start_row, width, height)
            }
            RedrawEvent::WindowFloatPosition {
                grid,
                anchor,
                anchor_grid,
                anchor_column: anchor_left,
                anchor_row: anchor_top,
                z_index,
                comp_index,
                screen_row,
                screen_col,
                ..
            } => {
                tracy_zone!("EditorWindowFloatPosition");
                let anchor_type = if comp_index.is_some() {
                    WindowAnchor::Absolute
                } else {
                    self.composition_order += 1;
                    anchor
                };
                let sort_order = SortOrder {
                    z_index,
                    composition_order: comp_index.unwrap_or(self.composition_order),
                };
                let anchor = AnchorInfo {
                    anchor_grid_id: anchor_grid,
                    anchor_type,
                    anchor_left,
                    anchor_top,
                    sort_order,
                };
                self.set_window_float_position(grid, anchor, screen_col, screen_row)
            }
            RedrawEvent::WindowHide { grid } => {
                tracy_zone!("EditorWindowHide");
                let window = self.windows.get_mut(&grid);
                if let Some(window) = window {
                    window.anchor_info = None;
                    window.hide(&mut self.draw_command_batcher);
                }
            }
            RedrawEvent::WindowClose { grid } => {
                tracy_zone!("EditorWindowClose");
                self.close_window(grid)
            }
            RedrawEvent::MessageSetPosition {
                grid, row, scrolled, z_index, comp_index, ..
            } => {
                tracy_zone!("EditorMessageSetPosition");
                self.set_message_position(grid, row, scrolled, z_index, comp_index)
            }
            RedrawEvent::WindowViewport {
                grid,
                // Don't send viewport events if they don't have a scroll delta
                scroll_delta: Some(scroll_delta),
                ..
            } => {
                tracy_zone!("EditorWindowViewport");
                self.set_ui_ready();
                self.draw_command_batcher.queue(DrawCommand::Window {
                    grid_id: grid,
                    command: WindowDrawCommand::Viewport { scroll_delta },
                });
            }
            RedrawEvent::WindowViewportMargins { grid, top, bottom, left, right } => {
                tracy_zone!("EditorWindowViewportMargins");
                self.draw_command_batcher.queue(DrawCommand::Window {
                    grid_id: grid,
                    command: WindowDrawCommand::ViewportMargins { top, bottom, left, right },
                });
            }
            // Interpreting suspend as a window minimize request
            RedrawEvent::Suspend => {
                self.send_window_command(WindowCommand::Minimize);
            }
            RedrawEvent::NeovideSetRedraw(enable) => {
                self.draw_command_batcher.set_enabled(enable, self.route_id, &self.event_loop_proxy)
            }
            RedrawEvent::NeovideIntroBannerAllowed(allowed) => {
                self.intro_message_extender.set_sponsor_allowed(
                    allowed,
                    &mut self.windows,
                    &mut self.draw_command_batcher,
                );
            }
            _ => {}
        };
    }

    fn close_window(&mut self, grid: u64) {
        if let Some(window) = self.windows.remove(&grid) {
            window.close(&mut self.draw_command_batcher);
        }
    }

    fn resize_window(&mut self, grid: u64, width: u64, height: u64) {
        if let Some(window) = self.windows.get_mut(&grid) {
            window.resize(&mut self.draw_command_batcher, (width, height));
            if let Some(anchor_info) = &window.anchor_info {
                let anchor_info = anchor_info.clone();

                self.set_window_float_position(grid, anchor_info, None, None)
            }
        } else {
            let window = Window::new(
                grid,
                WindowType::Editor,
                None,
                (0.0, 0.0),
                (width, height),
                &mut self.draw_command_batcher,
            );
            if grid != BASE_GRID_ID {
                // Non-root multigrid windows can be resized before Neovim tells us where
                // they actually belong. Keep those placeholder grids hidden until a later
                // win_pos, win_float_pos, or msg_set_pos explicitly shows them.
                window.hide(&mut self.draw_command_batcher);
            }
            self.windows.insert(grid, window);
        }
    }

    fn set_window_position(
        &mut self,
        grid: u64,
        start_left: u64,
        start_top: u64,
        width: u64,
        height: u64,
    ) {
        if let Some(window) = self.windows.get_mut(&grid) {
            window.position(
                &mut self.draw_command_batcher,
                None,
                (width, height),
                (start_left as f64, start_top as f64),
            );
            window.show(&mut self.draw_command_batcher);
        } else {
            let new_window = Window::new(
                grid,
                WindowType::Editor,
                None,
                (start_left as f64, start_top as f64),
                (width, height),
                &mut self.draw_command_batcher,
            );
            self.windows.insert(grid, new_window);
        }
    }

    fn set_window_float_position(
        &mut self,
        grid: u64,
        anchor: AnchorInfo,
        screen_col: Option<u64>,
        screen_row: Option<u64>,
    ) {
        if anchor.anchor_grid_id == grid {
            warn!("NeoVim requested a window to float relative to itself. This is not supported.");
            return;
        }

        let parent_position = self.get_window_top_left(anchor.anchor_grid_id);
        if let Some(window) = self.windows.get_mut(&grid) {
            let width = window.get_width();
            let height = window.get_height();
            let neovim_composed = anchor.anchor_type == WindowAnchor::Absolute;
            let (left, top, sort_order) =
                if neovim_composed {
                    // NOTE: screen_col is None when the window is just resized
                    if let (Some(screen_col), Some(screen_row)) = (screen_col, screen_row) {
                        (screen_col as f64, screen_row as f64, anchor.sort_order.clone())
                    } else {
                        let (left, top) = window.get_grid_position();
                        (left, top, anchor.sort_order.clone())
                    }
                } else {
                    let (mut modified_left, mut modified_top) = anchor
                        .anchor_type
                        .modified_top_left(anchor.anchor_left, anchor.anchor_top, width, height);

                    if let Some((parent_left, parent_top)) = parent_position {
                        modified_left += parent_left;
                        modified_top += parent_top;
                    }

                    // Only update the sort order if it's the first position request (no anchor_info), or
                    // the z_index changes
                    let sort_order = if let Some(old_anchor) = &window.anchor_info {
                        if anchor.sort_order.z_index == old_anchor.sort_order.z_index {
                            old_anchor.sort_order.clone()
                        } else {
                            anchor.sort_order.clone()
                        }
                    } else {
                        anchor.sort_order.clone()
                    };
                    (modified_left, modified_top, sort_order)
                };
            let mut anchor = anchor;
            anchor.sort_order = sort_order;

            window.position(
                &mut self.draw_command_batcher,
                Some(anchor),
                (width, height),
                (left, top),
            );
            window.show(&mut self.draw_command_batcher);
        } else {
            error!("Attempted to float window that does not exist.");
        }
    }

    fn set_message_position(
        &mut self,
        grid: u64,
        grid_top: u64,
        scrolled: bool,
        z_index: Option<u64>,
        comp_index: Option<u64>,
    ) {
        // HACK: workaround https://github.com/neovide/neovide/issues/3150 by ignoring grid id 0.
        // The real grid id should always be something else. But Neovim 0.11.3 sends an extra
        // msg_set_pos with grid id 0.
        if grid == 0 {
            return;
        }
        let z_index = z_index.unwrap_or(MSG_ZINDEX); // From the Neovim source code
        let parent_width = self.windows.get(&1).map(|parent| parent.get_width()).unwrap_or(1);

        let anchor_info = AnchorInfo {
            anchor_grid_id: 1, // Base Grid
            anchor_type: WindowAnchor::NorthWest,
            anchor_left: 0.0,
            anchor_top: grid_top as f64,
            sort_order: SortOrder {
                z_index,
                composition_order: comp_index.unwrap_or(self.composition_order),
            },
        };

        if let Some(window) = self.windows.get_mut(&grid) {
            window.window_type = WindowType::Message { scrolled };
            window.position(
                &mut self.draw_command_batcher,
                Some(anchor_info),
                (parent_width, window.get_height()),
                (0.0, grid_top as f64),
            );
            window.show(&mut self.draw_command_batcher);
        } else {
            let new_window = Window::new(
                grid,
                WindowType::Message { scrolled },
                Some(anchor_info),
                (0.0, grid_top as f64),
                (parent_width, 1),
                &mut self.draw_command_batcher,
            );
            self.windows.insert(grid, new_window);
        }
    }

    fn get_window_top_left(&self, grid: u64) -> Option<(f64, f64)> {
        let window = self.windows.get(&grid)?;
        let window_anchor_info = &window.anchor_info;

        match window_anchor_info {
            Some(AnchorInfo { anchor_type: WindowAnchor::Absolute, .. }) => {
                Some(window.get_grid_position())
            }
            Some(anchor_info) => {
                let (parent_anchor_left, parent_anchor_top) =
                    self.get_window_top_left(anchor_info.anchor_grid_id)?;

                let (anchor_modified_left, anchor_modified_top) =
                    anchor_info.anchor_type.modified_top_left(
                        anchor_info.anchor_left,
                        anchor_info.anchor_top,
                        window.get_width(),
                        window.get_height(),
                    );

                Some((
                    parent_anchor_left + anchor_modified_left,
                    parent_anchor_top + anchor_modified_top,
                ))
            }
            None => Some(window.get_grid_position()),
        }
    }

    fn set_cursor_position(&mut self, grid: u64, grid_left: u64, grid_top: u64) {
        let mut window = self.windows.get_mut(&grid);
        if let Some(window) = &mut window {
            if let Some(anchor) = window.anchor_info.as_mut() {
                // Neovim moves a window to the top of the layer each time the cursor enters it, so do the same here as well
                self.composition_order += 1;
                anchor.sort_order.composition_order = self.composition_order;
                self.draw_command_batcher.queue(DrawCommand::Window {
                    grid_id: grid,
                    command: WindowDrawCommand::SortOrder(anchor.sort_order.clone()),
                });
            }
        }

        if self.settings.get::<WindowSettings>().cursor_hack {
            if let Some(Window { window_type: WindowType::Message { .. }, .. }) = window {
                // When the user presses ":" to type a command, the cursor is sent to the gutter
                // in position 1 (right after the ":"). In all other cases, we want to skip
                // positioning to avoid confusing movements.
                let intentional = grid_left == 1;
                // If the cursor was already in this message, we can still move within it.
                let already_there = self.cursor.parent_window_id == grid;
                // This ^ check alone is a bit buggy though, since it fails when the cursor is
                // technically still in the edit window but "temporarily" at the cmdline. (#1207)
                let using_cmdline =
                    self.current_mode_index.map(|current| current == MODE_CMDLINE).unwrap_or(false);

                if !intentional && !already_there && !using_cmdline {
                    trace!(
                        "Cursor unexpectedly sent to message buffer {grid} ({grid_left}, {grid_top})"
                    );
                    return;
                }
            }
        }

        self.cursor.parent_window_id = grid;
        self.cursor.grid_position = (grid_left, grid_top);
    }

    fn draw_grid_line(&mut self, grid: u64, row: u64, column_start: u64, cells: &[GridLineCell]) {
        #[cfg(target_os = "macos")]
        self.update_match_paren_cache_from_grid_line(grid, row, column_start, cells);

        if let Some(window) = self.windows.get_mut(&grid) {
            window.draw_grid_line(
                &mut self.draw_command_batcher,
                row,
                column_start,
                cells.to_vec(),
                &self.defined_styles,
            );
        }
    }

    #[cfg(target_os = "macos")]
    fn reset_match_paren_cache_state(&mut self) {
        self.match_paren_cache.clear();
        self.match_paren_dirty = false;
        self.match_paren_cache_cleared_in_batch = false;
    }

    #[cfg(target_os = "macos")]
    fn update_match_paren_highlight_id(&mut self, id: u64, name: &str) {
        if name.starts_with("MatchParen") {
            log::info!("MatchParen highlight id defined: {id} ({name})");
            self.match_paren_highlight_ids.insert(id);
        } else {
            self.match_paren_highlight_ids.remove(&id);
        }

        self.reset_match_paren_cache_state();
    }

    #[cfg(target_os = "macos")]
    fn update_match_paren_highlight(&mut self, id: u64, name: Option<&str>) {
        let Some(name) = name else {
            return;
        };

        self.update_match_paren_highlight_id(id, name);
    }

    #[cfg(target_os = "macos")]
    fn register_match_paren_highlight_id(&mut self, id: u64) {
        self.match_paren_highlight_ids.insert(id);
        self.reset_match_paren_cache_state();
    }

    #[cfg(target_os = "macos")]
    fn grid_cell_text(&self, grid: u64, row: u64, column: u64) -> Option<String> {
        self.windows.get(&grid).and_then(|window| {
            let (text, _, _) = window.get_cursor_grid_cell(column, row);
            (!text.is_empty()).then_some(text)
        })
    }

    #[cfg(target_os = "macos")]
    fn match_paren_expected_char(&self) -> Option<char> {
        let (cursor_column, cursor_row) = self.cursor.grid_position;
        let cursor_grid = self.cursor.parent_window_id;
        if let Some(text) = self.grid_cell_text(cursor_grid, cursor_row, cursor_column) {
            if let Some(expected_char) = Self::match_paren_expected_char_from_text(&text) {
                return Some(expected_char);
            }
        }

        if let Some(left_column) = cursor_column.checked_sub(1) {
            if let Some(text) = self.grid_cell_text(cursor_grid, cursor_row, left_column) {
                if let Some(expected_char) = Self::match_paren_expected_char_from_text(&text) {
                    return Some(expected_char);
                }
            }
        }

        Self::match_paren_expected_char_from_text(&self.cursor.grid_cell.0)
    }

    #[cfg(target_os = "macos")]
    fn text_matches_expected_char(text: &Option<String>, expected: char) -> bool {
        text.as_deref().and_then(|value| value.chars().next()) == Some(expected)
    }

    #[cfg(target_os = "macos")]
    fn match_paren_expected_char_from_text(text: &str) -> Option<char> {
        let cursor_char = text.chars().next()?;
        match cursor_char {
            '(' => Some(')'),
            ')' => Some('('),
            '[' => Some(']'),
            ']' => Some('['),
            '{' => Some('}'),
            '}' => Some('{'),
            '<' => Some('>'),
            '>' => Some('<'),
            _ => None,
        }
    }

    #[cfg(target_os = "macos")]
    fn match_paren_kind_for_char(cursor_char: char) -> Option<MatchParenKind> {
        match cursor_char {
            '(' | ')' => Some(MatchParenKind::Paren),
            '[' | ']' => Some(MatchParenKind::Bracket),
            '{' | '}' => Some(MatchParenKind::Brace),
            '<' | '>' => Some(MatchParenKind::Angle),
            _ => None,
        }
    }

    #[cfg(target_os = "macos")]
    fn remove_match_paren_cache_range(
        &mut self,
        grid: u64,
        row: u64,
        column_start: u64,
        column_end: u64,
    ) {
        let Some(cache) = self.match_paren_cache.get_mut(&grid) else {
            return;
        };

        let mut to_remove = Vec::new();
        for &(cached_row, cached_column) in cache.keys() {
            if cached_row == row && cached_column >= column_start && cached_column < column_end {
                to_remove.push((cached_row, cached_column));
            }
        }

        for key in to_remove {
            cache.remove(&key);
        }

        if cache.is_empty() {
            self.match_paren_cache.remove(&grid);
        }
    }

    #[cfg(target_os = "macos")]
    fn update_match_paren_cache_range_with_text(
        &mut self,
        grid: u64,
        row: u64,
        column_start: u64,
        column_end: u64,
        highlight_id: u64,
        text: Option<&str>,
    ) {
        if !self.match_paren_highlight_ids.contains(&highlight_id) {
            return;
        }

        let cache = self.match_paren_cache.entry(grid).or_default();
        let value = text.map(str::to_string);
        for column in column_start..column_end {
            cache.insert((row, column), value.clone());
        }
    }

    #[cfg(target_os = "macos")]
    fn update_match_paren_cache_range_from_grid(
        &mut self,
        grid: u64,
        row: u64,
        column_start: u64,
        column_end: u64,
        highlight_id: u64,
    ) {
        if !self.match_paren_highlight_ids.contains(&highlight_id) {
            self.remove_match_paren_cache_range(grid, row, column_start, column_end);
            return;
        }

        let mut values = Vec::new();
        for column in column_start..column_end {
            let text = self.grid_cell_text(grid, row, column);
            values.push((column, text));
        }

        let cache = self.match_paren_cache.entry(grid).or_default();
        for (column, text) in values {
            cache.insert((row, column), text);
        }
    }

    #[cfg(target_os = "macos")]
    fn handle_match_paren_grid_highlight(
        &mut self,
        grid: u64,
        row: u64,
        column_start: u64,
        column_end: u64,
        highlight_id: u64,
    ) {
        let is_match_paren = self.match_paren_highlight_ids.contains(&highlight_id);
        if is_match_paren && !self.match_paren_cache_cleared_in_batch {
            self.match_paren_cache.clear();
            self.match_paren_cache_cleared_in_batch = true;
        }

        self.update_match_paren_cache_range_from_grid(
            grid,
            row,
            column_start,
            column_end,
            highlight_id,
        );

        if is_match_paren {
            self.match_paren_dirty = true;
        }
    }

    #[cfg(target_os = "macos")]
    fn match_paren_candidates_from_cache(&self, grid: u64) -> Vec<MatchParenCandidate> {
        if self.cursor.parent_window_id != grid {
            return Vec::new();
        }

        let Some(cache) = self.match_paren_cache.get(&grid) else {
            return Vec::new();
        };

        let cursor_column = self.cursor.grid_position.0;
        let cursor_row = self.cursor.grid_position.1;
        let cursor_span = if self.cursor.double_width { 2 } else { 1 };

        cache
            .iter()
            .map(|(&(row, column), text)| {
                let text = text.clone().or_else(|| self.grid_cell_text(grid, row, column));

                let is_cursor = row == cursor_row
                    && column >= cursor_column
                    && column < cursor_column.saturating_add(cursor_span);

                MatchParenCandidate { row, column, text, is_cursor }
            })
            .collect()
    }

    #[cfg(target_os = "macos")]
    fn match_paren_candidate_from_cache(&self, grid: u64) -> Option<MatchParenCandidate> {
        let candidates = self.match_paren_candidates_from_cache(grid);
        self.select_match_paren_candidate(candidates)
    }

    #[cfg(target_os = "macos")]
    fn select_match_paren_candidate(
        &self,
        candidates: Vec<MatchParenCandidate>,
    ) -> Option<MatchParenCandidate> {
        if candidates.is_empty() {
            return None;
        }

        let cursor_column = self.cursor.grid_position.0;
        let cursor_row = self.cursor.grid_position.1;
        let distance = |candidate: &MatchParenCandidate| {
            (candidate.row.abs_diff(cursor_row), candidate.column.abs_diff(cursor_column))
        };

        let expected = self.match_paren_expected_char().or_else(|| {
            candidates
                .iter()
                .find_map(|candidate| {
                    candidate.is_cursor.then(|| {
                        candidate
                            .text
                            .as_deref()
                            .and_then(Self::match_paren_expected_char_from_text)
                    })
                })
                .flatten()
        });

        let mut non_cursor: Vec<MatchParenCandidate> =
            candidates.into_iter().filter(|candidate| !candidate.is_cursor).collect();

        if non_cursor.is_empty() {
            return None;
        }

        let expected = expected?;

        let expected_kind = Self::match_paren_kind_for_char(expected);
        if let Some(expected_kind) = expected_kind {
            non_cursor.retain(|candidate| {
                candidate
                    .text
                    .as_deref()
                    .and_then(|value| value.chars().next())
                    .and_then(Self::match_paren_kind_for_char)
                    == Some(expected_kind)
            });
        }

        let matching_candidates: Vec<MatchParenCandidate> = non_cursor
            .into_iter()
            .filter(|candidate| Self::text_matches_expected_char(&candidate.text, expected))
            .collect();

        if matching_candidates.is_empty() {
            return None;
        }

        matching_candidates.into_iter().min_by_key(distance)
    }

    #[cfg(target_os = "macos")]
    fn update_match_paren_cache_from_grid_line(
        &mut self,
        grid: u64,
        row: u64,
        column_start: u64,
        cells: &[GridLineCell],
    ) {
        if self.match_paren_highlight_ids.is_empty() {
            return;
        }

        let mut column = column_start;
        let mut current_highlight = None;
        let mut saw_match_paren = false;

        for cell in cells {
            current_highlight = cell.highlight_id.or(current_highlight);
            let repeat = cell.repeat.unwrap_or(1);
            if repeat == 0 {
                continue;
            }

            let column_end = column.saturating_add(repeat);
            let Some(highlight_id) = current_highlight else {
                column = column_end;
                continue;
            };

            if self.match_paren_highlight_ids.contains(&highlight_id) {
                if !self.match_paren_cache_cleared_in_batch {
                    self.match_paren_cache.clear();
                    self.match_paren_cache_cleared_in_batch = true;
                }
                saw_match_paren = true;
            }

            let text = if cell.text.is_empty() { None } else { Some(cell.text.as_str()) };

            self.update_match_paren_cache_range_with_text(
                grid,
                row,
                column,
                column_end,
                highlight_id,
                text,
            );

            column = column_end;
        }

        if !saw_match_paren {
            return;
        }

        self.match_paren_dirty = true;
    }

    #[cfg(target_os = "macos")]
    fn maybe_flash_match_paren(&mut self, grid: u64, row: u64, column: u64, text: Option<String>) {
        if self.cursor.parent_window_id == grid && self.cursor.grid_position.1 == row {
            let cursor_column = self.cursor.grid_position.0;
            let cursor_span = if self.cursor.double_width { 2 } else { 1 };
            let cursor_end = cursor_column.saturating_add(cursor_span);
            if column >= cursor_column && column < cursor_end {
                return;
            }
        }

        const MATCH_PAREN_FLASH_MIN_INTERVAL: Duration = Duration::from_millis(200);
        let now = Instant::now();
        let match_position = (grid, row, column);
        let should_flash = !matches!(
            self.last_match_paren_flash,
            Some((last_grid, last_row, last_column, last_time))
                if (last_grid, last_row, last_column) == match_position
                    && now.duration_since(last_time) < MATCH_PAREN_FLASH_MIN_INTERVAL
        );

        self.last_match_paren_flash = Some((grid, row, column, now));
        if should_flash {
            self.send_window_command(WindowCommand::HighlightMatchingPair {
                grid,
                row,
                column,
                text,
            });
        }
    }

    #[cfg(target_os = "macos")]
    fn maybe_flash_match_paren_from_cache(&mut self) {
        if !self.match_paren_dirty {
            return;
        }

        if !self.settings.get::<WindowSettings>().highlight_matching_pair {
            return;
        }

        self.match_paren_dirty = false;

        if self.match_paren_expected_char().is_none() {
            return;
        }

        let grid = self.cursor.parent_window_id;
        if let Some(candidate) = self.match_paren_candidate_from_cache(grid) {
            self.maybe_flash_match_paren(grid, candidate.row, candidate.column, candidate.text);
        }
    }

    fn handle_intro_banner_for_line(&mut self, grid: u64, row: u64, cells: &[GridLineCell]) {
        if !self.intro_message_extender.sponsor_allowed() {
            return;
        }

        match self.intro_message_extender.preprocess_line(grid, cells) {
            IntroProcessing::Skip => return,
            IntroProcessing::ClearBanner => {
                self.intro_message_extender.maybe_hide_banner(
                    grid,
                    &mut self.windows,
                    &mut self.draw_command_batcher,
                );
                return;
            }
            IntroProcessing::Process => {}
        }

        let line_text = grid_line_cells_to_text(cells);
        let sponsor_banner_row =
            self.intro_message_extender.banner_injection_row(grid, row, &line_text);

        self.maybe_inject_intro_banner(grid, sponsor_banner_row);
        if sponsor_banner_row.is_none() {
            self.intro_message_extender.maybe_hide_banner(
                grid,
                &mut self.windows,
                &mut self.draw_command_batcher,
            );
        }
    }

    fn maybe_inject_intro_banner(&mut self, grid: u64, banner_row: Option<u64>) {
        if let Some(start_row) = banner_row {
            if let Some(window) = self.windows.get_mut(&grid) {
                self.intro_message_extender.inject_banner(
                    grid,
                    window,
                    start_row,
                    &mut self.draw_command_batcher,
                );
            }
        }
    }

    fn send_cursor_info(&mut self) {
        tracy_zone!("send_cursor_info");
        let (grid_left, grid_top) = self.cursor.grid_position;
        if let Some(window) = self.windows.get(&self.cursor.parent_window_id) {
            let (character, style, double_width) = window.get_cursor_grid_cell(grid_left, grid_top);
            self.cursor.grid_cell = (character, style);
            self.cursor.double_width = double_width;
        } else {
            self.cursor.double_width = false;
            self.cursor.grid_cell = (" ".to_string(), None);
        }
        self.draw_command_batcher.queue(DrawCommand::UpdateCursor(self.cursor.clone()));
    }

    fn set_option(&mut self, gui_option: GuiOption) {
        trace!("Option set {:?}", &gui_option);

        match gui_option {
            GuiOption::GuiFont(guifont) => {
                if guifont == *"*" {
                    self.send_window_command(WindowCommand::ListAvailableFonts);
                } else {
                    self.draw_command_batcher.queue(DrawCommand::FontChanged(guifont));

                    self.redraw_screen();
                }
            }
            GuiOption::LineSpace(linespace) => {
                self.draw_command_batcher.queue(DrawCommand::LineSpaceChanged(linespace as f32));

                self.redraw_screen();
            }
            _ => (),
        }
    }

    fn redraw_screen(&mut self) {
        for window in self.windows.values() {
            window.redraw(&mut self.draw_command_batcher);
        }
    }

    fn set_ui_ready(&mut self) {
        if !self.ui_ready {
            self.ui_ready = true;
            self.draw_command_batcher.queue(DrawCommand::UIReady);
        }
    }
}

fn grid_line_cells_to_text(cells: &[GridLineCell]) -> String {
    let mut text = String::new();
    for cell in cells {
        let repeat = cell.repeat.unwrap_or(1);
        for _ in 0..repeat {
            text.push_str(&cell.text);
        }
    }
    text
}

pub fn start_editor_handler(
    route_id: RouteId,
    event_loop_proxy: EventLoopProxy<EventPayload>,
    running_tracker: RunningTracker,
    settings: Arc<Settings>,
    clipboard: ClipboardHandle,
) -> NeovimHandler {
    let (redraw_event_sender, mut redraw_event_receiver) = unbounded_channel();
    let (ui_command_sender, ui_command_receiver) = unbounded_channel();
    let handler = NeovimHandler::new(
        redraw_event_sender,
        ui_command_sender,
        ui_command_receiver,
        event_loop_proxy.clone(),
        running_tracker,
        route_id,
        settings.clone(),
        clipboard,
    );
    thread::spawn(move || {
        let mut editor = Editor::new(route_id, event_loop_proxy.clone(), settings.clone());

        while let Some(editor_command) = redraw_event_receiver.blocking_recv() {
            editor.handle_redraw_event(editor_command);
        }
    });
    handler
}

/// Based on formula in https://graphicdesign.stackexchange.com/questions/62368/automatically-select-a-foreground-color-based-on-a-background-color
/// Check if the color is light or dark
fn is_light_color(color: &Color4f) -> bool {
    0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b > 0.5
}

/// Get the proper dark/light theme for a background_color.
fn window_theme_for_background(background_color: Option<Color4f>) -> Option<Theme> {
    background_color?;

    match background_color.unwrap() {
        color if is_light_color(&color) => Some(Theme::Light),
        _ => Some(Theme::Dark),
    }
}
