(** Pure flexbox layout engine.

    Two-pass algorithm:
    1. Measure (bottom-up): compute intrinsic sizes from text extents
       and children accumulation.
    2. Layout (top-down): allocate space starting from window bounds,
       distributing via flex-grow and handling alignment/gap. *)

open Sx_native_types

(* -- Text measurement -- *)

let measure_text (cr : Cairo.context) (family : [`Sans | `Mono]) (weight : [`Normal | `Bold])
    (slant : [`Normal | `Italic]) (size : float) (text : string) : float * float =
  let font_name = match family with `Sans -> "sans-serif" | `Mono -> "monospace" in
  let cairo_weight = match weight with `Normal -> Cairo.Normal | `Bold -> Cairo.Bold in
  let cairo_slant = match slant with `Normal -> Cairo.Upright | `Italic -> Cairo.Italic in
  Cairo.select_font_face cr ~slant:cairo_slant ~weight:cairo_weight font_name;
  Cairo.set_font_size cr size;
  let fe = Cairo.font_extents cr in
  if String.length text = 0 then (0., fe.ascent +. fe.descent)
  else begin
    (* Word wrap not needed for POC -- measure as single line *)
    let te = Cairo.text_extents cr text in
    (te.Cairo.width +. te.Cairo.x_bearing, fe.ascent +. fe.descent)
  end

(* -- Measure pass (bottom-up) -- *)

(** Set intrinsic [box.w] and [box.h] on each node based on text extents
    and children accumulation. Does NOT set x/y. *)
let rec measure (cr : Cairo.context) (node : node) : unit =
  (* Measure children first *)
  List.iter (measure cr) node.children;

  let pad = node.style.padding in
  let pad_h = pad.left +. pad.right in
  let pad_v = pad.top +. pad.bottom in

  match node.text with
  | Some txt ->
    (* Leaf text node: measure the text *)
    let (tw, th) = measure_text cr node.style.font_family node.style.font_weight
        node.style.font_style node.style.font_size txt in
    node.box.w <- tw +. pad_h;
    node.box.h <- th +. pad_v
  | None ->
    if node.style.display = `None then begin
      node.box.w <- 0.;
      node.box.h <- 0.
    end else begin
      let visible_children = List.filter (fun c -> c.style.display <> `None) node.children in
      let n_children = List.length visible_children in
      let total_gap = if n_children > 1 then node.style.gap *. float_of_int (n_children - 1) else 0. in
      match node.style.flex_direction with
      | `Column ->
        (* Stack vertically: width = max child width, height = sum of child heights + gaps *)
        let max_w = List.fold_left (fun acc c ->
          let cm = c.style.margin in
          Float.max acc (c.box.w +. cm.left +. cm.right)
        ) 0. visible_children in
        let sum_h = List.fold_left (fun acc c ->
          let cm = c.style.margin in
          acc +. c.box.h +. cm.top +. cm.bottom
        ) 0. visible_children in
        node.box.w <- max_w +. pad_h;
        node.box.h <- sum_h +. total_gap +. pad_v
      | `Row ->
        (* Stack horizontally: height = max child height, width = sum of child widths + gaps *)
        let sum_w = List.fold_left (fun acc c ->
          let cm = c.style.margin in
          acc +. c.box.w +. cm.left +. cm.right
        ) 0. visible_children in
        let max_h = List.fold_left (fun acc c ->
          let cm = c.style.margin in
          Float.max acc (c.box.h +. cm.top +. cm.bottom)
        ) 0. visible_children in
        node.box.w <- sum_w +. total_gap +. pad_h;
        node.box.h <- max_h +. pad_v
    end;

    (* Apply explicit width/height constraints *)
    (match node.style.width with
     | `Px w -> node.box.w <- w
     | `Full | `Auto -> ());
    (match node.style.height with
     | `Px h -> node.box.h <- h
     | `Full | `Auto -> ())


(* -- Layout pass (top-down) -- *)

(** Position all nodes within the given bounds [x, y, w, h].
    Distributes space according to flex-grow and handles alignment. *)
let rec layout (node : node) (x : float) (y : float) (avail_w : float) (avail_h : float) : unit =
  let margin = node.style.margin in
  let x = x +. margin.left in
  let y = y +. margin.top in
  let avail_w = avail_w -. margin.left -. margin.right in
  let avail_h = avail_h -. margin.top -. margin.bottom in

  node.box.x <- x;
  node.box.y <- y;

  (* Determine actual width/height.
     Container nodes with Auto width stretch to fill available space
     (like CSS block-level elements), while text nodes keep intrinsic width. *)
  let is_text_node = node.text <> None in
  let w = match node.style.width with
    | `Full -> avail_w
    | `Px pw -> Float.min pw avail_w
    | `Auto ->
      if is_text_node then Float.min node.box.w avail_w
      else avail_w  (* containers expand to fill *)
  in
  let h = match node.style.height with
    | `Full -> avail_h
    | `Px ph -> Float.min ph avail_h
    | `Auto -> node.box.h  (* Use intrinsic height *)
  in

  node.box.w <- w;
  node.box.h <- h;

  if node.style.display = `None then ()
  else begin
    let pad = node.style.padding in
    let inner_x = x +. pad.left in
    let inner_y = y +. pad.top in
    let inner_w = w -. pad.left -. pad.right in
    let inner_h = h -. pad.top -. pad.bottom in

    let visible_children = List.filter (fun c -> c.style.display <> `None) node.children in

    match visible_children with
    | [] -> ()  (* Leaf or empty container *)
    | children ->
      let n_children = List.length children in
      let total_gap = if n_children > 1 then node.style.gap *. float_of_int (n_children - 1) else 0. in

      match node.style.flex_direction with
      | `Column ->
        (* Calculate total intrinsic height and flex-grow sum *)
        let total_intrinsic = List.fold_left (fun acc c ->
          let cm = c.style.margin in
          acc +. c.box.h +. cm.top +. cm.bottom
        ) 0. children in
        let total_grow = List.fold_left (fun acc c -> acc +. c.style.flex_grow) 0. children in
        let remaining = Float.max 0. (inner_h -. total_intrinsic -. total_gap) in

        (* justify-content: space-between *)
        let (start_offset, between_extra) = match node.style.justify_content with
          | `Between when n_children > 1 ->
            (0., remaining /. float_of_int (n_children - 1))
          | `Center -> (remaining /. 2., 0.)
          | `End -> (remaining, 0.)
          | _ -> (0., 0.)
        in

        let cur_y = ref (inner_y +. start_offset) in
        List.iter (fun child ->
          let cm = child.style.margin in
          let child_w = match child.style.width with
            | `Full -> inner_w -. cm.left -. cm.right
            | _ -> Float.min child.box.w (inner_w -. cm.left -. cm.right)
          in
          let extra_h = if total_grow > 0. then remaining *. child.style.flex_grow /. total_grow else 0. in
          let child_h = child.box.h +. extra_h in

          (* align-items: cross-axis alignment *)
          let child_x = match node.style.align_items with
            | `Center -> inner_x +. (inner_w -. child_w -. cm.left -. cm.right) /. 2.
            | `End -> inner_x +. inner_w -. child_w -. cm.right
            | `Stretch ->
              (* Stretch: child takes full width *)
              layout child (inner_x) !cur_y (inner_w) child_h;
              cur_y := !cur_y +. child.box.h +. cm.top +. cm.bottom +. node.style.gap +. between_extra;
              (* skip the normal layout below *)
              inner_x  (* dummy, won't be used *)
            | _ -> inner_x
          in

          if node.style.align_items <> `Stretch then begin
            layout child child_x !cur_y child_w child_h;
            cur_y := !cur_y +. child.box.h +. cm.top +. cm.bottom +. node.style.gap +. between_extra
          end
        ) children

      | `Row ->
        (* Calculate total intrinsic width and flex-grow sum *)
        let total_intrinsic = List.fold_left (fun acc c ->
          let cm = c.style.margin in
          acc +. c.box.w +. cm.left +. cm.right
        ) 0. children in
        let total_grow = List.fold_left (fun acc c -> acc +. c.style.flex_grow) 0. children in
        let remaining = Float.max 0. (inner_w -. total_intrinsic -. total_gap) in

        let (start_offset, between_extra) = match node.style.justify_content with
          | `Between when n_children > 1 ->
            (0., remaining /. float_of_int (n_children - 1))
          | `Center -> (remaining /. 2., 0.)
          | `End -> (remaining, 0.)
          | _ -> (0., 0.)
        in

        let cur_x = ref (inner_x +. start_offset) in
        List.iter (fun child ->
          let cm = child.style.margin in
          let extra_w = if total_grow > 0. then remaining *. child.style.flex_grow /. total_grow else 0. in
          let child_w = child.box.w +. extra_w in
          let child_h = match child.style.height with
            | `Full -> inner_h -. cm.top -. cm.bottom
            | _ -> Float.min child.box.h (inner_h -. cm.top -. cm.bottom)
          in

          (* align-items: cross-axis alignment *)
          let child_y = match node.style.align_items with
            | `Center -> inner_y +. (inner_h -. child_h -. cm.top -. cm.bottom) /. 2.
            | `End -> inner_y +. inner_h -. child_h -. cm.bottom
            | `Stretch ->
              layout child !cur_x inner_y child_w inner_h;
              cur_x := !cur_x +. child.box.w +. cm.left +. cm.right +. node.style.gap +. between_extra;
              inner_y  (* dummy *)
            | _ -> inner_y
          in

          if node.style.align_items <> `Stretch then begin
            layout child !cur_x child_y child_w child_h;
            cur_x := !cur_x +. child.box.w +. cm.left +. cm.right +. node.style.gap +. between_extra
          end
        ) children
  end
