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

use std::cmp;

use melib::{
    utils::datetime::{timestamp_to_string, UnixTimestamp},
    Address,
};

use super::*;
use crate::{components::PageMovement, conf::data_types::ThreadLayout};

#[derive(Debug)]
struct ThreadEntry {
    index: (usize, ThreadNodeHash, usize),
    /// (indentation, `thread_node` index, line number in listing)
    indentation: usize,
    msg_hash: EnvelopeHash,
    seen: bool,
    dirty: bool,
    hidden: bool,
    heading: String,
    timestamp: UnixTimestamp,
    mailview: Box<MailView>,
}

#[derive(Clone, Copy, Debug, Default)]
pub enum ThreadViewFocus {
    #[default]
    None,
    Thread,
    MailView,
}

#[derive(Debug)]
pub struct ThreadView {
    new_cursor_pos: usize,
    cursor_pos: usize,
    expanded_pos: usize,
    new_expanded_pos: usize,
    reversed: bool,
    coordinates: (AccountHash, MailboxHash, EnvelopeHash),
    thread_group: ThreadHash,
    focus: ThreadViewFocus,
    entries: Vec<ThreadEntry>,
    visible_entries: Vec<Vec<usize>>,
    //indentation_colors: [ThemeAttribute; 6],
    use_color: bool,
    last_width: usize,
    thread_layout: ThreadLayout,
    movement: Option<PageMovement>,
    dirty: bool,
    content: Screen<Virtual>,
    id: ComponentId,
}

impl ThreadView {
    /// @`coordinates`: (account index, `mailbox_hash`, root set `thread_node`
    /// index)
    /// @`expanded_hash`: optional position of expanded entry when we
    ///                   render the `ThreadView`.
    ///                   default: expanded message is the last one.
    pub fn new(
        coordinates: (AccountHash, MailboxHash, EnvelopeHash),
        thread_group: ThreadHash,
        expanded_hash: Option<EnvelopeHash>,
        go_to_first_unread: bool,
        focus: Option<ThreadViewFocus>,
        context: &mut Context,
    ) -> Self {
        let theme_default = crate::conf::value(context, "theme_default");
        let mut view = Self {
            reversed: false,
            coordinates,
            thread_group,
            focus: focus.unwrap_or_default(),
            entries: Vec::new(),
            cursor_pos: 1,
            new_cursor_pos: 0,
            dirty: true,
            id: ComponentId::default(),
            //indentation_colors: [
            //    crate::conf::value(context, "mail.view.thread.indentation.a"),
            //    crate::conf::value(context, "mail.view.thread.indentation.b"),
            //    crate::conf::value(context, "mail.view.thread.indentation.c"),
            //    crate::conf::value(context, "mail.view.thread.indentation.d"),
            //    crate::conf::value(context, "mail.view.thread.indentation.e"),
            //    crate::conf::value(context, "mail.view.thread.indentation.f"),
            //],
            use_color: context.settings.terminal.use_color(),
            last_width: 0,
            thread_layout: *mailbox_settings!(
                context[coordinates.0][&coordinates.1].listing.thread_layout
            ),
            expanded_pos: 0,
            new_expanded_pos: 0,
            visible_entries: vec![],
            movement: None,
            content: Screen::<Virtual>::new(theme_default),
        };
        view.initiate(expanded_hash, go_to_first_unread, context);
        view.new_cursor_pos = view.new_expanded_pos;
        view
    }

    pub fn update(&mut self, context: &mut Context) {
        if self.entries.is_empty() {
            return;
        }

        let old_entries = std::mem::take(&mut self.entries);

        let old_focused_entry = if self.entries.len() > self.cursor_pos {
            Some(self.entries.remove(self.cursor_pos))
        } else {
            None
        };

        let old_expanded_entry = if self.entries.len() > self.expanded_pos {
            Some(self.entries.remove(self.expanded_pos))
        } else {
            None
        };

        let expanded_hash = old_expanded_entry.as_ref().map(|e| e.msg_hash);
        self.initiate(expanded_hash, false, context);

        let mut old_cursor = 0;
        let mut new_cursor = 0;
        loop {
            if old_cursor >= old_entries.len() || new_cursor >= self.entries.len() {
                break;
            }
            if old_entries[old_cursor].msg_hash == self.entries[new_cursor].msg_hash
                || old_entries[old_cursor].index == self.entries[new_cursor].index
                || old_entries[old_cursor].heading == self.entries[new_cursor].heading
            {
                self.entries[new_cursor].hidden = old_entries[old_cursor].hidden;
                old_cursor += 1;
            }
            new_cursor += 1;
            self.recalc_visible_entries();
        }

        if let Some(old_focused_entry) = old_focused_entry {
            if let Some(new_entry_idx) = self.entries.iter().position(|e| {
                e.msg_hash == old_focused_entry.msg_hash
                    || (e.index.1 == old_focused_entry.index.1
                        && e.index.2 == old_focused_entry.index.2)
            }) {
                self.cursor_pos = new_entry_idx;
            }
        }
        if let Some(old_expanded_entry) = old_expanded_entry {
            if let Some(new_entry_idx) = self.entries.iter().position(|e| {
                e.msg_hash == old_expanded_entry.msg_hash
                    || (e.index.1 == old_expanded_entry.index.1
                        && e.index.2 == old_expanded_entry.index.2)
            }) {
                self.expanded_pos = new_entry_idx;
            }
        }
        self.set_dirty(true);
    }

    fn initiate(
        &mut self,
        expanded_hash: Option<EnvelopeHash>,
        go_to_first_unread: bool,
        context: &mut Context,
    ) {
        #[inline(always)]
        fn make_entry(
            i: (usize, ThreadNodeHash, usize),
            (account_hash, mailbox_hash, msg_hash): (AccountHash, MailboxHash, EnvelopeHash),
            seen: bool,
            initialize_now: bool,
            timestamp: UnixTimestamp,
            context: &mut Context,
        ) -> ThreadEntry {
            let (ind, _, _) = i;
            ThreadEntry {
                index: i,
                indentation: ind,
                mailview: Box::new(MailView::new(
                    Some((account_hash, mailbox_hash, msg_hash)),
                    initialize_now,
                    context,
                )),
                msg_hash,
                seen,
                dirty: true,
                hidden: false,
                heading: String::new(),
                timestamp,
            }
        }

        let collection = context.accounts[&self.coordinates.0].collection.clone();
        let threads = collection.get_threads(self.coordinates.1);

        if !threads.groups.contains_key(&self.thread_group) {
            return;
        }
        let (account_hash, mailbox_hash, _) = self.coordinates;

        // Find out how many entries there are going to be, and prioritize
        // initialization to the open entry and the most recent ones.
        //
        // This helps skip initializing the whole thread at once, which will make the UI
        // loading slower.
        //
        // This won't help at all if the latest entry is a reply to an older entry but
        // oh well.
        let mut total_entries = vec![];
        for (_, thread_node_hash) in threads.thread_iter(self.thread_group) {
            if let Some(msg_hash) = threads.thread_nodes()[&thread_node_hash].message() {
                if Some(msg_hash) == expanded_hash {
                    continue;
                }
                let env_ref = collection.get_env(msg_hash);
                total_entries.push((msg_hash, env_ref.timestamp));
            };
        }
        total_entries.sort_by_key(|e| cmp::Reverse(e.1));
        let tokens = f64::from(u32::try_from(total_entries.len()).unwrap_or(0)) * 0.29;
        let tokens = tokens.ceil() as usize;
        total_entries.truncate(tokens);

        // Now, only the expanded envelope plus the ones that remained in total_entries
        // (around 30% of the total messages in the thread) will be scheduled
        // for loading immediately. The others will be lazily loaded when the
        // user opens them for reading.

        let thread_iter = threads.thread_iter(self.thread_group);
        self.entries.clear();
        let mut earliest_unread = 0;
        let mut earliest_unread_entry = 0;
        for (line, (ind, thread_node_hash)) in thread_iter.enumerate() {
            let entry = if let Some(msg_hash) = threads.thread_nodes()[&thread_node_hash].message()
            {
                let (is_seen, timestamp) = {
                    let env_ref = collection.get_env(msg_hash);
                    if !env_ref.is_seen()
                        && (earliest_unread == 0 || env_ref.timestamp < earliest_unread)
                    {
                        earliest_unread = env_ref.timestamp;
                        earliest_unread_entry = self.entries.len();
                    }
                    (env_ref.is_seen(), env_ref.timestamp)
                };
                let initialize_now = if total_entries.is_empty() {
                    false
                } else {
                    // ExtractIf but it hasn't been stabilized yet.
                    // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.extract_if
                    let mut i = 0;
                    let mut result = false;
                    while i < total_entries.len() {
                        if total_entries[i].0 == msg_hash {
                            total_entries.remove(i);
                            result = true;
                            break;
                        } else {
                            i += 1;
                        }
                    }
                    result
                };
                make_entry(
                    (ind, thread_node_hash, line),
                    (account_hash, mailbox_hash, msg_hash),
                    is_seen,
                    initialize_now || expanded_hash == Some(msg_hash),
                    timestamp,
                    context,
                )
            } else {
                continue;
            };
            match expanded_hash {
                Some(expanded_hash) if expanded_hash == entry.msg_hash => {
                    self.new_expanded_pos = self.entries.len();
                    self.expanded_pos = self.new_expanded_pos + 1;
                }
                _ => {}
            }
            self.entries.push(entry);
        }
        if expanded_hash.is_none() {
            self.new_expanded_pos = self
                .entries
                .iter()
                .enumerate()
                .reduce(|a, b| if a.1.timestamp > b.1.timestamp { a } else { b })
                .map(|el| el.0)
                .unwrap_or(0);
            self.expanded_pos = self.new_expanded_pos + 1;
        }
        if go_to_first_unread && earliest_unread > 0 {
            self.new_expanded_pos = earliest_unread_entry;
            self.expanded_pos = earliest_unread_entry + 1;
        }

        let height = self.entries.len();
        let mut width = 0;

        for e in &mut self.entries {
            let envelope: EnvelopeRef = context.accounts[&self.coordinates.0]
                .collection
                .get_env(e.msg_hash);
            let thread_node = &threads.thread_nodes()[&e.index.1];
            let from = Address::display_name_slice(envelope.from(), None);
            let date = timestamp_to_string(envelope.date(), Some("%Y-%m-%d %H:%M\0"), true);
            e.heading = if thread_node.show_subject() {
                let subject = envelope.subject();
                format!(
                    "{date} {subject:`>indent$} {from}",
                    indent = 2 * e.index.0 + subject.grapheme_width(),
                )
            } else {
                format!(
                    "{date} {from:`>indent$}",
                    indent = 2 * e.index.0 + from.grapheme_width()
                )
            };
            width = width.max(e.heading.grapheme_width() + 1);
        }
        if !self.content.resize_with_context(width, height, context) {
            return;
        }
        let theme_default = crate::conf::value(context, "theme_default");
        let highlight_theme = crate::conf::value(context, "theme_default");
        if self.reversed {
            for (y, e) in self.entries.iter().rev().enumerate() {
                {
                    let area = self
                        .content
                        .area()
                        .skip_rows(y)
                        .take(e.heading.grapheme_width() + 1, height - 1);
                    self.content.grid_mut().write_string(
                        &e.heading,
                        if e.seen {
                            theme_default.fg
                        } else {
                            highlight_theme.fg
                        },
                        if e.seen {
                            theme_default.bg
                        } else {
                            highlight_theme.bg
                        },
                        theme_default.attrs,
                        area,
                        None,
                        None,
                    );
                }
            }
        } else {
            for (y, e) in self.entries.iter().enumerate() {
                {
                    let area = self
                        .content
                        .area()
                        .skip_rows(y)
                        .take(e.heading.grapheme_width(), height - 1);
                    self.content.grid_mut().write_string(
                        &e.heading,
                        if e.seen {
                            theme_default.fg
                        } else {
                            highlight_theme.fg
                        },
                        if e.seen {
                            theme_default.bg
                        } else {
                            highlight_theme.bg
                        },
                        theme_default.attrs,
                        area,
                        None,
                        None,
                    );
                }
            }
        }
        self.visible_entries = vec![(0..self.entries.len()).collect()];
    }

    fn highlight_line(
        &self,
        grid: &mut CellBuffer,
        dest_area: Area,
        src_area: Area,
        idx: usize,
        context: &Context,
    ) {
        if self
            .visible_entries
            .iter()
            .flat_map(|v| v.iter())
            .nth(self.cursor_pos)
            == Some(&idx)
        {
            let mut highlight = crate::conf::value(context, "highlight");
            if self.use_color {
                highlight.attrs |= Attr::REVERSE;
            }

            grid.change_theme(dest_area, highlight);
            return;
        }

        grid.copy_area(self.content.grid(), dest_area, src_area);
    }

    fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        if self.entries.is_empty() {
            context.dirty_areas.push_back(area);
            return;
        }
        let height = self.content.area().height();
        if height == 0 {
            context.dirty_areas.push_back(area);
            return;
        }
        let rows = area.height();
        if rows == 0 {
            context.dirty_areas.push_back(area);
            return;
        }
        if let Some(mvm) = self.movement.take() {
            match mvm {
                PageMovement::Up(amount) => {
                    self.new_cursor_pos = self.new_cursor_pos.saturating_sub(amount);
                }
                PageMovement::PageUp(multiplier) => {
                    self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows * multiplier);
                }
                PageMovement::Down(amount) => {
                    if self.new_cursor_pos + amount + 1 < height {
                        self.new_cursor_pos += amount;
                    } else if self.new_cursor_pos + amount > height {
                        self.new_cursor_pos = height - 1;
                    } else {
                        self.new_cursor_pos = (height / rows) * rows;
                    }
                }
                PageMovement::PageDown(multiplier) => {
                    if self.new_cursor_pos + rows * multiplier + 1 < height {
                        self.new_cursor_pos += rows * multiplier;
                    } else {
                        self.new_cursor_pos = (height / rows) * rows;
                    }
                }
                PageMovement::Right(_) | PageMovement::Left(_) => {}
                PageMovement::Home => {
                    self.new_cursor_pos = 0;
                }
                PageMovement::End => {
                    self.new_cursor_pos = (height / rows) * rows;
                }
            }
        }
        if self.new_cursor_pos >= self.entries.len() {
            self.new_cursor_pos = self.entries.len().saturating_sub(1);
        }
        let prev_page_no = (self.cursor_pos).wrapping_div(rows);
        let page_no = (self.new_cursor_pos).wrapping_div(rows);

        let top_idx = page_no * rows;
        // returns the **line** of an entry in the ThreadView grid.
        let get_entry_area = |idx: usize| self.content.area().skip_rows(idx).take_rows(1);

        if self.dirty || (page_no != prev_page_no) {
            grid.clear_area(area, crate::conf::value(context, "theme_default"));
            let visibles: Vec<&usize> =
                self.visible_entries.iter().flat_map(|v| v.iter()).collect();

            for (visible_entry_counter, v) in visibles.iter().skip(top_idx).take(rows).enumerate() {
                if visible_entry_counter >= rows {
                    break;
                }
                let idx = *v;

                grid.copy_area(
                    self.content.grid(),
                    area.skip_rows(visible_entry_counter).take_rows(1),
                    self.content.area().skip_rows(*idx).take_rows(1),
                );
            }
            // If cursor position has changed, remove the highlight from the previous
            // position and apply it in the new one.
            self.cursor_pos = self.new_cursor_pos;
            if self.cursor_pos + 1 > visibles.len() {
                self.cursor_pos = visibles.len().saturating_sub(1);
            }
            let idx = *visibles[self.cursor_pos];
            let src_area = get_entry_area(idx);
            let dest_area = area.skip_rows(self.cursor_pos - top_idx).take_rows(1);

            self.highlight_line(grid, dest_area, src_area, idx, context);
            if rows < visibles.len() {
                ScrollBar::default().set_show_arrows(true).draw(
                    grid,
                    area.nth_col(area.width().saturating_sub(1)),
                    context,
                    self.cursor_pos,
                    rows,
                    visibles.len(),
                );
            }
            if top_idx + rows > visibles.len() {
                grid.clear_area(
                    area.skip_rows(visibles.len() - top_idx),
                    crate::conf::value(context, "theme_default"),
                );
            }
        } else {
            let old_cursor_pos = self.cursor_pos;
            self.cursor_pos = self.new_cursor_pos;
            // If cursor position has changed, remove the highlight from the previous
            // position and apply it in the new one.
            let visibles: Vec<&usize> =
                self.visible_entries.iter().flat_map(|v| v.iter()).collect();
            for &idx in &[old_cursor_pos, self.cursor_pos] {
                let entry_idx = *visibles[idx];
                let src_area = get_entry_area(entry_idx);
                let dest_area = area.skip_rows(visibles[..idx].len() - top_idx).take_rows(1);

                self.highlight_line(grid, dest_area, src_area, entry_idx, context);
                if rows < visibles.len() {
                    ScrollBar::default().set_show_arrows(true).draw(
                        grid,
                        area.nth_col(area.width().saturating_sub(1)),
                        context,
                        self.cursor_pos,
                        rows,
                        visibles.len(),
                    );
                }
            }
        }
        context.dirty_areas.push_back(area);
    }

    /// Calculate if a `ThreadLayout` value of `Auto` would be vertical.
    fn calculate_auto_thread_layout_is_vertical(&self) -> bool {
        if self.last_width == 0 {
            return true;
        }
        // Sorry, I'm just hardcoding this for now.
        self.content.area().width().min(self.last_width / 2) > 62
    }

    fn draw_vert(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        if self.entries.is_empty() {
            return;
        }
        let mid = self.content.area().width().min(area.width() / 2);
        if matches!(self.thread_layout, ThreadLayout::Auto)
            && !self.calculate_auto_thread_layout_is_vertical()
        {
            return self.draw_horz(grid, area, context);
        }

        let theme_default = crate::conf::value(context, "theme_default");
        // First draw the thread subject on the first row
        if self.dirty {
            grid.clear_area(area, theme_default);
            let account = &context.accounts[&self.coordinates.0];
            let threads = account.collection.get_threads(self.coordinates.1);
            let thread_root = threads.thread_iter(self.thread_group).next().unwrap().1;
            let thread_node = &threads.thread_nodes()[&thread_root];
            let i = thread_node.message().unwrap_or_else(|| {
                let mut iter_ptr = thread_node.children()[0];
                while threads.thread_nodes()[&iter_ptr].message().is_none() {
                    iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
                }
                threads.thread_nodes()[&iter_ptr].message().unwrap()
            });
            let envelope: EnvelopeRef = account.collection.get_env(i);

            let (_, y) = grid.write_string(
                &envelope.subject(),
                theme_default.fg,
                theme_default.bg,
                theme_default.attrs,
                area,
                None,
                Some(0),
            );
            context.dirty_areas.push_back(area);
            grid.clear_area(area.nth_col(mid), theme_default);
            grid.clear_area(area.skip(mid, y + 1), theme_default);
        };
        let area = area.skip_rows(2);
        let (width, height) = self.content.area().size();
        if height == 0 || width == 0 {
            return;
        }

        match self.focus {
            ThreadViewFocus::None => {
                self.draw_list(grid, area.take_cols(mid.saturating_sub(1)), context);
                self.entries[self.new_expanded_pos].mailview.draw(
                    grid,
                    area.skip_cols(mid + 1),
                    context,
                );
            }
            ThreadViewFocus::Thread => {
                grid.clear_area(area.skip_cols(mid + 1), theme_default);
                self.draw_list(grid, area, context);
            }
            ThreadViewFocus::MailView => {
                self.entries[self.new_expanded_pos]
                    .mailview
                    .draw(grid, area, context);
            }
        }
        context.dirty_areas.push_back(area);
    }

    fn draw_horz(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        if self.entries.is_empty() {
            return;
        }

        let mid = self.content.area().width().min(area.height() / 2);

        let theme_default = crate::conf::value(context, "theme_default");
        // First draw the thread subject on the first row
        if self.dirty {
            grid.clear_area(area, theme_default);
            let account = &context.accounts[&self.coordinates.0];
            let threads = account.collection.get_threads(self.coordinates.1);
            let thread_root = threads.thread_iter(self.thread_group).next().unwrap().1;
            let thread_node = &threads.thread_nodes()[&thread_root];
            let i = thread_node.message().unwrap_or_else(|| {
                let mut iter_ptr = thread_node.children()[0];
                while threads.thread_nodes()[&iter_ptr].message().is_none() {
                    iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
                }
                threads.thread_nodes()[&iter_ptr].message().unwrap()
            });
            let envelope: EnvelopeRef = account.collection.get_env(i);

            grid.write_string(
                &envelope.subject(),
                theme_default.fg,
                theme_default.bg,
                theme_default.attrs,
                area,
                None,
                Some(0),
            );
            context.dirty_areas.push_back(area);
        };

        let area = area.skip_rows(2);
        let (width, height) = self.content.area().size();
        if height == 0 || height == self.cursor_pos || width == 0 {
            return;
        }

        match self.focus {
            ThreadViewFocus::None => {
                self.draw_list(grid, area.take_rows(mid), context);
                self.entries[self.new_expanded_pos].mailview.draw(
                    grid,
                    area.skip_rows(mid + 1),
                    context,
                );
            }
            ThreadViewFocus::Thread => {
                self.dirty = true;
                self.draw_list(grid, area.skip_rows(0), context);
            }
            ThreadViewFocus::MailView => {
                self.entries[self.new_expanded_pos]
                    .mailview
                    .draw(grid, area, context);
            }
        }
        context.dirty_areas.push_back(area);
    }

    fn recalc_visible_entries(&mut self) {
        if self.entries.is_empty() {
            return;
        }
        if self
            .entries
            .iter_mut()
            .fold(false, |flag, e| e.dirty || flag)
        {
            self.visible_entries = self
                .entries
                .iter()
                .enumerate()
                .fold(
                    (vec![Vec::new()], SmallVec::<[_; 8]>::new(), false),
                    |(mut visies, mut stack, is_prev_hidden), (idx, e)| {
                        match (e.hidden, is_prev_hidden) {
                            (true, false) => {
                                visies.last_mut().unwrap().push(idx);
                                stack.push(e.indentation);
                                (visies, stack, e.hidden)
                            }
                            (true, true)
                                if !stack.is_empty() && stack[stack.len() - 1] == e.indentation =>
                            {
                                visies.push(vec![idx]);
                                (visies, stack, e.hidden)
                            }
                            (true, true) => (visies, stack, e.hidden),
                            (false, true)
                                if stack[stack.len() - 1] >= e.indentation
                                    && stack.len() > 1
                                    && stack[stack.len() - 2] >= e.indentation =>
                            {
                                // [ref:FIXME]: pop all until e.indentation
                                visies.push(vec![idx]);
                                stack.pop();
                                (visies, stack, e.hidden)
                            }
                            (false, true) if stack[stack.len() - 1] >= e.indentation => {
                                visies.push(vec![idx]);
                                stack.pop();
                                (visies, stack, e.hidden)
                            }
                            (false, true) => (visies, stack, is_prev_hidden),
                            (false, false) => {
                                visies.last_mut().unwrap().push(idx);
                                (visies, stack, e.hidden)
                            }
                        }
                    },
                )
                .0;
        }
        if self.reversed {
            self.visible_entries.reverse()
        }
    }

    /// Current position in self.entries (not in drawn entries which might
    /// exclude nonvisible ones)
    fn current_pos(&self) -> Option<usize> {
        self.visible_entries
            .iter()
            .flat_map(|v| v.iter())
            .nth(self.new_cursor_pos)
            .copied()
    }
}

impl std::fmt::Display for ThreadView {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
        if let Some(e) = self.entries.first() {
            e.mailview.fmt(fmt)
        } else {
            write!(fmt, "view thread")
        }
    }
}

impl Component for ThreadView {
    fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
        if self.entries.is_empty() {
            self.set_dirty(false);
        }
        if !self.is_dirty() {
            return;
        }
        self.last_width = area.width();

        // If user has selected another mail to view, change to it
        if self.new_expanded_pos != self.expanded_pos {
            self.expanded_pos = self.new_expanded_pos;
        }

        if self.entries.len() == 1 {
            self.entries[self.new_expanded_pos]
                .mailview
                .draw(grid, area, context);
        } else if matches!(self.thread_layout, ThreadLayout::Horizontal) {
            self.draw_horz(grid, area, context);
        } else {
            self.draw_vert(grid, area, context);
        }
        self.set_dirty(false);
    }

    fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
        if matches!(
            (&event, self.entries.is_empty()),
            (UIEvent::Action(Listing(OpenInNewTab)), false)
        ) {
            // Handle this before self.mailview does
            let mut new_tab = Self::new(
                self.coordinates,
                self.thread_group,
                Some(self.entries[self.expanded_pos].msg_hash),
                false,
                Some(self.focus),
                context,
            );
            new_tab.set_dirty(true);
            context
                .replies
                .push_back(UIEvent::Action(Tab(New(Some(Box::new(new_tab))))));
            return true;
        }

        if matches!(
            self.focus,
            ThreadViewFocus::None | ThreadViewFocus::MailView
        ) && !self.entries.is_empty()
            && self.entries[self.new_expanded_pos]
                .mailview
                .process_event(event, context)
        {
            return true;
        }

        let shortcuts = self.shortcuts(context);
        match *event {
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_layout"]) =>
            {
                if self.entries.len() > 1 {
                    match self.thread_layout {
                        ThreadLayout::Auto if self.calculate_auto_thread_layout_is_vertical() => {
                            self.thread_layout = ThreadLayout::Horizontal;
                        }
                        ThreadLayout::Auto => {
                            self.thread_layout = ThreadLayout::Vertical;
                        }
                        ThreadLayout::Horizontal => {
                            self.thread_layout = ThreadLayout::Auto;
                        }
                        ThreadLayout::Vertical => {
                            self.thread_layout = ThreadLayout::Horizontal;
                        }
                    }
                    context
                        .replies
                        .push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(format!(
                            "thread_layout set to {}",
                            toml::Value::try_from(self.thread_layout).expect("Cannot fail")
                        ))));
                    self.set_dirty(true);
                }
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["scroll_up"]) =>
            {
                if self.cursor_pos > 0 {
                    self.new_cursor_pos = self.new_cursor_pos.saturating_sub(1);
                    self.set_dirty(true);
                }
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["scroll_down"]) =>
            {
                let height = self.visible_entries.iter().flat_map(|v| v.iter()).count();
                if height > 0 && self.new_cursor_pos + 1 < height {
                    self.new_cursor_pos += 1;
                    self.set_dirty(true);
                }
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["prev_page"]) =>
            {
                self.movement = Some(PageMovement::PageUp(1));
                self.set_dirty(true);
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["next_page"]) =>
            {
                self.movement = Some(PageMovement::PageDown(1));
                self.set_dirty(true);
                true
            }
            UIEvent::Input(ref k) if shortcut!(k == shortcuts[Shortcuts::GENERAL]["home_page"]) => {
                self.movement = Some(PageMovement::Home);
                self.set_dirty(true);
                true
            }
            UIEvent::Input(ref k) if shortcut!(k == shortcuts[Shortcuts::GENERAL]["end_page"]) => {
                self.movement = Some(PageMovement::End);
                self.set_dirty(true);
                true
            }
            UIEvent::Input(ref k)
                if shortcut!(k == shortcuts[Shortcuts::GENERAL]["open_entry"]) =>
            {
                if self.entries.len() > 1 {
                    if let Some(new_expanded_pos) = self.current_pos() {
                        self.new_expanded_pos = new_expanded_pos;
                        self.expanded_pos = new_expanded_pos;
                        if matches!(self.focus, ThreadViewFocus::Thread) {
                            self.focus = ThreadViewFocus::None;
                        }
                        self.set_dirty(true);
                    }
                }
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_mailview"]) =>
            {
                self.focus = match self.focus {
                    ThreadViewFocus::None | ThreadViewFocus::MailView => ThreadViewFocus::Thread,
                    ThreadViewFocus::Thread => ThreadViewFocus::None,
                };
                self.set_dirty(true);
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_threadview"]) =>
            {
                self.focus = match self.focus {
                    ThreadViewFocus::None | ThreadViewFocus::Thread => ThreadViewFocus::MailView,
                    ThreadViewFocus::MailView => ThreadViewFocus::None,
                };
                self.set_dirty(true);
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["reverse_thread_order"]) =>
            {
                if !self.entries.is_empty() {
                    self.reversed = !self.reversed;
                    let expanded_hash = self.entries[self.expanded_pos].msg_hash;
                    self.initiate(Some(expanded_hash), false, context);
                    self.set_dirty(true);
                }
                true
            }
            UIEvent::Input(ref key)
                if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["collapse_subtree"]) =>
            {
                if self.entries.is_empty() {
                    return true;
                }
                if let Some(current_pos) = self.current_pos() {
                    self.entries[current_pos].hidden = !self.entries[current_pos].hidden;
                    self.entries[current_pos].dirty = true;
                    {
                        let visible_entries: Vec<&usize> =
                            self.visible_entries.iter().flat_map(|v| v.iter()).collect();
                        // search_old_cursor_pos
                        self.new_cursor_pos =
                            (|entries: Vec<&usize>, x: usize| {
                                let mut low = 0;
                                let mut high = entries.len() - 1;
                                while low <= high {
                                    let mid = low + (high - low) / 2;
                                    if *entries[mid] == x {
                                        return mid;
                                    }
                                    if x > *entries[mid] {
                                        low = mid + 1;
                                    } else {
                                        high = mid - 1;
                                    }
                                }
                                high + 1 //mid
                            })(visible_entries, self.cursor_pos);
                    }
                    self.cursor_pos = self.new_cursor_pos;
                    self.recalc_visible_entries();
                    self.set_dirty(true);
                }
                true
            }
            UIEvent::Resize | UIEvent::VisibilityChange(true) => {
                self.set_dirty(true);
                false
            }
            UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
                let account = &context.accounts[&self.coordinates.0];
                for e in self.entries.iter_mut() {
                    if e.msg_hash == *old_hash {
                        e.msg_hash = *new_hash;
                        let seen: bool = account.collection.get_env(*new_hash).is_seen();
                        e.dirty = e.seen != seen;
                        e.seen = seen;
                        e.mailview.process_event(
                            &mut UIEvent::EnvelopeRename(*old_hash, *new_hash),
                            context,
                        );
                        self.set_dirty(true);
                        break;
                    }
                }
                false
            }
            UIEvent::EnvelopeUpdate(ref env_hash) => {
                let account = &context.accounts[&self.coordinates.0];
                for e in self.entries.iter_mut() {
                    if e.msg_hash == *env_hash {
                        let seen: bool = account.collection.get_env(*env_hash).is_seen();
                        e.dirty = e.seen != seen;
                        e.seen = seen;
                        e.mailview
                            .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
                        self.set_dirty(true);
                        break;
                    }
                }
                false
            }
            UIEvent::Input(ref key)
                if context
                    .settings
                    .shortcuts
                    .thread_view
                    .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
                    }) =>
            {
                true
            }
            _ => {
                // [ref:VERIFY]: In what case do we need to forward a handled UIEvent to all
                // entries?
                if self
                    .entries
                    .iter_mut()
                    .any(|entry| entry.mailview.process_event(event, context))
                {
                    return true;
                }
                false
            }
        }
    }

    fn is_dirty(&self) -> bool {
        self.dirty
            || (!matches!(self.focus, ThreadViewFocus::Thread)
                && !self.entries.is_empty()
                && self.entries[self.new_expanded_pos].mailview.is_dirty())
    }

    fn set_dirty(&mut self, value: bool) {
        self.dirty = value;
        if let Some(entry) = self.entries.get_mut(self.new_expanded_pos) {
            entry.mailview.set_dirty(value);
        }
    }

    fn shortcuts(&self, context: &Context) -> ShortcutMaps {
        let mut map = if !self.entries.is_empty() {
            self.entries[self.new_expanded_pos]
                .mailview
                .shortcuts(context)
        } else {
            ShortcutMaps::default()
        };

        map.insert(
            Shortcuts::GENERAL,
            mailbox_settings!(
                context[self.coordinates.0][&self.coordinates.1]
                    .shortcuts
                    .general
            )
            .key_values(),
        );
        map.insert(
            Shortcuts::THREAD_VIEW,
            mailbox_settings!(
                context[self.coordinates.0][&self.coordinates.1]
                    .shortcuts
                    .thread_view
            )
            .key_values(),
        );

        map
    }

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

    fn kill(&mut self, id: ComponentId, context: &mut Context) {
        debug_assert!(self.id == id);
        context
            .replies
            .push_back(UIEvent::Action(Tab(Kill(self.id))));
    }
}
