//! Shared in-canvas modal overlay.
//!
//! Former pop-up *windows* (layer manager, style editors, About, …) render as
//! centered overlays on top of the main view instead of separate OS windows.
//! The native build has one main window and the web build has only the canvas,
//! so both stack dialogs here — one code path for every platform.

use crate::app::Message;
use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, overlay, renderer, Shell};
use iced::widget::{button, column, container, mouse_area, opaque, row, sensor, stack, Space};
use iced::{
    Background, Border, Element, Event, Length, Padding, Rectangle, Renderer, Size, Theme, Vector,
};

#[derive(Debug, Clone, Copy)]
pub struct ModalOptions {
    movable: bool,
    resizable: bool,
    neutral_close: bool,
}

/// Sizing used inside an intrinsically measured modal.
///
/// [`INTRINSIC`](Self::INTRINSIC) lets each widget report the most space it can
/// use, while [`FILL`](Self::FILL) distributes the resulting shared frame to
/// sibling toolbars, panes and scroll areas.
#[derive(Debug, Clone, Copy)]
pub struct ModalSizing {
    pub width: Length,
    pub height: Length,
}

impl ModalSizing {
    pub const INTRINSIC: Self = Self {
        width: Length::Fluid(iced_core::length::Constraint::Max),
        height: Length::Fluid(iced_core::length::Constraint::Max),
    };

    pub const FILL: Self = Self {
        width: Length::Fill,
        height: Length::Fill,
    };
}

/// Measure an intrinsic copy of `content`, then lay the fill copy out in the
/// resulting shared frame. `extra` grows that frame when the resize handle is
/// dragged; `max` only caps the initial intrinsic pass.
pub fn intrinsic<'a>(
    measurement: impl Into<Element<'a, Message>>,
    content: impl Into<Element<'a, Message>>,
    max: Size,
    extra: Vector,
) -> Element<'a, Message> {
    Element::new(Intrinsic {
        children: [measurement.into(), content.into()],
        max,
        extra,
    })
}

struct Intrinsic<'a> {
    children: [Element<'a, Message>; 2],
    max: Size,
    extra: Vector,
}

impl Widget<Message, Theme, Renderer> for Intrinsic<'_> {
    fn diff(&mut self, tree: &mut widget::Tree) {
        tree.diff_children(&mut self.children);
    }

    fn size(&self) -> Size<Length> {
        Size::new(
            Length::Fit.max(self.max.width + self.extra.x),
            Length::Fit.max(self.max.height + self.extra.y),
        )
    }

    fn layout(
        &mut self,
        tree: &mut widget::Tree,
        renderer: &Renderer,
        limits: &layout::Limits,
    ) -> layout::Node {
        let measure_max = Size::new(
            self.max.width.min(limits.max().width),
            self.max.height.min(limits.max().height),
        );
        let measure_limits = layout::Limits::new(Size::ZERO, measure_max);
        let measured = self.children[0]
            .as_widget_mut()
            .layout(&mut tree.children[0], renderer, &measure_limits);
        let size = Size::new(
            (measured.size().width + self.extra.x).min(limits.max().width),
            (measured.size().height + self.extra.y).min(limits.max().height),
        );
        let final_limits = layout::Limits::new(size, size);
        let content = self.children[1]
            .as_widget_mut()
            .layout(&mut tree.children[1], renderer, &final_limits);

        layout::Node::with_children(size, vec![measured, content])
    }

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

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

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

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

    fn overlay<'b>(
        &'b mut self,
        tree: &'b mut widget::Tree,
        layout: Layout<'b>,
        renderer: &Renderer,
        viewport: &Rectangle,
        translation: Vector,
    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
        self.children[1]
            .as_widget_mut()
            .overlay(
                &mut tree.children[1],
                layout.child(1),
                renderer,
                viewport,
                translation,
            )
    }
}

impl ModalOptions {
    pub const STANDARD: Self = Self {
        movable: true,
        resizable: true,
        neutral_close: false,
    };

    pub const NOTICE: Self = Self {
        movable: true,
        resizable: false,
        neutral_close: true,
    };
}

/// Dim and block the application beneath an overlay-owned dialog.
///
/// Some third-party overlays ignore pointer movement outside their interactive
/// controls. The shield owns the cursor and pointer presses across the window,
/// making such an overlay behave modally.
pub fn backdrop<'a>(
    base: impl Into<Element<'a, Message>>,
    on_close: Message,
) -> Element<'a, Message> {
    let shield = mouse_area(
        container(Space::new())
            .width(Length::Fill)
            .height(Length::Fill)
            .style(|theme: &Theme| container::Style {
                background: Some(Background::Color(
                    theme
                        .palette()
                        .background
                        .strongest
                        .color
                        .scale_alpha(0.55),
                )),
                ..Default::default()
            }),
    )
    .on_press(on_close)
    .interaction(iced::mouse::Interaction::Idle);

    stack![base.into(), opaque(shield)].into()
}

/// Stack `content` over `base` behind a dimmed backdrop, framed with a title bar
/// (the ✕ close button at its right end). The backdrop only dims and blocks
/// clicks from reaching the view beneath — it does **not** dismiss the dialog;
/// closing is the ✕ button alone (`on_close`).
///
/// `offset` shifts the dialog from screen-centre so it can be dragged by its
/// title bar; pass `Vector::ZERO` to keep it centred.
pub fn modal<'a>(
    base: impl Into<Element<'a, Message>>,
    title: impl iced::widget::text::IntoFragment<'a>,
    content: impl Into<Element<'a, Message>>,
    on_close: Message,
    offset: Vector,
    options: ModalOptions,
) -> Element<'a, Message> {
    let close = if options.neutral_close {
        button(crate::ui::icons::themed_secondary(
            crate::ui::icons::CLOSE,
            13.0,
        ))
        .on_press(on_close)
        .padding([1, 7])
        .style(neutral_close_style)
    } else {
        button(crate::ui::icons::themed_danger(
            crate::ui::icons::CLOSE,
            13.0,
        ))
        .on_press(on_close)
        .padding([1, 7])
        .style(close_style)
    };

    // The dialog name is centred across the content width with the ✕ overlaid
    // at the right edge. The title bar is itself an overlay: the content layer
    // below determines the intrinsic modal width before `Fill` is resolved.
    let title_text = iced::widget::text(title).size(15);
    let title_surface: Element<'a, Message> = container(title_text)
        .width(Length::Fill)
        .height(Length::Fixed(24.0))
        .align_x(iced::alignment::Horizontal::Center)
        .align_y(iced::alignment::Vertical::Center)
        .into();
    let title_surface: Element<'a, Message> = if options.movable {
        mouse_area(title_surface)
            .on_press(Message::ModalGrab)
            .interaction(iced::mouse::Interaction::Grab)
            .into()
    } else {
        title_surface
    };
    let controls: Element<'a, Message> = row![close].align_y(iced::Center).into();
    let title_bar = stack![
        title_surface,
        container(controls)
            .width(Length::Fill)
            .height(Length::Fixed(24.0))
            .align_x(iced::alignment::Horizontal::Right)
            .align_y(iced::alignment::Vertical::Center),
    ]
    .width(Length::Fill)
    .height(Length::Fixed(24.0));

    let panel_style = |theme: &Theme| container::Style {
        background: Some(Background::Color(
            theme.palette().background.base.color,
        )),
        border: Border {
            color: theme.palette().background.neutral.color,
            width: 1.0,
            radius: 6.0.into(),
        },
        ..Default::default()
    };

    // The first stack layer dictates its intrinsic size. Its top spacer reserves
    // room for the title, while the actual title bar and resize grip overlay it
    // without influencing the modal dimensions.
    let measured_content = sensor(content).on_resize(Message::ModalContentResized);
    let body_base = column![
        Space::new().height(Length::Fixed(24.0)),
        measured_content,
    ]
    .spacing(6);
    let mut body = stack![body_base, title_bar];
    if options.resizable {
        let resize = mouse_area(
            container(crate::ui::icons::themed_primary(crate::ui::icons::RESIZE, 15.0))
                .padding([0, 2]),
        )
        .on_press(Message::ModalResizeGrab)
        .interaction(iced::mouse::Interaction::Grab);
        body = body.push(
            container(resize)
                .width(Length::Fill)
                .height(Length::Fill)
                .align_x(iced::alignment::Horizontal::Right)
                .align_y(iced::alignment::Vertical::Bottom),
        );
    }
    let framed: Element<'a, Message> =
        container(body).padding(10).style(panel_style).into();

    // Position via asymmetric padding (padding is non-negative): shifting a
    // centred box by `d` on an axis needs (near − far) padding = 2·d there.
    let pad = Padding {
        top: offset.y.max(0.0) * 2.0,
        right: (-offset.x).max(0.0) * 2.0,
        bottom: (-offset.y).max(0.0) * 2.0,
        left: offset.x.max(0.0) * 2.0,
    };

    // The backdrop fills the screen so dragging keeps tracking the cursor even
    // when it leaves the title bar; release anywhere ends the drag.
    let backdrop = mouse_area(
        container(framed)
            .center_x(Length::Fill)
            .center_y(Length::Fill)
            .padding(pad)
            .style(|theme: &Theme| container::Style {
                background: Some(Background::Color(
                    theme
                        .palette()
                        .background
                        .strongest
                        .color
                        .scale_alpha(0.55),
                )),
                ..Default::default()
            }),
    )
    .on_move(Message::ModalDragMove)
    .on_release(Message::ModalDragRelease);

    stack![
        base.into(),
        // `opaque` blocks pointer events from passing through, so the dimmed
        // backdrop swallows clicks instead of closing or hitting the view.
        opaque(backdrop),
    ]
    .into()
}

fn close_style(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.palette();
    let pair = match status {
        button::Status::Hovered | button::Status::Pressed => palette.danger.strong,
        _ => palette.background.weakest,
    };
    button::Style {
        background: Some(Background::Color(pair.color)),
        text_color: pair.text,
        border: Border {
            radius: 4.0.into(),
            ..Default::default()
        },
        ..Default::default()
    }
}

fn neutral_close_style(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.palette();
    let pair = match status {
        button::Status::Hovered | button::Status::Pressed => palette.background.weak,
        _ => palette.background.weakest,
    };
    button::Style {
        background: Some(Background::Color(pair.color)),
        text_color: pair.text,
        border: Border {
            radius: 4.0.into(),
            ..Default::default()
        },
        ..Default::default()
    }
}
