#[doc(inline)]
pub use inflorescence_model::selection::{
    unify, Channel, CompareRemote, Dir, HeldKey, LogChange,
    LogChangeFileSelection, Msg, Primary, Select, State, Status, UnfilteredMsg,
    Unified,
};

use crate::{diff, file, log};
use inflorescence_iced_widget::nav_scrollable;
use inflorescence_model::model;
use inflorescence_model::model::{Log, Logs, Navigation};
use libflorescence::repo;

use iced::time::{Duration, Instant};
use iced_utils::Task;

use std::cmp;

/// Hot potato
pub struct Ctx<'a> {
    pub state: &'a mut State,
    pub files: &'a mut file::State,
    pub navigation: &'a mut Navigation,
    pub repo: &'a repo::State,
    pub logs: &'a Logs,
    pub record_dichotomy: Option<&'a repo::RecordDichotomy>,
}

pub fn update(
    msg: Msg,
    state: &mut State,
    files: &mut file::State,
    navigation: &mut Navigation,
    repo: &repo::State,
    logs: &Logs,
    record_dichotomy: Option<&repo::RecordDichotomy>,
) -> Task<crate::ManagingRepoMsg> {
    let mut ctx = Ctx {
        state,
        files,
        navigation,
        repo,
        logs,
        record_dichotomy,
    };
    let mctx = &mut ctx;

    match msg {
        Msg::PressDir(dir) => {
            let delta = {
                mctx.state.held_key.as_ref().and_then(
                    |HeldKey {
                         dir: held_dir,
                         last_tick,
                     }| {
                        (dir == *held_dir).then(|| {
                            // Ceil to 50 ms, because the first key repeat in
                            // Iced is delayed 500 ms and that creates too much
                            // of a jump
                            cmp::min(
                                Duration::from_millis(50),
                                Instant::now() - *last_tick,
                            )
                        })
                    },
                )
            };
            mctx.state.held_key = Some(HeldKey {
                dir,
                last_tick: Instant::now(),
            });

            match dir {
                Dir::Down => select_down(mctx, delta),
                Dir::Up => select_up(mctx, delta),
                Dir::Left => select_left(mctx),
                Dir::Right => select_right(mctx),
            }
        }
        Msg::AltPressDir(dir) => {
            let delta = {
                mctx.state.held_key.as_ref().and_then(
                    |HeldKey {
                         dir: held_dir,
                         last_tick,
                     }| {
                        (dir == *held_dir).then(|| {
                            // Ceil to 50 ms, because the first key repeat in
                            // Iced is delayed 500
                            // ms and that creates too much of a jump
                            cmp::min(
                                Duration::from_millis(50),
                                Instant::now() - *last_tick,
                            )
                        })
                    },
                )
            };
            mctx.state.held_key = Some(HeldKey {
                dir,
                last_tick: Instant::now(),
            });

            match dir {
                Dir::Down => alt_select_down(mctx, delta),
                Dir::Up => alt_select_up(mctx, delta),
                Dir::Left | Dir::Right => {
                    // Nothing here yet
                    Task::none()
                }
            }
        }
    }
}

pub fn update_unfiltered(
    msg: UnfilteredMsg,
    state: &mut State,
    files: &mut file::State,
    navigation: &mut Navigation,
    repo: &repo::State,
    logs: &Logs,
    record_dichotomy: Option<&repo::RecordDichotomy>,
) -> Task<crate::ManagingRepoMsg> {
    let mut ctx = Ctx {
        state,
        files,
        navigation,
        repo,
        logs,
        record_dichotomy,
    };
    let mctx = &mut ctx;

    match msg {
        UnfilteredMsg::ReleaseDir(dir) => release(dir, mctx),
        UnfilteredMsg::Select(select) => select_exact(select, mctx),
    }
}

fn select_down(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    match ctx.state.primary {
        Primary::Status => select_down_status(ctx, delta),
        Primary::Channel => select_down_channel(ctx, delta),
        Primary::EntireLog => select_down_entire_log(ctx, delta),
        Primary::CompareRemote => select_down_compare_remote(ctx, delta),
    }
}

fn select_down_status(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    let (selection, task) = match ctx.state.status.take() {
        Some(Status::UntrackedFile {
            ix,
            path,
            diff_selected,
        }) => {
            let (selection, task) = if diff_selected {
                nav_scrollable::alt_scroll_down(
                    &mut ctx.navigation.files_diffs.diffs_nav,
                    delta,
                );

                (
                    Status::UntrackedFile {
                        ix,
                        path,
                        diff_selected,
                    },
                    Task::none(),
                )
            } else if ctx.repo.untracked_files.len().saturating_sub(1) == ix {
                // Last untracked file selected
                if !ctx.repo.changed_files.is_empty() {
                    let ix = 0;
                    changed_file_selection(ix, VDir::Down, ctx)
                } else if !ctx.repo.short_log.is_empty() {
                    let ix = 0;
                    status_log_selection(ix, VDir::Down, ctx)
                } else {
                    let ix = 0;
                    untracked_file_selection(ix, VDir::Up, ctx)
                }
            } else {
                let ix = ix + 1;
                untracked_file_selection(ix, VDir::Down, ctx)
            };
            (Some(selection), task)
        }
        Some(Status::ChangedFile {
            ix,
            path,
            diff_selected,
        }) => {
            let (selection, task) = if diff_selected {
                nav_scrollable::scroll_down(
                    &mut ctx.navigation.files_diffs.diffs_nav,
                    delta,
                );

                (
                    Status::ChangedFile {
                        ix,
                        path,
                        diff_selected,
                    },
                    Task::none(),
                )
            } else if ctx.repo.changed_files.len().saturating_sub(1) == ix {
                // Last changed file selected
                if !ctx.repo.short_log.is_empty() {
                    let ix = 0;
                    status_log_selection(ix, VDir::Down, ctx)
                } else if !ctx.repo.untracked_files.is_empty() {
                    let ix = 0;
                    untracked_file_selection(ix, VDir::Up, ctx)
                } else {
                    let ix = 0;
                    changed_file_selection(ix, VDir::Up, ctx)
                }
            } else {
                let ix = ix + 1;
                changed_file_selection(ix, VDir::Down, ctx)
            };
            (Some(selection), task)
        }
        Some(Status::LogChange(LogChange {
            ix: log_ix,
            hash,
            message,
            file,
        })) => {
            let (selection, task) = match file {
                Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected,
                }) => {
                    if diff_selected {
                        let selection = Status::LogChange(LogChange {
                            ix: log_ix,
                            hash,
                            message,
                            file: Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected,
                            }),
                        });

                        nav_scrollable::scroll_down(
                            &mut ctx.navigation.status_logs_navs.diffs_nav,
                            delta,
                        );

                        (selection, Task::none())
                    } else {
                        let log_entry = ctx.repo.short_log.get(log_ix).unwrap();

                        let (file_ix, dir) =
                            if log_entry.file_paths.len().saturating_sub(1)
                                == file_ix
                            {
                                (0, VDir::Up)
                            } else {
                                (file_ix + 1, VDir::Down)
                            };

                        let (file, selection_task) = status_log_file_selection(
                            file_ix,
                            hash,
                            dir,
                            ctx.navigation,
                            log_entry,
                        );

                        let selection = Status::LogChange(LogChange {
                            ix: log_ix,
                            hash,
                            message,
                            file: Some(file),
                        });
                        (selection, selection_task)
                    }
                }
                None => {
                    if ctx.repo.short_log.len().saturating_sub(1) == log_ix {
                        // Last log selected
                        if !ctx.repo.untracked_files.is_empty() {
                            let ix = 0;
                            untracked_file_selection(ix, VDir::Up, ctx)
                        } else if !ctx.repo.changed_files.is_empty() {
                            let ix = 0;
                            changed_file_selection(ix, VDir::Up, ctx)
                        } else {
                            let ix = 0;
                            status_log_selection(ix, VDir::Up, ctx)
                        }
                    } else {
                        let ix = log_ix + 1;
                        status_log_selection(ix, VDir::Down, ctx)
                    }
                }
            };
            (Some(selection), task)
        }
        None => {
            if !ctx.repo.untracked_files.is_empty() {
                let ix = 0;
                let (selection, task) =
                    untracked_file_selection(ix, VDir::Down, ctx);
                (Some(selection), task)
            } else if !ctx.repo.changed_files.is_empty() {
                let ix = 0;
                let (selection, task) =
                    changed_file_selection(ix, VDir::Down, ctx);
                (Some(selection), task)
            } else if !ctx.repo.short_log.is_empty() {
                let ix = ctx.repo.short_log.len() - 1;
                let (selection, task) =
                    status_log_selection(ix, VDir::Down, ctx);
                (Some(selection), task)
            } else {
                (None, Task::none())
            }
        }
    };
    ctx.state.status = selection;
    task
}

fn select_down_channel(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    let (selection, task) = match ctx.state.channel.take() {
        Some(Channel {
            ix,
            name: _,
            log: None,
        }) => {
            let ix = if ix == ctx.repo.other_channels.len().saturating_sub(1) {
                0
            } else {
                ix + 1
            };
            channel_selection(ix, VDir::Down, ctx)
        }
        None => {
            let ix = 0;
            channel_selection(ix, VDir::Down, ctx)
        }
        Some(Channel {
            ix: channel_ix,
            name,
            log:
                Some(LogChange {
                    ix: change_ix,
                    hash,
                    message,
                    file,
                }),
        }) => {
            if let Some(Log::Loaded { log }) =
                ctx.logs.other_channels_logs.get(&name)
            {
                if let Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected,
                }) = file
                {
                    if diff_selected {
                        let selection = Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected,
                                }),
                            }),
                        };

                        nav_scrollable::scroll_down(
                            &mut ctx
                                .navigation
                                .other_channel_logs_navs
                                .diffs_nav,
                            delta,
                        );

                        (selection, Task::none())
                    } else {
                        let log_entry = log.get(change_ix).unwrap();

                        let file_ix =
                            if log_entry.file_paths.len().saturating_sub(1)
                                == file_ix
                            {
                                0
                            } else {
                                file_ix + 1
                            };

                        let (file, selection_task) = channel_log_file_selection(
                            file_ix,
                            hash,
                            VDir::Down,
                            ctx.navigation,
                            log_entry,
                        );

                        let selection = Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: Some(file),
                            }),
                        };
                        (selection, selection_task)
                    }
                } else {
                    let (selection, task) =
                        if log.len().saturating_sub(1) == change_ix {
                            let ix = 0;
                            channel_log_selection(ix, VDir::Up, ctx, log)
                        } else {
                            let ix = change_ix + 1;
                            channel_log_selection(ix, VDir::Down, ctx, log)
                        };

                    let selection = Channel {
                        ix: channel_ix,
                        name,
                        log: Some(selection),
                    };
                    (selection, task)
                }
            } else {
                let selection = Channel {
                    ix: channel_ix,
                    name,
                    log: Some(LogChange {
                        ix: change_ix,
                        hash,
                        message,
                        file,
                    }),
                };
                (selection, Task::none())
            }
        }
    };
    ctx.state.channel = Some(selection);
    task
}

fn select_down_entire_log(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    if let Some(Log::Loaded { log }) = ctx.logs.entire_log.as_ref() {
        let (selection, task) = if let Some(LogChange {
            ix: change_ix,
            hash,
            message,
            file,
        }) = ctx.state.entire_log.take()
        {
            if let Some(LogChangeFileSelection {
                ix: file_ix,
                path,
                diff_selected,
            }) = file
            {
                if diff_selected {
                    let selection = LogChange {
                        ix: change_ix,
                        hash,
                        message,
                        file: Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected,
                        }),
                    };

                    nav_scrollable::scroll_down(
                        &mut ctx.navigation.entire_logs_navs.diffs_nav,
                        delta,
                    );

                    (selection, Task::none())
                } else {
                    let log_entry = log.get(change_ix).unwrap();

                    let file_ix =
                        if log_entry.file_paths.len().saturating_sub(1)
                            == file_ix
                        {
                            0
                        } else {
                            file_ix + 1
                        };

                    let (file, selection_task) = entire_log_file_selection(
                        file_ix,
                        hash,
                        VDir::Down,
                        ctx.navigation,
                        log_entry,
                    );

                    let selection = LogChange {
                        ix: change_ix,
                        hash,
                        message,
                        file: Some(file),
                    };
                    (selection, selection_task)
                }
            } else if log.len().saturating_sub(1) == change_ix {
                let ix = 0;
                entire_log_selection(ix, VDir::Up, ctx, log)
            } else {
                let ix = change_ix + 1;
                entire_log_selection(ix, VDir::Down, ctx, log)
            }
        } else {
            let ix = 0;
            entire_log_selection(ix, VDir::Down, ctx, log)
        };
        ctx.state.entire_log = Some(selection);
        return task;
    }
    Task::none()
}

fn select_down_compare_remote(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    if let Some(record_dichotomy) = ctx.record_dichotomy {
        let len = record_dichotomy.len();
        if len > 0
            && let Some(CompareRemote {
                ix,
                hash,
                file,
                remote,
                remote_channel: channel,
            }) = ctx.state.compare_remote.take()
        {
            let (selection, task) = if let (Some(change_ix), Some(hash)) =
                (ix, hash)
            {
                if let Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected,
                }) = file
                {
                    if diff_selected {
                        let selection = CompareRemote {
                            ix: Some(change_ix),
                            hash: Some(hash),
                            file: Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected,
                            }),
                            remote,
                            remote_channel: channel,
                        };

                        nav_scrollable::scroll_down(
                            &mut ctx.navigation.compare_remote_navs.diffs_nav,
                            delta,
                        );

                        (selection, Task::none())
                    } else {
                        let log_entry =
                            record_dichotomy.get(change_ix).unwrap();

                        let file_ix =
                            if log_entry.file_paths.len().saturating_sub(1)
                                == file_ix
                            {
                                0
                            } else {
                                file_ix + 1
                            };

                        let (file, selection_task) =
                            compare_remote_file_selection(
                                file_ix,
                                hash,
                                VDir::Down,
                                ctx.navigation,
                                log_entry,
                            );

                        let selection = CompareRemote {
                            ix: Some(change_ix),
                            hash: Some(hash),
                            file: Some(file),
                            remote,
                            remote_channel: channel,
                        };
                        (selection, selection_task)
                    }
                } else {
                    if change_ix + 1 == len {
                        let ix = 0;
                        compare_remote_selection(
                            ix,
                            VDir::Up,
                            ctx,
                            record_dichotomy,
                            remote,
                            channel,
                        )
                    } else {
                        let ix = change_ix + 1;
                        compare_remote_selection(
                            ix,
                            VDir::Down,
                            ctx,
                            record_dichotomy,
                            remote,
                            channel,
                        )
                    }
                }
            } else {
                let ix = 0;
                compare_remote_selection(
                    ix,
                    VDir::Down,
                    ctx,
                    record_dichotomy,
                    remote,
                    channel,
                )
            };

            ctx.state.compare_remote = Some(selection);
            return task;
        }
    }
    Task::none()
}

fn select_up(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    match ctx.state.primary {
        Primary::Status => select_up_status(ctx, delta),
        Primary::Channel => select_up_channel(ctx, delta),
        Primary::EntireLog => select_up_entire_log(ctx, delta),
        Primary::CompareRemote => select_up_compare_remote(ctx, delta),
    }
}

fn select_up_status(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    let (selection, task) = match ctx.state.status.take() {
        Some(Status::UntrackedFile {
            ix,
            path,
            diff_selected,
        }) => {
            let (selection, task) = if diff_selected {
                nav_scrollable::alt_scroll_up(
                    &mut ctx.navigation.files_diffs.diffs_nav,
                    delta,
                );

                (
                    Status::UntrackedFile {
                        ix,
                        path,
                        diff_selected,
                    },
                    Task::none(),
                )
            } else if 0 == ix {
                // First untracked file selected
                if !ctx.repo.short_log.is_empty() {
                    let ix = ctx.repo.short_log.len() - 1;
                    status_log_selection(ix, VDir::Down, ctx)
                } else if !ctx.repo.changed_files.is_empty() {
                    let ix = ctx.repo.changed_files.len() - 1;
                    changed_file_selection(ix, VDir::Down, ctx)
                } else {
                    let ix = ctx.repo.untracked_files.len() - 1;
                    untracked_file_selection(ix, VDir::Down, ctx)
                }
            } else {
                let ix = ix - 1;
                untracked_file_selection(ix, VDir::Up, ctx)
            };
            (Some(selection), task)
        }
        Some(Status::ChangedFile {
            ix,
            path,
            diff_selected,
        }) => {
            let (selection, task) = if diff_selected {
                nav_scrollable::scroll_up(
                    &mut ctx.navigation.files_diffs.diffs_nav,
                    delta,
                );

                (
                    Status::ChangedFile {
                        ix,
                        path,
                        diff_selected,
                    },
                    Task::none(),
                )
            } else if 0 == ix {
                // First changed file selected
                if !ctx.repo.untracked_files.is_empty() {
                    let ix = ctx.repo.untracked_files.len() - 1;
                    untracked_file_selection(ix, VDir::Up, ctx)
                } else if !ctx.repo.short_log.is_empty() {
                    let ix = ctx.repo.short_log.len() - 1;
                    status_log_selection(ix, VDir::Down, ctx)
                } else {
                    let ix = ctx.repo.changed_files.len() - 1;
                    changed_file_selection(ix, VDir::Down, ctx)
                }
            } else {
                let ix = ix - 1;
                changed_file_selection(ix, VDir::Up, ctx)
            };
            (Some(selection), task)
        }
        Some(Status::LogChange(LogChange {
            ix: log_ix,
            hash,
            message,
            file,
        })) => {
            let (selection, task) = match file {
                Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected,
                }) => {
                    if diff_selected {
                        let selection = Status::LogChange(LogChange {
                            ix: log_ix,
                            hash,
                            message,
                            file: Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected,
                            }),
                        });

                        nav_scrollable::scroll_up(
                            &mut ctx.navigation.status_logs_navs.diffs_nav,
                            delta,
                        );

                        (selection, Task::none())
                    } else {
                        let log_entry = ctx.repo.short_log.get(log_ix).unwrap();

                        let (file_ix, dir) = if 0 == file_ix {
                            (log_entry.file_paths.len() - 1, VDir::Down)
                        } else {
                            (file_ix - 1, VDir::Up)
                        };

                        let (file, selection_task) = status_log_file_selection(
                            file_ix,
                            hash,
                            dir,
                            ctx.navigation,
                            log_entry,
                        );

                        let selection = Status::LogChange(LogChange {
                            ix: log_ix,
                            hash,
                            message,
                            file: Some(file),
                        });
                        (selection, selection_task)
                    }
                }
                None => {
                    if 0 == log_ix {
                        // First log selected
                        if !ctx.repo.changed_files.is_empty() {
                            let ix = ctx.repo.changed_files.len() - 1;
                            changed_file_selection(ix, VDir::Up, ctx)
                        } else if !ctx.repo.untracked_files.is_empty() {
                            let ix = ctx.repo.untracked_files.len() - 1;
                            untracked_file_selection(ix, VDir::Up, ctx)
                        } else {
                            let ix = ctx.repo.short_log.len() - 1;
                            status_log_selection(ix, VDir::Down, ctx)
                        }
                    } else {
                        let ix = log_ix - 1;
                        status_log_selection(ix, VDir::Up, ctx)
                    }
                }
            };
            (Some(selection), task)
        }
        None => {
            if !ctx.repo.short_log.is_empty() {
                let ix = ctx.repo.short_log.len() - 1;
                let (selection, task) = status_log_selection(ix, VDir::Up, ctx);
                (Some(selection), task)
            } else if !ctx.repo.changed_files.is_empty() {
                let ix = ctx.repo.changed_files.len() - 1;
                let (selection, task) =
                    changed_file_selection(ix, VDir::Up, ctx);
                (Some(selection), task)
            } else if !ctx.repo.untracked_files.is_empty() {
                let ix = ctx.repo.untracked_files.len() - 1;
                let (selection, task) =
                    untracked_file_selection(ix, VDir::Up, ctx);
                (Some(selection), task)
            } else {
                (None, Task::none())
            }
        }
    };

    ctx.state.status = selection;
    task
}

fn select_up_channel(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    let (selection, task) = match ctx.state.channel.take() {
        Some(Channel {
            ix,
            name: _,
            log: None,
        }) => {
            let ix = if ix == 0 {
                ctx.repo.other_channels.len().saturating_sub(1)
            } else {
                ix - 1
            };
            channel_selection(ix, VDir::Up, ctx)
        }
        None => {
            let ix = ctx.repo.other_channels.len().saturating_sub(1);
            channel_selection(ix, VDir::Up, ctx)
        }
        Some(Channel {
            ix: channel_ix,
            name,
            log:
                Some(LogChange {
                    ix: change_ix,
                    hash,
                    message,
                    file,
                }),
        }) => {
            if let Some(Log::Loaded { log }) =
                ctx.logs.other_channels_logs.get(&name)
            {
                if let Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected,
                }) = file
                {
                    if diff_selected {
                        let selection = Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected,
                                }),
                            }),
                        };

                        nav_scrollable::scroll_up(
                            &mut ctx
                                .navigation
                                .other_channel_logs_navs
                                .diffs_nav,
                            delta,
                        );

                        (selection, Task::none())
                    } else {
                        let log_entry = log.get(change_ix).unwrap();

                        let file_ix = if 0 == file_ix {
                            log_entry.file_paths.len().saturating_sub(1)
                        } else {
                            file_ix - 1
                        };

                        let (file, selection_task) = channel_log_file_selection(
                            file_ix,
                            hash,
                            VDir::Up,
                            ctx.navigation,
                            log_entry,
                        );

                        let selection = Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: Some(file),
                            }),
                        };
                        (selection, selection_task)
                    }
                } else {
                    let (selection, task) = if 0 == change_ix {
                        let ix = log.len().saturating_sub(1);
                        channel_log_selection(ix, VDir::Down, ctx, log)
                    } else {
                        let ix = change_ix - 1;
                        channel_log_selection(ix, VDir::Up, ctx, log)
                    };

                    let selection = Channel {
                        ix: channel_ix,
                        name,
                        log: Some(selection),
                    };
                    (selection, task)
                }
            } else {
                let selection = Channel {
                    ix: channel_ix,
                    name,
                    log: Some(LogChange {
                        ix: change_ix,
                        hash,
                        message,
                        file,
                    }),
                };
                (selection, Task::none())
            }
        }
    };
    ctx.state.channel = Some(selection);
    task
}

fn select_up_entire_log(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    if let Some(Log::Loaded { log }) = ctx.logs.entire_log.as_ref() {
        let (selection, task) = if let Some(LogChange {
            ix: log_ix,
            hash,
            message,
            file,
        }) = ctx.state.entire_log.take()
        {
            if let Some(LogChangeFileSelection {
                ix: file_ix,
                path,
                diff_selected,
            }) = file
            {
                if diff_selected {
                    let selection = LogChange {
                        ix: log_ix,
                        hash,
                        message,
                        file: Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected,
                        }),
                    };

                    nav_scrollable::scroll_up(
                        &mut ctx.navigation.entire_logs_navs.diffs_nav,
                        delta,
                    );

                    (selection, Task::none())
                } else {
                    let log_entry = log.get(log_ix).unwrap();

                    let file_ix = if 0 == file_ix {
                        log_entry.file_paths.len().saturating_sub(1)
                    } else {
                        file_ix - 1
                    };

                    let (file, selection_task) = entire_log_file_selection(
                        file_ix,
                        hash,
                        VDir::Up,
                        ctx.navigation,
                        log_entry,
                    );

                    let selection = LogChange {
                        ix: log_ix,
                        hash,
                        message,
                        file: Some(file),
                    };
                    (selection, selection_task)
                }
            } else if 0 == log_ix {
                let ix = log.len().saturating_sub(1);
                entire_log_selection(ix, VDir::Down, ctx, log)
            } else {
                let ix = log_ix - 1;
                entire_log_selection(ix, VDir::Up, ctx, log)
            }
        } else {
            let ix = 0;
            entire_log_selection(ix, VDir::Down, ctx, log)
        };
        ctx.state.entire_log = Some(selection);
        return task;
    }
    Task::none()
}

fn select_up_compare_remote(
    ctx: &mut Ctx<'_>,
    delta: Option<Duration>,
) -> Task<crate::ManagingRepoMsg> {
    if let Some(record_dichotomy) = ctx.record_dichotomy {
        let len = record_dichotomy.len();
        if len > 0
            && let Some(CompareRemote {
                ix,
                hash,
                file,
                remote,
                remote_channel: channel,
            }) = ctx.state.compare_remote.take()
        {
            let (selection, task) = if let (Some(change_ix), Some(hash)) =
                (ix, hash)
            {
                if let Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected,
                }) = file
                {
                    if diff_selected {
                        let selection = CompareRemote {
                            ix: Some(change_ix),
                            hash: Some(hash),
                            file: Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected,
                            }),
                            remote,
                            remote_channel: channel,
                        };

                        nav_scrollable::scroll_up(
                            &mut ctx.navigation.compare_remote_navs.diffs_nav,
                            delta,
                        );

                        (selection, Task::none())
                    } else {
                        let log_entry =
                            record_dichotomy.get(change_ix).unwrap();

                        let file_ix = if 0 == file_ix {
                            log_entry.file_paths.len().saturating_sub(1)
                        } else {
                            file_ix - 1
                        };

                        let (file, selection_task) =
                            compare_remote_file_selection(
                                file_ix,
                                hash,
                                VDir::Up,
                                ctx.navigation,
                                log_entry,
                            );

                        let selection = CompareRemote {
                            ix: Some(change_ix),
                            hash: Some(hash),
                            file: Some(file),
                            remote,
                            remote_channel: channel,
                        };
                        (selection, selection_task)
                    }
                } else {
                    if change_ix == 0 {
                        let ix = len.saturating_sub(1);
                        compare_remote_selection(
                            ix,
                            VDir::Up,
                            ctx,
                            record_dichotomy,
                            remote,
                            channel,
                        )
                    } else {
                        let ix = change_ix - 1;
                        compare_remote_selection(
                            ix,
                            VDir::Up,
                            ctx,
                            record_dichotomy,
                            remote,
                            channel,
                        )
                    }
                }
            } else {
                let ix = len.saturating_sub(1);
                compare_remote_selection(
                    ix,
                    VDir::Down,
                    ctx,
                    record_dichotomy,
                    remote,
                    channel,
                )
            };
            ctx.state.compare_remote = Some(selection);
            return task;
        }
    }
    Task::none()
}

fn alt_select_down<M>(ctx: &mut Ctx<'_>, delta: Option<Duration>) -> Task<M> {
    match ctx.state.status.as_mut() {
        Some(Status::UntrackedFile {
            diff_selected: true,
            ..
        }) => {
            nav_scrollable::alt_scroll_down(
                &mut ctx.navigation.files_diffs.diffs_nav,
                delta,
            );

            Task::none()
        }
        Some(Status::ChangedFile {
            diff_selected: true,
            ..
        }) => {
            nav_scrollable::alt_scroll_down(
                &mut ctx.navigation.files_diffs.diffs_nav,
                delta,
            );

            Task::none()
        }
        Some(Status::LogChange(LogChange {
            file:
                Some(LogChangeFileSelection {
                    diff_selected: true,
                    ..
                }),
            ..
        })) => {
            nav_scrollable::alt_scroll_down(
                &mut ctx.navigation.status_logs_navs.diffs_nav,
                delta,
            );

            Task::none()
        }
        Some(Status::UntrackedFile { .. })
        | Some(Status::ChangedFile { .. })
        | Some(Status::LogChange(LogChange { .. }))
        | None => Task::none(),
    }
}

fn alt_select_up<M>(ctx: &mut Ctx<'_>, delta: Option<Duration>) -> Task<M> {
    match ctx.state.status.as_mut() {
        Some(Status::UntrackedFile {
            diff_selected: true,
            ..
        }) => {
            nav_scrollable::alt_scroll_up(
                &mut ctx.navigation.files_diffs.diffs_nav,
                delta,
            );

            Task::none()
        }
        Some(Status::ChangedFile {
            diff_selected: true,
            ..
        }) => {
            nav_scrollable::alt_scroll_up(
                &mut ctx.navigation.files_diffs.diffs_nav,
                delta,
            );

            Task::none()
        }
        Some(Status::LogChange(LogChange {
            file:
                Some(LogChangeFileSelection {
                    diff_selected: true,
                    ..
                }),
            ..
        })) => {
            nav_scrollable::alt_scroll_up(
                &mut ctx.navigation.status_logs_navs.diffs_nav,
                delta,
            );

            Task::none()
        }
        Some(Status::UntrackedFile { .. })
        | Some(Status::ChangedFile { .. })
        | Some(Status::LogChange(LogChange { .. }))
        | None => Task::none(),
    }
}

fn select_left(ctx: &mut Ctx<'_>) -> Task<crate::ManagingRepoMsg> {
    match ctx.state.primary {
        Primary::Status => {
            let (selection, task): (
                Option<Status>,
                Task<crate::ManagingRepoMsg>,
            ) = match ctx.state.status.take() {
                Some(Status::LogChange(LogChange {
                    ix,
                    hash,
                    message,
                    file:
                        Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected,
                        }),
                })) => {
                    if diff_selected {
                        (
                            Some(Status::LogChange(LogChange {
                                ix,
                                hash,
                                message,
                                file: Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected: false,
                                }),
                            })),
                            Task::none(),
                        )
                    } else {
                        let selection = Status::LogChange(LogChange {
                            ix,
                            hash,
                            message,
                            file: None,
                        });
                        (Some(selection), Task::none())
                    }
                }
                Some(Status::UntrackedFile {
                    ix,
                    path,
                    diff_selected: true,
                }) => (
                    Some(Status::UntrackedFile {
                        ix,
                        path,
                        diff_selected: false,
                    }),
                    Task::none(),
                ),
                Some(Status::ChangedFile {
                    ix,
                    path,
                    diff_selected: true,
                }) => (
                    Some(Status::ChangedFile {
                        ix,
                        path,
                        diff_selected: false,
                    }),
                    Task::none(),
                ),
                selection @ (Some(Status::UntrackedFile { .. })
                | Some(Status::ChangedFile { .. })
                | Some(Status::LogChange(LogChange {
                    file: None,
                    ..
                }))
                | None) => (selection, Task::none()),
            };
            ctx.state.status = selection;
            task
        }
        Primary::Channel => {
            let (selection, task) = match ctx.state.channel.take() {
                Some(Channel {
                    ix: channel_ix,
                    name,
                    log: Some(LogChange { file: None, .. }),
                }) => {
                    let selection = Channel {
                        ix: channel_ix,
                        name,
                        log: None,
                    };
                    (Some(selection), Task::none())
                }
                Some(Channel {
                    ix: channel_ix,
                    name,
                    log:
                        Some(LogChange {
                            ix: change_ix,
                            hash,
                            message,
                            file:
                                Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected,
                                }),
                        }),
                }) => {
                    if diff_selected {
                        let selection = Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected: false,
                                }),
                            }),
                        };
                        (Some(selection), Task::none())
                    } else {
                        let selection = Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: None,
                            }),
                        };
                        (Some(selection), Task::none())
                    }
                }
                selection @ (Some(Channel { .. }) | None) => {
                    (selection, Task::none())
                }
            };
            ctx.state.channel = selection;
            task
        }
        Primary::EntireLog => {
            let (selection, task) = match ctx.state.entire_log.take() {
                Some(LogChange {
                    ix,
                    hash,
                    message,
                    file:
                        Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected,
                        }),
                }) => {
                    if diff_selected {
                        (
                            Some(LogChange {
                                ix,
                                hash,
                                message,
                                file: Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected: false,
                                }),
                            }),
                            Task::none(),
                        )
                    } else {
                        let selection = LogChange {
                            ix,
                            hash,
                            message,
                            file: None,
                        };
                        (Some(selection), Task::none())
                    }
                }
                selection @ (Some(LogChange { file: None, .. }) | None) => {
                    (selection, Task::none())
                }
            };
            ctx.state.entire_log = selection;
            task
        }
        Primary::CompareRemote => {
            let (selection, task) = match ctx.state.compare_remote.take() {
                Some(CompareRemote {
                    ix,
                    hash,
                    file:
                        Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected,
                        }),
                    remote,
                    remote_channel: channel,
                }) => {
                    if diff_selected {
                        (
                            Some(CompareRemote {
                                ix,
                                hash,
                                file: Some(LogChangeFileSelection {
                                    ix: file_ix,
                                    path,
                                    diff_selected: false,
                                }),
                                remote,
                                remote_channel: channel,
                            }),
                            Task::none(),
                        )
                    } else {
                        let selection = CompareRemote {
                            ix,
                            hash,
                            file: None,
                            remote,
                            remote_channel: channel,
                        };
                        (Some(selection), Task::none())
                    }
                }
                selection @ (Some(CompareRemote { file: None, .. }) | None) => {
                    (selection, Task::none())
                }
            };
            ctx.state.compare_remote = selection;
            task
        }
    }
}

fn select_right(ctx: &mut Ctx<'_>) -> Task<crate::ManagingRepoMsg> {
    match ctx.state.primary {
        Primary::Status => select_right_status(ctx),
        Primary::Channel => select_right_channel(ctx),
        Primary::EntireLog => select_right_entire_log(ctx),
        Primary::CompareRemote => select_right_compare_remote(ctx),
    }
}

fn select_right_status(ctx: &mut Ctx<'_>) -> Task<crate::ManagingRepoMsg> {
    let (selection, task): (Option<Status>, Task<crate::ManagingRepoMsg>) =
        match ctx.state.status.take() {
            Some(Status::UntrackedFile {
                ix,
                path,
                diff_selected: false,
            }) => {
                let diff_selected = diff::file_diff_needs_scrolling(
                    &ctx.navigation.files_diffs,
                );
                (
                    Some(Status::UntrackedFile {
                        ix,
                        path,
                        diff_selected,
                    }),
                    Task::none(),
                )
            }
            Some(Status::ChangedFile {
                ix,
                path,
                diff_selected: false,
            }) => {
                // Always allow right move for `to_record` selection
                let diff_selected = true;
                (
                    Some(Status::ChangedFile {
                        ix,
                        path,
                        diff_selected,
                    }),
                    Task::none(),
                )
            }
            Some(Status::LogChange(LogChange {
                ix,
                hash,
                message,
                file: None,
            })) => {
                let log_entry = ctx.repo.short_log.get(ix).unwrap();
                let (file, task) = if let Some(path) =
                    log_entry.file_paths.first()
                {
                    let file_id = file::log_id_parts_hash(log_entry.hash, path);
                    // If the log is not loaded yet, the nav will be initialized
                    // once it's loaded (`repo::MsgOut::GotChangeDiffs`)
                    if let Some(log) =
                        ctx.navigation.log_diffs.diffs.get(&file_id)
                    {
                        // Init log diffs nav
                        let unchanged_sections =
                            diff::unchanged_sections(&log.file);
                        log::init_diffs_nav(
                            &mut ctx.navigation.status_logs_navs,
                            file_id,
                        )
                        .set_skip_sections(unchanged_sections);
                    };

                    let (selection, task) = status_log_file_selection(
                        0,
                        hash,
                        VDir::Down,
                        ctx.navigation,
                        log_entry,
                    );
                    (Some(selection), task)
                } else {
                    (None, Task::none())
                };
                (
                    Some(Status::LogChange(LogChange {
                        ix,
                        hash,
                        message,
                        file,
                    })),
                    task,
                )
            }
            Some(Status::LogChange(LogChange {
                ix,
                hash,
                message,
                file:
                    Some(LogChangeFileSelection {
                        ix: file_ix,
                        path,
                        diff_selected: false,
                    }),
            })) => {
                let is_diff_scrollable =
                    log::diff_needs_scrolling(&ctx.navigation.status_logs_navs);
                (
                    Some(Status::LogChange(LogChange {
                        ix,
                        hash,
                        message,
                        file: Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected: is_diff_scrollable,
                        }),
                    })),
                    Task::none(),
                )
            }
            selection => (selection, Task::none()),
        };
    ctx.state.status = selection;
    task
}

fn select_right_channel(ctx: &mut Ctx<'_>) -> Task<crate::ManagingRepoMsg> {
    if let Some(channel) = ctx.state.channel.take() {
        let (selection, task) = match channel {
            Channel {
                ix: channel_ix,
                name,
                log: None,
            } => {
                if let Some(Log::Loaded { log }) =
                    ctx.logs.other_channels_logs.get(&name)
                {
                    let change_ix = 0;
                    let (log_selection, task) =
                        channel_log_selection(change_ix, VDir::Down, ctx, log);
                    let selection = Some(Channel {
                        ix: channel_ix,
                        name,
                        log: Some(log_selection),
                    });
                    (selection, task)
                } else {
                    (
                        Some(Channel {
                            ix: channel_ix,
                            name,
                            log: None,
                        }),
                        Task::none(),
                    )
                }
            }
            Channel {
                ix: channel_ix,
                name,
                log:
                    Some(LogChange {
                        ix: change_ix,
                        hash,
                        message,
                        file: None,
                    }),
            } => {
                if let Some(Log::Loaded { log }) =
                    ctx.logs.other_channels_logs.get(&name)
                {
                    let log_entry = log.get(change_ix).unwrap();
                    let (file, task) =
                        if let Some(path) = log_entry.file_paths.first() {
                            let file_id =
                                file::log_id_parts_hash(log_entry.hash, path);
                            // If the log is not loaded yet, the nav will be
                            // initialized once it's
                            // loaded (`repo::MsgOut::GotChangeDiffs`)
                            if let Some(log) =
                                ctx.navigation.log_diffs.diffs.get(&file_id)
                            {
                                let unchanged_sections =
                                    diff::unchanged_sections(&log.file);
                                log::init_diffs_nav(
                                    &mut ctx.navigation.other_channel_logs_navs,
                                    file_id,
                                )
                                .set_skip_sections(unchanged_sections);
                            };

                            let (file, task) = channel_log_file_selection(
                                0,
                                hash,
                                VDir::Down,
                                ctx.navigation,
                                log_entry,
                            );
                            (Some(file), task)
                        } else {
                            (None, Task::none())
                        };

                    (
                        Some(Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file,
                            }),
                        }),
                        task,
                    )
                } else {
                    (
                        Some(Channel {
                            ix: channel_ix,
                            name,
                            log: Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file: None,
                            }),
                        }),
                        Task::none(),
                    )
                }
            }
            Channel {
                ix: channel_ix,
                name,
                log:
                    Some(LogChange {
                        ix,
                        hash,
                        message,
                        file:
                            Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected: false,
                            }),
                    }),
            } => {
                let navs = &ctx.navigation.other_channel_logs_navs;
                let is_diff_scrollable = log::diff_needs_scrolling(navs);
                (
                    Some(Channel {
                        ix: channel_ix,
                        name,
                        log: Some(LogChange {
                            ix,
                            hash,
                            message,
                            file: Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected: is_diff_scrollable,
                            }),
                        }),
                    }),
                    Task::none(),
                )
            }
            selection @ Channel { .. } => (Some(selection), Task::none()),
        };
        ctx.state.channel = selection;
        return task;
    }
    Task::none()
}

fn select_right_entire_log(ctx: &mut Ctx<'_>) -> Task<crate::ManagingRepoMsg> {
    if let Some(Log::Loaded { log }) = ctx.logs.entire_log.as_ref()
        && let Some(entire_log) = ctx.state.entire_log.take()
    {
        let (selection, task) = match entire_log {
            LogChange {
                ix,
                hash,
                message,
                file: None,
            } => {
                let log_entry = log.get(ix).unwrap();
                let (file, task) = if let Some(path) =
                    log_entry.file_paths.first()
                {
                    let file_id = file::log_id_parts_hash(log_entry.hash, path);
                    // If the log is not loaded yet, the nav will be
                    // initialized once it's
                    // loaded (`repo::MsgOut::GotChangeDiffs`)
                    if let Some(log) =
                        ctx.navigation.log_diffs.diffs.get(&file_id)
                    {
                        let unchanged_sections =
                            diff::unchanged_sections(&log.file);
                        log::init_diffs_nav(
                            &mut ctx.navigation.entire_logs_navs,
                            file_id,
                        )
                        .set_skip_sections(unchanged_sections);
                    };

                    (
                        Some(LogChangeFileSelection {
                            ix: 0,
                            path: path.clone(),
                            diff_selected: false,
                        }),
                        Task::none(),
                    )
                } else {
                    (None, Task::none())
                };
                (
                    Some(LogChange {
                        ix,
                        hash,
                        message,
                        file,
                    }),
                    task,
                )
            }
            LogChange {
                ix,
                hash,
                message,
                file:
                    Some(LogChangeFileSelection {
                        ix: file_ix,
                        path,
                        diff_selected: false,
                    }),
            } => {
                let is_diff_scrollable =
                    log::diff_needs_scrolling(&ctx.navigation.entire_logs_navs);
                (
                    Some(LogChange {
                        ix,
                        hash,
                        message,
                        file: Some(LogChangeFileSelection {
                            ix: file_ix,
                            path,
                            diff_selected: is_diff_scrollable,
                        }),
                    }),
                    Task::none(),
                )
            }
            selection @ LogChange { .. } => (Some(selection), Task::none()),
        };
        ctx.state.entire_log = selection;
        return task;
    }
    Task::none()
}

fn select_right_compare_remote(
    ctx: &mut Ctx<'_>,
) -> Task<crate::ManagingRepoMsg> {
    if let Some(record_dichotomy) = ctx.record_dichotomy {
        let len = record_dichotomy.len();
        if len > 0
            && let Some(CompareRemote {
                ix: Some(change_ix),
                hash: Some(hash),
                file,
                remote,
                remote_channel: channel,
            }) = ctx.state.compare_remote.take()
        {
            let (selection, task) = match file {
                None => {
                    let log_entry = record_dichotomy.get(change_ix).unwrap();
                    let (file, task) =
                        if let Some(path) = log_entry.file_paths.first() {
                            let file_id =
                                file::log_id_parts_hash(log_entry.hash, path);
                            // If the log is not loaded yet, the nav will be
                            // initialized once it's
                            // loaded (`repo::MsgOut::GotChangeDiffs`)
                            if let Some(log) =
                                ctx.navigation.log_diffs.diffs.get(&file_id)
                            {
                                let unchanged_sections =
                                    diff::unchanged_sections(&log.file);
                                log::init_diffs_nav(
                                    &mut ctx.navigation.compare_remote_navs,
                                    file_id,
                                )
                                .set_skip_sections(unchanged_sections);
                            };

                            (
                                Some(LogChangeFileSelection {
                                    ix: 0,
                                    path: path.clone(),
                                    diff_selected: false,
                                }),
                                Task::none(),
                            )
                        } else {
                            (None, Task::none())
                        };
                    (
                        Some(CompareRemote {
                            ix: Some(change_ix),
                            hash: Some(hash),
                            file,
                            remote,
                            remote_channel: channel,
                        }),
                        task,
                    )
                }
                Some(LogChangeFileSelection {
                    ix: file_ix,
                    path,
                    diff_selected: false,
                }) => {
                    let is_diff_scrollable = log::diff_needs_scrolling(
                        &ctx.navigation.compare_remote_navs,
                    );
                    (
                        Some(CompareRemote {
                            ix: Some(change_ix),
                            hash: Some(hash),
                            file: Some(LogChangeFileSelection {
                                ix: file_ix,
                                path,
                                diff_selected: is_diff_scrollable,
                            }),
                            remote,
                            remote_channel: channel,
                        }),
                        Task::none(),
                    )
                }
                file => (
                    Some(CompareRemote {
                        ix: Some(change_ix),
                        hash: Some(hash),
                        file,
                        remote,
                        remote_channel: channel,
                    }),
                    Task::none(),
                ),
            };
            ctx.state.compare_remote = selection;
            return task;
        }
    }
    Task::none()
}

fn release<M>(dir: Dir, ctx: &mut Ctx<'_>) -> Task<M> {
    let Ctx {
        state,
        files: _,
        navigation,
        repo: _,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    if state.held_key.as_ref().map(|key| key.dir) == Some(dir) {
        state.held_key = None;

        if let Dir::Down | Dir::Up = dir {
            match state.status.as_mut() {
                Some(Status::UntrackedFile {
                    diff_selected: true,
                    ..
                }) => {
                    let nav = &mut navigation.files_diffs.diffs_nav;
                    nav_scrollable::reset_skip_delay(nav);
                }
                Some(Status::ChangedFile {
                    diff_selected: true,
                    ..
                }) => {
                    let nav = &mut navigation.files_diffs.diffs_nav;
                    nav_scrollable::reset_skip_delay(nav);
                }
                Some(Status::LogChange(LogChange {
                    file:
                        Some(LogChangeFileSelection {
                            diff_selected: true,
                            ..
                        }),
                    ..
                })) => {
                    let nav = &mut navigation.status_logs_navs.diffs_nav;
                    nav_scrollable::reset_skip_delay(nav);
                }
                Some(Status::UntrackedFile { .. })
                | Some(Status::ChangedFile { .. })
                | Some(Status::LogChange(LogChange { .. }))
                | None => {}
            }
        }
    }
    Task::none()
}

fn select_exact(
    select: Select,
    ctx: &mut Ctx<'_>,
) -> Task<crate::ManagingRepoMsg> {
    match ctx.state.primary {
        Primary::Status => {
            let (selection, task) = match select {
                Select::UntrackedFile { ix, path } => {
                    let id = file::Id {
                        path: path.clone(),
                        file_kind: file::Kind::Untracked,
                    };
                    file::load_src_file_if_not_cached(ctx.files, id);
                    let (selection, selection_task) =
                        untracked_file_selection(ix, VDir::Down, ctx);
                    (Some(selection), selection_task)
                }
                Select::ChangedFile { ix, path } => {
                    let id = file::Id {
                        path: path.clone(),
                        file_kind: file::Kind::Changed,
                    };
                    let file_task = if let Some(diffs) =
                        ctx.repo.changed_files.get(&path)
                    {
                        if diff::should_file_exist(diffs) {
                            file::load_src_file_if_not_cached(ctx.files, id);
                            Task::none()
                        } else {
                            file::src_file_doesnt_exist(ctx.files, id, diffs)
                                .map(crate::ManagingRepoMsg::File)
                        }
                    } else {
                        Task::none()
                    };
                    let (selection, selection_task) =
                        changed_file_selection(ix, VDir::Down, ctx);
                    (Some(selection), Task::batch([selection_task, file_task]))
                }
                Select::LogChange {
                    ix,
                    hash: _,
                    message: _,
                } => {
                    let (selection, task) =
                        status_log_selection(ix, VDir::Down, ctx);
                    (Some(selection), task)
                }
                Select::LogChangeFile {
                    ix: file_ix,
                    path: _,
                } => match ctx.state.status.take() {
                    Some(Status::LogChange(LogChange {
                        ix: log_ix,
                        hash,
                        message,
                        file: _,
                    })) => {
                        let log_entry = ctx.repo.short_log.get(log_ix).unwrap();

                        let (file, selection_task) = status_log_file_selection(
                            file_ix,
                            hash,
                            VDir::Down,
                            ctx.navigation,
                            log_entry,
                        );

                        let selection = Some(Status::LogChange(LogChange {
                            ix: log_ix,
                            hash,
                            message,
                            file: Some(file),
                        }));
                        (selection, selection_task)
                    }
                    selection => (selection, Task::none()),
                },
                Select::Channel { .. }
                | Select::CompareRemote { .. }
                | Select::CompareRemoteFile { .. } => {
                    unreachable!()
                }
            };
            ctx.state.status = selection;
            return task;
        }
        Primary::Channel => {
            let (selection, task) = match select {
                Select::Channel { ix, name: _ } => {
                    let (selection, task) =
                        channel_selection(ix, VDir::Down, ctx);
                    (Some(selection), task)
                }
                Select::LogChange {
                    ix,
                    hash: _,
                    message: _,
                } => match ctx.state.channel.take() {
                    Some(Channel {
                        ix: channel_ix,
                        name,
                        log,
                    }) => {
                        if let Some(Log::Loaded { log }) =
                            ctx.logs.other_channels_logs.get(&name)
                        {
                            let (selection, task) =
                                channel_log_selection(ix, VDir::Down, ctx, log);
                            (
                                Some(Channel {
                                    ix: channel_ix,
                                    name,
                                    log: Some(selection),
                                }),
                                task,
                            )
                        } else {
                            (
                                Some(Channel {
                                    ix: channel_ix,
                                    name,
                                    log,
                                }),
                                Task::none(),
                            )
                        }
                    }
                    selection => (selection, Task::none()),
                },
                Select::LogChangeFile {
                    ix: file_ix,
                    path: _,
                } => match ctx.state.channel.take() {
                    Some(Channel {
                        ix: channel_ix,
                        name,
                        log:
                            Some(LogChange {
                                ix: change_ix,
                                hash,
                                message,
                                file,
                            }),
                    }) => {
                        if let Some(Log::Loaded { log }) =
                            ctx.logs.other_channels_logs.get(&name)
                            && let Some(log_entry) = log.get(change_ix)
                        {
                            let (selection, task) = channel_log_file_selection(
                                file_ix,
                                hash,
                                VDir::Down,
                                ctx.navigation,
                                log_entry,
                            );
                            (
                                Some(Channel {
                                    ix: channel_ix,
                                    name,
                                    log: Some(LogChange {
                                        ix: change_ix,
                                        hash,
                                        message,
                                        file: Some(selection),
                                    }),
                                }),
                                task,
                            )
                        } else {
                            (
                                Some(Channel {
                                    ix: channel_ix,
                                    name,
                                    log: Some(LogChange {
                                        ix: change_ix,
                                        hash,
                                        message,
                                        file,
                                    }),
                                }),
                                Task::none(),
                            )
                        }
                    }
                    selection => (selection, Task::none()),
                },
                Select::UntrackedFile { .. }
                | Select::ChangedFile { .. }
                | Select::CompareRemote { .. }
                | Select::CompareRemoteFile { .. } => {
                    unreachable!()
                }
            };
            ctx.state.channel = selection;
            return task;
        }
        Primary::EntireLog => {
            if let Some(Log::Loaded { log }) = ctx.logs.entire_log.as_ref() {
                let (selection, task) = match select {
                    Select::LogChange {
                        ix,
                        hash: _,
                        message: _,
                    } => {
                        let (selection, task) =
                            entire_log_selection(ix, VDir::Down, ctx, log);
                        (Some(selection), task)
                    }
                    Select::LogChangeFile {
                        ix: file_ix,
                        path: _,
                    } => match ctx.state.entire_log.take() {
                        Some(LogChange {
                            ix: log_ix,
                            hash,
                            message,
                            file: _,
                        }) => {
                            let log_entry = log.get(log_ix).unwrap();

                            let (file, selection_task) =
                                entire_log_file_selection(
                                    file_ix,
                                    hash,
                                    VDir::Down,
                                    ctx.navigation,
                                    log_entry,
                                );

                            let selection = Some(LogChange {
                                ix: log_ix,
                                hash,
                                message,
                                file: Some(file),
                            });
                            (selection, selection_task)
                        }
                        selection => (selection, Task::none()),
                    },
                    Select::UntrackedFile { .. }
                    | Select::ChangedFile { .. }
                    | Select::Channel { .. }
                    | Select::CompareRemote { .. }
                    | Select::CompareRemoteFile { .. } => unreachable!(),
                };
                ctx.state.entire_log = selection;
                return task;
            }
        }
        Primary::CompareRemote => {
            if let Some((
                record_dichotomy,
                CompareRemote {
                    ix: record_ix,
                    hash,
                    file: _,
                    remote,
                    remote_channel: channel,
                },
            )) = ctx.record_dichotomy.zip(ctx.state.compare_remote.take())
            {
                let (selection, task) = match select {
                    Select::CompareRemote { ix } => {
                        let (selection, task) = compare_remote_selection(
                            ix,
                            VDir::Down,
                            ctx,
                            record_dichotomy,
                            remote,
                            channel,
                        );
                        (Some(selection), task)
                    }
                    Select::CompareRemoteFile {
                        ix: file_ix,
                        path: _,
                    } => {
                        let record_ix = record_ix.unwrap();
                        let entry = record_dichotomy.get(record_ix).unwrap();
                        let (file, task) = compare_remote_file_selection(
                            file_ix,
                            hash.unwrap(),
                            VDir::Down,
                            ctx.navigation,
                            entry,
                        );
                        let selection = Some(CompareRemote {
                            ix: Some(record_ix),
                            hash,
                            file: Some(file),
                            remote,
                            remote_channel: channel,
                        });
                        (selection, task)
                    }
                    Select::UntrackedFile { .. }
                    | Select::ChangedFile { .. }
                    | Select::LogChange { .. }
                    | Select::LogChangeFile { .. }
                    | Select::Channel { .. } => unreachable!(),
                };
                ctx.state.compare_remote = selection;
                return task;
            }
        }
    }
    Task::none()
}

enum StatusSectionKind {
    Untracked,
    Changed,
    Log,
}

fn status_section_ix(
    repo: &repo::State,
    ix: usize,
    kind: StatusSectionKind,
) -> usize {
    // The literals are for section headers
    match kind {
        StatusSectionKind::Untracked => 1 + ix,
        StatusSectionKind::Changed => 2 + repo.untracked_files.len() + ix,
        StatusSectionKind::Log => {
            3 + repo.untracked_files.len() + repo.changed_files.len() + ix
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum VDir {
    Up,
    Down,
}

pub fn untracked_file_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
) -> (Status, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files,
        navigation,
        repo,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    let path = repo.untracked_files.iter().nth(ix).unwrap();

    let file_id = file::id_parts_hash(path, file::Kind::Untracked);
    match file::try_get_src_file(files, file_id) {
        Some(file_diff) => {
            // If the diff is already loaded init nav for it
            let unchanged_sections = diff::unchanged_sections(file_diff);
            diff::init_diffs_nav(
                &mut navigation.files_diffs,
                #[cfg(debug_assertions)]
                file_id,
            )
            .set_skip_sections(unchanged_sections);
        }
        None => {
            // Nav will be initialize once the diff is loaded
            // (`crate::Msg::File`)
            let id = file::Id {
                path: path.clone(),
                file_kind: file::Kind::Untracked,
            };
            file::load_src_file_if_not_cached(files, id);
        }
    }

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.status_nav,
            status_section_ix(repo, ix, StatusSectionKind::Untracked),
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.status_nav,
            status_section_ix(repo, ix, StatusSectionKind::Untracked),
        ),
    }

    let selection = Status::UntrackedFile {
        ix,
        path: path.clone(),
        diff_selected: false,
    };

    (selection, Task::none())
}

pub fn changed_file_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
) -> (Status, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files,
        navigation,
        repo,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    let (path, diffs) = repo.changed_files.iter().nth(ix).unwrap();

    let file_id = file::id_parts_hash(path, file::Kind::Changed);
    let task = match file::try_get_src_file(files, file_id) {
        Some(file_diff) => {
            // If the diff is already loaded init nav for it
            let unchanged_sections = diff::unchanged_sections(file_diff);
            diff::init_diffs_nav(
                &mut navigation.files_diffs,
                #[cfg(debug_assertions)]
                file_id,
            )
            .set_skip_sections(unchanged_sections);
            Task::none()
        }
        None => {
            // Nav will be initialize once the diff is loaded
            // (`crate::Msg::File`)
            let id = file::Id {
                path: path.clone(),
                file_kind: file::Kind::Changed,
            };
            if diff::should_file_exist(diffs) {
                file::load_src_file_if_not_cached(files, id);
                Task::none()
            } else {
                file::src_file_doesnt_exist(files, id, diffs)
                    .map(crate::ManagingRepoMsg::File)
            }
        }
    };

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.status_nav,
            status_section_ix(repo, ix, StatusSectionKind::Changed),
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.status_nav,
            status_section_ix(repo, ix, StatusSectionKind::Changed),
        ),
    };

    let selection = Status::ChangedFile {
        ix,
        path: path.clone(),
        diff_selected: false,
    };

    (selection, task)
}

fn status_log_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
) -> (Status, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files: _,
        navigation,
        repo,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    let entry = repo.short_log.get(ix).unwrap();
    let hash = entry.hash;

    let task = if !navigation
        .log_diffs
        .changes_with_loaded_diffs
        .contains(&hash)
    {
        Task::done(crate::ManagingRepoMsg::ToRepo(
            repo::MsgIn::GetChangeDiffs { hash },
        ))
    } else {
        Task::none()
    };

    log::init_files_nav(&mut navigation.status_logs_navs, hash);

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.status_nav,
            status_section_ix(repo, ix, StatusSectionKind::Log),
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.status_nav,
            status_section_ix(repo, ix, StatusSectionKind::Log),
        ),
    }

    let selection = Status::LogChange(LogChange {
        ix,
        hash,
        message: entry.message.clone(),
        file: None,
    });

    (selection, task)
}

fn status_log_file_selection(
    ix: usize,
    hash: repo::ChangeHash,
    dir: VDir,
    navigation: &mut Navigation,
    log_entry: &repo::LogEntry,
) -> (LogChangeFileSelection, Task<crate::ManagingRepoMsg>) {
    let path = log_entry.file_paths.get(ix).unwrap().clone();

    log::init_diffs_nav(
        &mut navigation.status_logs_navs,
        file::log_id_parts_hash(hash, &path),
    );

    let nav = &mut navigation.status_logs_navs.files_nav;
    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(nav, ix),
        VDir::Down => nav_scrollable::scroll_down_to_section(nav, ix),
    }

    (
        LogChangeFileSelection {
            ix,
            path,
            diff_selected: false,
        },
        Task::none(),
    )
}

fn channel_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
) -> (Channel, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files: _,
        navigation,
        repo,
        logs,
        record_dichotomy: _,
    } = ctx;
    let name = repo.other_channels.iter().nth(ix).unwrap().clone();

    let task = if !logs.other_channels_logs.contains_key(&name) {
        Task::done(crate::ManagingRepoMsg::ToRepo(
            repo::MsgIn::LoadOtherChannelLog(name.clone()),
        ))
    } else {
        Task::none()
    };

    model::init_channel_nav(
        navigation,
        #[cfg(debug_assertions)]
        name.clone(),
    );

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.other_channels_nav,
            ix,
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.other_channels_nav,
            ix,
        ),
    }

    let selection = Channel {
        ix,
        name,
        log: None,
    };
    (selection, task)
}

fn channel_log_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
    log: &repo::Log,
) -> (LogChange, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files: _,
        navigation,
        repo: _,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    let entry = log.get(ix).unwrap();
    let hash = entry.hash;

    let task = if !navigation
        .log_diffs
        .changes_with_loaded_diffs
        .contains(&hash)
    {
        Task::done(crate::ManagingRepoMsg::ToRepo(
            repo::MsgIn::GetChangeDiffs { hash },
        ))
    } else {
        Task::none()
    };

    log::init_files_nav(&mut navigation.other_channel_logs_navs, hash);

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.other_channel_log_nav,
            ix,
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.other_channel_log_nav,
            ix,
        ),
    }

    let selection = LogChange {
        ix,
        hash,
        message: entry.message.clone(),
        file: None,
    };

    (selection, task)
}

fn channel_log_file_selection(
    ix: usize,
    hash: repo::ChangeHash,
    dir: VDir,
    navigation: &mut Navigation,
    log_entry: &repo::LogEntry,
) -> (LogChangeFileSelection, Task<crate::ManagingRepoMsg>) {
    let path = log_entry.file_paths.get(ix).unwrap().clone();

    log::init_diffs_nav(
        &mut navigation.other_channel_logs_navs,
        file::log_id_parts_hash(hash, &path),
    );

    let nav = &mut navigation.other_channel_logs_navs.files_nav;
    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(nav, ix),
        VDir::Down => nav_scrollable::scroll_down_to_section(nav, ix),
    }

    (
        LogChangeFileSelection {
            ix,
            path,
            diff_selected: false,
        },
        Task::none(),
    )
}

fn entire_log_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
    log: &repo::Log,
) -> (LogChange, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files: _,
        navigation,
        repo: _,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    let entry = log.get(ix).unwrap();
    let hash = entry.hash;

    let task = if !navigation
        .log_diffs
        .changes_with_loaded_diffs
        .contains(&hash)
    {
        Task::done(crate::ManagingRepoMsg::ToRepo(
            repo::MsgIn::GetChangeDiffs { hash },
        ))
    } else {
        Task::none()
    };

    log::init_files_nav(&mut navigation.entire_logs_navs, hash);

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.entire_log_nav,
            ix,
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.entire_log_nav,
            ix,
        ),
    }

    let selection = LogChange {
        ix,
        hash,
        message: entry.message.clone(),
        file: None,
    };

    (selection, task)
}

fn entire_log_file_selection(
    ix: usize,
    hash: repo::ChangeHash,
    dir: VDir,
    navigation: &mut Navigation,
    log_entry: &repo::LogEntry,
) -> (LogChangeFileSelection, Task<crate::ManagingRepoMsg>) {
    let path = log_entry.file_paths.get(ix).unwrap().clone();

    log::init_diffs_nav(
        &mut navigation.entire_logs_navs,
        file::log_id_parts_hash(hash, &path),
    );

    let nav = &mut navigation.entire_logs_navs.files_nav;
    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(nav, ix),
        VDir::Down => nav_scrollable::scroll_down_to_section(nav, ix),
    }

    (
        LogChangeFileSelection {
            ix,
            path,
            diff_selected: false,
        },
        Task::none(),
    )
}

fn compare_remote_selection(
    ix: usize,
    dir: VDir,
    ctx: &mut Ctx<'_>,
    record_dichotomy: &repo::RecordDichotomy,
    remote: String,
    channel: String,
) -> (CompareRemote, Task<crate::ManagingRepoMsg>) {
    let Ctx {
        state: _,
        files: _,
        navigation,
        repo: _,
        logs: _,
        record_dichotomy: _,
    } = ctx;
    let entry = record_dichotomy.get(ix).unwrap();
    let hash = entry.hash;

    let task = if !navigation
        .log_diffs
        .changes_with_loaded_diffs
        .contains(&hash)
    {
        Task::done(crate::ManagingRepoMsg::ToRepo(
            repo::MsgIn::GetChangeDiffs { hash },
        ))
    } else {
        Task::none()
    };

    log::init_files_nav(&mut navigation.compare_remote_navs, hash);

    // For scrolling we need to skip indices of the header sections
    let repo::RecordDichotomy {
        local_records,
        remote_records,
        remote_unrecords: _,
    } = record_dichotomy;

    let scroll_section_ix = if ix >= local_records.len() + remote_records.len()
    {
        ix + 3
    } else if ix >= local_records.len() {
        ix + 2
    } else {
        ix + 1
    };

    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(
            &mut navigation.compare_remote_nav,
            scroll_section_ix,
        ),
        VDir::Down => nav_scrollable::scroll_down_to_section(
            &mut navigation.compare_remote_nav,
            scroll_section_ix,
        ),
    }

    let selection = CompareRemote {
        ix: Some(ix),
        hash: Some(hash),
        file: None,
        remote,
        remote_channel: channel,
    };

    (selection, task)
}

fn compare_remote_file_selection(
    ix: usize,
    hash: repo::ChangeHash,
    dir: VDir,
    navigation: &mut Navigation,
    log_entry: &repo::LogEntry,
) -> (LogChangeFileSelection, Task<crate::ManagingRepoMsg>) {
    let path = log_entry.file_paths.get(ix).unwrap().clone();

    log::init_diffs_nav(
        &mut navigation.compare_remote_navs,
        file::log_id_parts_hash(hash, &path),
    );

    let nav = &mut navigation.compare_remote_navs.files_nav;
    match dir {
        VDir::Up => nav_scrollable::scroll_up_to_section(nav, ix),
        VDir::Down => nav_scrollable::scroll_down_to_section(nav, ix),
    }

    (
        LogChangeFileSelection {
            ix,
            path,
            diff_selected: false,
        },
        Task::none(),
    )
}