//! Types used to construct a menu

use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};

use serde::Serialize;
use zbus::zvariant::{OwnedValue, Str, Type, Value};

// pub struct Properties {
//     /// Tells if the menus are in a normal state or they believe that they
//     /// could use some attention.  Cases for showing them would be if help
//     /// were referring to them or they accessors were being highlighted.
//     /// This property can have two values: "normal" in almost all cases and
//     /// "notice" when they should have a higher priority to be shown.
//     pub status: Status,
//     /// A list of directories that should be used for finding icons using
//     /// the icon naming spec.  Idealy there should only be one for the icon
//     /// theme, but additional ones are often added by applications for
//     /// app specific icons.
//     pub icon_theme_path: Vec<String>,
// }

/// Direction of texts
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Type, Serialize)]
#[zvariant(signature = "s")]
pub enum TextDirection {
    #[serde(rename = "ltr")]
    LeftToRight,
    #[serde(rename = "rtl")]
    RightToLeft,
}

// The Value dervie macro can only handle `dict` or `a{sv}` values
// so we impl it manually
impl From<TextDirection> for Value<'_> {
    fn from(value: TextDirection) -> Self {
        value.to_string().into()
    }
}

impl fmt::Display for TextDirection {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        self.serialize(f)
    }
}

#[derive(Type, Serialize)]
#[zvariant(signature = "s")]
#[serde(rename_all = "lowercase")]
pub(crate) enum Status {
    Normal,
    Notice,
}

// The Value dervie macro can only handle `dict` or `a{sv}` values
// so we impl it manually
impl From<Status> for Value<'_> {
    fn from(value: Status) -> Self {
        value.to_string().into()
    }
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        self.serialize(f)
    }
}

/// All types of menu item
///
/// Do not use directly (except [`MenuItem::Separator`]), see examples in top level documents
pub enum MenuItem<T> {
    Standard(StandardItem<T>),
    /// A separator
    Separator,
    Checkmark(CheckmarkItem<T>),
    SubMenu(SubMenu<T>),
    RadioGroup(RadioGroup<T>),
}

/// Menu item, the standard one
pub struct StandardItem<T> {
    /// Text of the item, except that:
    /// -# two consecutive underscore characters "__" are displayed as a
    /// single underscore,
    /// -# any remaining underscore characters are not displayed at all,
    /// -# the first of those remaining underscore characters (unless it is
    /// the last character in the string) indicates that the following
    /// character is the access key.
    pub label: String,
    /// Whether the item can be activated or not.
    pub enabled: bool,
    /// True if the item is visible in the menu.
    pub visible: bool,
    /// Icon name of the item, following the freedesktop.org icon spec.
    pub icon_name: String,
    /// PNG data of the icon.
    pub icon_data: Vec<u8>,
    /// The shortcut of the item. Each array represents the key press
    /// in the list of keypresses. Each list of strings contains a list of
    /// modifiers and then the key that is used. The modifier strings
    /// allowed are: "Control", "Alt", "Shift" and "Super".
    /// - A simple shortcut like Ctrl+S is represented as:
    ///   [["Control", "S"]]
    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
    ///   [["Control", "Q"], ["Alt", "X"]]
    pub shortcut: Vec<Vec<String>>,
    /// How the menuitem feels the information it's displaying to the
    /// user should be presented.
    pub disposition: Disposition,
    /// Callback invoked when the item is activated
    ///
    /// The first parameter is a mutable reference to the [`crate::Tray`] that
    /// owns this menu. Any changes you make to it inside the callback are
    /// propagated back to the menu after this callback returns.
    ///
    /// The primary purpose of this callback is to notify your application
    /// that the user activated this menu item, so avoid blocking operations
    /// here or the menu will freeze. Hand off work to your main application
    /// logic (e.g., via channels; most unbounded channels provide a non-blocking send)
    /// or `spawn` a new task and keep this handler lightweight. If you need to
    /// update the tray after doing work elsewhere, call [`crate::Handle::update`].
    ///
    /// See the [examples/realworld.rs] for typical usage patterns.
    /// 
    /// [examples/realworld.rs]: https://github.com/iovxw/ksni/blob/master/examples/realworld.rs
    pub activate: Box<dyn Fn(&mut T) + Send>,
}

impl<T> Default for StandardItem<T> {
    fn default() -> Self {
        StandardItem {
            label: String::default(),
            enabled: true,
            visible: true,
            icon_name: String::default(),
            icon_data: Vec::default(),
            shortcut: Vec::default(),
            disposition: Disposition::Normal,
            activate: Box::new(|_this| {}),
        }
    }
}

impl<T> From<StandardItem<T>> for MenuItem<T> {
    fn from(item: StandardItem<T>) -> Self {
        MenuItem::Standard(item)
    }
}

impl<T: 'static> From<StandardItem<T>> for RawMenuItem<T> {
    fn from(item: StandardItem<T>) -> Self {
        let activate = item.activate;
        Self {
            r#type: ItemType::Standard,
            label: item.label,
            enabled: item.enabled,
            visible: item.visible,
            icon_name: item.icon_name,
            icon_data: item.icon_data,
            shortcut: item.shortcut,
            disposition: item.disposition,
            on_clicked: Box::new(move |this: &mut T, _id| {
                (activate)(this);
            }),
            ..Default::default()
        }
    }
}

/// Menu item, a container of another menu tree
pub struct SubMenu<T> {
    /// Text of the item, except that:
    /// -# two consecutive underscore characters "__" are displayed as a
    /// single underscore,
    /// -# any remaining underscore characters are not displayed at all,
    /// -# the first of those remaining underscore characters (unless it is
    /// the last character in the string) indicates that the following
    /// character is the access key.
    pub label: String,
    /// Whether the item can be activated or not.
    pub enabled: bool,
    /// True if the item is visible in the menu.
    pub visible: bool,
    /// Icon name of the item, following the freedesktop.org icon spec.
    pub icon_name: String,
    /// PNG data of the icon.
    pub icon_data: Vec<u8>,
    /// The shortcut of the item. Each array represents the key press
    /// in the list of keypresses. Each list of strings contains a list of
    /// modifiers and then the key that is used. The modifier strings
    /// allowed are: "Control", "Alt", "Shift" and "Super".
    /// - A simple shortcut like Ctrl+S is represented as:
    ///   [["Control", "S"]]
    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
    ///   [["Control", "Q"], ["Alt", "X"]]
    pub shortcut: Vec<Vec<String>>,
    /// How the menuitem feels the information it's displaying to the
    /// user should be presented.
    pub disposition: Disposition,
    /// List of submenu items
    pub submenu: Vec<MenuItem<T>>,
}

impl<T> Default for SubMenu<T> {
    fn default() -> Self {
        Self {
            label: String::default(),
            enabled: true,
            visible: true,
            icon_name: String::default(),
            icon_data: Vec::default(),
            shortcut: Vec::default(),
            disposition: Disposition::Normal,
            submenu: Vec::default(),
        }
    }
}

impl<T> From<SubMenu<T>> for MenuItem<T> {
    fn from(item: SubMenu<T>) -> Self {
        MenuItem::SubMenu(item)
    }
}

impl<T> From<SubMenu<T>> for RawMenuItem<T> {
    fn from(item: SubMenu<T>) -> Self {
        Self {
            r#type: ItemType::Standard,
            label: item.label,
            enabled: item.enabled,
            visible: item.visible,
            icon_name: item.icon_name,
            icon_data: item.icon_data,
            shortcut: item.shortcut,
            disposition: item.disposition,
            on_clicked: Box::new(move |_this: &mut T, _id| Default::default()),
            ..Default::default()
        }
    }
}

/// Menu item, but checkable
pub struct CheckmarkItem<T> {
    /// Text of the item, except that:
    /// -# two consecutive underscore characters "__" are displayed as a
    /// single underscore,
    /// -# any remaining underscore characters are not displayed at all,
    /// -# the first of those remaining underscore characters (unless it is
    /// the last character in the string) indicates that the following
    /// character is the access key.
    pub label: String,
    /// Whether the item can be activated or not.
    pub enabled: bool,
    /// True if the item is visible in the menu.
    pub visible: bool,
    pub checked: bool,
    /// PNG data of the icon.
    pub icon_name: String,
    /// PNG data of the icon.
    pub icon_data: Vec<u8>,
    /// The shortcut of the item. Each array represents the key press
    /// in the list of keypresses. Each list of strings contains a list of
    /// modifiers and then the key that is used. The modifier strings
    /// allowed are: "Control", "Alt", "Shift" and "Super".
    /// - A simple shortcut like Ctrl+S is represented as:
    ///   [["Control", "S"]]
    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
    ///   [["Control", "Q"], ["Alt", "X"]]
    pub shortcut: Vec<Vec<String>>,
    /// How the menuitem feels the information it's displaying to the
    /// user should be presented.
    pub disposition: Disposition,
    /// Callback invoked when the item is activated
    ///
    /// The first parameter is a mutable reference to the [`crate::Tray`] that
    /// owns this menu. Any changes you make to it inside the callback are
    /// propagated back to the menu after this callback returns.
    ///
    /// The primary purpose of this callback is to notify your application
    /// that the user activated this menu item, so avoid blocking operations
    /// here or the menu will freeze. Hand off work to your main application
    /// logic (e.g., via channels; most unbounded channels provide a non-blocking send)
    /// or `spawn` a new task and keep this handler lightweight. If you need to
    /// update the tray after doing work elsewhere, call [`crate::Handle::update`].
    ///
    /// # Examples
    /// ```
    /// struct MyTray {
    ///     checked: bool,
    /// }
    /// impl ksni::Tray for MyTray {
    /// #   fn id(&self) -> String { "MyTray".into() }
    ///     fn menu(&self) -> Vec<ksni::menu::MenuItem<Self>> {
    ///         vec![
    ///             ksni::menu::CheckmarkItem {
    ///                 label: "Check me".into(),
    ///                 checked: self.checked,
    ///                 activate: Box::new(|tray: &mut Self| {
    ///                     tray.checked = !tray.checked;
    ///                 }),
    ///                 ..Default::default()
    ///             }
    ///             .into(),
    ///         ]
    ///     }
    /// }
    /// ```
    pub activate: Box<dyn Fn(&mut T) + Send>,
}

impl<T> Default for CheckmarkItem<T> {
    fn default() -> Self {
        CheckmarkItem {
            label: String::default(),
            enabled: true,
            visible: true,
            checked: false,
            icon_name: String::default(),
            icon_data: Vec::default(),
            shortcut: Vec::default(),
            disposition: Disposition::Normal,
            activate: Box::new(|_this| {}),
        }
    }
}

impl<T> From<CheckmarkItem<T>> for MenuItem<T> {
    fn from(item: CheckmarkItem<T>) -> Self {
        MenuItem::Checkmark(item)
    }
}

impl<T: 'static> From<CheckmarkItem<T>> for RawMenuItem<T> {
    fn from(item: CheckmarkItem<T>) -> Self {
        let activate = item.activate;
        Self {
            r#type: ItemType::Standard,
            label: item.label,
            enabled: item.enabled,
            visible: item.visible,
            icon_name: item.icon_name,
            icon_data: item.icon_data,
            shortcut: item.shortcut,
            toggle_type: ToggleType::Checkmark,
            toggle_state: if item.checked {
                ToggleState::On
            } else {
                ToggleState::Off
            },
            disposition: item.disposition,
            on_clicked: Box::new(move |this: &mut T, _id| {
                (activate)(this);
            }),
            ..Default::default()
        }
    }
}

/// Menu item, a group of [`RadioItem`]s
pub struct RadioGroup<T> {
    /// Index of the current selected radio item
    pub selected: usize,
    /// Callback invoked when an item is selected
    /// 
    /// The first parameter is a mutable reference to the [`crate::Tray`] that
    /// owns this menu. Any changes you make to it inside the callback are
    /// propagated back to the menu after this callback returns.
    /// 
    /// The second parameter is the index of the user clicked radio item.
    ///
    /// The primary purpose of this callback is to notify your application
    /// that the user activated this menu item, so avoid blocking operations
    /// here or the menu will freeze. Hand off work to your main application
    /// logic (e.g., via channels; most unbounded channels provide a non-blocking send)
    /// or `spawn` a new task and keep this handler lightweight. If you need to
    /// update the tray after doing work elsewhere, call [`crate::Handle::update`].
    ///
    /// # Examples
    /// ```
    /// struct MyTray {
    ///     selected: usize,
    /// }
    /// impl ksni::Tray for MyTray {
    /// #   fn id(&self) -> String { "MyTray".into() }
    ///     fn menu(&self) -> Vec<ksni::menu::MenuItem<Self>> {
    ///         vec![
    ///             ksni::menu::RadioGroup {
    ///                 selected: self.selected,
    ///                 select: Box::new(|tray: &mut Self, index: usize| {
    ///                     tray.selected = index;
    ///                 }),
    ///                 options: vec![
    ///                     ksni::menu::RadioItem {
    ///                         label: "Option 1".into(),
    ///                         ..Default::default()
    ///                     },
    ///                 ],
    ///             }
    ///             .into(),
    ///         ]
    ///     }
    /// }
    /// ```
    pub select: Box<dyn Fn(&mut T, usize) + Send>,
    /// List of radio items
    pub options: Vec<RadioItem>,
}

impl<T> Default for RadioGroup<T> {
    fn default() -> Self {
        Self {
            selected: 0,
            select: Box::new(|_, _| {}),
            options: Default::default(),
        }
    }
}

impl<T> From<RadioGroup<T>> for MenuItem<T> {
    fn from(item: RadioGroup<T>) -> Self {
        MenuItem::RadioGroup(item)
    }
}

/// Not a menu item, but items of [`RadioGroup`]
/// 
/// See [`RadioGroup`] for details
pub struct RadioItem {
    /// Text of the item, except that:
    /// -# two consecutive underscore characters "__" are displayed as a
    /// single underscore,
    /// -# any remaining underscore characters are not displayed at all,
    /// -# the first of those remaining underscore characters (unless it is
    /// the last character in the string) indicates that the following
    /// character is the access key.
    pub label: String,
    /// Whether the item can be activated or not.
    pub enabled: bool,
    /// True if the item is visible in the menu.
    pub visible: bool,
    /// Icon name of the item, following the freedesktop.org icon spec.
    pub icon_name: String,
    /// PNG data of the icon.
    pub icon_data: Vec<u8>,
    /// The shortcut of the item. Each array represents the key press
    /// in the list of keypresses. Each list of strings contains a list of
    /// modifiers and then the key that is used. The modifier strings
    /// allowed are: "Control", "Alt", "Shift" and "Super".
    /// - A simple shortcut like Ctrl+S is represented as:
    ///   [["Control", "S"]]
    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
    ///   [["Control", "Q"], ["Alt", "X"]]
    pub shortcut: Vec<Vec<String>>,
    /// How the menuitem feels the information it's displaying to the
    /// user should be presented.
    pub disposition: Disposition,
}

impl Default for RadioItem {
    fn default() -> Self {
        Self {
            label: String::default(),
            enabled: true,
            visible: true,
            icon_name: String::default(),
            icon_data: Vec::default(),
            shortcut: Vec::default(),
            disposition: Disposition::Normal,
        }
    }
}

pub(crate) struct RawMenuItem<T> {
    r#type: ItemType,
    /// Text of the item, except that:
    /// -# two consecutive underscore characters "__" are displayed as a
    /// single underscore,
    /// -# any remaining underscore characters are not displayed at all,
    /// -# the first of those remaining underscore characters (unless it is
    /// the last character in the string) indicates that the following
    /// character is the access key.
    label: String,
    /// Whether the item can be activated or not.
    enabled: bool,
    /// True if the item is visible in the menu.
    visible: bool,
    /// Icon name of the item, following the freedesktop.org icon spec.
    icon_name: String,
    /// PNG data of the icon.
    icon_data: Vec<u8>,
    /// The shortcut of the item. Each array represents the key press
    /// in the list of keypresses. Each list of strings contains a list of
    /// modifiers and then the key that is used. The modifier strings
    /// allowed are: "Control", "Alt", "Shift" and "Super".
    /// - A simple shortcut like Ctrl+S is represented as:
    ///   [["Control", "S"]]
    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
    ///   [["Control", "Q"], ["Alt", "X"]]
    shortcut: Vec<Vec<String>>,
    toggle_type: ToggleType,
    /// Describe the current state of a "togglable" item.
    /// Note:
    /// The implementation does not itself handle ensuring that only one
    /// item in a radio group is set to "on", or that a group does not have
    /// "on" and "indeterminate" items simultaneously; maintaining this
    /// policy is up to the toolkit wrappers.
    toggle_state: ToggleState,
    /// How the menuitem feels the information it's displaying to the
    /// user should be presented.
    disposition: Disposition,
    pub on_clicked: Box<dyn Fn(&mut T, usize) + Send>,
}

macro_rules! if_not_default_then_insert {
    ($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident) => {
        if_not_default_then_insert!($map, $item, $default, $filter, $property, (|r| r));
    };
    ($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident, $to_refarg: tt) => {{
        let name = stringify!($property).replace('_', "-");
        if_not_default_then_insert!($map, $item, $default, $filter, $property, name, $to_refarg);
    }};
    ($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident, $property_name: tt, $to_refarg: tt) => {
        if ($filter.is_empty() || $filter.contains(&$property_name.to_string()))
            && $item.$property != $default.$property
        {
            $map.insert(
                $property_name.to_string(),
                OwnedValue::from($to_refarg($item.$property.clone())),
            );
        }
    };
}

impl<T> fmt::Debug for RawMenuItem<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Item {}", self.label)
    }
}

impl<T> RawMenuItem<T> {
    pub(crate) fn to_dbus_map(&self, property_filter: &[String]) -> HashMap<String, OwnedValue> {
        let mut properties: HashMap<String, OwnedValue> = HashMap::with_capacity(11);

        let default: RawMenuItem<T> = RawMenuItem::default();
        if_not_default_then_insert!(
            properties,
            self,
            default,
            property_filter,
            r#type,
            "type",
            (|r: ItemType| r)
        );
        if_not_default_then_insert!(
            properties,
            self,
            default,
            property_filter,
            label,
            (|r: String| -> Str { r.into() })
        );
        if_not_default_then_insert!(properties, self, default, property_filter, enabled);
        if_not_default_then_insert!(properties, self, default, property_filter, visible);
        if_not_default_then_insert!(
            properties,
            self,
            default,
            property_filter,
            icon_name,
            (|r: String| -> Str { r.into() })
        );
        if_not_default_then_insert!(
            properties,
            self,
            default,
            property_filter,
            icon_data,
            (|r: Vec<u8>| -> OwnedValue {
                Value::from(r)
                    .try_into()
                    .expect("unreachable: Vec<u8> to OwnedValue")
            })
        );
        if_not_default_then_insert!(
            properties,
            self,
            default,
            property_filter,
            shortcut,
            (|r: Vec<Vec<String>>| -> OwnedValue {
                Value::from(r)
                    .try_into()
                    .expect("unreachable: Vec<Vec<String>> to OwnedValue")
            })
        );
        if_not_default_then_insert!(properties, self, default, property_filter, toggle_type);
        if_not_default_then_insert!(properties, self, default, property_filter, toggle_state);
        if_not_default_then_insert!(properties, self, default, property_filter, disposition);

        properties
    }

    pub(crate) fn diff(&self, other: &Self) -> Option<(HashMap<String, OwnedValue>, Vec<String>)> {
        let default = Self::default();
        let mut updated_props: HashMap<String, OwnedValue> = HashMap::new();
        let mut removed_props = Vec::new();
        if self.r#type != other.r#type {
            if other.r#type == default.r#type {
                removed_props.push("type".into());
            } else {
                updated_props.insert(
                    "type".into(),
                    <OwnedValue as From<Str>>::from(other.r#type.to_string().into()),
                );
            }
        }
        if self.label != other.label {
            if other.label == default.label {
                removed_props.push("label".into());
            } else {
                updated_props.insert(
                    "label".into(),
                    <OwnedValue as From<Str>>::from(other.label.clone().into()),
                );
            }
        }
        if self.enabled != other.enabled {
            if other.enabled == default.enabled {
                removed_props.push("enabled".into());
            } else {
                updated_props.insert("enabled".into(), OwnedValue::from(other.enabled));
            }
        }
        if self.visible != other.visible {
            if other.visible == default.visible {
                removed_props.push("visible".into());
            } else {
                updated_props.insert("visible".into(), OwnedValue::from(other.visible));
            }
        }
        if self.icon_name != other.icon_name {
            if other.icon_name == default.icon_name {
                removed_props.push("icon-name".into());
            } else {
                updated_props.insert(
                    "icon-name".into(),
                    <OwnedValue as From<Str>>::from(other.icon_name.clone().into()),
                );
            }
        }
        if self.icon_data != other.icon_data {
            if other.icon_data == default.icon_data {
                removed_props.push("icon-data".into());
            } else {
                updated_props.insert(
                    "icon-data".into(),
                    <OwnedValue as TryFrom<Value>>::try_from(other.icon_data.clone().into())
                        .expect("unreachable: Vec<u8> to OwnedValue"),
                );
            }
        }
        if self.shortcut != other.shortcut {
            if other.shortcut == default.shortcut {
                removed_props.push("shortcut".into());
            } else {
                updated_props.insert(
                    "shortcut".into(),
                    <OwnedValue as TryFrom<Value>>::try_from(other.shortcut.clone().into())
                        .expect("unreachable: Vec<Vec<u8>> to OwnedValue"),
                );
            }
        }
        if self.toggle_type != other.toggle_type {
            if other.toggle_type == default.toggle_type {
                removed_props.push("toggle-type".into());
            } else {
                updated_props.insert(
                    "toggle-type".into(),
                    <OwnedValue as From<Str>>::from(other.toggle_type.to_string().into()),
                );
            }
        }
        if self.toggle_state != other.toggle_state {
            if other.toggle_state == default.toggle_state {
                removed_props.push("toggle-state".into());
            } else {
                updated_props.insert(
                    "toggle-state".into(),
                    OwnedValue::from(other.toggle_state as i32),
                );
            }
        }
        if self.disposition != other.disposition {
            if other.disposition == default.disposition {
                removed_props.push("disposition".into());
            } else {
                updated_props.insert(
                    "disposition".into(),
                    <OwnedValue as From<Str>>::from(other.disposition.to_string().into()),
                );
            }
        }
        if updated_props.is_empty() && removed_props.is_empty() {
            None
        } else {
            Some((updated_props, removed_props))
        }
    }
}

impl<T> Default for RawMenuItem<T> {
    fn default() -> Self {
        RawMenuItem {
            r#type: ItemType::Standard,
            label: String::default(),
            enabled: true,
            visible: true,
            icon_name: String::default(),
            icon_data: Vec::default(),
            shortcut: Vec::default(),
            toggle_type: ToggleType::Null,
            toggle_state: ToggleState::Indeterminate,
            disposition: Disposition::Normal,
            //submenu: Vec::default(),
            on_clicked: Box::new(|_this: &mut T, _id| Default::default()),
        }
    }
}

#[allow(dead_code)]
#[derive(Debug, Eq, PartialEq, Clone)]
enum ItemType {
    /// An item which can be clicked to trigger an action or show another menu
    Standard,
    /// A separator
    Separator,
}

impl fmt::Display for ItemType {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        use ItemType::*;
        match self {
            Standard => f.write_str("standard"),
            Separator => f.write_str("separator"),
        }
    }
}

impl From<ItemType> for OwnedValue {
    fn from(value: ItemType) -> Self {
        use ItemType::*;
        let s = match value {
            Standard => "standard",
            Separator => "separator",
        };
        Str::from_static(s).into()
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum ToggleType {
    /// Item is an independent togglable item
    Checkmark,
    /// Item is part of a group where only one item can be toggled at a time
    Radio,
    /// Item cannot be toggled
    Null,
}

impl fmt::Display for ToggleType {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        use ToggleType::*;
        let r = match self {
            Checkmark => "checkmark",
            Radio => "radio",
            Null => "",
        };
        f.write_str(r)
    }
}

impl From<ToggleType> for OwnedValue {
    fn from(value: ToggleType) -> Self {
        use ToggleType::*;
        let s = match value {
            Checkmark => "checkmark",
            Radio => "radio",
            Null => "",
        };
        Str::from_static(s).into()
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum ToggleState {
    Off = 0,
    On = 1,
    Indeterminate = -1,
}

impl From<ToggleState> for OwnedValue {
    fn from(value: ToggleState) -> Self {
        (value as i32).into()
    }
}

/// How the menuitem feels the information it's displaying to the
/// user should be presented.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Disposition {
    /// A standard menu item
    Normal,
    /// Providing additional information to the user
    Informative,
    /// Looking at potentially harmful results
    Warning,
    /// Something bad could potentially happen
    Alert,
}

/// Item disposition
impl fmt::Display for Disposition {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        use Disposition::*;
        let r = match self {
            Normal => "normal",
            Informative => "informative",
            Warning => "warning",
            Alert => "alert",
        };
        f.write_str(r)
    }
}

impl From<Disposition> for OwnedValue {
    fn from(value: Disposition) -> Self {
        use Disposition::*;
        let s = match value {
            Normal => "normal",
            Informative => "informative",
            Warning => "warning",
            Alert => "alert",
        };
        Str::from_static(s).into()
    }
}

pub(crate) fn menu_flatten<T: 'static>(
    items: Vec<MenuItem<T>>,
) -> Vec<(RawMenuItem<T>, Vec<usize>)> {
    let mut list: Vec<(RawMenuItem<T>, Vec<usize>)> =
        vec![(RawMenuItem::default(), Vec::with_capacity(items.len()))];

    let mut stack = vec![(items, 0)]; // (menu, menu's parent)

    while let Some((mut current_menu, parent_index)) = stack.pop() {
        while !current_menu.is_empty() {
            match current_menu.remove(0) {
                MenuItem::Standard(item) => {
                    let index = list.len();
                    list.push((item.into(), Vec::new()));
                    // Add self to parent's submenu
                    list[parent_index].1.push(index);
                }
                MenuItem::Separator => {
                    let item = RawMenuItem {
                        r#type: ItemType::Separator,
                        ..Default::default()
                    };
                    let index = list.len();
                    list.push((item, Vec::new()));
                    list[parent_index].1.push(index);
                }
                MenuItem::Checkmark(item) => {
                    let index = list.len();
                    list.push((item.into(), Vec::new()));
                    list[parent_index].1.push(index);
                }
                MenuItem::SubMenu(mut item) => {
                    let submenu = std::mem::replace(&mut item.submenu, Default::default());
                    let index = list.len();
                    list.push((item.into(), Vec::with_capacity(submenu.len())));
                    list[parent_index].1.push(index);
                    if !submenu.is_empty() {
                        stack.push((current_menu, parent_index));
                        stack.push((submenu, index));
                        break;
                    }
                }
                MenuItem::RadioGroup(group) => {
                    let offset = list.len();
                    let on_selected = Arc::new(Mutex::new(group.select));
                    for (idx, option) in group.options.into_iter().enumerate() {
                        let on_selected = on_selected.clone();
                        let item = RawMenuItem {
                            r#type: ItemType::Standard,
                            label: option.label,
                            enabled: option.enabled,
                            visible: option.visible,
                            icon_name: option.icon_name,
                            icon_data: option.icon_data,
                            shortcut: option.shortcut,
                            toggle_type: ToggleType::Radio,
                            toggle_state: if idx == group.selected {
                                ToggleState::On
                            } else {
                                ToggleState::Off
                            },
                            disposition: option.disposition,
                            on_clicked: Box::new(move |this: &mut T, id| {
                                (on_selected.lock().unwrap())(this, id - offset);
                            }),
                            ..Default::default()
                        };
                        let index = list.len();
                        list.push((item, Vec::new()));
                        list[parent_index].1.push(index);
                    }
                }
            }
        }
    }

    list
}

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

    #[test]
    fn test_enums() {
        assert_eq!(TextDirection::LeftToRight.to_string(), "ltr");
        assert_eq!(TextDirection::RightToLeft.to_string(), "rtl");
    }

    #[test]
    fn test_menu_flatten() {
        let x: Vec<MenuItem<()>> = vec![
            SubMenu {
                label: "a".into(),
                submenu: vec![
                    SubMenu {
                        label: "a1".into(),
                        submenu: vec![StandardItem {
                            label: "a1.1".into(),
                            ..Default::default()
                        }
                        .into()],
                        ..Default::default()
                    }
                    .into(),
                    StandardItem {
                        label: "a2".into(),
                        ..Default::default()
                    }
                    .into(),
                ],
                ..Default::default()
            }
            .into(),
            StandardItem {
                label: "b".into(),
                ..Default::default()
            }
            .into(),
            SubMenu {
                label: "c".into(),
                submenu: vec![
                    StandardItem {
                        label: "c1".into(),
                        ..Default::default()
                    }
                    .into(),
                    SubMenu {
                        label: "c2".into(),
                        submenu: vec![StandardItem {
                            label: "c2.1".into(),
                            ..Default::default()
                        }
                        .into()],
                        ..Default::default()
                    }
                    .into(),
                ],
                ..Default::default()
            }
            .into(),
        ];

        let r = menu_flatten(x);
        let expect: Vec<(RawMenuItem<()>, Vec<usize>)> = vec![
            (
                RawMenuItem {
                    label: "".into(),
                    ..Default::default()
                },
                vec![1, 5, 6],
            ),
            (
                RawMenuItem {
                    label: "a".into(),
                    ..Default::default()
                },
                vec![2, 4],
            ),
            (
                RawMenuItem {
                    label: "a1".into(),
                    ..Default::default()
                },
                vec![3],
            ),
            (
                RawMenuItem {
                    label: "a1.1".into(),
                    ..Default::default()
                },
                vec![],
            ),
            (
                RawMenuItem {
                    label: "a2".into(),
                    ..Default::default()
                },
                vec![],
            ),
            (
                RawMenuItem {
                    label: "b".into(),
                    ..Default::default()
                },
                vec![],
            ),
            (
                RawMenuItem {
                    label: "c".into(),
                    ..Default::default()
                },
                vec![7, 8],
            ),
            (
                RawMenuItem {
                    label: "c1".into(),
                    ..Default::default()
                },
                vec![],
            ),
            (
                RawMenuItem {
                    label: "c2".into(),
                    ..Default::default()
                },
                vec![9],
            ),
            (
                RawMenuItem {
                    label: "c2.1".into(),
                    ..Default::default()
                },
                vec![],
            ),
        ];
        assert_eq!(r.len(), 10);
        assert_eq!(r[0].1, expect[0].1);
        assert_eq!(r[1].1, expect[1].1);
        assert_eq!(r[2].1, expect[2].1);
        assert_eq!(r[3].1, expect[3].1);
        assert_eq!(r[4].1, expect[4].1);
        assert_eq!(r[5].1, expect[5].1);
        assert_eq!(r[6].1, expect[6].1);
        assert_eq!(r[7].1, expect[7].1);
        assert_eq!(r[8].1, expect[8].1);
        assert_eq!(r[9].1, expect[9].1);
        assert_eq!(r[0].0.label, expect[0].0.label);
        assert_eq!(r[1].0.label, expect[1].0.label);
        assert_eq!(r[2].0.label, expect[2].0.label);
        assert_eq!(r[3].0.label, expect[3].0.label);
        assert_eq!(r[4].0.label, expect[4].0.label);
        assert_eq!(r[5].0.label, expect[5].0.label);
        assert_eq!(r[6].0.label, expect[6].0.label);
        assert_eq!(r[7].0.label, expect[7].0.label);
        assert_eq!(r[8].0.label, expect[8].0.label);
        assert_eq!(r[9].0.label, expect[9].0.label);
    }
}
