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
}
