mod command_bar;
mod events;
pub mod report;
mod status_bar;
mod title_bar;

use self::{
    command_bar::CommandBar,
    report::{Report, Severity},
    status_bar::StatusBar,
    title_bar::TitleBar,
};
use super::{
    config::ExploreConfig,
    nu_common::{NuColor, NuConfig, NuStyle},
    registry::{Command, CommandRegistry},
    views::{Layout, View, ViewConfig, util::nu_style_to_tui},
};
use anyhow::Result;
use crossterm::{
    event::{KeyCode, KeyEvent, KeyModifiers},
    execute,
    terminal::{
        Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
        enable_raw_mode,
    },
};
use events::UIEvents;
use lscolors::LsColors;
use nu_color_config::StyleComputer;
use nu_protocol::{
    Value,
    engine::{EngineState, Stack},
};
use ratatui::{backend::CrosstermBackend, layout::Rect, widgets::Block};
use std::{
    cmp::min,
    io::{self, Stdout},
    result,
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

pub type Frame<'a> = ratatui::Frame<'a>;
pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;

#[derive(Debug, Clone)]
pub struct Pager<'a> {
    config: PagerConfig<'a>,
    message: Option<String>,
    cmd_buf: CommandBuf,
    search_buf: SearchBuf,
}

#[derive(Debug, Clone, Default)]
struct SearchBuf {
    buf_cmd: String,
    buf_cmd_input: String,
    search_results: Vec<usize>,
    search_index: usize,
    is_reversed: bool,
    is_search_input: bool,
}

#[derive(Debug, Clone, Default)]
struct CommandBuf {
    is_cmd_input: bool,
    run_cmd: bool,
    buf_cmd2: String,
    cursor_pos: usize,
    cmd_history: Vec<String>,
    cmd_history_allow: bool,
    cmd_history_pos: usize,
    cmd_exec_info: Option<String>,
}

impl<'a> Pager<'a> {
    pub fn new(config: PagerConfig<'a>) -> Self {
        Self {
            config,
            cmd_buf: CommandBuf::default(),
            search_buf: SearchBuf::default(),
            message: None,
        }
    }

    pub fn show_message(&mut self, text: impl Into<String>) {
        self.message = Some(text.into());
    }

    pub fn run(
        &mut self,
        engine_state: &EngineState,
        stack: &mut Stack,
        view: Option<Page>,
        commands: CommandRegistry,
    ) -> Result<Option<Value>> {
        // setup terminal
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        execute!(stdout, EnterAlternateScreen, Clear(ClearType::All))?;

        let backend = CrosstermBackend::new(stdout);
        let mut terminal = Terminal::new(backend)?;

        let mut info = ViewInfo {
            status: Some(Report::default()),
            ..Default::default()
        };

        if let Some(text) = self.message.take() {
            info.status = Some(Report::message(text, Severity::Info));
        }

        let result = render_ui(
            &mut terminal,
            engine_state,
            stack,
            self,
            &mut info,
            view,
            commands,
        )?;

        // restore terminal
        disable_raw_mode()?;
        execute!(io::stdout(), LeaveAlternateScreen)?;

        Ok(result)
    }
}

#[derive(Debug, Clone)]
pub enum Transition {
    Ok,
    Exit,
    Cmd(String),
    None,
}

#[derive(Debug, Clone)]
pub enum StatusTopOrEnd {
    Top,
    End,
    None,
}

#[derive(Debug, Clone)]
pub struct PagerConfig<'a> {
    pub nu_config: &'a NuConfig,
    pub explore_config: &'a ExploreConfig,
    pub style_computer: &'a StyleComputer<'a>,
    pub lscolors: &'a LsColors,
    // If true, when quitting output the value of the cell the cursor was on
    pub peek_value: bool,
    pub tail: bool,
    // Just a cached dir we are working in used for color manipulations
    pub cwd: String,
}

impl<'a> PagerConfig<'a> {
    pub fn new(
        nu_config: &'a NuConfig,
        explore_config: &'a ExploreConfig,
        style_computer: &'a StyleComputer,
        lscolors: &'a LsColors,
        peek_value: bool,
        tail: bool,
        cwd: &str,
    ) -> Self {
        Self {
            nu_config,
            explore_config,
            style_computer,
            lscolors,
            peek_value,
            tail,
            cwd: cwd.to_string(),
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn render_ui(
    term: &mut Terminal,
    engine_state: &EngineState,
    stack: &mut Stack,
    pager: &mut Pager<'_>,
    info: &mut ViewInfo,
    view: Option<Page>,
    commands: CommandRegistry,
) -> Result<Option<Value>> {
    let events = UIEvents::new();
    let mut view_stack = ViewStack::new(view, Vec::new());

    loop {
        if engine_state.signals().interrupted() {
            break Ok(None);
        }

        // Give the view a chance to update its internal state (e.g., receive streaming data)
        // and update the status bar before drawing.
        if let Some(page) = view_stack.curr_view.as_mut() {
            page.view.update(info);
        }

        let mut layout = Layout::default();
        {
            let info = info.clone();
            term.draw(|f| {
                draw_frame(f, &mut view_stack.curr_view, pager, &mut layout, info);
            })?;
        }

        // Note that this will return within the configured tick_rate of events. In particular this
        // means this loop keeps redrawing the UI about 4 times a second, whether it needs to or
        // not. That's OK-ish because ratatui will detect real changes and not send unnecessary
        // output to the terminal (something that may especially be important with ssh). While not
        // needed at the moment, the idea is that this behavior allows for some degree of
        // animation (so that the UI can update over time, even without user input).
        let transition = handle_events(
            engine_state,
            stack,
            &events,
            &layout,
            info,
            &mut pager.search_buf,
            &mut pager.cmd_buf,
            view_stack.curr_view.as_mut().map(|p| &mut p.view),
        );

        let (exit, cmd_name) = react_to_event_result(
            transition,
            engine_state,
            &commands,
            pager,
            &mut view_stack,
            stack,
            info,
        );

        if let Some(value) = exit {
            break Ok(value);
        }

        if !cmd_name.is_empty() {
            if let Some(r) = info.report.as_mut() {
                r.message = cmd_name;
                r.level = Severity::Success;
            } else {
                info.report = Some(Report::success(cmd_name));
            }

            let info = info.clone();
            term.draw(|f| {
                draw_info(f, pager, info);
            })?;
        }

        if pager.cmd_buf.run_cmd {
            let args = pager.cmd_buf.buf_cmd2.clone();
            pager.cmd_buf.run_cmd = false;
            pager.cmd_buf.buf_cmd2.clear();

            let out =
                pager_run_command(engine_state, stack, pager, &mut view_stack, &commands, args);
            match out {
                Ok(result) => {
                    if result.exit {
                        break Ok(peek_value_from_view(&mut view_stack.curr_view, pager));
                    }

                    if result.view_change && !result.cmd_name.is_empty() {
                        if let Some(r) = info.report.as_mut() {
                            r.message = result.cmd_name;
                            r.level = Severity::Success;
                        } else {
                            info.report = Some(Report::success(result.cmd_name));
                        }

                        let info = info.clone();
                        term.draw(|f| {
                            draw_info(f, pager, info);
                        })?;
                    }
                }
                Err(err) => info.report = Some(Report::error(err)),
            }
        }
    }
}

fn react_to_event_result(
    status: Transition,
    engine_state: &EngineState,
    commands: &CommandRegistry,
    pager: &mut Pager<'_>,
    view_stack: &mut ViewStack,
    stack: &mut Stack,
    info: &mut ViewInfo,
) -> (Option<Option<Value>>, String) {
    match status {
        Transition::Exit => (
            Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
            String::default(),
        ),
        Transition::Ok => {
            let exit = view_stack.stack.is_empty();
            if exit {
                return (
                    Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
                    String::default(),
                );
            }

            // try to pop the view stack
            if let Some(v) = view_stack.stack.pop() {
                view_stack.curr_view = Some(v);
            }

            (None, String::default())
        }
        Transition::Cmd(cmd) => {
            let out = pager_run_command(engine_state, stack, pager, view_stack, commands, cmd);
            match out {
                Ok(result) if result.exit => (
                    Some(peek_value_from_view(&mut view_stack.curr_view, pager)),
                    String::default(),
                ),
                Ok(result) => (None, result.cmd_name),
                Err(err) => {
                    info.report = Some(Report::error(err));
                    (None, String::default())
                }
            }
        }
        Transition::None => (None, String::default()),
    }
}

fn peek_value_from_view(view: &mut Option<Page>, pager: &mut Pager<'_>) -> Option<Value> {
    if pager.config.peek_value {
        let view = view.as_mut().map(|p| &mut p.view);
        view.and_then(|v| v.exit())
    } else {
        None
    }
}

fn draw_frame(
    f: &mut Frame,
    view: &mut Option<Page>,
    pager: &mut Pager<'_>,
    layout: &mut Layout,
    info: ViewInfo,
) {
    let area = f.area();

    // Reserve space: 1 line for title bar at top, 2 lines for status/cmd bars at bottom
    let title_area = Rect::new(area.x, area.y, area.width, 1);
    let content_area = Rect::new(
        area.x,
        area.y + 1,
        area.width,
        area.height.saturating_sub(3),
    );

    // Render title bar
    render_title_bar(f, title_area, pager.config.explore_config);

    if let Some(page) = view {
        let cfg = create_view_config(pager);
        page.view.draw(f, content_area, cfg, layout);
    }

    draw_info(f, pager, info);

    highlight_search_results(f, pager, layout, pager.config.explore_config.highlight);
    set_cursor_cmd_bar(f, area, pager);
}

fn render_title_bar(f: &mut Frame, area: Rect, theme: &ExploreConfig) {
    let mut title_bar = TitleBar::new("Explore")
        .with_info_left("Navigate: ←↑↓→")
        .with_info_right(":help for help");
    title_bar.set_background_style(theme.title_bar_background);
    title_bar.set_title_style(theme.title_bar_text);
    title_bar.set_info_style(theme.title_bar_text);

    f.render_widget(title_bar, area);
}

fn draw_info(f: &mut Frame, pager: &mut Pager<'_>, info: ViewInfo) {
    let area = f.area();

    if let Some(report) = info.status {
        let last_2nd_line = area.bottom().saturating_sub(2);
        let area = Rect::new(area.left(), last_2nd_line, area.width, 1);
        render_status_bar(f, area, report, pager.config.explore_config);
    }

    {
        let last_line = area.bottom().saturating_sub(1);
        let area = Rect::new(area.left(), last_line, area.width, 1);
        render_cmd_bar(f, area, pager, info.report, pager.config.explore_config);
    }
}

fn create_view_config<'a>(pager: &'a Pager<'_>) -> ViewConfig<'a> {
    let cfg = &pager.config;
    ViewConfig::new(
        cfg.nu_config,
        cfg.explore_config,
        cfg.style_computer,
        cfg.lscolors,
        &pager.config.cwd,
    )
}

fn pager_run_command(
    engine_state: &EngineState,
    stack: &mut Stack,
    pager: &mut Pager,
    view_stack: &mut ViewStack,
    commands: &CommandRegistry,
    args: String,
) -> result::Result<CmdResult, String> {
    let command = commands.find(&args);
    match command {
        Some(Ok(command)) => {
            let result = run_command(engine_state, stack, pager, view_stack, command);
            match result {
                Ok(value) => Ok(value),
                Err(err) => Err(format!("Error: command {args:?} failed: {err}")),
            }
        }
        Some(Err(err)) => Err(format!(
            "Error: command {args:?} was not provided with correct arguments: {err}"
        )),
        None => Err(format!("Error: command {args:?} was not recognized")),
    }
}

fn run_command(
    engine_state: &EngineState,
    stack: &mut Stack,
    pager: &mut Pager,
    view_stack: &mut ViewStack,
    command: Command,
) -> Result<CmdResult> {
    match command {
        Command::Reactive(mut command) => {
            // what we do we just replace the view.
            let value = view_stack.curr_view.as_mut().and_then(|p| p.view.exit());
            let transition = command.react(engine_state, stack, pager, value)?;
            match transition {
                Transition::Ok => Ok(CmdResult::new(false, false, String::new())),
                Transition::Exit => Ok(CmdResult::new(true, false, String::new())),
                Transition::Cmd { .. } => todo!("not used so far"),
                Transition::None => panic!("Transition::None not expected from command.react()"),
            }
        }
        Command::View { mut cmd, stackable } => {
            // what we do we just replace the view.
            let value = view_stack.curr_view.as_mut().and_then(|p| p.view.exit());
            let view_cfg = create_view_config(pager);

            let new_view = cmd.spawn(engine_state, stack, value, &view_cfg)?;
            if let Some(view) = view_stack.curr_view.take()
                && view.stackable
            {
                view_stack.stack.push(view);
            }

            view_stack.curr_view = Some(Page::raw(new_view, stackable));

            Ok(CmdResult::new(false, true, cmd.name().to_owned()))
        }
    }
}

fn set_cursor_cmd_bar(f: &mut Frame, area: Rect, pager: &Pager) {
    // Account for left padding (1) + prefix char like ':' or '/' (1)
    const LEFT_OFFSET: u16 = 2;

    if pager.search_buf.is_search_input {
        // todo: deal with a situation where we exceed the bar width
        let next_pos = pager.search_buf.buf_cmd_input.width() as u16 + LEFT_OFFSET;
        if next_pos < area.width {
            f.set_cursor_position((next_pos, area.height - 1));
        }
    }
}

fn render_status_bar(f: &mut Frame, area: Rect, report: Report, theme: &ExploreConfig) {
    let msg_style = report_msg_style(&report, theme, theme.status_bar_text);
    let mut status_bar = create_status_bar(report);
    status_bar.set_background_style(theme.status_bar_background);
    status_bar.set_message_style(msg_style);
    status_bar.set_ctx1_style(theme.status_bar_text);
    status_bar.set_ctx2_style(theme.status_bar_text);
    status_bar.set_ctx3_style(theme.status_bar_text);

    f.render_widget(status_bar, area);
}

fn create_status_bar(report: Report) -> StatusBar {
    StatusBar::new(
        report.message,
        report.context1,
        report.context2,
        report.context3,
    )
}

fn report_msg_style(report: &Report, config: &ExploreConfig, style: NuStyle) -> NuStyle {
    match report.level {
        Severity::Info => style,
        _ => report_level_style(report.level, config),
    }
}

fn render_cmd_bar(
    f: &mut Frame,
    area: Rect,
    pager: &Pager,
    report: Option<Report>,
    config: &ExploreConfig,
) {
    if let Some(report) = report {
        let style = report_msg_style(&report, config, config.cmd_bar_text);
        let bar = CommandBar::new(
            &report.message,
            &report.context1,
            style,
            config.cmd_bar_background,
        );

        f.render_widget(bar, area);
        return;
    }

    if pager.cmd_buf.is_cmd_input {
        render_cmd_bar_cmd(f, area, pager, config);
        return;
    }

    if pager.search_buf.is_search_input || !pager.search_buf.buf_cmd_input.is_empty() {
        render_cmd_bar_search(f, area, pager, config);
    }
}

fn render_cmd_bar_search(f: &mut Frame, area: Rect, pager: &Pager<'_>, config: &ExploreConfig) {
    if pager.search_buf.search_results.is_empty() && !pager.search_buf.is_search_input {
        let message = format!("Pattern not found: {}", pager.search_buf.buf_cmd_input);
        let style = NuStyle {
            background: Some(NuColor::Red),
            foreground: Some(NuColor::White),
            ..Default::default()
        };

        let bar = CommandBar::new(&message, "", style, config.cmd_bar_background);
        f.render_widget(bar, area);
        return;
    }

    let prefix = if pager.search_buf.is_reversed {
        '?'
    } else {
        '/'
    };
    let text = format!("{}{}", prefix, pager.search_buf.buf_cmd_input);
    let info = if pager.search_buf.search_results.is_empty() {
        String::from("[0/0]")
    } else {
        let index = pager.search_buf.search_index + 1;
        let total = pager.search_buf.search_results.len();
        format!("[{index}/{total}]")
    };

    let bar = CommandBar::new(&text, &info, config.cmd_bar_text, config.cmd_bar_background);
    f.render_widget(bar, area);
}

fn render_cmd_bar_cmd(f: &mut Frame, area: Rect, pager: &Pager, config: &ExploreConfig) {
    let input = pager.cmd_buf.buf_cmd2.as_str();
    let cursor_pos = pager.cmd_buf.cursor_pos;

    let input_width = input.width();
    let max_len = area.width as usize - 1; // -1 for :
    let mut display_input = input;
    let mut skipped_chars = 0;
    if input_width > max_len {
        // Truncate from left to fit max_len
        let mut current_width = 0;
        let mut start_byte = 0;
        for (i, c) in input.char_indices() {
            let c_width = c.width().unwrap_or(1);
            if current_width + c_width > input_width - max_len {
                start_byte = i;
                break;
            }
            current_width += c_width;
            skipped_chars += 1;
        }
        display_input = &input[start_byte..];
    }

    // Further truncate to match CommandBar's max_text_width
    let max_text_width = area.width.saturating_sub(1) as usize; // TEXT_PADDING_LEFT=1, no info padding
    let max_for_display = max_text_width.saturating_sub(1);
    let mut truncated_display = display_input;
    if truncated_display.width() > max_for_display {
        let mut current_width = 0;
        let mut end = 0;
        for (i, c) in truncated_display.char_indices() {
            let c_width = c.width().unwrap_or(1);
            if current_width + c_width > max_for_display {
                break;
            }
            current_width += c_width;
            end = i + c.len_utf8();
        }
        truncated_display = &truncated_display[..end];
    }

    let prefix = ':';
    let text = format!("{prefix}{truncated_display}");

    let bar = CommandBar::new(&text, "", config.cmd_bar_text, config.cmd_bar_background);
    f.render_widget(bar, area);

    // Set the terminal cursor position
    if pager.cmd_buf.is_cmd_input {
        let cursor_in_input = cursor_pos; // char index of cursor in buf
        let cursor_col = if cursor_in_input >= skipped_chars {
            let mut cursor_in_display = cursor_in_input - skipped_chars;
            // Clamp to truncated_display length
            let truncated_char_count = truncated_display.chars().count();
            cursor_in_display = cursor_in_display.min(truncated_char_count);
            // Get the substring before the cursor
            let before_display: String =
                truncated_display.chars().take(cursor_in_display).collect();
            2 + before_display.width() as u16 // 2 for :, then visual width
        } else {
            2 // cursor is in truncated part, place at start after :
        };
        f.set_cursor_position((area.x + cursor_col, area.y));
    }
}

fn highlight_search_results(f: &mut Frame, pager: &Pager, layout: &Layout, style: NuStyle) {
    if pager.search_buf.search_results.is_empty() {
        return;
    }

    let highlight_block = Block::default().style(nu_style_to_tui(style));

    for e in &layout.data {
        let text = ansi_str::AnsiStr::ansi_strip(&e.text);

        if let Some(p) = text.find(&pager.search_buf.buf_cmd_input) {
            let p = covert_bytes_to_chars(&text, p);

            // this width is a best guess
            let w = pager.search_buf.buf_cmd_input.width() as u16;
            let area = Rect::new(e.area.x + p as u16, e.area.y, w, 1);

            f.render_widget(highlight_block.clone(), area);
        }
    }
}

fn covert_bytes_to_chars(text: &str, p: usize) -> usize {
    let mut b = 0;
    let mut i = 0;
    for c in text.chars() {
        b += c.len_utf8();
        if b > p {
            break;
        }

        i += 1;
    }

    i
}

#[allow(clippy::too_many_arguments)]
fn handle_events<V: View>(
    engine_state: &EngineState,
    stack: &mut Stack,
    events: &UIEvents,
    layout: &Layout,
    info: &mut ViewInfo,
    search: &mut SearchBuf,
    command: &mut CommandBuf,
    mut view: Option<&mut V>,
) -> Transition {
    // We are only interested in Pressed events;
    // It's crucial because there are cases where terminal MIGHT produce false events;
    // 2 events 1 for release 1 for press.
    // Want to react only on 1 of them so we do.
    let mut key = match events.next_key_press() {
        Ok(Some(key)) => key,
        Ok(None) => return Transition::None,
        Err(e) => {
            log::error!("Failed to read key event: {e}");
            return Transition::None;
        }
    };

    // Sometimes we get a BIG list of events;
    // for example when someone scrolls via a mouse either UP or DOWN.
    // This MIGHT cause freezes as we have a 400 delay for a next command read.
    //
    // To eliminate that we are trying to read all possible commands which we should act upon.
    loop {
        let result = handle_event(
            engine_state,
            stack,
            layout,
            info,
            search,
            command,
            view.as_deref_mut(),
            key,
        );
        if !matches!(result, Transition::None) {
            return result;
        }
        match events.try_next_key_press() {
            Ok(Some(next_key)) => key = next_key,
            Ok(None) => return Transition::None,
            Err(e) => {
                log::error!("Failed to peek key event: {e}");
                return Transition::None;
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn handle_event<V: View>(
    engine_state: &EngineState,
    stack: &mut Stack,
    layout: &Layout,
    info: &mut ViewInfo,
    search: &mut SearchBuf,
    command: &mut CommandBuf,
    mut view: Option<&mut V>,
    key: KeyEvent,
) -> Transition {
    if handle_exit_key_event(&key) {
        return Transition::Exit;
    }

    if handle_general_key_events1(&key, search, command, view.as_deref_mut()) {
        return Transition::None;
    }

    if let Some(view) = &mut view {
        let t = view.handle_input(engine_state, stack, layout, info, key);
        match t {
            Transition::Exit => return Transition::Ok,
            Transition::Cmd(cmd) => return Transition::Cmd(cmd),
            Transition::Ok => return Transition::None,
            Transition::None => {}
        }
    }

    // was not handled so we must check our default controls
    handle_general_key_events2(&key, search, command, view, info);

    Transition::None
}

fn handle_exit_key_event(key: &KeyEvent) -> bool {
    if key.modifiers == KeyModifiers::CONTROL {
        // these are all common things people might try, might as well handle them all
        if let KeyCode::Char('c') | KeyCode::Char('d') | KeyCode::Char('q') = key.code {
            return true;
        }
    }
    false
}

fn handle_general_key_events1<V>(
    key: &KeyEvent,
    search: &mut SearchBuf,
    command: &mut CommandBuf,
    view: Option<&mut V>,
) -> bool
where
    V: View,
{
    if search.is_search_input {
        return search_input_key_event(search, view, key);
    }

    if command.is_cmd_input {
        return cmd_input_key_event(command, key);
    }

    false
}

fn handle_general_key_events2<V>(
    key: &KeyEvent,
    search: &mut SearchBuf,
    command: &mut CommandBuf,
    view: Option<&mut V>,
    info: &mut ViewInfo,
) where
    V: View,
{
    match key.code {
        KeyCode::Char('?') => {
            search.buf_cmd_input.clear();
            search.is_search_input = true;
            search.is_reversed = true;

            info.report = None;
        }
        KeyCode::Char('/') => {
            search.buf_cmd_input.clear();
            search.is_search_input = true;
            search.is_reversed = false;

            info.report = None;
        }
        KeyCode::Char(':') => {
            command.buf_cmd2.clear();
            command.is_cmd_input = true;
            command.cmd_exec_info = None;

            info.report = None;
        }
        KeyCode::Char('n') => {
            if !search.search_results.is_empty() {
                if search.buf_cmd_input.is_empty() {
                    search.buf_cmd_input.clone_from(&search.buf_cmd);
                }

                if search.search_index + 1 == search.search_results.len() {
                    search.search_index = 0
                } else {
                    search.search_index += 1;
                }

                let pos = search.search_results[search.search_index];
                if let Some(view) = view {
                    view.show_data(pos);
                }
            }
        }
        KeyCode::Char('N') => {
            if !search.search_results.is_empty() {
                if search.buf_cmd_input.is_empty() {
                    search.buf_cmd_input.clone_from(&search.buf_cmd);
                }

                if search.search_index == 0 {
                    search.search_index = search.search_results.len() - 1;
                } else {
                    search.search_index -= 1;
                }

                let pos = search.search_results[search.search_index];
                if let Some(view) = view {
                    view.show_data(pos);
                }
            }
        }
        _ => {}
    }
}

fn search_input_key_event(
    buf: &mut SearchBuf,
    view: Option<&mut impl View>,
    key: &KeyEvent,
) -> bool {
    match &key.code {
        KeyCode::Esc => {
            buf.buf_cmd_input.clear();

            if let Some(view) = view
                && !buf.buf_cmd.is_empty()
            {
                let data = view.collect_data().into_iter().map(|(text, _)| text);
                buf.search_results = search_pattern(data, &buf.buf_cmd, buf.is_reversed);
                buf.search_index = 0;
            }

            buf.is_search_input = false;

            true
        }
        KeyCode::Enter => {
            buf.buf_cmd.clone_from(&buf.buf_cmd_input);
            buf.is_search_input = false;

            if let Some(view) = view
                && !buf.buf_cmd.is_empty()
            {
                let data = view.collect_data().into_iter().map(|(text, _)| text);
                buf.search_results = search_pattern(data, &buf.buf_cmd, buf.is_reversed);
                buf.search_index = 0;

                if !buf.search_results.is_empty() {
                    let pos = buf.search_results[buf.search_index];
                    view.show_data(pos);
                }
            }

            true
        }
        KeyCode::Backspace => {
            if buf.buf_cmd_input.is_empty() {
                buf.is_search_input = false;
                buf.is_reversed = false;
            } else {
                buf.buf_cmd_input.pop();

                if let Some(view) = view
                    && !buf.buf_cmd_input.is_empty()
                {
                    let data = view.collect_data().into_iter().map(|(text, _)| text);
                    buf.search_results = search_pattern(data, &buf.buf_cmd_input, buf.is_reversed);
                    buf.search_index = 0;

                    if !buf.search_results.is_empty() {
                        let pos = buf.search_results[buf.search_index];
                        view.show_data(pos);
                    }
                }
            }

            true
        }
        KeyCode::Char(c) => {
            buf.buf_cmd_input.push(*c);

            if let Some(view) = view
                && !buf.buf_cmd_input.is_empty()
            {
                let data = view.collect_data().into_iter().map(|(text, _)| text);
                buf.search_results = search_pattern(data, &buf.buf_cmd_input, buf.is_reversed);
                buf.search_index = 0;

                if !buf.search_results.is_empty() {
                    let pos = buf.search_results[buf.search_index];
                    view.show_data(pos);
                }
            }

            true
        }
        _ => false,
    }
}

fn search_pattern(data: impl Iterator<Item = String>, pat: &str, rev: bool) -> Vec<usize> {
    let mut matches = Vec::new();
    for (row, text) in data.enumerate() {
        if text.contains(pat) {
            matches.push(row);
        }
    }

    if rev {
        matches.reverse();
    }

    matches
}

fn cmd_input_key_event(buf: &mut CommandBuf, key: &KeyEvent) -> bool {
    match &key.code {
        KeyCode::Esc => {
            buf.is_cmd_input = false;
            buf.buf_cmd2 = String::new();
            buf.cursor_pos = 0;
            true
        }
        KeyCode::Enter => {
            buf.is_cmd_input = false;
            buf.run_cmd = true;
            buf.cmd_history.push(buf.buf_cmd2.clone());
            buf.cmd_history_pos = buf.cmd_history.len();
            buf.cursor_pos = 0;
            true
        }
        KeyCode::Backspace => {
            if buf.cursor_pos > 0 {
                buf.cursor_pos -= 1;
                buf.buf_cmd2.remove(buf.cursor_pos);
                buf.cmd_history_allow = false;
            } else if buf.buf_cmd2.is_empty() {
                buf.is_cmd_input = false;
            }
            true
        }
        KeyCode::Delete => {
            if buf.cursor_pos < buf.buf_cmd2.len() {
                buf.buf_cmd2.remove(buf.cursor_pos);
                buf.cmd_history_allow = false;
            }
            true
        }
        KeyCode::Left => {
            if buf.cursor_pos > 0 {
                buf.cursor_pos -= 1;
            }
            true
        }
        KeyCode::Right => {
            if buf.cursor_pos < buf.buf_cmd2.len() {
                buf.cursor_pos += 1;
            }
            true
        }
        KeyCode::Char(c) => {
            buf.buf_cmd2.insert(buf.cursor_pos, *c);
            buf.cursor_pos += 1;
            buf.cmd_history_allow = false;
            true
        }
        KeyCode::Down if buf.buf_cmd2.is_empty() || buf.cmd_history_allow => {
            if !buf.cmd_history.is_empty() {
                buf.cmd_history_allow = true;
                buf.cmd_history_pos = min(
                    buf.cmd_history_pos + 1,
                    buf.cmd_history.len().saturating_sub(1),
                );
                buf.buf_cmd2
                    .clone_from(&buf.cmd_history[buf.cmd_history_pos]);
                buf.cursor_pos = buf.buf_cmd2.len();
            }

            true
        }
        KeyCode::Up if buf.buf_cmd2.is_empty() || buf.cmd_history_allow => {
            if !buf.cmd_history.is_empty() {
                buf.cmd_history_allow = true;
                buf.cmd_history_pos = buf.cmd_history_pos.saturating_sub(1);
                buf.buf_cmd2
                    .clone_from(&buf.cmd_history[buf.cmd_history_pos]);
                buf.cursor_pos = buf.buf_cmd2.len();
            }

            true
        }
        _ => true,
    }
}

fn report_level_style(level: Severity, config: &ExploreConfig) -> NuStyle {
    match level {
        Severity::Info => config.status_info,
        Severity::Success => config.status_success,
        Severity::Warn => config.status_warn,
        Severity::Err => config.status_error,
    }
}

#[derive(Debug, Default, Clone)]
pub struct ViewInfo {
    pub cursor: Option<Position>,
    pub status: Option<Report>,
    pub report: Option<Report>,
}

#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Position {
    pub x: u16,
    pub y: u16,
}

impl Position {
    pub fn new(x: u16, y: u16) -> Self {
        Self { x, y }
    }
}

pub struct Page {
    pub view: Box<dyn View>,
    /// Controls what happens when this view is the current view and a new view is created.
    /// If true, view will be pushed to the stack, otherwise, it will be deleted.
    pub stackable: bool,
}

impl Page {
    pub fn raw(view: Box<dyn View>, stackable: bool) -> Self {
        Self { view, stackable }
    }

    pub fn new<V>(view: V, stackable: bool) -> Self
    where
        V: View + 'static,
    {
        Self::raw(Box::new(view), stackable)
    }
}

struct ViewStack {
    curr_view: Option<Page>,
    stack: Vec<Page>,
}

impl ViewStack {
    fn new(view: Option<Page>, stack: Vec<Page>) -> Self {
        Self {
            curr_view: view,
            stack,
        }
    }
}

struct CmdResult {
    exit: bool,
    view_change: bool,
    cmd_name: String,
}

impl CmdResult {
    fn new(exit: bool, view_change: bool, cmd_name: String) -> Self {
        Self {
            exit,
            view_change,
            cmd_name,
        }
    }
}
