use crate::output::{CharacterChunk, Output};
use crate::panes::terminal_character::AnsiCode;
use crate::panes::PaneId;
use crate::tab::Pane;
use crate::ui::boundaries::Boundaries;
use crate::ui::pane_boundaries_frame::{FrameParams, StackListEntry};
use crate::ClientId;
use std::collections::{HashMap, HashSet};
use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style};
use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::PaneGeom;

pub fn dim_character_chunks(character_chunks: &mut Vec<CharacterChunk>) {
    for chunk in character_chunks.iter_mut() {
        for terminal_character in chunk.terminal_characters.iter_mut() {
            terminal_character.styles.update(|styles| {
                styles.dim = Some(AnsiCode::On);
            });
        }
    }
}

pub struct PaneContentsAndUi<'a> {
    pane: &'a mut Box<dyn Pane>,
    output: &'a mut Output,
    style: Style,
    focused_clients: Vec<ClientId>,
    multiple_users_exist_in_session: bool,
    z_index: Option<usize>,
    pane_is_stacked_under: bool,
    pane_is_stacked_over: bool,
    should_draw_pane_frames: bool,
    mouse_is_hovering_over_pane_for_clients: HashSet<ClientId>,
    current_pane_group: HashMap<ClientId, Vec<PaneId>>,
    show_help_text: bool,
    omit_title: bool,
    frame_geom_override: Option<PaneGeom>,
    stack_list_entry_width: Option<usize>,
    stack_list_entry_is_selected: bool,
    stack_list_entry_stack_is_focused: bool,
    blank_title: bool,
    mouse_scroll_resize: bool,
    mouse_hover_tips: bool,
    dimmed_for_clients: HashSet<ClientId>,
}

impl<'a> PaneContentsAndUi<'a> {
    pub fn new(
        pane: &'a mut Box<dyn Pane>,
        output: &'a mut Output,
        style: Style,
        active_panes: &HashMap<ClientId, PaneId>,
        multiple_users_exist_in_session: bool,
        z_index: Option<usize>,
        pane_is_stacked_under: bool,
        pane_is_stacked_over: bool,
        should_draw_pane_frames: bool,
        mouse_hover_pane_id: &HashMap<ClientId, PaneId>,
        current_pane_group: HashMap<ClientId, Vec<PaneId>>,
        show_help_text: bool,
        omit_title: bool,
        mouse_scroll_resize: bool,
        mouse_hover_tips: bool,
        dimmed_for_clients: HashSet<ClientId>,
    ) -> Self {
        let mut focused_clients: Vec<ClientId> = active_panes
            .iter()
            .filter(|(_c_id, p_id)| **p_id == pane.pid())
            .map(|(c_id, _p_id)| *c_id)
            .collect();
        focused_clients.sort_unstable();
        let mouse_is_hovering_over_pane_for_clients = mouse_hover_pane_id
            .iter()
            .filter_map(|(client_id, pane_id)| {
                if pane_id == &pane.pid() {
                    Some(*client_id)
                } else {
                    None
                }
            })
            .collect();
        PaneContentsAndUi {
            pane,
            output,
            style,
            focused_clients,
            multiple_users_exist_in_session,
            z_index,
            pane_is_stacked_under,
            pane_is_stacked_over,
            should_draw_pane_frames,
            mouse_is_hovering_over_pane_for_clients,
            current_pane_group,
            show_help_text,
            omit_title,
            frame_geom_override: None,
            stack_list_entry_width: None,
            stack_list_entry_is_selected: false,
            stack_list_entry_stack_is_focused: false,
            blank_title: false,
            mouse_scroll_resize,
            mouse_hover_tips,
            dimmed_for_clients,
        }
    }
    fn frame_is_dimmed_for_client(&self, client_id: ClientId) -> bool {
        self.dimmed_for_clients.contains(&client_id) && !self.focused_clients.contains(&client_id)
    }
    pub fn set_frame_geom_override(&mut self, frame_geom_override: Option<PaneGeom>) {
        self.frame_geom_override = frame_geom_override;
    }
    pub fn set_blank_title(&mut self, blank_title: bool) {
        self.blank_title = blank_title;
    }
    pub fn set_stack_list_entry(
        &mut self,
        stack_list_entry_width: Option<usize>,
        stack_list_entry_is_selected: bool,
        stack_list_entry_stack_is_focused: bool,
    ) {
        self.stack_list_entry_width = stack_list_entry_width;
        self.stack_list_entry_is_selected = stack_list_entry_is_selected;
        self.stack_list_entry_stack_is_focused = stack_list_entry_stack_is_focused;
    }
    pub fn render_pane_contents_to_multiple_clients(
        &mut self,
        clients: impl Iterator<Item = ClientId>,
    ) -> Result<()> {
        let err_context = "failed to render pane contents to multiple clients";

        // here we drop the fake cursors so that their lines will be updated
        // and we can clear them from the UI below
        drop(self.pane.drain_fake_cursors());

        if let Some((character_chunks, raw_vte_output, sixel_image_chunks, kitty_image_chunks)) =
            self.pane.render(None).context(err_context)?
        {
            let clients: Vec<ClientId> = clients.collect();
            self.output
                .add_character_chunks_to_multiple_clients(
                    character_chunks,
                    clients.iter().copied(),
                    self.z_index,
                )
                .context(err_context)?;
            self.output.add_sixel_image_chunks_to_multiple_clients(
                sixel_image_chunks,
                clients.iter().copied(),
                self.z_index,
            );
            self.output.add_kitty_image_chunks_to_multiple_clients(
                self.pane.pid(),
                kitty_image_chunks,
                clients.iter().copied(),
                self.z_index,
            );
            if let Some(raw_vte_output) = raw_vte_output {
                if !raw_vte_output.is_empty() {
                    self.output.add_post_vte_instruction_to_multiple_clients(
                        clients.iter().copied(),
                        &format!(
                            "\u{1b}[{};{}H\u{1b}[m{}",
                            self.pane.y() + 1,
                            self.pane.x() + 1,
                            raw_vte_output
                        ),
                    );
                }
            }
        }
        Ok(())
    }
    pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) -> Result<()> {
        let err_context = || format!("failed to render pane contents for client {client_id}");

        if let Some((character_chunks, raw_vte_output, sixel_image_chunks, kitty_image_chunks)) =
            self.pane
                .render(Some(client_id))
                .with_context(err_context)?
        {
            self.output
                .add_character_chunks_to_client(client_id, character_chunks, self.z_index)
                .with_context(err_context)?;
            self.output.add_sixel_image_chunks_to_client(
                client_id,
                sixel_image_chunks,
                self.z_index,
            );
            self.output.add_kitty_image_chunks_to_client(
                client_id,
                self.pane.pid(),
                kitty_image_chunks,
                self.z_index,
            );
            if let Some(raw_vte_output) = raw_vte_output {
                self.output.add_post_vte_instruction_to_client(
                    client_id,
                    &format!(
                        "\u{1b}[{};{}H\u{1b}[m{}",
                        self.pane.y() + 1,
                        self.pane.x() + 1,
                        raw_vte_output
                    ),
                );
            }
        }
        Ok(())
    }
    pub fn client_has_guest_modal(&self, client_id: ClientId) -> bool {
        self.pane.guest_modal_selection(client_id).is_some()
    }
    pub fn drain_pane_render_state(&mut self) {
        drop(self.pane.drain_fake_cursors());
        let _ = self.pane.render(None);
    }
    pub fn render_guest_modal_for_client(&mut self, client_id: ClientId) -> Result<()> {
        let err_context = || format!("failed to render guest modal for client {client_id}");
        let selection = self.pane.guest_modal_selection(client_id).unwrap_or(0);
        let session_name = self
            .pane
            .guest_session_name()
            .unwrap_or_else(|| String::from("unknown"));
        let columns = self.pane.get_content_columns();
        let rows = self.pane.get_content_rows();
        let content_x = self.pane.get_content_x();
        let content_y = self.pane.get_content_y();
        let shortcuts = self.pane.guest_modal_shortcuts();
        let chunks = crate::panes::nested_session_modal::guest_modal_chunks(
            columns,
            rows,
            content_x,
            content_y,
            &self.style,
            &session_name,
            selection,
            &shortcuts,
        );
        self.output
            .add_character_chunks_to_client(client_id, chunks, self.z_index)
            .with_context(err_context)?;
        Ok(())
    }
    pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) -> Result<()> {
        let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
        let pane_focused_for_different_client = self
            .focused_clients
            .iter()
            .filter(|&&c_id| c_id != client_id)
            .count()
            > 0;
        if pane_focused_for_different_client && !pane_focused_for_client_id {
            let fake_cursor_client_id = self
                .focused_clients
                .iter()
                .find(|&&c_id| c_id != client_id)
                .with_context(|| {
                    format!("failed to render fake cursor if needed for client {client_id}")
                })?;
            if let Some(colors) = client_id_to_colors(
                *fake_cursor_client_id,
                self.style.colors.multiplayer_user_colors,
            ) {
                let cursor_is_visible = self
                    .pane
                    .cursor_coordinates(Some(*fake_cursor_client_id))
                    .and_then(|(x, y, is_visible)| {
                        if is_visible {
                            Some(self.output.cursor_is_visible(
                                self.pane.x() + x,
                                self.pane.y() + y,
                                self.z_index,
                            ))
                        } else {
                            None
                        }
                    })
                    .unwrap_or(false);
                if cursor_is_visible {
                    if let Some(vte_output) = self.pane.render_fake_cursor(colors.0, colors.1) {
                        self.output.add_post_vte_instruction_to_client(
                            client_id,
                            &format!(
                                "\u{1b}[{};{}H\u{1b}[m{}",
                                self.pane.y() + 1,
                                self.pane.x() + 1,
                                vte_output
                            ),
                        );
                    }
                }
            }
        }
        Ok(())
    }
    pub fn render_terminal_title_if_needed(
        &mut self,
        client_id: ClientId,
        client_mode: InputMode,
        previous_title: &mut Option<String>,
    ) {
        if !self.focused_clients.contains(&client_id) {
            return;
        }
        let vte_output = self.pane.render_terminal_title(client_mode);
        if let Some(previous_title) = previous_title {
            if *previous_title == vte_output {
                return;
            }
        }
        *previous_title = Some(vte_output.clone());
        self.output
            .add_post_vte_instruction_to_client(client_id, &vte_output);
    }
    pub fn render_pane_frame(
        &mut self,
        client_id: ClientId,
        client_mode: InputMode,
        session_is_mirrored: bool,
        pane_is_floating: bool,
        pane_is_selectable: bool,
    ) -> Result<()> {
        let err_context = || format!("failed to render pane frame for client {client_id}");

        let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
        let other_focused_clients: Vec<ClientId> = self
            .focused_clients
            .iter()
            .filter(|&&c_id| c_id != client_id)
            .copied()
            .collect();
        let pane_focused_for_differet_client = !other_focused_clients.is_empty();

        let frame_color = self.frame_color(client_id, client_mode, session_is_mirrored);
        let highlight_tooltip = self.pane.cached_hover_tooltip();
        let focused_client = if pane_focused_for_client_id {
            Some(client_id)
        } else if pane_focused_for_differet_client {
            Some(*other_focused_clients.first().with_context(err_context)?)
        } else {
            None
        };
        let pane_is_stacked = self.pane.current_geom().is_stacked();
        let pane_is_in_group = self
            .current_pane_group
            .get(&client_id)
            .map(|p| p.contains(&self.pane.pid()))
            .unwrap_or(false);
        let stack_list_entry = self.stack_list_entry_width.map(|width| StackListEntry {
            width,
            label: self.pane.stack_list_entry_label(),
            is_selected: self.stack_list_entry_is_selected,
            stack_is_focused: self.stack_list_entry_stack_is_focused,
            is_emphasized: pane_is_in_group
                || (self
                    .mouse_is_hovering_over_pane_for_clients
                    .contains(&client_id)
                    && !pane_focused_for_client_id),
        });
        let frame_is_dimmed = self.frame_is_dimmed_for_client(client_id);
        let guest_choice_indicator = self.pane.guest_choice_indicator(client_id);
        let frame_params = if session_is_mirrored {
            FrameParams {
                focused_client,
                is_main_client: pane_focused_for_client_id,
                other_focused_clients: vec![],
                style: self.style,
                color: frame_color.map(|c| c.0),
                other_cursors_exist_in_session: false,
                pane_is_stacked_over: self.pane_is_stacked_over,
                pane_is_stacked_under: self.pane_is_stacked_under,
                pane_is_stacked,
                should_draw_pane_frames: self.should_draw_pane_frames,
                pane_is_floating,
                content_offset: self.pane.get_content_offset(),
                mouse_is_hovering_over_pane: self
                    .mouse_is_hovering_over_pane_for_clients
                    .contains(&client_id),
                pane_is_selectable,
                show_help_text: self.show_help_text,
                highlight_tooltip: highlight_tooltip.clone(),
                omit_title: self.omit_title,
                frame_geom_override: self.frame_geom_override,
                stack_list_entry: stack_list_entry.clone(),
                blank_title: self.blank_title,
                mouse_scroll_resize: self.mouse_scroll_resize,
                mouse_hover_tips: self.mouse_hover_tips,
                dimmed: frame_is_dimmed,
                guest_choice_indicator,
            }
        } else {
            FrameParams {
                focused_client,
                is_main_client: pane_focused_for_client_id,
                other_focused_clients,
                style: self.style,
                color: frame_color.map(|c| c.0),
                other_cursors_exist_in_session: self.multiple_users_exist_in_session,
                pane_is_stacked_over: self.pane_is_stacked_over,
                pane_is_stacked_under: self.pane_is_stacked_under,
                pane_is_stacked,
                should_draw_pane_frames: self.should_draw_pane_frames,
                pane_is_floating,
                content_offset: self.pane.get_content_offset(),
                mouse_is_hovering_over_pane: self
                    .mouse_is_hovering_over_pane_for_clients
                    .contains(&client_id),
                pane_is_selectable,
                show_help_text: self.show_help_text,
                highlight_tooltip,
                omit_title: self.omit_title,
                frame_geom_override: self.frame_geom_override,
                stack_list_entry,
                blank_title: self.blank_title,
                mouse_scroll_resize: self.mouse_scroll_resize,
                mouse_hover_tips: self.mouse_hover_tips,
                dimmed: frame_is_dimmed,
                guest_choice_indicator,
            }
        };

        if let Some((mut frame_terminal_characters, vte_output)) = self
            .pane
            .render_frame(client_id, frame_params, client_mode)
            .with_context(err_context)?
        {
            if self.frame_is_dimmed_for_client(client_id) {
                dim_character_chunks(&mut frame_terminal_characters);
            }
            self.output
                .add_character_chunks_to_client(client_id, frame_terminal_characters, self.z_index)
                .with_context(err_context)?;
            if let Some(vte_output) = vte_output {
                self.output
                    .add_post_vte_instruction_to_client(client_id, &vte_output);
            }
        }

        Ok(())
    }
    pub fn render_pane_boundaries(
        &self,
        client_id: ClientId,
        client_mode: InputMode,
        boundaries: &mut Boundaries,
        session_is_mirrored: bool,
        pane_is_on_top_of_stack: bool,
        pane_is_on_bottom_of_stack: bool,
    ) {
        let color = self.frame_color(client_id, client_mode, session_is_mirrored);
        boundaries.add_rect(
            self.pane.as_ref(),
            color,
            pane_is_on_top_of_stack,
            pane_is_on_bottom_of_stack,
            self.pane_is_stacked_under,
        );
    }
    fn frame_color(
        &self,
        client_id: ClientId,
        mode: InputMode,
        session_is_mirrored: bool,
    ) -> Option<(PaletteColor, usize)> {
        // (color, color_precedence) (the color_precedence is used
        // for the no-pane-frames mode)
        let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
        let pane_is_in_group = self
            .current_pane_group
            .get(&client_id)
            .map(|p| p.contains(&self.pane.pid()))
            .unwrap_or(false);
        if self.pane.frame_color_override().is_some() && !pane_is_in_group {
            self.pane
                .frame_color_override()
                .map(|override_color| (override_color, 4))
        } else if pane_is_in_group && !pane_focused_for_client_id {
            Some((self.style.colors.frame_highlight.emphasis_0, 2))
        } else if pane_is_in_group && pane_focused_for_client_id {
            Some((self.style.colors.frame_highlight.emphasis_1, 3))
        } else if pane_focused_for_client_id {
            match mode {
                InputMode::Normal | InputMode::Locked => {
                    if session_is_mirrored || !self.multiple_users_exist_in_session {
                        Some((self.style.colors.frame_selected.base, 3))
                    } else {
                        let colors = client_id_to_colors(
                            client_id,
                            self.style.colors.multiplayer_user_colors,
                        );
                        colors.map(|colors| (colors.0, 3))
                    }
                },
                _ => Some((self.style.colors.frame_highlight.base, 3)),
            }
        } else if self
            .mouse_is_hovering_over_pane_for_clients
            .contains(&client_id)
        {
            Some((self.style.colors.frame_highlight.base, 1))
        } else {
            self.style
                .colors
                .frame_unselected
                .map(|frame| (frame.base, 0))
        }
    }
}
