//! Prompts for input.

use std::char;
use std::fmt::Write;

use termwiz::cell::{AttributeChange, CellAttributes};
use termwiz::color::{AnsiColor, ColorAttribute};
use termwiz::input::KeyEvent;
use termwiz::surface::change::Change;
use termwiz::surface::Position;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

use crate::display::DisplayAction;
use crate::error::Error;
use crate::prompt_history::PromptHistory;
use crate::screen::Screen;
use crate::util;

type PromptRunFn = dyn FnMut(&mut Screen, &str) -> Result<DisplayAction, Error>;

/// A prompt for input from the user.
pub(crate) struct Prompt {
    /// The text of the prompt to display to the user.
    prompt: String,

    /// The current prompt history,
    history: PromptHistory,

    /// The closure to run when the user presses Return.  Will only be called once.
    run: Option<Box<PromptRunFn>>,
}

pub(crate) struct PromptState {
    /// The value the user is typing in.
    value: Vec<char>,

    /// The offset within the value that we are displaying from.
    offset: usize,

    /// The cursor position within the value.
    position: usize,
}

impl PromptState {
    pub(crate) fn new() -> PromptState {
        PromptState {
            value: Vec::new(),
            offset: 0,
            position: 0,
        }
    }

    pub(crate) fn load(data: &str) -> PromptState {
        let mut value = Vec::new();
        let mut iter = data.chars();
        while let Some(c) = iter.next() {
            if c == '\\' {
                if let Some(c) = iter.next() {
                    if c == 'x' {
                        if let (Some(c1), Some(c2)) = (iter.next(), iter.next()) {
                            let hex: String = [c1, c2].iter().collect();
                            if let Some(c) =
                                u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
                            {
                                value.push(c);
                            }
                        }
                    } else {
                        value.push(c);
                    }
                }
            } else {
                value.push(c);
            }
        }
        let position = value.len();
        PromptState {
            value,
            offset: 0,
            position,
        }
    }

    pub(crate) fn save(&self) -> String {
        let mut data = String::new();
        for &c in self.value.iter() {
            if c == '\\' {
                data.push_str("\\\\");
            } else if c < ' ' || c == '\x7f' {
                write!(data, "\\x{:02X}", c as u8).expect("writes to strings can't fail")
            } else {
                data.push(c);
            }
        }
        data
    }

    /// Returns the column for the cursor.
    pub(crate) fn cursor_position(&self) -> usize {
        let mut position = 0;
        for c in self.value[self.offset..self.position].iter() {
            position += render_width(*c);
        }
        position
    }

    /// Clamp the offset to values appropriate for the length of the value and
    /// the current cursor position.  Keeps at least 4 characters visible to the
    /// left and right of the value if possible.
    fn clamp_offset(&mut self, width: usize) {
        if self.offset > self.position {
            self.offset = self.position;
        }
        while self.cursor_position() < 5 && self.offset > 0 {
            self.offset -= 1;
        }
        while self.cursor_position() > width - 5 && self.offset < self.position {
            self.offset += 1;
        }
    }

    /// Renders the prompt onto the terminal.
    fn render(&mut self, changes: &mut Vec<Change>, mut position: usize, width: usize) {
        let mut start = self.offset;
        let mut end = self.offset;
        while end < self.value.len() {
            let c = self.value[end];
            if let Some(render) = special_render(self.value[end]) {
                if end > start {
                    let value: String = self.value[start..end].iter().collect();
                    changes.push(Change::Text(value));
                }
                let render = util::truncate_string(render, 0, width - position);
                position += render.width();
                changes.push(Change::Attribute(AttributeChange::Reverse(true)));
                changes.push(Change::Text(render));
                changes.push(Change::Attribute(AttributeChange::Reverse(false)));
                start = end + 1;
                // Control characters can't compose, so stop if we hit the end.
                if position >= width {
                    break;
                }
            } else {
                let w = c.width().unwrap_or(0);
                if position + w > width {
                    // This character would take us past the end, so stop.
                    break;
                }
                position += w;
            }
            end += 1;
        }
        if end > start {
            let value: String = self.value[start..end].iter().collect();
            changes.push(Change::Text(value));
        }
        if position < width {
            changes.push(Change::ClearToEndOfLine(ColorAttribute::default()));
        }
    }

    /// Insert a character at the current position.
    fn insert_char(&mut self, c: char, width: usize) -> DisplayAction {
        self.value.insert(self.position, c);
        self.position += 1;
        if self.position == self.value.len() && self.cursor_position() < width - 5 {
            DisplayAction::Change(Change::Text(c.to_string()))
        } else {
            DisplayAction::RefreshPrompt
        }
    }

    fn insert_str(&mut self, s: &str) -> DisplayAction {
        let old_len = self.value.len();
        self.value.splice(self.position..self.position, s.chars());
        self.position += self.value.len() - old_len;
        DisplayAction::RefreshPrompt
    }

    /// Delete previous character.
    fn delete_prev_char(&mut self) -> DisplayAction {
        if self.position > 0 {
            self.value.remove(self.position - 1);
            self.position -= 1;
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Delete next character.
    fn delete_next_char(&mut self) -> DisplayAction {
        if self.position < self.value.len() {
            self.value.remove(self.position);
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Delete previous word.
    fn delete_prev_word(&mut self) -> DisplayAction {
        let dest = move_word_backwards(self.value.as_slice(), self.position);
        if dest != self.position {
            self.value.splice(dest..self.position, None);
            self.position = dest;
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Delete next word.
    fn delete_next_word(&mut self) -> DisplayAction {
        let dest = move_word_forwards(self.value.as_slice(), self.position);
        if dest != self.position {
            self.value.splice(self.position..dest, None);
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Move right one character.
    fn move_next_char(&mut self) -> DisplayAction {
        if self.position < self.value.len() {
            self.position += 1;
            while self.position < self.value.len() {
                let w = render_width(self.value[self.position]);
                if w != 0 {
                    break;
                }
                self.position += 1;
            }
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Move left one character.
    fn move_prev_char(&mut self) -> DisplayAction {
        if self.position > 0 {
            while self.position > 0 {
                self.position -= 1;
                let w = render_width(self.value[self.position]);
                if w != 0 {
                    break;
                }
            }
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Move right one word.
    fn move_next_word(&mut self) -> DisplayAction {
        let dest = move_word_forwards(self.value.as_slice(), self.position);
        if dest != self.position {
            self.position = dest;
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Move left one word.
    fn move_prev_word(&mut self) -> DisplayAction {
        let dest = move_word_backwards(self.value.as_slice(), self.position);
        if dest != self.position {
            self.position = dest;
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Delete to end of line.
    fn delete_to_end(&mut self) -> DisplayAction {
        if self.position < self.value.len() {
            self.value.splice(self.position.., None);
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Delete to start of line.
    fn delete_to_start(&mut self) -> DisplayAction {
        if self.position > 0 {
            self.value.splice(..self.position, None);
            self.position = 0;
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    /// Move to end of line.
    fn move_to_end(&mut self) -> DisplayAction {
        self.position = self.value.len();
        DisplayAction::RefreshPrompt
    }

    /// Move to beginning of line.
    fn move_to_start(&mut self) -> DisplayAction {
        self.position = 0;
        DisplayAction::RefreshPrompt
    }

    /// Transpose characters.
    fn transpose_chars(&mut self) -> DisplayAction {
        if self.position > 0 && self.value.len() > 1 {
            if self.position < self.value.len() {
                self.position += 1;
            }
            self.value.swap(self.position - 2, self.position - 1);
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }
}

impl Prompt {
    /// Create a new prompt.
    pub(crate) fn new(ident: impl Into<String>, prompt: &str, run: Box<PromptRunFn>) -> Prompt {
        Prompt {
            prompt: prompt.to_string(),
            history: PromptHistory::open(ident),
            run: Some(run),
        }
    }

    fn state(&self) -> &PromptState {
        self.history.state()
    }

    fn state_mut(&mut self) -> &mut PromptState {
        self.history.state_mut()
    }

    /// Returns the column for the cursor.
    pub(crate) fn cursor_position(&self) -> usize {
        self.prompt.width() + 4 + self.state().cursor_position()
    }

    /// Renders the prompt onto the terminal.
    pub(crate) fn render(&mut self, changes: &mut Vec<Change>, row: usize, width: usize) {
        changes.push(Change::CursorPosition {
            x: Position::Absolute(0),
            y: Position::Absolute(row),
        });
        changes.push(Change::AllAttributes(
            CellAttributes::default()
                .set_foreground(AnsiColor::Black)
                .set_background(AnsiColor::Silver)
                .clone(),
        ));
        changes.push(Change::Text(format!("  {} ", self.prompt)));
        changes.push(Change::AllAttributes(CellAttributes::default()));
        changes.push(Change::Text(" ".into()));
        let offset = self.prompt.width() + 4;
        self.state_mut().render(changes, offset, width);
    }

    /// Dispatch a key press to the prompt.
    pub(crate) fn dispatch_key(&mut self, key: KeyEvent, width: usize) -> DisplayAction {
        use termwiz::input::{KeyCode::*, Modifiers};
        const CTRL: Modifiers = Modifiers::CTRL;
        const NONE: Modifiers = Modifiers::NONE;
        const ALT: Modifiers = Modifiers::ALT;
        let value_width = width - self.prompt.width() - 4;
        let action = match (key.modifiers, key.key) {
            (NONE, Enter) | (CTRL, Char('J')) | (CTRL, Char('M')) => {
                // Finish.
                let _ = self.history.save();
                let mut run = self.run.take();
                let value: String = self.state().value[..].iter().collect();
                return DisplayAction::Run(Box::new(move |screen: &mut Screen| {
                    screen.clear_prompt();
                    if let Some(ref mut run) = run {
                        run(screen, &value)
                    } else {
                        Ok(DisplayAction::Render)
                    }
                }));
            }
            (NONE, Escape) | (CTRL, Char('C')) => {
                // Cancel.
                return DisplayAction::Run(Box::new(|screen: &mut Screen| {
                    screen.clear_prompt();
                    Ok(DisplayAction::Render)
                }));
            }
            (NONE, Char(c)) => self.state_mut().insert_char(c, value_width),
            (NONE, Backspace) | (CTRL, Char('H')) => self.state_mut().delete_prev_char(),
            (NONE, Delete) | (CTRL, Char('D')) => self.state_mut().delete_next_char(),
            (CTRL, Char('W')) | (ALT, Backspace) => self.state_mut().delete_prev_word(),
            (ALT, Char('d')) => self.state_mut().delete_next_word(),
            (NONE, RightArrow) | (CTRL, Char('F')) => self.state_mut().move_next_char(),
            (NONE, LeftArrow) | (CTRL, Char('B')) => self.state_mut().move_prev_char(),
            (CTRL, RightArrow) | (ALT, Char('f')) => self.state_mut().move_next_word(),
            (CTRL, LeftArrow) | (ALT, Char('b')) => self.state_mut().move_prev_word(),
            (CTRL, Char('K')) => self.state_mut().delete_to_end(),
            (CTRL, Char('U')) => self.state_mut().delete_to_start(),
            (NONE, End) | (CTRL, Char('E')) => self.state_mut().move_to_end(),
            (NONE, Home) | (CTRL, Char('A')) => self.state_mut().move_to_start(),
            (CTRL, Char('T')) => self.state_mut().transpose_chars(),
            (NONE, UpArrow) => self.history.previous(),
            (NONE, DownArrow) => self.history.next(),
            _ => return DisplayAction::None,
        };
        self.state_mut().clamp_offset(value_width);
        action
    }

    /// Paste some text into the prompt.
    pub(crate) fn paste(&mut self, text: &str, width: usize) -> DisplayAction {
        let value_width = width - self.prompt.width() - 4;
        let action = self.state_mut().insert_str(text);
        self.state_mut().clamp_offset(value_width);
        action
    }
}

fn move_word_forwards(value: &[char], mut position: usize) -> usize {
    let len = value.len();
    while position < len && value[position].is_whitespace() {
        position += 1;
    }
    while position < len && !value[position].is_whitespace() {
        position += 1;
    }
    position
}

fn move_word_backwards(value: &[char], mut position: usize) -> usize {
    while position > 0 {
        position -= 1;
        if !value[position].is_whitespace() {
            break;
        }
    }
    while position > 0 {
        if value[position].is_whitespace() {
            position += 1;
            break;
        }
        position -= 1;
    }
    position
}

/// Determine the rendering width for a character.
fn render_width(c: char) -> usize {
    if c < ' ' || c == '\x7F' {
        // Render as <XX>
        4
    } else if let Some(w) = c.width() {
        // Render as the character itself
        w
    } else {
        // Render as <U+XXXX>
        8
    }
}

/// Determine the special rendering for a character, if any.
fn special_render(c: char) -> Option<String> {
    if c < ' ' || c == '\x7F' {
        Some(format!("<{:02X}>", c as u8))
    } else if c.width().is_none() {
        Some(format!("<U+{:04X}>", c as u32))
    } else {
        None
    }
}


//! Prompt History.

use std::fs::File;
use std::io::{BufRead, BufReader, Write};

use tempfile::NamedTempFile;

use crate::display::DisplayAction;
use crate::error::Error;
use crate::prompt::PromptState;

const HISTORY_LENGTH: usize = 1000;

struct HistoryEntry {
    /// The stored state of the history entry.
    stored: Option<String>,

    /// The active state of the history entry.
    state: Option<PromptState>,
}

impl HistoryEntry {
    fn new() -> Self {
        HistoryEntry {
            stored: None,
            state: Some(PromptState::new()),
        }
    }

    fn load(data: String) -> Self {
        HistoryEntry {
            stored: Some(data),
            state: None,
        }
    }

    fn save(&self) -> Option<String> {
        self.state.as_ref().map(|state| state.save())
    }

    fn activate(&mut self) {
        if self.state.is_none() {
            if let Some(stored) = &self.stored {
                self.state = Some(PromptState::load(stored));
            } else {
                self.state = Some(PromptState::new());
            }
        }
    }

    fn state(&self) -> &PromptState {
        self.state.as_ref().expect("state should exist")
    }

    fn state_mut(&mut self) -> &mut PromptState {
        self.state.as_mut().expect("state should exist")
    }
}

pub(crate) struct PromptHistory {
    ident: String,

    entries: Vec<HistoryEntry>,

    active_index: usize,
}

impl PromptHistory {
    pub(crate) fn open(ident: impl Into<String>) -> Self {
        let ident = ident.into();
        let mut entries = Vec::new();
        if let Some(mut path) = dirs::data_dir() {
            path.push("streampager");
            path.push("history");
            path.push(format!("{}.history", ident));
            if let Ok(file) = File::open(path) {
                let file = BufReader::new(file);
                entries = file
                    .lines()
                    .filter_map(|entry| entry.map(HistoryEntry::load).ok())
                    .collect();
            }
        }
        let active_index = entries.len();
        entries.push(HistoryEntry::new());
        PromptHistory {
            ident,
            entries,
            active_index,
        }
    }

    pub(crate) fn state(&self) -> &PromptState {
        self.entries[self.active_index].state()
    }

    pub(crate) fn state_mut(&mut self) -> &mut PromptState {
        self.entries[self.active_index].state_mut()
    }

    /// Stored value from history.
    pub(crate) fn stored(&self) -> Option<String> {
        self.entries[self.active_index].stored.clone()
    }

    pub(crate) fn previous(&mut self) -> DisplayAction {
        if self.active_index > 0 {
            self.active_index -= 1;
            self.entries[self.active_index].activate();
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    pub(crate) fn next(&mut self) -> DisplayAction {
        if self.active_index < self.entries.len() - 1 {
            self.active_index += 1;
            self.entries[self.active_index].activate();
            DisplayAction::RefreshPrompt
        } else {
            DisplayAction::None
        }
    }

    pub(crate) fn save(&mut self) -> Result<(), Error> {
        if let Some(data) = self.entries[self.active_index].save() {
            if data.is_empty() {
                return Ok(());
            }
            if self.entries.len() > 1 {
                if let Some(previous_data) = &self.entries[self.entries.len() - 2].stored {
                    if data == *previous_data {
                        return Ok(());
                    }
                }
            }
            if let Some(mut path) = dirs::data_dir() {
                path.push("streampager");
                path.push("history");
                std::fs::create_dir_all(&path)?;
                let mut new_file = NamedTempFile::new_in(&path)?;
                path.push(format!("{}.history", &self.ident));
                if let Ok(file) = File::open(&path) {
                    let file = BufReader::new(file);
                    for line in file
                        .lines()
                        .skip(self.entries.len().saturating_sub(HISTORY_LENGTH))
                    {
                        writeln!(new_file, "{}", line?)?;
                    }
                }
                writeln!(new_file, "{}", data)?;
                new_file.persist(&path)?;
            }
        }
        Ok(())
    }
}

/// Peak the last entry from history.
pub(crate) fn peek_last(ident: &str) -> Option<String> {
    let mut history = PromptHistory::open(ident);
    history.previous();
    history.stored()
}


//! Track screen refresh regions.

use std::cmp::{max, min};

use bit_set::BitSet;

/// Tracks which parts of the screen need to be refreshed.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Refresh {
    /// Nothing to render.
    None,

    /// The rows in the bitset must be rendered.
    Rows(BitSet),

    /// The whole screen must be rendered.
    All,
}

fn fill_range(b: &mut BitSet, start: usize, end: usize, fill: bool) {
    if fill {
        b.extend(start..end);
    } else {
        for row in start..end {
            b.remove(row);
        }
    }
}

impl Refresh {
    /// Add a range of rows to the rows that must be rendered.
    pub(crate) fn add_range(&mut self, start: usize, end: usize) {
        match *self {
            Refresh::None => {
                let mut b = BitSet::new();
                b.extend(start..end);
                *self = Refresh::Rows(b);
            }
            Refresh::Rows(ref mut b) => {
                b.extend(start..end);
            }
            Refresh::All => {}
        }
    }

    /// Rotate the range of rows between start and end upwards (towards 0).  Rows that roll past
    /// the start are dropped.  New rows introduced are filled with the fill value.
    pub(crate) fn rotate_range_up(&mut self, start: usize, end: usize, step: usize, fill: bool) {
        match *self {
            Refresh::All => {}
            Refresh::None => {
                if fill {
                    let mut b = BitSet::new();
                    let mid = max(start, end.saturating_sub(step));
                    b.extend(mid..end);
                    *self = Refresh::Rows(b);
                }
            }
            Refresh::Rows(ref mut b) => {
                let mid = max(start, end.saturating_sub(step));
                for row in start..mid {
                    if b.contains(row + step) {
                        b.insert(row);
                    } else {
                        b.remove(row);
                    }
                }
                fill_range(b, mid, end, fill);
            }
        }
    }

    /// Rotate the range of rows between start and end downwards (away from 0).  Rows that roll
    /// past the end are dropped.  New rows introduced are filled with the fill value.
    pub(crate) fn rotate_range_down(&mut self, start: usize, end: usize, step: usize, fill: bool) {
        match *self {
            Refresh::None => {
                if fill {
                    let mut b = BitSet::new();
                    let mid = min(start.saturating_add(step), end);
                    b.extend(start..mid);
                    *self = Refresh::Rows(b);
                }
            }
            Refresh::Rows(ref mut b) => {
                let mid = min(start.saturating_add(step), end);
                for row in (mid..end).rev() {
                    if b.contains(row - step) {
                        b.insert(row);
                    } else {
                        b.remove(row);
                    }
                }
                fill_range(b, start, mid, fill);
            }
            Refresh::All => {}
        }
    }

    /// Does the range contain the given orow
    pub(crate) fn contains(&self, row: usize) -> bool {
        match *self {
            Refresh::None => false,
            Refresh::Rows(ref b) => b.contains(row),
            Refresh::All => true,
        }
    }
}


//! The Ruler

use std::cmp::{max, min};
use std::fmt::Write;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Instant;

use termwiz::surface::change::Change;
use unicode_width::UnicodeWidthStr;

use crate::bar::{Bar, BarItem, BarString, BarStyle};
use crate::config::WrappingMode;
use crate::file::{File, FileInfo};
use crate::util;

pub(crate) struct Ruler {
    position: Arc<PositionIndicator>,
    loading: Arc<LoadingIndicator>,
    repeat_count: Arc<RepeatCountIndicator>,
    ruler_bar: Bar,
}

impl Ruler {
    pub(crate) fn new(file: File) -> Self {
        let title = Arc::new(BarString::new(file.title().to_string()));
        let file_info = Arc::new(FileInformationIndicator::new(file.clone()));
        let position = Arc::new(PositionIndicator::new(file.clone()));
        let loading = Arc::new(LoadingIndicator::new(file));
        let repeat_count = Arc::new(RepeatCountIndicator::default());

        let mut ruler_bar = Bar::new(BarStyle::Normal);
        ruler_bar.add_left_item(title);
        ruler_bar.add_right_item(repeat_count.clone());
        ruler_bar.add_right_item(file_info);
        ruler_bar.add_right_item(position.clone());
        ruler_bar.add_right_item(loading.clone());

        Ruler {
            position,
            loading,
            repeat_count,
            ruler_bar,
        }
    }

    pub(crate) fn bar(&self) -> &Bar {
        &self.ruler_bar
    }

    pub(crate) fn set_position(
        &self,
        top: usize,
        left: usize,
        bottom: Option<usize>,
        wrapping_mode: WrappingMode,
    ) {
        self.position.top.store(top, Ordering::SeqCst);
        self.position.left.store(left, Ordering::SeqCst);
        let (bottom, following_end) = match bottom {
            Some(bottom) => (bottom, false),
            None => (0, true),
        };
        self.position.bottom.store(bottom, Ordering::SeqCst);
        self.position.line_wrapping.store(
            wrapping_mode == WrappingMode::GraphemeBoundary,
            Ordering::SeqCst,
        );
        self.position.word_wrapping.store(
            wrapping_mode == WrappingMode::WordBoundary,
            Ordering::SeqCst,
        );
        self.loading
            .following_end
            .store(following_end, Ordering::SeqCst);
    }

    pub(crate) fn set_repeat_count(&self, count: Option<usize>) {
        self.repeat_count
            .count
            .store(count.unwrap_or(0), Ordering::Relaxed);
    }
}

/// Shows the file's additional information.
struct FileInformationIndicator {
    file: File,
}

impl FileInformationIndicator {
    fn new(file: File) -> Self {
        FileInformationIndicator { file }
    }
}

impl BarItem for FileInformationIndicator {
    fn width(&self) -> usize {
        self.file.info().width()
    }

    fn render(&self, changes: &mut Vec<Change>, width: usize) {
        changes.push(Change::Text(util::truncate_string(
            self.file.info(),
            0,
            width,
        )));
    }
}

/// Indicates the current position within the file.
struct PositionIndicator {
    file: File,
    top: AtomicUsize,
    left: AtomicUsize,
    bottom: AtomicUsize,
    line_wrapping: AtomicBool,
    word_wrapping: AtomicBool,
}

impl PositionIndicator {
    pub(crate) fn new(file: File) -> Self {
        PositionIndicator {
            file,
            top: AtomicUsize::new(0),
            left: AtomicUsize::new(0),
            bottom: AtomicUsize::new(0),
            line_wrapping: AtomicBool::new(false),
            word_wrapping: AtomicBool::new(false),
        }
    }
}

impl BarItem for PositionIndicator {
    fn width(&self) -> usize {
        let top = self.top.load(Ordering::SeqCst);
        let left = self.left.load(Ordering::SeqCst);
        let bottom = self.bottom.load(Ordering::SeqCst);
        let line_wrapping = self.line_wrapping.load(Ordering::SeqCst);
        let word_wrapping = self.word_wrapping.load(Ordering::SeqCst);
        let mut width = 0;
        let file_lines = self.file.lines();
        let nw = max(3, util::number_width(max(file_lines, max(bottom, top + 1))));

        if line_wrapping || word_wrapping {
            width += 6;
        } else if left > 1 {
            // Indicate horizontal position as "+N" if we are not at the very left.
            width += util::number_width(left + 1) + 3;
        }

        if top > file_lines {
            // We are past end of the file, show as "line NNN/NNN".
            width += 2 * nw + 6;
        } else {
            // We are displaying normally, show as "lines NNN-NNN/NNN".
            width += 3 * nw + 8;
        }

        width
    }

    fn render(&self, changes: &mut Vec<Change>, width: usize) {
        let top = self.top.load(Ordering::SeqCst);
        let left = self.left.load(Ordering::SeqCst);
        let bottom = self.bottom.load(Ordering::SeqCst);
        let line_wrapping = self.line_wrapping.load(Ordering::SeqCst);
        let word_wrapping = self.word_wrapping.load(Ordering::SeqCst);
        let file_lines = self.file.lines();
        let mut out = String::new();
        let nw = max(3, util::number_width(max(file_lines, max(bottom, top + 1))));

        if line_wrapping {
            write!(out, "wrap  ").expect("writes to strings should not fail");
        } else if word_wrapping {
            write!(out, "word  ").expect("writes to strings should not fail");
        } else if left > 0 {
            write!(out, "{:+}  ", left + 1,).expect("writes to strings should not fail");
        }

        if top > file_lines {
            write!(out, "line {1:0}/{2:0$}", nw, top + 1, file_lines)
        } else if bottom > 0 {
            write!(
                out,
                "lines {1:0$}-{2:0$}/{3:0$.0$}",
                nw,
                top + 1,
                min(bottom, file_lines),
                file_lines,
            )
        } else {
            write!(
                out,
                "lines {1:0$}-{2:0$}/{3:0$.0$}",
                nw,
                top + 1,
                "END",
                file_lines,
            )
        }
        .expect("writes to strings can't fail");

        changes.push(Change::Text(util::truncate_string(&out, 0, width)));
    }
}

/// Shows whether or not the file is loading.
struct LoadingIndicator {
    file: File,
    following_end: AtomicBool,
    animation_start: Instant,
}

impl LoadingIndicator {
    fn new(file: File) -> Self {
        LoadingIndicator {
            file,
            following_end: AtomicBool::new(false),
            animation_start: Instant::now(),
        }
    }

    fn content(&self) -> Option<&'static str> {
        if self.file.loaded() {
            None
        } else if self.file.paused() && !self.following_end.load(Ordering::SeqCst) {
            Some("[loading paused]")
        } else {
            let frame_index = (self.animation_start.elapsed().subsec_millis() / 200) as usize;
            let frame = [
                "[loading â€¢     ]",
                "[loading  â€¢    ]",
                "[loading   â€¢   ]",
                "[loading    â€¢  ]",
                "[loading     â€¢ ]",
            ][frame_index];
            Some(frame)
        }
    }
}

impl BarItem for LoadingIndicator {
    fn width(&self) -> usize {
        if self.file.loaded() {
            0
        } else {
            16
        }
    }

    fn render(&self, changes: &mut Vec<Change>, width: usize) {
        if let Some(content) = self.content() {
            changes.push(Change::Text(util::truncate_string(content, 0, width)));
        }
    }
}

#[derive(Default)]
struct RepeatCountIndicator {
    count: AtomicUsize,
}

impl BarItem for RepeatCountIndicator {
    fn width(&self) -> usize {
        let mut count = self.count.load(Ordering::Relaxed);
        let mut width = 0;
        while count > 0 {
            count /= 10;
            width += 1;
        }
        width
    }

    fn render(&self, changes: &mut Vec<Change>, width: usize) {
        let count = self.count.load(Ordering::Relaxed);
        if count > 0 {
            let content = format!("{}", count);
            changes.push(Change::Text(util::truncate_string(content, 0, width)));
        }
    }
}


//! A screen displaying a single file.
//!
//! Some terms are used for specific meanings within this file:
//!
//! * `line` means a line in the file.
//! * `row` means a row on the screen.
//! * `height` means a height in rows.
//! * `portion` means the portion within a line shown on a single row
//!    when a line has been wrapped onto multiple rows.
//!
//! An example of how these might map to the screen is shown below:
//!
//! ```text
//! File                Screen
//! ====                ======
//! LINE 0 PORTION 0 \  +------------------+__v top_line = 0, top_line_portion = 1
//! LINE 0 PORTION 1 \  | LINE 0 PORTION 1 |  ^
//! LINE 0 PORTION 2    | LINE 0 PORTION 2 |  |
//! LINE 1              | LINE 1           |  |
//! LINE 2 PORTION 0 \  | LINE 2 PORTION 0 |  | height = 8
//! LINE 2 PORTION 1    | LINE 2 PORTION 1 |  |
//! LINE 3              | LINE 3           |  |
//! LINE 4 PORTION 0 \  | LINE 4 PORTION 0 |__|___v bottom_line = 4
//! LINE 4 PORTION 1    |<= RULER ========>|__v___  overlay_height = 1
//!                     +------------------+      ^
//!
//! ```

use std::cmp::{max, min};
use std::num::NonZeroUsize;
use std::sync::Arc;

use termwiz::cell::{CellAttributes, Intensity};
use termwiz::color::{AnsiColor, ColorAttribute};
use termwiz::input::KeyEvent;
use termwiz::surface::change::Change;
use termwiz::surface::{CursorVisibility, Position};

use crate::action::Action;
use crate::bindings::{Binding, Keymap};
use crate::command;
use crate::config::{Config, WrappingMode};
use crate::display::Capabilities;
use crate::display::DisplayAction;
use crate::error::Error;
use crate::event::EventSender;
use crate::file::{File, FileInfo};
use crate::line::Line;
use crate::line_cache::LineCache;
use crate::progress::Progress;
use crate::prompt::Prompt;
use crate::prompt_history;
use crate::refresh::Refresh;
use crate::ruler::Ruler;
use crate::search::{MatchMotion, Search, SearchKind};
use crate::util::number_width;

const LINE_CACHE_SIZE: usize = 1000;

/// The state of the previous render.
#[derive(Clone, Debug, Default)]
struct RenderState {
    /// The number of columns on screen.
    width: usize,

    /// The number of rows on screen.
    height: usize,

    /// The file line at the top of the screen.
    top_line: usize,

    /// The porition of the file line at the top of the screen.
    top_line_portion: usize,

    /// The file line at the bottom of the screen.
    bottom_line: usize,

    /// The column at the left of the screen.
    left: usize,

    /// The height of the overlay.
    overlay_height: usize,

    /// The number of lines in the file.
    file_lines: usize,

    /// The number of searched lines.
    searched_lines: usize,

    /// The number of lines in the error file.
    error_file_lines: usize,

    /// The last line portion of the error file.  This may be incomplete and needs to be
    /// re-rendered every time.
    error_file_last_line_portion: Option<(usize, usize)>,

    /// The number of rows in the progress indicator.
    progress_height: usize,

    /// The number of rows showing the error file.
    error_file_height: usize,

    /// The row the ruler was rendered to.
    ruler_row: Option<usize>,

    /// The row the prompt was rendered to.
    prompt_row: Option<usize>,

    /// The row the error message was rendered to.
    error_row: Option<usize>,

    /// The row search status was rendered to.
    search_row: Option<usize>,

    /// The start and end row of each file line in view.
    file_line_rows: Vec<(usize, usize)>,
}

impl RenderState {
    /// Returns the start and end row of the file line on the screen, if the
    /// file line is currently visible.
    fn file_line_rows(&self, file_line_index: usize) -> Option<(usize, usize)> {
        if file_line_index >= self.top_line && file_line_index < self.bottom_line {
            self.file_line_rows
                .get(file_line_index - self.top_line)
                .cloned()
        } else {
            None
        }
    }
}

/// A screen that is displaying a single file.
pub(crate) struct Screen {
    /// The file being displayed.
    pub(crate) file: File,

    /// An error file potentially being overlayed.
    error_file: Option<File>,

    /// The progress indicator potentially being overlayed.
    progress: Option<Progress>,

    /// The keymap in use.
    keymap: Arc<Keymap>,

    /// The current width.
    width: usize,

    /// The current height.
    height: usize,

    /// The current left-most column when not wrapping
    left: usize,

    /// The current top-most line
    top_line: usize,

    /// The top-most portion of the top-most line
    top_line_portion: usize,

    /// Wrapping mode.
    wrapping_mode: WrappingMode,

    /// The state of the previous render.
    rendered: RenderState,

    /// Whether line numbers are being displayed.
    line_numbers: bool,

    /// Cache of `Line`s to display.
    line_cache: LineCache,

    /// Cache of `Line`s for the current search.
    search_line_cache: LineCache,

    /// The current error that should be displayed to the user.
    pub(crate) error: Option<String>,

    /// The current prompt that the user is entering a response into.
    prompt: Option<Prompt>,

    /// The current ongoing search.
    search: Option<Search>,

    /// The ruler.
    ruler: Ruler,

    /// Whether the ruler should be shown.
    show_ruler: bool,

    /// Whether we are following the end of the file.  If `true`, we will scroll down to the
    /// end as new input arrives.
    following_end: bool,

    /// Scroll to a particular line in the file.
    pending_absolute_scroll: Option<usize>,

    /// Scroll relative number of rows.
    pending_relative_scroll: isize,

    /// Which parts of the screens need to be re-rendered.
    pending_refresh: Refresh,

    /// Configuration set by the top-level `Pager`.
    config: Arc<Config>,

    /// Repeat the next operation for the given times.
    repeat_count: Option<usize>,
}

impl Screen {
    /// Create a screen that displays a file.
    pub(crate) fn new(file: File, config: Arc<Config>) -> Result<Screen, Error> {
        Ok(Screen {
            error_file: None,
            progress: None,
            keymap: config.keymap.load()?,
            width: 0,
            height: 0,
            left: 0,
            top_line: 0,
            top_line_portion: 0,
            wrapping_mode: config.wrapping_mode,
            rendered: RenderState::default(),
            line_numbers: false,
            line_cache: LineCache::new(NonZeroUsize::new(LINE_CACHE_SIZE).unwrap()),
            search_line_cache: LineCache::new(NonZeroUsize::new(LINE_CACHE_SIZE).unwrap()),
            error: None,
            prompt: None,
            search: None,
            ruler: Ruler::new(file.clone()),
            show_ruler: config.show_ruler,
            following_end: false,
            pending_absolute_scroll: None,
            pending_relative_scroll: 0,
            pending_refresh: Refresh::None,
            config,
            file,
            repeat_count: None,
        })
    }

    /// Resize the screen
    pub(crate) fn resize(&mut self, width: usize, height: usize) {
        if self.width != width || self.height != height {
            self.width = width;
            self.height = height;
            self.pending_refresh = Refresh::All;
        }
    }

    /// Get the screen width
    pub(crate) fn width(&self) -> usize {
        self.width
    }

    /// Get the current overlay height
    pub(crate) fn overlay_height(&self) -> usize {
        self.rendered.overlay_height
    }

    /// Get the screen's keymap
    pub(crate) fn keymap(&self) -> &Keymap {
        &self.keymap
    }

    /// Renders the part of the screen that has changed.
    pub(crate) fn render(&mut self, caps: &Capabilities) -> Vec<Change> {
        let mut changes = vec![
            // Hide the cursor while we render things.
            Change::CursorVisibility(CursorVisibility::Hidden),
        ];

        // Set up the render state.
        let mut render = RenderState {
            width: self.width,
            height: self.height,
            file_lines: self.file.lines(),
            error_file_lines: self.error_file.as_ref().map(|f| f.lines()).unwrap_or(0),
            ..Default::default()
        };
        if let Some(search) = self.search.as_ref() {
            render.searched_lines = search.searched_lines();
        }
        let mut pending_refresh = self.pending_refresh.clone();
        let file_loaded = self.file.loaded();
        let file_width = if self.line_numbers {
            render.width - number_width(render.file_lines) - 2
        } else {
            render.width
        };

        #[derive(Copy, Clone, Debug)]
        enum RowContent {
            Empty,
            FileLinePortions {
                line: usize,
                first_portion: usize,
                rows: usize,
            },
            Blank,
            Error,
            Prompt,
            Search,
            Ruler,
            ErrorFileLinePortion(usize, usize),
            ProgressLine(usize),
        }

        let mut row_contents = vec![RowContent::Empty; render.height];

        // Assign the lines of the error file to rows (in reverse order).
        let error_file_line_portions: Vec<_> = (0..render.error_file_lines)
            .rev()
            .flat_map(|line_index| {
                let line = self
                    .error_file
                    .as_ref()
                    .and_then(|f| f.with_line(line_index, |line| Line::new(line_index, line)));
                if let Some(line) = line {
                    let height = line.height(render.width, WrappingMode::WordBoundary);
                    (0..height)
                        .rev()
                        .map(|portion| (line_index, portion))
                        .collect()
                } else {
                    Vec::new()
                }
            })
            .take(8)
            .collect();

        // Compute where the overlay will go
        let ruler_height = self.show_ruler as usize;
        render.progress_height = self.progress.as_ref().map(|f| f.lines()).unwrap_or(0);
        render.error_file_height = error_file_line_portions.len();
        render.overlay_height = render.progress_height
            + render.error_file_height
            + ruler_height
            + self.search.is_some() as usize
            + self.prompt.is_some() as usize
            + self.error.is_some() as usize;

        if render.overlay_height < render.height {
            let mut row = render.height - render.progress_height;
            for progress_line in 0..render.progress_height {
                row_contents[row + progress_line] = RowContent::ProgressLine(progress_line);
            }
            row -= render.error_file_height;
            render.error_file_last_line_portion = error_file_line_portions.get(0).cloned();
            for (error_file_row, error_file_line_portion) in
                error_file_line_portions.into_iter().rev().enumerate()
            {
                row_contents[row + error_file_row] = RowContent::ErrorFileLinePortion(
                    error_file_line_portion.0,
                    error_file_line_portion.1,
                );
            }
            if self.show_ruler {
                row -= 1;
                row_contents[row] = RowContent::Ruler;
                render.ruler_row = Some(row);
            }
            if self.search.is_some() {
                row -= 1;
                row_contents[row] = RowContent::Search;
                render.search_row = Some(row);
            }
            if self.prompt.is_some() {
                row -= 1;
                row_contents[row] = RowContent::Prompt;
                render.prompt_row = Some(row);
            }
            if self.error.is_some() {
                row -= 1;
                row_contents[row] = RowContent::Error;
                render.error_row = Some(row);
            }
        } else {
            // The overlay doesn't fit.  Only show the prompt (if any).
            render.overlay_height = self.prompt.is_some() as usize;
            render.progress_height = 0;
            render.error_file_height = 0;
            render.error_file_last_line_portion = None;
            if self.prompt.is_some() {
                let prompt_row = render.height.saturating_sub(1);
                row_contents[prompt_row] = RowContent::Prompt;
                render.prompt_row = Some(prompt_row);
            }
        }

        let file_view_height = render.height - render.overlay_height;

        let (end_top_line, end_top_line_portion) = {
            let mut top_line = render.file_lines;
            let mut top_line_portion = 0;
            let mut remaining = file_view_height;
            while top_line > 0 && remaining > 0 {
                top_line -= 1;
                if let Some(line) = self.line_cache.get_or_create(&self.file, top_line, None) {
                    let line_height = line.height(file_width, self.wrapping_mode);
                    if line_height > remaining {
                        top_line_portion = line_height - remaining;
                        break;
                    }
                    remaining -= line_height;
                }
            }
            (top_line, top_line_portion)
        };

        // Scroll to end
        if self.following_end {
            // See if this is a small relative downwards scroll
            let mut relative_scroll = None;
            if (end_top_line, end_top_line_portion) >= (self.top_line, self.top_line_portion) {
                let mut scroll_by = 0;
                let mut scroll_line = self.top_line;
                let mut scroll_line_portion = self.top_line_portion;
                while scroll_line < end_top_line {
                    if let Some(line) = self.line_cache.get_or_create(&self.file, scroll_line, None)
                    {
                        let line_height = line.height(file_width, self.wrapping_mode);
                        scroll_by += line_height.saturating_sub(scroll_line_portion);
                        if scroll_by > file_view_height {
                            // We've scrolled an entire screen, just jump straight to the end.
                            break;
                        }
                    }
                    scroll_line += 1;
                    scroll_line_portion = 0;
                }
                if scroll_line == end_top_line {
                    scroll_by += end_top_line_portion.saturating_sub(scroll_line_portion);
                    relative_scroll = Some(scroll_by);
                }
            }
            if let Some(relative_scroll) = relative_scroll {
                self.pending_relative_scroll = relative_scroll as isize;
            } else {
                self.top_line = end_top_line;
                self.top_line_portion = end_top_line_portion;
                pending_refresh.add_range(0, file_view_height);
            }
        }

        // Perform pending absolute scroll
        if let Some(line) = self.pending_absolute_scroll.take() {
            self.top_line = line;
            self.top_line_portion = 0;
            pending_refresh.add_range(0, file_view_height);
            // Scroll up so that the target line is in the center of the
            // file view.
            self.pending_relative_scroll -= (file_view_height / 2) as isize;
        }

        enum Direction {
            None,
            Up,
            Down,
        }

        // Perform pending relative scroll
        let mut scroll_direction = Direction::None;
        let mut scroll_distance = 0;
        if self.pending_relative_scroll < 0 {
            scroll_direction = Direction::Up;
            let mut scroll_up = (-self.pending_relative_scroll) as usize;
            let mut top_line = self.top_line;
            let mut top_line_portion = self.top_line_portion;
            if top_line_portion > 0 {
                let top_line_remaining = min(top_line_portion, scroll_up);
                top_line_portion -= top_line_remaining;
                scroll_up -= top_line_remaining;
                scroll_distance += top_line_remaining;
            }
            while scroll_up > 0 && top_line > 0 {
                top_line -= 1;
                top_line_portion = 0;
                if let Some(line) = self.line_cache.get_or_create(&self.file, top_line, None) {
                    let line_height = line.height(file_width, self.wrapping_mode);
                    if line_height > scroll_up {
                        scroll_distance += scroll_up;
                        top_line_portion = line_height - scroll_up;
                        break;
                    }
                    scroll_distance += line_height;
                    scroll_up -= line_height;
                }
            }
            self.top_line = top_line;
            self.top_line_portion = top_line_portion;
        } else if self.pending_relative_scroll > 0 {
            scroll_direction = Direction::Down;
            let mut scroll_down = self.pending_relative_scroll as usize;
            let mut top_line = self.top_line;
            let mut top_line_portion = self.top_line_portion;
            let (max_top_line, max_top_line_portion) = if self.config.scroll_past_eof {
                let last_line = render.file_lines.saturating_sub(1);
                let line_height = if let Some(line) =
                    self.line_cache.get_or_create(&self.file, last_line, None)
                {
                    line.height(file_width, self.wrapping_mode)
                } else {
                    1
                };
                (last_line, line_height.saturating_sub(1))
            } else {
                (end_top_line, end_top_line_portion)
            };
            while scroll_down > 0
                && (top_line, top_line_portion) < (max_top_line, max_top_line_portion)
            {
                if let Some(line) = self.line_cache.get_or_create(&self.file, top_line, None) {
                    let line_height = line.height(file_width, self.wrapping_mode);
                    let line_height_remaining = line_height.saturating_sub(top_line_portion);
                    if line_height_remaining > scroll_down {
                        scroll_distance += scroll_down;
                        top_line_portion += scroll_down;
                        break;
                    }
                    scroll_distance += line_height_remaining;
                    scroll_down -= line_height_remaining;
                }
                top_line += 1;
                top_line_portion = 0;
            }
            self.top_line = top_line;
            self.top_line_portion = top_line_portion;
        }
        render.top_line = self.top_line;
        render.top_line_portion = self.top_line_portion;
        render.left = self.left;
        self.pending_relative_scroll = 0;

        // Scroll the region of the screen that had and still has file lines
        if pending_refresh != Refresh::All {
            let scroll_start = 0;
            let scroll_end = min(
                file_view_height,
                self.rendered.height - self.rendered.overlay_height,
            );
            match scroll_direction {
                Direction::None => {}
                _ if scroll_distance > scroll_end - scroll_start => {
                    pending_refresh.add_range(scroll_start, scroll_end);
                }
                Direction::Up if caps.scroll_up => {
                    changes.push(Change::ScrollRegionDown {
                        first_row: scroll_start,
                        region_size: scroll_end - scroll_start,
                        scroll_count: scroll_distance,
                    });
                    pending_refresh.rotate_range_down(
                        scroll_start,
                        scroll_end,
                        scroll_distance,
                        true,
                    );
                }
                Direction::Down if caps.scroll_down => {
                    changes.push(Change::ScrollRegionUp {
                        first_row: scroll_start,
                        region_size: scroll_end - scroll_start,
                        scroll_count: scroll_distance,
                    });
                    pending_refresh.rotate_range_up(
                        scroll_start,
                        scroll_end,
                        scroll_distance,
                        true,
                    );
                }
                _ if scroll_distance > 0 => {
                    pending_refresh.add_range(scroll_start, scroll_end);
                }
                _ => {}
            }
            if file_view_height > scroll_end {
                pending_refresh.add_range(scroll_end, file_view_height);
            }
        }

        // Assign lines to the rows on screen
        {
            let mut file_line_rows = Vec::new();
            let mut row = 0;
            let mut top_portion = render.top_line_portion;
            for file_line in render.top_line..render.file_lines {
                if let Some(line) = self.line_cache.get_or_create(&self.file, file_line, None) {
                    let line_height = line.height(file_width, self.wrapping_mode);
                    let visible_line_height = min(
                        line_height.saturating_sub(top_portion),
                        file_view_height - row,
                    );
                    for offset in 0..visible_line_height {
                        row_contents[row + offset] = RowContent::FileLinePortions {
                            line: file_line,
                            first_portion: top_portion + offset,
                            rows: 1,
                        };
                    }
                    file_line_rows.push((row, row + visible_line_height));
                    row += visible_line_height;
                } else {
                    file_line_rows.push((row, row));
                }
                top_portion = 0;
                if row >= file_view_height {
                    break;
                }
            }
            render.bottom_line = render.top_line + file_line_rows.len();
            render.file_line_rows = file_line_rows;
            for blank_row in row_contents.iter_mut().take(file_view_height).skip(row) {
                *blank_row = RowContent::Blank;
            }
        }

        // Update the ruler with the new position.
        self.ruler.set_position(
            render.top_line,
            render.left,
            if !self.following_end {
                Some(render.bottom_line)
            } else {
                None
            },
            self.wrapping_mode,
        );

        // Work out what else needs to be refreshed
        if pending_refresh != Refresh::All {
            // What needs to be refreshed because more of the file was loaded?
            if !file_loaded {
                let last_line = self.rendered.file_lines.saturating_sub(1);
                if let Some((start, end)) = render.file_line_rows(last_line) {
                    pending_refresh.add_range(start, end);
                }
            }
            if render.file_lines > self.rendered.file_lines {
                let start_line = max(self.rendered.file_lines, render.top_line);
                let end_line = min(render.file_lines, render.bottom_line);
                for file_line in start_line..end_line {
                    if let Some((start, end)) = render.file_line_rows(file_line) {
                        pending_refresh.add_range(start, end);
                    }
                }
            }

            // What needs to be refreshed because search has progressed?
            if let Some(search) = self.search.as_ref() {
                if render.searched_lines > self.rendered.searched_lines {
                    let start_line = max(render.top_line, self.rendered.searched_lines);
                    let end_line = min(render.bottom_line, render.searched_lines);
                    for line in search.matching_lines(start_line, end_line).into_iter() {
                        if let Some((start_row, end_row)) = render.file_line_rows(line) {
                            pending_refresh.add_range(start_row, end_row);
                        }
                    }
                }
            }

            // What needs to be refreshed because the overlay got smaller?
            if file_view_height > self.rendered.height - self.rendered.overlay_height {
                pending_refresh.add_range(
                    self.rendered.height - self.rendered.overlay_height,
                    file_view_height,
                );
            }

            // Which parts of the error file need to be refreshed because they moved?
            let bottom_row = render.height - render.progress_height;
            if !file_loaded && self.rendered.error_file_lines > 0 {
                pending_refresh.add_range(bottom_row - 1, bottom_row);
            }
            if self.rendered.error_file_lines != render.error_file_lines
                || self.rendered.progress_height != render.progress_height
                || self.rendered.error_file_last_line_portion != render.error_file_last_line_portion
            {
                pending_refresh.add_range(bottom_row - render.error_file_height, bottom_row);
            }

            // Did the ruler move or does it need updating?
            if let Some(ruler_row) = render.ruler_row {
                if self.rendered.ruler_row != Some(ruler_row)
                    || render.top_line != self.rendered.top_line
                    || render.bottom_line != self.rendered.bottom_line
                    || render.left != self.rendered.left
                {
                    pending_refresh.add_range(ruler_row, ruler_row + 1);
                }
            }

            // Did the prompt move?
            if let Some(prompt_row) = render.prompt_row {
                if self.rendered.prompt_row != Some(prompt_row) {
                    pending_refresh.add_range(prompt_row, prompt_row + 1);
                }
            }

            // Did the error message move?
            if let Some(error_row) = render.error_row {
                if self.rendered.error_row != Some(error_row) {
                    pending_refresh.add_range(error_row, error_row + 1);
                }
            }
        }

        if self.wrapping_mode == WrappingMode::GraphemeBoundary && !self.line_numbers {
            // In wrapped mode with line numbers off, render full lines at once
            // so that the terminal can handle wrapped lines properly.
            let mut first_row: Option<(usize, &mut RowContent)> = None;
            for (row, row_content) in row_contents.iter_mut().enumerate() {
                match row_content {
                    RowContent::FileLinePortions {
                        line: this_line,
                        first_portion: this_portion,
                        rows: _,
                    } => {
                        match first_row {
                            Some((
                                first_row,
                                &mut RowContent::FileLinePortions {
                                    line,
                                    first_portion,
                                    ref mut rows,
                                },
                            )) if *this_line == line && *this_portion == first_portion + *rows => {
                                *rows += 1;
                                *row_content = RowContent::Empty;
                                if pending_refresh.contains(row) {
                                    pending_refresh.add_range(first_row, first_row + 1);
                                }
                                continue;
                            }
                            _ => {}
                        }
                        first_row = Some((row, row_content));
                    }
                    _ => {
                        first_row = None;
                    }
                }
            }
        }

        // Render pending rows
        for (row, row_content) in row_contents.into_iter().enumerate() {
            if pending_refresh.contains(row) {
                match row_content {
                    RowContent::Empty => {}
                    RowContent::FileLinePortions {
                        line,
                        first_portion,
                        rows,
                    } => {
                        self.render_file_line(
                            &mut changes,
                            row,
                            line,
                            first_portion,
                            rows,
                            render.left,
                            render.width,
                        );
                    }
                    RowContent::Blank => {
                        self.render_blank_line(&mut changes, row);
                    }
                    RowContent::Error => {
                        self.render_error(&mut changes, row, render.width);
                    }
                    RowContent::Prompt => {
                        self.prompt
                            .as_mut()
                            .expect("prompt should be visible")
                            .render(&mut changes, row, render.width);
                    }
                    RowContent::Search => {
                        if let Some(search) = self.search.as_mut() {
                            search.render(&mut changes, row, render.width);
                        }
                    }
                    RowContent::Ruler => {
                        self.ruler.bar().render(&mut changes, row, render.width);
                    }
                    RowContent::ErrorFileLinePortion(line, portion) => {
                        self.render_error_file_line(&mut changes, row, line, portion, render.width);
                    }
                    RowContent::ProgressLine(line) => {
                        self.render_progress_line(&mut changes, row, line, render.width);
                    }
                }
            }
        }

        // Set the cursor to the right position and shape.
        if let Some(prompt) = self.prompt.as_ref() {
            changes.push(Change::CursorPosition {
                x: Position::Absolute(prompt.cursor_position()),
                y: Position::Absolute(
                    render
                        .prompt_row
                        .expect("prompt row should have been calculated"),
                ),
            });
            changes.push(Change::CursorVisibility(CursorVisibility::Visible));
        } else {
            changes.push(Change::CursorPosition {
                x: Position::Absolute(0),
                y: Position::Relative(0),
            });
            if self.config.show_cursor {
                changes.push(Change::CursorVisibility(CursorVisibility::Visible));
            }
        }

        // Restore attributes to default.
        changes.push(Change::AllAttributes(CellAttributes::default()));

        // Record what we've rendered.
        self.rendered = render;
        self.pending_refresh = Refresh::None;

        changes
    }

    /// Renders a line of the file on the screen.
    fn render_file_line(
        &mut self,
        changes: &mut Vec<Change>,
        row: usize,
        line_index: usize,
        first_portion: usize,
        rows: usize,
        left: usize,
        width: usize,
    ) {
        let line = match self.search {
            Some(ref search) if search.line_matches(line_index) => self
                .search_line_cache
                .get_or_create(&self.file, line_index, Some(search.regex())),
            _ => self.line_cache.get_or_create(&self.file, line_index, None),
        };

        let match_index = self
            .search
            .as_ref()
            .and_then(|ref search| search.current_match())
            .and_then(|(match_line_index, match_index)| {
                if match_line_index == line_index {
                    Some(match_index)
                } else {
                    None
                }
            });

        if let Some(line) = line {
            changes.push(Change::CursorPosition {
                x: Position::Absolute(0),
                y: Position::Absolute(row),
            });
            changes.push(Change::AllAttributes(CellAttributes::default()));

            let start = left;
            let mut end = left.saturating_add(width);
            if self.line_numbers {
                let lw = number_width(self.file.lines());
                if lw + 2 < width {
                    changes.push(Change::AllAttributes(
                        CellAttributes::default()
                            .set_foreground(AnsiColor::Black)
                            .set_background(AnsiColor::Silver)
                            .clone(),
                    ));
                    if first_portion == 0 {
                        changes.push(Change::Text(format!(" {:>1$} ", line_index + 1, lw)));
                    } else {
                        changes.push(Change::Text(" ".repeat(lw + 2)));
                    };
                    changes.push(Change::AllAttributes(CellAttributes::default()));
                    end -= lw + 2;
                }
            }
            if self.wrapping_mode == WrappingMode::Unwrapped {
                line.render(changes, start, end, match_index);
            } else {
                line.render_wrapped(
                    changes,
                    first_portion,
                    rows,
                    end - start,
                    self.wrapping_mode,
                    match_index,
                );
            }
        } else {
            self.render_blank_line(changes, row);
        }
    }

    fn render_blank_line(&self, changes: &mut Vec<Change>, row: usize) {
        changes.push(Change::CursorPosition {
            x: Position::Absolute(0),
            y: Position::Absolute(row),
        });
        changes.push(Change::AllAttributes(CellAttributes::default()));
        changes.push(Change::AllAttributes(
            CellAttributes::default()
                .set_foreground(AnsiColor::Navy)
                .set_intensity(Intensity::Bold)
                .clone(),
        ));
        changes.push(Change::Text("~".into()));
        changes.push(Change::ClearToEndOfLine(ColorAttribute::default()));
    }

    fn render_error_file_line(
        &mut self,
        changes: &mut Vec<Change>,
        row: usize,
        line_index: usize,
        portion: usize,
        width: usize,
    ) {
        if let Some(error_file) = self.error_file.as_ref() {
            changes.push(Change::CursorPosition {
                x: Position::Absolute(0),
                y: Position::Absolute(row),
            });
            changes.push(Change::AllAttributes(CellAttributes::default()));
            if let Some(line) = error_file.with_line(line_index, |line| Line::new(line_index, line))
            {
                line.render_wrapped(changes, portion, 1, width, WrappingMode::WordBoundary, None);
            } else {
                changes.push(Change::ClearToEndOfLine(ColorAttribute::default()));
            }
        }
    }

    fn render_progress_line(
        &mut self,
        changes: &mut Vec<Change>,
        row: usize,
        line_index: usize,
        width: usize,
    ) {
        if let Some(progress) = self.progress.as_ref() {
            changes.push(Change::CursorPosition {
                x: Position::Absolute(0),
                y: Position::Absolute(row),
            });
            changes.push(Change::AllAttributes(CellAttributes::default()));
            if let Some(line) = progress.with_line(line_index, |line| Line::new(line_index, line)) {
                line.render(changes, 0, width, None);
            } else {
                changes.push(Change::ClearToEndOfLine(ColorAttribute::default()));
            }
        }
    }

    /// Renders the error message at the bottom of the screen.
    fn render_error(&mut self, changes: &mut Vec<Change>, row: usize, _width: usize) {
        if let Some(error) = self.error.as_ref() {
            changes.push(Change::CursorPosition {
                x: Position::Absolute(0),
                y: Position::Absolute(row),
            });
            changes.push(Change::AllAttributes(
                CellAttributes::default()
                    .set_foreground(AnsiColor::Black)
                    .set_background(AnsiColor::Maroon)
                    .clone(),
            ));
            // TODO: truncate at width
            changes.push(Change::Text(format!("  {}  ", error)));
            changes.push(Change::AllAttributes(CellAttributes::default()));
            changes.push(Change::ClearToEndOfLine(ColorAttribute::default()));
        }
    }

    /// Refreshes the ruler on the next render.
    pub(crate) fn refresh_ruler(&mut self) {
        if let Some(ruler_row) = self.rendered.ruler_row {
            self.pending_refresh.add_range(ruler_row, ruler_row + 1);
        }
    }

    /// Refreshes the search bar on the next render.
    pub(crate) fn refresh_search_status(&mut self) {
        if let Some(search_row) = self.rendered.search_row {
            self.pending_refresh.add_range(search_row, search_row + 1);
        }
    }

    /// Refreshes the prompt on the next render.
    pub(crate) fn refresh_prompt(&mut self) {
        if let Some(prompt_row) = self.rendered.prompt_row {
            self.pending_refresh.add_range(prompt_row, prompt_row + 1);
        }
    }

    /// Refreshes the overlay on the next render.
    pub(crate) fn refresh_overlay(&mut self) {
        let start = self
            .rendered
            .height
            .saturating_sub(self.rendered.overlay_height);
        let end = self.rendered.height;
        self.pending_refresh.add_range(start, end);
    }

    /// Refreshes the progress section on the next render.
    pub(crate) fn refresh_progress(&mut self) {
        let start = self
            .rendered
            .height
            .saturating_sub(self.rendered.progress_height);
        let end = self.height;
        self.pending_refresh.add_range(start, end);
    }

    /// Refresh a file line.
    pub(crate) fn refresh_file_line(&mut self, file_line_index: usize) {
        if let Some((start_row, end_row)) = self.rendered.file_line_rows(file_line_index) {
            self.pending_refresh.add_range(start_row, end_row);
        }
    }

    /// Refresh the line with the current match (if any).
    pub(crate) fn refresh_matched_line(&mut self) {
        if let Some(ref search) = self.search {
            if let Some((line_index, _match_index)) = search.current_match() {
                self.refresh_file_line(line_index);
            }
        }
    }

    /// Refresh all lines with any matches.
    pub(crate) fn refresh_matched_lines(&mut self) {
        if let Some(ref search) = self.search {
            for line in search
                .matching_lines(self.rendered.top_line, self.rendered.bottom_line)
                .into_iter()
            {
                self.refresh_file_line(line);
            }
        }
    }

    /// Triggers a full refresh on the next render.
    pub(crate) fn refresh(&mut self) {
        self.pending_refresh = Refresh::All;
    }

    /// Scrolls to the given line number.
    pub(crate) fn scroll_to(&mut self, line: usize) {
        self.pending_absolute_scroll = Some(line);
        self.pending_relative_scroll = 0;
        self.following_end = false;
    }

    /// Scroll the screen `step` characters up.
    fn scroll_up(&mut self, step: usize) {
        self.pending_relative_scroll -= step as isize;
        self.following_end = false;
    }

    /// Scroll the screen `step` characters down.
    fn scroll_down(&mut self, step: usize) {
        self.pending_relative_scroll += step as isize;
        self.following_end = false;
    }

    /// Scroll the screen `step` characters to the left.
    fn scroll_left(&mut self, step: usize) {
        if self.wrapping_mode == WrappingMode::Unwrapped && self.left > 0 && step > 0 {
            self.left = self.left.saturating_sub(step);
            self.refresh();
        }
    }

    /// Scroll the screen `step` characters to the right.
    fn scroll_right(&mut self, step: usize) {
        if self.wrapping_mode == WrappingMode::Unwrapped && step != 0 {
            self.left = self.left.saturating_add(step);
            self.refresh();
        }
    }

    /// Scroll up (screen / n) * repeat lines.
    fn scroll_up_screen_fraction(&mut self, n: usize, repeat: usize) {
        if n != 0 {
            let lines = (self.rendered.height - self.rendered.overlay_height) / n;
            self.scroll_up(lines.saturating_mul(repeat));
        }
    }

    /// Scroll down (screen / n) * repeat lines.
    fn scroll_down_screen_fraction(&mut self, n: usize, repeat: usize) {
        if n != 0 {
            let lines = (self.rendered.height - self.rendered.overlay_height) / n;
            self.scroll_down(lines.saturating_mul(repeat));
        }
    }

    /// Scroll left (screen / n) * repeat columns.
    fn scroll_left_screen_fraction(&mut self, n: usize, repeat: usize) {
        if n != 0 {
            let columns = self.rendered.width / n;
            self.scroll_left(columns.saturating_mul(repeat));
        }
    }

    /// Scroll right (screen / n) * repeat columns.
    fn scroll_right_screen_fraction(&mut self, n: usize, repeat: usize) {
        if n != 0 {
            let columns = self.rendered.width / n;
            self.scroll_right(columns.saturating_mul(repeat));
        }
    }

    /// Dispatch an action to navigate the displayed file.
    pub(crate) fn dispatch_action(
        &mut self,
        action: Action,
        event_sender: &EventSender,
    ) -> DisplayAction {
        use Action::*;
        match action {
            Quit => return DisplayAction::Quit,
            Refresh => return DisplayAction::Refresh,
            Help => return DisplayAction::ShowHelp,
            Cancel => {
                if self.repeat_count.is_some() {
                    self.clear_repeat_count();
                } else {
                    self.error_file = None;
                    self.set_search(None);
                    self.error = None;
                    self.refresh();
                    return DisplayAction::ClearOverlay;
                }
            }
            PreviousFile => return DisplayAction::PreviousFile,
            NextFile => return DisplayAction::NextFile,
            ToggleRuler => {
                self.show_ruler = !self.show_ruler;
            }
            ScrollUpLines(n) => {
                let n = self.apply_repeat_count(n);
                self.scroll_up(n)
            }
            ScrollDownLines(n) => {
                let n = self.apply_repeat_count(n);
                self.scroll_down(n)
            }
            ScrollUpScreenFraction(n) => {
                let repeat = self.apply_repeat_count(1);
                self.scroll_up_screen_fraction(n, repeat)
            }
            ScrollDownScreenFraction(n) => {
                let repeat = self.apply_repeat_count(1);
                self.scroll_down_screen_fraction(n, repeat)
            }
            ScrollToTop | ScrollToBottom if self.repeat_count.is_some() => {
                if let Some(n) = self.repeat_count {
                    // Convert 1-based to 0-based line number.
                    self.scroll_to(n.max(1) - 1);
                }
            }
            ScrollToTop => self.scroll_to(0),
            ScrollToBottom => self.following_end = true,
            ScrollLeftColumns(n) => {
                let n = self.apply_repeat_count(n);
                self.scroll_left(n)
            }
            ScrollRightColumns(n) => {
                let n = self.apply_repeat_count(n);
                self.scroll_right(n)
            }
            ScrollLeftScreenFraction(n) => {
                let repeat = self.apply_repeat_count(1);
                self.scroll_left_screen_fraction(n, repeat)
            }
            ScrollRightScreenFraction(n) => {
                let repeat = self.apply_repeat_count(1);
                self.scroll_right_screen_fraction(n, repeat)
            }
            ToggleLineNumbers => {
                self.line_numbers = !self.line_numbers;
                return DisplayAction::Refresh;
            }
            ToggleLineWrapping => {
                self.wrapping_mode = self.wrapping_mode.next_mode();
                return DisplayAction::Refresh;
            }
            PromptGoToLine => self.prompt = Some(command::goto()),
            PromptSearchFromStart => {
                self.prompt = Some(command::search(SearchKind::First, event_sender.clone()))
            }
            PromptSearchForwards => {
                self.prompt = Some(command::search(
                    SearchKind::FirstAfter(self.rendered.top_line),
                    event_sender.clone(),
                ))
            }
            PromptSearchBackwards => {
                self.prompt = Some(command::search(
                    SearchKind::FirstBefore(self.rendered.bottom_line),
                    event_sender.clone(),
                ))
            }
            PreviousMatch => self.create_or_move_match(MatchMotion::Previous, event_sender.clone()),
            NextMatch => self.create_or_move_match(MatchMotion::Next, event_sender.clone()),
            PreviousMatchLine => {
                self.create_or_move_match(MatchMotion::PreviousLine, event_sender.clone())
            }
            NextMatchLine => self.create_or_move_match(MatchMotion::NextLine, event_sender.clone()),
            PreviousMatchScreen => {
                self.create_or_move_match(MatchMotion::PreviousScreen, event_sender.clone())
            }
            NextMatchScreen => {
                self.create_or_move_match(MatchMotion::NextScreen, event_sender.clone())
            }
            FirstMatch => self.create_or_move_match(MatchMotion::First, event_sender.clone()),
            LastMatch => self.create_or_move_match(MatchMotion::Last, event_sender.clone()),
            AppendDigitToRepeatCount(n) => self.append_digit_to_repeat_count(n),
        }
        if !matches!(action, AppendDigitToRepeatCount(_)) {
            self.clear_repeat_count();
        }
        DisplayAction::Render
    }

    /// Dispatch a keypress to navigate the displayed file.
    pub(crate) fn dispatch_key(
        &mut self,
        key: KeyEvent,
        event_sender: &EventSender,
    ) -> DisplayAction {
        if let Some(binding) = self.keymap.get(key.modifiers, key.key) {
            match binding {
                Binding::Action(action) => {
                    let action = action.clone();
                    return self.dispatch_action(action, event_sender);
                }
                Binding::Custom(b) => b.run(self.file.index()),
                Binding::Unrecognized(_) => {}
            }
        }
        DisplayAction::Render
    }

    /// Append a digit to the repeat count.
    pub(crate) fn append_digit_to_repeat_count(&mut self, digit: usize) {
        assert!(digit < 10);
        let new_count = match self.repeat_count {
            None if digit > 0 => Some(digit),
            None => None,
            Some(count) => Some(count.saturating_mul(10).saturating_add(digit)),
        };
        self.ruler.set_repeat_count(new_count);
        self.refresh_ruler();
        self.repeat_count = new_count;
    }

    /// Clear the repeat count.
    pub(crate) fn clear_repeat_count(&mut self) {
        self.ruler.set_repeat_count(None);
        self.refresh_ruler();
        self.repeat_count = None;
    }

    /// Multiply `n` by the repeat count.
    pub(crate) fn apply_repeat_count(&self, n: usize) -> usize {
        self.repeat_count.unwrap_or(1).saturating_mul(n)
    }

    /// Set the search for this file.
    pub(crate) fn set_search(&mut self, search: Option<Search>) {
        self.search = search;
        self.search_line_cache.clear();
    }

    /// Set the error file for this file.
    pub(crate) fn set_error_file(&mut self, error_file: Option<File>) {
        self.error_file = error_file;
    }

    /// Set the progress indicator for this file.
    pub(crate) fn set_progress(&mut self, progress: Option<Progress>) {
        self.progress = progress;
    }

    /// Returns true if this screen is currently animating for any reason.
    pub(crate) fn animate(&self) -> bool {
        self.error_file.is_some()
            || (!self.file.loaded() && !self.file.paused())
            || self.following_end
            || self
                .search
                .as_ref()
                .map(|search| !search.finished())
                .unwrap_or(false)
    }

    /// Dispatch an animation timeout, updating for the next animation frame.
    pub(crate) fn dispatch_animation(&mut self) -> DisplayAction {
        if !self.file.loaded() {
            self.refresh_ruler();
        }
        if self
            .search
            .as_ref()
            .map(|search| !search.finished())
            .unwrap_or(false)
        {
            self.refresh_overlay();
        }
        if let Some(ref error_file) = self.error_file {
            if error_file.lines() != self.rendered.error_file_lines {
                self.refresh_overlay();
            }
        }
        match &self.pending_refresh {
            Refresh::None => DisplayAction::None,
            _ => DisplayAction::Render,
        }
    }

    pub(crate) fn prompt(&mut self) -> &mut Option<Prompt> {
        &mut self.prompt
    }

    /// Clears the prompt from the screen.
    pub(crate) fn clear_prompt(&mut self) {
        // Refresh the prompt before we remove it, so that we know which line to refresh.
        self.refresh_prompt();
        self.prompt = None;
    }

    /// Called when a search finds its first match in order to scroll to that match.
    pub(crate) fn search_first_match(&mut self) -> DisplayAction {
        let current_match = self
            .search
            .as_ref()
            .and_then(|ref search| search.current_match());
        if let Some((line_index, _match_index)) = current_match {
            self.scroll_to(line_index);
            self.refresh_matched_lines();
            self.refresh_overlay();
            return DisplayAction::Render;
        }
        DisplayAction::None
    }

    /// Called when a search completes.
    #[allow(clippy::unnecessary_wraps)]
    pub(crate) fn search_finished(&mut self) -> DisplayAction {
        self.refresh_matched_lines();
        self.refresh_overlay();
        DisplayAction::Render
    }

    /// Move the currently selected match to a new match.
    pub(crate) fn move_match(&mut self, motion: MatchMotion) {
        self.refresh_matched_line();
        if let Some(ref mut search) = self.search {
            let scope = self.rendered.top_line..=self.rendered.bottom_line;
            search.move_match(motion, scope);
            if let Some((line_index, _match_index)) = search.current_match() {
                self.scroll_to(line_index);
            }
            self.refresh_matched_line();
            self.refresh_search_status();
        }
    }

    /// Like `move_match`, but create a new search from history based on the
    /// last pattern on demand.
    pub(crate) fn create_or_move_match(&mut self, motion: MatchMotion, event_sender: EventSender) {
        if self.search.is_some() {
            self.move_match(motion)
        } else {
            // Attempt to load search from history.
            if let Some(pattern) = prompt_history::peek_last("search") {
                if !pattern.is_empty() {
                    let kind = match motion {
                        MatchMotion::First => SearchKind::First,
                        MatchMotion::Last => SearchKind::FirstBefore(self.file.lines()),
                        MatchMotion::Next | MatchMotion::NextLine | MatchMotion::NextScreen => {
                            SearchKind::FirstAfter(self.rendered.top_line)
                        }
                        MatchMotion::Previous
                        | MatchMotion::PreviousLine
                        | MatchMotion::PreviousScreen => {
                            SearchKind::FirstBefore(self.rendered.bottom_line)
                        }
                    };
                    if let Ok(search) = Search::new(&self.file, &pattern, kind, event_sender) {
                        self.search = Some(search);
                        self.move_match(motion)
                    }
                }
            }
        }
    }

    pub(crate) fn flush_line_caches(&mut self) {
        self.line_cache.clear();
        self.search_line_cache.clear();
    }

    /// Load more lines from a stream.
    pub(crate) fn maybe_load_more(&mut self) {
        // Fetch 1 screen + config.read_ahead_lines.
        let needed_lines = self.rendered.bottom_line + self.height + self.config.read_ahead_lines;
        self.file.set_needed_lines(needed_lines);
    }
}