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, Step12, StrokeWidth, SurfaceLevel};
use crate::widget_id::{WidgetId, WidgetKey};

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

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PanelVariant {
    Plain,
    Card,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct PanelState {
    pub collapsed: bool,
}

impl PanelState {
    #[must_use]
    pub const fn open() -> Self {
        Self { collapsed: false }
    }

    #[must_use]
    pub const fn collapsed() -> Self {
        Self { collapsed: true }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PanelTitlebar {
    pub label: StringKey,
    pub height: LayoutPx,
    pub collapsible: bool,
}

#[derive(Debug, PartialEq)]
pub struct Panel<'state> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub variant: PanelVariant,
    pub titlebar: Option<PanelTitlebar>,
    pub state: &'state mut PanelState,
}

impl<'state> Panel<'state> {
    #[must_use]
    pub fn new(id: WidgetId, rect: LayoutRect, state: &'state mut PanelState) -> Self {
        Self {
            id,
            rect,
            variant: PanelVariant::Plain,
            titlebar: None,
            state,
        }
    }

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

    #[must_use]
    pub fn titlebar(self, titlebar: PanelTitlebar) -> Self {
        Self {
            titlebar: Some(titlebar),
            ..self
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PanelResponse {
    pub body_rect: Option<LayoutRect>,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_panel(ctx: &mut FrameCtx<'_>, panel: Panel<'_>) -> PanelResponse {
    let Panel {
        id,
        rect,
        variant,
        titlebar,
        state,
    } = panel;
    let host = match titlebar {
        Some(bar) => AccessNode::new(Role::Pane).with_label(bar.label),
        None => AccessNode::new(Role::Pane),
    };
    ctx.a11y.push(id, rect, host);
    let mut paint = panel_surface(ctx, rect, variant);
    let body_origin_y = match titlebar {
        None => rect.origin.y,
        Some(bar) => {
            paint.extend(draw_titlebar(ctx, id, rect, bar, state));
            LayoutPx::new(rect.origin.y.value() + bar.height.value())
        }
    };
    let body_rect = if state.collapsed {
        None
    } else {
        Some(LayoutRect::new(
            LayoutPos::new(rect.origin.x, body_origin_y),
            LayoutSize::new(
                rect.size.width,
                LayoutPx::saturating_nonneg(
                    rect.size.height.value() - (body_origin_y.value() - rect.origin.y.value()),
                ),
            ),
        ))
    };
    PanelResponse { body_rect, paint }
}

fn panel_surface(ctx: &FrameCtx<'_>, rect: LayoutRect, variant: PanelVariant) -> Vec<WidgetPaint> {
    let neutral = ctx.theme().colors.neutral;
    let (fill, border) = match variant {
        PanelVariant::Plain => (ctx.theme().colors.surface(SurfaceLevel::L0), None),
        PanelVariant::Card => (
            ctx.theme().colors.surface(SurfaceLevel::L1),
            Some(Border {
                width: StrokeWidth::HAIRLINE,
                color: neutral.step(Step12::SUBTLE_BORDER),
            }),
        ),
    };
    vec![WidgetPaint::Surface {
        rect,
        fill,
        border,
        radius: ctx.theme().radius.sm,
        elevation: None,
    }]
}

fn draw_titlebar(
    ctx: &mut FrameCtx<'_>,
    id: WidgetId,
    panel_rect: LayoutRect,
    bar: PanelTitlebar,
    state: &mut PanelState,
) -> Vec<WidgetPaint> {
    let title_rect = LayoutRect::new(
        panel_rect.origin,
        LayoutSize::new(panel_rect.size.width, bar.height),
    );
    let mut paint = vec![WidgetPaint::Surface {
        rect: title_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.sm,
        elevation: None,
    }];
    let toggle_id = id.child(WidgetKey::new("titlebar"));
    let toggle_rect = title_rect;
    if bar.collapsible {
        let interaction = ctx.interact(
            InteractDeclaration::new(toggle_id, toggle_rect, Sense::INTERACTIVE)
                .focusable(true)
                .a11y(
                    AccessNode::new(Role::Button)
                        .with_label(bar.label)
                        .with_expanded(!state.collapsed),
                ),
        );
        let live_focused = ctx.is_focused(toggle_id);
        if interaction.click() {
            state.collapsed = !state.collapsed;
        }
        if live_focused {
            let activated = take_key(
                ctx.input,
                &[
                    TakeKey::named(crate::input::NamedKey::Enter),
                    TakeKey::named(crate::input::NamedKey::Space),
                ],
            );
            if activated.is_some() {
                state.collapsed = !state.collapsed;
            }
        }
        let chevron_rect = chevron_rect(title_rect);
        paint.push(WidgetPaint::Mark {
            rect: chevron_rect,
            kind: if state.collapsed {
                GlyphMark::DisclosureClosed
            } else {
                GlyphMark::DisclosureOpen
            },
            color: ctx.theme().colors.text_secondary(),
        });
        push_focus_ring(
            ctx,
            &mut paint,
            title_rect,
            ctx.theme().radius.sm,
            live_focused,
        );
    }
    paint.push(WidgetPaint::AlignedLabel {
        rect: label_rect(title_rect, bar.collapsible),
        text: LabelText::Key(bar.label),
        color: ctx.theme().colors.text_primary(),
        role: ctx.theme().typography.title,
        align: HorizontalAlign::Start,
    });
    paint
}

const CHEVRON_PX: f32 = 14.0;
const CHEVRON_GAP: f32 = 6.0;

fn chevron_rect(title: LayoutRect) -> LayoutRect {
    let pad = (title.size.height.value() - CHEVRON_PX).max(0.0) / 2.0;
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(title.origin.x.value() + title.size.width.value() - CHEVRON_PX - pad),
            LayoutPx::new(title.origin.y.value() + pad),
        ),
        LayoutSize::new(LayoutPx::new(CHEVRON_PX), LayoutPx::new(CHEVRON_PX)),
    )
}

fn label_rect(title: LayoutRect, leave_room_for_chevron: bool) -> LayoutRect {
    let trail = if leave_room_for_chevron {
        CHEVRON_PX + CHEVRON_GAP
    } else {
        CHEVRON_GAP
    };
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(title.origin.x.value() + CHEVRON_GAP),
            title.origin.y,
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(title.size.width.value() - CHEVRON_GAP - trail),
            title.size.height,
        ),
    )
}

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

    use super::{Panel, PanelState, PanelTitlebar, PanelVariant, show_panel};
    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 panel_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("panel"))
    }

    fn panel_rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(120.0)),
        )
    }

    const TITLE: StringKey = StringKey::new("panel.title");

    fn titlebar(collapsible: bool) -> PanelTitlebar {
        PanelTitlebar {
            label: TITLE,
            height: LayoutPx::new(28.0),
            collapsible,
        }
    }

    fn render(
        state: &mut PanelState,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
        bar: Option<PanelTitlebar>,
    ) -> (super::PanelResponse, 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,
            );
            let mut p = Panel::new(panel_id(), panel_rect(), state).variant(PanelVariant::Card);
            if let Some(t) = bar {
                p = p.titlebar(t);
            }
            show_panel(&mut ctx, p)
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    #[test]
    fn open_panel_with_titlebar_returns_body_rect_below_bar() {
        let mut state = PanelState::open();
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(
            &mut state,
            &mut focus,
            &mut snap,
            &prev,
            Some(titlebar(true)),
        );
        let Some(body) = response.body_rect else {
            panic!("expected body rect");
        };
        assert!(body.origin.y.value() >= 28.0);
        assert!(body.size.height.value() <= 120.0 - 28.0 + 0.001);
    }

    #[test]
    fn collapsed_panel_omits_body_rect() {
        let mut state = PanelState::collapsed();
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(
            &mut state,
            &mut focus,
            &mut snap,
            &prev,
            Some(titlebar(true)),
        );
        assert!(response.body_rect.is_none());
    }

    #[test]
    fn click_titlebar_toggles_collapsed() {
        let mut state = PanelState::open();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let title_pos = LayoutPos::new(LayoutPx::new(40.0), LayoutPx::new(14.0));
        [press(title_pos), release(title_pos), idle(title_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(
                    &mut state,
                    &mut focus,
                    &mut snap,
                    &prev,
                    Some(titlebar(true)),
                );
                prev = next;
            });
        assert!(state.collapsed);
    }

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

        let toggle_id = panel_id().child(WidgetKey::new("titlebar"));
        let mut state = PanelState::open();
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        focus.request_focus(toggle_id);
        let mut warm = InputSnapshot::idle(FrameInstant::ZERO);
        let _ = render(
            &mut state,
            &mut focus,
            &mut warm,
            &prev,
            Some(titlebar(true)),
        );
        assert_eq!(focus.focused(), Some(toggle_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,
            Some(titlebar(true)),
        );
        assert!(state.collapsed, "Enter on focused titlebar must collapse");

        let mut space = InputSnapshot::idle(FrameInstant::ZERO);
        space.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Space),
            ModifierMask::NONE,
        ));
        let _ = render(
            &mut state,
            &mut focus,
            &mut space,
            &prev,
            Some(titlebar(true)),
        );
        assert!(!state.collapsed, "Space on focused titlebar must expand");
    }

    #[test]
    fn no_titlebar_returns_body_equal_to_panel() {
        let mut state = PanelState::open();
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(&mut state, &mut focus, &mut snap, &prev, None);
        let Some(body) = response.body_rect else {
            panic!("expected body");
        };
        assert_eq!(body, panel_rect());
    }

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