use std::ops::Deref;
use std::rc::Rc;

use gpui::prelude::FluentBuilder;
use gpui::{
    actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, AsKeystroke,
    Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
    InteractiveElement, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render,
    ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity,
    Window,
};
use theme::ActiveTheme;

use crate::button::Button;
use crate::list::ListItem;
use crate::popover::Popover;
use crate::scroll::{Scrollbar, ScrollbarState};
use crate::{h_flex, v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt};

actions!(
    menu,
    [
        /// Trigger confirm action when user presses enter button
        Confirm,
        /// Trigger dismiss action when user presses escape button
        Dismiss,
        /// Select the next item when user presses up button
        SelectNext,
        /// Select the previous item when user preses down button
        SelectPrev
    ]
);

const ITEM_HEIGHT: Pixels = px(26.);

pub fn init(cx: &mut App) {
    let context = Some("PopupMenu");

    cx.bind_keys([
        KeyBinding::new("enter", Confirm, context),
        KeyBinding::new("escape", Dismiss, context),
        KeyBinding::new("up", SelectPrev, context),
        KeyBinding::new("down", SelectNext, context),
    ]);
}

pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + 'static {
    /// Create a popup menu with the given items, anchored to the TopLeft corner
    fn popup_menu(
        self,
        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
    ) -> Popover<PopupMenu> {
        self.popup_menu_with_anchor(Corner::TopLeft, f)
    }

    /// Create a popup menu with the given items, anchored to the given corner
    fn popup_menu_with_anchor(
        mut self,
        anchor: impl Into<Corner>,
        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
    ) -> Popover<PopupMenu> {
        let style = self.style().clone();
        let id = self.interactivity().element_id.clone();

        Popover::new(SharedString::from(format!("popup-menu:{id:?}")))
            .no_style()
            .trigger(self)
            .trigger_style(style)
            .anchor(anchor.into())
            .content(move |window, cx| {
                PopupMenu::build(window, cx, |menu, window, cx| f(menu, window, cx))
            })
    }
}

impl PopupMenuExt for Button {}

enum PopupMenuItem {
    Separator,
    Item {
        icon: Option<Icon>,
        label: SharedString,
        action: Option<Box<dyn Action>>,
        #[allow(clippy::type_complexity)]
        handler: Rc<dyn Fn(&mut Window, &mut App)>,
    },
    ElementItem {
        #[allow(clippy::type_complexity)]
        render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
        #[allow(clippy::type_complexity)]
        handler: Rc<dyn Fn(&mut Window, &mut App)>,
    },
    Submenu {
        icon: Option<Icon>,
        label: SharedString,
        menu: Entity<PopupMenu>,
    },
}

impl PopupMenuItem {
    fn is_clickable(&self) -> bool {
        !matches!(self, PopupMenuItem::Separator)
    }

    fn is_separator(&self) -> bool {
        matches!(self, PopupMenuItem::Separator)
    }

    fn has_icon(&self) -> bool {
        matches!(self, PopupMenuItem::Item { icon: Some(_), .. })
    }
}

pub struct PopupMenu {
    /// The parent menu of this menu, if this is a submenu
    parent_menu: Option<WeakEntity<Self>>,
    focus_handle: FocusHandle,
    menu_items: Vec<PopupMenuItem>,
    has_icon: bool,
    selected_index: Option<usize>,
    min_width: Pixels,
    max_width: Pixels,
    hovered_menu_ix: Option<usize>,
    bounds: Bounds<Pixels>,

    scrollable: bool,
    scroll_handle: ScrollHandle,
    scroll_state: ScrollbarState,

    action_focus_handle: Option<FocusHandle>,
    #[allow(dead_code)]
    subscriptions: Vec<Subscription>,
}

impl PopupMenu {
    pub fn build(
        window: &mut Window,
        cx: &mut App,
        f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
    ) -> Entity<Self> {
        cx.new(|cx| {
            let focus_handle = cx.focus_handle();
            let subscriptions =
                vec![
                    cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| {
                        this.dismiss(&Dismiss, window, cx)
                    }),
                ];
            let menu = Self {
                focus_handle,
                action_focus_handle: None,
                parent_menu: None,
                menu_items: Vec::new(),
                selected_index: None,
                min_width: px(120.),
                max_width: px(500.),
                has_icon: false,
                hovered_menu_ix: None,
                bounds: Bounds::default(),
                scrollable: false,
                scroll_handle: ScrollHandle::default(),
                scroll_state: ScrollbarState::default(),
                subscriptions,
            };

            f(menu, window, cx)
        })
    }

    /// Bind the focus handle of the menu, when clicked, it will focus back to this handle and then dispatch the action
    pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
        self.action_focus_handle = Some(focus_handle.clone());
        self
    }

    /// Set min width of the popup menu, default is 120px
    pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
        self.min_width = width.into();
        self
    }

    /// Set max width of the popup menu, default is 500px
    pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
        self.max_width = width.into();
        self
    }

    /// Set the menu to be scrollable to show vertical scrollbar.
    ///
    /// NOTE: If this is true, the sub-menus will cannot be support.
    pub fn scrollable(mut self) -> Self {
        self.scrollable = true;
        self
    }

    /// Add Menu Item
    pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
        self.add_menu_item(label, None, action);
        self
    }

    /// Add Menu to open link
    pub fn link(mut self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
        let href = href.into();
        self.menu_items.push(PopupMenuItem::Item {
            icon: None,
            label: label.into(),
            action: None,
            handler: Rc::new(move |_window, cx| cx.open_url(&href)),
        });
        self
    }

    /// Add Menu to open link
    pub fn link_with_icon(
        mut self,
        label: impl Into<SharedString>,
        icon: impl Into<Icon>,
        href: impl Into<String>,
    ) -> Self {
        let href = href.into();
        self.menu_items.push(PopupMenuItem::Item {
            icon: Some(icon.into()),
            label: label.into(),
            action: None,
            handler: Rc::new(move |_window, cx| cx.open_url(&href)),
        });
        self
    }

    /// Add Menu Item with Icon
    pub fn menu_with_icon(
        mut self,
        label: impl Into<SharedString>,
        icon: impl Into<Icon>,
        action: Box<dyn Action>,
    ) -> Self {
        self.add_menu_item(label, Some(icon.into()), action);
        self
    }

    /// Add Menu Item with check icon
    pub fn menu_with_check(
        mut self,
        label: impl Into<SharedString>,
        checked: bool,
        action: Box<dyn Action>,
    ) -> Self {
        if checked {
            self.add_menu_item(label, Some(IconName::Check.into()), action);
        } else {
            self.add_menu_item(label, None, action);
        }

        self
    }

    /// Add Menu Item with custom element render.
    pub fn menu_with_element<F, E>(mut self, builder: F, action: Box<dyn Action>) -> Self
    where
        F: Fn(&mut Window, &mut App) -> E + 'static,
        E: IntoElement,
    {
        self.menu_items.push(PopupMenuItem::ElementItem {
            render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
            handler: self.wrap_handler(action),
        });
        self
    }

    #[allow(clippy::type_complexity)]
    fn wrap_handler(&self, action: Box<dyn Action>) -> Rc<dyn Fn(&mut Window, &mut App)> {
        let action_focus_handle = self.action_focus_handle.clone();

        Rc::new(move |window, cx| {
            window.activate_window();

            // Focus back to the user expected focus handle
            // Then the actions listened on that focus handle can be received
            //
            // For example:
            //
            // TabPanel
            //   |- PopupMenu
            //   |- PanelContent (actions are listened here)
            //
            // The `PopupMenu` and `PanelContent` are at the same level in the TabPanel
            // If the actions are listened on the `PanelContent`,
            // it can't receive the actions from the `PopupMenu`, unless we focus on `PanelContent`.
            if let Some(handle) = action_focus_handle.as_ref() {
                window.focus(handle);
            }

            window.dispatch_action(action.boxed_clone(), cx);
        })
    }

    fn add_menu_item(
        &mut self,
        label: impl Into<SharedString>,
        icon: Option<Icon>,
        action: Box<dyn Action>,
    ) -> &mut Self {
        if icon.is_some() {
            self.has_icon = true;
        }

        self.menu_items.push(PopupMenuItem::Item {
            icon,
            label: label.into(),
            action: Some(action.boxed_clone()),
            handler: self.wrap_handler(action),
        });
        self
    }

    /// Add a separator Menu Item
    pub fn separator(mut self) -> Self {
        if self.menu_items.is_empty() {
            return self;
        }

        if let Some(PopupMenuItem::Separator) = self.menu_items.last() {
            return self;
        }

        self.menu_items.push(PopupMenuItem::Separator);
        self
    }

    pub fn submenu(
        self,
        label: impl Into<SharedString>,
        window: &mut Window,
        cx: &mut Context<Self>,
        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
    ) -> Self {
        self.submenu_with_icon(None, label, window, cx, f)
    }

    /// Add a Submenu item with icon
    pub fn submenu_with_icon(
        mut self,
        icon: Option<Icon>,
        label: impl Into<SharedString>,
        window: &mut Window,
        cx: &mut Context<Self>,
        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
    ) -> Self {
        let submenu = PopupMenu::build(window, cx, f);
        let parent_menu = cx.entity().downgrade();

        submenu.update(cx, |view, _| {
            view.parent_menu = Some(parent_menu);
        });

        self.menu_items.push(PopupMenuItem::Submenu {
            icon,
            label: label.into(),
            menu: submenu,
        });
        self
    }

    pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
        if let Some(ix) = self.hovered_menu_ix {
            if let Some(item) = self.menu_items.get(ix) {
                return match item {
                    PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
                    _ => None,
                };
            }
        }

        None
    }

    pub fn is_empty(&self) -> bool {
        self.menu_items.is_empty()
    }

    fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
        self.menu_items
            .iter()
            .enumerate()
            .filter(|(_, item)| item.is_clickable())
    }

    fn on_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
        cx.stop_propagation();
        window.prevent_default();
        self.selected_index = Some(ix);
        self.confirm(&Confirm, window, cx);
    }

    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(index) = self.selected_index {
            let item = self.menu_items.get(index);
            match item {
                Some(PopupMenuItem::Item { handler, .. }) => {
                    handler(window, cx);
                    self.dismiss(&Dismiss, window, cx)
                }
                Some(PopupMenuItem::ElementItem { handler, .. }) => {
                    handler(window, cx);
                    self.dismiss(&Dismiss, window, cx)
                }
                _ => {}
            }
        }
    }

    fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
        let count = self.clickable_menu_items().count();
        if count > 0 {
            let last_ix = count.saturating_sub(1);
            let ix = self
                .selected_index
                .map(|index| if index == last_ix { 0 } else { index + 1 })
                .unwrap_or(0);

            self.selected_index = Some(ix);
            cx.notify();
        }
    }

    fn select_prev(&mut self, _: &SelectPrev, _window: &mut Window, cx: &mut Context<Self>) {
        let count = self.clickable_menu_items().count();
        if count > 0 {
            let last_ix = count.saturating_sub(1);

            let ix = self
                .selected_index
                .map(|index| {
                    if index == last_ix {
                        0
                    } else {
                        index.saturating_sub(1)
                    }
                })
                .unwrap_or(last_ix);
            self.selected_index = Some(ix);
            cx.notify();
        }
    }

    // TODO: fix this
    #[allow(clippy::only_used_in_recursion)]
    fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
        if self.active_submenu().is_some() {
            return;
        }

        cx.emit(DismissEvent);

        // Dismiss parent menu, when this menu is dismissed
        if let Some(parent_menu) = self.parent_menu.clone().and_then(|menu| menu.upgrade()) {
            parent_menu.update(cx, |view, cx| {
                view.hovered_menu_ix = None;
                view.dismiss(&Dismiss, window, cx);
            })
        }
    }

    fn render_keybinding(
        action: Option<Box<dyn Action>>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Option<impl IntoElement> {
        if let Some(action) = action {
            if let Some(keybinding) = window.bindings_for_action(action.deref()).first() {
                let el = div().text_color(cx.theme().text_muted).children(
                    keybinding
                        .keystrokes()
                        .iter()
                        .map(|key| key_shortcut(key.as_keystroke().clone())),
                );

                return Some(el);
            }
        }

        None
    }

    fn render_icon(
        has_icon: bool,
        icon: Option<Icon>,
        _window: &Window,
        _cx: &Context<Self>,
    ) -> Option<impl IntoElement> {
        let icon_placeholder = if has_icon { Some(Icon::empty()) } else { None };

        if !has_icon {
            return None;
        }

        let icon = h_flex()
            .w_3p5()
            .h_3p5()
            .items_center()
            .justify_center()
            .text_sm()
            .map(|this| {
                if let Some(icon) = icon {
                    this.child(icon.clone().small())
                } else {
                    this.children(icon_placeholder.clone())
                }
            });

        Some(icon)
    }
}

impl FluentBuilder for PopupMenu {}

impl EventEmitter<DismissEvent> for PopupMenu {}

impl Focusable for PopupMenu {
    fn focus_handle(&self, _: &App) -> FocusHandle {
        self.focus_handle.clone()
    }
}

impl Render for PopupMenu {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let view = cx.entity().clone();
        let has_icon = self.menu_items.iter().any(|item| item.has_icon());
        let items_count = self.menu_items.len();
        let max_width = self.max_width;
        let bounds = self.bounds;

        let window_haft_height = window.window_bounds().get_bounds().size.height * 0.5;
        let max_height = window_haft_height.min(px(450.));

        v_flex()
            .id("popup-menu")
            .key_context("PopupMenu")
            .track_focus(&self.focus_handle)
            .on_action(cx.listener(Self::select_next))
            .on_action(cx.listener(Self::select_prev))
            .on_action(cx.listener(Self::confirm))
            .on_action(cx.listener(Self::dismiss))
            .on_mouse_down_out(cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)))
            .popover_style(cx)
            .relative()
            .p_1()
            .child(
                div()
                    .id("popup-menu-items")
                    .when(self.scrollable, |this| {
                        this.max_h(max_height)
                            .overflow_y_scroll()
                            .track_scroll(&self.scroll_handle)
                    })
                    .child(
                        v_flex()
                            .gap_y_0p5()
                            .min_w(self.min_width)
                            .max_w(self.max_width)
                            .min_w(rems(8.))
                            .child({
                                canvas(
                                    move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
                                    |_, _, _, _| {},
                                )
                                .absolute()
                                .size_full()
                            })
                            .children(
                                self.menu_items
                                    .iter_mut()
                                    .enumerate()
                                    // Skip last separator
                                    .filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator()))
                                    .map(|(ix, item)| {
                                        let this = ListItem::new(("menu-item", ix))
                                            .relative()
                                            .items_center()
                                            .py_0()
                                            .px_2()
                                            .rounded_md()
                                            .text_sm()
                                            .on_mouse_enter(cx.listener(move |this, _, _window, cx| {
                                                this.hovered_menu_ix = Some(ix);
                                                cx.notify();
                                            }));

                                        match item {
                                            PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
                                                div()
                                                    .rounded_none()
                                                    .h(px(1.))
                                                    .mx_neg_1()
                                                    .my_0p5()
                                                    .bg(cx.theme().border_disabled),
                                            ),
                                            PopupMenuItem::ElementItem { render, .. } => this
                                                .on_click(
                                                    cx.listener(move |this, _, window, cx| {
                                                        this.on_click(ix, window, cx)
                                                    }),
                                                )
                                                .child(
                                                    h_flex()
                                                        .min_h(ITEM_HEIGHT)
                                                        .items_center()
                                                        .gap_x_1()
                                                        .children(Self::render_icon(has_icon, None, window, cx))
                                                        .child((render)(window, cx)),
                                                ),
                                            PopupMenuItem::Item {
                                                icon, label, action, ..
                                            } => {
                                                let action = action.as_ref().map(|action| action.boxed_clone());
                                                let key = Self::render_keybinding(action, window, cx);

                                                this.on_click(
                                                    cx.listener(move |this, _, window, cx| {
                                                        this.on_click(ix, window, cx)
                                                    }),
                                                )
                                                .child(
                                                    h_flex()
                                                        .h(ITEM_HEIGHT)
                                                        .items_center()
                                                        .gap_x_1p5()
                                                        .children(Self::render_icon(has_icon, icon.clone(), window, cx))
                                                        .child(
                                                            h_flex()
                                                                .flex_1()
                                                                .gap_2()
                                                                .items_center()
                                                                .justify_between()
                                                                .child(label.clone())
                                                                .children(key),
                                                        ),
                                                )
                                            }
                                            PopupMenuItem::Submenu { icon, label, menu } => this
                                                .when(self.hovered_menu_ix == Some(ix), |this| this.selected(true))
                                                .child(
                                                    h_flex()
                                                        .items_start()
                                                        .child(
                                                            h_flex()
                                                                .size_full()
                                                                .items_center()
                                                                .gap_x_1p5()
                                                                .children(Self::render_icon(
                                                                    has_icon,
                                                                    icon.clone(),
                                                                    window,
                                                                    cx,
                                                                ))
                                                                .child(
                                                                    h_flex()
                                                                        .flex_1()
                                                                        .gap_2()
                                                                        .items_center()
                                                                        .justify_between()
                                                                        .child(label.clone())
                                                                        .child(IconName::CaretRight),
                                                                ),
                                                        )
                                                        .when_some(self.hovered_menu_ix, |this, hovered_ix| {
                                                            let (anchor, left) = if window.bounds().size.width
                                                                - bounds.origin.x
                                                                < max_width
                                                            {
                                                                (Corner::TopRight, -px(15.))
                                                            } else {
                                                                (Corner::TopLeft, bounds.size.width - px(10.))
                                                            };

                                                            let top = if bounds.origin.y + bounds.size.height
                                                                > window.bounds().size.height
                                                            {
                                                                px(32.)
                                                            } else {
                                                                -px(10.)
                                                            };

                                                            if hovered_ix == ix {
                                                                this.child(
                                                                    anchored()
                                                                        .anchor(anchor)
                                                                        .child(
                                                                            div()
                                                                                .occlude()
                                                                                .top(top)
                                                                                .left(left)
                                                                                .child(menu.clone()),
                                                                        )
                                                                        .snap_to_window_with_margin(Edges::all(px(8.))),
                                                                )
                                                            } else {
                                                                this
                                                            }
                                                        }),
                                                ),
                                        }
                                    }),
                            ),
                    ),
            )
            .when(self.scrollable, |this| {
                // TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
                this.child(
                    div()
                        .absolute()
                        .top_0()
                        .left_0()
                        .right_0p5()
                        .bottom_0()
                        .child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)),
                )
            })
    }
}

/// Return the Platform specific keybinding string by KeyStroke
pub fn key_shortcut(key: Keystroke) -> String {
    if cfg!(target_os = "macos") {
        return format!("{key}");
    }

    let mut parts = vec![];
    if key.modifiers.control {
        parts.push("Ctrl");
    }
    if key.modifiers.alt {
        parts.push("Alt");
    }
    if key.modifiers.platform {
        parts.push("Win");
    }
    if key.modifiers.shift {
        parts.push("Shift");
    }

    // Capitalize the first letter
    let key = if let Some(first_c) = key.key.chars().next() {
        format!("{}{}", first_c.to_uppercase(), &key.key[1..])
    } else {
        key.key.to_string()
    };

    parts.push(&key);
    parts.join("+")
}
