use bone_render::{CameraTween, lower_f32};
use bone_types::{Camera3, StandardView, Tolerance, UnitVec3};
use bone_ui::a11y::{AccessNode, Role};
use bone_ui::frame::{FrameCtx, InteractDeclaration};
use bone_ui::hit_test::Sense;
use bone_ui::input::{FrameInstant, PointerButton};
use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use bone_ui::strings::StringKey;
use bone_ui::theme::{Border, Color, Step12, StrokeWidth, Theme};
use bone_ui::widgets::{
    Button, ButtonVariant, ContextMenu, ConvexPoly, Menu, MenuItem, MenuState, PolyPath, Tooltip,
    TooltipPlacement, TooltipState, WidgetPaint, show_button, show_context_menu, show_menu,
    show_tooltip,
};
use bone_ui::{WidgetId, WidgetKey};

use crate::strings;

const CUBE_TOLERANCE: Tolerance = Tolerance::new(1.0e-9);
const CUBE_SIDE_PX: f32 = 150.0;
const CUBE_INSET_PX: f32 = 12.0;
const CUBE_MARGIN_PX: f32 = 8.0;
const FACE_LABEL_W_PX: f32 = 46.0;
const FACE_LABEL_H_PX: f32 = 22.0;
const EDGE_HIT_PX: f32 = 18.0;
const CORNER_HIT_PX: f32 = 16.0;
const EDGE_HOVER_WIDTH_PX: f32 = 3.0;
const CORNER_HOVER_HALF_PX: f32 = 7.0;
const LIGHT_DIR: (f64, f64, f64) = (0.35, -0.45, 1.0);
const HOME_WIDTH_PX: f32 = 54.0;
const HOME_HEIGHT_PX: f32 = 16.0;
const HOME_GAP_PX: f32 = 6.0;
const CONFIRM_RESERVE_PX: f32 = 56.0;
const TOOLTIP_SIZE: LayoutSize = LayoutSize::new(LayoutPx::new(72.0), LayoutPx::new(18.0));
const VISIBILITY_EPSILON: f64 = 1.0e-6;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CellKind {
    Face,
    Edge,
    Corner,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CubeCell {
    x: i8,
    y: i8,
    z: i8,
}

impl CubeCell {
    fn try_new(x: i8, y: i8, z: i8) -> Option<Self> {
        let in_range = [x, y, z].into_iter().all(|c| (-1..=1).contains(&c));
        ((x, y, z) != (0, 0, 0) && in_range).then_some(Self { x, y, z })
    }

    #[must_use]
    pub fn all() -> Vec<Self> {
        (-1..=1)
            .flat_map(|x| (-1..=1).flat_map(move |y| (-1..=1).map(move |z| (x, y, z))))
            .filter_map(|(x, y, z)| Self::try_new(x, y, z))
            .collect()
    }

    fn index(self) -> u64 {
        let code = |component: i8| match component {
            -1 => 0,
            0 => 1,
            _ => 2,
        };
        code(self.x) * 9 + code(self.y) * 3 + code(self.z)
    }

    #[must_use]
    pub fn kind(self) -> CellKind {
        match [self.x, self.y, self.z]
            .into_iter()
            .filter(|c| *c != 0)
            .count()
        {
            1 => CellKind::Face,
            2 => CellKind::Edge,
            _ => CellKind::Corner,
        }
    }

    #[must_use]
    pub fn direction(self) -> UnitVec3 {
        let Ok(dir) = UnitVec3::try_from_components(
            f64::from(self.x),
            f64::from(self.y),
            f64::from(self.z),
            CUBE_TOLERANCE,
        ) else {
            unreachable!("a non-zero cell of small integers normalizes");
        };
        dir
    }

    #[must_use]
    pub fn standard_view(self) -> Option<StandardView> {
        match (self.x, self.y, self.z) {
            (0, -1, 0) => Some(StandardView::Front),
            (0, 1, 0) => Some(StandardView::Back),
            (-1, 0, 0) => Some(StandardView::Left),
            (1, 0, 0) => Some(StandardView::Right),
            (0, 0, 1) => Some(StandardView::Top),
            (0, 0, -1) => Some(StandardView::Bottom),
            _ => None,
        }
    }

    fn pick(self) -> ViewPick {
        self.standard_view()
            .map_or_else(|| ViewPick::Direction(self.direction()), ViewPick::Standard)
    }

    fn a11y_label(self) -> StringKey {
        match self.kind() {
            CellKind::Face => {
                standard_view_label(self.standard_view().unwrap_or(StandardView::Isometric))
            }
            CellKind::Edge => strings::VIEW_CUBE_EDGE,
            CellKind::Corner => strings::VIEW_CUBE_CORNER,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ViewPick {
    Standard(StandardView),
    Direction(UnitVec3),
    Home,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ViewCubeMenuAction {
    SetAsHome,
    FitToWindow,
    ViewNormalTo,
}

#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct ViewCubeOutcome {
    pub pick: Option<ViewPick>,
    pub menu: Option<ViewCubeMenuAction>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ActiveTween {
    pub tween: CameraTween,
    pub started: FrameInstant,
}

#[derive(Clone, Debug)]
pub struct ViewUi {
    pub cube_hidden: bool,
    pub selector_open: bool,
    pub selector_menu: MenuState,
    pub cube_menu_open: bool,
    pub cube_menu_anchor: LayoutPos,
    pub cube_menu: MenuState,
    pub cube_tooltip: TooltipState,
    pub home: Option<Camera3>,
    pub tween: Option<ActiveTween>,
}

impl Default for ViewUi {
    fn default() -> Self {
        Self {
            cube_hidden: false,
            selector_open: false,
            selector_menu: MenuState::default(),
            cube_menu_open: false,
            cube_menu_anchor: LayoutPos::ORIGIN,
            cube_menu: MenuState::default(),
            cube_tooltip: TooltipState::default(),
            home: None,
            tween: None,
        }
    }
}

impl ViewUi {
    pub fn toggle_cube(&mut self) {
        self.cube_hidden = !self.cube_hidden;
    }

    pub fn toggle_selector(&mut self) {
        self.selector_open = !self.selector_open;
        if self.selector_open {
            self.selector_menu = MenuState::default();
        }
    }
}

#[must_use]
pub fn standard_view_label(view: StandardView) -> StringKey {
    match view {
        StandardView::Front => strings::VIEW_FRONT,
        StandardView::Back => strings::VIEW_BACK,
        StandardView::Left => strings::VIEW_LEFT,
        StandardView::Right => strings::VIEW_RIGHT,
        StandardView::Top => strings::VIEW_TOP,
        StandardView::Bottom => strings::VIEW_BOTTOM,
        StandardView::Isometric => strings::VIEW_ISOMETRIC,
        StandardView::NormalTo => strings::VIEW_NORMAL_TO,
    }
}

#[derive(Copy, Clone)]
struct Basis {
    right: Vec3f,
    up: Vec3f,
    forward: Vec3f,
}

#[derive(Copy, Clone)]
struct Vec3f {
    x: f64,
    y: f64,
    z: f64,
}

impl Vec3f {
    const fn new(x: f64, y: f64, z: f64) -> Self {
        Self { x, y, z }
    }

    fn dot(self, other: Self) -> f64 {
        self.x * other.x + self.y * other.y + self.z * other.z
    }

    fn cross(self, other: Self) -> Self {
        Self::new(
            self.y * other.z - self.z * other.y,
            self.z * other.x - self.x * other.z,
            self.x * other.y - self.y * other.x,
        )
    }

    fn normalized(self) -> Option<Self> {
        let norm = self.dot(self).sqrt();
        (norm > VISIBILITY_EPSILON).then(|| Self::new(self.x / norm, self.y / norm, self.z / norm))
    }
}

fn view_basis(camera: Camera3) -> Option<Basis> {
    let (ex, ey, ez) = camera.eye().coords_mm();
    let (tx, ty, tz) = camera.target().coords_mm();
    let (ux, uy, uz) = camera.up().components();
    let forward = Vec3f::new(tx - ex, ty - ey, tz - ez).normalized()?;
    let right = forward.cross(Vec3f::new(ux, uy, uz)).normalized()?;
    let up = right.cross(forward);
    Some(Basis { right, up, forward })
}

fn cell_vec(cell: CubeCell) -> Vec3f {
    Vec3f::new(f64::from(cell.x), f64::from(cell.y), f64::from(cell.z))
}

fn cell_visible(cell: CubeCell, forward: Vec3f) -> bool {
    [
        (cell.x, forward.x),
        (cell.y, forward.y),
        (cell.z, forward.z),
    ]
    .into_iter()
    .any(|(component, toward)| f64::from(component) * toward < -VISIBILITY_EPSILON)
}

#[derive(Clone)]
enum CellGeom {
    Face {
        quad: ConvexPoly,
        label_at: LayoutPos,
    },
    Edge {
        a: LayoutPos,
        b: LayoutPos,
    },
    Corner {
        at: LayoutPos,
    },
}

#[derive(Clone)]
struct Placement {
    cell: CubeCell,
    geom: CellGeom,
    pick_rect: LayoutRect,
    depth: f64,
}

fn project(point: (f64, f64, f64), basis: Basis, center: (f32, f32), radius: f32) -> LayoutPos {
    let v = Vec3f::new(point.0, point.1, point.2);
    let scale = radius / 3.0_f32.sqrt();
    let sx = lower_f32(v.dot(basis.right)) * scale;
    let sy = lower_f32(v.dot(basis.up)) * scale;
    LayoutPos::new(
        LayoutPx::saturating(center.0 + sx),
        LayoutPx::saturating(center.1 - sy),
    )
}

fn cell_corners(cell: CubeCell) -> Vec<(f64, f64, f64)> {
    let base = [f64::from(cell.x), f64::from(cell.y), f64::from(cell.z)];
    let zeros: Vec<usize> = (0..3).filter(|&i| base[i] == 0.0).collect();
    let corner = |overrides: &[(usize, f64)]| {
        let at = |i: usize| {
            overrides
                .iter()
                .find_map(|&(j, v)| (j == i).then_some(v))
                .unwrap_or(base[i])
        };
        (at(0), at(1), at(2))
    };
    match zeros.as_slice() {
        [z] => [-1.0, 1.0]
            .into_iter()
            .map(|s| corner(&[(*z, s)]))
            .collect(),
        [u, v] => [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)]
            .into_iter()
            .map(|(su, sv)| corner(&[(*u, su), (*v, sv)]))
            .collect(),
        _ => vec![corner(&[])],
    }
}

fn placement(cell: CubeCell, basis: Basis, center: (f32, f32), radius: f32) -> Option<Placement> {
    let projected: Vec<LayoutPos> = cell_corners(cell)
        .into_iter()
        .map(|c| project(c, basis, center, radius))
        .collect();
    let geom = match cell.kind() {
        CellKind::Face => CellGeom::Face {
            quad: ConvexPoly::new(projected)?,
            label_at: project(
                (f64::from(cell.x), f64::from(cell.y), f64::from(cell.z)),
                basis,
                center,
                radius,
            ),
        },
        CellKind::Edge => CellGeom::Edge {
            a: *projected.first()?,
            b: *projected.get(1)?,
        },
        CellKind::Corner => CellGeom::Corner {
            at: *projected.first()?,
        },
    };
    let pick_rect = pick_rect_for(&geom);
    Some(Placement {
        cell,
        geom,
        pick_rect,
        depth: cell_vec(cell).dot(basis.forward),
    })
}

fn pick_rect_for(geom: &CellGeom) -> LayoutRect {
    match geom {
        CellGeom::Face { label_at, .. } => {
            centered_rect(*label_at, FACE_LABEL_W_PX, FACE_LABEL_H_PX)
        }
        CellGeom::Edge { a, b } => centered_rect(midpoint(*a, *b), EDGE_HIT_PX, EDGE_HIT_PX),
        CellGeom::Corner { at } => centered_rect(*at, CORNER_HIT_PX, CORNER_HIT_PX),
    }
}

fn centered_rect(at: LayoutPos, width: f32, height: f32) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::saturating(at.x.value() - width / 2.0),
            LayoutPx::saturating(at.y.value() - height / 2.0),
        ),
        LayoutSize::new(LayoutPx::new(width), LayoutPx::new(height)),
    )
}

fn midpoint(a: LayoutPos, b: LayoutPos) -> LayoutPos {
    LayoutPos::new(
        LayoutPx::saturating((a.x.value() + b.x.value()) * 0.5),
        LayoutPx::saturating((a.y.value() + b.y.value()) * 0.5),
    )
}

fn face_shade(cell: CubeCell) -> f32 {
    let (nx, ny, nz) = cell.direction().components();
    let norm =
        (LIGHT_DIR.0 * LIGHT_DIR.0 + LIGHT_DIR.1 * LIGHT_DIR.1 + LIGHT_DIR.2 * LIGHT_DIR.2).sqrt();
    let dot = (nx * LIGHT_DIR.0 + ny * LIGHT_DIR.1 + nz * LIGHT_DIR.2) / norm;
    lower_f32(0.55 + 0.45 * dot.clamp(-1.0, 1.0))
}

fn kind_rank(kind: CellKind) -> u8 {
    match kind {
        CellKind::Face => 0,
        CellKind::Edge => 1,
        CellKind::Corner => 2,
    }
}

fn cube_anchor(viewport: LayoutRect, top_offset: f32) -> Option<(LayoutRect, (f32, f32), f32)> {
    let span_w = CUBE_SIDE_PX + 2.0 * CUBE_INSET_PX;
    let span_h = span_w + top_offset;
    if viewport.size.width.value() < span_w || viewport.size.height.value() < span_h {
        return None;
    }
    let right_edge = viewport.min_x().value() + viewport.size.width.value() - CUBE_INSET_PX;
    let top = viewport.min_y().value() + CUBE_INSET_PX + top_offset;
    let rect = LayoutRect::new(
        LayoutPos::new(LayoutPx::new(right_edge - CUBE_SIDE_PX), LayoutPx::new(top)),
        LayoutSize::new(LayoutPx::new(CUBE_SIDE_PX), LayoutPx::new(CUBE_SIDE_PX)),
    );
    let center = (right_edge - CUBE_SIDE_PX / 2.0, top + CUBE_SIDE_PX / 2.0);
    let radius = CUBE_SIDE_PX / 2.0 - CUBE_MARGIN_PX;
    Some((rect, center, radius))
}

fn ordered_visible(basis: Basis, center: (f32, f32), radius: f32) -> Vec<Placement> {
    let mut placements: Vec<Placement> = CubeCell::all()
        .into_iter()
        .filter(|cell| cell_visible(*cell, basis.forward))
        .filter_map(|cell| placement(cell, basis, center, radius))
        .collect();
    placements.sort_by(|a, b| {
        kind_rank(a.cell.kind())
            .cmp(&kind_rank(b.cell.kind()))
            .then(
                b.depth
                    .partial_cmp(&a.depth)
                    .unwrap_or(core::cmp::Ordering::Equal),
            )
            .then(a.cell.index().cmp(&b.cell.index()))
    });
    placements
}

pub struct ViewCubeInputs<'a> {
    pub viewport: LayoutRect,
    pub camera: Camera3,
    pub base: WidgetId,
    pub menu_id: WidgetId,
    pub view: &'a mut ViewUi,
    pub normal_to_available: bool,
    pub confirm_visible: bool,
}

#[must_use]
pub fn render_view_cube(
    ctx: &mut FrameCtx<'_>,
    inputs: ViewCubeInputs<'_>,
    paints: &mut Vec<WidgetPaint>,
    popover_paints: &mut Vec<WidgetPaint>,
) -> ViewCubeOutcome {
    let ViewCubeInputs {
        viewport,
        camera,
        base,
        menu_id,
        view,
        normal_to_available,
        confirm_visible,
    } = inputs;
    if view.cube_hidden {
        return ViewCubeOutcome::default();
    }
    let top_offset = if confirm_visible {
        CONFIRM_RESERVE_PX
    } else {
        0.0
    };
    let Some((anchor, center, radius)) = cube_anchor(viewport, top_offset) else {
        return ViewCubeOutcome::default();
    };
    let Some(basis) = view_basis(camera) else {
        return ViewCubeOutcome::default();
    };
    ctx.a11y.push(
        base,
        anchor,
        AccessNode::new(Role::Group).with_label(strings::VIEW_CUBE),
    );
    let placements = ordered_visible(basis, center, radius);
    let mut pick: Option<ViewPick> = None;
    let mut hovered: Option<(CubeCell, LayoutRect)> = None;
    let mut labels: Vec<(LayoutRect, StringKey, Color)> = Vec::new();
    placements.iter().for_each(|place| {
        let id = base.child_indexed(WidgetKey::new("cell"), place.cell.index());
        let interaction = ctx.interact(
            InteractDeclaration::new(id, place.pick_rect, Sense::INTERACTIVE)
                .a11y(AccessNode::new(Role::Button).with_label(place.cell.a11y_label())),
        );
        let active = interaction.hover();
        draw_cell(ctx.theme(), place, active, paints, &mut labels);
        if interaction.click() && pick.is_none() {
            pick = Some(place.cell.pick());
        }
        if active && hovered.is_none() {
            hovered = Some((place.cell, place.pick_rect));
        }
    });
    let label_role = ctx.theme().typography.label;
    labels.into_iter().for_each(|(rect, key, color)| {
        paints.push(WidgetPaint::Label {
            rect,
            text: key.into(),
            color,
            role: label_role,
        });
    });
    if let Some((cell, rect)) = hovered {
        let id = base.child_indexed(WidgetKey::new("cell"), cell.index());
        let tip = show_tooltip(
            ctx,
            Tooltip::new(
                id,
                rect,
                cell.a11y_label(),
                TooltipPlacement::Left,
                TOOLTIP_SIZE,
            ),
            &mut view.cube_tooltip,
        );
        popover_paints.extend(tip);
    }
    if view.home.is_some() && draw_home_button(ctx, base, anchor, paints) && pick.is_none() {
        pick = Some(ViewPick::Home);
    }
    let menu = run_cube_menu(
        ctx,
        menu_id,
        anchor,
        view,
        normal_to_available,
        popover_paints,
    );
    ViewCubeOutcome { pick, menu }
}

fn draw_home_button(
    ctx: &mut FrameCtx<'_>,
    base: WidgetId,
    anchor: LayoutRect,
    paints: &mut Vec<WidgetPaint>,
) -> bool {
    let rect = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(
                anchor.min_x().value() + (anchor.size.width.value() - HOME_WIDTH_PX) / 2.0,
            ),
            LayoutPx::new(anchor.min_y().value() + anchor.size.height.value() + HOME_GAP_PX),
        ),
        LayoutSize::new(LayoutPx::new(HOME_WIDTH_PX), LayoutPx::new(HOME_HEIGHT_PX)),
    );
    let response = show_button(
        ctx,
        Button::new(
            base.child(WidgetKey::new("home")),
            rect,
            strings::VIEW_HOME,
            ButtonVariant::Secondary,
        ),
    );
    paints.extend(response.paint);
    response.activated
}

fn draw_cell(
    theme: &Theme,
    place: &Placement,
    active: bool,
    paints: &mut Vec<WidgetPaint>,
    labels: &mut Vec<(LayoutRect, StringKey, Color)>,
) {
    match &place.geom {
        CellGeom::Face { quad, label_at } => {
            let fill = face_fill(theme, place.cell, active);
            paints.push(WidgetPaint::ConvexFill {
                poly: quad.clone(),
                fill,
                border: Some(Border {
                    width: StrokeWidth::HAIRLINE,
                    color: theme.colors.neutral.step(Step12::TEXT_MUTED),
                }),
            });
            let bounds = quad.bounds();
            if bounds.size.width.value() >= FACE_LABEL_W_PX
                && bounds.size.height.value() >= FACE_LABEL_H_PX
            {
                labels.push((
                    centered_rect(*label_at, FACE_LABEL_W_PX, FACE_LABEL_H_PX),
                    place.cell.a11y_label(),
                    theme.colors.contrast_text(fill),
                ));
            }
        }
        CellGeom::Edge { a, b } if active => {
            if let Some(path) = PolyPath::open(vec![*a, *b]) {
                paints.push(WidgetPaint::Stroke {
                    path,
                    width: StrokeWidth::px(EDGE_HOVER_WIDTH_PX),
                    color: theme.colors.accent_solid(),
                });
            }
        }
        CellGeom::Corner { at } if active => {
            if let Some(diamond) = corner_diamond(*at) {
                paints.push(WidgetPaint::ConvexFill {
                    poly: diamond,
                    fill: theme.colors.accent_solid(),
                    border: None,
                });
            }
        }
        _ => {}
    }
}

fn face_fill(theme: &Theme, cell: CubeCell, active: bool) -> Color {
    let base = theme.colors.neutral.step(Step12::SUBTLE_BORDER).blend(
        theme.colors.neutral.step(Step12::ELEMENT_BG),
        face_shade(cell),
    );
    if active {
        base.blend(theme.colors.accent_solid(), 0.35)
    } else {
        base
    }
}

fn corner_diamond(at: LayoutPos) -> Option<ConvexPoly> {
    let (x, y) = (at.x.value(), at.y.value());
    let h = CORNER_HOVER_HALF_PX;
    ConvexPoly::new(vec![
        LayoutPos::new(LayoutPx::saturating(x), LayoutPx::saturating(y - h)),
        LayoutPos::new(LayoutPx::saturating(x + h), LayoutPx::saturating(y)),
        LayoutPos::new(LayoutPx::saturating(x), LayoutPx::saturating(y + h)),
        LayoutPos::new(LayoutPx::saturating(x - h), LayoutPx::saturating(y)),
    ])
}

fn run_cube_menu(
    ctx: &mut FrameCtx<'_>,
    menu_id: WidgetId,
    anchor: LayoutRect,
    view: &mut ViewUi,
    normal_to_available: bool,
    popover_paints: &mut Vec<WidgetPaint>,
) -> Option<ViewCubeMenuAction> {
    if let Some(cursor) = secondary_press_in(ctx, anchor) {
        view.cube_menu_open = true;
        view.cube_menu_anchor = cursor;
        view.cube_menu = MenuState::default();
    }
    if !view.cube_menu_open {
        return None;
    }
    let items = cube_menu_items(menu_id, normal_to_available);
    let response = show_context_menu(
        ctx,
        ContextMenu::at_cursor(
            menu_id,
            view.cube_menu_anchor,
            strings::VIEW_CUBE,
            &items,
            &mut view.cube_menu,
        ),
    );
    popover_paints.extend(response.paint);
    if response.close {
        view.cube_menu_open = false;
    }
    response
        .activated
        .and_then(|id| menu_action_for(menu_id, id))
}

fn cube_menu_items(menu_id: WidgetId, normal_to_available: bool) -> Vec<MenuItem> {
    vec![
        MenuItem::Action {
            id: menu_id.child(WidgetKey::new("home")),
            label: strings::VIEW_CUBE_SET_HOME,
            shortcut: None,
            disabled: false,
        },
        MenuItem::Action {
            id: menu_id.child(WidgetKey::new("fit")),
            label: strings::VIEW_CUBE_FIT,
            shortcut: None,
            disabled: false,
        },
        MenuItem::Action {
            id: menu_id.child(WidgetKey::new("normal_to")),
            label: strings::VIEW_CUBE_NORMAL_TO,
            shortcut: None,
            disabled: !normal_to_available,
        },
    ]
}

fn menu_action_for(menu_id: WidgetId, activated: WidgetId) -> Option<ViewCubeMenuAction> {
    [
        ("home", ViewCubeMenuAction::SetAsHome),
        ("fit", ViewCubeMenuAction::FitToWindow),
        ("normal_to", ViewCubeMenuAction::ViewNormalTo),
    ]
    .into_iter()
    .find_map(|(key, action)| (menu_id.child(WidgetKey::new(key)) == activated).then_some(action))
}

fn secondary_press_in(ctx: &FrameCtx<'_>, rect: LayoutRect) -> Option<LayoutPos> {
    let pressed = ctx.input.buttons_pressed.contains(PointerButton::Secondary);
    let cursor = ctx.input.pointer.map(|p| p.position)?;
    (pressed && rect.contains(cursor)).then_some(cursor)
}

const SELECTOR_VIEWS: [StandardView; 8] = [
    StandardView::Front,
    StandardView::Back,
    StandardView::Left,
    StandardView::Right,
    StandardView::Top,
    StandardView::Bottom,
    StandardView::Isometric,
    StandardView::NormalTo,
];

#[must_use]
pub fn render_view_selector(
    ctx: &mut FrameCtx<'_>,
    viewport: LayoutRect,
    menu_id: WidgetId,
    view: &mut ViewUi,
    normal_to_available: bool,
    popover_paints: &mut Vec<WidgetPaint>,
) -> Option<StandardView> {
    if !view.selector_open {
        return None;
    }
    let items: Vec<MenuItem> = SELECTOR_VIEWS
        .into_iter()
        .map(|v| MenuItem::Action {
            id: selector_item_id(menu_id, v),
            label: standard_view_label(v),
            shortcut: None,
            disabled: matches!(v, StandardView::NormalTo) && !normal_to_available,
        })
        .collect();
    let origin = selector_origin(viewport);
    let response = show_menu(
        ctx,
        Menu::new(
            menu_id,
            origin,
            strings::VIEW_SELECTOR,
            &items,
            &mut view.selector_menu,
        ),
    );
    popover_paints.extend(response.paint);
    if response.close {
        view.selector_open = false;
    }
    response.activated.and_then(|id| {
        SELECTOR_VIEWS
            .into_iter()
            .find(|v| selector_item_id(menu_id, *v) == id)
    })
}

fn selector_item_id(menu_id: WidgetId, view: StandardView) -> WidgetId {
    menu_id.child_named(WidgetKey::new("view"), view.label())
}

fn selector_origin(viewport: LayoutRect) -> LayoutPos {
    LayoutPos::new(
        LayoutPx::new(viewport.min_x().value() + CUBE_INSET_PX),
        LayoutPx::new(viewport.min_y().value() + CUBE_INSET_PX),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::path::PathBuf;
    use std::sync::Arc;

    use bone_types::{Point3, Projection};
    use bone_ui::a11y::AccessTreeBuilder;
    use bone_ui::focus::FocusManager;
    use bone_ui::hit_test::{HitFrame, HitState};
    use bone_ui::hotkey::HotkeyTable;
    use bone_ui::input::InputSnapshot;
    use bone_ui::raster::{CanvasPx, CanvasSize, decode_png, encode_png, rasterize};
    use bone_ui::strings::Locale;
    use bone_ui::theme::Theme;
    use uom::si::f64::Length;
    use uom::si::length::millimeter;

    const CANVAS: CanvasSize = CanvasSize::new(CanvasPx::new(256), CanvasPx::new(256));
    const CHANNEL_TOLERANCE: u8 = 2;

    fn ortho_camera(eye: Point3, up: UnitVec3) -> Camera3 {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(2.0)) else {
            unreachable!("half height is positive");
        };
        let Ok(camera) = Camera3::new(eye, Point3::origin(), up, projection) else {
            unreachable!("camera is non-degenerate");
        };
        camera
    }

    fn front_camera() -> Camera3 {
        ortho_camera(Point3::from_mm(0.0, -10.0, 0.0), UnitVec3::z_axis())
    }

    fn isometric_camera() -> Camera3 {
        ortho_camera(Point3::from_mm(10.0, 10.0, 10.0), UnitVec3::z_axis())
    }

    fn midpoint_camera() -> Camera3 {
        let top = ortho_camera(Point3::from_mm(0.0, 0.0, 10.0), UnitVec3::y_axis());
        let tween = CameraTween::eased(
            front_camera(),
            top,
            core::time::Duration::from_millis(200),
            bone_types::CubicEasing::STANDARD,
        );
        let Ok(camera) = tween.sample(core::time::Duration::from_millis(100)) else {
            unreachable!("a front-to-top tween samples at its midpoint");
        };
        camera
    }

    fn visible_faces(camera: Camera3) -> usize {
        let Some(basis) = view_basis(camera) else {
            unreachable!("a non-degenerate camera has a basis");
        };
        CubeCell::all()
            .into_iter()
            .filter(|cell| cell.kind() == CellKind::Face && cell_visible(*cell, basis.forward))
            .count()
    }

    #[test]
    fn cube_has_twenty_six_cells() {
        assert_eq!(CubeCell::all().len(), 26);
    }

    #[test]
    fn cells_partition_into_faces_edges_and_corners() {
        let counts = CubeCell::all()
            .into_iter()
            .fold([0_usize; 3], |mut acc, cell| {
                let slot = match cell.kind() {
                    CellKind::Face => 0,
                    CellKind::Edge => 1,
                    CellKind::Corner => 2,
                };
                acc[slot] += 1;
                acc
            });
        assert_eq!(counts, [6, 12, 8]);
    }

    #[test]
    fn cell_indices_are_distinct() {
        let mut indices: Vec<u64> = CubeCell::all().into_iter().map(CubeCell::index).collect();
        indices.sort_unstable();
        indices.dedup();
        assert_eq!(
            indices.len(),
            26,
            "every cell maps to a distinct child index"
        );
    }

    #[test]
    fn the_six_faces_map_to_standard_views() {
        let faces: Vec<StandardView> = CubeCell::all()
            .into_iter()
            .filter_map(CubeCell::standard_view)
            .collect();
        assert_eq!(faces.len(), 6);
        [
            StandardView::Front,
            StandardView::Back,
            StandardView::Left,
            StandardView::Right,
            StandardView::Top,
            StandardView::Bottom,
        ]
        .into_iter()
        .for_each(|view| assert!(faces.contains(&view), "{view} must be a cube face"));
    }

    #[test]
    fn edges_and_corners_pick_a_direction_not_a_standard_view() {
        CubeCell::all()
            .into_iter()
            .filter(|cell| cell.kind() != CellKind::Face)
            .for_each(|cell| {
                assert_eq!(cell.standard_view(), None);
                assert!(matches!(cell.pick(), ViewPick::Direction(_)));
            });
    }

    #[test]
    fn front_view_shows_the_front_face_and_culls_the_back() {
        let Some(basis) = view_basis(front_camera()) else {
            unreachable!("front camera has a basis");
        };
        let Some(front) = CubeCell::try_new(0, -1, 0) else {
            unreachable!("front face is a valid cell");
        };
        let Some(back) = CubeCell::try_new(0, 1, 0) else {
            unreachable!("back face is a valid cell");
        };
        assert!(
            cell_visible(front, basis.forward),
            "front face faces the camera"
        );
        assert!(!cell_visible(back, basis.forward), "back face is culled");
        assert_eq!(
            visible_faces(front_camera()),
            1,
            "a face-on view shows one face"
        );
    }

    #[test]
    fn isometric_view_shows_three_faces() {
        assert_eq!(
            visible_faces(isometric_camera()),
            3,
            "the isometric view shows exactly three faces",
        );
    }

    fn render_cube(theme: &Theme, camera: Camera3) -> Vec<u8> {
        let strings = crate::strings::make_strings(Locale::EnUs);
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(256.0), LayoutPx::new(256.0)),
        );
        let base = WidgetId::ROOT.child(WidgetKey::new("view_cube"));
        let menu_id = WidgetId::ROOT.child(WidgetKey::new("view_cube.menu"));
        let mut view = ViewUi {
            home: Some(camera),
            ..ViewUi::default()
        };
        let mut paints = Vec::new();
        let mut popover = Vec::new();
        {
            let mut focus = FocusManager::new();
            let table = HotkeyTable::new();
            let mut hits = HitFrame::new();
            let previous = HitState::new();
            let mut input = InputSnapshot::idle(bone_ui::input::FrameInstant::ZERO);
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                Arc::new((*theme).clone()),
                &mut input,
                &mut focus,
                &table,
                &strings,
                &mut hits,
                &previous,
                &mut a11y,
                &mut shaper,
            );
            let _ = render_view_cube(
                &mut ctx,
                ViewCubeInputs {
                    viewport,
                    camera,
                    base,
                    menu_id,
                    view: &mut view,
                    normal_to_available: false,
                    confirm_visible: false,
                },
                &mut paints,
                &mut popover,
            );
        }
        rasterize(theme, &paints, CANVAS, &strings)
    }

    fn background(theme: &Theme) -> [u8; 4] {
        let probe = rasterize(
            theme,
            &[],
            CANVAS,
            &crate::strings::make_strings(Locale::EnUs),
        );
        [probe[0], probe[1], probe[2], probe[3]]
    }

    fn painted_pixels(rgba: &[u8], background: [u8; 4]) -> usize {
        rgba.chunks_exact(4)
            .filter(|px| px[..3] != background[..3])
            .count()
    }

    fn differ(a: &[u8], b: &[u8]) -> bool {
        a.chunks_exact(4).zip(b.chunks_exact(4)).any(|(x, y)| {
            x[..3]
                .iter()
                .zip(&y[..3])
                .any(|(p, q)| p.abs_diff(*q) > CHANNEL_TOLERANCE)
        })
    }

    fn golden_path(name: &str) -> PathBuf {
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("tests");
        path.push("goldens");
        path.push(format!("view_cube_{name}_256.png"));
        path
    }

    fn bless_or_compare(name: &str, rgba: &[u8]) {
        let path = golden_path(name);
        if std::env::var_os("BONE_UPDATE_VIEW_CUBE_GOLDENS").is_some() {
            let Ok(png) = encode_png(rgba, CANVAS) else {
                panic!("encode_png({name}) failed");
            };
            if let Some(parent) = path.parent() {
                let Ok(()) = fs::create_dir_all(parent) else {
                    panic!("create goldens dir failed");
                };
            }
            let Ok(()) = fs::write(&path, &png) else {
                panic!("write golden {name} failed");
            };
            return;
        }
        let Ok(bytes) = fs::read(&path) else {
            panic!(
                "golden {} missing; run with BONE_UPDATE_VIEW_CUBE_GOLDENS=1 to bless",
                path.display(),
            );
        };
        let Ok(pinned) = decode_png(&bytes, CANVAS) else {
            panic!("decode golden {name} failed");
        };
        assert!(
            !differ(rgba, &pinned),
            "view cube {name} render drifted from its golden; \
             re-bless with BONE_UPDATE_VIEW_CUBE_GOLDENS=1 after eyeballing",
        );
    }

    #[test]
    fn view_cube_snapshot_reflects_orientation() {
        [("", Theme::light()), ("_dark", Theme::dark())]
            .into_iter()
            .for_each(|(suffix, theme)| {
                let bg = background(&theme);
                let front = render_cube(&theme, front_camera());
                let iso = render_cube(&theme, isometric_camera());
                let mid = render_cube(&theme, midpoint_camera());
                [("front", &front), ("iso", &iso), ("mid", &mid)]
                    .into_iter()
                    .for_each(|(name, rgba)| {
                        assert!(
                            painted_pixels(rgba, bg) > 0,
                            "the {name}{suffix} cube must paint something",
                        );
                    });
                assert!(
                    differ(&front, &iso),
                    "front and isometric cubes must differ"
                );
                assert!(differ(&front, &mid), "front and midpoint cubes must differ");
                assert!(
                    differ(&iso, &mid),
                    "isometric and midpoint cubes must differ"
                );
                bless_or_compare(&format!("front{suffix}"), &front);
                bless_or_compare(&format!("iso{suffix}"), &iso);
                bless_or_compare(&format!("mid{suffix}"), &mid);
            });
    }
}
