/*
 * meli
 *
 * Copyright 2017-2018 Manos Pitsidianakis
 *
 * This file is part of meli.
 *
 * meli is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * meli is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with meli. If not, see <http://www.gnu.org/licenses/>.
 */

//! Terminal grid cells, keys, colors, etc.
use std::io::{BufWriter, Write};

use melib::{log, uuid};
use termion::{clear, cursor, raw::IntoRawMode, screen::AlternateScreen};

use crate::{
    terminal::{
        cells::CellBuffer, Alignment, BracketModeEnd, BracketModeStart, Cell, Color,
        DisableAlternateScrollMode, DisableMouse, DisableSGRMouse, DisableWraparoundMode,
        EnableAlternateScrollMode, EnableMouse, EnableSGRMouse, Pos, QueryBackground,
        QueryForeground, QuerySynchronizedOutputSupport, RestoreWindowTitleIconFromStack,
        RestoreWraparoundMode, SaveWindowTitleIconToStack, SaveWraparoundMode,
    },
    Attr, Context, ThemeAttribute,
};

pub type StateStdout = termion::screen::AlternateScreen<
    termion::raw::RawTerminal<BufWriter<Box<dyn Write + 'static>>>,
>;

type DrawHorizontalSegmentFn =
    fn(&mut CellBuffer, &mut StateStdout, std::ops::Range<usize>, usize) -> ();

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct ScreenGeneration((u64, u64));

impl ScreenGeneration {
    pub const NIL: Self = Self((0, 0));

    #[inline]
    #[must_use]
    pub fn next(self) -> Self {
        Self(uuid::Uuid::new_v4().as_u64_pair())
    }
}

impl Default for ScreenGeneration {
    fn default() -> Self {
        Self::NIL
    }
}

impl std::fmt::Display for ScreenGeneration {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
        let pair = self.0;
        write!(
            fmt,
            "{}",
            uuid::Uuid::from_u64_pair(pair.0, pair.1).as_simple()
        )
    }
}

#[derive(Clone, Copy, Debug)]
pub struct Virtual;

pub struct Tty {
    stdout: Option<StateStdout>,
    background_query: Option<Color>,
    mouse: bool,
    draw_horizontal_segment_fn: DrawHorizontalSegmentFn,
}

impl Tty {
    #[inline]
    pub fn stdout_mut(&mut self) -> Option<&mut StateStdout> {
        self.stdout.as_mut()
    }

    #[inline]
    pub fn draw_fn(&self) -> DrawHorizontalSegmentFn {
        self.draw_horizontal_segment_fn
    }

    #[inline]
    pub const fn mouse(&self) -> bool {
        self.mouse
    }

    pub fn set_mouse(&mut self, mouse: bool) -> &mut Self {
        self.mouse = mouse;
        let Some(stdout) = self.stdout.as_mut() else {
            return self;
        };
        write!(
            stdout,
            "{enable_mouse}{enable_sgr_mouse}{enable_alt_scroll}",
            enable_mouse = if mouse {
                EnableMouse.as_ref()
            } else {
                DisableMouse.as_ref()
            },
            enable_sgr_mouse = if mouse {
                EnableSGRMouse.as_ref()
            } else {
                DisableSGRMouse.as_ref()
            },
            enable_alt_scroll = if mouse {
                EnableAlternateScrollMode.as_ref()
            } else {
                DisableAlternateScrollMode.as_ref()
            },
        )
        .expect("Could not write to stdout");
        _ = stdout.flush();

        self
    }

    #[inline]
    pub fn set_draw_fn(
        &mut self,
        draw_horizontal_segment_fn: DrawHorizontalSegmentFn,
    ) -> &mut Self {
        self.draw_horizontal_segment_fn = draw_horizontal_segment_fn;
        self
    }
}

mod private {
    pub trait Sealed {}
}
impl private::Sealed for Virtual {}
impl private::Sealed for Tty {}

pub struct Screen<Display: private::Sealed> {
    cols: usize,
    rows: usize,
    grid: CellBuffer,
    overlay_grid: CellBuffer,
    display: Display,
    generation: ScreenGeneration,
}

impl<D: private::Sealed> std::fmt::Debug for Screen<D> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct(stringify!(Screen))
            .field("cols", &self.cols)
            .field("rows", &self.rows)
            .field("grid", &self.grid)
            .field("overlay_grid", &self.overlay_grid)
            .field("generation", &self.generation)
            .finish()
    }
}

impl<D: private::Sealed> Screen<D> {
    #[inline]
    pub fn init(display: D, theme_default: ThemeAttribute) -> Self {
        let area = Area {
            offset: (0, 0),
            upper_left: (0, 0),
            bottom_right: (0, 0),
            empty: true,
            canvas_cols: 0,
            canvas_rows: 0,
            generation: ScreenGeneration::NIL,
        };
        let mut retval = Self {
            cols: 0,
            rows: 0,
            grid: CellBuffer::nil(area),
            overlay_grid: CellBuffer::nil(area),
            display,
            generation: ScreenGeneration::NIL,
        };
        retval.grid_mut().default_cell = theme_default.into();
        retval.overlay_grid_mut().default_cell = theme_default.into();
        retval
    }

    #[inline]
    pub fn with_cols_and_rows(mut self, cols: usize, rows: usize) -> Self {
        self.generation = self.generation.next();
        self.cols = cols;
        self.rows = rows;
        Self {
            cols,
            rows,
            grid: CellBuffer::new(Cell::with_char(' '), self.area()),
            overlay_grid: CellBuffer::new(Cell::with_char(' '), self.area()),
            ..self
        }
    }

    pub const fn area(&self) -> Area {
        let upper_left = (0, 0);
        let bottom_right = (self.cols.saturating_sub(1), self.rows.saturating_sub(1));
        Area {
            offset: upper_left,
            upper_left,
            bottom_right,
            empty: matches!((self.cols, self.rows), (0, 0)),
            canvas_cols: self.cols,
            canvas_rows: self.rows,
            generation: self.generation,
        }
    }

    #[inline]
    pub fn grid(&self) -> &CellBuffer {
        &self.grid
    }

    #[inline]
    pub fn grid_mut(&mut self) -> &mut CellBuffer {
        &mut self.grid
    }

    #[inline]
    pub fn overlay_grid(&self) -> &CellBuffer {
        &self.overlay_grid
    }

    #[inline]
    pub fn overlay_grid_mut(&mut self) -> &mut CellBuffer {
        &mut self.overlay_grid
    }

    #[inline]
    pub fn grid_and_overlay_grid_mut(&mut self) -> (&mut CellBuffer, &mut CellBuffer) {
        (&mut self.grid, &mut self.overlay_grid)
    }

    #[inline(always)]
    pub const fn generation(&self) -> ScreenGeneration {
        self.generation
    }

    #[inline]
    pub const fn cols(&self) -> usize {
        self.cols
    }

    #[inline]
    pub const fn rows(&self) -> usize {
        self.rows
    }
}

impl Clone for Screen<Virtual> {
    fn clone(&self) -> Self {
        Self {
            grid: self.grid.clone(),
            overlay_grid: self.overlay_grid.clone(),
            ..*self
        }
    }
}

impl Screen<Tty> {
    #[inline]
    pub fn new(theme_default: ThemeAttribute) -> Self {
        Self::init(
            Tty {
                stdout: None,
                mouse: false,
                background_query: None,
                draw_horizontal_segment_fn: Self::draw_horizontal_segment,
            },
            theme_default,
        )
    }

    #[inline]
    pub fn with_tty(self, display: Tty) -> Self {
        Self { display, ..self }
    }

    #[inline]
    pub fn tty(&self) -> &Tty {
        &self.display
    }

    #[inline]
    pub fn tty_mut(&mut self) -> &mut Tty {
        &mut self.display
    }

    #[inline]
    pub fn draw(&mut self, xs: std::ops::Range<usize>, y: usize) {
        let Some(stdout) = self.display.stdout.as_mut() else {
            return;
        };
        (self.display.draw_horizontal_segment_fn)(&mut self.grid, stdout, xs, y);
    }

    #[inline]
    pub fn draw_overlay(&mut self, xs: std::ops::Range<usize>, y: usize) {
        let Some(stdout) = self.display.stdout.as_mut() else {
            return;
        };
        (self.display.draw_horizontal_segment_fn)(&mut self.overlay_grid, stdout, xs, y);
    }

    /// On `SIGWNICH` the `State` redraws itself according to the new
    /// terminal size.
    pub fn update_size(&mut self) {
        let termsize = termion::terminal_size().ok();
        let termcols = termsize.map(|(w, _)| w);
        let termrows = termsize.map(|(_, h)| h);
        if termcols.unwrap_or(72) as usize != self.cols
            || termrows.unwrap_or(120) as usize != self.rows
        {
            log::trace!(
                "Size updated, from ({}, {}) -> ({:?}, {:?})",
                self.cols,
                self.rows,
                termcols,
                termrows
            );
        }
        let cols = termcols.unwrap_or(72) as usize;
        let rows = termrows.unwrap_or(120) as usize;
        if self.grid.resize(cols, rows, None) && self.overlay_grid.resize(cols, rows, None) {
            self.generation = self.generation.next();
            self.cols = cols;
            self.rows = rows;
            self.grid.area = self.area();
            self.overlay_grid.area = self.area();
        } else {
            log::warn!("Terminal size too big: ({} cols, {} rows)", cols, rows);
        }
    }

    /// Switch back to the terminal's main screen (The command line the user
    /// sees before opening the application)
    pub fn switch_to_main_screen(&mut self) {
        let Some(stdout) = self.display.stdout.as_mut() else {
            return;
        };
        let mouse = self.display.mouse;
        write!(
            stdout,
            "{restore_wraparound}{}{}{}{}{disable_sgr_mouse}{disable_mouse}{disable_alt_scroll}",
            termion::screen::ToMainScreen,
            cursor::Show,
            RestoreWindowTitleIconFromStack,
            BracketModeEnd,
            restore_wraparound = RestoreWraparoundMode,
            disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" },
            disable_mouse = if mouse { DisableMouse.as_ref() } else { "" },
            disable_alt_scroll = if mouse {
                DisableAlternateScrollMode.as_ref()
            } else {
                ""
            },
        )
        .unwrap();
        self.flush();
        self.display.stdout = None;
    }

    pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) {
        let mut stdout = BufWriter::with_capacity(
            240 * 80,
            Box::new(std::io::stdout()) as Box<dyn std::io::Write>,
        );

        write!(
            &mut stdout,
            "{save_title_to_stack}{}{}{}{save_wraparound}{disable_wraparound}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}{enable_alt_scroll}",
            termion::screen::ToAlternateScreen,
            cursor::Hide,
            clear::All,
            cursor::Goto(1, 1),
            BracketModeStart,
            save_wraparound = SaveWraparoundMode,
            disable_wraparound = DisableWraparoundMode,
            save_title_to_stack = SaveWindowTitleIconToStack,
            window_title = if let Some(ref title) = context.settings.terminal.window_title {
                format!("\x1b]2;{title}\x07")
            } else {
                String::new()
            },
            enable_mouse = if self.display.mouse {
                EnableMouse.as_ref()
            } else {
                ""
            },
            enable_sgr_mouse = if self.display.mouse {
                EnableSGRMouse.as_ref()
            } else {
                ""
            },
            enable_alt_scroll = if self.display.mouse {
                EnableAlternateScrollMode.as_ref()
            } else {
                ""
            },
        )
        .unwrap();

        self.display.stdout = Some(AlternateScreen::from(stdout.into_raw_mode().unwrap()));
        self.flush();
    }

    #[inline]
    pub fn flush(&mut self) {
        if let Some(stdout) = self.display.stdout.as_mut() {
            stdout.flush().unwrap();
        }
    }

    /// Draw only a specific `area` on the screen.
    pub fn draw_horizontal_segment(
        grid: &mut CellBuffer,
        stdout: &mut StateStdout,
        xs: std::ops::Range<usize>,
        y: usize,
    ) {
        write!(
            stdout,
            "{}",
            cursor::Goto(xs.start as u16 + 1, (y + 1) as u16)
        )
        .unwrap();
        let mut current_fg = Color::Default;
        let mut current_bg = Color::Default;
        let mut current_attrs = Attr::DEFAULT;
        write!(stdout, "\x1B[m").unwrap();
        for x in xs {
            let c = &grid[(x, y)];
            if c.attrs() != current_attrs {
                c.attrs().write(current_attrs, stdout).unwrap();
                current_attrs = c.attrs();
            }
            if c.bg() != current_bg {
                c.bg().write_bg(stdout).unwrap();
                current_bg = c.bg();
            }
            if c.fg() != current_fg {
                c.fg().write_fg(stdout).unwrap();
                current_fg = c.fg();
            }
            if !c.empty() {
                write!(stdout, "{}", c.ch()).unwrap();
                if c.attrs().intersects(Attr::FORCE_TEXT) {
                    _ = write!(stdout, "\u{FE0E}");
                }
            }
        }
    }

    pub fn draw_horizontal_segment_no_color(
        grid: &mut CellBuffer,
        stdout: &mut StateStdout,
        xs: std::ops::Range<usize>,
        y: usize,
    ) {
        write!(
            stdout,
            "{}",
            cursor::Goto(xs.start as u16 + 1, (y + 1) as u16)
        )
        .unwrap();
        let mut current_attrs = Attr::DEFAULT;
        write!(stdout, "\x1B[m").unwrap();
        for x in xs {
            let c = &grid[(x, y)];
            if c.attrs() != current_attrs {
                c.attrs().write(current_attrs, stdout).unwrap();
                current_attrs = c.attrs();
            }
            if !c.empty() {
                write!(stdout, "{}", c.ch()).unwrap();
                if c.attrs().intersects(Attr::FORCE_TEXT) {
                    _ = write!(stdout, "\u{FE0E}");
                }
            }
        }
    }

    pub const fn background_query(&self) -> Option<Color> {
        self.display.background_query
    }

    pub fn do_background_query(&mut self) {
        let Some(stdout) = self.display.stdout.as_mut() else {
            return;
        };
        write!(stdout, "{}", QueryBackground.as_ref()).expect("Could not write to stdout");
        write!(stdout, "{}", QueryForeground.as_ref()).expect("Could not write to stdout");
        write!(stdout, "{}", QuerySynchronizedOutputSupport.as_ref())
            .expect("Could not write to stdout");
        _ = stdout.flush();
    }
}

impl Screen<Virtual> {
    #[inline]
    pub fn new(theme_default: ThemeAttribute) -> Self {
        Self::init(Virtual, theme_default)
    }

    #[must_use]
    pub fn resize(&mut self, cols: usize, rows: usize) -> bool {
        if (cols, rows) == (self.grid.cols, self.grid.rows)
            && (cols, rows) == (self.overlay_grid.cols, self.overlay_grid.rows)
        {
            return true;
        }
        if self.grid.resize(cols, rows, None) && self.overlay_grid.resize(cols, rows, None) {
            self.generation = self.generation.next();
            self.cols = cols;
            self.rows = rows;
            self.grid.area = self.area();
            self.overlay_grid.area = self.area();
            return true;
        }

        false
    }

    #[must_use]
    pub fn resize_with_context(&mut self, cols: usize, rows: usize, context: &Context) -> bool {
        if (cols, rows) == (self.grid.cols, self.grid.rows)
            && (cols, rows) == (self.overlay_grid.cols, self.overlay_grid.rows)
        {
            return true;
        }

        if self.grid.resize_with_context(cols, rows, context)
            && self.overlay_grid.resize_with_context(cols, rows, context)
        {
            self.generation = self.generation.next();
            self.cols = cols;
            self.rows = rows;
            self.grid.area = self.area();
            self.overlay_grid.area = self.area();
            return true;
        }

        false
    }
}

/// An `Area` consists of two points: the upper left and bottom right corners.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Area {
    offset: Pos,
    upper_left: Pos,
    bottom_right: Pos,
    empty: bool,
    canvas_cols: usize,
    canvas_rows: usize,
    generation: ScreenGeneration,
}

impl std::fmt::Debug for Area {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct(stringify!(Area))
            .field("width", &self.width())
            .field("height", &self.height())
            .field("offset", &self.offset)
            .field("upper_left", &self.upper_left)
            .field("bottom_right", &self.bottom_right)
            .field("empty flag", &self.empty)
            .field("is_empty", &self.is_empty())
            .field("canvas_cols", &self.canvas_cols)
            .field("canvas_rows", &self.canvas_rows)
            .field("generation", &self.generation)
            .finish()
    }
}

impl<D: private::Sealed> From<&Screen<D>> for Area {
    fn from(sc: &Screen<D>) -> Self {
        sc.area()
    }
}

/// Convenience trait to turn both single `usize` values and `(usize, _)`
/// positions to `x` coordinate.
pub trait IntoColumns: private::Sealed {
    #[must_use]
    fn into(self) -> usize;
}

impl private::Sealed for usize {}
impl private::Sealed for Pos {}

impl IntoColumns for usize {
    fn into(self) -> usize {
        self
    }
}

impl IntoColumns for Pos {
    fn into(self) -> usize {
        get_x(self)
    }
}

impl Area {
    #[inline]
    pub fn height(&self) -> usize {
        if self.is_empty() {
            return 0;
        }
        get_y(self.bottom_right).saturating_sub(get_y(self.upper_left)) + 1
    }

    #[inline]
    pub fn width(&self) -> usize {
        if self.is_empty() {
            return 0;
        }
        get_x(self.bottom_right).saturating_sub(get_x(self.upper_left)) + 1
    }

    #[inline]
    pub fn size(&self) -> (usize, usize) {
        (self.width(), self.height())
    }

    /// Get `n`th row of `area` or its last one.
    #[inline]
    #[must_use]
    pub fn nth_row(&self, n: usize) -> Self {
        let Self {
            offset,
            upper_left,
            bottom_right,
            empty,
            canvas_cols,
            canvas_rows,
            generation,
        } = *self;
        let (_, max_y) = bottom_right;
        let n = std::cmp::min(n, self.height());
        if self.is_empty() || max_y < (get_y(upper_left) + n) {
            return self.into_empty();
        }
        let y = std::cmp::min(max_y, get_y(upper_left) + n);
        Self {
            offset: pos_inc(offset, (0, n)),
            upper_left: set_y(upper_left, y),
            bottom_right: set_y(bottom_right, y),
            empty,
            canvas_cols,
            canvas_rows,
            generation,
        }
    }

    /// Get `n`th col of `area` or its last one.
    #[inline]
    #[must_use]
    pub fn nth_col(&self, n: usize) -> Self {
        let Self {
            offset,
            upper_left,
            bottom_right,
            empty,
            canvas_cols,
            canvas_rows,
            generation,
        } = *self;
        let (max_x, _) = bottom_right;
        let n = std::cmp::min(n, self.width());
        if self.is_empty() || max_x < (get_x(upper_left) + n) {
            return self.into_empty();
        }
        let x = std::cmp::min(max_x, get_x(upper_left) + n);
        Self {
            offset: pos_inc(offset, (x, 0)),
            upper_left: set_x(upper_left, x),
            bottom_right: set_x(bottom_right, x),
            empty,
            canvas_cols,
            canvas_rows,
            generation,
        }
    }

    /// Place box given by `(width, height)` in corner of `area`
    #[must_use]
    pub fn place_inside(&self, (width, height): (usize, usize), upper: bool, left: bool) -> Self {
        if self.is_empty() || width < 3 || height < 3 {
            return *self;
        }
        let (upper_x, upper_y) = self.upper_left;
        let (max_x, max_y) = self.bottom_right;
        let x = if upper {
            upper_x + 2
        } else {
            max_x.saturating_sub(2).saturating_sub(width)
        };

        let y = if left {
            upper_y + 2
        } else {
            max_y.saturating_sub(2).saturating_sub(height)
        };
        let upper_left = (std::cmp::min(x, max_x), std::cmp::min(y, max_y));
        let bottom_right = (
            std::cmp::min(x + width, max_x),
            std::cmp::min(y + height, max_y),
        );

        Self {
            offset: pos_inc(
                self.offset,
                (
                    (get_x(upper_left) - get_x(self.upper_left)),
                    (get_y(upper_left) - get_y(self.upper_left)),
                ),
            ),
            upper_left,
            bottom_right,
            empty: self.empty,
            canvas_cols: self.canvas_cols,
            canvas_rows: self.canvas_rows,
            generation: self.generation,
        }
    }

    /// Place given area of dimensions `(width, height)` inside `area` according
    /// to given alignment
    #[must_use]
    pub fn align_inside(
        &self,
        (width, height): (usize, usize),
        horizontal_alignment: Alignment,
        vertical_alignment: Alignment,
    ) -> Self {
        if self.is_empty() || width == 0 || height == 0 {
            return *self;
        }
        let (top_x, width) = match horizontal_alignment {
            Alignment::Center => (
                { std::cmp::max(self.width() / 2, width / 2) - width / 2 },
                width,
            ),
            Alignment::Start => (0, self.width().min(width)),
            Alignment::End => (self.width().saturating_sub(width), self.width().min(width)),
            Alignment::Fill => (0, self.width()),
        };
        let (top_y, height) = match vertical_alignment {
            Alignment::Center => (
                { std::cmp::max(self.height() / 2, height / 2) - height / 2 },
                self.height().min(height),
            ),
            Alignment::Start => (0, self.height().min(height)),
            Alignment::End => (self.height().saturating_sub(height), self.height()),
            Alignment::Fill => (0, self.height()),
        };

        self.skip(top_x, top_y).take(width, height)
    }

    /// Place box given by `dimensions` in center of `area`
    #[inline]
    #[must_use]
    pub fn center_inside(&self, dimensions: (usize, usize)) -> Self {
        self.align_inside(dimensions, Alignment::Center, Alignment::Center)
    }

    #[inline]
    pub fn contains(&self, other: Self) -> bool {
        debug_assert_eq!(self.generation, other.generation);
        if self.is_empty() {
            return false;
        } else if other.is_empty() {
            return true;
        }
        get_y(other.bottom_right) <= get_y(self.bottom_right)
            && get_x(other.upper_left) >= get_x(self.upper_left)
            && get_y(other.upper_left) >= get_y(self.upper_left)
            && get_x(other.bottom_right) <= get_x(self.bottom_right)
    }

    /// Skip `n` rows and return the remaining area.
    /// Return value will be an empty area if `n` is more than the height.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use meli::terminal::{Screen, Virtual, Area};
    /// # let mut screen = Screen::<Virtual>::new(Default::default());
    /// # assert!(screen.resize(120, 20));
    /// # let area = screen.area();
    /// assert_eq!(area.width(), 120);
    /// assert_eq!(area.height(), 20);
    /// // Skip first two rows:
    /// let body = area.skip_rows(2);
    /// assert_eq!(body.height(), 18);
    /// ```
    #[inline]
    #[must_use]
    pub fn skip_rows(&self, n: usize) -> Self {
        let n = std::cmp::min(n, self.height());
        if self.is_empty() || self.upper_left.1 + n > self.bottom_right.1 {
            return self.into_empty();
        }

        Self {
            offset: pos_inc(self.offset, (0, n)),
            upper_left: pos_inc(self.upper_left, (0, n)),
            ..*self
        }
    }

    /// Skip the last `n` rows and return the remaining area.
    /// Return value will be an empty area if `n` is more than the height.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use meli::terminal::{Screen, Virtual, Area};
    /// # let mut screen = Screen::<Virtual>::new(Default::default());
    /// # assert!(screen.resize(120, 20));
    /// # let area = screen.area();
    /// assert_eq!(area.width(), 120);
    /// assert_eq!(area.height(), 20);
    /// // Take only first two rows (equivalent to area.take_rows(2))
    /// let header = area.skip_rows_from_end(18);
    /// assert_eq!(header.height(), 2);
    /// assert_eq!(header, area.take_rows(2));
    /// ```
    #[inline]
    #[must_use]
    pub fn skip_rows_from_end(&self, n: usize) -> Self {
        let n = std::cmp::min(n, self.height());
        if self.is_empty() || self.bottom_right.1 < n {
            return self.into_empty();
        }

        Self {
            bottom_right: (self.bottom_right.0, self.bottom_right.1 - n),
            ..*self
        }
    }

    #[inline]
    #[must_use]
    fn _skip_cols_inner(&self, n: usize) -> Self {
        let n = std::cmp::min(n, self.width());
        if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n {
            return self.into_empty();
        }

        Self {
            offset: pos_inc(self.offset, (n, 0)),
            upper_left: pos_inc(self.upper_left, (n, 0)),
            ..*self
        }
    }

    /// Skip the first `n` rows and return the remaining area.
    /// Return value will be an empty area if `n` is more than the width.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use meli::terminal::{Screen, Virtual, Area};
    /// # let mut screen = Screen::<Virtual>::new(Default::default());
    /// # assert!(screen.resize(120, 20));
    /// # let area = screen.area();
    /// assert_eq!(area.width(), 120);
    /// assert_eq!(area.height(), 20);
    /// // Skip first two columns
    /// let indent = area.skip_cols(2);
    /// assert_eq!(indent.width(), 118);
    /// ```
    #[inline]
    #[must_use]
    pub fn skip_cols(&self, n: impl IntoColumns) -> Self {
        let n: usize = n.into();
        self._skip_cols_inner(n)
    }

    /// Skip the last `n` rows and return the remaining area.
    /// Return value will be an empty area if `n` is more than the width.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use meli::terminal::{Screen, Virtual, Area};
    /// # let mut screen = Screen::<Virtual>::new(Default::default());
    /// # assert!(screen.resize(120, 20));
    /// # let area = screen.area();
    /// assert_eq!(area.width(), 120);
    /// assert_eq!(area.height(), 20);
    /// // Skip last two columns
    /// let indent = area.skip_cols_from_end(2);
    /// assert_eq!(indent.width(), 118);
    /// assert_eq!(indent, area.take_cols(118));
    /// ```
    #[inline]
    #[must_use]
    pub fn skip_cols_from_end(&self, n: usize) -> Self {
        let n = std::cmp::min(n, self.width());
        if self.is_empty() || self.bottom_right.0 < n {
            return self.into_empty();
        }
        Self {
            bottom_right: (self.bottom_right.0 - n, self.bottom_right.1),
            ..*self
        }
    }

    /// Shortcut for using `Area::skip_cols` and `Area::skip_rows` together.
    #[inline]
    #[must_use]
    pub fn skip(&self, n_cols: usize, n_rows: usize) -> Self {
        self.skip_cols(n_cols).skip_rows(n_rows)
    }

    /// Take the first `n` rows and return the remaining area.
    /// Return value will be an empty area if `n` is more than the height.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use meli::terminal::{Screen, Virtual, Area};
    /// # let mut screen = Screen::<Virtual>::new(Default::default());
    /// # assert!(screen.resize(120, 20));
    /// # let area = screen.area();
    /// assert_eq!(area.width(), 120);
    /// assert_eq!(area.height(), 20);
    /// // Take only first two rows
    /// let header = area.take_rows(2);
    /// assert_eq!(header.height(), 2);
    /// ```
    #[inline]
    #[must_use]
    pub fn take_rows(&self, n: usize) -> Self {
        let n = std::cmp::min(n, self.height());
        if self.is_empty() || self.bottom_right.1 < (self.height() - n) {
            return self.into_empty();
        }

        Self {
            bottom_right: (
                self.bottom_right.0,
                self.bottom_right.1 - (self.height() - n),
            ),
            ..*self
        }
    }

    /// Take the first `n` columns and return the remaining area.
    /// Return value will be an empty area if `n` is more than the width.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use meli::terminal::{Screen, Virtual, Area};
    /// # let mut screen = Screen::<Virtual>::new(Default::default());
    /// # assert!(screen.resize(120, 20));
    /// # let area = screen.area();
    /// assert_eq!(area.width(), 120);
    /// assert_eq!(area.height(), 20);
    /// // Take only first two columns
    /// let header = area.take_cols(2);
    /// assert_eq!(header.width(), 2);
    /// ```
    #[inline]
    #[must_use]
    pub fn take_cols(&self, n: usize) -> Self {
        let n = std::cmp::min(n, self.width());
        if self.is_empty() || self.bottom_right.0 < (self.width() - n) {
            return self.into_empty();
        }

        Self {
            bottom_right: (
                self.bottom_right.0 - (self.width() - n),
                self.bottom_right.1,
            ),
            ..*self
        }
    }

    /// Shortcut for using `Area::take_cols` and `Area::take_rows` together.
    #[inline]
    #[must_use]
    pub fn take(&self, n_cols: usize, n_rows: usize) -> Self {
        self.take_cols(n_cols).take_rows(n_rows)
    }

    #[inline]
    #[must_use]
    pub const fn upper_left(&self) -> Pos {
        self.upper_left
    }

    #[inline]
    #[must_use]
    pub const fn bottom_right(&self) -> Pos {
        self.bottom_right
    }

    #[inline]
    #[must_use]
    pub const fn upper_right(&self) -> Pos {
        set_x(self.upper_left, get_x(self.bottom_right))
    }

    #[inline]
    #[must_use]
    pub const fn bottom_left(&self) -> Pos {
        set_y(self.upper_left, get_y(self.bottom_right))
    }

    #[inline]
    #[must_use]
    pub const fn offset(&self) -> Pos {
        self.offset
    }

    #[inline]
    #[must_use]
    pub const fn generation(&self) -> ScreenGeneration {
        self.generation
    }

    #[inline]
    #[must_use]
    pub const fn new_empty(generation: ScreenGeneration) -> Self {
        Self {
            offset: (0, 0),
            upper_left: (0, 0),
            bottom_right: (0, 0),
            canvas_rows: 0,
            canvas_cols: 0,
            empty: true,
            generation,
        }
    }

    #[inline]
    #[must_use]
    pub const fn into_empty(self) -> Self {
        Self {
            offset: (0, 0),
            upper_left: (0, 0),
            bottom_right: (0, 0),
            empty: true,
            ..self
        }
    }

    #[inline]
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.empty
            || (self.upper_left.0 > self.bottom_right.0 || self.upper_left.1 > self.bottom_right.1)
    }
}

#[inline(always)]
#[must_use]
const fn pos_inc(p: Pos, inc: (usize, usize)) -> Pos {
    (p.0 + inc.0, p.1 + inc.1)
}

#[inline(always)]
#[must_use]
const fn get_x(p: Pos) -> usize {
    p.0
}

#[inline(always)]
#[must_use]
const fn get_y(p: Pos) -> usize {
    p.1
}

#[inline(always)]
#[must_use]
const fn set_x(p: Pos, new_x: usize) -> Pos {
    (new_x, p.1)
}

#[inline(always)]
#[must_use]
const fn set_y(p: Pos, new_y: usize) -> Pos {
    (p.0, new_y)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_skip_rows() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 1..=area.height() {
            assert_eq!(area.skip_rows(i).height(), area.height() - i);
            assert!(area.contains(area.skip_rows(i)));
            if i < area.height() {
                assert!(!area.take_rows(i).contains(area.skip_rows(i)));
            } else {
                assert!(area.take_rows(i).contains(area.skip_rows(i)));
            }
        }

        assert!(area.skip_rows(area.height()).is_empty());
        assert_eq!(area.skip_rows(0), area);
    }

    #[test]
    fn test_skip_rows_from_end() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 1..=area.height() {
            assert_eq!(area.skip_rows_from_end(i).height(), area.height() - i);
            assert!(area.contains(area.skip_rows_from_end(i)));
        }

        assert!(area.skip_rows_from_end(area.height()).is_empty());
        assert_eq!(area.skip_rows_from_end(0), area);
    }

    #[test]
    fn test_skip_cols() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 1..=area.width() {
            assert_eq!(area.skip_cols(i).width(), area.width() - i);
            assert!(area.contains(area.skip_cols(i)));
            if i < area.width() {
                assert!(!area.take_cols(i).contains(area.skip_cols(i)));
            } else {
                assert!(area.take_cols(i).contains(area.skip_cols(i)));
            }
        }

        assert!(area.skip_cols(area.width()).is_empty());
        assert_eq!(area.skip_cols(0), area);
    }

    #[test]
    fn test_skip_cols_from_end() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 1..=area.width() {
            assert_eq!(area.skip_cols_from_end(i).width(), area.width() - i);
            assert!(area.contains(area.skip_cols_from_end(i)));
        }

        assert!(area.skip_cols_from_end(area.width()).is_empty());
        assert_eq!(area.skip_cols_from_end(0), area);
    }

    #[test]
    fn test_take_rows() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 1..=area.height() {
            assert_eq!(area.take_rows(i).height(), i);
            assert!(area.contains(area.take_rows(i)));
        }

        assert!(area.take_rows(0).is_empty());
        assert_eq!(area.take_rows(area.height()), area);
    }

    #[test]
    fn test_take_cols() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 1..=area.width() {
            assert_eq!(area.take_cols(i).width(), i);
            assert!(area.contains(area.take_cols(i)));
        }

        assert!(area.take_cols(0).is_empty());
        assert_eq!(area.take_cols(area.width()), area);
    }

    #[test]
    fn test_nth_area() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for i in 0..area.width() {
            assert_eq!(area.nth_col(i).width(), 1);
            assert!(area.contains(area.nth_col(i)));
            if i + 1 == area.width() {
                assert!(area.nth_col(i).contains(area.nth_col(i + 1)));
            } else {
                assert!(!area.nth_col(i).contains(area.nth_col(i + 1)));
            }
        }

        for i in 0..area.height() {
            assert_eq!(area.nth_row(i).height(), 1);
            assert!(area.contains(area.nth_row(i)));
            if i + 1 == area.height() {
                assert!(area.nth_row(i).contains(area.nth_row(i + 1)));
            } else {
                assert!(!area.nth_row(i).contains(area.nth_row(i + 1)));
            }
        }
    }

    #[test]
    fn test_place_inside_area() {
        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        for width in 0..area.width() {
            for height in 0..area.height() {
                for upper in [true, false] {
                    for left in [true, false] {
                        let inner = area.place_inside((width, height), upper, left);
                        assert!(area.contains(inner));
                        if (3..area.height() - 2).contains(&height)
                            && (3..area.width() - 2).contains(&width)
                        {
                            assert_ne!(area, inner);
                        }
                    }
                }
            }
        }
    }

    #[test]
    fn test_align_inside_area() {
        use Alignment::{Center, End, Fill, Start};

        const ALIGNMENTS: [Alignment; 4] = [Fill, Start, End, Center];

        let mut screen = Screen::<Virtual>::new(Default::default());
        assert!(screen.resize(120, 20));
        let area = screen.area();
        assert_eq!(area.width(), 120);
        assert_eq!(area.height(), 20);

        // Ask for all subsets that area has:
        for width in 1..=area.width() {
            for height in 1..=area.height() {
                for horz in ALIGNMENTS {
                    for vert in ALIGNMENTS {
                        let inner = area.align_inside((width, height), horz, vert);
                        assert!(area.contains(inner));
                        assert!(!inner.is_empty());
                        match (horz, vert) {
                            (Fill, Fill) => {
                                assert_eq!(area, inner);
                            }
                            (Fill, _) => {
                                assert_eq!(inner.width(), area.width());
                                assert_eq!(inner.height(), height);
                            }
                            (_, Fill) => {
                                assert_eq!(inner.height(), area.height());
                                assert_eq!(inner.width(), width);
                            }
                            _ => {
                                assert_eq!((width, height), inner.size());
                                if (width, height) != area.size() {
                                    assert_ne!(area, inner);
                                }
                            }
                        }
                    }
                }
            }
        }

        // Ask for more width/height than area has:
        for width in 1..=(2 * area.width()) {
            for height in 1..=(2 * area.height()) {
                for horz in ALIGNMENTS {
                    for vert in ALIGNMENTS {
                        let inner = area.align_inside((width, height), horz, vert);
                        assert!(area.contains(inner));
                        assert!(!inner.is_empty());
                        match (horz, vert) {
                            (Fill, Fill) => {
                                assert_eq!(area, inner);
                            }
                            (Fill, _) => {
                                assert_eq!(inner.width(), area.width());
                                assert!(
                                    height >= inner.height() && area.height() >= inner.height()
                                );
                            }
                            (_, Fill) => {
                                assert_eq!(inner.height(), area.height());
                                assert!(width >= inner.width() && area.width() >= inner.width());
                            }
                            _ => {
                                assert!(
                                    height >= inner.height() && area.height() >= inner.height()
                                );
                                assert!(width >= inner.width() && area.width() >= inner.width());
                            }
                        }
                    }
                }
            }
        }
    }
}
