use core::time::Duration;

use bone_types::IconId;

use crate::a11y::{AccessNode, Role};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::Sense;
use crate::input::{FrameInstant, NamedKey};
use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::strings::StringKey;
use crate::theme::{Border, Color, Step12, StrokeWidth};
use crate::widget_id::{WidgetId, WidgetKey};

use super::keys::{TakeKey, take_key};
use super::paint::{GlyphMark, IconTint, LabelText, WidgetPaint};
use super::visuals::push_focus_ring;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ToastKind {
    Info,
    Success,
    Warning,
    Danger,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct ToastState {
    pub spawned_at: Option<FrameInstant>,
    pub dismissed: bool,
}

impl ToastState {
    #[must_use]
    pub const fn fresh() -> Self {
        Self {
            spawned_at: None,
            dismissed: false,
        }
    }
}

#[derive(Debug, PartialEq)]
pub struct Toast<'state> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub kind: ToastKind,
    pub message: StringKey,
    pub dismissible: bool,
    pub ttl: Duration,
    pub state: &'state mut ToastState,
}

impl<'state> Toast<'state> {
    #[must_use]
    pub fn new(
        id: WidgetId,
        rect: LayoutRect,
        kind: ToastKind,
        message: StringKey,
        state: &'state mut ToastState,
    ) -> Self {
        Self {
            id,
            rect,
            kind,
            message,
            dismissible: true,
            ttl: Duration::from_secs(4),
            state,
        }
    }

    #[must_use]
    pub fn ttl(self, ttl: Duration) -> Self {
        Self { ttl, ..self }
    }

    #[must_use]
    pub fn dismissible(self, dismissible: bool) -> Self {
        Self {
            dismissible,
            ..self
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct ToastResponse {
    pub visible: bool,
    pub dismissed_now: bool,
    pub paint: Vec<WidgetPaint>,
}

const CLOSE_PX: f32 = 14.0;
const CLOSE_GAP: f32 = 8.0;
const TOAST_PADDING: f32 = 12.0;

#[must_use]
pub fn show_toast(ctx: &mut FrameCtx<'_>, toast: Toast<'_>) -> ToastResponse {
    let Toast {
        id,
        rect,
        kind,
        message,
        dismissible,
        ttl,
        state,
    } = toast;
    let now = ctx.input.frame;
    if state.spawned_at.is_none() {
        state.spawned_at = Some(now);
    }
    let was_dismissed = state.dismissed;
    let aged_out = state.spawned_at.is_some_and(|t| now.since(t) >= ttl);
    if aged_out {
        state.dismissed = true;
    }
    let mut paint = Vec::new();
    if was_dismissed {
        return ToastResponse {
            visible: false,
            dismissed_now: false,
            paint,
        };
    }
    if aged_out {
        return ToastResponse {
            visible: false,
            dismissed_now: true,
            paint,
        };
    }
    ctx.a11y
        .push(id, rect, AccessNode::new(Role::Alert).with_label(message));
    paint.push(WidgetPaint::Surface {
        rect,
        fill: surface_fill(ctx, kind),
        border: Some(Border {
            width: StrokeWidth::HAIRLINE,
            color: border_color(ctx, kind),
        }),
        radius: ctx.theme().radius.md,
        elevation: Some(ctx.theme().elevation.level1),
    });
    paint.push(leading_mark(
        leading_mark_rect(rect),
        kind,
        leading_mark_color(ctx, kind),
    ));
    paint.push(WidgetPaint::Label {
        rect: message_rect(rect, dismissible),
        text: LabelText::Key(message),
        color: ctx.theme().colors.text_primary(),
        role: ctx.theme().typography.body,
    });
    let dismissed_now = dismissible && draw_close_button(ctx, id, rect, state, &mut paint);
    ToastResponse {
        visible: true,
        dismissed_now,
        paint,
    }
}

fn draw_close_button(
    ctx: &mut FrameCtx<'_>,
    id: WidgetId,
    rect: LayoutRect,
    state: &mut ToastState,
    paint: &mut Vec<WidgetPaint>,
) -> bool {
    let close_id = id.child(WidgetKey::new("close"));
    let close_rect = close_button_rect(rect);
    let interaction = ctx.interact(
        InteractDeclaration::new(close_id, close_rect, Sense::INTERACTIVE)
            .focusable(true)
            .a11y(AccessNode::new(Role::Button).with_label(StringKey::new("toast.close"))),
    );
    let live_focused = ctx.is_focused(close_id);
    let key_activated = live_focused
        && take_key(
            ctx.input,
            &[
                TakeKey::named(NamedKey::Enter),
                TakeKey::named(NamedKey::Space),
            ],
        )
        .is_some();
    let dismissed_now = interaction.click() || key_activated;
    if dismissed_now {
        state.dismissed = true;
    }
    paint.push(WidgetPaint::Surface {
        rect: close_rect,
        fill: if interaction.hover() {
            ctx.theme().colors.neutral.step(Step12::HOVER_BG)
        } else {
            Color::TRANSPARENT
        },
        border: None,
        radius: ctx.theme().radius.sm,
        elevation: None,
    });
    paint.push(WidgetPaint::Icon {
        rect: close_rect,
        icon: IconId::Cross,
        tint: IconTint::Solid(ctx.theme().colors.text_secondary()),
    });
    push_focus_ring(ctx, paint, close_rect, ctx.theme().radius.sm, live_focused);
    dismissed_now
}

fn surface_fill(ctx: &FrameCtx<'_>, kind: ToastKind) -> Color {
    let scale = match kind {
        ToastKind::Info => ctx.theme().colors.info,
        ToastKind::Success => ctx.theme().colors.success,
        ToastKind::Warning => ctx.theme().colors.warning,
        ToastKind::Danger => ctx.theme().colors.danger,
    };
    scale.step(Step12::SUBTLE_BG)
}

fn border_color(ctx: &FrameCtx<'_>, kind: ToastKind) -> Color {
    let scale = match kind {
        ToastKind::Info => ctx.theme().colors.info,
        ToastKind::Success => ctx.theme().colors.success,
        ToastKind::Warning => ctx.theme().colors.warning,
        ToastKind::Danger => ctx.theme().colors.danger,
    };
    scale.step(Step12::BORDER)
}

fn leading_mark_color(ctx: &FrameCtx<'_>, kind: ToastKind) -> Color {
    let scale = match kind {
        ToastKind::Info => ctx.theme().colors.info,
        ToastKind::Success => ctx.theme().colors.success,
        ToastKind::Warning => ctx.theme().colors.warning,
        ToastKind::Danger => ctx.theme().colors.danger,
    };
    scale.step(Step12::SOLID)
}

fn leading_mark(rect: LayoutRect, kind: ToastKind, color: Color) -> WidgetPaint {
    let icon = match kind {
        ToastKind::Info | ToastKind::Success => Some(IconId::Check),
        ToastKind::Danger => Some(IconId::Cross),
        ToastKind::Warning => None,
    };
    match icon {
        Some(icon) => WidgetPaint::Icon {
            rect,
            icon,
            tint: IconTint::Solid(color),
        },
        None => WidgetPaint::Mark {
            rect,
            kind: GlyphMark::Indeterminate,
            color,
        },
    }
}

fn leading_mark_rect(toast: LayoutRect) -> LayoutRect {
    let pad = (toast.size.height.value() - CLOSE_PX) / 2.0;
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(toast.origin.x.value() + TOAST_PADDING),
            LayoutPx::new(toast.origin.y.value() + pad),
        ),
        LayoutSize::new(LayoutPx::new(CLOSE_PX), LayoutPx::new(CLOSE_PX)),
    )
}

fn message_rect(toast: LayoutRect, has_close: bool) -> LayoutRect {
    let leading = TOAST_PADDING + CLOSE_PX + CLOSE_GAP;
    let trailing = if has_close {
        TOAST_PADDING + CLOSE_PX
    } else {
        TOAST_PADDING
    };
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(toast.origin.x.value() + leading),
            toast.origin.y,
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(toast.size.width.value() - leading - trailing),
            toast.size.height,
        ),
    )
}

fn close_button_rect(toast: LayoutRect) -> LayoutRect {
    let pad = (toast.size.height.value() - CLOSE_PX) / 2.0;
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(
                toast.origin.x.value() + toast.size.width.value() - CLOSE_PX - TOAST_PADDING,
            ),
            LayoutPx::new(toast.origin.y.value() + pad),
        ),
        LayoutSize::new(LayoutPx::new(CLOSE_PX), LayoutPx::new(CLOSE_PX)),
    )
}

#[cfg(test)]
mod tests {
    use core::time::Duration;
    use std::sync::Arc;

    use super::{Toast, ToastKind, ToastState, show_toast};
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, resolve};
    use crate::hotkey::HotkeyTable;
    use crate::input::{
        FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, PointerSample,
    };
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{StringKey, StringTable};
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

    fn toast_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("toast"))
    }

    fn rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(20.0), LayoutPx::new(560.0)),
            LayoutSize::new(LayoutPx::new(360.0), LayoutPx::new(48.0)),
        )
    }

    fn render(
        state: &mut ToastState,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
        ttl_ms: u64,
    ) -> (super::ToastResponse, HitState) {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                snap,
                focus,
                &table,
                StringTable::empty(),
                &mut hits,
                prev,
                &mut a11y,
                &mut shaper,
            );
            show_toast(
                &mut ctx,
                Toast::new(
                    toast_id(),
                    rect(),
                    ToastKind::Info,
                    StringKey::new("toast.msg"),
                    state,
                )
                .ttl(Duration::from_millis(ttl_ms)),
            )
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    #[test]
    fn first_frame_seeds_spawn_time() {
        let mut state = ToastState::fresh();
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(100)));
        let prev = HitState::new();
        let _ = render(&mut state, &mut focus, &mut snap, &prev, 4000);
        assert_eq!(
            state.spawned_at,
            Some(FrameInstant::from_duration(Duration::from_millis(100))),
        );
    }

    #[test]
    fn toast_auto_dismisses_after_ttl() {
        let mut state = ToastState::fresh();
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(0)));
        let _ = render(&mut state, &mut focus, &mut snap, &prev, 1000);
        let mut snap_late =
            InputSnapshot::idle(FrameInstant::from_duration(Duration::from_secs(2)));
        let (response, _) = render(&mut state, &mut focus, &mut snap_late, &prev, 1000);
        assert!(state.dismissed);
        assert!(!response.visible);
    }

    #[test]
    fn enter_on_focused_close_dismisses_toast() {
        use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey};

        let close_id = toast_id().child(WidgetKey::new("close"));
        let mut state = ToastState::fresh();
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        focus.request_focus(close_id);
        let mut warm = InputSnapshot::idle(FrameInstant::ZERO);
        let _ = render(&mut state, &mut focus, &mut warm, &prev, 4000);
        assert_eq!(focus.focused(), Some(close_id));

        let mut enter = InputSnapshot::idle(FrameInstant::ZERO);
        enter.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let _ = render(&mut state, &mut focus, &mut enter, &prev, 4000);
        assert!(
            state.dismissed,
            "Enter on focused close button must dismiss"
        );
    }

    #[test]
    fn click_close_dismisses_toast() {
        let mut state = ToastState::fresh();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let close_x = rect().origin.x.value() + rect().size.width.value() - 12.0 - 7.0;
        let close_y = rect().origin.y.value() + rect().size.height.value() / 2.0;
        let close_pos = LayoutPos::new(LayoutPx::new(close_x), LayoutPx::new(close_y));
        [press(close_pos), release(close_pos), idle(close_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(&mut state, &mut focus, &mut snap, &prev, 4000);
                prev = next;
            });
        assert!(state.dismissed);
    }

    fn press(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn release(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s.buttons_released = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn idle(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s
    }
}
