// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
fn[C] compute_grid_layout(
  view : ChicleView[C],
  node_id : NodeId,
  known_dimensions : Size[Double?],
  available_space : Size[AvailableSpace],
  absolute_origin : Point[Double],
  is_layout_root : Bool,
) -> Unit raise ChicleError {
  let tree = view.tree
  ignore(is_layout_root)
  let node = match tree.nodes.get(node_id) {
    Some(n) => n
    None => raise InvalidNodeId(node_id)
  }

  let padding = @util.resolve_rect_width_basis(
    node.style.padding,
    available_space,
  )
  let border = @util.resolve_rect_width_basis(
    node.style.border,
    available_space,
  )
  let scrollbar_w = node.style.scrollbar_width
  let scrollbar_x = match node.style.overflow.x {
    OverflowScroll => scrollbar_w
    _ => 0.0
  }
  let scrollbar_y = match node.style.overflow.y {
    OverflowScroll => scrollbar_w
    _ => 0.0
  }
  let horiz_non_scroll_inset = padding.left +
    padding.right +
    border.left +
    border.right
  let vert_non_scroll_inset = padding.top +
    padding.bottom +
    border.top +
    border.bottom
  // Overflow::Scroll reserves scrollbar space inside the border box.
  // Horizontal scrollbar consumes cross (vertical) space; vertical scrollbar consumes main (horizontal) space.
  let horiz_inset = horiz_non_scroll_inset + scrollbar_y
  let vert_inset = vert_non_scroll_inset + scrollbar_x
  let style_width = @util.resolve_optional_dimension(
    node.style.size.width,
    available_space.width,
  )
  let style_height = @util.resolve_optional_dimension(
    node.style.size.height,
    available_space.height,
  )
  let specified_width = match known_dimensions.width {
    Some(w) => Some(w)
    None => style_width
  }
  let specified_height = match known_dimensions.height {
    Some(h) => Some(h)
    None => style_height
  }

  // If the container size is definite, resolve track sizing against the content box.
  // Otherwise fall back to a simplified "fixed only" resolution.
  let mut content_width = 0.0
  let mut content_height = 0.0
  let mut border_box_width = 0.0
  let mut border_box_height = 0.0
  let definite_width = grid_definite_axis_border_content_size(
    specified_width,
    node.style.min_size.width,
    node.style.max_size.width,
    available_space.width,
    horiz_inset,
    horiz_non_scroll_inset,
  )
  border_box_width = definite_width.0
  content_width = definite_width.1
  let definite_height = grid_definite_axis_border_content_size(
    specified_height,
    node.style.min_size.height,
    node.style.max_size.height,
    available_space.height,
    vert_inset,
    vert_non_scroll_inset,
  )
  border_box_height = definite_height.0
  content_height = definite_height.1

  // Grid gaps: treat `gap.width` as column gap, and `gap.height` as row gap.
  // For `percent` gaps, follow CSS semantics and resolve against the container's inline content size (width).
  // If that basis is not yet known (intrinsic sizing), percent gaps are deferred and handled after sizing.
  let gap_width_percent = match node.style.gap.width {
    DimPercent(p) => Some(p)
    _ => None
  }
  let gap_height_percent = match node.style.gap.height {
    DimPercent(p) => Some(p)
    _ => None
  }
  let gap_inline_basis = match specified_width {
    Some(_) => content_width
    None =>
      match available_space.width {
        AvailDefinite(v) => @util.max_double(v - horiz_inset, 0.0)
        _ => 0.0
      }
  }
  let mut col_gap = match gap_width_percent {
    Some(p) => gap_inline_basis * p
    None => @util.resolve_dimension(node.style.gap.width, available_space.width)
  }
  let mut row_gap = match gap_height_percent {
    Some(p) => gap_inline_basis * p
    None =>
      @util.resolve_dimension(node.style.gap.height, available_space.height)
  }

  // Expand `@style_helpers.repeat(...)` in grid templates.
  let col_expanded = expand_grid_template_axis(
    node.style.grid_template_columns,
    content_width,
    col_gap,
  )
  let row_expanded = expand_grid_template_axis(
    node.style.grid_template_rows,
    content_height,
    row_gap,
  )
  let mut explicit_col_template = col_expanded.0
  let mut explicit_row_template = row_expanded.0
  let mut non_auto_fit_col_count = col_expanded.1
  let has_auto_fit_cols = col_expanded.2
  let mut non_auto_fit_row_count = row_expanded.1
  let has_auto_fit_rows = row_expanded.2

  // If an axis has no explicit tracks, treat it as having a single implicit `auto` track.
  // This handles column-only and row-only templates consistently.
  if explicit_col_template.length() == 0 {
    explicit_col_template = [DimAuto]
    non_auto_fit_col_count = 1
  }
  if explicit_row_template.length() == 0 {
    explicit_row_template = [DimAuto]
    non_auto_fit_row_count = 1
  }
  let explicit_col_count = explicit_col_template.length()
  let explicit_row_count = explicit_row_template.length()

  let track_counts = compute_grid_size_estimate(
    tree,
    tree.children[node_id],
    explicit_col_count,
    explicit_row_count,
  )
  let col_counts = track_counts.0
  let row_counts = track_counts.1
  let negative_cols = col_counts.negative_implicit
  let negative_rows = row_counts.negative_implicit

  // Build an initial implicit grid from templates + auto tracks.
  let auto_cols = axis_auto_track_list(node.style.grid_auto_columns)
  let auto_rows = axis_auto_track_list(node.style.grid_auto_rows)
  let mut col_tracks = initialize_grid_tracks(
    col_counts, explicit_col_template, auto_cols,
  )
  let mut row_tracks = initialize_grid_tracks(
    row_counts, explicit_row_template, auto_rows,
  )

  let placement_count = tree.children[node_id].length()
  let grid_items = make_grid_items(tree, tree.children[node_id])
  let occ = matrix_make(row_tracks.length(), col_tracks.length())

  // Placement algorithm: CSS Grid auto-placement for explicit and implicit tracks.
  let primary_is_col = match node.style.grid_auto_flow {
    Row | RowDense => true
    Column | ColumnDense => false
  }
  let dense = match node.style.grid_auto_flow {
    RowDense | ColumnDense => true
    _ => false
  }

  // Pass 1: both axes definite.
  for i in 0..
          mark_grid_item_placed(
            grid_items,
            occ,
            row_tracks,
            col_tracks,
            auto_rows,
            auto_cols,
            negative_rows,
            negative_cols,
            explicit_row_count,
            explicit_col_count,
            i,
            r,
            c,
            def.1,
            def.3,
          )
        _ => ()
      }
    }
  }

  // Pass 2: definite in secondary axis only.
  for i in 0.. {
            let row_span = def.1
            let col_span = def.3
            let mut c = 0
            while true {
              ensure_rows(
                occ,
                row_tracks,
                auto_rows,
                negative_rows,
                explicit_row_count,
                r + row_span - 1,
              )
              ensure_cols(
                occ,
                col_tracks,
                auto_cols,
                negative_cols,
                explicit_col_count,
                c + col_span - 1,
              )
              if region_is_free(occ, r, c, row_span, col_span) {
                mark_grid_item_placed(
                  grid_items, occ, row_tracks, col_tracks, auto_rows, auto_cols,
                  negative_rows, negative_cols, explicit_row_count, explicit_col_count,
                  i, r, c, row_span, col_span,
                )
                break
              }
              c = c + 1
            }
          }
          _ => ()
        }
      } else {
        // Flow columns: primary axis is rows, secondary axis is columns.
        match (def.0, def.2) {
          (None, Some(c)) => {
            let row_span = def.1
            let col_span = def.3
            let mut r = 0
            while true {
              ensure_rows(
                occ,
                row_tracks,
                auto_rows,
                negative_rows,
                explicit_row_count,
                r + row_span - 1,
              )
              ensure_cols(
                occ,
                col_tracks,
                auto_cols,
                negative_cols,
                explicit_col_count,
                c + col_span - 1,
              )
              if region_is_free(occ, r, c, row_span, col_span) {
                mark_grid_item_placed(
                  grid_items, occ, row_tracks, col_tracks, auto_rows, auto_cols,
                  negative_rows, negative_cols, explicit_row_count, explicit_col_count,
                  i, r, c, row_span, col_span,
                )
                break
              }
              r = r + 1
            }
          }
          _ => ()
        }
      }
    }
  }

  // Pass 3: remaining items (auto-placement in the flow direction).
  // Dense: restart the scan for each item, filling earlier holes.
  // Sparse: resume placement from the last cursor position.
  let mut cursor_sec = 0
  let mut cursor_prim = 0
  for i in 0..
              while true {
                if !dense && sec == cursor_sec && c0 < cursor_prim {
                  sec = sec + 1
                }
                ensure_rows(
                  occ,
                  row_tracks,
                  auto_rows,
                  negative_rows,
                  explicit_row_count,
                  sec + row_span - 1,
                )
                ensure_cols(
                  occ,
                  col_tracks,
                  auto_cols,
                  negative_cols,
                  explicit_col_count,
                  c0 + col_span - 1,
                )
                if region_is_free(occ, sec, c0, row_span, col_span) {
                  mark_grid_item_placed(
                    grid_items, occ, row_tracks, col_tracks, auto_rows, auto_cols,
                    negative_rows, negative_cols, explicit_row_count, explicit_col_count,
                    i, sec, c0, row_span, col_span,
                  )
                  if !dense {
                    cursor_sec = sec
                    cursor_prim = c0 + col_span
                    if cursor_prim >= col_tracks.length() {
                      cursor_sec = cursor_sec + 1
                      cursor_prim = 0
                    }
                  }
                  sec = -1
                  break
                }
                sec = sec + 1
              }
            None => {
              ensure_cols(
                occ,
                col_tracks,
                auto_cols,
                negative_cols,
                explicit_col_count,
                col_span - 1,
              )
              while true {
                ensure_rows(
                  occ,
                  row_tracks,
                  auto_rows,
                  negative_rows,
                  explicit_row_count,
                  sec + row_span - 1,
                )
                let mut prim = if dense || sec != cursor_sec {
                  0
                } else {
                  cursor_prim
                }
                let max_prim = col_tracks.length() - col_span + 1
                let mut found = false
                while prim < max_prim {
                  if region_is_free(occ, sec, prim, row_span, col_span) {
                    mark_grid_item_placed(
                      grid_items, occ, row_tracks, col_tracks, auto_rows, auto_cols,
                      negative_rows, negative_cols, explicit_row_count, explicit_col_count,
                      i, sec, prim, row_span, col_span,
                    )
                    if !dense {
                      cursor_sec = sec
                      cursor_prim = prim + col_span
                      if cursor_prim >= col_tracks.length() {
                        cursor_sec = cursor_sec + 1
                        cursor_prim = 0
                      }
                    }
                    found = true
                    break
                  }
                  prim = prim + 1
                }
                if found {
                  sec = -1
                  break
                }
                sec = sec + 1
              }
            }
          }
          if sec == -1 {
            break
          }
        } else {
          // Auto-flow columns: scan col-by-col
          let fixed_row = def.0
          match fixed_row {
            Some(r0) =>
              while true {
                if !dense && sec == cursor_sec && r0 < cursor_prim {
                  sec = sec + 1
                }
                ensure_cols(
                  occ,
                  col_tracks,
                  auto_cols,
                  negative_cols,
                  explicit_col_count,
                  sec + col_span - 1,
                )
                ensure_rows(
                  occ,
                  row_tracks,
                  auto_rows,
                  negative_rows,
                  explicit_row_count,
                  r0 + row_span - 1,
                )
                if region_is_free(occ, r0, sec, row_span, col_span) {
                  mark_grid_item_placed(
                    grid_items, occ, row_tracks, col_tracks, auto_rows, auto_cols,
                    negative_rows, negative_cols, explicit_row_count, explicit_col_count,
                    i, r0, sec, row_span, col_span,
                  )
                  if !dense {
                    cursor_sec = sec
                    cursor_prim = r0 + row_span
                    if cursor_prim >= row_tracks.length() {
                      cursor_sec = cursor_sec + 1
                      cursor_prim = 0
                    }
                  }
                  sec = -1
                  break
                }
                sec = sec + 1
              }
            None => {
              ensure_rows(
                occ,
                row_tracks,
                auto_rows,
                negative_rows,
                explicit_row_count,
                row_span - 1,
              )
              while true {
                ensure_cols(
                  occ,
                  col_tracks,
                  auto_cols,
                  negative_cols,
                  explicit_col_count,
                  sec + col_span - 1,
                )
                let mut prim = if dense || sec != cursor_sec {
                  0
                } else {
                  cursor_prim
                }
                let max_prim = row_tracks.length() - row_span + 1
                let mut found = false
                while prim < max_prim {
                  if region_is_free(occ, prim, sec, row_span, col_span) {
                    mark_grid_item_placed(
                      grid_items, occ, row_tracks, col_tracks, auto_rows, auto_cols,
                      negative_rows, negative_cols, explicit_row_count, explicit_col_count,
                      i, prim, sec, row_span, col_span,
                    )
                    if !dense {
                      cursor_sec = sec
                      cursor_prim = prim + row_span
                      if cursor_prim >= row_tracks.length() {
                        cursor_sec = cursor_sec + 1
                        cursor_prim = 0
                      }
                    }
                    found = true
                    break
                  }
                  prim = prim + 1
                }
                if found {
                  sec = -1
                  break
                }
                sec = sec + 1
              }
            }
          }
          if sec == -1 {
            break
          }
        }
      }
    }
  }
  let mut col_count = col_tracks.length()
  let mut row_count = row_tracks.length()
  initialize_grid_item_track_crossings(grid_items, col_tracks, row_tracks)

  // Compute min-content and max-content contributions for each track.
  let track_contributions = GridTrackContributions(
    column_count=col_count,
    row_count~,
  )

  let justify_content = match node.style.justify_content {
    Some(v) => v
    None => AlignStart
  }
  let default_align_items_for_contrib = match node.style.align_items {
    Some(v) => v
    None => ItemsStretch
  }
  let default_justify_items_for_contrib = match node.style.justify_items {
    Some(v) => v
    None => ItemsStretch
  }
  let initial_sizing = compute_initial_grid_track_sizing(
    view, grid_items, placement_count, col_tracks, row_tracks, track_contributions,
    col_gap, row_gap, content_width, content_height, available_space, horiz_inset,
    vert_inset, specified_width, specified_height, justify_content, default_align_items_for_contrib,
    default_justify_items_for_contrib,
  )
  let mut col_sizes = initial_sizing.0
  let mut row_sizes = initial_sizing.1
  let col_min_basis = initial_sizing.2
  let col_max_basis = initial_sizing.3
  let col_available_base = initial_sizing.4
  let row_min_basis = initial_sizing.5
  let row_max_basis = initial_sizing.6
  let row_available_base = initial_sizing.7
  let mut used_cols = grid_axis_used_size(col_sizes, col_gap)
  let mut used_rows = grid_axis_used_size(row_sizes, row_gap)

  // Container sizing (border-box). If width/height are not specified, size to tracks.
  if specified_width is None {
    let auto_width = grid_auto_axis_border_content_size(
      available_space.width,
      horiz_inset,
      horiz_non_scroll_inset,
      node.style.min_size.width,
      node.style.max_size.width,
      col_min_basis,
      col_max_basis,
      used_cols,
    )
    border_box_width = auto_width.0
    content_width = auto_width.1
  }
  if specified_height is None {
    let auto_height = grid_auto_axis_border_content_size(
      available_space.height,
      vert_inset,
      vert_non_scroll_inset,
      node.style.min_size.height,
      node.style.max_size.height,
      row_min_basis,
      row_max_basis,
      used_rows,
    )
    border_box_height = auto_height.0
    content_height = auto_height.1
  }

  // Deferred percentage gaps: if the container inline size was unknown during intrinsic sizing,
  // re-resolve percentage gaps against the computed inline content size and recompute track sizes.
  let resolved_gap_rerun = resolve_deferred_grid_percent_gaps(
    content_width, col_gap, row_gap, gap_width_percent, gap_height_percent,
  )
  col_gap = resolved_gap_rerun.0
  row_gap = resolved_gap_rerun.1
  let rerun_track_sizing = resolved_gap_rerun.2
  if rerun_track_sizing {
    let rerun_result = rerun_grid_track_sizing_after_percent_gap_resolution(
      view, grid_items, placement_count, col_tracks, row_tracks, track_contributions,
      col_available_base, row_available_base, col_gap, row_gap, content_height, specified_width,
      specified_height, justify_content, default_align_items_for_contrib, default_justify_items_for_contrib,
    )
    col_sizes = rerun_result.0
    row_sizes = rerun_result.1
    used_cols = rerun_result.2
    used_rows = rerun_result.3
  }
  tree.set_unrounded_layout(node_id, {
    ..Layout::zero(),
    location: absolute_origin,
    size: Size(width=border_box_width, height=border_box_height),
  })

  // Track alignment within the container's content box.
  let align_content = match node.style.align_content {
    Some(v) => v
    None => AlignStart
  }

  // `@style_helpers.repeat(auto-fit, ...)` collapses empty tracks.
  if has_auto_fit_cols || has_auto_fit_rows {
    let mut max_row_used = 0
    let mut max_col_used = 0
    for i in 0.. max_row_used {
        max_row_used = row_end
      }
      if col_end > max_col_used {
        max_col_used = col_end
      }
    }
    if has_auto_fit_cols {
      let needed = max_col_used + 1
      let keep = if needed > non_auto_fit_col_count {
        needed
      } else {
        non_auto_fit_col_count
      }
      if keep > 0 && keep < col_count {
        col_count = keep
        let truncated : Array[Double] = Array::make(col_count, 0.0)
        for i in 0.. 1 {
          used_cols = used_cols + col_gap * (col_count - 1).to_double()
        }
      }
    }
    if has_auto_fit_rows {
      let needed = max_row_used + 1
      let keep = if needed > non_auto_fit_row_count {
        needed
      } else {
        non_auto_fit_row_count
      }
      if keep > 0 && keep < row_count {
        row_count = keep
        let truncated : Array[Double] = Array::make(row_count, 0.0)
        for i in 0.. 1 {
          used_rows = used_rows + row_gap * (row_count - 1).to_double()
        }
      }
    }
  }
  let leftover_x = content_width - used_cols
  let leftover_y = content_height - used_rows
  let mut start_x = 0.0
  let mut col_gap_effective = col_gap
  match justify_content {
    AlignCenter => start_x = leftover_x / 2.0
    AlignFlexEnd | AlignEnd => start_x = leftover_x
    AlignFlexStart | AlignStart => ()
    AlignSpaceBetween =>
      if col_count > 1 && leftover_x > 0.0 {
        col_gap_effective = col_gap_effective +
          leftover_x / (col_count - 1).to_double()
      }
    AlignSpaceAround =>
      if col_count > 0 && leftover_x > 0.0 {
        let extra = leftover_x / col_count.to_double()
        if is_near_int(leftover_x) {
          let gap_extra_floor = extra.floor()
          let gap_extra = if extra > gap_extra_floor {
            gap_extra_floor + 1.0
          } else {
            gap_extra_floor
          }
          col_gap_effective = col_gap_effective + gap_extra
          start_x = (extra / 2.0).floor()
        } else {
          col_gap_effective = col_gap_effective + extra
          start_x = extra / 2.0
        }
      }
    AlignSpaceEvenly =>
      if col_count > 0 && leftover_x > 0.0 {
        let spaces = (col_count + 1).to_double()
        let extra = leftover_x / spaces
        if leftover_x > 0.0 && is_near_int(leftover_x) {
          let start_extra = extra.floor()
          let gap_extra = if extra > start_extra {
            start_extra + 1.0
          } else {
            start_extra
          }
          col_gap_effective = col_gap_effective + gap_extra
          start_x = start_extra
        } else {
          col_gap_effective = col_gap_effective + extra
          start_x = extra
        }
      }
    AlignStretch =>
      if col_count > 0 {
        let extra = leftover_x / col_count.to_double()
        for i in 0.. 0.0 { v } else { 0.0 }
        }
      }
  }
  let mut start_y = 0.0
  let mut row_gap_effective = row_gap
  match align_content {
    AlignCenter => start_y = leftover_y / 2.0
    AlignFlexEnd | AlignEnd => start_y = leftover_y
    AlignFlexStart | AlignStart => ()
    AlignSpaceBetween =>
      if row_count > 1 && leftover_y > 0.0 {
        row_gap_effective = row_gap_effective +
          leftover_y / (row_count - 1).to_double()
      }
    AlignSpaceAround =>
      if row_count > 0 && leftover_y > 0.0 {
        let extra = leftover_y / row_count.to_double()
        if is_near_int(leftover_y) {
          let gap_extra_floor = extra.floor()
          let gap_extra = if extra > gap_extra_floor {
            gap_extra_floor + 1.0
          } else {
            gap_extra_floor
          }
          row_gap_effective = row_gap_effective + gap_extra
          start_y = (extra / 2.0).floor()
        } else {
          row_gap_effective = row_gap_effective + extra
          start_y = extra / 2.0
        }
      }
    AlignSpaceEvenly =>
      if row_count > 0 && leftover_y > 0.0 {
        let spaces = (row_count + 1).to_double()
        let extra = leftover_y / spaces
        if leftover_y > 0.0 && is_near_int(leftover_y) {
          let start_extra = extra.floor()
          let gap_extra = if extra > start_extra {
            start_extra + 1.0
          } else {
            start_extra
          }
          row_gap_effective = row_gap_effective + gap_extra
          start_y = start_extra
        } else {
          row_gap_effective = row_gap_effective + extra
          start_y = extra
        }
      }
    AlignStretch =>
      if row_count > 0 {
        let extra = leftover_y / row_count.to_double()
        for i in 0.. 0.0 { v } else { 0.0 }
        }
      }
  }
  let default_align_items = match node.style.align_items {
    Some(v) => v
    None => ItemsStretch
  }
  let default_justify_items = match node.style.justify_items {
    Some(v) => v
    None => ItemsStretch
  }
  compute_grid_relative_children_final_placement(
    view, node_id, grid_items, placement_count, col_tracks, row_tracks, col_sizes,
    row_sizes, col_gap_effective, row_gap_effective, start_x, start_y, absolute_origin,
    border, padding, content_width, default_align_items, default_justify_items, row_count,
    col_count,
  )
  compute_grid_absolute_placement(
    view,
    tree.children[node_id],
    node.style,
    col_sizes,
    row_sizes,
    col_gap_effective,
    row_gap_effective,
    start_x,
    start_y,
    absolute_origin,
    border,
    padding,
    border_box_width,
    border_box_height,
    explicit_col_count,
    explicit_row_count,
    negative_cols,
    negative_rows,
  )
  let resolved_margin = @util.resolve_rect_width_basis(
    node.style.margin,
    available_space,
  )
  set_effective_margin_states(
    tree,
    node_id,
    margin_collapse_state_from(resolved_margin.top),
    margin_collapse_state_from(resolved_margin.bottom),
  )
}