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

use super::keys::take_activation;
use super::paint::{HorizontalAlign, LabelText, WidgetPaint};
use super::visuals::push_focus_ring;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum StatusAlign {
    Start,
    Center,
    End,
}

#[derive(Clone, Debug, PartialEq)]
pub struct StatusItem {
    pub id: WidgetId,
    pub label: LabelText,
    pub align: StatusAlign,
    pub width: LayoutPx,
    pub interactive: bool,
    pub badge: Option<Color>,
}

impl StatusItem {
    #[must_use]
    pub fn new(id: WidgetId, label: StringKey, align: StatusAlign, width: LayoutPx) -> Self {
        Self::with_text(id, LabelText::Key(label), align, width)
    }

    #[must_use]
    pub fn with_text(id: WidgetId, label: LabelText, align: StatusAlign, width: LayoutPx) -> Self {
        Self {
            id,
            label,
            align,
            width,
            interactive: false,
            badge: None,
        }
    }

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

    #[must_use]
    pub fn badge(self, color: Color) -> Self {
        Self {
            badge: Some(color),
            ..self
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct StatusBar<'a> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub items: &'a [StatusItem],
}

impl<'a> StatusBar<'a> {
    #[must_use]
    pub const fn new(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        items: &'a [StatusItem],
    ) -> Self {
        Self {
            id,
            rect,
            label,
            items,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct StatusBarResponse {
    pub activated: Option<WidgetId>,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_status_bar(ctx: &mut FrameCtx<'_>, bar: StatusBar<'_>) -> StatusBarResponse {
    ctx.a11y.push(
        bar.id,
        bar.rect,
        AccessNode::new(Role::Status).with_label(bar.label),
    );
    let mut paint = vec![WidgetPaint::Surface {
        rect: bar.rect,
        fill: ctx.theme().colors.neutral.step(Step12::SUBTLE_BG),
        border: Some(Border {
            width: StrokeWidth::HAIRLINE,
            color: ctx.theme().colors.neutral.step(Step12::SUBTLE_BORDER),
        }),
        radius: ctx.theme().radius.none,
        elevation: None,
    }];
    let direction = ctx.direction();
    let layouts: Vec<LayoutRect> = lay_out_items(bar.rect, bar.items)
        .into_iter()
        .map(|r| r.mirror_horizontally_within(bar.rect, direction))
        .collect();
    paint.extend(cell_dividers(ctx, bar.rect, bar.items, &layouts));
    let mut activated: Option<WidgetId> = None;
    bar.items
        .iter()
        .zip(layouts.iter())
        .for_each(|(item, item_rect)| {
            let item_paint = draw_item(ctx, item, *item_rect, &mut activated);
            paint.extend(item_paint);
        });
    StatusBarResponse { activated, paint }
}

fn draw_item(
    ctx: &mut FrameCtx<'_>,
    item: &StatusItem,
    rect: LayoutRect,
    activated: &mut Option<WidgetId>,
) -> Vec<WidgetPaint> {
    let mut paint = Vec::new();
    let live_focused = if item.interactive {
        let interaction = ctx.interact(
            InteractDeclaration::new(item.id, rect, Sense::INTERACTIVE)
                .focusable(true)
                .a11y(AccessNode::new(Role::Button).with_label_text(item.label.clone())),
        );
        let focused = ctx.is_focused(item.id);
        let activated_via_pointer = interaction.click();
        let activated_via_key = focused && take_activation(ctx.input);
        if (activated_via_pointer || activated_via_key) && activated.is_none() {
            *activated = Some(item.id);
        }
        if interaction.hover() || interaction.pressed() {
            paint.push(WidgetPaint::Surface {
                rect,
                fill: ctx.theme().colors.neutral.step(if interaction.pressed() {
                    Step12::SELECTED_BG
                } else {
                    Step12::HOVER_BG
                }),
                border: None,
                radius: ctx.theme().radius.none,
                elevation: None,
            });
        }
        focused
    } else {
        false
    };
    if let Some(badge) = item.badge {
        let dot_rect = badge_rect(rect);
        paint.push(WidgetPaint::Surface {
            rect: dot_rect,
            fill: badge,
            border: None,
            radius: ctx.theme().radius.pill,
            elevation: None,
        });
    }
    let text_align = if item.badge.is_some() {
        HorizontalAlign::Start
    } else {
        match item.align {
            StatusAlign::Start => HorizontalAlign::Start,
            StatusAlign::Center => HorizontalAlign::Center,
            StatusAlign::End => HorizontalAlign::End,
        }
    };
    paint.push(WidgetPaint::AlignedLabel {
        rect: label_rect(rect, item.badge.is_some()),
        text: item.label.clone(),
        color: ctx.theme().colors.text_primary(),
        role: ctx.theme().typography.label,
        align: text_align,
    });
    push_focus_ring(ctx, &mut paint, rect, ctx.theme().radius.none, live_focused);
    paint
}

const BADGE_PX: f32 = 8.0;
const BADGE_GAP: f32 = 6.0;
const ITEM_PAD_X: f32 = 6.0;

fn badge_rect(item: LayoutRect) -> LayoutRect {
    let pad = (item.size.height.value() - BADGE_PX).max(0.0) / 2.0;
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(item.origin.x.value() + ITEM_PAD_X),
            LayoutPx::new(item.origin.y.value() + pad),
        ),
        LayoutSize::new(LayoutPx::new(BADGE_PX), LayoutPx::new(BADGE_PX)),
    )
}

fn label_rect(item: LayoutRect, has_badge: bool) -> LayoutRect {
    let lead = ITEM_PAD_X + if has_badge { BADGE_PX + BADGE_GAP } else { 0.0 };
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(item.origin.x.value() + lead), item.origin.y),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(item.size.width.value() - lead - ITEM_PAD_X),
            item.size.height,
        ),
    )
}

fn cell_dividers(
    ctx: &FrameCtx<'_>,
    bar: LayoutRect,
    items: &[StatusItem],
    layouts: &[LayoutRect],
) -> Vec<WidgetPaint> {
    items
        .windows(2)
        .zip(layouts.windows(2))
        .filter(|(pair, _)| pair[0].align == pair[1].align)
        .filter_map(|(_, rects)| divider_at_seam(ctx, bar, rects[0], rects[1]))
        .collect()
}

fn divider_at_seam(
    ctx: &FrameCtx<'_>,
    bar: LayoutRect,
    a: LayoutRect,
    b: LayoutRect,
) -> Option<WidgetPaint> {
    let (left, right) = if a.origin.x.value() <= b.origin.x.value() {
        (a, b)
    } else {
        (b, a)
    };
    let seam = left.max_x().value();
    if (right.origin.x.value() - seam).abs() > 0.5 {
        return None;
    }
    Some(WidgetPaint::Surface {
        rect: LayoutRect::new(
            LayoutPos::new(LayoutPx::new(seam), bar.origin.y),
            LayoutSize::new(
                LayoutPx::new(StrokeWidth::HAIRLINE.value_px()),
                bar.size.height,
            ),
        ),
        fill: ctx.theme().colors.neutral.step(Step12::SUBTLE_BORDER),
        border: None,
        radius: ctx.theme().radius.none,
        elevation: None,
    })
}

#[derive(Copy, Clone)]
struct AlignTotals {
    start: f32,
    center: f32,
    end: f32,
}

fn lay_out_items(bar: LayoutRect, items: &[StatusItem]) -> Vec<LayoutRect> {
    let totals = items.iter().fold(
        AlignTotals {
            start: 0.0,
            center: 0.0,
            end: 0.0,
        },
        |t, item| match item.align {
            StatusAlign::Start => AlignTotals {
                start: t.start + item.width.value(),
                ..t
            },
            StatusAlign::Center => AlignTotals {
                center: t.center + item.width.value(),
                ..t
            },
            StatusAlign::End => AlignTotals {
                end: t.end + item.width.value(),
                ..t
            },
        },
    );
    let bar_x = bar.origin.x.value();
    let bar_w = bar.size.width.value();
    let start_end = bar_x + totals.start;
    let ideal_center = bar_x + (bar_w - totals.center) / 2.0;
    let center_origin = ideal_center.max(start_end);
    let center_end = center_origin + totals.center;
    let ideal_end = bar_x + bar_w - totals.end;
    let end_origin = ideal_end.max(center_end);
    items
        .iter()
        .scan(
            (bar_x, center_origin, end_origin),
            |(start_cursor, center_cursor, end_cursor), item| {
                let origin_x = match item.align {
                    StatusAlign::Start => {
                        let r = *start_cursor;
                        *start_cursor += item.width.value();
                        r
                    }
                    StatusAlign::Center => {
                        let r = *center_cursor;
                        *center_cursor += item.width.value();
                        r
                    }
                    StatusAlign::End => {
                        let r = *end_cursor;
                        *end_cursor += item.width.value();
                        r
                    }
                };
                Some(LayoutRect::new(
                    LayoutPos::new(LayoutPx::new(origin_x), bar.origin.y),
                    LayoutSize::new(item.width, bar.size.height),
                ))
            },
        )
        .collect()
}

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

    use super::{StatusAlign, StatusBar, StatusItem, show_status_bar};
    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 item_id(name: &'static str) -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new(name))
    }

    fn bar_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("status_bar"))
    }

    fn items() -> Vec<StatusItem> {
        vec![
            StatusItem::new(
                item_id("status"),
                StringKey::new("status.fully"),
                StatusAlign::Start,
                LayoutPx::new(140.0),
            ),
            StatusItem::new(
                item_id("zoom"),
                StringKey::new("status.zoom"),
                StatusAlign::End,
                LayoutPx::new(80.0),
            )
            .interactive(true),
        ]
    }

    fn bar_rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::new(700.0)),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(24.0)),
        )
    }

    fn render(
        items: &[StatusItem],
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
    ) -> (super::StatusBarResponse, 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_status_bar(
                &mut ctx,
                StatusBar::new(bar_id(), bar_rect(), StringKey::new("test.status"), items),
            )
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    #[test]
    fn renders_one_paint_per_item_plus_surface() {
        let items = items();
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(&items, &mut focus, &mut snap, &prev);
        let label_count = response
            .paint
            .iter()
            .filter(|p| matches!(p, super::WidgetPaint::AlignedLabel { .. }))
            .count();
        assert_eq!(label_count, 2);
    }

    #[test]
    fn click_interactive_item_activates_it() {
        let items = items();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(760.0), LayoutPx::new(712.0));
        let mut last: Option<super::StatusBarResponse> = None;
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render(&items, &mut focus, &mut snap, &prev);
                last = Some(response);
                prev = next;
            });
        let Some(response) = last else {
            panic!("response missing")
        };
        assert_eq!(response.activated, Some(items[1].id));
    }

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

        let items = items();
        let interactive_id = items[1].id;
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        focus.request_focus(interactive_id);
        let mut warm = InputSnapshot::idle(FrameInstant::ZERO);
        let _ = render(&items, &mut focus, &mut warm, &prev);
        assert_eq!(focus.focused(), Some(interactive_id));

        let mut enter = InputSnapshot::idle(FrameInstant::ZERO);
        enter.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let (response, _) = render(&items, &mut focus, &mut enter, &prev);
        assert_eq!(response.activated, Some(interactive_id));
    }

    #[test]
    fn overflow_packs_center_and_end_after_start_without_underflow() {
        let crowded = vec![
            StatusItem::new(
                item_id("a"),
                StringKey::new("status.a"),
                StatusAlign::Start,
                LayoutPx::new(400.0),
            ),
            StatusItem::new(
                item_id("b"),
                StringKey::new("status.b"),
                StatusAlign::Center,
                LayoutPx::new(400.0),
            ),
            StatusItem::new(
                item_id("c"),
                StringKey::new("status.c"),
                StatusAlign::End,
                LayoutPx::new(400.0),
            ),
        ];
        let rects = super::lay_out_items(bar_rect(), &crowded);
        assert_eq!(rects.len(), 3);
        let bar_x = bar_rect().origin.x.value();
        rects.iter().for_each(|r| {
            assert!(
                r.origin.x.value() >= bar_x,
                "item origin must not underflow bar start",
            );
        });
        assert!(rects[1].origin.x.value() >= rects[0].origin.x.value() + 400.0);
        assert!(rects[2].origin.x.value() >= rects[1].origin.x.value() + 400.0);
    }

    #[test]
    fn non_interactive_item_does_not_activate() {
        let items = items();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(40.0), LayoutPx::new(712.0));
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render(&items, &mut focus, &mut snap, &prev);
                assert!(response.activated.is_none());
                prev = next;
            });
    }

    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
    }
}
