use core::time::Duration;

use serde::Serialize;

use crate::a11y::{AccessNode, Role};
use crate::frame::FrameCtx;
use crate::hit_test::Interaction;
use crate::input::FrameInstant;
use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::strings::StringKey;
use crate::widget_id::{WidgetId, WidgetKey};

use super::paint::{LabelText, WidgetPaint};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub enum TooltipPlacement {
    Below,
    Above,
    Right,
    Left,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct TooltipState {
    pub showing: bool,
    pub hover_began: Option<FrameInstant>,
    pub focus_began: Option<FrameInstant>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Tooltip {
    pub anchor: WidgetId,
    pub anchor_rect: LayoutRect,
    pub label: StringKey,
    pub placement: TooltipPlacement,
    pub gap: LayoutPx,
    pub size: LayoutSize,
    pub delay: Duration,
}

impl Tooltip {
    #[must_use]
    pub const fn new(
        anchor: WidgetId,
        anchor_rect: LayoutRect,
        label: StringKey,
        placement: TooltipPlacement,
        size: LayoutSize,
    ) -> Self {
        Self {
            anchor,
            anchor_rect,
            label,
            placement,
            gap: LayoutPx::new(4.0),
            size,
            delay: Duration::from_millis(500),
        }
    }

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

#[must_use]
pub fn show_tooltip(
    ctx: &mut FrameCtx<'_>,
    tooltip: Tooltip,
    state: &mut TooltipState,
) -> Vec<WidgetPaint> {
    let interaction = ctx.previous.interaction(tooltip.anchor);
    let now = ctx.input.frame;
    update_state(state, &interaction, now);
    state.showing = should_show(state, now, tooltip.delay);
    if !state.showing {
        return Vec::new();
    }
    let rect = popup_rect(
        tooltip.anchor_rect,
        tooltip.placement,
        tooltip.gap,
        tooltip.size,
    );
    ctx.a11y.push(
        tooltip.anchor.child(WidgetKey::new("tooltip")),
        rect,
        AccessNode::new(Role::Tooltip).with_label(tooltip.label),
    );
    vec![WidgetPaint::Tooltip {
        rect,
        text: LabelText::Key(tooltip.label),
        anchor: tooltip.anchor,
        elevation: ctx.theme().elevation.level2,
    }]
}

fn update_state(state: &mut TooltipState, interaction: &Interaction, now: FrameInstant) {
    if interaction.hover() {
        if state.hover_began.is_none() {
            state.hover_began = Some(now);
        }
    } else {
        state.hover_began = None;
    }
    if interaction.focused() {
        if state.focus_began.is_none() {
            state.focus_began = Some(now);
        }
    } else {
        state.focus_began = None;
    }
}

fn should_show(state: &TooltipState, now: FrameInstant, delay: Duration) -> bool {
    let qualifies = |start: FrameInstant| now.since(start) >= delay;
    state.hover_began.is_some_and(qualifies) || state.focus_began.is_some_and(qualifies)
}

fn popup_rect(
    anchor: LayoutRect,
    placement: TooltipPlacement,
    gap: LayoutPx,
    size: LayoutSize,
) -> LayoutRect {
    let centered_x =
        anchor.origin.x.value() + anchor.size.width.value() / 2.0 - size.width.value() / 2.0;
    let centered_y =
        anchor.origin.y.value() + anchor.size.height.value() / 2.0 - size.height.value() / 2.0;
    let origin = match placement {
        TooltipPlacement::Below => LayoutPos::new(
            LayoutPx::new(centered_x),
            LayoutPx::new(anchor.origin.y.value() + anchor.size.height.value() + gap.value()),
        ),
        TooltipPlacement::Above => LayoutPos::new(
            LayoutPx::new(centered_x),
            LayoutPx::new(anchor.origin.y.value() - size.height.value() - gap.value()),
        ),
        TooltipPlacement::Right => LayoutPos::new(
            LayoutPx::new(anchor.origin.x.value() + anchor.size.width.value() + gap.value()),
            LayoutPx::new(centered_y),
        ),
        TooltipPlacement::Left => LayoutPos::new(
            LayoutPx::new(anchor.origin.x.value() - size.width.value() - gap.value()),
            LayoutPx::new(centered_y),
        ),
    };
    LayoutRect::new(origin, size)
}

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

    use super::{Tooltip, TooltipPlacement, TooltipState, show_tooltip};
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, Interaction, InteractionState};
    use crate::hotkey::HotkeyTable;
    use crate::input::{FrameInstant, InputSnapshot};
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::StringKey;
    use crate::strings::StringTable;
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};
    use crate::widgets::WidgetPaint;

    const LABEL: StringKey = StringKey::new("tooltip.label");

    fn anchor_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("anchor"))
    }

    fn anchor_rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(50.0), LayoutPx::new(40.0)),
            LayoutSize::new(LayoutPx::new(60.0), LayoutPx::new(20.0)),
        )
    }

    fn tooltip() -> Tooltip {
        Tooltip::new(
            anchor_id(),
            anchor_rect(),
            LABEL,
            TooltipPlacement::Below,
            LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(18.0)),
        )
    }

    fn run(
        prev: &HitState,
        frame: FrameInstant,
        state: &mut TooltipState,
        tooltip: Tooltip,
    ) -> Vec<WidgetPaint> {
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let mut input = InputSnapshot::idle(frame);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = crate::a11y::AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            prev,
            &mut a11y,
            &mut shaper,
        );
        show_tooltip(&mut ctx, tooltip, state)
    }

    fn prev_with_hover() -> HitState {
        let mut state = HitState::new();
        state.interactions.insert(
            anchor_id(),
            Interaction {
                state: InteractionState::HOVER,
                ..Interaction::idle()
            },
        );
        state
    }

    fn prev_with_focus() -> HitState {
        let mut state = HitState::new();
        state.interactions.insert(
            anchor_id(),
            Interaction {
                state: InteractionState::FOCUSED,
                ..Interaction::idle()
            },
        );
        state
    }

    fn prev_idle() -> HitState {
        HitState::new()
    }

    #[test]
    fn hover_for_less_than_delay_does_not_show() {
        let mut state = TooltipState::default();
        let prev = prev_with_hover();
        let _ = run(
            &prev,
            FrameInstant::from_duration(Duration::from_millis(100)),
            &mut state,
            tooltip(),
        );
        assert!(!state.showing);
    }

    #[test]
    fn hover_past_delay_shows_tooltip() {
        let mut state = TooltipState::default();
        let prev = prev_with_hover();
        let _ = run(
            &prev,
            FrameInstant::from_duration(Duration::from_millis(10)),
            &mut state,
            tooltip(),
        );
        let paint = run(
            &prev,
            FrameInstant::from_duration(Duration::from_millis(800)),
            &mut state,
            tooltip(),
        );
        assert!(state.showing);
        assert_eq!(paint.len(), 1);
    }

    #[test]
    fn losing_hover_resets_timer() {
        let mut state = TooltipState::default();
        let _ = run(
            &prev_with_hover(),
            FrameInstant::from_duration(Duration::from_millis(10)),
            &mut state,
            tooltip(),
        );
        let _ = run(
            &prev_idle(),
            FrameInstant::from_duration(Duration::from_millis(100)),
            &mut state,
            tooltip(),
        );
        assert!(state.hover_began.is_none());
        assert!(!state.showing);
    }

    #[test]
    fn focus_satisfies_show_condition() {
        let mut state = TooltipState::default();
        let _ = run(
            &prev_with_focus(),
            FrameInstant::from_duration(Duration::from_millis(10)),
            &mut state,
            tooltip(),
        );
        let paint = run(
            &prev_with_focus(),
            FrameInstant::from_duration(Duration::from_millis(700)),
            &mut state,
            tooltip(),
        );
        assert!(state.showing);
        assert_eq!(paint.len(), 1);
    }

    #[test]
    fn placement_below_positions_under_anchor() {
        let mut state = TooltipState::default();
        let _ = run(
            &prev_with_hover(),
            FrameInstant::from_duration(Duration::from_millis(10)),
            &mut state,
            tooltip(),
        );
        let paint = run(
            &prev_with_hover(),
            FrameInstant::from_duration(Duration::from_millis(900)),
            &mut state,
            tooltip(),
        );
        let WidgetPaint::Tooltip { rect, .. } = &paint[0] else {
            panic!("expected tooltip paint")
        };
        assert!(
            rect.origin.y.value()
                > anchor_rect().origin.y.value() + anchor_rect().size.height.value()
        );
    }

    #[test]
    fn placement_above_inverts_y() {
        let mut state = TooltipState::default();
        let above = Tooltip::new(
            anchor_id(),
            anchor_rect(),
            LABEL,
            TooltipPlacement::Above,
            LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(18.0)),
        );
        let _ = run(
            &prev_with_hover(),
            FrameInstant::from_duration(Duration::from_millis(10)),
            &mut state,
            above,
        );
        let paint = run(
            &prev_with_hover(),
            FrameInstant::from_duration(Duration::from_millis(900)),
            &mut state,
            above,
        );
        let WidgetPaint::Tooltip { rect, .. } = &paint[0] else {
            panic!("expected tooltip paint")
        };
        assert!(rect.origin.y.value() < anchor_rect().origin.y.value());
    }
}
