//! Main app view

#[cfg(test)]
mod test;

use crate::{checkbox, diff, el, theme, Theme};
use inflorescence_iced_widget::{dir_picker, nav_scrollable, report};
use inflorescence_model::model::{
    IndexSet, Job, Log, ManagingRepoSubState, PickingProjectSelection,
    ReadyState, RecordChanges,
};
use inflorescence_model::{action, model, selection, to_record};
use libflorescence::identity::Id;
use libflorescence::prelude::*;
use libflorescence::{file, repo, store};

use iced::widget::{
    button, column, container, row, text, text_editor, text_input, Button,
    Column,
};
use iced::{
    alignment, font, window, Alignment, Element, Font, Length, Padding,
};

use std::borrow::Cow;
use std::cmp;
use std::path::{Path, PathBuf};

const SPACING: u32 = 10;

/// Default min. width of a main view column
const DEFAULT_MIN_COL_WIDTH: usize = 512;

#[derive(Debug, Clone)]
pub enum Msg {
    /// Actions bound to keys
    Action(action::FilteredMsg),
    PickingRepoDir(dir_picker::Msg),
    EditRecordMsg(String),
    EditRecordDesc(text_editor::Action),
    EditForkChannelName(String),
    UnfilteredSelection(selection::UnfilteredMsg),
    ToRecord(to_record::Msg),
    OpenProject(PathBuf),
    PickNewProject,
    SelectIdentity(usize),
    SubMenuPushSelectRemote(String),
    SubMenuPullSelectRemote(String),
    SubMenuCompareRemoteSelectRemote(String),
    SubMenuCompareRemoteInputRemoteChannel(String),
}

pub fn main<'a, F>(
    state: &'a model::State,
    get_diff: F,
    _window_id: window::Id,
) -> Element<'a, Msg, Theme>
where
    F: Fn(file::IdHash) -> Option<&'a diff::File>,
{
    let model::State {
        window_size,
        // TODO: use scale
        window_scale: _,
        sub,
        allowed_actions,
        sub_menu,
        report,
    } = state;

    if let Some(sub) = sub_menu {
        match sub {
            model::SubMenu::Push {
                opt: Some(opt),
                remote: _,
            } => match opt {
                model::PushOption::SelectingRemote { remote } => {
                    return push_selecting_remote(
                        state,
                        *window_size,
                        allowed_actions,
                        report,
                        remote,
                        sub_menu,
                    );
                }
            },
            model::SubMenu::Pull {
                opt: Some(opt),
                remote: _,
            } => match opt {
                model::PullOption::SelectingRemote { remote } => {
                    return pull_selecting_remote(
                        state,
                        *window_size,
                        allowed_actions,
                        report,
                        remote,
                        sub_menu,
                    );
                }
            },
            model::SubMenu::CompareRemote {
                opt: Some(opt),
                remote: _,
                remote_channel: _,
            } => match opt {
                model::CompareRemoteOption::SelectingRemote { remote } => {
                    return compare_remote_selecting_remote(
                        state,
                        *window_size,
                        allowed_actions,
                        report,
                        remote,
                        sub_menu,
                    );
                }
                model::CompareRemoteOption::InputingRemoteChannel {
                    channel: remote_channel,
                } => {
                    return compare_remote_input_remote_channel(
                        *window_size,
                        allowed_actions,
                        report,
                        remote_channel,
                        sub_menu,
                    );
                }
            },
            model::SubMenu::Push { opt: None, .. }
            | model::SubMenu::Pull { opt: None, .. }
            | model::SubMenu::ResetChange
            | model::SubMenu::InitRepo { path: _ }
            | model::SubMenu::ImportFromGit { path: _ }
            | model::SubMenu::Add { recursive: _ }
            | model::SubMenu::CompareRemote { opt: None, .. } => {}
        }
    }

    match sub {
        model::SubState::PickingProject(state) => picking_project(
            state,
            *window_size,
            allowed_actions,
            report,
            sub_menu,
        ),
        model::SubState::PickingRepoDir(state) => picking_repo_dir(
            state,
            *window_size,
            allowed_actions,
            report,
            sub_menu,
        ),
        model::SubState::ManagingRepo(state) => managing_repo(
            state,
            get_diff,
            *window_size,
            allowed_actions,
            report,
            sub_menu,
        ),
    }
}

fn picking_project<'a>(
    state: &'a model::PickingProject,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
    let model::PickingProject {
        projects,
        is_blocking,
        selection,
        projects_nav,
    } = state;

    let main = if let Some(projects) = projects {
        el(column([
            el(column([el(row([
                el(button(text("Find or create a new project"))
                    .on_press(Msg::PickNewProject)
                    .class(
                        if matches!(
                            selection,
                            PickingProjectSelection::FindOrCreate
                        ) {
                            theme::Button::Selected
                        } else {
                            theme::Button::Normal
                        },
                    )),
                el(text("or pick a known project below")),
            ])
            .spacing(SPACING)
            .align_y(alignment::Vertical::Center))])),
            el(nav_scrollable(
                projects_nav,
                projects.iter().enumerate().map(
                    |(ix, store::Project {
                         last_closed_time: _,
                         path,
                     })| {
                        el(button(text(format!("{}", path.to_string_lossy())))
                            .on_press_with(|| Msg::OpenProject(path.clone()))
                            .class(
                                if matches!(
                                    selection,
                                    PickingProjectSelection::Existing { ix: selection } if *selection == ix
                                ) {
                                    theme::Button::Selected
                                } else {
                                    theme::Button::Normal
                                },
                            )
                        )
                    },
                ),
            ).class(if matches!(selection, PickingProjectSelection::Existing { .. }) {
                theme::Scrollable::Selected
            } else {
                theme::Scrollable::Normal
            }).width(Length::Fill).height(Length::Fill)),
        ])
        .spacing(SPACING))
    } else {
        el(text(if *is_blocking {
            "Waiting for a lock release on projects file..."
        } else {
            "Loading projects..."
        }))
    };
    let main = el(container(main).width(Length::Fill).height(Length::Fill));

    add_actions_and_report(
        None,
        main,
        window_size,
        allowed_actions,
        report,
        sub_menu,
        false,
    )
}

fn picking_repo_dir<'a>(
    state: &'a model::PickingRepoDir,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
    let model::PickingRepoDir {
        picker,
        waiting_to_init,
    } = state;
    let main = if let Some(init_kind) = waiting_to_init {
        el(column([
            el(text("Waiting to finish initializing Pijul...")),
            el(text(match init_kind {
                model::ProjectInitKind::New => "",
                model::ProjectInitKind::ImportFromGit => {
                    "Importing from Git is still experimental and it might take a while"
                }
            })),
        ])
        .spacing(SPACING)
        .width(Length::Fill)
        .height(Length::Fill))
    } else {
        el(column([
            el(text("Select project directory:")),
            dir_picker::view(
                picker,
                theme::Container::FadedBorder,
                theme::Container::NavSelectedSection,
                theme::Button::Normal,
                theme::Button::Selected,
                theme::Scrollable::Normal,
                theme::Scrollable::Selected,
                theme::Text::SlightlyFaded,
            )
            .map(Msg::PickingRepoDir),
        ])
        .spacing(SPACING)
        .width(Length::Fill)
        .height(Length::Fill))
    };

    add_actions_and_report(
        None,
        main,
        window_size,
        allowed_actions,
        report,
        sub_menu,
        false,
    )
}

fn managing_repo<'a, F>(
    state: &'a model::ManagingRepo,
    get_diff: F,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme>
where
    F: Fn(file::IdHash) -> Option<&'a diff::File>,
{
    let model::ManagingRepo { repo_path, sub } = state;

    // TODO: show report `SubState::SelectingId`
    let inner = match sub {
        ManagingRepoSubState::Loading { .. } => {
            let main = el(container(text("Loading..."))
                .width(Length::Fill)
                .height(Length::Fill));

            add_actions_and_report(
                None,
                main,
                window_size,
                allowed_actions,
                report,
                sub_menu,
                false,
            )
        }
        ManagingRepoSubState::SelectingIdentity {
            ids,
            selection_ix,
            selection_nav,
            confirmed_selection_ix,
            repo: _,
        } => {
            let main = el(container(view_selecting_identity(
                ids,
                confirmed_selection_ix.unwrap_or_else(|| *selection_ix),
                selection_nav,
            ))
            .width(Length::Fill)
            .height(Length::Fill));

            add_actions_and_report(
                None,
                main,
                window_size,
                allowed_actions,
                report,
                sub_menu,
                false,
            )
        }
        ManagingRepoSubState::Ready(state) => view_ready(
            window_size,
            repo_path,
            state,
            get_diff,
            allowed_actions,
            report,
            sub_menu,
        ),
        ManagingRepoSubState::NoIdFound { repo: _ } => {
            let main = el(container(text("No Pijul identity found. Create a new one in with Pijul CLI and then reload it here."))
                .width(Length::Fill)
                .height(Length::Fill));

            add_actions_and_report(
                None,
                main,
                window_size,
                allowed_actions,
                report,
                sub_menu,
                false,
            )
        }
    };

    el(container(inner)
        .class(theme::Container::AppBg)
        .width(Length::Fill)
        .height(Length::Fill)
        .padding(Padding::from([2, 5])))
}

fn push_selecting_remote<'a>(
    state: &'a model::State,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    remote: &'a Option<String>,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
    view_selecting_remote(
        state,
        window_size,
        allowed_actions,
        report,
        remote,
        sub_menu,
        Msg::SubMenuPushSelectRemote,
    )
}

fn pull_selecting_remote<'a>(
    state: &'a model::State,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    remote: &'a Option<String>,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
    view_selecting_remote(
        state,
        window_size,
        allowed_actions,
        report,
        remote,
        sub_menu,
        Msg::SubMenuPullSelectRemote,
    )
}

fn compare_remote_selecting_remote<'a>(
    state: &'a model::State,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    remote: &'a Option<String>,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
    view_selecting_remote(
        state,
        window_size,
        allowed_actions,
        report,
        remote,
        sub_menu,
        Msg::SubMenuCompareRemoteSelectRemote,
    )
}

fn compare_remote_input_remote_channel<'a>(
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    remote: &'a Option<String>,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
    view_input_remote_channel(
        window_size,
        allowed_actions,
        report,
        remote,
        sub_menu,
        Msg::SubMenuCompareRemoteInputRemoteChannel,
    )
}

fn view_selecting_remote<'a>(
    state: &'a model::State,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    remote: &'a Option<String>,
    sub_menu: &'a Option<model::SubMenu>,
    on_select: impl FnOnce(String) -> Msg + Copy + 'static,
) -> Element<'a, Msg, Theme> {
    let main = if let model::SubState::ManagingRepo(state) = &state.sub
        && let Some(model::ReadyState { repo, .. }) = model::is_ready(state)
    {
        let repo::State {
            remotes: repo::Remotes { default, other },
            ..
        } = repo;
        let mut cols =
            Vec::with_capacity(1 + default.iter().len() + other.len());
        cols.push(view_header("Select remote:"));
        for name in default.iter().chain(other) {
            cols.push(el(button(text(name))
                .on_press_with(move || on_select(name.clone()))
                .class(if Some(name) == remote.as_ref() {
                    theme::Button::Selected
                } else {
                    theme::Button::Normal
                })));
        }
        el(column(cols))
    } else {
        el(row([]))
    };

    let main = el(container(main).width(Length::Fill).height(Length::Fill));

    add_actions_and_report(
        None,
        main,
        window_size,
        allowed_actions,
        report,
        sub_menu,
        false,
    )
}

fn view_input_remote_channel<'a>(
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    channel: &'a Option<String>,
    sub_menu: &'a Option<model::SubMenu>,
    on_input: impl Fn(String) -> Msg + Copy + 'static,
) -> Element<'a, Msg, Theme> {
    let main = el(column([
        view_header("Name of a remote channel:"),
        el(text("Hint: to view a changes from e.g. a discussion number 42 enter \"name:42\".")),
        el(text_input(
            "",
            channel
                .as_ref()
                .map(Cow::Borrowed)
                .unwrap_or_default()
                .as_ref(),
        )
        .on_input(on_input)),
    ]));

    let main = el(container(main).width(Length::Fill).height(Length::Fill));

    add_actions_and_report(
        None,
        main,
        window_size,
        allowed_actions,
        report,
        sub_menu,
        false,
    )
}

fn view_selecting_identity<'a>(
    ids: &'a [Id],
    selection_ix: usize,
    selection_nav: &'a nav_scrollable::State,
) -> Element<'a, Msg, Theme> {
    el(column([
        el(text("Select identity:")),
        el(nav_scrollable(
            selection_nav,
            ids.iter().enumerate().map(|(ix, id)| {
                el(button(text(id.name.to_string()))
                    .on_press(Msg::SelectIdentity(ix))
                    .class(if ix == selection_ix {
                        theme::Button::Selected
                    } else {
                        theme::Button::Normal
                    }))
            }),
        )),
    ]))
}

fn add_actions_and_report<'a>(
    above_actions: Option<Column<'a, Msg, Theme>>,
    main: Element<'a, Msg, Theme>,
    window_size: iced::Size,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    sub_menu: &'a Option<model::SubMenu>,
    force_highlight_actions: bool,
) -> Element<'a, Msg, Theme> {
    let mut actions_inner =
        above_actions.unwrap_or_else(|| column([]).spacing(4));

    if let Some(sub) = sub_menu {
        match sub {
            model::SubMenu::Push { opt: _, remote } => {
                if let Some(remote) = remote {
                    actions_inner = actions_inner
                        .push(el(text(format!("Remote: {remote}"))));
                }
            }
            model::SubMenu::Pull { opt: _, remote } => {
                if let Some(remote) = remote {
                    actions_inner = actions_inner
                        .push(el(text(format!("Remote: {remote}"))));
                }
            }
            model::SubMenu::CompareRemote {
                opt: _,
                remote,
                remote_channel,
            } => {
                if let Some(remote) = remote {
                    actions_inner = actions_inner
                        .push(el(text(format!("Remote: {remote}"))));
                }
                if let Some(channel) = remote_channel {
                    actions_inner = actions_inner
                        .push(el(text(format!("Remote channel: {channel}"))));
                }
            }
            model::SubMenu::ResetChange
            | model::SubMenu::InitRepo { path: _ }
            | model::SubMenu::ImportFromGit { path: _ }
            | model::SubMenu::Add { recursive: _ } => {}
        }
    }

    let highlight_actions = force_highlight_actions || sub_menu.is_some();
    let actions_inner = actions_inner.push(view_actions(allowed_actions));
    let actions = el(container(actions_inner)
        .class(if highlight_actions {
            theme::Container::ActionsHighlightBg
        } else {
            theme::Container::ActionsBg
        })
        .width(Length::Fill)
        .height(Length::Shrink)
        .padding(Padding::from([2, 5])));

    let main = overlay_report(main, report, &window_size);

    el(column([main, actions]).spacing(SPACING))
}

fn view_ready<'a, F>(
    window_size: iced::Size,
    _repo_path: &'a Path,
    state: &'a ReadyState,
    get_diff: F,
    allowed_actions: &'a [action::Binding],
    report: &'a report::Container,
    sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme>
where
    F: Fn(file::IdHash) -> Option<&'a diff::File>,
{
    let ReadyState {
        user_id: _,
        repo:
            repo::State {
                dir_name,
                channel,
                other_channels,
                untracked_files,
                changed_files,
                short_log,
                remotes: _,
            },
        selection,
        navigation,
        record_changes: record_msg,
        forking_channel_name,
        logs,
        to_record,
        jobs,
        record_dichotomy,
    } = state;
    let selection = selection::unify(selection);

    let has_other_channels = !other_channels.is_empty();

    let view_repo_info = || {
        el(row([
            el(text(dir_name)),
            el(text(", channel: ")),
            el(button(text(channel))
                .on_press_maybe(has_other_channels.then_some(Msg::Action(
                    action::FilteredMsg::SelectChannel,
                )))),
        ])
        .align_y(alignment::Vertical::Center))
    };

    let view_untracked_files = || {
        untracked_files.iter().enumerate().map(|(ix, file)| {
            let selection = match selection {
                selection::Unified::Status(Some(
                    selection::Status::UntrackedFile {
                        ix: selected_ix,
                        diff_selected,
                        ..
                    },
                )) if &ix == selected_ix => {
                    if *diff_selected {
                        HierarchicalSelection::ChildSelected
                    } else {
                        HierarchicalSelection::Selected
                    }
                }
                _ => HierarchicalSelection::NotSelected,
            };
            el(button(
                text(file.to_string())
                    .shaping(text::Shaping::Advanced)
                    .wrapping(text::Wrapping::WordOrGlyph),
            )
            .on_press(Msg::UnfilteredSelection(
                selection::UnfilteredMsg::Select(
                    selection::Select::UntrackedFile {
                        ix,
                        path: file.clone(),
                    },
                ),
            ))
            .class(hierarchical_button_class(selection)))
        })
    };

    let view_changed_files = || {
        changed_files
            .iter()
            .enumerate()
            .map(|(ix, (file, _diffs))| {
                let state = to_record::determine_file(to_record, file);
                let to_record_toggle = el(checkbox::three_way(state)
                    .on_press_with(|| {
                        Msg::ToRecord(to_record::Msg::ToggleFile {
                            path: file.clone(),
                        })
                    }));
                let selection = match selection {
                    selection::Unified::Status(Some(
                        selection::Status::ChangedFile {
                            ix: selected_ix,
                            diff_selected,
                            ..
                        },
                    )) if &ix == selected_ix => {
                        if *diff_selected {
                            HierarchicalSelection::ChildSelected
                        } else {
                            HierarchicalSelection::Selected
                        }
                    }
                    _ => HierarchicalSelection::NotSelected,
                };
                let file_name_view = el(button(
                    text(file.to_string())
                        .shaping(text::Shaping::Advanced)
                        .wrapping(text::Wrapping::WordOrGlyph),
                )
                .on_press(Msg::UnfilteredSelection(
                    selection::UnfilteredMsg::Select(
                        selection::Select::ChangedFile {
                            ix,
                            path: file.clone(),
                        },
                    ),
                ))
                .class(hierarchical_button_class(selection)));
                el(row([to_record_toggle, file_name_view]))
            })
    };

    let view_status_log = || {
        short_log.iter().enumerate().map(|(ix, entry)| {
            let selection = match selection {
                selection::Unified::Status(Some(
                    selection::Status::LogChange(selection::LogChange {
                        ix: selected_ix,
                        file,
                        ..
                    }),
                )) if &ix == selected_ix => {
                    if file.is_some() {
                        HierarchicalSelection::ChildSelected
                    } else {
                        HierarchicalSelection::Selected
                    }
                }
                _ => HierarchicalSelection::NotSelected,
            };
            view_log_change_selection(ix, entry, selection)
        })
    };

    let record_msg_editor = || {
        if let Some(RecordChanges::Typing { msg, desc }) = record_msg.as_ref() {
            el(column([
                el(column([
                    view_header("Short message:"),
                    el(text_input("Type short message here...", msg)
                        .on_input(Msg::EditRecordMsg)),
                ])),
                view_header("Longer description (optional):"),
                el(column([el(text_editor(desc)
                    .placeholder("Type longer description here...")
                    .on_action(Msg::EditRecordDesc))])),
                el(column([el(row([
                    el(button(text("Save")).on_press(Msg::Action(
                        action::FilteredMsg::SaveRecord,
                    ))),
                    el(button(text("Postpone")).on_press(Msg::Action(
                        action::FilteredMsg::PostponeRecord,
                    ))),
                    el(button(text("Discard")).on_press(Msg::Action(
                        action::FilteredMsg::DiscardRecord,
                    ))),
                ]))])),
            ])
            .spacing(SPACING))
        } else {
            el(row([]))
        }
    };

    let selection_details = || {
        if let selection::Unified::Status(selection) = selection {
            match selection {
                Some(selection::Status::UntrackedFile {
                    ix: _,
                    path,
                    diff_selected,
                }) => {
                    let file_id =
                        file::id_parts_hash(path, file::Kind::Untracked);
                    let diff = get_diff(file_id);
                    let state = navigation.files_diffs.diffs.get(&file_id);
                    let nav = &navigation.files_diffs.diffs_nav;
                    let diffs = match diff.zip(state) {
                        Some((file, state)) => diff::view(
                            state,
                            nav,
                            file,
                            Some(path),
                            *diff_selected,
                            false,
                            to_record,
                        ),
                        None => el(text("Loading diff...")),
                    };

                    el(column([
                        view_header(format!(
                            "Untracked {} contents:",
                            path.raw
                        )),
                        diffs,
                    ])
                    .spacing(SPACING))
                }
                Some(selection::Status::ChangedFile {
                    path,
                    ix: _,
                    diff_selected,
                }) => {
                    let file_id =
                        file::id_parts_hash(path, file::Kind::Changed);
                    let diff = get_diff(file_id);
                    let state = navigation.files_diffs.diffs.get(&file_id);
                    let nav = &navigation.files_diffs.diffs_nav;
                    let diffs = match diff.zip(state) {
                        Some((file, state)) => diff::view(
                            state,
                            nav,
                            file,
                            Some(path),
                            *diff_selected,
                            true,
                            to_record,
                        ),
                        None => el(text("Loading diff...")),
                    };

                    el(column([
                        view_header(format!("{} diff:", path.raw)),
                        diffs,
                    ])
                    .spacing(SPACING))
                }
                Some(selection::Status::LogChange(selection::LogChange {
                    ix,
                    hash: _,
                    message: _,
                    file,
                })) => {
                    let entry = short_log.get(*ix).unwrap();
                    let change_selected = match file.as_ref() {
                        Some(selection::LogChangeFileSelection {
                            ix: _,
                            path: _,
                            diff_selected,
                        }) => !*diff_selected,
                        _ => false,
                    };

                    let files = entry.file_paths.iter().enumerate().map(
                        |(ix, path)| {
                            let selection = match file {
                                Some(selection::LogChangeFileSelection {
                                    path: selected_path,
                                    diff_selected,
                                    ..
                                }) if selected_path == path => {
                                    if *diff_selected {
                                        HierarchicalSelection::ChildSelected
                                    } else {
                                        HierarchicalSelection::Selected
                                    }
                                }

                                _ => HierarchicalSelection::NotSelected,
                            };
                            el(button(text(path))
                                .on_press_with(move || {
                                    Msg::UnfilteredSelection(
                                        selection::UnfilteredMsg::Select(
                                            selection::Select::LogChangeFile {
                                                ix,
                                                path: path.clone(),
                                            },
                                        ),
                                    )
                                })
                                .class(hierarchical_button_class(selection)))
                        },
                    );
                    let nav = &navigation.status_logs_navs.files_nav;
                    let files = el(nav_scrollable(nav, files)
                        .class(if change_selected {
                            theme::Scrollable::Selected
                        } else {
                            theme::Scrollable::Normal
                        })
                        .width(Length::Fill)
                        .height(Length::Fill));

                    el(column([view_log_change_header(entry), files])
                        .width(Length::Fill)
                        .height(Length::Fill)
                        .spacing(SPACING))
                }
                None => el(row([])),
            }
        } else {
            el(row([]))
        }
    };

    // The number of visible columns depends on the screen width. Any cols on
    // left that don't fit will be hidden
    let max_visible_cols = cmp::max(
        2,
        window_size.width.floor() as usize / DEFAULT_MIN_COL_WIDTH,
    );

    let depth = if let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
    {
        1
    } else {
        match selection {
            selection::Unified::EntireLog(Some(selection::LogChange {
                file,
                ..
            })) => {
                if file.is_none() {
                    1
                } else {
                    2
                }
            }
            selection::Unified::Status(Some(selection)) => match selection {
                selection::Status::UntrackedFile { .. }
                | selection::Status::ChangedFile { .. }
                | selection::Status::LogChange(selection::LogChange {
                    file: None,
                    ..
                }) => 1,
                selection::Status::LogChange(selection::LogChange {
                    file: Some(_),
                    ..
                }) => 2,
            },
            selection::Unified::Channel(Some(selection)) => match selection {
                selection::Channel { log: None, .. } => 1,
                selection::Channel {
                    log: Some(selection::LogChange { file: None, .. }),
                    ..
                } => 2,
                selection::Channel {
                    log: Some(selection::LogChange { file: Some(_), .. }),
                    ..
                } => 3,
            },
            selection::Unified::CompareRemote(Some(
                selection::CompareRemote { file, .. },
            )) => {
                if file.is_none() {
                    1
                } else {
                    2
                }
            }
            selection::Unified::Status(None)
            | selection::Unified::Channel(None)
            | selection::Unified::EntireLog(None)
            | selection::Unified::CompareRemote(None) => 0,
        }
    };

    let hidden_cols = (depth + 1_usize).saturating_sub(max_visible_cols);
    // dbg!(
    //     window_size.width,
    //     max_visible_cols,
    //     depth,
    //     hidden_cols
    // );

    let changed_files_header = el(text(if changed_files.is_empty() {
        "No changed files"
    } else {
        "Changed files:"
    }));

    // Only show overall toggle if there is more than one changed file
    let changed_files_header_with_toggle = if changed_files.len() > 1 {
        let to_record_toggle = el(checkbox::three_way(to_record.overall)
            .on_press_with(|| Msg::ToRecord(to_record::Msg::ToggleOverall)));

        el(container(el(row([to_record_toggle, changed_files_header])
            .align_y(Alignment::Center)
            .spacing(SPACING)))
        .padding(Padding::ZERO.top(SPACING)))
    } else {
        changed_files_header
    };

    let status_nav_children = || {
        [el(container(el(text(if untracked_files.is_empty() {
            "No untracked files"
        } else {
            "Untracked files:"
        })))
        .padding(Padding::ZERO.top(SPACING)))]
        .into_iter()
        .chain(view_untracked_files())
        .chain([changed_files_header_with_toggle])
        .chain(view_changed_files())
        .chain([el(container(el(text("Recent records:")))
            .padding(Padding::ZERO.top(SPACING)))])
        .chain(view_status_log())
    };

    let status_selected =
        || match selection {
            selection::Unified::Status(Some(
                selection::Status::UntrackedFile { diff_selected, .. },
            )) => !diff_selected,
            selection::Unified::Status(Some(
                selection::Status::ChangedFile { diff_selected, .. },
            )) => !diff_selected,
            selection::Unified::Status(Some(selection::Status::LogChange(
                selection::LogChange { file, .. },
            ))) => file.is_none(),
            selection::Unified::CompareRemote(Some(
                selection::CompareRemote { file, .. },
            )) => file.is_none(),
            selection::Unified::Status(None) => true,
            selection::Unified::Channel(_)
            | selection::Unified::EntireLog(_)
            | selection::Unified::CompareRemote(_) => false,
        };

    let status_col_0 = || {
        el(column([
            view_repo_info(),
            el(
                nav_scrollable(&navigation.status_nav, status_nav_children())
                    .class(if status_selected() {
                        theme::Scrollable::Selected
                    } else {
                        theme::Scrollable::Normal
                    })
                    .width(Length::Fill)
                    .height(Length::Fill),
            ),
        ])
        .width(Length::Fill)
        .height(Length::Fill))
    };

    let status_col_1 = || {
        el(column([
            el(column([record_msg_editor(), selection_details()])
                .width(Length::Fill)
                .height(Length::Fill)),
            if hidden_cols == 1 {
                el(button(row([el(
                    text("← Status").shaping(text::Shaping::Advanced)
                )]))
                .on_press(Msg::Action(
                    action::FilteredMsg::Selection(selection::Msg::PressDir(
                        selection::Dir::Left,
                    )),
                )))
            } else {
                el(row([]))
            },
        ])
        .width(Length::Fill)
        .height(Length::Fill)
        .spacing(SPACING))
    };

    let status_col_2 = || match selection {
        selection::Unified::Status(Some(selection::Status::LogChange(
            selection::LogChange {
                ix: _,
                hash,
                message: _,
                file:
                    Some(selection::LogChangeFileSelection {
                        ix: _,
                        path,
                        diff_selected,
                    }),
            },
        ))) => {
            let file_id = file::log_id_parts_hash(*hash, path);
            let state = navigation.log_diffs.diffs.get(&file_id);
            el(column([
                el(column([
                    view_header(format!(
                        "{path} changes in {}:",
                        display_short_hash(hash)
                    )),
                    match state {
                        Some(diff::FileAndState { file, state }) => {
                            let nav = &navigation.status_logs_navs.diffs_nav;
                            diff::view(
                                state,
                                nav,
                                file,
                                None,
                                *diff_selected,
                                false,
                                to_record,
                            )
                        }
                        None => el(text("Loading diff..")),
                    },
                ])
                .spacing(SPACING)),
                // NOTE: This is currently never true - there are only up to 3
                // cols
                if hidden_cols == 2 {
                    el(button(row([el(
                        text("← Files").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(column([])),
    };

    let other_channels_selected = || match selection {
        selection::Unified::Channel(channel) => match channel {
            Some(selection::Channel { log, .. }) => log.is_none(),
            None => true,
        },
        _ => false,
    };

    let other_channel_log_selected = || match selection {
        selection::Unified::Channel(Some(selection::Channel {
            ix: _,
            name: _,
            log: Some(selection::LogChange { file, .. }),
        })) => file.is_none(),
        _ => false,
    };

    let other_channel_log_change_selected = || match selection {
        selection::Unified::Channel(Some(selection::Channel {
            ix: _,
            name: _,
            log:
                Some(selection::LogChange {
                    file:
                        Some(selection::LogChangeFileSelection {
                            diff_selected,
                            ..
                        }),
                    ..
                }),
        })) => !*diff_selected,
        _ => false,
    };

    let view_channels = || {
        other_channels.iter().enumerate().map(|(ix, channel)| {
            let selection = match selection {
                selection::Unified::Channel(Some(selection::Channel {
                    ix: selected_ix,
                    log,
                    ..
                })) if &ix == selected_ix => {
                    if log.is_some() {
                        HierarchicalSelection::ChildSelected
                    } else {
                        HierarchicalSelection::Selected
                    }
                }
                _ => HierarchicalSelection::NotSelected,
            };
            el(button(text(channel))
                .on_press(Msg::UnfilteredSelection(
                    selection::UnfilteredMsg::Select(
                        selection::Select::Channel {
                            ix,
                            name: channel.clone(),
                        },
                    ),
                ))
                .class(hierarchical_button_class(selection)))
        })
    };

    // Other channel selection
    let channel_col_0 = || {
        el(column([
            el(text(format!("Current channel: {channel}"))),
            el(
                nav_scrollable(&navigation.other_channels_nav, view_channels())
                    .class(if other_channels_selected() {
                        theme::Scrollable::Selected
                    } else {
                        theme::Scrollable::Normal
                    })
                    .width(Length::Fill)
                    .height(Length::Fill),
            ),
        ]))
    };

    // Other channel change selection in selected channel
    let channel_col_1 = || match selection {
        selection::Unified::Channel(Some(selection::Channel {
            ix: _channel_ix,
            name,
            log: _,
        })) => {
            let Some(Log::Loaded { log }) = logs.other_channels_logs.get(name)
            else {
                return el(column([]));
            };
            let entries = log.iter().enumerate().map(|(ix, entry)| {
                let selection = match selection {
                    selection::Unified::Channel(Some(selection::Channel {
                        log:
                            Some(selection::LogChange {
                                ix: selected_ix,
                                file,
                                ..
                            }),
                        ..
                    })) if &ix == selected_ix => {
                        if file.is_some() {
                            HierarchicalSelection::ChildSelected
                        } else {
                            HierarchicalSelection::Selected
                        }
                    }
                    _ => HierarchicalSelection::NotSelected,
                };
                view_log_change_selection(ix, entry, selection)
            });

            let len = log.len();
            let selected_ix = match selection {
                selection::Unified::EntireLog(Some(selection::LogChange {
                    ix,
                    ..
                })) => Some(len - *ix),
                _ => None,
            };

            el(column([
                if let Some(selected_ix) = selected_ix {
                    el(text(format!(
                        "Channel {name} log ({selected_ix}/{len})"
                    )))
                } else {
                    el(text(format!("Channel {name} log ({len})")))
                },
                el(nav_scrollable(&navigation.other_channel_log_nav, entries)
                    .class(if other_channel_log_selected() {
                        theme::Scrollable::Selected
                    } else {
                        theme::Scrollable::Normal
                    })
                    .width(Length::Fill)
                    .height(Length::Fill)),
                if hidden_cols == 1 {
                    el(button(row([el(text("← Other channels")
                        .shaping(text::Shaping::Advanced))]))
                    .on_press(Msg::Action(action::FilteredMsg::Selection(
                        selection::Msg::PressDir(selection::Dir::Left),
                    ))))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill))
        }
        _ => el(column([])),
    };

    // Other channel log file selection in selected channel's change
    let channel_col_2 = || match selection {
        selection::Unified::Channel(Some(selection::Channel {
            ix: _channel_ix,
            name,
            log:
                Some(selection::LogChange {
                    ix,
                    hash: _,
                    message: _,
                    file,
                }),
        })) => {
            let nav = &navigation.other_channel_logs_navs.files_nav;
            let files_view = match logs.other_channels_logs.get(name) {
                Some(Log::Loaded { log }) => {
                    let entry = log.get(*ix).unwrap();
                    let files = entry.file_paths.iter().enumerate().map(
                        |(ix, path)| {
                            let selection = match file {
                                Some(selection::LogChangeFileSelection {
                                    path: selected_path,
                                    diff_selected,
                                    ..
                                }) if selected_path == path => {
                                    if *diff_selected {
                                        HierarchicalSelection::ChildSelected
                                    } else {
                                        HierarchicalSelection::Selected
                                    }
                                }
                                _ => HierarchicalSelection::NotSelected,
                            };
                            el(button(text(path))
                                .on_press_with(move || {
                                    Msg::UnfilteredSelection(
                                        selection::UnfilteredMsg::Select(
                                            selection::Select::LogChangeFile {
                                                ix,
                                                path: path.clone(),
                                            },
                                        ),
                                    )
                                })
                                .class(hierarchical_button_class(selection)))
                        },
                    );
                    el(column([
                        view_log_change_header(entry),
                        el(nav_scrollable(nav, files)
                            .class(if other_channel_log_change_selected() {
                                theme::Scrollable::Selected
                            } else {
                                theme::Scrollable::Normal
                            })
                            .width(Length::Fill)
                            .height(Length::Fill)),
                    ]))
                }
                _ => el(text("Loading...")),
            };

            el(column([
                files_view,
                if hidden_cols == 2 {
                    el(button(row([el(
                        text("← Log").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(row([])),
    };

    // Other channel log diff selection in selected channel's change's file
    let channel_col_3 = || match selection {
        selection::Unified::Channel(Some(selection::Channel {
            name: _,
            log:
                Some(selection::LogChange {
                    ix: _,
                    hash,
                    message: _,
                    file:
                        Some(selection::LogChangeFileSelection {
                            ix: _,
                            path,
                            diff_selected,
                        }),
                }),
            ..
        })) => {
            let file_id = file::log_id_parts_hash(*hash, path);
            let state = navigation.log_diffs.diffs.get(&file_id);
            el(column([
                el(column([
                    view_header(format!(
                        "{path} changes in {}:",
                        display_short_hash(hash)
                    )),
                    match state {
                        Some(diff::FileAndState { file, state }) => {
                            let nav =
                                &navigation.other_channel_logs_navs.diffs_nav;
                            diff::view(
                                state,
                                nav,
                                file,
                                None,
                                *diff_selected,
                                false,
                                to_record,
                            )
                        }
                        None => el(text("Loading diff..")),
                    },
                ])
                .spacing(SPACING)),
                if hidden_cols == 3 {
                    el(button(row([el(
                        text("← Files").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(column([])),
    };

    let entire_log_selected = || match selection {
        selection::Unified::EntireLog(log_change) => match log_change {
            Some(selection::LogChange { file, .. }) => file.is_none(),
            None => true,
        },
        _ => false,
    };
    let entire_log_change_selected = || match selection {
        selection::Unified::EntireLog(Some(selection::LogChange {
            file: Some(selection::LogChangeFileSelection { diff_selected, .. }),
            ..
        })) => !*diff_selected,
        _ => false,
    };

    // Entire log change selection
    let entire_log_col_0 = || {
        let Some(Log::Loaded { log }) = logs.entire_log.as_ref() else {
            unreachable!()
        };
        let entries = log.iter().enumerate().map(|(ix, entry)| {
            let selection = match selection {
                selection::Unified::EntireLog(Some(selection::LogChange {
                    ix: selected_ix,
                    file,
                    ..
                })) if &ix == selected_ix => {
                    if file.is_some() {
                        HierarchicalSelection::ChildSelected
                    } else {
                        HierarchicalSelection::Selected
                    }
                }
                _ => HierarchicalSelection::NotSelected,
            };
            view_log_change_selection(ix, entry, selection)
        });

        let len = log.len();
        let selected_ix = match selection {
            selection::Unified::EntireLog(Some(selection::LogChange {
                ix,
                ..
            })) => Some(len - *ix),
            _ => None,
        };

        el(column([
            view_repo_info(),
            if let Some(selected_ix) = selected_ix {
                el(text(format!("Entire log ({selected_ix}/{len})")))
            } else {
                el(text(format!("Entire log ({len})")))
            },
            el(nav_scrollable(&navigation.entire_log_nav, entries)
                .class(if entire_log_selected() {
                    theme::Scrollable::Selected
                } else {
                    theme::Scrollable::Normal
                })
                .width(Length::Fill)
                .height(Length::Fill)),
        ])
        .width(Length::Fill)
        .height(Length::Fill))
    };

    // Entire log file selection in selected change
    let entire_log_col_1 = || match selection {
        selection::Unified::EntireLog(Some(selection::LogChange {
            ix,
            hash: _,
            message: _,
            file,
        })) => {
            let files_view = match logs.entire_log.as_ref() {
                Some(Log::Loaded { log }) => {
                    let nav = &navigation.entire_logs_navs.files_nav;
                    let entry = log.get(*ix).unwrap();
                    let files = entry.file_paths.iter().enumerate().map(
                        |(ix, path)| {
                            let selection = match file {
                                Some(selection::LogChangeFileSelection {
                                    path: selected_path,
                                    diff_selected,
                                    ..
                                }) if selected_path == path => {
                                    if *diff_selected {
                                        HierarchicalSelection::ChildSelected
                                    } else {
                                        HierarchicalSelection::Selected
                                    }
                                }
                                _ => HierarchicalSelection::NotSelected,
                            };
                            el(button(text(path))
                                .on_press_with(move || {
                                    Msg::UnfilteredSelection(
                                        selection::UnfilteredMsg::Select(
                                            selection::Select::LogChangeFile {
                                                ix,
                                                path: path.clone(),
                                            },
                                        ),
                                    )
                                })
                                .class(hierarchical_button_class(selection)))
                        },
                    );
                    el(column([
                        view_log_change_header(entry),
                        el(nav_scrollable(nav, files)
                            .class(if entire_log_change_selected() {
                                theme::Scrollable::Selected
                            } else {
                                theme::Scrollable::Normal
                            })
                            .width(Length::Fill)
                            .height(Length::Fill)),
                    ]))
                }
                _ => el(text("Loading...")),
            };

            el(column([
                files_view,
                if hidden_cols == 1 {
                    el(button(row([el(
                        text("← Log").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(row([])),
    };

    // TODO more re-use with status_col_2
    // Entire log diff selection in selected change's file
    let entire_log_col_2 = || match selection {
        selection::Unified::EntireLog(Some(selection::LogChange {
            ix: _,
            hash,
            message: _,
            file:
                Some(selection::LogChangeFileSelection {
                    ix: _,
                    path,
                    diff_selected,
                }),
        })) => {
            let file_id = file::log_id_parts_hash(*hash, path);
            let state = navigation.log_diffs.diffs.get(&file_id);
            el(column([
                el(column([
                    view_header(format!(
                        "{path} changes in {}:",
                        display_short_hash(hash)
                    )),
                    match state {
                        Some(diff::FileAndState { file, state }) => {
                            let nav = &navigation.entire_logs_navs.diffs_nav;
                            diff::view(
                                state,
                                nav,
                                file,
                                None,
                                *diff_selected,
                                false,
                                to_record,
                            )
                        }
                        None => el(text("Loading diff..")),
                    },
                ])
                .spacing(SPACING)),
                // NOTE: This is currently never true - there are only up to 3
                // cols
                if hidden_cols == 2 {
                    el(button(row([el(
                        text("← Files").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(column([])),
    };

    let compare_remote_nav_children = || {
        if let selection::Unified::CompareRemote(Some(
            selection::CompareRemote {
                ix: _,
                hash: _,
                file: _,
                remote,
                remote_channel,
            },
        )) = selection
            && let Some(record_dichotomy) = model::get_record_dichotomy(
                record_dichotomy,
                remote,
                remote_channel,
            )
        {
            let repo::RecordDichotomy {
                local_records,
                remote_records,
                remote_unrecords,
            } = record_dichotomy;
            let count = |records: &[_]| {
                records.len() + if records.is_empty() { 0 } else { 1 } // account for a header if non-empty
            };
            let row_count = count(local_records)
                + count(remote_records)
                + count(remote_unrecords);
            if row_count == 0 {
                vec![el(text("Local and remote are the same!"))]
            } else {
                let mut rows = Vec::with_capacity(row_count);

                let view = |rows: &mut Vec<_>, ix, entry: &repo::LogEntry| {
                    let selection = match selection {
                        selection::Unified::CompareRemote(Some(
                            selection::CompareRemote {
                                ix: Some(selected_ix),
                                hash: _,
                                file,
                                remote: _,
                                remote_channel: _,
                            },
                        )) if &ix == selected_ix => {
                            if file.is_some() {
                                HierarchicalSelection::ChildSelected
                            } else {
                                HierarchicalSelection::Selected
                            }
                        }
                        _ => HierarchicalSelection::NotSelected,
                    };
                    rows.push(el(button(el(text(display_short_hash(
                        &entry.hash,
                    ))
                    .font(Font::MONOSPACE)))
                    .on_press(Msg::UnfilteredSelection(
                        selection::UnfilteredMsg::Select(
                            selection::Select::CompareRemote { ix },
                        ),
                    ))
                    .class(hierarchical_button_class(selection))));
                };

                if !local_records.is_empty() {
                    rows.push(el(container(el(text("Local records:")))
                        .padding(Padding::ZERO.top(SPACING))));
                }
                let ix_offset = 0;
                record_dichotomy.local_records.iter().enumerate().for_each(
                    |(ix, entry)| view(&mut rows, ix + ix_offset, entry),
                );

                if !remote_records.is_empty() {
                    rows.push(el(container(el(text("Remote records:")))
                        .padding(Padding::ZERO.top(SPACING))));
                }
                let ix_offset = local_records.len();
                record_dichotomy.remote_records.iter().enumerate().for_each(
                    |(ix, entry)| view(&mut rows, ix + ix_offset, entry),
                );

                if !remote_unrecords.is_empty() {
                    rows.push(el(container(el(text("Remote unrecords:")))
                        .padding(Padding::ZERO.top(SPACING))));
                }
                let ix_offset = ix_offset + remote_records.len();
                record_dichotomy
                    .remote_unrecords
                    .iter()
                    .enumerate()
                    .for_each(|(ix, entry)| {
                        view(&mut rows, ix + ix_offset, entry)
                    });

                rows
            }
        } else {
            vec![el(text("Loading..."))]
        }
    };

    // Compare remote records dichotomy
    let compare_remote_col_0 = || {
        el(column([el(nav_scrollable(
            &navigation.compare_remote_nav,
            compare_remote_nav_children(),
        )
        .class(if status_selected() {
            theme::Scrollable::Selected
        } else {
            theme::Scrollable::Normal
        })
        .width(Length::Fill)
        .height(Length::Fill))])
        .width(Length::Fill)
        .height(Length::Fill))
    };

    // Compare remote file selection in selected change
    let compare_remote_col_1 = || match selection {
        selection::Unified::CompareRemote(Some(selection::CompareRemote {
            ix: Some(ix),
            hash: _,
            file,
            remote,
            remote_channel,
        })) => {
            let files_view = if let Some(record_dichotomy) =
                model::get_record_dichotomy(
                    record_dichotomy,
                    remote,
                    remote_channel,
                ) {
                let nav = &navigation.compare_remote_navs.files_nav;
                let entry = record_dichotomy.get(*ix).unwrap();
                let files =
                    entry.file_paths.iter().enumerate().map(|(ix, path)| {
                        let selection = match file {
                            Some(selection::LogChangeFileSelection {
                                path: selected_path,
                                diff_selected,
                                ..
                            }) if selected_path == path => {
                                if *diff_selected {
                                    HierarchicalSelection::ChildSelected
                                } else {
                                    HierarchicalSelection::Selected
                                }
                            }
                            _ => HierarchicalSelection::NotSelected,
                        };
                        el(button(text(path))
                            .on_press_with(move || {
                                Msg::UnfilteredSelection(
                                    selection::UnfilteredMsg::Select(
                                        selection::Select::CompareRemoteFile {
                                            ix,
                                            path: path.clone(),
                                        },
                                    ),
                                )
                            })
                            .class(hierarchical_button_class(selection)))
                    });
                el(column([
                    view_log_change_header(entry),
                    el(nav_scrollable(nav, files)
                        .class(if entire_log_change_selected() {
                            theme::Scrollable::Selected
                        } else {
                            theme::Scrollable::Normal
                        })
                        .width(Length::Fill)
                        .height(Length::Fill)),
                ]))
            } else {
                el(text("Loading..."))
            };

            el(column([
                files_view,
                if hidden_cols == 1 {
                    el(button(row([el(
                        text("← Records").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(row([])),
    };

    // TODO more re-use with status_col_2
    // Compare remote log diff selection in selected change's file
    let compare_remote_col_2 = || match selection {
        selection::Unified::CompareRemote(Some(selection::CompareRemote {
            ix: _,
            hash: Some(hash),
            file:
                Some(selection::LogChangeFileSelection {
                    ix: _,
                    path,
                    diff_selected,
                }),
            remote: _,
            remote_channel: _,
        })) => {
            let file_id = file::log_id_parts_hash(*hash, path);
            let state = navigation.log_diffs.diffs.get(&file_id);
            el(column([
                el(column([
                    view_header(format!(
                        "{path} changes in {}:",
                        display_short_hash(hash)
                    )),
                    match state {
                        Some(diff::FileAndState { file, state }) => {
                            let nav = &navigation.compare_remote_navs.diffs_nav;
                            diff::view(
                                state,
                                nav,
                                file,
                                None,
                                *diff_selected,
                                false,
                                to_record,
                            )
                        }
                        None => el(text("Loading diff..")),
                    },
                ])
                .spacing(SPACING)),
                // NOTE: This is currently never true - there are only up to 3
                // cols
                if hidden_cols == 2 {
                    el(button(row([el(
                        text("← Files").shaping(text::Shaping::Advanced)
                    )]))
                    .on_press(Msg::Action(
                        action::FilteredMsg::Selection(
                            selection::Msg::PressDir(selection::Dir::Left),
                        ),
                    )))
                } else {
                    el(row([]))
                },
            ])
            .width(Length::Fill)
            .height(Length::Fill)
            .spacing(SPACING))
        }
        _ => el(column([])),
    };

    let main = match selection {
        selection::Unified::Status(_) => {
            let cols = [status_col_0(), status_col_1(), status_col_2()]
                .into_iter()
                .skip(hidden_cols);
            el(row(cols)
                .spacing(SPACING)
                .width(Length::Fill)
                .height(Length::Fill))
        }
        selection::Unified::Channel(_) => {
            if other_channels.is_empty() {
                el(column([el(text(format!("Current channel: {channel}")))])
                    .width(Length::Fill)
                    .height(Length::Fill))
            } else {
                let cols = [
                    channel_col_0(),
                    channel_col_1(),
                    channel_col_2(),
                    channel_col_3(),
                ]
                .into_iter()
                .skip(hidden_cols);
                el(row(cols).width(Length::Fill).height(Length::Fill))
            }
        }
        selection::Unified::EntireLog(_) => match logs.entire_log.as_ref() {
            None | Some(Log::Loading) => el(column([
                view_repo_info(),
                el(text("Entire log")),
                el(text("Loading...")),
            ])
            .width(Length::Fill)
            .height(Length::Fill)),
            Some(Log::Loaded { .. }) => {
                let cols = [
                    entire_log_col_0(),
                    entire_log_col_1(),
                    entire_log_col_2(),
                ]
                .into_iter()
                .skip(hidden_cols);
                el(row(cols)
                    .spacing(SPACING)
                    .width(Length::Fill)
                    .height(Length::Fill))
            }
        },
        selection::Unified::CompareRemote(_) => {
            let cols = [
                compare_remote_col_0(),
                compare_remote_col_1(),
                compare_remote_col_2(),
            ]
            .into_iter()
            .skip(hidden_cols);
            el(row(cols)
                .spacing(SPACING)
                .width(Length::Fill)
                .height(Length::Fill))
        }
    };

    let actions_inner = column([]).spacing(4);

    let mut force_highlight_actions = false;
    let actions_inner = if let Some(forking_channel) = forking_channel_name {
        let channel_name_input = text_input("channel name...", forking_channel)
            .on_input(Msg::EditForkChannelName);
        force_highlight_actions = true;
        actions_inner.push(channel_name_input)
    } else {
        actions_inner
    };

    let actions_inner = if !jobs.is_empty() {
        let jobs = view_jobs(jobs, channel);
        actions_inner.push(jobs)
    } else {
        actions_inner
    };

    add_actions_and_report(
        Some(actions_inner),
        main,
        window_size,
        allowed_actions,
        report,
        sub_menu,
        force_highlight_actions,
    )
}

fn overlay_report<'a>(
    overlaid: Element<'a, Msg, Theme>,
    report: &'a report::Container,
    window_size: &iced::Size,
) -> Element<'a, Msg, Theme> {
    report::view(report, overlaid, |report| {
        let report_width =
            if window_size.width > DEFAULT_MIN_COL_WIDTH as f32 * 1.3 {
                window_size.width * 0.45
            } else {
                window_size.width * 0.95
            };

        let max_height = window_size.height * 0.85;

        el(container(column([
            el(row([el(container(
                button(text("").shaping(text::Shaping::Advanced))
                    .on_press(Msg::Action(action::FilteredMsg::ToggleReports)),
            )
            .width(Length::Fill)
            .align_x(Alignment::End))])
            .width(Length::Fill)),
            el(container(column(
                report.entries.iter().rev().map(view_report_entry),
            ))
            .padding([4, 6])
            .class(theme::Container::Report)),
        ]))
        .width(Length::Fixed(report_width))
        .height(Length::Shrink)
        .max_height(max_height))
    })
}

fn view_report_entry<'a>(entry: &'a report::Entry) -> Element<'a, Msg, Theme>
where
    Msg: 'a,
{
    let report::Entry {
        level,
        msg,
        time,
        is_read,
    } = entry;
    let time = time.strftime("%I:%M:%S.%3f").to_string();
    let text_class = if *is_read {
        theme::Text::SlightlyFaded
    } else {
        theme::Text::Normal
    };
    el(row([
        el(text(msg)
            .width(Length::Fill)
            .wrapping(text::Wrapping::WordOrGlyph)
            .class(text_class)),
        el(container(row([]))
            .width(Length::Fixed(8.0))
            .height(Length::Fill)
            .class(theme::Container::ReportLevel {
                level: *level,
                is_read: *is_read,
            })),
        el(text(time)
            .align_x(Alignment::End)
            .size(11.0)
            .class(text_class)),
    ])
    .spacing(6)
    .padding(4)
    .width(Length::Fill)
    .height(Length::Shrink))
}

fn view_header<'a>(
    header: impl text::IntoFragment<'a>,
) -> Element<'a, Msg, Theme> {
    el(text(header).font(Font {
        weight: font::Weight::Bold,
        ..default()
    }))
}

fn view_actions<'a>(
    allowed_actions: &[action::Binding],
) -> Element<'a, Msg, Theme> {
    let buttons: Vec<_> = allowed_actions
        .iter()
        .map(
            |action::Binding {
                 keys_str,
                 keys: _,
                 label,
                 msg,
             }| {
                if let Some(msg) = msg {
                    action_button(keys_str, label, Msg::Action(msg.clone()))
                } else {
                    el(action_button_inner(keys_str, label))
                }
            },
        )
        .collect();
    el(row(buttons).spacing(2).wrap())
}

fn view_jobs<'a>(
    jobs: &IndexSet<Job>,
    current_channel: &str,
) -> Element<'a, Msg, Theme> {
    let jobs: Vec<_> = itertools::intersperse_with(
        jobs.iter().map(|job| view_job(job, current_channel)),
        || el(text(", ")),
    )
    .collect();
    el(row(jobs).spacing(2).wrap())
}

fn view_job<'a>(job: &Job, current_channel: &str) -> Element<'a, Msg, Theme> {
    match job {
        Job::Pull { remote, channel } => {
            if channel == current_channel {
                el(text(format!("Pulling from remote {remote}")))
            } else {
                el(text(format!(
                    "Pulling from remote {remote}, channel {channel}"
                )))
            }
        }
        Job::Push { remote, channel } => {
            if channel == current_channel {
                el(text(format!("Pushing to remote {remote}")))
            } else {
                el(text(format!(
                    "Pushing to remote {remote}, channel {channel}"
                )))
            }
        }
        Job::CompareRemote {
            remote,
            remote_channel: channel,
        } => el(text(format!(
            "Fetching from remote {remote}, channel {channel}",
        ))),
    }
}

fn action_button<'a>(
    key: &'a str,
    label: &'a str,
    on_press: Msg,
) -> Element<'a, Msg, Theme> {
    el(action_button_inner(key, label).on_press(on_press))
}

fn action_button_inner<'a>(
    key: &'a str,
    label: &'a str,
) -> Button<'a, Msg, Theme> {
    let row = row([el(text(key)
        .shaping(text::Shaping::Advanced)
        .font(Font {
            weight: font::Weight::Bold,
            ..default()
        })
        .class(theme::Text::HighlightOnLightBg))])
    .spacing(6);
    let row = if label.is_empty() {
        row
    } else {
        row.push(el(text(label)))
    };
    button(row)
}

#[derive(Debug)]
enum HierarchicalSelection {
    NotSelected,
    Selected,
    ChildSelected,
}

fn hierarchical_button_class(
    selection: HierarchicalSelection,
) -> theme::Button {
    match selection {
        HierarchicalSelection::NotSelected => theme::Button::Normal,
        HierarchicalSelection::Selected => theme::Button::Selected,
        HierarchicalSelection::ChildSelected => theme::Button::ChildSelected,
    }
}

fn display_short_hash(hash: &repo::ChangeHash) -> String {
    let mut short_hash = repo::hash_to_string(hash);
    short_hash.truncate(8);
    short_hash
}

fn view_log_change_selection(
    ix: usize,
    repo::LogEntry {
        hash,
        message,
        description: _,
        timestamp: _,
        file_paths: _,
    }: &repo::LogEntry,
    selection: HierarchicalSelection,
) -> Element<'_, Msg, Theme> {
    let short_hash = display_short_hash(hash);

    el(row([
        el(button(text(short_hash).font(Font::MONOSPACE))
            .on_press(Msg::UnfilteredSelection(
                selection::UnfilteredMsg::Select(
                    selection::Select::LogChange {
                        ix,
                        hash: *hash,
                        message: message.clone(),
                    },
                ),
            ))
            .class(hierarchical_button_class(selection))),
        el(text(message.split("\n").next().unwrap())
            .shaping(text::Shaping::Advanced)),
    ])
    .spacing(SPACING))
}

fn view_log_change_header(
    repo::LogEntry {
        hash,
        message,
        description,
        timestamp,
        file_paths: _,
    }: &repo::LogEntry,
) -> Element<'_, Msg, Theme> {
    let short_hash = display_short_hash(hash);

    let mut cols = Vec::with_capacity(3 + description.iter().len());

    cols.push(el(row([
        el(text(short_hash.to_string()).font(Font {
            weight: font::Weight::Bold,
            ..Font::MONOSPACE
        })),
        view_timestamp(timestamp),
    ])
    .spacing(SPACING)));
    cols.push(el(row([
        view_header("Message:"),
        el(text(message).shaping(text::Shaping::Advanced)),
    ])
    .spacing(SPACING)));
    if let Some(description) = description {
        cols.push(el(row([
            view_header("Description:"),
            el(text(description).shaping(text::Shaping::Advanced)),
        ])
        .spacing(SPACING)));
    }
    cols.push(view_header("Files:".to_string()));

    el(column(cols).spacing(SPACING))
}

fn view_timestamp(timestamp: &Timestamp) -> Element<'_, Msg, Theme> {
    el(text(timestamp.strftime("%H:%M:%S, %b %d, %Y").to_string()))
}