Documentation
use fission_core::env::{Env, RuntimeState};
use fission_core::lowering::{build_layout_tree, LoweringContext};
use fission_core::ui::{Checkbox, Node, Radio};
use fission_ir::{CoreIR, LayoutOp, NodeId, Op};
use fission_layout::{LayoutEngine, LayoutSize, TextMeasurer};
use std::collections::HashMap;
use std::sync::Arc;

struct SimpleMeasurer;

impl TextMeasurer for SimpleMeasurer {
    fn measure(&self, text: &str, _font_size: f32, available_width: Option<f32>) -> (f32, f32) {
        let char_width = 8.0;
        let line_height = 16.0;
        let width = text.len() as f32 * char_width;
        if let Some(max_w) = available_width {
            if max_w > 0.0 && width > max_w {
                let lines = (width / max_w).ceil();
                return (max_w, lines * line_height);
            }
        }
        (width, line_height)
    }

    fn measure_rich_text(&self, runs: &[fission_ir::op::TextRun], available_width: Option<f32>) -> (f32, f32) {
        let text: String = runs.iter().map(|r| r.text.clone()).collect();
        self.measure(&text, 16.0, available_width)
    }
}

fn approx_eq(a: f32, b: f32) -> bool {
    (a - b).abs() < 0.5
}

fn parent_map(ir: &CoreIR) -> HashMap<NodeId, NodeId> {
    let mut map = HashMap::new();
    for (id, node) in &ir.nodes {
        for child in &node.children {
            map.insert(*child, *id);
        }
    }
    map
}

fn find_boxes_by_size(ir: &CoreIR, width: f32, height: f32) -> Vec<NodeId> {
    let mut out = Vec::new();
    for (id, node) in &ir.nodes {
        if let Op::Layout(LayoutOp::Box { width: Some(w), height: Some(h), .. }) = &node.op {
            if approx_eq(*w, width) && approx_eq(*h, height) {
                out.push(*id);
            }
        }
    }
    out
}

fn layout_from_node(node: Node) -> (CoreIR, fission_layout::LayoutSnapshot) {
    let env = Env::default();
    let runtime_state = RuntimeState::default();
    let measurer: Arc<dyn TextMeasurer> = Arc::new(SimpleMeasurer);
    let measurer_ref = measurer.clone();

    let mut cx = LoweringContext::new(&env, &runtime_state, Some(&measurer_ref), None);
    let root_id = node.lower(&mut cx);
    cx.ir.root = Some(root_id);
    let input_nodes = build_layout_tree(&cx.ir, &env);

    let mut engine = LayoutEngine::new().with_measurer(measurer);
    engine.rebuild(&input_nodes).unwrap();
    let snapshot = engine
        .compute_layout(&input_nodes, root_id, LayoutSize::new(200.0, 200.0), &|_| 0.0)
        .unwrap();
    (cx.ir, snapshot)
}

fn rect_center(rect: fission_layout::LayoutRect) -> (f32, f32) {
    (rect.x() + rect.width() / 2.0, rect.y() + rect.height() / 2.0)
}

#[test]
fn checkbox_checkmark_centered() {
    let checkbox = Checkbox { checked: true, ..Default::default() };
    let (ir, snapshot) = layout_from_node(checkbox.into_node());
    let parents = parent_map(&ir);

    let check_id = find_boxes_by_size(&ir, 10.0, 10.0)
        .into_iter()
        .next()
        .expect("checkbox check box");
    let mut current = Some(check_id);
    let mut square_id = None;
    while let Some(id) = current {
        if let Op::Layout(LayoutOp::Box { width: Some(w), height: Some(h), .. }) =
            &ir.nodes.get(&id).unwrap().op
        {
            if approx_eq(*w, 18.0) && approx_eq(*h, 18.0) {
                square_id = Some(id);
                break;
            }
        }
        current = parents.get(&id).copied();
    }
    let square_id = square_id.expect("checkbox square box");

    let square_rect = snapshot.get_node_geometry(square_id).unwrap().rect;
    let check_rect = snapshot.get_node_geometry(check_id).unwrap().rect;

    let (sx, sy) = rect_center(square_rect);
    let (cx, cy) = rect_center(check_rect);

    assert!(approx_eq(sx, cx) && approx_eq(sy, cy), "checkbox checkmark should be centered");
}

#[test]
fn radio_dot_centered() {
    let radio = Radio { checked: true, ..Default::default() };
    let (ir, snapshot) = layout_from_node(radio.into_node());
    let parents = parent_map(&ir);

    let dot_id = find_boxes_by_size(&ir, 10.0, 10.0)
        .into_iter()
        .next()
        .expect("radio dot box");

    let mut current = Some(dot_id);
    let mut container_id = None;
    while let Some(id) = current {
        if let Op::Layout(LayoutOp::Box { width: Some(w), height: Some(h), .. }) =
            &ir.nodes.get(&id).unwrap().op
        {
            if approx_eq(*w, 18.0) && approx_eq(*h, 18.0) {
                container_id = Some(id);
                break;
            }
        }
        current = parents.get(&id).copied();
    }

    let container_id = container_id.expect("radio dot container");
    let container_rect = snapshot.get_node_geometry(container_id).unwrap().rect;
    let dot_rect = snapshot.get_node_geometry(dot_id).unwrap().rect;

    let (sx, sy) = rect_center(container_rect);
    let (cx, cy) = rect_center(dot_rect);

    assert!(approx_eq(sx, cx) && approx_eq(sy, cy), "radio dot should be centered");
}