//! Pure-Rust context menu data model and sink.
//!
//! [`build_model`] converts raw CEF flag integers into a typed
//! [`Vec<ContextMenuItem>`].  It is intentionally free of any CEF types so
//! it can be unit-tested without a live CEF runtime.
//!
//! [`ContextMenuSink`] is the thread-safe queue that `run_context_menu`
//! pushes [`ContextMenuRequest`]s onto.  The apps layer drains it each tick
//! via [`BrowserHost::drain_context_menu_requests`].

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};

// ── CEF bit-flag constants (from cef::sys / cef-dll-sys) ─────────────────────

// cef_context_menu_type_flags_t inner values (transparent u32 wrapper)
pub const TYPEFLAG_NONE: u32 = 0;
pub const TYPEFLAG_PAGE: u32 = 1;
pub const TYPEFLAG_FRAME: u32 = 2;
pub const TYPEFLAG_LINK: u32 = 4;
pub const TYPEFLAG_MEDIA: u32 = 8;
pub const TYPEFLAG_SELECTION: u32 = 16;
pub const TYPEFLAG_EDITABLE: u32 = 32;

// cef_context_menu_media_type_t enum values
pub const MEDIATYPE_NONE: u32 = 0;
pub const MEDIATYPE_IMAGE: u32 = 1;
pub const MEDIATYPE_VIDEO: u32 = 2;
pub const MEDIATYPE_AUDIO: u32 = 3;

// cef_context_menu_media_state_flags_t inner values
pub const MEDIAFLAG_NONE: u32 = 0;
pub const MEDIAFLAG_IN_ERROR: u32 = 1;
pub const MEDIAFLAG_PAUSED: u32 = 2;
pub const MEDIAFLAG_MUTED: u32 = 4;
pub const MEDIAFLAG_LOOP: u32 = 8;
pub const MEDIAFLAG_CAN_SAVE: u32 = 16;
pub const MEDIAFLAG_CAN_TOGGLE_CONTROLS: u32 = 64;
pub const MEDIAFLAG_CONTROLS: u32 = 128;
pub const MEDIAFLAG_CAN_PICTURE_IN_PICTURE: u32 = 1024;
pub const MEDIAFLAG_PICTURE_IN_PICTURE: u32 = 2048;

// ── Queue capacity ────────────────────────────────────────────────────────────

/// Maximum number of [`ContextMenuRequest`]s held in the sink at once.
/// When the queue is full the oldest entry is dropped before the new one
/// is pushed, so the UI thread always sees the most-recent requests.
pub const CONTEXT_MENU_REQUEST_QUEUE_CAP: usize = 8;

// ── Item model ────────────────────────────────────────────────────────────────

/// One entry in a context menu.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContextMenuItem {
    // ── Navigation (Page bucket) ──────────────────────────────────────────
    HistoryBack {
        enabled: bool,
    },
    HistoryForward {
        enabled: bool,
    },
    Reload,
    StopLoading,
    ViewPageSource,
    InspectElement,

    // ── Selection ─────────────────────────────────────────────────────────
    CopySelection,
    /// Search selected text with the default engine.
    SearchSelection,

    // ── Link ──────────────────────────────────────────────────────────────
    OpenLinkInNewTab,
    OpenLinkInBackgroundTab,
    OpenLinkInNewWindow,
    CopyLinkAddress,
    SaveLinkAs,

    // ── Image ─────────────────────────────────────────────────────────────
    OpenImageInNewTab,
    SaveImageAs,
    CopyImage,
    CopyImageAddress,
    RotateClockwise,
    RotateCounterclockwise,

    // ── Video / Audio ─────────────────────────────────────────────────────
    MediaPlayPause {
        playing: bool,
    },
    MediaMute {
        muted: bool,
    },
    MediaLoop {
        looped: bool,
    },
    MediaShowControls,
    MediaSaveAs,
    CopyMediaAddress,
    PictureInPicture,

    // ── Editable ──────────────────────────────────────────────────────────
    Cut,
    Copy,
    Paste,
    PasteAsPlainText,
    SelectAll,
    Undo,
    Redo,

    Separator,
}

impl ContextMenuItem {
    /// Human-readable label for the menu item.
    ///
    /// Used by the overlay renderer so label text lives in one place.
    /// `Separator` returns an empty string — callers render it as a
    /// divider line, not as text.
    pub fn label(&self) -> &'static str {
        use ContextMenuItem as I;
        match self {
            I::HistoryBack { .. } => "Back",
            I::HistoryForward { .. } => "Forward",
            I::Reload => "Reload",
            I::StopLoading => "Stop Loading",
            I::ViewPageSource => "View Page Source",
            I::InspectElement => "Inspect Element",
            I::CopySelection => "Copy",
            I::SearchSelection => "Search for Selection",
            I::OpenLinkInNewTab => "Open Link in New Tab",
            I::OpenLinkInBackgroundTab => "Open Link in Background Tab",
            I::OpenLinkInNewWindow => "Open Link in New Window",
            I::CopyLinkAddress => "Copy Link Address",
            I::SaveLinkAs => "Save Link As…",
            I::OpenImageInNewTab => "Open Image in New Tab",
            I::SaveImageAs => "Save Image As…",
            I::CopyImage => "Copy Image",
            I::CopyImageAddress => "Copy Image Address",
            I::RotateClockwise => "Rotate Clockwise",
            I::RotateCounterclockwise => "Rotate Counterclockwise",
            I::MediaPlayPause { playing: true } => "Pause",
            I::MediaPlayPause { playing: false } => "Play",
            I::MediaMute { muted: true } => "Unmute",
            I::MediaMute { muted: false } => "Mute",
            I::MediaLoop { looped: true } => "Disable Loop",
            I::MediaLoop { looped: false } => "Enable Loop",
            I::MediaShowControls => "Show Controls",
            I::MediaSaveAs => "Save Media As…",
            I::CopyMediaAddress => "Copy Media Address",
            I::PictureInPicture => "Picture in Picture",
            I::Cut => "Cut",
            I::Copy => "Copy",
            I::Paste => "Paste",
            I::PasteAsPlainText => "Paste as Plain Text",
            I::SelectAll => "Select All",
            I::Undo => "Undo",
            I::Redo => "Redo",
            I::Separator => "",
        }
    }

    /// Whether this item is a visual separator (non-selectable).
    pub fn is_separator(&self) -> bool {
        matches!(self, ContextMenuItem::Separator)
    }
}

// ── Request ───────────────────────────────────────────────────────────────────

/// A right-click that has been translated into a menu model.
///
/// All data is owned — no CEF handles are kept alive. The apps layer can
/// inspect and dispatch the request entirely from Rust without crossing
/// thread boundaries back into CEF.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextMenuRequest {
    /// Click coordinates in browser-local (CEF) pixel space.
    pub x: i32,
    pub y: i32,
    /// CEF `Browser::identifier()`. Routes dispatch to the right tab.
    pub browser_id: i32,
    /// Ordered list of items to render.
    pub items: Vec<ContextMenuItem>,
    /// Source data carried so dispatch does not have to re-query CEF.
    /// Empty when the click was not on a link / image / media element.
    pub link_url: String,
    pub source_url: String,
    pub selection_text: String,
}

// ── Sink ──────────────────────────────────────────────────────────────────────

/// Thread-safe queue of [`ContextMenuRequest`]s.
///
/// Produced by [`crate::handlers::BuffrContextMenuHandler`] on the CEF IO
/// thread and drained by the apps layer on the UI thread.
pub type ContextMenuSink = Arc<Mutex<VecDeque<ContextMenuRequest>>>;

/// Construct an empty [`ContextMenuSink`].
pub fn new_context_menu_sink() -> ContextMenuSink {
    Arc::new(Mutex::new(VecDeque::new()))
}

// ── Builder ───────────────────────────────────────────────────────────────────

/// Build the ordered item list for a right-click event.
///
/// All inputs are plain integers extracted from CEF's `ContextMenuParams` by
/// the caller.  This keeps the function pure and unit-testable without a live
/// CEF runtime.
///
/// ## Bucket priority (highest → lowest)
///
/// 1. Editable  — `TYPEFLAG_EDITABLE` set
/// 2. Link      — `TYPEFLAG_LINK` set  (wins over Image even if both flags fire)
/// 3. Image     — `MEDIATYPE_IMAGE` and `TYPEFLAG_MEDIA` set
/// 4. Media     — `MEDIATYPE_VIDEO` or `MEDIATYPE_AUDIO` with `TYPEFLAG_MEDIA`
/// 5. Selection — `TYPEFLAG_SELECTION` set (but not Editable)
/// 6. Page      — fallback; always includes navigation items
///
/// Within Page: Reload and StopLoading are mutually exclusive — when
/// `is_loading` is true only `StopLoading` is emitted, otherwise only
/// `Reload`.
#[allow(clippy::too_many_arguments)]
pub fn build_model(
    type_flags: u32,
    media_type: u32,
    media_state_flags: u32,
    is_editable: bool,
    has_link_url: bool,
    has_selection: bool,
    can_go_back: bool,
    can_go_forward: bool,
    is_loading: bool,
) -> Vec<ContextMenuItem> {
    use ContextMenuItem as I;

    let flag = |f: u32| type_flags & f != 0;

    // ── 1. Editable ───────────────────────────────────────────────────────
    if is_editable || flag(TYPEFLAG_EDITABLE) {
        let mut items = vec![I::Cut, I::Copy, I::Paste, I::PasteAsPlainText];
        items.push(I::Separator);
        items.push(I::SelectAll);
        items.push(I::Separator);
        items.push(I::Undo);
        items.push(I::Redo);
        return items;
    }

    // ── 2. Link ───────────────────────────────────────────────────────────
    if flag(TYPEFLAG_LINK) || has_link_url {
        let mut items = vec![
            I::OpenLinkInNewTab,
            I::OpenLinkInBackgroundTab,
            I::OpenLinkInNewWindow,
        ];
        items.push(I::Separator);
        items.push(I::CopyLinkAddress);
        items.push(I::SaveLinkAs);
        items.push(I::Separator);
        items.push(I::InspectElement);
        return items;
    }

    // ── 3. Image ─────────────────────────────────────────────────────────
    if flag(TYPEFLAG_MEDIA) && media_type == MEDIATYPE_IMAGE {
        let mut items = vec![
            I::OpenImageInNewTab,
            I::SaveImageAs,
            I::CopyImage,
            I::CopyImageAddress,
        ];
        items.push(I::Separator);
        items.push(I::RotateClockwise);
        items.push(I::RotateCounterclockwise);
        items.push(I::Separator);
        items.push(I::InspectElement);
        return items;
    }

    // ── 4. Video / Audio ─────────────────────────────────────────────────
    if flag(TYPEFLAG_MEDIA) && (media_type == MEDIATYPE_VIDEO || media_type == MEDIATYPE_AUDIO) {
        let playing = media_state_flags & MEDIAFLAG_PAUSED == 0
            && media_state_flags & MEDIAFLAG_IN_ERROR == 0;
        let muted = media_state_flags & MEDIAFLAG_MUTED != 0;
        let looped = media_state_flags & MEDIAFLAG_LOOP != 0;
        let can_save = media_state_flags & MEDIAFLAG_CAN_SAVE != 0;
        let can_pip = media_state_flags & MEDIAFLAG_CAN_PICTURE_IN_PICTURE != 0;
        let show_controls = media_state_flags & MEDIAFLAG_CAN_TOGGLE_CONTROLS != 0;

        let mut items = vec![
            I::MediaPlayPause { playing },
            I::MediaMute { muted },
            I::MediaLoop { looped },
        ];
        if show_controls {
            items.push(I::MediaShowControls);
        }
        items.push(I::Separator);
        if can_save {
            items.push(I::MediaSaveAs);
        }
        items.push(I::CopyMediaAddress);
        if can_pip {
            items.push(I::Separator);
            items.push(I::PictureInPicture);
        }
        items.push(I::Separator);
        items.push(I::InspectElement);
        return items;
    }

    // ── 5. Selection ─────────────────────────────────────────────────────
    if (flag(TYPEFLAG_SELECTION) || has_selection) && !is_editable {
        let mut items = vec![I::CopySelection, I::SearchSelection];
        items.push(I::Separator);
        items.push(I::InspectElement);
        return items;
    }

    // ── 6. Page (fallback) ───────────────────────────────────────────────
    let mut items = vec![
        I::HistoryBack {
            enabled: can_go_back,
        },
        I::HistoryForward {
            enabled: can_go_forward,
        },
    ];
    if is_loading {
        items.push(I::StopLoading);
    } else {
        items.push(I::Reload);
    }
    items.push(I::Separator);
    items.push(I::ViewPageSource);
    items.push(I::InspectElement);
    items
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    fn page_click(
        can_go_back: bool,
        can_go_forward: bool,
        is_loading: bool,
    ) -> Vec<ContextMenuItem> {
        build_model(
            TYPEFLAG_PAGE | TYPEFLAG_FRAME,
            MEDIATYPE_NONE,
            MEDIAFLAG_NONE,
            false,
            false,
            false,
            can_go_back,
            can_go_forward,
            is_loading,
        )
    }

    #[test]
    fn page_click_no_history() {
        let items = page_click(false, false, false);
        assert!(items.contains(&I::HistoryBack { enabled: false }));
        assert!(items.contains(&I::HistoryForward { enabled: false }));
        assert!(items.contains(&I::Reload));
        assert!(!items.contains(&I::StopLoading));
        assert!(items.contains(&I::ViewPageSource));
        assert!(items.contains(&I::InspectElement));
    }

    #[test]
    fn page_click_with_history() {
        let items = page_click(true, true, false);
        assert!(items.contains(&I::HistoryBack { enabled: true }));
        assert!(items.contains(&I::HistoryForward { enabled: true }));
        assert!(items.contains(&I::Reload));
    }

    #[test]
    fn page_click_while_loading() {
        let items = page_click(false, false, true);
        assert!(items.contains(&I::StopLoading));
        assert!(!items.contains(&I::Reload));
    }

    #[test]
    fn selection_gives_copy_and_search() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_SELECTION,
            MEDIATYPE_NONE,
            MEDIAFLAG_NONE,
            false,
            false,
            true,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::CopySelection));
        assert!(items.contains(&I::SearchSelection));
        // Must NOT have page nav items
        assert!(!items.iter().any(|i| matches!(i, I::HistoryBack { .. })));
    }

    #[test]
    fn editable_click() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_EDITABLE,
            MEDIATYPE_NONE,
            MEDIAFLAG_NONE,
            true,
            false,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::Cut));
        assert!(items.contains(&I::Copy));
        assert!(items.contains(&I::Paste));
        assert!(items.contains(&I::PasteAsPlainText));
        assert!(items.contains(&I::SelectAll));
        assert!(items.contains(&I::Undo));
        assert!(items.contains(&I::Redo));
        // Must NOT have link or page items
        assert!(!items.contains(&I::OpenLinkInNewTab));
        assert!(!items.iter().any(|i| matches!(i, I::HistoryBack { .. })));
    }

    #[test]
    fn link_click() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_LINK,
            MEDIATYPE_NONE,
            MEDIAFLAG_NONE,
            false,
            true,
            false,
            true,
            false,
            false,
        );
        assert!(items.contains(&I::OpenLinkInNewTab));
        assert!(items.contains(&I::OpenLinkInBackgroundTab));
        assert!(items.contains(&I::OpenLinkInNewWindow));
        assert!(items.contains(&I::CopyLinkAddress));
        assert!(items.contains(&I::SaveLinkAs));
        assert!(items.contains(&I::InspectElement));
        // Must NOT have page navigation items
        assert!(!items.iter().any(|i| matches!(i, I::HistoryBack { .. })));
    }

    #[test]
    fn image_click() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_MEDIA,
            MEDIATYPE_IMAGE,
            MEDIAFLAG_NONE,
            false,
            false,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::OpenImageInNewTab));
        assert!(items.contains(&I::SaveImageAs));
        assert!(items.contains(&I::CopyImage));
        assert!(items.contains(&I::CopyImageAddress));
        assert!(items.contains(&I::RotateClockwise));
        assert!(items.contains(&I::RotateCounterclockwise));
        assert!(items.contains(&I::InspectElement));
    }

    #[test]
    fn video_click_playing() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_MEDIA,
            MEDIATYPE_VIDEO,
            MEDIAFLAG_NONE, // not paused → playing
            false,
            false,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::MediaPlayPause { playing: true }));
        assert!(items.contains(&I::MediaMute { muted: false }));
        assert!(items.contains(&I::MediaLoop { looped: false }));
    }

    #[test]
    fn video_click_paused() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_MEDIA,
            MEDIATYPE_VIDEO,
            MEDIAFLAG_PAUSED | MEDIAFLAG_MUTED,
            false,
            false,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::MediaPlayPause { playing: false }));
        assert!(items.contains(&I::MediaMute { muted: true }));
    }

    #[test]
    fn link_wins_over_image() {
        // LINK + MEDIA + IMAGE — link bucket must win
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_LINK | TYPEFLAG_MEDIA,
            MEDIATYPE_IMAGE,
            MEDIAFLAG_NONE,
            false,
            true,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::OpenLinkInNewTab));
        assert!(!items.contains(&I::OpenImageInNewTab));
    }

    #[test]
    fn editable_wins_over_link() {
        // Editable + Link — editable must win
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_LINK | TYPEFLAG_EDITABLE,
            MEDIATYPE_NONE,
            MEDIAFLAG_NONE,
            true,
            true,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::Cut));
        assert!(!items.contains(&I::OpenLinkInNewTab));
    }

    #[test]
    fn reload_vs_stop_mutually_exclusive() {
        let loading = page_click(false, false, true);
        let idle = page_click(false, false, false);

        assert!(loading.contains(&I::StopLoading));
        assert!(!loading.contains(&I::Reload));

        assert!(idle.contains(&I::Reload));
        assert!(!idle.contains(&I::StopLoading));
    }

    #[test]
    fn audio_click() {
        let items = build_model(
            TYPEFLAG_PAGE | TYPEFLAG_MEDIA,
            MEDIATYPE_AUDIO,
            MEDIAFLAG_NONE,
            false,
            false,
            false,
            false,
            false,
            false,
        );
        assert!(items.contains(&I::MediaPlayPause { playing: true }));
        assert!(items.contains(&I::CopyMediaAddress));
    }
}
