use std::borrow::Cow;
use std::num::NonZeroU32;
use std::ops::Deref;
use std::{cmp, mem};

use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Indexed};
use alacritty_terminal::index::{Column, Line, Point};
use alacritty_terminal::selection::SelectionRange;
use alacritty_terminal::term::cell::{Cell, Flags, Hyperlink};
use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::{self, RenderableContent as TerminalContent, Term, TermMode};
use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor};

use crate::config::UiConfig;
use crate::display::color::{CellRgb, List, Rgb, DIM_FACTOR};
use crate::display::hint::{self, HintState};
use crate::display::{Display, SizeInfo};
use crate::event::SearchState;

/// Minimum contrast between a fixed cursor color and the cell's background.
pub const MIN_CURSOR_CONTRAST: f64 = 1.5;

/// Renderable terminal content.
///
/// This provides the terminal cursor and an iterator over all non-empty cells.
pub struct RenderableContent<'a> {
    terminal_content: TerminalContent<'a>,
    cursor: RenderableCursor,
    cursor_shape: CursorShape,
    cursor_point: Point<usize>,
    search: Option<HintMatches<'a>>,
    hint: Option<Hint<'a>>,
    config: &'a UiConfig,
    colors: &'a List,
    focused_match: Option<&'a Match>,
    size: &'a SizeInfo,
}

impl<'a> RenderableContent<'a> {
    pub fn new<T: EventListener>(
        config: &'a UiConfig,
        display: &'a mut Display,
        term: &'a Term<T>,
        search_state: &'a mut SearchState,
    ) -> Self {
        let search = search_state.dfas().map(|dfas| HintMatches::visible_regex_matches(term, dfas));
        let focused_match = search_state.focused_match();
        let terminal_content = term.renderable_content();

        // Find terminal cursor shape.
        let cursor_shape = if terminal_content.cursor.shape == CursorShape::Hidden
            || display.cursor_hidden
            || search_state.regex().is_some()
            || display.ime.preedit().is_some()
        {
            CursorShape::Hidden
        } else if !term.is_focused && config.cursor.unfocused_hollow {
            CursorShape::HollowBlock
        } else {
            terminal_content.cursor.shape
        };

        // Convert terminal cursor point to viewport position.
        let cursor_point = terminal_content.cursor.point;
        let display_offset = terminal_content.display_offset;
        let cursor_point = term::point_to_viewport(display_offset, cursor_point).unwrap();

        let hint = if display.hint_state.active() {
            display.hint_state.update_matches(term);
            Some(Hint::from(&display.hint_state))
        } else {
            None
        };

        Self {
            colors: &display.colors,
            size: &display.size_info,
            cursor: RenderableCursor::new_hidden(),
            terminal_content,
            focused_match,
            cursor_shape,
            cursor_point,
            search,
            config,
            hint,
        }
    }

    /// Viewport offset.
    pub fn display_offset(&self) -> usize {
        self.terminal_content.display_offset
    }

    /// Get the terminal cursor.
    pub fn cursor(mut self) -> RenderableCursor {
        // Assure this function is only called after the iterator has been drained.
        debug_assert!(self.next().is_none());

        self.cursor
    }

    /// Get the RGB value for a color index.
    pub fn color(&self, color: usize) -> Rgb {
        self.terminal_content.colors[color].map(Rgb).unwrap_or(self.colors[color])
    }

    pub fn selection_range(&self) -> Option<SelectionRange> {
        self.terminal_content.selection
    }

    /// Assemble the information required to render the terminal cursor.
    fn renderable_cursor(&mut self, cell: &RenderableCell) -> RenderableCursor {
        // Cursor colors.
        let color = if self.terminal_content.mode.contains(TermMode::VI) {
            self.config.colors.vi_mode_cursor
        } else {
            self.config.colors.cursor
        };
        let cursor_color = self.terminal_content.colors[NamedColor::Cursor]
            .map_or(color.background, |c| CellRgb::Rgb(Rgb(c)));
        let text_color = color.foreground;

        let insufficient_contrast = (!matches!(cursor_color, CellRgb::Rgb(_))
            || !matches!(text_color, CellRgb::Rgb(_)))
            && cell.fg.contrast(*cell.bg) < MIN_CURSOR_CONTRAST;

        // Convert from cell colors to RGB.
        let mut text_color = text_color.color(cell.fg, cell.bg);
        let mut cursor_color = cursor_color.color(cell.fg, cell.bg);

        // Invert cursor color with insufficient contrast to prevent invisible cursors.
        if insufficient_contrast {
            cursor_color = self.config.colors.primary.foreground;
            text_color = self.config.colors.primary.background;
        }

        let width = if cell.flags.contains(Flags::WIDE_CHAR) {
            NonZeroU32::new(2).unwrap()
        } else {
            NonZeroU32::new(1).unwrap()
        };
        RenderableCursor {
            width,
            shape: self.cursor_shape,
            point: self.cursor_point,
            cursor_color,
            text_color,
        }
    }
}

impl Iterator for RenderableContent<'_> {
    type Item = RenderableCell;

    /// Gets the next renderable cell.
    ///
    /// Skips empty (background) cells and applies any flags to the cell state
    /// (eg. invert fg and bg colors).
    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let cell = self.terminal_content.display_iter.next()?;
            let mut cell = RenderableCell::new(self, cell);

            if self.cursor_point == cell.point {
                // Store the cursor which should be rendered.
                self.cursor = self.renderable_cursor(&cell);
                if self.cursor.shape == CursorShape::Block {
                    cell.fg = self.cursor.text_color;
                    cell.bg = self.cursor.cursor_color;

                    // Since we draw Block cursor by drawing cell below it with a proper color,
                    // we must adjust alpha to make it visible.
                    cell.bg_alpha = 1.;
                }

                return Some(cell);
            } else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
                // Skip empty cells and wide char spacers.
                return Some(cell);
            }
        }
    }
}

/// Cell ready for rendering.
#[derive(Clone, Debug)]
pub struct RenderableCell {
    pub character: char,
    pub point: Point<usize>,
    pub fg: Rgb,
    pub bg: Rgb,
    pub bg_alpha: f32,
    pub underline: Rgb,
    pub flags: Flags,
    pub extra: Option<Box<RenderableCellExtra>>,
}

/// Extra storage with rarely present fields for [`RenderableCell`], to reduce the cell size we
/// pass around.
#[derive(Clone, Debug)]
pub struct RenderableCellExtra {
    pub zerowidth: Option<Vec<char>>,
    pub hyperlink: Option<Hyperlink>,
}

impl RenderableCell {
    fn new(content: &mut RenderableContent<'_>, cell: Indexed<&Cell>) -> Self {
        // Lookup RGB values.
        let mut fg = Self::compute_fg_rgb(content, cell.fg, cell.flags);
        let mut bg = Self::compute_bg_rgb(content, cell.bg);

        let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) {
            mem::swap(&mut fg, &mut bg);
            1.0
        } else {
            Self::compute_bg_alpha(content.config, cell.bg)
        };

        let is_selected = content.terminal_content.selection.map_or(false, |selection| {
            selection.contains_cell(
                &cell,
                content.terminal_content.cursor.point,
                content.cursor_shape,
            )
        });

        let display_offset = content.terminal_content.display_offset;
        let viewport_start = Point::new(Line(-(display_offset as i32)), Column(0));
        let colors = &content.config.colors;
        let mut character = cell.c;
        let mut flags = cell.flags;

        let num_cols = content.size.columns();
        if let Some((c, is_first)) = content
            .hint
            .as_mut()
            .and_then(|hint| hint.advance(viewport_start, num_cols, cell.point))
        {
            if is_first {
                let (config_fg, config_bg) =
                    (colors.hints.start.foreground, colors.hints.start.background);
                Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg);
            } else if c.is_some() {
                let (config_fg, config_bg) =
                    (colors.hints.end.foreground, colors.hints.end.background);
                Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg);
            } else {
                flags.insert(Flags::UNDERLINE);
            }

            character = c.unwrap_or(character);
        } else if is_selected {
            let config_fg = colors.selection.foreground;
            let config_bg = colors.selection.background;
            Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg);

            if fg == bg && !cell.flags.contains(Flags::HIDDEN) {
                // Reveal inversed text when fg/bg is the same.
                fg = content.color(NamedColor::Background as usize);
                bg = content.color(NamedColor::Foreground as usize);
                bg_alpha = 1.0;
            }
        } else if content.search.as_mut().map_or(false, |search| search.advance(cell.point)) {
            let focused = content.focused_match.map_or(false, |fm| fm.contains(&cell.point));
            let (config_fg, config_bg) = if focused {
                (colors.search.focused_match.foreground, colors.search.focused_match.background)
            } else {
                (colors.search.matches.foreground, colors.search.matches.background)
            };
            Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg);
        }

        // Apply transparency to all renderable cells if `transparent_background_colors` is set
        if bg_alpha > 0. && content.config.colors.transparent_background_colors {
            bg_alpha = content.config.window_opacity();
        }

        // Convert cell point to viewport position.
        let cell_point = cell.point;
        let point = term::point_to_viewport(display_offset, cell_point).unwrap();

        let underline = cell
            .underline_color()
            .map_or(fg, |underline| Self::compute_fg_rgb(content, underline, flags));

        let zerowidth = cell.zerowidth();
        let hyperlink = cell.hyperlink();

        let extra = (zerowidth.is_some() || hyperlink.is_some()).then(|| {
            Box::new(RenderableCellExtra {
                zerowidth: zerowidth.map(|zerowidth| zerowidth.to_vec()),
                hyperlink,
            })
        });

        RenderableCell { flags, character, bg_alpha, point, fg, bg, underline, extra }
    }

    /// Check if cell contains any renderable content.
    fn is_empty(&self) -> bool {
        self.bg_alpha == 0.
            && self.character == ' '
            && self.extra.is_none()
            && !self.flags.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT)
    }

    /// Apply [`CellRgb`] colors to the cell's colors.
    fn compute_cell_rgb(
        cell_fg: &mut Rgb,
        cell_bg: &mut Rgb,
        bg_alpha: &mut f32,
        fg: CellRgb,
        bg: CellRgb,
    ) {
        let old_fg = mem::replace(cell_fg, fg.color(*cell_fg, *cell_bg));
        *cell_bg = bg.color(old_fg, *cell_bg);

        if bg != CellRgb::CellBackground {
            *bg_alpha = 1.0;
        }
    }

    /// Get the RGB color from a cell's foreground color.
    fn compute_fg_rgb(content: &RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb {
        let config = &content.config;
        match fg {
            Color::Spec(rgb) => match flags & Flags::DIM {
                Flags::DIM => {
                    let rgb: Rgb = rgb.into();
                    rgb * DIM_FACTOR
                },
                _ => rgb.into(),
            },
            Color::Named(ansi) => {
                match (config.colors.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) {
                    // If no bright foreground is set, treat it like the BOLD flag doesn't exist.
                    (_, Flags::DIM_BOLD)
                        if ansi == NamedColor::Foreground
                            && config.colors.primary.bright_foreground.is_none() =>
                    {
                        content.color(NamedColor::DimForeground as usize)
                    },
                    // Draw bold text in bright colors *and* contains bold flag.
                    (true, Flags::BOLD) => content.color(ansi.to_bright() as usize),
                    // Cell is marked as dim and not bold.
                    (_, Flags::DIM) | (false, Flags::DIM_BOLD) => {
                        content.color(ansi.to_dim() as usize)
                    },
                    // None of the above, keep original color..
                    _ => content.color(ansi as usize),
                }
            },
            Color::Indexed(idx) => {
                let idx = match (
                    config.colors.draw_bold_text_with_bright_colors,
                    flags & Flags::DIM_BOLD,
                    idx,
                ) {
                    (true, Flags::BOLD, 0..=7) => idx as usize + 8,
                    (false, Flags::DIM, 8..=15) => idx as usize - 8,
                    (false, Flags::DIM, 0..=7) => NamedColor::DimBlack as usize + idx as usize,
                    _ => idx as usize,
                };

                content.color(idx)
            },
        }
    }

    /// Get the RGB color from a cell's background color.
    #[inline]
    fn compute_bg_rgb(content: &RenderableContent<'_>, bg: Color) -> Rgb {
        match bg {
            Color::Spec(rgb) => rgb.into(),
            Color::Named(ansi) => content.color(ansi as usize),
            Color::Indexed(idx) => content.color(idx as usize),
        }
    }

    /// Compute background alpha based on cell's original color.
    ///
    /// Since an RGB color matching the background should not be transparent, this is computed
    /// using the named input color, rather than checking the RGB of the background after its color
    /// is computed.
    #[inline]
    fn compute_bg_alpha(config: &UiConfig, bg: Color) -> f32 {
        if bg == Color::Named(NamedColor::Background) {
            0.
        } else if config.colors.transparent_background_colors {
            config.window_opacity()
        } else {
            1.
        }
    }
}

/// Cursor storing all information relevant for rendering.
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct RenderableCursor {
    shape: CursorShape,
    cursor_color: Rgb,
    text_color: Rgb,
    width: NonZeroU32,
    point: Point<usize>,
}

impl RenderableCursor {
    fn new_hidden() -> Self {
        let shape = CursorShape::Hidden;
        let cursor_color = Rgb::default();
        let text_color = Rgb::default();
        let width = NonZeroU32::new(1).unwrap();
        let point = Point::default();
        Self { shape, cursor_color, text_color, width, point }
    }
}

impl RenderableCursor {
    pub fn new(
        point: Point<usize>,
        shape: CursorShape,
        cursor_color: Rgb,
        width: NonZeroU32,
    ) -> Self {
        Self { shape, cursor_color, text_color: cursor_color, width, point }
    }

    pub fn color(&self) -> Rgb {
        self.cursor_color
    }

    pub fn shape(&self) -> CursorShape {
        self.shape
    }

    pub fn width(&self) -> NonZeroU32 {
        self.width
    }

    pub fn point(&self) -> Point<usize> {
        self.point
    }
}

/// Regex hints for keyboard shortcuts.
struct Hint<'a> {
    /// Hint matches and position.
    matches: HintMatches<'a>,

    /// Last match checked against current cell position.
    labels: &'a Vec<Vec<char>>,
}

impl Hint<'_> {
    /// Advance the hint iterator.
    ///
    /// If the point is within a hint, the keyboard shortcut character that should be displayed at
    /// this position will be returned.
    ///
    /// The tuple's [`bool`] will be `true` when the character is the first for this hint.
    ///
    /// The tuple's [`Option<char>`] will be [`None`] when the point is part of the match, but not
    /// part of the hint label.
    fn advance(
        &mut self,
        viewport_start: Point,
        num_cols: usize,
        point: Point,
    ) -> Option<(Option<char>, bool)> {
        // Check if we're within a match at all.
        if !self.matches.advance(point) {
            return None;
        }

        // Match starting position on this line; linebreaks interrupt the hint labels.
        let start = self
            .matches
            .get(self.matches.index)
            .map(|bounds| cmp::max(*bounds.start(), viewport_start))?;

        // Position within the hint label.
        let line_delta = point.line.0 - start.line.0;
        let col_delta = point.column.0 as i32 - start.column.0 as i32;
        let label_position = usize::try_from(line_delta * num_cols as i32 + col_delta).unwrap_or(0);
        let is_first = label_position == 0;

        // Hint label character.
        let hint_char = self.labels[self.matches.index]
            .get(label_position)
            .copied()
            .map(|c| (Some(c), is_first))
            .unwrap_or((None, false));

        Some(hint_char)
    }
}

impl<'a> From<&'a HintState> for Hint<'a> {
    fn from(hint_state: &'a HintState) -> Self {
        let matches = HintMatches::new(hint_state.matches());
        Self { labels: hint_state.labels(), matches }
    }
}

/// Visible hint match tracking.
#[derive(Default)]
struct HintMatches<'a> {
    /// All visible matches.
    matches: Cow<'a, [Match]>,

    /// Index of the last match checked.
    index: usize,
}

impl<'a> HintMatches<'a> {
    /// Create new renderable matches iterator..
    fn new(matches: impl Into<Cow<'a, [Match]>>) -> Self {
        Self { matches: matches.into(), index: 0 }
    }

    /// Create from regex matches on term visible part.
    fn visible_regex_matches<T>(term: &Term<T>, dfas: &mut RegexSearch) -> Self {
        let matches = hint::visible_regex_match_iter(term, dfas).collect::<Vec<_>>();
        Self::new(matches)
    }

    /// Advance the regex tracker to the next point.
    ///
    /// This will return `true` if the point passed is part of a regex match.
    fn advance(&mut self, point: Point) -> bool {
        while let Some(bounds) = self.get(self.index) {
            if bounds.start() > &point {
                break;
            } else if bounds.end() < &point {
                self.index += 1;
            } else {
                return true;
            }
        }
        false
    }
}

impl Deref for HintMatches<'_> {
    type Target = [Match];

    fn deref(&self) -> &Self::Target {
        self.matches.deref()
    }
}
