use iced::advanced::layout;
use iced::advanced::overlay;
use iced::advanced::renderer;
use iced::advanced::widget::{Operation, Tree};
use iced::advanced::{Clipboard, Layout, Shell, Widget};
use iced::keyboard::{self, Key, key};
use iced::mouse;
use iced::{Element, Event, Length, Point, Rectangle, Size, Vector};

/// An interactive popup anchored below the right edge of its trigger.
pub struct Popover<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> {
    trigger: Element<'a, Message, Theme, Renderer>,
    popup: Element<'a, Message, Theme, Renderer>,
    is_open: bool,
    on_dismiss: Message,
    gap: f32,
    align_right: bool,
}

impl<'a, Message, Theme, Renderer> Popover<'a, Message, Theme, Renderer> {
    pub fn new(
        trigger: impl Into<Element<'a, Message, Theme, Renderer>>,
        popup: impl Into<Element<'a, Message, Theme, Renderer>>,
        is_open: bool,
        on_dismiss: Message,
    ) -> Self {
        Self {
            trigger: trigger.into(),
            popup: popup.into(),
            is_open,
            on_dismiss,
            gap: 8.0,
            align_right: true,
        }
    }

    /// Anchor the popup to the trigger's left edge.
    pub fn align_left(mut self) -> Self {
        self.align_right = false;
        self
    }
}

fn popup_position(
    anchor: Rectangle,
    popup: Size,
    viewport: Size,
    gap: f32,
    align_right: bool,
) -> Point {
    let max_x = (viewport.width - popup.width).max(0.0);
    let x = if align_right {
        anchor.x + anchor.width - popup.width
    } else {
        anchor.x
    }
    .clamp(0.0, max_x);
    let below = anchor.y + anchor.height + gap;
    let y = if below + popup.height <= viewport.height {
        below
    } else {
        (anchor.y - gap - popup.height).max(0.0)
    };
    Point::new(x, y)
}

impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
    for Popover<'a, Message, Theme, Renderer>
where
    Message: Clone,
    Renderer: renderer::Renderer,
{
    fn children(&self) -> Vec<Tree> {
        vec![Tree::new(&self.trigger), Tree::new(&self.popup)]
    }

    fn diff(&self, tree: &mut Tree) {
        tree.diff_children(&[self.trigger.as_widget(), self.popup.as_widget()]);
    }

    fn size(&self) -> Size<Length> {
        self.trigger.as_widget().size()
    }

    fn layout(
        &mut self,
        tree: &mut Tree,
        renderer: &Renderer,
        limits: &layout::Limits,
    ) -> layout::Node {
        self.trigger
            .as_widget_mut()
            .layout(&mut tree.children[0], renderer, limits)
    }

    fn operate(
        &mut self,
        tree: &mut Tree,
        layout: Layout<'_>,
        renderer: &Renderer,
        operation: &mut dyn Operation,
    ) {
        self.trigger
            .as_widget_mut()
            .operate(&mut tree.children[0], layout, renderer, operation);
    }

    fn update(
        &mut self,
        tree: &mut Tree,
        event: &Event,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        renderer: &Renderer,
        clipboard: &mut dyn Clipboard,
        shell: &mut Shell<'_, Message>,
        viewport: &Rectangle,
    ) {
        self.trigger.as_widget_mut().update(
            &mut tree.children[0],
            event,
            layout,
            cursor,
            renderer,
            clipboard,
            shell,
            viewport,
        );
    }

    fn mouse_interaction(
        &self,
        tree: &Tree,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        viewport: &Rectangle,
        renderer: &Renderer,
    ) -> mouse::Interaction {
        self.trigger.as_widget().mouse_interaction(
            &tree.children[0],
            layout,
            cursor,
            viewport,
            renderer,
        )
    }

    fn draw(
        &self,
        tree: &Tree,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &renderer::Style,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        viewport: &Rectangle,
    ) {
        self.trigger.as_widget().draw(
            &tree.children[0],
            renderer,
            theme,
            style,
            layout,
            cursor,
            viewport,
        );
    }

    fn overlay<'b>(
        &'b mut self,
        tree: &'b mut Tree,
        layout: Layout<'b>,
        renderer: &Renderer,
        viewport: &Rectangle,
        translation: Vector,
    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
        let (trigger_tree, popup_tree) = tree.children.split_at_mut(1);
        let trigger_overlay = self.trigger.as_widget_mut().overlay(
            &mut trigger_tree[0],
            layout,
            renderer,
            viewport,
            translation,
        );
        let popup_overlay = self.is_open.then(|| {
            overlay::Element::new(Box::new(PopupOverlay {
                anchor: layout.bounds() + translation,
                popup: &mut self.popup,
                state: &mut popup_tree[0],
                on_dismiss: self.on_dismiss.clone(),
                gap: self.gap,
                align_right: self.align_right,
            }))
        });

        if trigger_overlay.is_some() || popup_overlay.is_some() {
            Some(
                overlay::Group::with_children(
                    trigger_overlay.into_iter().chain(popup_overlay).collect(),
                )
                .overlay(),
            )
        } else {
            None
        }
    }
}

impl<'a, Message, Theme, Renderer> From<Popover<'a, Message, Theme, Renderer>>
    for Element<'a, Message, Theme, Renderer>
where
    Message: Clone + 'a,
    Theme: 'a,
    Renderer: renderer::Renderer + 'a,
{
    fn from(popover: Popover<'a, Message, Theme, Renderer>) -> Self {
        Element::new(popover)
    }
}

struct PopupOverlay<'a, 'b, Message, Theme, Renderer> {
    anchor: Rectangle,
    popup: &'b mut Element<'a, Message, Theme, Renderer>,
    state: &'b mut Tree,
    on_dismiss: Message,
    gap: f32,
    align_right: bool,
}

impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
    for PopupOverlay<'_, '_, Message, Theme, Renderer>
where
    Message: Clone,
    Renderer: renderer::Renderer,
{
    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
        let popup = self.popup.as_widget_mut().layout(
            self.state,
            renderer,
            &layout::Limits::new(Size::ZERO, bounds),
        );
        let position = popup_position(
            self.anchor,
            popup.size(),
            bounds,
            self.gap,
            self.align_right,
        );
        layout::Node::with_children(popup.size(), vec![popup])
            .translate(Vector::new(position.x, position.y))
    }

    fn draw(
        &self,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &renderer::Style,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
    ) {
        self.popup.as_widget().draw(
            self.state,
            renderer,
            theme,
            style,
            layout.children().next().expect("popover child layout"),
            cursor,
            &Rectangle::INFINITE,
        );
    }

    fn operate(&mut self, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
        self.popup.as_widget_mut().operate(
            self.state,
            layout.children().next().expect("popover child layout"),
            renderer,
            operation,
        );
    }

    fn update(
        &mut self,
        event: &Event,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        renderer: &Renderer,
        clipboard: &mut dyn Clipboard,
        shell: &mut Shell<'_, Message>,
    ) {
        let popup_layout = layout.children().next().expect("popover child layout");
        self.popup.as_widget_mut().update(
            self.state,
            event,
            popup_layout,
            cursor,
            renderer,
            clipboard,
            shell,
            &Rectangle::INFINITE,
        );
        if shell.is_event_captured() {
            return;
        }

        let escape = matches!(
            event,
            Event::Keyboard(keyboard::Event::KeyPressed {
                key: Key::Named(key::Named::Escape),
                ..
            })
        );
        let outside_press = matches!(
            event,
            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
                | Event::Touch(iced::touch::Event::FingerPressed { .. })
        ) && !cursor.is_over(layout.bounds());
        if escape || outside_press {
            shell.publish(self.on_dismiss.clone());
            shell.capture_event();
        }
    }

    fn mouse_interaction(
        &self,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        renderer: &Renderer,
    ) -> mouse::Interaction {
        self.popup.as_widget().mouse_interaction(
            self.state,
            layout.children().next().expect("popover child layout"),
            cursor,
            &Rectangle::INFINITE,
            renderer,
        )
    }

    fn overlay<'b>(
        &'b mut self,
        layout: Layout<'b>,
        renderer: &Renderer,
    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
        self.popup.as_widget_mut().overlay(
            self.state,
            layout.children().next().expect("popover child layout"),
            renderer,
            &Rectangle::INFINITE,
            Vector::ZERO,
        )
    }
}

pub fn popover<'a, Message: Clone + 'a>(
    trigger: impl Into<Element<'a, Message>>,
    popup: impl Into<Element<'a, Message>>,
    is_open: bool,
    on_dismiss: Message,
) -> Popover<'a, Message> {
    Popover::new(trigger, popup, is_open, on_dismiss)
}

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

    #[test]
    fn popup_is_right_aligned_below_its_trigger() {
        assert_eq!(
            popup_position(
                Rectangle::new(Point::new(400.0, 20.0), Size::new(100.0, 30.0)),
                Size::new(220.0, 180.0),
                Size::new(800.0, 600.0),
                8.0,
                true,
            ),
            Point::new(280.0, 58.0)
        );
    }

    #[test]
    fn popup_flips_above_and_stays_inside_the_viewport() {
        assert_eq!(
            popup_position(
                Rectangle::new(Point::new(10.0, 570.0), Size::new(40.0, 24.0)),
                Size::new(220.0, 180.0),
                Size::new(800.0, 600.0),
                8.0,
                true,
            ),
            Point::new(0.0, 382.0)
        );
    }

    #[test]
    fn popup_can_be_left_aligned_to_its_trigger() {
        assert_eq!(
            popup_position(
                Rectangle::new(Point::new(120.0, 20.0), Size::new(80.0, 30.0)),
                Size::new(220.0, 180.0),
                Size::new(800.0, 600.0),
                8.0,
                false,
            ),
            Point::new(120.0, 58.0)
        );
    }
}
