/*
 * meli - pager
 *
 * Copyright 2020 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/>.
 */

use melib::text::{LineBreakText, Truncate};

use super::*;
use crate::{
    jobs::{IsAsync, JoinHandle},
    terminal::embedded::EmbeddedGrid,
};

/// A pager for text.
/// `Pager` holds its own content in its own `CellBuffer` and when `draw` is
/// called, it draws the current view of the text. It is responsible for
/// scrolling etc.
#[derive(Debug, Default)]
pub struct Pager {
    text: String,
    cursor: (usize, usize),
    reflow: Reflow,
    height: usize,
    width: usize,
    minimum_width: usize,
    search: Option<SearchPattern>,
    dirty: bool,

    colors: ThemeAttribute,
    initialised: bool,
    show_scrollbar: bool,
    /// At the last draw, were the visible columns plus horizontal cursor less
    /// than total width? Used to decide whether to accept `scroll_right`
    /// key events.
    cols_lt_width: bool,
    /// At the last draw, were the visible rows plus vertical cursor less than
    /// total height? Used to decide whether to accept `scroll_down` key
    /// events.
    rows_lt_height: bool,
    filtered_content: Option<(String, Result<EmbeddedGrid>)>,
    filter_job: Option<(String, JoinHandle<Result<EmbeddedGrid>>)>,
    text_lines: Vec<String>,
    line_breaker: LineBreakText,
    movement: Option<PageMovement>,
    id: ComponentId,
}

impl Clone for Pager {
    fn clone(&self) -> Self {
        Self {
            filter_job: None,
            text: self.text.clone(),
            cursor: self.cursor,
            reflow: self.reflow,
            height: self.height,
            width: self.width,
            minimum_width: self.minimum_width,
            search: self.search.clone(),
            dirty: true,
            colors: self.colors,
            initialised: false,
            show_scrollbar: self.show_scrollbar,
            cols_lt_width: self.cols_lt_width,
            rows_lt_height: self.rows_lt_height,
            filtered_content: self.filtered_content.clone(),
            text_lines: self.text_lines.clone(),
            line_breaker: self.line_breaker.clone(),
            movement: self.movement,
            id: ComponentId::default(),
        }
    }
}

impl std::fmt::Display for Pager {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "pager")
    }
}

impl Pager {
    const PAGES_AHEAD_TO_RENDER_NO: usize = 16;

    pub fn new(context: &Context) -> Self {
        let mut ret = Self {
            minimum_width: context.settings.pager.minimum_width,
            ..Self::default()
        };
        ret.set_colors(crate::conf::value(context, "theme_default"))
            .set_reflow(if context.settings.pager.split_long_lines {
                Reflow::All
            } else {
                Reflow::No
            });
        ret
    }

    pub fn set_show_scrollbar(&mut self, new_val: bool) -> &mut Self {
        self.show_scrollbar = new_val;
        self
    }

    pub fn set_colors(&mut self, new_val: ThemeAttribute) -> &mut Self {
        self.colors = new_val;
        self
    }

    pub fn set_reflow(&mut self, new_val: Reflow) -> &mut Self {
        self.reflow = new_val;
        self
    }

    pub fn set_initialised(&mut self, new_val: bool) -> &mut Self {
        self.initialised = new_val;
        self
    }

    pub fn reflow(&self) -> Reflow {
        self.reflow
    }

    pub fn update_from_str(&mut self, text: &str, mut width: Option<usize>) {
        if let Some(ref mut width) = width.as_mut() {
            if **width < self.minimum_width {
                **width = self.minimum_width;
            }
        }

        self.text = text.to_string();
        self.text_lines.clear();
        self.line_breaker = LineBreakText::new(self.text.clone(), self.reflow, width);
        self.height = 0;
        self.width = 0;
        self.search = None;
        self.set_dirty(true);
        self.initialised = false;
        self.cursor = (0, 0);
    }

    pub fn from_string(
        text: String,
        context: &Context,
        cursor_pos: Option<usize>,
        mut width: Option<usize>,
        colors: ThemeAttribute,
    ) -> Self {
        let pager_filter: Option<&String> = context.settings.pager.filter.as_ref();

        let pager_minimum_width: usize = context.settings.pager.minimum_width;

        let reflow: Reflow = if context.settings.pager.split_long_lines {
            Reflow::All
        } else {
            Reflow::No
        };

        if let Some(ref mut width) = width.as_mut() {
            if **width < pager_minimum_width {
                **width = pager_minimum_width;
            }
        }

        let mut ret = Self {
            text,
            text_lines: vec![],
            reflow,
            cursor: (0, cursor_pos.unwrap_or(0)),
            height: 1,
            width: 1,
            minimum_width: pager_minimum_width,
            initialised: false,
            dirty: true,
            id: ComponentId::default(),
            filtered_content: None,
            colors,
            ..Default::default()
        };

        if let Some(bin) = pager_filter {
            ret.filter(bin, context);
        }

        ret
    }

    pub fn filter(&mut self, cmd: &str, context: &Context) {
        async fn filter_fut(bin: String, text: String) -> Result<EmbeddedGrid> {
            use std::{
                io::Write,
                process::{Command, Stdio},
            };
            let mut filter_child = Command::new("sh")
                .args(["-c", &bin])
                .stdin(Stdio::piped())
                .stdout(Stdio::piped())
                .spawn()
                .chain_err_summary(|| "Failed to start pager filter process")?;
            let stdin = filter_child.stdin.as_mut().ok_or("failed to open stdin")?;
            stdin
                .write_all(text.as_bytes())
                .chain_err_summary(|| "Failed to write to stdin")?;
            let out = filter_child
                .wait_with_output()
                .chain_err_summary(|| "Failed to wait on filter")?
                .stdout;
            let mut dev_null = std::fs::File::open("/dev/null")?;
            let mut embedded = EmbeddedGrid::new();
            embedded.set_terminal_size((80, 20));

            for b in out {
                embedded.process_byte(&mut dev_null, b);
            }
            Ok(embedded)
        }
        let fut = Box::pin(filter_fut(cmd.to_string(), self.text.clone()));
        let handle = context.main_loop_handler.job_executor.spawn(
            format!("Running pager filter {cmd}").into(),
            fut,
            IsAsync::Blocking,
        );
        self.filter_job = Some((cmd.to_string(), handle));
    }

    pub fn cursor_pos(&self) -> usize {
        self.cursor.1
    }

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

    pub fn initialise(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        let mut width = area.width();
        if width < self.minimum_width {
            width = self.minimum_width;
        }
        if self.filtered_content.is_none() {
            if self.line_breaker.width() != Some(width.saturating_sub(4)) {
                let line_breaker = LineBreakText::new(
                    self.text.clone(),
                    self.reflow,
                    Some(width.saturating_sub(4)),
                );

                self.line_breaker = line_breaker;
                self.text_lines.clear();
            };
            self.height = self.text_lines.len();
            self.width = width;
            if let Some(ref mut search) = self.search {
                use melib::text::search::KMP;
                search.positions.clear();
                for (y, l) in self.text_lines.iter().enumerate() {
                    search.positions.extend(
                        l.kmp_search(&search.pattern)
                            .into_iter()
                            .map(|offset| (y, offset)),
                    );
                }
                if let Some(pos) = search.positions.get(search.cursor) {
                    if self.cursor.1 > pos.0 || self.cursor.1 + area.height() < pos.0 {
                        self.cursor.1 = pos.0.saturating_sub(3);
                    }
                }
            }
            self.draw_lines_up_to(
                grid,
                area,
                context,
                self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * area.height(),
            );
        }
        self.draw_page(grid, area, context);

        self.initialised = true;
    }

    pub fn draw_lines_up_to(
        &mut self,
        _grid: &mut CellBuffer,
        area: Area,
        _context: &mut Context,
        up_to: usize,
    ) {
        if self.line_breaker.is_finished() || self.filtered_content.is_some() {
            return;
        }
        let old_lines_no = self.text_lines.len();
        if up_to == 0 {
            self.text_lines.extend(self.line_breaker.by_ref());
        } else {
            if old_lines_no >= up_to + area.height() {
                return;
            }
            let new_lines_no = (up_to + area.height()) - old_lines_no;
            self.text_lines
                .extend(self.line_breaker.by_ref().take(new_lines_no));
        };
        let new_lines_no = self.text_lines.len() - old_lines_no;
        if let Some(ref mut search) = self.search {
            use melib::text::search::KMP;
            for (y, l) in self.text_lines.iter().enumerate().skip(old_lines_no) {
                search.positions.extend(
                    l.kmp_search(&search.pattern)
                        .into_iter()
                        .map(|offset| (y, offset)),
                );
            }
        }
        self.height += new_lines_no;
    }

    fn draw_page(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        if let Some((ref cmd, ref filtered_content)) = self.filtered_content {
            match filtered_content {
                Ok(ref content) => {
                    grid.copy_area(
                        content.buffer(),
                        area,
                        content
                            .area()
                            .skip_cols(self.cursor.0)
                            .skip_rows(self.cursor.1)
                            .take_cols(content.terminal_size().0.min(area.width()))
                            .take_rows(content.terminal_size().1.min(area.height())),
                    );
                    context
                        .replies
                        .push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
                            cmd.to_string(),
                        )));
                }
                Err(ref err) => {
                    let mut cmd = cmd.as_str();
                    cmd.truncate_at_boundary(4);
                    context
                        .replies
                        .push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(format!(
                            "{}: {}",
                            cmd, err
                        ))));
                }
            }
            return;
        }

        {
            let mut area2 = area;
            for l in self
                .text_lines
                .iter()
                .skip(self.cursor.1)
                .take(area2.height())
            {
                if area2.is_empty() {
                    break;
                }
                grid.write_string(
                    l,
                    self.colors.fg,
                    self.colors.bg,
                    Attr::DEFAULT,
                    area2,
                    None,
                    None,
                );
                if l.starts_with('⤷') {
                    grid[area2.upper_left()]
                        .set_fg(crate::conf::value(context, "highlight").fg)
                        .set_attrs(crate::conf::value(context, "highlight").attrs);
                }
                area2 = area2.skip_rows(1);
            }

            if area2.height() <= 1 {
                grid.clear_area(area2, crate::conf::value(context, "theme_default"));
            }
        }

        {
            {
                let area3 = area;
                for text_formatter in
                    crate::conf::text_format_regexps(context, "pager.envelope.body")
                {
                    let t = grid.insert_tag(text_formatter.tag);
                    for (i, l) in self
                        .text_lines
                        .iter()
                        .skip(self.cursor.1)
                        .enumerate()
                        .take(area3.height() + 1)
                    {
                        let i = i + area3.upper_left().1;
                        for (start, end) in text_formatter.regexp.find_iter(l) {
                            let start = start + area3.upper_left().0;
                            let end = end + area3.upper_left().0;
                            grid.set_tag(t, (start, i), (end, i));
                        }
                    }
                }
            }
            if let Some(ref mut search) = self.search {
                // Last row will be reserved for the "Results for ..." line.
                let area3 = area.skip_rows_from_end(1);
                let cursor_line = self.cursor.1;
                let results_attr = crate::conf::value(context, "pager.highlight_search");
                let results_current_attr =
                    crate::conf::value(context, "pager.highlight_search_current");
                search.cursor =
                    std::cmp::min(search.positions.len().saturating_sub(1), search.cursor);
                for (i, (y, offset)) in search
                    .positions
                    .iter()
                    .enumerate()
                    .filter(|(_, &(y, _))| y >= cursor_line && y < cursor_line + area3.height())
                {
                    let attr = if i == search.cursor {
                        results_current_attr
                    } else {
                        results_attr
                    };

                    let (y, x) = (*y, *offset);
                    let row_iter = grid.row_iter(
                        area3.nth_row(y - cursor_line),
                        x..x + search.pattern.grapheme_width(),
                        0,
                    );
                    debug_assert_eq!(row_iter.area().width(), search.pattern.grapheme_width());
                    for c in row_iter {
                        grid[c]
                            .set_fg(attr.fg)
                            .set_bg(attr.bg)
                            .set_attrs(attr.attrs);
                    }
                }
            }
        }
    }
}

impl Component for Pager {
    fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        if !self.is_dirty() {
            return;
        }

        if !self.initialised {
            self.initialise(grid, area, context);
        }

        self.dirty = false;

        if self.height == 0 || self.width == 0 {
            grid.clear_area(area, crate::conf::value(context, "theme_default"));
            return;
        }

        let (mut cols, mut rows) = (area.width(), area.height());
        let (has_more_lines, (width, height)) = if self.filtered_content.is_some() {
            (false, (self.width, self.height))
        } else {
            (
                !self.line_breaker.is_finished(),
                (self.line_breaker.width().unwrap_or(cols), self.height),
            )
        };
        if cols < 2 || rows < 2 {
            return;
        }

        if self.show_scrollbar && rows < height {
            cols -= 1;
            rows -= 1;
        } else if self.search.is_some() {
            rows -= 1;
        }

        if self.show_scrollbar && cols < width {
            rows -= 1;
        }

        if let Some(mvm) = self.movement.take() {
            match mvm {
                PageMovement::Up(amount) => {
                    self.cursor.1 = self.cursor.1.saturating_sub(amount);
                }
                PageMovement::PageUp(multiplier) => {
                    self.cursor.1 = self.cursor.1.saturating_sub(rows * multiplier);
                }
                PageMovement::Down(amount) => {
                    if self.cursor.1 + amount + 1 < self.height {
                        self.cursor.1 += amount;
                    } else {
                        self.cursor.1 = self.height.saturating_sub(1);
                    }
                    self.draw_lines_up_to(
                        grid,
                        area,
                        context,
                        self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * rows,
                    );
                }
                PageMovement::PageDown(multiplier) => {
                    if self.cursor.1 + rows * multiplier + 1 < self.height {
                        self.cursor.1 += rows * multiplier;
                    } else if self.cursor.1 + rows * multiplier > self.height {
                        self.cursor.1 = self.height.saturating_sub(1);
                    } else {
                        self.cursor.1 = (self.height / rows) * rows;
                    }
                    self.draw_lines_up_to(
                        grid,
                        area,
                        context,
                        self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * rows,
                    );
                }
                PageMovement::Right(amount) => {
                    if self.cursor.0 + amount + 1 < self.width {
                        self.cursor.0 += amount;
                    } else {
                        self.cursor.0 = self.width.saturating_sub(1);
                    }
                }
                PageMovement::Left(amount) => {
                    self.cursor.0 = self.cursor.0.saturating_sub(amount);
                }
                PageMovement::Home => {
                    self.cursor.1 = 0;
                }
                PageMovement::End => {
                    self.draw_lines_up_to(grid, area, context, 0);
                    self.cursor.1 = self.height.saturating_sub(1);
                }
            }
        }

        if let Some(ref mut search) = self.search {
            if !search.positions.is_empty() {
                if let Some(mvm) = search.movement.take() {
                    match mvm {
                        SearchMovement::First | SearchMovement::Last => {
                            self.cursor.1 = search.positions[search.cursor].0;
                        }
                        SearchMovement::Previous => {
                            if self.cursor.1 > search.positions[search.cursor].0 {
                                self.cursor.1 = search.positions[search.cursor].0;
                            }
                        }
                        SearchMovement::Next => {
                            if self.cursor.1 + rows < search.positions[search.cursor].0 {
                                self.cursor.1 = search.positions[search.cursor].0;
                            }
                        }
                    }
                }
            }
        }

        grid.clear_area(area, crate::conf::value(context, "theme_default"));

        self.cols_lt_width = cols + self.cursor.0 < width;
        self.rows_lt_height = rows + self.cursor.1 < height;

        self.cursor = (
            std::cmp::min(width.saturating_sub(cols), self.cursor.0),
            std::cmp::min(height.saturating_sub(rows), self.cursor.1),
        );
        self.draw_page(grid, area.take_cols(cols).take_rows(rows), context);
        if self.show_scrollbar && rows < height {
            ScrollBar::default().set_show_arrows(true).draw(
                grid,
                area.nth_col(area.width().saturating_sub(1)),
                context,
                /* position */
                self.cursor.1,
                /* visible_rows */
                rows,
                /* length */
                height,
            );
        }
        if self.show_scrollbar && cols < width {
            ScrollBar::default().set_show_arrows(true).draw_horizontal(
                grid,
                area.nth_row(area.height().saturating_sub(1)),
                context,
                self.cursor.0,
                cols,
                width,
            );
        }
        if (rows < height) || self.search.is_some() {
            const RESULTS_STR: &str = "Results for ";
            let shown_lines = self.cursor.1 + rows;
            let total_lines = height;
            if rows < height {
                context
                    .replies
                    .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
                        ScrollUpdate::Update {
                            id: self.id,
                            context: ScrollContext {
                                shown_lines,
                                total_lines,
                                has_more_lines,
                            },
                        },
                    )));
            } else {
                context
                    .replies
                    .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
                        ScrollUpdate::End(self.id),
                    )));
            };
            if let Some(ref search) = self.search {
                let status_message = format!(
                    "{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}",
                    results_str = RESULTS_STR,
                    search_pattern = &search.pattern,
                    current_pos = if search.positions.is_empty() {
                        0
                    } else {
                        search.cursor + 1
                    },
                    total_results = search.positions.len(),
                    has_more_lines = if !has_more_lines { "" } else { "(+)" }
                );
                let mut attribute = crate::conf::value(context, "status.bar");
                if !context.settings.terminal.use_color() {
                    attribute.attrs |= Attr::REVERSE;
                }
                grid.write_string(
                    &status_message,
                    attribute.fg,
                    attribute.bg,
                    attribute.attrs,
                    area.nth_row(area.height().saturating_sub(1)),
                    None,
                    None,
                );
                /* set search pattern to italics */
                let start_x = RESULTS_STR.len();
                let row_iter = grid.row_iter(
                    area.nth_row(area.height().saturating_sub(1)),
                    start_x..(start_x + search.pattern.grapheme_width()),
                    0,
                );
                debug_assert_eq!(row_iter.area().width(), search.pattern.grapheme_width());
                for c in row_iter {
                    grid[c].set_attrs(attribute.attrs | Attr::ITALICS);
                }
            }
        }
        context.dirty_areas.push_back(area);
    }

    fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
        let shortcuts = self.shortcuts(context);
        match event {
            UIEvent::ConfigReload { old_settings: _ } => {
                self.set_colors(crate::conf::value(context, "theme_default"));
                self.set_dirty(true);
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"])
                    && self.cursor.1 > 0 =>
            {
                self.movement = Some(PageMovement::Up(1));
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"])
                    && self.rows_lt_height =>
            {
                self.movement = Some(PageMovement::Down(1));
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"])
                    && self.cursor.0 > 0 =>
            {
                self.movement = Some(PageMovement::Left(1));
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"])
                    && self.cols_lt_width =>
            {
                self.movement = Some(PageMovement::Right(1));
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_up"]) => {
                self.movement = Some(PageMovement::PageUp(1));
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_down"]) =>
            {
                self.movement = Some(PageMovement::PageDown(1));
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
            {
                self.movement = Some(PageMovement::Home);
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
            {
                self.movement = Some(PageMovement::End);
                self.dirty = true;
                return true;
            }
            UIEvent::ChangeMode(UIMode::Normal) => {
                self.dirty = true;
            }
            UIEvent::Action(View(Pipe(ref bin, ref args))) => {
                use std::{
                    io::Write,
                    process::{Command, Stdio},
                };
                let mut command_obj = match Command::new(bin)
                    .args(args.as_slice())
                    .stdin(Stdio::piped())
                    .stdout(Stdio::piped())
                    .spawn()
                {
                    Ok(o) => o,
                    Err(e) => {
                        context.replies.push_back(UIEvent::StatusEvent(
                            StatusEvent::DisplayMessage(format!(
                                "Could not pipe to {}: {}",
                                bin, e
                            )),
                        ));
                        return true;
                    }
                };
                let stdin = command_obj.stdin.as_mut().expect("failed to open stdin");
                stdin
                    .write_all(self.text.as_bytes())
                    .expect("Failed to write to stdin");

                context
                    .replies
                    .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!(
                        "Pager text piped to '{}{}{}'",
                        &bin,
                        if args.is_empty() { "" } else { " " },
                        args.join(" ")
                    ))));
                return true;
            }
            ev if matches!(ev, UIEvent::Action(View(Filter(None))))
                || matches!(ev, UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::PAGER]["select_filter"])) =>
            {
                let filters = context
                    .settings
                    .pager
                    .named_filters
                    .iter()
                    .map(|(k, v)| (v.to_string(), k.to_string()))
                    .collect::<Vec<_>>();
                if !filters.is_empty() {
                    context.replies.push_back(UIEvent::GlobalUIDialog {
                        value: Box::new(UIDialog::new(
                            "select filter",
                            filters,
                            true,
                            Some(Box::new(move |_id: ComponentId, results: &[String]| {
                                Some(UIEvent::Action(View(Filter(Some(
                                    results.first().cloned()?,
                                )))))
                            })),
                            context,
                        )),
                        parent: Some(self.id()),
                    });
                }
                return true;
            }
            UIEvent::Action(View(Filter(Some(ref cmd)))) => {
                self.filter(cmd, context);
                self.initialised = false;
                self.dirty = true;
                return true;
            }
            UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
                if self
                    .filter_job
                    .as_ref()
                    .map(|(_, h)| h == job_id)
                    .unwrap_or(false) =>
            {
                let (cmd, mut handle) = self.filter_job.take().unwrap();
                match handle.chan.try_recv() {
                    Err(_) => { /* search was canceled */ }
                    Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
                    Ok(Some(buf)) => {
                        if let Some((width, height)) =
                            buf.as_ref().ok().map(EmbeddedGrid::terminal_size)
                        {
                            self.width = width;
                            self.height = height;
                        }
                        self.filtered_content = Some((cmd, buf));
                    }
                }
                self.initialised = false;
                self.set_dirty(true);
            }
            UIEvent::Action(Action::Listing(ListingAction::Search(pattern))) => {
                self.search = Some(SearchPattern {
                    pattern: pattern.to_string(),
                    positions: vec![],
                    cursor: 0,
                    movement: Some(SearchMovement::First),
                });
                self.initialised = false;
                self.dirty = true;
                return true;
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_search_result"])
                    && self.search.is_some() =>
            {
                if let Some(ref mut search) = self.search {
                    search.movement = Some(SearchMovement::Next);
                    search.cursor += 1;
                    self.initialised = false;
                    self.dirty = true;
                    return true;
                }
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::GENERAL]["previous_search_result"])
                    && self.search.is_some() =>
            {
                if let Some(ref mut search) = self.search {
                    search.movement = Some(SearchMovement::Previous);
                    search.cursor = search.cursor.saturating_sub(1);
                    self.initialised = false;
                    self.dirty = true;
                    return true;
                }
            }
            UIEvent::Input(Key::Esc) if self.search.is_some() => {
                self.search = None;
                self.initialised = false;
                self.dirty = true;
                return true;
            }
            UIEvent::Input(Key::Esc) if self.filtered_content.is_some() => {
                self.filtered_content = None;
                self.initialised = false;
                self.dirty = true;
                context
                    .replies
                    .push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
                        String::new(),
                    )));
                return true;
            }
            UIEvent::Resize => {
                self.initialised = false;
                self.set_dirty(true);
            }
            UIEvent::VisibilityChange(true) => {
                self.set_dirty(true);
            }
            UIEvent::VisibilityChange(false) => {
                context
                    .replies
                    .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
                        ScrollUpdate::End(self.id),
                    )));
                context
                    .replies
                    .push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
                        String::new(),
                    )));
            }
            UIEvent::Input(ref key)
                if context.settings.shortcuts.pager.commands.iter().any(|cmd| {
                    if cmd.shortcut == *key {
                        for cmd in &cmd.command {
                            context.replies.push_back(UIEvent::Command(cmd.to_string()));
                        }
                        return true;
                    }
                    false
                }) =>
            {
                return true;
            }
            _ => {}
        }
        false
    }

    fn is_dirty(&self) -> bool {
        self.dirty || !self.initialised
    }

    fn set_dirty(&mut self, value: bool) {
        self.dirty = value;
    }

    fn shortcuts(&self, context: &Context) -> ShortcutMaps {
        let mut ret: ShortcutMaps = Default::default();
        ret.insert(
            Shortcuts::PAGER,
            context.settings.shortcuts.pager.key_values(),
        );
        ret.insert(
            Shortcuts::GENERAL,
            context.settings.shortcuts.general.key_values(),
        );
        ret
    }

    fn id(&self) -> ComponentId {
        self.id
    }

    fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
        if self.id != uuid {
            return;
        }

        context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
    }
}
