//! Safe macOS menu adapter over the shared desktop action registry.

use muda::{
    Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
    accelerator::{Accelerator, Code, Modifiers},
};

use crate::action::{self, ActionContext, MenuGroup, UiAction};

pub struct NativeMenu {
    menu: Menu,
    window_menu: Submenu,
    help_menu: Submenu,
    items: Vec<(UiAction, MenuItem)>,
}

impl NativeMenu {
    pub fn install(context: ActionContext) -> Result<Self, muda::Error> {
        let menu = Menu::new();
        let mut items = Vec::new();
        let mut window_menu = None;
        let mut help_menu = None;
        for group in MenuGroup::ALL {
            let submenu = Submenu::new(group.label(), true);
            match group {
                MenuGroup::App => {
                    append_action(&submenu, UiAction::About, context, &mut items)?;
                    submenu.append(&PredefinedMenuItem::separator())?;
                    append_action(&submenu, UiAction::Settings, context, &mut items)?;
                    submenu.append(&PredefinedMenuItem::separator())?;
                    submenu.append(&PredefinedMenuItem::services(None))?;
                    submenu.append(&PredefinedMenuItem::separator())?;
                    submenu.append(&PredefinedMenuItem::hide(None))?;
                    submenu.append(&PredefinedMenuItem::hide_others(None))?;
                    submenu.append(&PredefinedMenuItem::show_all(None))?;
                    submenu.append(&PredefinedMenuItem::separator())?;
                    append_action(&submenu, UiAction::Quit, context, &mut items)?;
                }
                MenuGroup::Window => {
                    submenu.append(&PredefinedMenuItem::minimize(None))?;
                    submenu.append(&PredefinedMenuItem::bring_all_to_front(None))?;
                }
                MenuGroup::Edit => {
                    submenu.append(&PredefinedMenuItem::undo(None))?;
                    submenu.append(&PredefinedMenuItem::redo(None))?;
                    submenu.append(&PredefinedMenuItem::separator())?;
                    submenu.append(&PredefinedMenuItem::cut(None))?;
                    submenu.append(&PredefinedMenuItem::copy(None))?;
                    submenu.append(&PredefinedMenuItem::paste(None))?;
                    submenu.append(&PredefinedMenuItem::select_all(None))?;
                    submenu.append(&PredefinedMenuItem::separator())?;
                    append_action(&submenu, UiAction::Find, context, &mut items)?;
                    append_action(&submenu, UiAction::SearchContents, context, &mut items)?;
                }
                _ => append_actions(&submenu, group, context, &mut items)?,
            }
            if group == MenuGroup::Window {
                window_menu = Some(submenu.clone());
            } else if group == MenuGroup::Help {
                help_menu = Some(submenu.clone());
            }
            menu.append(&submenu)?;
        }
        let native = Self {
            menu,
            window_menu: window_menu.expect("the Window menu is registered"),
            help_menu: help_menu.expect("the Help menu is registered"),
            items,
        };
        native.activate();
        Ok(native)
    }

    pub fn activate(&self) {
        self.menu.init_for_nsapp();
        self.window_menu.set_as_windows_menu_for_nsapp();
        self.help_menu.set_as_help_menu_for_nsapp();
    }

    pub fn sync(&self, context: ActionContext) {
        for (action, item) in &self.items {
            item.set_enabled(action::enabled(*action, context));
        }
    }

    pub fn poll(&self) -> Option<UiAction> {
        let event = MenuEvent::receiver().try_recv().ok()?;
        self.items
            .iter()
            .find_map(|(action, item)| (item.id() == &event.id).then_some(*action))
    }
}

fn append_actions(
    submenu: &Submenu,
    group: MenuGroup,
    context: ActionContext,
    items: &mut Vec<(UiAction, MenuItem)>,
) -> Result<(), muda::Error> {
    for spec in action::actions_in(group).filter(|spec| {
        !matches!(
            spec.action,
            UiAction::About
                | UiAction::Undo
                | UiAction::Redo
                | UiAction::Cut
                | UiAction::CopyEditedField
                | UiAction::Paste
                | UiAction::Minimize
                | UiAction::TogglePaneFocus
        )
    }) {
        append_action(submenu, spec.action, context, items)?;
    }
    Ok(())
}

fn append_action(
    submenu: &Submenu,
    action: UiAction,
    context: ActionContext,
    items: &mut Vec<(UiAction, MenuItem)>,
) -> Result<(), muda::Error> {
    let spec = action::spec_for(action);
    let item = MenuItem::with_id(
        format!("ironstorage.{}", action.id()),
        spec.label,
        action::enabled(action, context),
        accelerator(action),
    );
    submenu.append(&item)?;
    items.push((action, item));
    Ok(())
}

fn accelerator(action: UiAction) -> Option<Accelerator> {
    let command = Modifiers::SUPER;
    let (modifiers, code) = match action {
        UiAction::Settings => (command, Code::Comma),
        UiAction::NewEntry => (command, Code::KeyN),
        UiAction::NewFolder => (command | Modifiers::SHIFT, Code::KeyN),
        UiAction::OpenFolder => (command, Code::KeyO),
        UiAction::Save => (command, Code::KeyS),
        UiAction::CloseWindow => (command, Code::KeyW),
        UiAction::Quit => (command, Code::KeyQ),
        UiAction::Undo => (command, Code::KeyZ),
        UiAction::Redo => (command | Modifiers::SHIFT, Code::KeyZ),
        UiAction::Cut => (command, Code::KeyX),
        UiAction::Paste => (command, Code::KeyV),
        UiAction::Find => (command, Code::KeyF),
        UiAction::SearchContents => (command | Modifiers::SHIFT, Code::KeyF),
        UiAction::CommandPalette => (command, Code::KeyK),
        UiAction::Refresh => (command, Code::KeyR),
        UiAction::ReloadEntry => (command | Modifiers::SHIFT, Code::KeyR),
        UiAction::EditEntry => (command, Code::KeyE),
        UiAction::Lock => (command, Code::KeyL),
        UiAction::Help => (Modifiers::empty(), Code::F1),
        UiAction::About
        | UiAction::InitializeStore
        | UiAction::CopyField
        | UiAction::CopyEditedField
        | UiAction::TogglePaneFocus
        | UiAction::OpenEntry
        | UiAction::GeneratePassword
        | UiAction::MoveEntry
        | UiAction::CopyEntry
        | UiAction::DeleteEntry
        | UiAction::GenerateOtp
        | UiAction::CopyOtp
        | UiAction::ImportOtp
        | UiAction::ImportKdbx
        | UiAction::ShowOtpUri
        | UiAction::CopyOtpUri
        | UiAction::ShowOtpQr
        | UiAction::RemoveOtp
        | UiAction::GitStatus
        | UiAction::GitPull
        | UiAction::GitPush
        | UiAction::GitSync
        | UiAction::Minimize => return None,
    };
    Some(Accelerator::new(Some(modifiers), code))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn native_menu_uses_standard_macos_accelerators() {
        for (action, modifiers, code) in [
            (UiAction::Settings, Modifiers::SUPER, Code::Comma),
            (UiAction::NewEntry, Modifiers::SUPER, Code::KeyN),
            (UiAction::OpenFolder, Modifiers::SUPER, Code::KeyO),
            (UiAction::Save, Modifiers::SUPER, Code::KeyS),
            (UiAction::CloseWindow, Modifiers::SUPER, Code::KeyW),
            (UiAction::Quit, Modifiers::SUPER, Code::KeyQ),
            (UiAction::Find, Modifiers::SUPER, Code::KeyF),
            (
                UiAction::SearchContents,
                Modifiers::SUPER | Modifiers::SHIFT,
                Code::KeyF,
            ),
            (UiAction::CommandPalette, Modifiers::SUPER, Code::KeyK),
            (UiAction::Refresh, Modifiers::SUPER, Code::KeyR),
            (UiAction::Lock, Modifiers::SUPER, Code::KeyL),
            (UiAction::Help, Modifiers::empty(), Code::F1),
        ] {
            let accelerator = accelerator(action).expect("registered native accelerator");
            assert_eq!(accelerator.modifiers(), modifiers, "{action:?}");
            assert_eq!(accelerator.key(), code, "{action:?}");
        }
        assert!(accelerator(UiAction::GeneratePassword).is_none());
    }
}
