use crate::model::{ReadyState, RecordChanges};
use crate::{model, selection, to_record};
use iced::keyboard::key::Named;
use inflorescence_iced_widget::{dir_picker, nav_scrollable};
use libflorescence::{diff, repo};

use iced::keyboard::{Key, Modifiers as Mods};

#[derive(Debug, Clone, Hash)]
pub struct Binding {
    // NOTE: This could be derived from keys, but preferrably at compile time
    pub keys_str: &'static str,
    pub keys: ModKeys,
    pub label: &'static str,
    pub msg: Option<FilteredMsg>,
}

#[derive(Debug, Clone, Hash)]
pub enum ModKeys {
    One(ModKey),
    Two(ModKey, ModKey),
}

#[derive(Debug, Clone, Hash)]
pub struct ModKey {
    pub key: Key,
    pub mods: Mods,
}

/// Msgs that are bound to some key(s). These have to be explicitly allowed
/// depending on the current state (see [`is_allowed`]).
#[derive(Debug, Clone, Hash)]
pub enum FilteredMsg {
    Confirm,
    Cancel,
    Selection(selection::Msg),
    PostponeRecord,
    SaveRecord,
    DiscardRecord,
    ToggleRecursive,
    RmChange,
    StartRecord,
    /// Show a list of channels to switch to
    SelectChannel,
    ForkChannel,
    RefreshRepo,
    ShowEntireLog,
    FocusNext,
    FocusPrev,
    ClipboardCopy,
    ToggleReports,
    ClipboardCopyReports,
    ToRecord(to_record::Msg),
    /// Same purpose as `to_record::ToggleFile` or `to_record::ToggleChange`,
    /// but without file name for action filtering
    ToRecordToggleSelectedFileOrChange,
    EnterSubMenu(model::SubMenu),
    SubMenuPushOption(model::PushOption),
    SubMenuPullOption(model::PullOption),
    SubMenuCompareRemoteOption(model::CompareRemoteOption),
    ReloadIdentity,
}

pub fn is_allowed(allowed_actions: &[Binding], msg: &FilteredMsg) -> bool {
    allowed_actions.iter().any(
        |Binding {
             msg: allowed_msg, ..
         }| {
            if let Some(allowed_msg) = allowed_msg {
                is_same_msg(msg, allowed_msg)
            } else {
                false
            }
        },
    )
}

/// Eq used for action filters. Properties that are not determined by which
/// actions are allowed are ignored.
pub fn is_same_msg(left: &FilteredMsg, right: &FilteredMsg) -> bool {
    use FilteredMsg::*;

    match (left, right) {
        (Confirm, Confirm) => true,
        (Cancel, Cancel) => true,
        (Selection(left), Selection(right)) => left == right,
        (PostponeRecord, PostponeRecord) => true,
        (SaveRecord, SaveRecord) => true,
        (DiscardRecord, DiscardRecord) => true,
        (ToggleRecursive, ToggleRecursive) => true,
        (RmChange, RmChange) => true,
        (StartRecord, StartRecord) => true,
        (SelectChannel, SelectChannel) => true,
        (ForkChannel, ForkChannel) => true,
        (RefreshRepo, RefreshRepo) => true,
        (ShowEntireLog, ShowEntireLog) => true,
        (FocusNext, FocusNext) => true,
        (FocusPrev, FocusPrev) => true,
        (ClipboardCopy, ClipboardCopy) => true,
        (ToggleReports, ToggleReports) => true,
        (ClipboardCopyReports, ClipboardCopyReports) => true,
        (ToRecord(left), ToRecord(right)) => left == right,
        (
            ToRecordToggleSelectedFileOrChange,
            ToRecordToggleSelectedFileOrChange,
        ) => true,
        (
            ToRecord(to_record::Msg::ToggleFile { path: _ })
            | ToRecord(to_record::Msg::ToggleChange {
                path: _,
                diff_id: _,
            }),
            ToRecordToggleSelectedFileOrChange,
        ) => true,
        (
            ToRecordToggleSelectedFileOrChange,
            ToRecord(to_record::Msg::ToggleFile { path: _ })
            | ToRecord(to_record::Msg::ToggleChange {
                path: _,
                diff_id: _,
            }),
        ) => true,
        (EnterSubMenu(left), EnterSubMenu(right)) => {
            core::mem::discriminant(left) == core::mem::discriminant(right)
        }
        (SubMenuPushOption(left), SubMenuPushOption(right)) => {
            core::mem::discriminant(left) == core::mem::discriminant(right)
        }
        (SubMenuPullOption(left), SubMenuPullOption(right)) => {
            core::mem::discriminant(left) == core::mem::discriminant(right)
        }
        (
            SubMenuCompareRemoteOption(left),
            SubMenuCompareRemoteOption(right),
        ) => core::mem::discriminant(left) == core::mem::discriminant(right),
        (ReloadIdentity, ReloadIdentity) => true,
        (Confirm, _) => false,
        (Cancel, _) => false,
        (Selection(_), _) => false,
        (PostponeRecord, _) => false,
        (SaveRecord, _) => false,
        (DiscardRecord, _) => false,
        (ToggleRecursive, _) => false,
        (RmChange, _) => false,
        (StartRecord, _) => false,
        (SelectChannel, _) => false,
        (ForkChannel, _) => false,
        (RefreshRepo, _) => false,
        (ShowEntireLog, _) => false,
        (FocusNext, _) => false,
        (FocusPrev, _) => false,
        (ClipboardCopy, _) => false,
        (ToggleReports, _) => false,
        (ClipboardCopyReports, _) => false,
        (ToRecord(_), _) => false,
        (ToRecordToggleSelectedFileOrChange, _) => false,
        (EnterSubMenu(_), _) => false,
        (SubMenuPushOption(_), _) => false,
        (SubMenuPullOption(_), _) => false,
        (SubMenuCompareRemoteOption(_), _) => false,
        (ReloadIdentity, _) => false,
    }
}

impl ModKeys {
    pub fn iter(&self) -> ModKeysIter<'_> {
        ModKeysIter { keys: self, ix: 0 }
    }
}

pub struct ModKeysIter<'a> {
    keys: &'a ModKeys,
    ix: usize,
}

impl<'a> Iterator for ModKeysIter<'a> {
    type Item = &'a ModKey;

    fn next(&mut self) -> Option<Self::Item> {
        let Self { keys, ix } = self;
        match keys {
            ModKeys::One(mod_key) => {
                if *ix == 0 {
                    *ix += 1;
                    Some(mod_key)
                } else {
                    None
                }
            }
            ModKeys::Two(mod_key_0, mod_key_1) => {
                if *ix == 0 {
                    *ix += 1;
                    Some(mod_key_0)
                } else if *ix == 1 {
                    *ix += 1;
                    Some(mod_key_1)
                } else {
                    None
                }
            }
        }
    }
}

pub fn update_allowed_actions(state: &mut model::State) {
    let mut bindings = if let Some(sub_menu) = &state.sub_menu {
        match sub_menu {
            model::SubMenu::Push { remote: _, opt } => {
                if opt.is_none() {
                    vec![
                        Binding {
                            keys_str: "Enter | S-p",
                            keys: ModKeys::Two(
                                ModKey {
                                    key: Key::Named(Named::Enter),
                                    mods: Mods::NONE,
                                },
                                ModKey {
                                    key: Key::Character("p".into()),
                                    mods: Mods::SHIFT,
                                },
                            ),
                            label: "confirm push",
                            msg: Some(FilteredMsg::Confirm),
                        },
                        select_remote(Some(FilteredMsg::SubMenuPushOption(
                            model::PushOption::SelectingRemote { remote: None },
                        ))),
                        cancel(),
                    ]
                } else {
                    vec![
                        confirm("confirm remote selection"),
                        down(),
                        up(),
                        cancel(),
                    ]
                }
            }
            model::SubMenu::Pull { remote: _, opt } => {
                if opt.is_none() {
                    vec![
                        Binding {
                            keys_str: "Enter | S-f",
                            keys: ModKeys::Two(
                                ModKey {
                                    key: Key::Named(Named::Enter),
                                    mods: Mods::NONE,
                                },
                                ModKey {
                                    key: Key::Character("f".into()),
                                    mods: Mods::SHIFT,
                                },
                            ),
                            label: "confirm pull",
                            msg: Some(FilteredMsg::Confirm),
                        },
                        select_remote(Some(FilteredMsg::SubMenuPullOption(
                            model::PullOption::SelectingRemote { remote: None },
                        ))),
                        cancel(),
                    ]
                } else {
                    vec![
                        confirm("confirm remote selection"),
                        down(),
                        up(),
                        cancel(),
                    ]
                }
            }
            model::SubMenu::ResetChange => {
                vec![confirm("confirm reset selection"), cancel()]
            }
            model::SubMenu::InitRepo { path: _ } => {
                vec![confirm("confirm init repo"), cancel()]
            }
            model::SubMenu::ImportFromGit { path: _ } => {
                vec![confirm("confirm import Git repo"), cancel()]
            }
            model::SubMenu::Add { recursive } => {
                vec![
                    confirm("confirm add"),
                    toggle_recursive(*recursive),
                    cancel(),
                ]
            }
            model::SubMenu::CompareRemote {
                remote: _,
                remote_channel: _,
                opt,
            } => {
                if let Some(opt) = opt {
                    let confirm_label = match opt {
                        model::CompareRemoteOption::SelectingRemote {
                            ..
                        } => "confirm remote selection",
                        model::CompareRemoteOption::InputingRemoteChannel {
                            ..
                        } => "confirm channel input",
                    };
                    vec![confirm(confirm_label), down(), up(), cancel()]
                } else {
                    vec![
                        Binding {
                            keys_str: "Enter | S-c",
                            keys: ModKeys::Two(
                                ModKey {
                                    key: Key::Named(Named::Enter),
                                    mods: Mods::NONE,
                                },
                                ModKey {
                                    key: Key::Character("c".into()),
                                    mods: Mods::SHIFT,
                                },
                            ),
                            label: "compare remote",
                            msg: Some(FilteredMsg::Confirm),
                        },
                        select_remote(Some(
                            FilteredMsg::SubMenuCompareRemoteOption(
                                model::CompareRemoteOption::SelectingRemote {
                                    remote: None,
                                },
                            ),
                        )),
                        select_remote_channel(Some(
                            FilteredMsg::SubMenuCompareRemoteOption(
                                model::CompareRemoteOption::InputingRemoteChannel {
                                    channel: None,
                                },
                            ),
                        )),
                        cancel(),
                    ]
                }
            }
        }
    } else {
        match &state.sub {
            model::SubState::PickingRepoDir(sub) => {
                get_allowed_in_picking_repo_dir(sub)
            }
            model::SubState::ManagingRepo(sub) => {
                get_allowed_in_managing_repo(&sub.sub)
            }
            model::SubState::PickingProject(sub) => {
                get_allowed_in_picking_project(sub)
            }
        }
    };

    if !state.report.entries.is_empty() {
        let label = if !state.report.hidden {
            "hide reports"
        } else {
            "show reports"
        };
        bindings.push(Binding {
            keys_str: "S-r",
            keys: ModKeys::One(ModKey {
                key: Key::Character("r".into()),
                mods: Mods::SHIFT,
            }),
            label,
            msg: Some(FilteredMsg::ToggleReports),
        });

        if !state.report.hidden {
            bindings.push(Binding {
                keys_str: "S-C-c",
                keys: ModKeys::One(ModKey {
                    key: Key::Character("c".into()),
                    mods: Mods::SHIFT.union(Mods::CTRL),
                }),
                label: "copy reports",
                msg: Some(FilteredMsg::ClipboardCopyReports),
            });
        }
    }

    state.allowed_actions = bindings;
}

fn get_allowed_in_picking_project(
    state: &model::PickingProject,
) -> Vec<Binding> {
    let model::PickingProject {
        projects,
        is_blocking: _,
        selection: _,
        projects_nav: _,
    } = state;

    let mut actions = vec![];
    let ma = &mut actions;

    if projects.is_some() {
        push(|| confirm("confirm selection"), ma);
        push(down, ma);
        push(up, ma);
    }

    actions
}

fn get_allowed_in_picking_repo_dir(
    state: &model::PickingRepoDir,
) -> Vec<Binding> {
    let model::PickingRepoDir {
        picker:
            dir_picker::State {
                current_kind,
                matched_child_dirs,
                child_dirs,
                found_repos_dirs_pijul,
                found_repos_dirs_git,
                selection,
                ..
            },
        waiting_to_init,
    } = state;

    let down = || Binding {
        keys_str: "C-(↓| j)",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowDown),
                mods: Mods::CTRL,
            },
            ModKey {
                key: Key::Character("j".into()),
                mods: Mods::CTRL,
            },
        ),
        label: "down",
        msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
            selection::Dir::Down,
        ))),
    };
    let up = || Binding {
        keys_str: "C-(↑| k)",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowUp),
                mods: Mods::CTRL,
            },
            ModKey {
                key: Key::Character("k".into()),
                mods: Mods::CTRL,
            },
        ),
        label: "up",
        msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
            selection::Dir::Up,
        ))),
    };
    let left = || Binding {
        keys_str: "C-(←| h)",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowLeft),
                mods: Mods::CTRL,
            },
            ModKey {
                key: Key::Character("h".into()),
                mods: Mods::CTRL,
            },
        ),
        label: "left",
        msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
            selection::Dir::Left,
        ))),
    };
    let right = || Binding {
        keys_str: "C-(→| l)",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowRight),
                mods: Mods::CTRL,
            },
            ModKey {
                key: Key::Character("l".into()),
                mods: Mods::CTRL,
            },
        ),
        label: "right",
        msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
            selection::Dir::Right,
        ))),
    };

    let mut actions = vec![];
    let ma = &mut actions;

    if waiting_to_init.is_none() {
        let can_down_or_up = match selection {
            dir_picker::Selection::Input => {
                !matched_child_dirs.is_empty() || !child_dirs.is_empty()
            }
            dir_picker::Selection::SubDir(_) => true,
            dir_picker::Selection::ProjectPijul(_)
            | dir_picker::Selection::ProjectGit(_) => {
                !found_repos_dirs_pijul.is_empty()
                    || !found_repos_dirs_git.is_empty()
            }
        };

        push(
            || {
                confirm(match current_kind {
                    Some(dir_picker::RepoKind::Pijul) => {
                        "open Pijul repository"
                    }
                    Some(dir_picker::RepoKind::Git) => "import from Git",
                    None => "initialize new repository",
                })
            },
            ma,
        );
        push_if(can_down_or_up, down, ma);
        push_if(can_down_or_up, up, ma);
        push_if(
            matches!(
                selection,
                dir_picker::Selection::ProjectPijul(_)
                    | dir_picker::Selection::ProjectGit(_)
            ),
            left,
            ma,
        );
        push_if(
            matches!(
                selection,
                dir_picker::Selection::Input | dir_picker::Selection::SubDir(_)
            ),
            right,
            ma,
        );
        push(focus_next, ma);
    }
    actions
}

fn get_allowed_in_managing_repo(
    state: &model::ManagingRepoSubState,
) -> Vec<Binding> {
    match state {
        model::ManagingRepoSubState::Loading { .. } => vec![],
        model::ManagingRepoSubState::SelectingIdentity { .. } => {
            vec![confirm("confirm"), down(), up()]
        }
        model::ManagingRepoSubState::Ready(ready_state) => {
            get_ready_allowed(ready_state)
        }
        model::ManagingRepoSubState::NoIdFound { .. } => vec![Binding {
            keys_str: "r",
            keys: ModKeys::One(ModKey {
                key: Key::Character("r".into()),
                mods: Mods::NONE,
            }),
            label: "Reload identity",
            msg: Some(FilteredMsg::ReloadIdentity),
        }],
    }
}

fn get_ready_allowed(state: &ReadyState) -> Vec<Binding> {
    let State {
        has_other_channels,
        has_default_remote,
        sub,
    } = derive_state(state);
    let down_no_skip = || Binding {
        keys_str: "C-(↓| j)",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowDown),
                mods: Mods::CTRL,
            },
            ModKey {
                key: Key::Character("j".into()),
                mods: Mods::CTRL,
            },
        ),
        label: "no skip",
        msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
            selection::Dir::Down,
        ))),
    };
    let up_no_skip = || Binding {
        keys_str: "C-(↑| k)",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowUp),
                mods: Mods::CTRL,
            },
            ModKey {
                key: Key::Character("k".into()),
                mods: Mods::CTRL,
            },
        ),
        label: "no skip",
        msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
            selection::Dir::Up,
        ))),
    };

    let add_untracked = || Binding {
        keys_str: "a",
        keys: ModKeys::One(ModKey {
            key: Key::Character("a".into()),
            mods: Mods::NONE,
        }),
        label: "track file",
        msg: Some(FilteredMsg::EnterSubMenu(model::SubMenu::Add {
            recursive: false,
        })),
    };
    let reset_changed_file = || Binding {
        keys_str: "x",
        keys: ModKeys::One(ModKey {
            key: Key::Character("x".into()),
            mods: Mods::NONE,
        }),
        label: "reset file",
        msg: Some(FilteredMsg::RmChange),
    };
    let reset_changed_hunk = || Binding {
        keys_str: "x",
        keys: ModKeys::One(ModKey {
            key: Key::Character("x".into()),
            mods: Mods::NONE,
        }),
        label: "reset hunk",
        msg: Some(FilteredMsg::RmChange),
    };
    let rm_added_file = || Binding {
        keys_str: "x",
        keys: ModKeys::One(ModKey {
            key: Key::Character("x".into()),
            mods: Mods::NONE,
        }),
        label: "untrack file",
        msg: Some(FilteredMsg::RmChange),
    };

    let start_record = || Binding {
        keys_str: "r",
        keys: ModKeys::One(ModKey {
            key: Key::Character("r".into()),
            mods: Mods::NONE,
        }),
        label: "record",
        msg: Some(FilteredMsg::StartRecord),
    };
    let save_record = || Binding {
        keys_str: "C-s",
        keys: ModKeys::One(ModKey {
            key: Key::Character("s".into()),
            mods: Mods::CTRL,
        }),
        label: "save record",
        msg: Some(FilteredMsg::SaveRecord),
    };
    let postpone_record = || Binding {
        keys_str: "C-p",
        keys: ModKeys::One(ModKey {
            key: Key::Character("p".into()),
            mods: Mods::CTRL,
        }),
        label: "postpone record",
        msg: Some(FilteredMsg::PostponeRecord),
    };
    let discard_record = || Binding {
        keys_str: "C-d",
        keys: ModKeys::One(ModKey {
            key: Key::Character("d".into()),
            mods: Mods::CTRL,
        }),
        label: "discard record",
        msg: Some(FilteredMsg::DiscardRecord),
    };

    let select_channel = || Binding {
        keys_str: "c",
        keys: ModKeys::One(ModKey {
            key: Key::Character("c".into()),
            mods: Mods::NONE,
        }),
        label: "select channel",
        msg: Some(FilteredMsg::SelectChannel),
    };
    let fork_channel = || Binding {
        keys_str: "f",
        keys: ModKeys::One(ModKey {
            key: Key::Character("f".into()),
            mods: Mods::NONE,
        }),
        label: "fork channel",
        msg: Some(FilteredMsg::ForkChannel),
    };

    let refresh_repo = || Binding {
        keys_str: "C-r",
        keys: ModKeys::One(ModKey {
            key: Key::Character("r".into()),
            mods: Mods::CTRL,
        }),
        label: "refresh repo",
        msg: Some(FilteredMsg::RefreshRepo),
    };

    let cant_confirm = |label: &'static str| Binding {
        keys_str: "Enter",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Enter),
            mods: Mods::NONE,
        }),
        label,
        msg: None,
    };
    let exit_entire_log = || Binding {
        keys_str: "Esc",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Escape),
            mods: Mods::NONE,
        }),
        label: "exit entire log",
        msg: Some(FilteredMsg::Cancel),
    };
    let exit_other_channels = || Binding {
        keys_str: "Esc",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Escape),
            mods: Mods::NONE,
        }),
        label: "exit channel selection",
        msg: Some(FilteredMsg::Cancel),
    };
    let show_entire_log = || Binding {
        keys_str: "e",
        keys: ModKeys::One(ModKey {
            key: Key::Character("e".into()),
            mods: Mods::NONE,
        }),
        label: "entire log",
        msg: Some(FilteredMsg::ShowEntireLog),
    };
    let clipboard_copy_change_hash = || Binding {
        keys_str: "C-c",
        keys: ModKeys::One(ModKey {
            key: Key::Character("c".into()),
            mods: Mods::CTRL,
        }),
        label: "copy hash",
        msg: Some(FilteredMsg::ClipboardCopy),
    };

    let to_record_toggle_overall = |next: to_record::PickSet| {
        let label = match next {
            to_record::PickSet::Include => "include all files",
            to_record::PickSet::Exclude => "exclude all files",
            to_record::PickSet::Partial => "partial files",
        };
        Binding {
            keys_str: "S-t",
            keys: ModKeys::One(ModKey {
                key: Key::Character("t".into()),
                mods: Mods::SHIFT,
            }),
            label,
            msg: Some(FilteredMsg::ToRecord(to_record::Msg::ToggleOverall)),
        }
    };
    let to_record_toggle_file = |next: to_record::PickSet| {
        let label = match next {
            to_record::PickSet::Include => "include selected file",
            to_record::PickSet::Exclude => "exclude selected file",
            to_record::PickSet::Partial => "partial selected file",
        };
        Binding {
            keys_str: "t",
            keys: ModKeys::One(ModKey {
                key: Key::Character("t".into()),
                mods: Mods::NONE,
            }),
            label,
            msg: Some(FilteredMsg::ToRecordToggleSelectedFileOrChange),
        }
    };
    let to_record_toggle_file_change = |next: to_record::Pick| {
        let label = match next {
            to_record::Pick::Include => "include selected change",
            to_record::Pick::Exclude => "exclude selected change",
        };
        Binding {
            keys_str: "t",
            keys: ModKeys::One(ModKey {
                key: Key::Character("t".into()),
                mods: Mods::NONE,
            }),
            label,
            msg: Some(FilteredMsg::ToRecordToggleSelectedFileOrChange),
        }
    };
    let push_sub_menu = |can_push: bool| -> Binding {
        Binding {
            keys_str: "S-p",
            keys: ModKeys::One(ModKey {
                key: Key::Character("p".into()),
                mods: Mods::SHIFT,
            }),
            label: "push",
            msg: can_push.then_some(FilteredMsg::EnterSubMenu(
                model::SubMenu::Push {
                    remote: None,
                    opt: None,
                },
            )),
        }
    };
    let pull_sub_menu = |can_pull: bool| -> Binding {
        Binding {
            keys_str: "S-f",
            keys: ModKeys::One(ModKey {
                key: Key::Character("f".into()),
                mods: Mods::SHIFT,
            }),
            label: "pull",
            msg: can_pull.then_some(FilteredMsg::EnterSubMenu(
                model::SubMenu::Pull {
                    remote: None,
                    opt: None,
                },
            )),
        }
    };
    let compare_remote = || -> Binding {
        Binding {
            keys_str: "S-c",
            keys: ModKeys::One(ModKey {
                key: Key::Character("c".into()),
                mods: Mods::SHIFT,
            }),
            label: "compare remote",
            msg: Some(FilteredMsg::EnterSubMenu(
                model::SubMenu::CompareRemote {
                    remote: None,
                    remote_channel: None,
                    opt: None,
                },
            )),
        }
    };

    let mut actions = vec![];
    let ma = &mut actions;
    match sub {
        SubState::Main {
            selection,
            can_select_right,
            can_record,
            next_to_record_pick,
            next_to_record_file_pick,
            can_push_pull,
        } => {
            push(down, ma);
            push(up, ma);
            if let Some(selection) = selection {
                match selection {
                    StatusSelection::Untracked => {
                        push(add_untracked, ma);
                        push_if(can_select_right, right, ma);
                    }
                    StatusSelection::Changed => {
                        // Always allow right move for `to_record` selection
                        push(right, ma);
                        push(reset_changed_file, ma);
                    }
                    StatusSelection::AddedFromUntracked => {
                        push(rm_added_file, ma);
                        push_if(can_select_right, right, ma);
                    }
                    StatusSelection::LogChange => {
                        push_if(can_select_right, right, ma);
                        push(clipboard_copy_change_hash, ma);
                    }
                }
            }
            push_if(can_record, start_record, ma);
            push(show_entire_log, ma);
            push_if(has_other_channels, select_channel, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
            push_if_some(next_to_record_pick, to_record_toggle_overall, ma);
            push_if_some(next_to_record_file_pick, to_record_toggle_file, ma);
            push(|| push_sub_menu(can_push_pull), ma);
            push(|| pull_sub_menu(can_push_pull), ma);
            push_if(has_default_remote, compare_remote, ma);
        }
        SubState::StatusLogChange {
            can_select_right,
            can_record,
        } => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push_if(can_select_right, right, ma);
            push_if(can_record, start_record, ma);
            push(clipboard_copy_change_hash, ma);
            push(show_entire_log, ma);
            push_if(has_other_channels, select_channel, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
            push_if(has_default_remote, compare_remote, ma);
        }
        SubState::StatusLogDiff {
            can_record,
            can_skip_navigate,
            can_reset_hunk,
            next_to_record_file_pick,
            next_to_record_file_change_pick,
        } => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push_if(can_skip_navigate, down_no_skip, ma);
            push_if(can_skip_navigate, up_no_skip, ma);

            // TODO support reset of selected hunk
            {
                let _ = (can_reset_hunk, reset_changed_hunk);
                // push_if(can_reset_hunk, reset_changed_hunk, ma);
            }

            push(clipboard_copy_change_hash, ma);
            push_if(can_record, start_record, ma);
            push(show_entire_log, ma);
            push_if(has_other_channels, select_channel, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);

            // TODO maybe in here S+t should control file pick and lowercase
            // used for selected change
            let _ = next_to_record_file_pick;
            // push_if_some(next_to_record_file_pick, to_record_toggle_file,
            // ma);

            push_if_some(
                next_to_record_file_change_pick,
                to_record_toggle_file_change,
                ma,
            );
            push_if(has_default_remote, compare_remote, ma);
        }
        SubState::Recording => {
            push(focus_next, ma);
            push(focus_prev, ma);
            push(save_record, ma);
            push(postpone_record, ma);
            push(discard_record, ma);
            push_if(has_default_remote, compare_remote, ma);
        }
        SubState::SelectingChannel(state) => {
            match state {
                SelectingChannelState::NoOtherChannels => push(cancel, ma),
                SelectingChannelState::NothingSelected => {
                    push(down, ma);
                    push(up, ma);
                    push(cancel, ma);
                }
                SelectingChannelState::SomethingSelected {
                    can_switch,
                    can_select_right,
                } => {
                    push(down, ma);
                    push(up, ma);
                    push_if(can_select_right, right, ma);
                    push(cancel, ma);
                    push_if(
                        !can_switch,
                        || {
                            cant_confirm(
                                "cannot switch with unrecorded changes",
                            )
                        },
                        ma,
                    );
                    push_if(can_switch, || confirm("switch channel"), ma);
                }
            };
            push(show_entire_log, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::OtherChannelLog { can_select_right } => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push_if(can_select_right, right, ma);
            push(clipboard_copy_change_hash, ma);
            push(exit_other_channels, ma);
            push(show_entire_log, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::OtherChannelLogChange { can_select_right } => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push_if(can_select_right, right, ma);
            push(clipboard_copy_change_hash, ma);
            push(exit_other_channels, ma);
            push(show_entire_log, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::OtherChannelLogChangeDiff => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push(down_no_skip, ma);
            push(up_no_skip, ma);
            push(clipboard_copy_change_hash, ma);
            push(exit_other_channels, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::ForkingChannel { empty, unique } => {
            push_if(!empty && unique, || confirm("confirm fork"), ma);
            push_if(!unique, || cant_confirm("channel already exists"), ma);
            push(cancel, ma);
        }
        SubState::EntireLog { can_select_right } => {
            push(down, ma);
            push(up, ma);
            push_if(can_select_right, right, ma);
            push(clipboard_copy_change_hash, ma);
            push(exit_entire_log, ma);
            push_if(has_other_channels, select_channel, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::EntireLogChange { can_select_right } => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push_if(can_select_right, right, ma);
            push(clipboard_copy_change_hash, ma);
            push(exit_entire_log, ma);
            push_if(has_other_channels, select_channel, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::EntireLogChangeDiff => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push(down_no_skip, ma);
            push(up_no_skip, ma);
            push(clipboard_copy_change_hash, ma);
            push(exit_entire_log, ma);
            push(fork_channel, ma);
            push(refresh_repo, ma);
        }
        SubState::CompareRemote {
            can_select_right,
            has_any_diff,
        } => {
            push_if(has_any_diff, down, ma);
            push_if(has_any_diff, up, ma);
            push_if(has_any_diff && can_select_right, right, ma);
            push_if(has_any_diff, clipboard_copy_change_hash, ma);
            push(cancel, ma);
        }
        SubState::CompareRemoteChange { can_select_right } => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push_if(can_select_right, right, ma);
            push(clipboard_copy_change_hash, ma);
            push(cancel, ma);
        }
        SubState::CompareRemoteChangeDiff => {
            push(left, ma);
            push(down, ma);
            push(up, ma);
            push(clipboard_copy_change_hash, ma);
            push(cancel, ma);
        }
    };

    actions
}

fn push<F>(to_add: F, actions: &mut Vec<Binding>)
where
    F: Fn() -> Binding,
{
    actions.push(to_add())
}

fn push_if<F>(predicate: bool, to_add: F, actions: &mut Vec<Binding>)
where
    F: Fn() -> Binding,
{
    if predicate {
        actions.push(to_add())
    }
}

fn push_if_some<T, F>(
    predicate: Option<T>,
    to_add: F,
    actions: &mut Vec<Binding>,
) where
    F: Fn(T) -> Binding,
{
    if let Some(value) = predicate {
        actions.push(to_add(value))
    }
}

// ____________________________________________________________________________
// Simplified state used for determining allowed bindings

#[derive(Debug, Clone, Copy)]
struct State {
    has_other_channels: bool,
    has_default_remote: bool,
    sub: SubState,
}

#[derive(Debug, Clone, Copy)]
enum SubState {
    /// Main status
    Main {
        selection: Option<StatusSelection>,
        can_select_right: bool,
        can_record: bool,
        next_to_record_pick: Option<to_record::PickSet>,
        next_to_record_file_pick: Option<to_record::PickSet>,
        can_push_pull: bool,
    },
    /// Selected a log change, but not diff
    StatusLogChange {
        can_select_right: bool,
        can_record: bool,
    },
    /// Untracked, changed or status log change's diff
    StatusLogDiff {
        can_record: bool,
        can_skip_navigate: bool,
        can_reset_hunk: bool,
        next_to_record_file_pick: Option<to_record::PickSet>,
        next_to_record_file_change_pick: Option<to_record::Pick>,
    },
    /// Making a record from current changes
    Recording,
    /// Selecting an other channel
    SelectingChannel(SelectingChannelState),
    /// Viewing other channel's log
    OtherChannelLog {
        can_select_right: bool,
    },
    /// Viewing other channel's log with some change selected
    OtherChannelLogChange {
        can_select_right: bool,
    },
    /// Viewing other channel's log with some diff of a change selected
    OtherChannelLogChangeDiff,
    ForkingChannel {
        empty: bool,
        unique: bool,
    },
    /// Viewing entire log
    EntireLog {
        can_select_right: bool,
    },
    /// Viewing entire log with some change selected
    EntireLogChange {
        can_select_right: bool,
    },
    /// Viewing entire log with some diff of a change selected
    EntireLogChangeDiff,
    /// Comparing records in local vs. remote
    CompareRemote {
        can_select_right: bool,
        has_any_diff: bool,
    },
    /// Viewing a change in comparing records in local vs. remote
    CompareRemoteChange {
        can_select_right: bool,
    },
    /// Viewing a file diff of a change in comparing records in local vs.
    /// remote
    CompareRemoteChangeDiff,
}

#[derive(Debug, Clone, Copy)]
enum StatusSelection {
    Untracked,
    AddedFromUntracked,
    LogChange,
    Changed,
}

#[derive(Debug, Clone, Copy)]
enum SelectingChannelState {
    NoOtherChannels,
    NothingSelected,
    SomethingSelected {
        can_switch: bool,
        can_select_right: bool,
    },
}

/// Determine state for actions menu
fn derive_state(state: &ReadyState) -> State {
    let has_other_channels = !state.repo.other_channels.is_empty();
    let has_default_remote = state.repo.remotes.default.is_some();
    let sub = derive_sub_state(state);
    State {
        has_other_channels,
        has_default_remote,
        sub,
    }
}

/// Determine state for actions menu
fn derive_sub_state(state: &ReadyState) -> SubState {
    let ReadyState {
        user_id: _,
        repo:
            repo::State {
                dir_name: _,
                channel,
                other_channels,
                untracked_files: _,
                changed_files,
                short_log: _,
                remotes: _,
            },
        selection,
        navigation,
        record_changes,
        forking_channel_name,
        logs,
        to_record,
        jobs,
        record_dichotomy,
    } = state;
    let selection::State {
        primary,
        status: status_selection,
        channel: channel_selection,
        entire_log: entire_log_selection,
        compare_remote: compare_remote_selection,
        held_key: _,
    } = selection;

    match record_changes {
        Some(RecordChanges::Typing { .. }) => return SubState::Recording,
        Some(RecordChanges::Canceled { .. }) | None => {
            if let Some(name) = forking_channel_name.as_ref() {
                let name = name.trim();
                let empty = name.is_empty();
                let unique = channel != name
                    && !other_channels.iter().any(|n| n == name);
                return SubState::ForkingChannel { empty, unique };
            }
        }
    }

    let can_push_pull = jobs.iter().all(|job| match job {
        model::Job::Pull {
            remote: _,
            channel: c,
        }
        | model::Job::Push {
            remote: _,
            channel: c,
        } => c != channel,
        model::Job::CompareRemote { .. } => true,
    });
    match primary {
        selection::Primary::Status => {
            let can_record = !changed_files.is_empty();
            let next_to_record_pick = can_record.then(|| {
                to_record::next_overall_pick(to_record, changed_files)
            });

            match status_selection {
                Some(selection::Status::UntrackedFile {
                    ix: _,
                    path: _,
                    diff_selected,
                }) => {
                    let next_to_record_file_pick = None;
                    if *diff_selected {
                        SubState::StatusLogDiff {
                            can_record,
                            can_skip_navigate: false,
                            can_reset_hunk: false,
                            next_to_record_file_pick,
                            next_to_record_file_change_pick: None,
                        }
                    } else {
                        let diffs_nav = &navigation.files_diffs.diffs_nav;
                        let can_select_right =
                            nav_scrollable::needs_scrolling(diffs_nav);
                        SubState::Main {
                            selection: Some(StatusSelection::Untracked),
                            can_select_right,
                            can_record,
                            next_to_record_pick,
                            next_to_record_file_pick,
                            can_push_pull,
                        }
                    }
                }
                Some(selection::Status::ChangedFile {
                    ix: _,
                    path,
                    diff_selected,
                }) => {
                    let is_added_from_untracked = changed_files
                        .get(path)
                        .map(|diffs| {
                            diffs.iter().any(|diff| {
                                matches!(
                                    diff,
                                    repo::ChangedFileDiff::Add { .. }
                                )
                            })
                        })
                        .unwrap_or_default();
                    let main_selection = || {
                        if is_added_from_untracked {
                            StatusSelection::AddedFromUntracked
                        } else {
                            StatusSelection::Changed
                        }
                    };
                    let next_to_record_file_pick =
                        Some(to_record::next_file_pick(path, to_record));
                    if *diff_selected {
                        let diff_ix = navigation
                            .files_diffs
                            .diffs_nav
                            .get_selected_section_ix();
                        let selected_diff = diff_ix.and_then(|diff_ix| {
                            changed_files
                                .get(path)
                                .and_then(|diffs| diffs.iter().nth(diff_ix))
                        });
                        let next_to_record_file_change_pick = selected_diff
                            .map(|selected_diff| {
                                to_record::next_file_change_pick(
                                    path,
                                    diff::id_parts_hash(selected_diff),
                                    to_record,
                                )
                            });
                        SubState::StatusLogDiff {
                            can_record,
                            can_skip_navigate: true,
                            can_reset_hunk: true,
                            next_to_record_file_pick,
                            next_to_record_file_change_pick,
                        }
                    } else {
                        let diffs_nav = &navigation.files_diffs.diffs_nav;
                        let can_select_right =
                            nav_scrollable::needs_scrolling(diffs_nav);
                        SubState::Main {
                            selection: Some(main_selection()),
                            can_select_right,
                            can_record,
                            next_to_record_pick,
                            next_to_record_file_pick,
                            can_push_pull,
                        }
                    }
                }
                Some(selection::Status::LogChange(selection::LogChange {
                    ix: _,
                    hash: _,
                    message: _,
                    file,
                })) => match file {
                    Some(selection::LogChangeFileSelection {
                        ix: _,
                        path: _,
                        diff_selected,
                    }) => {
                        if *diff_selected {
                            let next_to_record_file_pick = None;
                            SubState::StatusLogDiff {
                                can_record,
                                can_skip_navigate: true,
                                can_reset_hunk: false,
                                next_to_record_file_pick,
                                next_to_record_file_change_pick: None,
                            }
                        } else {
                            let diffs_nav =
                                &navigation.status_logs_navs.diffs_nav;
                            let can_select_right =
                                nav_scrollable::needs_scrolling(diffs_nav);
                            SubState::StatusLogChange {
                                can_record,
                                can_select_right,
                            }
                        }
                    }
                    None => {
                        let files_nav = &navigation.status_logs_navs.files_nav;
                        let can_select_right =
                            nav_scrollable::has_sections(files_nav);
                        SubState::Main {
                            selection: Some(StatusSelection::LogChange),
                            can_select_right,
                            can_record,
                            next_to_record_pick,
                            next_to_record_file_pick: None,
                            can_push_pull,
                        }
                    }
                },
                None => SubState::Main {
                    selection: None,
                    can_select_right: false,
                    can_record,
                    next_to_record_pick,
                    next_to_record_file_pick: None,
                    can_push_pull,
                },
            }
        }
        selection::Primary::Channel => match channel_selection {
            Some(selection::Channel {
                log: None, name, ..
            }) => {
                let sub_state = if other_channels.is_empty() {
                    SelectingChannelState::NoOtherChannels
                } else {
                    let log = logs.other_channels_logs.get(name);
                    let can_switch = changed_files.is_empty();
                    let can_select_right = log.is_some();
                    SelectingChannelState::SomethingSelected {
                        can_switch,
                        can_select_right,
                    }
                };
                SubState::SelectingChannel(sub_state)
            }
            Some(selection::Channel {
                log: Some(selection::LogChange { file: None, .. }),
                ..
            }) => {
                let files_nav = &navigation.other_channel_logs_navs.files_nav;
                let can_select_right = nav_scrollable::has_sections(files_nav);
                SubState::OtherChannelLog { can_select_right }
            }
            Some(selection::Channel {
                log:
                    Some(selection::LogChange {
                        file:
                            Some(selection::LogChangeFileSelection {
                                diff_selected,
                                ..
                            }),
                        ..
                    }),
                ..
            }) => {
                if *diff_selected {
                    SubState::OtherChannelLogChangeDiff
                } else {
                    let diffs_nav =
                        &navigation.other_channel_logs_navs.diffs_nav;
                    let can_select_right =
                        nav_scrollable::needs_scrolling(diffs_nav);
                    SubState::OtherChannelLogChange { can_select_right }
                }
            }
            None => SubState::SelectingChannel(
                SelectingChannelState::NothingSelected,
            ),
        },
        selection::Primary::EntireLog => {
            if let Some(selection::LogChange { file, .. }) =
                entire_log_selection
            {
                match file {
                    Some(selection::LogChangeFileSelection {
                        diff_selected,
                        ..
                    }) => {
                        if *diff_selected {
                            SubState::EntireLogChangeDiff
                        } else {
                            let diffs_nav =
                                &navigation.entire_logs_navs.diffs_nav;
                            let can_select_right =
                                nav_scrollable::needs_scrolling(diffs_nav);
                            SubState::EntireLogChange { can_select_right }
                        }
                    }
                    None => {
                        let files_nav = &navigation.entire_logs_navs.files_nav;
                        let can_select_right =
                            nav_scrollable::has_sections(files_nav);
                        SubState::EntireLog { can_select_right }
                    }
                }
            } else {
                SubState::EntireLog {
                    can_select_right: false,
                }
            }
        }
        selection::Primary::CompareRemote => {
            if let Some(selection::CompareRemote {
                ix: _,
                hash: _,
                file,
                remote,
                remote_channel,
            }) = compare_remote_selection
            {
                let has_any_diff = model::get_record_dichotomy(
                    record_dichotomy,
                    remote,
                    remote_channel,
                )
                .map(|d| !d.is_empty())
                .unwrap_or(false);
                match file {
                    Some(selection::LogChangeFileSelection {
                        diff_selected,
                        ..
                    }) => {
                        if *diff_selected {
                            SubState::CompareRemoteChangeDiff
                        } else {
                            let diffs_nav =
                                &navigation.compare_remote_navs.diffs_nav;
                            let can_select_right =
                                nav_scrollable::needs_scrolling(diffs_nav);
                            SubState::CompareRemoteChange { can_select_right }
                        }
                    }
                    None => {
                        let files_nav =
                            &navigation.compare_remote_navs.files_nav;
                        let can_select_right =
                            nav_scrollable::has_sections(files_nav);
                        SubState::CompareRemote {
                            can_select_right,
                            has_any_diff,
                        }
                    }
                }
            } else {
                SubState::CompareRemote {
                    has_any_diff: false,
                    can_select_right: false,
                }
            }
        }
    }
}

// ____________________________________________________________________________
// Bindings

fn left() -> Binding {
    Binding {
        keys_str: "←| h",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowLeft),
                mods: Mods::NONE,
            },
            ModKey {
                key: Key::Character("h".into()),
                mods: Mods::NONE,
            },
        ),
        label: "",
        msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
            selection::Dir::Left,
        ))),
    }
}

fn right() -> Binding {
    Binding {
        keys_str: "→| l",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowRight),
                mods: Mods::NONE,
            },
            ModKey {
                key: Key::Character("l".into()),
                mods: Mods::NONE,
            },
        ),
        label: "",
        msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
            selection::Dir::Right,
        ))),
    }
}

fn down() -> Binding {
    Binding {
        keys_str: "↓| j",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowDown),
                mods: Mods::NONE,
            },
            ModKey {
                key: Key::Character("j".into()),
                mods: Mods::NONE,
            },
        ),
        label: "",
        msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
            selection::Dir::Down,
        ))),
    }
}

fn up() -> Binding {
    Binding {
        keys_str: "↑| k",
        keys: ModKeys::Two(
            ModKey {
                key: Key::Named(Named::ArrowUp),
                mods: Mods::NONE,
            },
            ModKey {
                key: Key::Character("k".into()),
                mods: Mods::NONE,
            },
        ),
        label: "",
        msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
            selection::Dir::Up,
        ))),
    }
}

fn confirm(label: &'static str) -> Binding {
    Binding {
        keys_str: "Enter",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Enter),
            mods: Mods::NONE,
        }),
        label,
        msg: Some(FilteredMsg::Confirm),
    }
}

fn toggle_recursive(is_recursive: bool) -> Binding {
    Binding {
        keys_str: "r",
        keys: ModKeys::One(ModKey {
            key: Key::Character("r".into()),
            mods: Mods::NONE,
        }),
        label: if is_recursive {
            "add non-recursive"
        } else {
            "add recursive"
        },
        msg: Some(FilteredMsg::ToggleRecursive),
    }
}

fn cancel() -> Binding {
    Binding {
        keys_str: "Esc",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Escape),
            mods: Mods::NONE,
        }),
        label: "cancel",
        msg: Some(FilteredMsg::Cancel),
    }
}

fn focus_next() -> Binding {
    Binding {
        keys_str: "Tab",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Tab),
            mods: Mods::NONE,
        }),
        label: "focus next",
        msg: Some(FilteredMsg::FocusNext),
    }
}

fn focus_prev() -> Binding {
    Binding {
        keys_str: "S-Tab",
        keys: ModKeys::One(ModKey {
            key: Key::Named(Named::Tab),
            mods: Mods::SHIFT,
        }),
        label: "focus previous",
        msg: Some(FilteredMsg::FocusPrev),
    }
}

fn select_remote(msg: Option<FilteredMsg>) -> Binding {
    Binding {
        keys_str: "r",
        keys: ModKeys::One(ModKey {
            key: Key::Character("r".into()),
            mods: Mods::NONE,
        }),
        label: "select remote",
        msg,
    }
}

fn select_remote_channel(msg: Option<FilteredMsg>) -> Binding {
    Binding {
        keys_str: "c",
        keys: ModKeys::One(ModKey {
            key: Key::Character("c".into()),
            mods: Mods::NONE,
        }),
        label: "select remote channel",
        msg,
    }
}