//! One desktop action registry shared by every presentation entry point.

use iced::keyboard::{self, key::Named};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DesktopPlatform {
    MacOs,
    Linux,
    Windows,
}

impl DesktopPlatform {
    pub const fn current() -> Self {
        if cfg!(target_os = "macos") {
            Self::MacOs
        } else if cfg!(target_os = "windows") {
            Self::Windows
        } else {
            Self::Linux
        }
    }

    const fn primary_modifier(self) -> keyboard::Modifiers {
        match self {
            Self::MacOs => keyboard::Modifiers::LOGO,
            Self::Linux | Self::Windows => keyboard::Modifiers::CTRL,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum UiAction {
    About,
    Settings,
    InitializeStore,
    NewFolder,
    NewEntry,
    OpenFolder,
    OpenEntry,
    Save,
    CloseWindow,
    Quit,
    Undo,
    Redo,
    Cut,
    CopyField,
    CopyEditedField,
    Paste,
    Find,
    SearchContents,
    CommandPalette,
    TogglePaneFocus,
    Refresh,
    ReloadEntry,
    EditEntry,
    GeneratePassword,
    MoveEntry,
    CopyEntry,
    DeleteEntry,
    GenerateOtp,
    CopyOtp,
    ImportOtp,
    ImportKdbx,
    ShowOtpUri,
    CopyOtpUri,
    ShowOtpQr,
    RemoveOtp,
    GitStatus,
    GitPull,
    GitPush,
    GitSync,
    Lock,
    Minimize,
    Help,
}

impl UiAction {
    pub const fn id(self) -> &'static str {
        match self {
            Self::About => "about",
            Self::Settings => "settings",
            Self::InitializeStore => "initialize-store",
            Self::NewFolder => "new-folder",
            Self::NewEntry => "new-entry",
            Self::OpenFolder => "open-folder",
            Self::OpenEntry => "open-entry",
            Self::Save => "save",
            Self::CloseWindow => "close-window",
            Self::Quit => "quit",
            Self::Undo => "undo",
            Self::Redo => "redo",
            Self::Cut => "cut",
            Self::CopyField => "copy-field",
            Self::CopyEditedField => "copy-edited-field",
            Self::Paste => "paste",
            Self::Find => "find",
            Self::SearchContents => "search-contents",
            Self::CommandPalette => "command-palette",
            Self::TogglePaneFocus => "toggle-pane-focus",
            Self::Refresh => "refresh",
            Self::ReloadEntry => "reload-entry",
            Self::EditEntry => "edit-entry",
            Self::GeneratePassword => "generate-password",
            Self::MoveEntry => "move-entry",
            Self::CopyEntry => "copy-entry",
            Self::DeleteEntry => "delete-entry",
            Self::GenerateOtp => "generate-otp",
            Self::CopyOtp => "copy-otp",
            Self::ImportOtp => "import-otp",
            Self::ImportKdbx => "import-kdbx",
            Self::ShowOtpUri => "show-otp-uri",
            Self::CopyOtpUri => "copy-otp-uri",
            Self::ShowOtpQr => "show-otp-qr",
            Self::RemoveOtp => "remove-otp",
            Self::GitStatus => "git-status",
            Self::GitPull => "git-pull",
            Self::GitPush => "git-push",
            Self::GitSync => "git-sync",
            Self::Lock => "lock",
            Self::Minimize => "minimize",
            Self::Help => "help",
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MenuGroup {
    App,
    File,
    Edit,
    View,
    Entry,
    Tools,
    Window,
    Help,
}

impl MenuGroup {
    pub const ALL: [Self; 8] = [
        Self::App,
        Self::File,
        Self::Edit,
        Self::View,
        Self::Entry,
        Self::Tools,
        Self::Window,
        Self::Help,
    ];

    pub const fn label(self) -> &'static str {
        match self {
            Self::App => "IronStorage",
            Self::File => "File",
            Self::Edit => "Edit",
            Self::View => "View",
            Self::Entry => "Entry",
            Self::Tools => "Tools",
            Self::Window => "Window",
            Self::Help => "Help",
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ActionContext {
    pub storage_ready: bool,
    pub tree_loading: bool,
    pub unlocked: bool,
    pub document_open: bool,
    pub editing: bool,
    pub dirty: bool,
    pub saving: bool,
    pub switching_vault: bool,
    pub modal_open: bool,
    pub focused_field: bool,
    pub focused_generatable: bool,
    pub focused_otp: bool,
    pub entry_path: bool,
    pub selected_object: bool,
    pub git_running: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ActionSpec {
    pub action: UiAction,
    pub group: MenuGroup,
    pub label: &'static str,
    pub shortcut: Option<&'static str>,
}

pub const ACTIONS: &[ActionSpec] = &[
    spec(UiAction::About, MenuGroup::App, "About IronStorage", None),
    spec(UiAction::Settings, MenuGroup::App, "Settings…", Some("⌘,")),
    spec(
        UiAction::InitializeStore,
        MenuGroup::File,
        "Initialize Store…",
        None,
    ),
    spec(
        UiAction::NewFolder,
        MenuGroup::File,
        "New Folder…",
        Some("⇧⌘N"),
    ),
    spec(
        UiAction::Quit,
        MenuGroup::App,
        "Quit IronStorage",
        Some("⌘Q"),
    ),
    spec(UiAction::NewEntry, MenuGroup::File, "New Entry", Some("⌘N")),
    spec(
        UiAction::OpenFolder,
        MenuGroup::File,
        "Open Folder…",
        Some("⌘O"),
    ),
    spec(UiAction::OpenEntry, MenuGroup::Entry, "Open Entry", None),
    spec(UiAction::Save, MenuGroup::File, "Save", Some("⌘S")),
    spec(
        UiAction::CloseWindow,
        MenuGroup::File,
        "Close Window",
        Some("⌘W"),
    ),
    spec(UiAction::Undo, MenuGroup::Edit, "Undo", Some("⌘Z")),
    spec(UiAction::Redo, MenuGroup::Edit, "Redo", Some("⇧⌘Z")),
    spec(UiAction::Cut, MenuGroup::Edit, "Cut", Some("⌘X")),
    spec(
        UiAction::CopyField,
        MenuGroup::Entry,
        "Copy Field",
        Some("⌘C"),
    ),
    spec(
        UiAction::CopyEditedField,
        MenuGroup::Entry,
        "Copy Edited Field",
        Some("⌘C"),
    ),
    spec(UiAction::Paste, MenuGroup::Edit, "Paste", Some("⌘V")),
    spec(UiAction::Find, MenuGroup::Edit, "Find", Some("⌘F")),
    spec(
        UiAction::SearchContents,
        MenuGroup::Edit,
        "Search Decrypted Contents…",
        Some("⇧⌘F"),
    ),
    spec(
        UiAction::CommandPalette,
        MenuGroup::View,
        "Command Palette…",
        Some("⌘K"),
    ),
    spec(
        UiAction::TogglePaneFocus,
        MenuGroup::View,
        "Next Pane",
        Some("Tab"),
    ),
    spec(UiAction::Refresh, MenuGroup::View, "Refresh", Some("⌘R")),
    spec(
        UiAction::ReloadEntry,
        MenuGroup::Entry,
        "Reload Entry",
        Some("⇧⌘R"),
    ),
    spec(
        UiAction::EditEntry,
        MenuGroup::Entry,
        "Edit Entry",
        Some("⌘E"),
    ),
    spec(
        UiAction::GeneratePassword,
        MenuGroup::Entry,
        "Generate Password…",
        None,
    ),
    spec(
        UiAction::MoveEntry,
        MenuGroup::Entry,
        "Move or Rename…",
        None,
    ),
    spec(UiAction::CopyEntry, MenuGroup::Entry, "Copy Entry…", None),
    spec(UiAction::DeleteEntry, MenuGroup::Entry, "Delete…", None),
    spec(
        UiAction::GenerateOtp,
        MenuGroup::Entry,
        "Generate OTP Code",
        None,
    ),
    spec(UiAction::CopyOtp, MenuGroup::Entry, "Copy OTP Code", None),
    spec(
        UiAction::ImportOtp,
        MenuGroup::Entry,
        "Import OTP URI or QR…",
        None,
    ),
    spec(UiAction::ShowOtpUri, MenuGroup::Entry, "Show OTP URI", None),
    spec(UiAction::CopyOtpUri, MenuGroup::Entry, "Copy OTP URI", None),
    spec(UiAction::ShowOtpQr, MenuGroup::Entry, "Show OTP QR", None),
    spec(UiAction::RemoveOtp, MenuGroup::Entry, "Remove OTP", None),
    spec(UiAction::GitStatus, MenuGroup::Tools, "Git Status…", None),
    spec(
        UiAction::ImportKdbx,
        MenuGroup::Tools,
        "Import KeePass Database…",
        None,
    ),
    spec(UiAction::GitPull, MenuGroup::Tools, "Pull", None),
    spec(UiAction::GitPush, MenuGroup::Tools, "Push", None),
    spec(UiAction::GitSync, MenuGroup::Tools, "Synchronize", None),
    spec(UiAction::Lock, MenuGroup::Tools, "Lock", Some("⌘L")),
    spec(
        UiAction::Minimize,
        MenuGroup::Window,
        "Minimize",
        Some("⌘M"),
    ),
    spec(
        UiAction::Help,
        MenuGroup::Help,
        "IronStorage Help",
        Some("F1"),
    ),
];

const fn spec(
    action: UiAction,
    group: MenuGroup,
    label: &'static str,
    shortcut: Option<&'static str>,
) -> ActionSpec {
    ActionSpec {
        action,
        group,
        label,
        shortcut,
    }
}

pub fn spec_for(action: UiAction) -> &'static ActionSpec {
    ACTIONS
        .iter()
        .find(|spec| spec.action == action)
        .expect("every UI action is registered")
}

pub fn actions_in(group: MenuGroup) -> impl Iterator<Item = &'static ActionSpec> {
    ACTIONS.iter().filter(move |spec| spec.group == group)
}

pub fn shortcut_label(action: UiAction) -> Option<String> {
    shortcut_label_for(action, DesktopPlatform::current())
}

pub fn shortcut_label_for(action: UiAction, platform: DesktopPlatform) -> Option<String> {
    let shortcut = spec_for(action).shortcut?;
    if platform == DesktopPlatform::MacOs {
        Some(shortcut.to_owned())
    } else {
        Some(shortcut.replace('⇧', "Shift+").replace('⌘', "Ctrl+"))
    }
}

pub fn enabled(action: UiAction, context: ActionContext) -> bool {
    if context.git_running && !matches!(action, UiAction::Lock | UiAction::Minimize) {
        return false;
    }
    match action {
        UiAction::About | UiAction::Help => !context.modal_open,
        UiAction::Settings => {
            context.storage_ready
                && !context.saving
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::CommandPalette => !context.modal_open,
        UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry => {
            context.storage_ready
                && !context.dirty
                && !context.saving
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::OpenFolder => {
            context.storage_ready && !context.saving && !context.switching_vault
        }
        UiAction::OpenEntry => {
            context.storage_ready
                && context.entry_path
                && !context.saving
                && !context.switching_vault
        }
        UiAction::Save => {
            context.unlocked
                && context.editing
                && context.dirty
                && !context.saving
                && !context.switching_vault
        }
        UiAction::CloseWindow | UiAction::Quit => !context.switching_vault,
        UiAction::Minimize => true,
        UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste => false,
        UiAction::Find | UiAction::SearchContents => {
            context.storage_ready
                && !context.saving
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::CopyField => {
            context.unlocked
                && context.document_open
                && !context.editing
                && !context.switching_vault
                && context.focused_field
        }
        UiAction::CopyEditedField => {
            context.unlocked && context.editing && !context.switching_vault && context.focused_field
        }
        UiAction::TogglePaneFocus => !context.modal_open,
        UiAction::Refresh => {
            context.storage_ready && !context.tree_loading && !context.switching_vault
        }
        UiAction::ReloadEntry => {
            context.unlocked && context.document_open && !context.saving && !context.switching_vault
        }
        UiAction::EditEntry => {
            context.unlocked
                && context.document_open
                && !context.editing
                && !context.switching_vault
        }
        UiAction::GeneratePassword => {
            context.unlocked
                && context.editing
                && context.focused_generatable
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry => {
            context.storage_ready
                && context.selected_object
                && !context.saving
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp => {
            context.unlocked
                && context.document_open
                && !context.editing
                && context.focused_otp
                && !context.saving
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::ImportOtp | UiAction::ImportKdbx => {
            context.storage_ready
                && context.unlocked
                && !context.dirty
                && !context.saving
                && !context.switching_vault
                && !context.modal_open
        }
        UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync => {
            context.storage_ready
                && !context.saving
                && !context.switching_vault
                && !context.git_running
                && !context.modal_open
        }
        UiAction::Lock => context.unlocked,
    }
}

pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'static str> {
    if enabled(action, context) {
        return None;
    }
    if context.git_running && !matches!(action, UiAction::Lock | UiAction::Minimize) {
        return Some("Cancel or wait for the active Git operation");
    }
    Some(match action {
        UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry
            if !context.storage_ready =>
        {
            "Shared configuration is unavailable"
        }
        UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry if context.dirty => {
            "Save or discard the current draft first"
        }
        UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry if context.saving => {
            "Wait for the active save"
        }
        UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry
            if context.switching_vault =>
        {
            "Wait for vault validation"
        }
        UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry => {
            "Close the current screen first"
        }
        UiAction::OpenFolder if !context.storage_ready => "Shared configuration is unavailable",
        UiAction::OpenFolder if context.saving => "Wait for the active save",
        UiAction::OpenFolder => "Wait for vault validation",
        UiAction::OpenEntry if !context.storage_ready => "Shared configuration is unavailable",
        UiAction::OpenEntry if !context.entry_path => "Enter or select an entry path",
        UiAction::OpenEntry if context.saving => "Wait for the active save",
        UiAction::OpenEntry => "Wait for vault validation",
        UiAction::Save if !context.unlocked => "Unlock the current entry first",
        UiAction::Save if !context.editing => "Open an entry editor first",
        UiAction::Save if !context.dirty => "The editor has no changes",
        UiAction::Save if context.saving => "A save is already running",
        UiAction::Save => "Wait for vault validation",
        UiAction::CloseWindow | UiAction::Quit => "Wait for vault validation",
        UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste => {
            "Use the focused native text field"
        }
        UiAction::Find | UiAction::SearchContents if !context.storage_ready => {
            "Shared configuration is unavailable"
        }
        UiAction::Find | UiAction::SearchContents if context.saving => "Wait for the active save",
        UiAction::Find | UiAction::SearchContents if context.switching_vault => {
            "Wait for vault validation"
        }
        UiAction::Find | UiAction::SearchContents => "Close the current screen first",
        UiAction::CopyField | UiAction::CopyEditedField if !context.unlocked => {
            "Unlock an entry first"
        }
        UiAction::CopyField | UiAction::CopyEditedField if !context.document_open => {
            "Open an entry first"
        }
        UiAction::CopyField | UiAction::CopyEditedField if !context.focused_field => {
            "Select a field first"
        }
        UiAction::CopyField | UiAction::CopyEditedField if context.switching_vault => {
            "Wait for vault validation"
        }
        UiAction::CopyField => "Use Copy Edited Field while editing",
        UiAction::CopyEditedField => "Open the entry editor first",
        UiAction::Refresh if !context.storage_ready => "Shared configuration is unavailable",
        UiAction::Refresh if context.tree_loading => "A refresh is already running",
        UiAction::Refresh => "Wait for vault validation",
        UiAction::ReloadEntry if !context.unlocked => "Unlock an entry first",
        UiAction::ReloadEntry if !context.document_open => "Open an entry first",
        UiAction::ReloadEntry if context.saving => "Wait for the active save",
        UiAction::ReloadEntry => "Wait for vault validation",
        UiAction::EditEntry if !context.unlocked => "Unlock an entry first",
        UiAction::EditEntry if !context.document_open => "Open an entry first",
        UiAction::EditEntry if context.editing => "The entry editor is already open",
        UiAction::EditEntry => "Wait for vault validation",
        UiAction::GeneratePassword if !context.unlocked => "Unlock an entry first",
        UiAction::GeneratePassword if !context.editing => "Open the entry editor first",
        UiAction::GeneratePassword if !context.focused_generatable => {
            "Select a password or other generatable secret field first"
        }
        UiAction::GeneratePassword if context.switching_vault => "Wait for vault validation",
        UiAction::GeneratePassword => "Close the current screen first",
        UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry
            if !context.storage_ready =>
        {
            "Shared configuration is unavailable"
        }
        UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry
            if !context.selected_object =>
        {
            "Select an entry or folder first"
        }
        UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry if context.saving => {
            "Wait for the active save"
        }
        UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry
            if context.switching_vault =>
        {
            "Wait for vault validation"
        }
        UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry => {
            "Close the current screen first"
        }
        UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp
            if !context.unlocked =>
        {
            "Unlock an entry first"
        }
        UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp
            if !context.document_open =>
        {
            "Open an entry first"
        }
        UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp
            if !context.focused_otp =>
        {
            "Select an OTP URI field first"
        }
        UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp
            if context.editing =>
        {
            "Finish editing the entry first"
        }
        UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp => "Close the current screen first",
        UiAction::ImportOtp | UiAction::ImportKdbx if !context.storage_ready => {
            "Shared configuration is unavailable"
        }
        UiAction::ImportOtp | UiAction::ImportKdbx if !context.unlocked => {
            "Unlock the password store first"
        }
        UiAction::ImportOtp | UiAction::ImportKdbx if context.dirty => {
            "Save or discard the current draft first"
        }
        UiAction::ImportOtp | UiAction::ImportKdbx if context.saving => "Wait for the active save",
        UiAction::ImportOtp | UiAction::ImportKdbx if context.switching_vault => {
            "Wait for vault validation"
        }
        UiAction::ImportOtp | UiAction::ImportKdbx => "Close the current screen first",
        UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync
            if !context.storage_ready =>
        {
            "Shared configuration is unavailable"
        }
        UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync
            if context.saving =>
        {
            "Wait for the active save"
        }
        UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync
            if context.switching_vault =>
        {
            "Wait for vault validation"
        }
        UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync
            if context.git_running =>
        {
            "A Git operation is already running"
        }
        UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync => {
            "Close the current screen first"
        }
        UiAction::Lock => "The password store is already locked",
        UiAction::CommandPalette => "Finish the current confirmation first",
        UiAction::Settings if !context.storage_ready => "Shared configuration is unavailable",
        UiAction::Settings if context.saving => "Wait for the active save",
        UiAction::Settings if context.switching_vault => "Wait for vault validation",
        UiAction::About | UiAction::Settings | UiAction::TogglePaneFocus | UiAction::Help => {
            "Close the current screen first"
        }
        UiAction::Minimize => "Action is unavailable",
    })
}

pub const fn aliases(action: UiAction) -> &'static [&'static str] {
    match action {
        UiAction::About => &["version", "license", "credits"],
        UiAction::Settings => &["preferences", "configuration", "config"],
        UiAction::InitializeStore => &["pass init", "recipients", "gpg id"],
        UiAction::NewFolder => &["create folder", "directory", "nested recipients"],
        UiAction::NewEntry => &["insert", "add password", "create entry"],
        UiAction::OpenFolder => &["open vault", "open store", "choose folder"],
        UiAction::OpenEntry => &["show entry", "view password"],
        UiAction::Save => &["write", "save entry"],
        UiAction::CloseWindow => &["close"],
        UiAction::Quit => &["exit"],
        UiAction::Undo => &["revert edit"],
        UiAction::Redo => &["repeat edit"],
        UiAction::Cut => &["remove selection"],
        UiAction::CopyField | UiAction::CopyEditedField => &["copy value", "clipboard"],
        UiAction::Paste => &["insert clipboard"],
        UiAction::Find => &["search text"],
        UiAction::SearchContents => &["grep", "decrypted search", "search passwords"],
        UiAction::CommandPalette => &["commands", "actions", "search commands"],
        UiAction::TogglePaneFocus => &["next pane", "switch pane", "focus"],
        UiAction::Refresh => &["reload vault", "refresh tree"],
        UiAction::ReloadEntry => &["revert entry", "refresh entry"],
        UiAction::EditEntry => &["modify entry"],
        UiAction::GeneratePassword => &["random password", "replace password", "generate"],
        UiAction::MoveEntry => &["rename", "mv", "move folder"],
        UiAction::CopyEntry => &["duplicate entry", "copy folder", "pass cp"],
        UiAction::DeleteEntry => &["remove", "rm", "delete folder"],
        UiAction::GenerateOtp => &["totp", "hotp", "one time password"],
        UiAction::CopyOtp => &["copy totp", "copy hotp", "otp clipboard"],
        UiAction::ImportOtp => &["add otp", "scan qr", "import otpauth"],
        UiAction::ImportKdbx => &["keepass", "kdbx", "import database"],
        UiAction::ShowOtpUri => &["show otpauth", "otp secret"],
        UiAction::CopyOtpUri => &["copy otpauth", "copy otp secret"],
        UiAction::ShowOtpQr => &["otp qr", "export otp"],
        UiAction::RemoveOtp => &["delete otp", "remove otpauth"],
        UiAction::GitStatus => &["repository status", "git history"],
        UiAction::GitPull => &["fetch", "download changes"],
        UiAction::GitPush => &["upload changes"],
        UiAction::GitSync => &["synchronize", "pull and push"],
        UiAction::Lock => &["secure", "log out", "relock"],
        UiAction::Minimize => &["hide window"],
        UiAction::Help => &["shortcuts", "documentation"],
    }
}

pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<UiAction> {
    shortcut_action_for(DesktopPlatform::current(), key, modifiers)
}

pub fn shortcut_action_for(
    platform: DesktopPlatform,
    key: &keyboard::Key,
    modifiers: keyboard::Modifiers,
) -> Option<UiAction> {
    if key.as_ref() == keyboard::Key::Named(Named::Tab) && modifiers.is_empty() {
        return Some(UiAction::TogglePaneFocus);
    }
    if key.as_ref() == keyboard::Key::Named(Named::F1) {
        return Some(UiAction::Help);
    }
    if !modifiers.contains(platform.primary_modifier()) {
        return None;
    }
    match key.as_ref() {
        keyboard::Key::Character(",") => Some(UiAction::Settings),
        keyboard::Key::Character("n" | "N") if modifiers.shift() => Some(UiAction::NewFolder),
        keyboard::Key::Character("n" | "N") => Some(UiAction::NewEntry),
        keyboard::Key::Character("o" | "O") => Some(UiAction::OpenFolder),
        keyboard::Key::Character("s" | "S") => Some(UiAction::Save),
        keyboard::Key::Character("w" | "W") => Some(UiAction::CloseWindow),
        keyboard::Key::Character("q" | "Q") => Some(UiAction::Quit),
        keyboard::Key::Character("z" | "Z") if modifiers.shift() => Some(UiAction::Redo),
        keyboard::Key::Character("z" | "Z") => Some(UiAction::Undo),
        keyboard::Key::Character("x" | "X") => Some(UiAction::Cut),
        keyboard::Key::Character("c" | "C") => Some(UiAction::CopyField),
        keyboard::Key::Character("v" | "V") => Some(UiAction::Paste),
        keyboard::Key::Character("f" | "F") if modifiers.shift() => Some(UiAction::SearchContents),
        keyboard::Key::Character("f" | "F") => Some(UiAction::Find),
        keyboard::Key::Character("k" | "K") => Some(UiAction::CommandPalette),
        keyboard::Key::Character("r" | "R") if modifiers.shift() => Some(UiAction::ReloadEntry),
        keyboard::Key::Character("r" | "R") => Some(UiAction::Refresh),
        keyboard::Key::Character("e" | "E") => Some(UiAction::EditEntry),
        keyboard::Key::Character("l" | "L") => Some(UiAction::Lock),
        keyboard::Key::Character("m" | "M") => Some(UiAction::Minimize),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use super::*;

    fn context() -> ActionContext {
        ActionContext {
            storage_ready: true,
            tree_loading: false,
            unlocked: true,
            document_open: true,
            editing: true,
            dirty: true,
            saving: false,
            switching_vault: false,
            modal_open: false,
            focused_field: true,
            focused_generatable: true,
            focused_otp: true,
            entry_path: true,
            selected_object: true,
            git_running: false,
        }
    }

    #[test]
    fn registry_is_complete_unique_and_grouped_for_every_platform_menu() {
        let actions = ACTIONS
            .iter()
            .map(|spec| spec.action)
            .collect::<BTreeSet<_>>();
        assert_eq!(actions.len(), ACTIONS.len());
        for group in MenuGroup::ALL {
            assert!(actions_in(group).next().is_some(), "missing {group:?} menu");
        }
        for action in actions {
            assert_eq!(spec_for(action).action, action);
        }
    }

    #[test]
    fn enablement_closes_authentication_dirty_and_loading_bypasses() {
        let ready = context();
        assert!(!enabled(UiAction::NewEntry, ready));
        assert!(enabled(UiAction::Save, ready));
        assert!(enabled(UiAction::CopyEditedField, ready));
        assert!(enabled(UiAction::GeneratePassword, ready));
        assert!(!enabled(UiAction::CopyField, ready));
        for action in [
            UiAction::GitStatus,
            UiAction::GitPull,
            UiAction::GitPush,
            UiAction::GitSync,
        ] {
            assert!(enabled(action, ready));
        }

        let viewing = ActionContext {
            editing: false,
            dirty: false,
            ..ready
        };
        assert!(enabled(UiAction::CopyField, viewing));
        assert!(!enabled(UiAction::CopyEditedField, viewing));
        assert!(enabled(UiAction::InitializeStore, viewing));
        assert!(enabled(UiAction::NewFolder, viewing));
        assert!(enabled(UiAction::NewEntry, viewing));
        for action in [
            UiAction::GenerateOtp,
            UiAction::CopyOtp,
            UiAction::ShowOtpUri,
            UiAction::CopyOtpUri,
            UiAction::ShowOtpQr,
            UiAction::RemoveOtp,
        ] {
            assert!(enabled(action, viewing), "{action:?}");
        }
        assert!(enabled(UiAction::ImportOtp, viewing));

        let locked = ActionContext {
            unlocked: false,
            ..ready
        };
        for action in [
            UiAction::Save,
            UiAction::CopyField,
            UiAction::CopyEditedField,
            UiAction::GeneratePassword,
            UiAction::GenerateOtp,
            UiAction::CopyOtp,
            UiAction::ImportOtp,
            UiAction::ShowOtpUri,
            UiAction::CopyOtpUri,
            UiAction::ShowOtpQr,
            UiAction::RemoveOtp,
            UiAction::Lock,
        ] {
            assert!(!enabled(action, locked));
        }
        for action in [
            UiAction::Find,
            UiAction::SearchContents,
            UiAction::MoveEntry,
            UiAction::CopyEntry,
            UiAction::DeleteEntry,
            UiAction::GitPull,
            UiAction::GitPush,
            UiAction::GitSync,
        ] {
            assert!(
                enabled(action, locked),
                "{action:?} can initiate authentication"
            );
        }
        assert!(!enabled(
            UiAction::Refresh,
            ActionContext {
                tree_loading: true,
                ..ready
            }
        ));
        assert!(!enabled(
            UiAction::Save,
            ActionContext {
                dirty: false,
                ..ready
            }
        ));
        assert!(!enabled(
            UiAction::OpenFolder,
            ActionContext {
                switching_vault: true,
                ..ready
            }
        ));
        let switching = ActionContext {
            switching_vault: true,
            ..ready
        };
        for action in [
            UiAction::Settings,
            UiAction::Save,
            UiAction::CloseWindow,
            UiAction::Quit,
            UiAction::CopyField,
            UiAction::CopyEditedField,
            UiAction::Refresh,
            UiAction::ReloadEntry,
            UiAction::EditEntry,
            UiAction::GeneratePassword,
            UiAction::Find,
            UiAction::SearchContents,
            UiAction::MoveEntry,
            UiAction::CopyEntry,
            UiAction::DeleteEntry,
        ] {
            assert!(!enabled(action, switching), "{action:?}");
        }
        for spec in ACTIONS {
            if !enabled(spec.action, ready) {
                assert!(
                    disabled_reason(spec.action, ready).is_some(),
                    "{:?}",
                    spec.action
                );
            }
        }
        assert!(!enabled(
            UiAction::CommandPalette,
            ActionContext {
                modal_open: true,
                ..ready
            }
        ));
        assert_eq!(
            disabled_reason(
                UiAction::CommandPalette,
                ActionContext {
                    modal_open: true,
                    ..ready
                }
            ),
            Some("Finish the current confirmation first")
        );

        let git_running = ActionContext {
            git_running: true,
            ..ready
        };
        assert!(enabled(UiAction::Lock, git_running));
        assert!(enabled(UiAction::Minimize, git_running));
        assert!(!enabled(UiAction::GitStatus, git_running));
        assert!(!enabled(UiAction::CloseWindow, git_running));
        assert_eq!(
            disabled_reason(UiAction::CloseWindow, git_running),
            Some("Cancel or wait for the active Git operation")
        );
        let modal = ActionContext {
            modal_open: true,
            ..ready
        };
        for action in [
            UiAction::About,
            UiAction::Settings,
            UiAction::TogglePaneFocus,
            UiAction::Help,
        ] {
            assert!(!enabled(action, modal), "{action:?}");
        }
        assert!(!enabled(
            UiAction::Settings,
            ActionContext {
                storage_ready: false,
                ..ready
            }
        ));
    }

    #[test]
    fn conventional_shortcuts_resolve_on_every_supported_platform() {
        for (platform, primary, label) in [
            (DesktopPlatform::MacOs, keyboard::Modifiers::LOGO, "⌘S"),
            (DesktopPlatform::Linux, keyboard::Modifiers::CTRL, "Ctrl+S"),
            (
                DesktopPlatform::Windows,
                keyboard::Modifiers::CTRL,
                "Ctrl+S",
            ),
        ] {
            assert_eq!(
                shortcut_label_for(UiAction::Save, platform).as_deref(),
                Some(label)
            );
            for (key, expected) in [
                ("o", UiAction::OpenFolder),
                ("s", UiAction::Save),
                ("f", UiAction::Find),
                ("k", UiAction::CommandPalette),
                ("n", UiAction::NewEntry),
                ("w", UiAction::CloseWindow),
                ("q", UiAction::Quit),
                ("x", UiAction::Cut),
                ("c", UiAction::CopyField),
                ("v", UiAction::Paste),
                (",", UiAction::Settings),
            ] {
                assert_eq!(
                    shortcut_action_for(platform, &keyboard::Key::Character(key.into()), primary,),
                    Some(expected)
                );
            }
            assert_eq!(
                shortcut_action_for(
                    platform,
                    &keyboard::Key::Character("f".into()),
                    primary | keyboard::Modifiers::SHIFT,
                ),
                Some(UiAction::SearchContents)
            );
            assert_eq!(
                shortcut_action_for(
                    platform,
                    &keyboard::Key::Named(Named::F1),
                    keyboard::Modifiers::NONE,
                ),
                Some(UiAction::Help)
            );
        }
        assert_eq!(
            shortcut_action_for(
                DesktopPlatform::MacOs,
                &keyboard::Key::Character("s".into()),
                keyboard::Modifiers::CTRL,
            ),
            None
        );
        assert_eq!(shortcut_label(UiAction::Help).as_deref(), Some("F1"));
    }
}
