use std::borrow::Cow;
use std::cmp::{max, min};
use std::mem;
use std::ops::{Deref, DerefMut, RangeInclusive};

use alacritty_terminal::ansi::{Color, CursorShape, NamedColor};
use alacritty_terminal::config::Config;
use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Indexed};
use alacritty_terminal::index::{Column, Direction, Line, Point};
use alacritty_terminal::term::cell::{Cell, Flags};
use alacritty_terminal::term::color::{CellRgb, Rgb};
use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
use alacritty_terminal::term::{RenderableContent as TerminalContent, Term, TermMode};

use crate::config::ui_config::UiConfig;
use crate::display::color::{List, DIM_FACTOR};
use crate::display::hint::HintState;
use crate::display::{self, Display, MAX_SEARCH_LINES};
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: Option<RenderableCursor>,
    cursor_shape: CursorShape,
    cursor_point: Point<usize>,
    search: Option<Regex<'a>>,
    hint: Option<Hint<'a>>,
    config: &'a Config<UiConfig>,
    colors: &'a List,
    focused_match: Option<&'a Match>,
}

impl<'a> RenderableContent<'a> {
    pub fn new<T: EventListener>(
        config: &'a Config<UiConfig>,
        display: &'a mut Display,
        term: &'a Term<T>,
        search_state: &'a SearchState,
    ) -> Self {
        let search = search_state.dfas().map(|dfas| Regex::new(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()
        {
            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 = display::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,
            cursor: None,
            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) -> Option<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].unwrap_or(self.colors[color])
    }

    /// Assemble the information required to render the terminal cursor.
    ///
    /// This will return `None` when there is no cursor visible.
    fn renderable_cursor(&mut self, cell: &RenderableCell) -> Option<RenderableCursor> {
        if self.cursor_shape == CursorShape::Hidden {
            return None;
        }

        // Cursor colors.
        let color = if self.terminal_content.mode.contains(TermMode::VI) {
            self.config.ui_config.colors.vi_mode_cursor
        } else {
            self.config.ui_config.colors.cursor
        };
        let mut cursor_color =
            self.terminal_content.colors[NamedColor::Cursor].map_or(color.background, CellRgb::Rgb);
        let mut text_color = color.foreground;

        // Invert the cursor if it has a fixed background close to the cell's background.
        if matches!(
            cursor_color,
            CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST
        ) {
            cursor_color = CellRgb::CellForeground;
            text_color = CellRgb::CellBackground;
        }

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

        Some(RenderableCursor {
            is_wide: cell.flags.contains(Flags::WIDE_CHAR),
            shape: self.cursor_shape,
            point: self.cursor_point,
            cursor_color,
            text_color,
        })
    }
}

impl<'a> Iterator for RenderableContent<'a> {
    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).map(|cursor| {
                    if cursor.shape == CursorShape::Block {
                        cell.fg = cursor.text_color;
                        cell.bg = 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.;
                    }

                    cursor
                });

                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 zerowidth: Option<Vec<char>>,
    pub point: Point<usize>,
    pub fg: Rgb,
    pub bg: Rgb,
    pub bg_alpha: f32,
    pub flags: Flags,
}

impl RenderableCell {
    fn new<'a>(content: &mut RenderableContent<'a>, 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(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.ui_config.colors;
        let mut character = cell.c;

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

            character = c;
        } 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);
        }

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

        RenderableCell {
            zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
            flags: cell.flags,
            character,
            bg_alpha,
            point,
            fg,
            bg,
        }
    }

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

    /// 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: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb {
        let ui_config = &content.config.ui_config;
        match fg {
            Color::Spec(rgb) => match flags & Flags::DIM {
                Flags::DIM => rgb * DIM_FACTOR,
                _ => rgb,
            },
            Color::Named(ansi) => {
                match (ui_config.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
                            && ui_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 (
                    ui_config.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: &mut RenderableContent<'_>, bg: Color) -> Rgb {
        match bg {
            Color::Spec(rgb) => rgb,
            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(bg: Color) -> f32 {
        if bg == Color::Named(NamedColor::Background) {
            0.
        } 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,
    is_wide: bool,
    point: Point<usize>,
}

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

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

    pub fn is_wide(&self) -> bool {
        self.is_wide
    }

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

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

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

impl<'a> Hint<'a> {
    /// 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.
    fn advance(&mut self, viewport_start: Point, point: Point) -> Option<(char, bool)> {
        // Check if we're within a match at all.
        if !self.regex.advance(point) {
            return None;
        }

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

        // Position within the hint label.
        let label_position = point.column.0 - start.column.0;
        let is_first = label_position == 0;

        // Hint label character.
        self.labels[self.regex.index].get(label_position).copied().map(|c| (c, is_first))
    }
}

impl<'a> From<&'a HintState> for Hint<'a> {
    fn from(hint_state: &'a HintState) -> Self {
        let regex = Regex { matches: Cow::Borrowed(hint_state.matches()), index: 0 };
        Self { labels: hint_state.labels(), regex }
    }
}

/// Wrapper for finding visible regex matches.
#[derive(Default, Clone)]
pub struct RegexMatches(pub Vec<RangeInclusive<Point>>);

impl RegexMatches {
    /// Find all visible matches.
    pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
        let viewport_start = Line(-(term.grid().display_offset() as i32));
        let viewport_end = viewport_start + term.bottommost_line();

        // Compute start of the first and end of the last line.
        let start_point = Point::new(viewport_start, Column(0));
        let mut start = term.line_search_left(start_point);
        let end_point = Point::new(viewport_end, term.last_column());
        let mut end = term.line_search_right(end_point);

        // Set upper bound on search before/after the viewport to prevent excessive blocking.
        start.line = max(start.line, viewport_start - MAX_SEARCH_LINES);
        end.line = min(end.line, viewport_end + MAX_SEARCH_LINES);

        // Create an iterater for the current regex search for all visible matches.
        let iter = RegexIter::new(start, end, Direction::Right, term, dfas)
            .skip_while(move |rm| rm.end().line < viewport_start)
            .take_while(move |rm| rm.start().line <= viewport_end);

        Self(iter.collect())
    }
}

impl Deref for RegexMatches {
    type Target = Vec<RangeInclusive<Point>>;

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

impl DerefMut for RegexMatches {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

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

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

impl<'a> Regex<'a> {
    /// Create a new renderable regex iterator.
    fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
        let matches = Cow::Owned(RegexMatches::new(term, dfas));
        Self { index: 0, 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(regex_match) = self.matches.get(self.index) {
            if regex_match.start() > &point {
                break;
            } else if regex_match.end() < &point {
                self.index += 1;
            } else {
                return true;
            }
        }
        false
    }
}
