defmodule Raxol.UI.Layout.Engine do
  @moduledoc """
  Core layout engine that translates the logical view structure into absolute positions.

  This module is responsible for:
  * Calculating element positions based on available space
  * Resolving layout constraints
  * Managing the layout pipeline
  """

  require Raxol.Core.Runtime.Log

  alias Raxol.UI.Layout.{
    Containers,
    CSSGrid,
    Elements,
    Flexbox,
    Grid,
    Inputs,
    Panels,
    PreparedElement,
    Responsive,
    SplitPane,
    Table
  }

  @known_style_attrs [
    :bold,
    :italic,
    :underline,
    :strikethrough,
    :reverse,
    :dim
  ]

  @typedoc """
  Terminal viewport dimensions in cells.
  """
  @type dimensions :: %{
          required(:width) => non_neg_integer(),
          required(:height) => non_neg_integer()
        }

  @typedoc """
  Available space for a layout pass. Origin (`x`, `y`) and extent
  (`width`, `height`) all in cell units relative to the viewport.
  """
  @type space :: %{
          required(:x) => non_neg_integer(),
          required(:y) => non_neg_integer(),
          required(:width) => non_neg_integer(),
          required(:height) => non_neg_integer()
        }

  @typedoc """
  Input element. Every layout element has a `:type` atom; the rest of the
  shape is type-specific. Containers carry `:children`; leaves carry
  primitive fields like `:text`, `:content`, or `:width`/`:height`.

  Container types: `:view`, `:panel`, `:row`, `:column`, `:grid`, `:flex`,
  `:css_grid`, `:responsive`, `:responsive_grid`, `:box`, `:split_pane`,
  `:absolute_layer`.

  Leaf types: `:text`, `:label`, `:button`, `:text_input`, `:checkbox`,
  `:image`, `:divider`, `:spacer`, `:table`.
  """
  @type element :: %{required(:type) => atom(), optional(atom()) => any()}

  @typedoc """
  Output of `apply_layout/3`: a flat list of positioned elements where each
  carries absolute coordinates and dimensions. The renderer consumes these
  in order to produce cell tuples.
  """
  @type positioned_element :: %{
          required(:type) => atom(),
          required(:x) => non_neg_integer(),
          required(:y) => non_neg_integer(),
          optional(:width) => non_neg_integer(),
          optional(:height) => non_neg_integer(),
          optional(atom()) => any()
        }

  @typedoc """
  Intrinsic measurement -- the minimum space an element wants when laid out.
  """
  @type measurement :: %{
          required(:width) => non_neg_integer(),
          required(:height) => non_neg_integer()
        }

  @doc """
  Applies layout to a view, calculating absolute positions for all elements.

  ## Parameters

  * `view` - The view to calculate layout for
  * `dimensions` - Terminal dimensions `%{width: w, height: h}`
  * `prepared_tree` - Optional `PreparedElement` tree with cached text
    measurements from the prepare phase (Pretext two-phase architecture).
    When provided, `measure_element` will use cached `measured_width` /
    `measured_height` instead of re-calling `TextMeasure.display_width`.

  ## Returns

  A list of positioned elements with absolute coordinates.
  """
  @spec apply_layout(element(), dimensions(), PreparedElement.t() | nil) ::
          [positioned_element()]
  def apply_layout(view, dimensions, prepared_tree \\ nil) do
    # Build a flat lookup from element identity to cached measurements.
    # Uses :erlang.phash2 on the element map as key so measure_element
    # can look up without threading the cache through every call site.
    if prepared_tree do
      cache = build_measurement_cache(prepared_tree)
      Process.put(:raxol_measurement_cache, cache)
    end

    # Start with the full screen as available space
    available_space = %{
      x: 0,
      y: 0,
      width: dimensions.width,
      height: dimensions.height
    }

    # Process the view tree
    result =
      process_element(view, available_space, [])
      |> List.flatten()

    # Clean up process dictionary
    Process.delete(:raxol_measurement_cache)

    result
  end

  # Build a flat map from element content hash to {measured_width, measured_height}.
  defp build_measurement_cache(nil), do: %{}

  defp build_measurement_cache(%PreparedElement{} = pe) do
    entry =
      if pe.content_hash && (pe.measured_width > 0 || pe.measured_height > 0) do
        %{{pe.type, pe.content_hash} => {pe.measured_width, pe.measured_height}}
      else
        %{}
      end

    children_entries =
      case pe.children do
        children when is_list(children) ->
          Enum.reduce(children, %{}, fn child, acc ->
            Map.merge(acc, build_measurement_cache(child))
          end)

        _ ->
          %{}
      end

    Map.merge(entry, children_entries)
  end

  # --- Element Processing Logic ---
  # Moved all process_element/3 definitions here for grouping

  @doc """
  Lays out a single element against `space`, prepending positioned elements
  to `acc`. Dispatches by `:type`; falls through to a logged warning for
  unknown types.

  Used internally by `apply_layout/3` and by container layout modules
  (`Containers`, `Flexbox`, `Grid`, etc.) when recursing into children.
  """
  @spec process_element(element() | any(), space(), [positioned_element()]) ::
          [positioned_element()]
  # Process a view element
  def process_element(%{type: :view, children: children}, space, acc)
      when is_list(children) do
    # Process children with the available space
    process_children(children, space, acc)
  end

  def process_element(%{type: :view, children: children}, space, acc) do
    # Handle case where children is not a list
    process_element(children, space, acc)
  end

  def process_element(%{type: :panel} = panel, space, acc) do
    # Delegate to panel-specific layout processing
    Panels.process(panel, space, acc)
  end

  def process_element(%{type: :row} = row, space, acc) do
    # Delegate to row layout processing
    Containers.process_row(row, space, acc)
  end

  def process_element(%{type: :column} = column, space, acc) do
    # Delegate to column layout processing
    Containers.process_column(column, space, acc)
  end

  def process_element(%{type: :grid} = grid, space, acc) do
    # Delegate to grid layout processing
    Grid.process(grid, space, acc)
  end

  def process_element(%{type: :flex} = flex, space, acc) do
    # Build attrs from top-level keys so parse_flex_properties works
    # (View DSL puts direction/gap/etc. at top level, not in :attrs)
    enriched = enrich_flex_attrs(flex)
    Flexbox.process_flex(enriched, space, acc)
  end

  def process_element(%{type: :css_grid} = css_grid, space, acc) do
    # Delegate to CSS Grid layout processing
    CSSGrid.process_css_grid(css_grid, space, acc)
  end

  def process_element(%{type: :responsive} = responsive, space, acc) do
    # Delegate to responsive layout processing
    Responsive.process_responsive(responsive, space, acc)
  end

  def process_element(%{type: :responsive_grid} = responsive_grid, space, acc) do
    # Delegate to responsive grid layout processing
    Responsive.process_responsive_grid(responsive_grid, space, acc)
  end

  # Process basic text/label (old format with :attrs)
  def process_element(%{type: type, attrs: attrs} = _element, space, acc)
      when type in [:label, :text] do
    # Convert keyword list to map if needed
    attrs_map = convert_attrs_to_map(attrs)

    # Create a text element at the given position
    style_map = style_to_map(Map.get(attrs_map, :style, %{}))

    text_element = %{
      type: :text,
      x: space.x,
      y: space.y,
      text: Map.get(attrs_map, :content, Map.get(attrs_map, :text, "")),
      fg: Map.get(attrs_map, :fg),
      bg: Map.get(attrs_map, :bg),
      style: style_map,
      # Pass original attributes through, let Renderer handle styling
      attrs: Map.put(attrs_map, :original_type, type)
    }

    [text_element | acc]
  end

  # Process text elements in new widget format (flat map with :content)
  def process_element(%{type: :text, content: content} = element, space, acc)
      when is_binary(content) do
    style_map = style_to_map(Map.get(element, :style, %{}))

    text_element = %{
      type: :text,
      x: space.x,
      y: space.y,
      text: content,
      fg: Map.get(element, :fg),
      bg: Map.get(element, :bg),
      style: style_map,
      attrs: %{
        style: style_map,
        id: Map.get(element, :id),
        original_type: :text
      }
    }

    [text_element | acc]
  end

  def process_element(%{type: :button, attrs: attrs} = _element, space, acc) do
    text = Map.get(attrs, :label, "Button")
    component_attrs = Map.put(attrs, :component_type, :button)
    build_button_elements(text, component_attrs, space) ++ acc
  end

  def process_element(%{type: :text_input, attrs: attrs} = _element, space, acc) do
    # Create a text input element composed of box and text
    value = Map.get(attrs, :value, "")
    placeholder = Map.get(attrs, :placeholder, "")
    display_text = get_display_text(value, placeholder)

    # Add component_type, potentially pass placeholder/value info if Renderer needs it
    component_attrs = Map.put(attrs, :component_type, :text_input)

    style_map = style_to_map(Map.get(attrs, :style, %{}))

    text_input_elements = [
      # Input box
      %{
        type: :box,
        x: space.x,
        y: space.y,
        width:
          min(Raxol.UI.TextMeasure.display_width(display_text) + 4, space.width),
        height: 3,
        style: style_map,
        attrs: component_attrs
      },
      # Input text (or placeholder)
      %{
        type: :text,
        x: space.x + 2,
        y: space.y + 1,
        text: display_text,
        fg: Map.get(attrs, :fg),
        bg: Map.get(attrs, :bg),
        style: style_map,
        # Pass component_attrs; Renderer can check :value == "" and use placeholder style
        attrs:
          Map.merge(component_attrs, %{
            placeholder: value == "",
            style: style_map
          })
      }
    ]

    text_input_elements ++ acc
  end

  def process_element(%{type: :checkbox, attrs: attrs} = _element, space, acc) do
    # Create a checkbox element (simple text for now)
    checked = Map.get(attrs, :checked, false)
    label = Map.get(attrs, :label, "")
    component_attrs = Map.put(attrs, :component_type, :checkbox)

    checkbox_text = get_checkbox_text(checked)

    style_map = style_to_map(Map.get(attrs, :style, %{}))

    checkbox_elements = [
      # Checkbox text (box + label)
      %{
        type: :text,
        x: space.x,
        y: space.y,
        text: "#{checkbox_text} #{label}",
        fg: Map.get(attrs, :fg),
        bg: Map.get(attrs, :bg),
        style: style_map,
        # Pass attributes for theme styling
        attrs: Map.put(component_attrs, :style, style_map)
      }
    ]

    checkbox_elements ++ acc
  end

  # Process box elements in new View DSL format (no :attrs key)
  def process_element(%{type: :box, children: %{} = child} = box, space, acc) do
    process_element(%{box | children: [child]}, space, acc)
  end

  def process_element(%{type: :box, children: children} = box, space, acc)
      when is_list(children) do
    style = resolve_style(box)
    padding = Map.get(box, :padding, 0)
    border = Map.get(box, :border) || Map.get(style, :border, :none)

    inner_space = box_inner_space(space, border, padding)

    box_style = Map.get(box, :style, %{})
    animation_hints = Map.get(box, :animation_hints, [])

    box_element = %{
      type: :box,
      x: space.x,
      y: space.y,
      width: space.width,
      height: space.height,
      style: box_style,
      animation_hints: animation_hints,
      attrs: %{
        border: border,
        padding: padding,
        style: box_style
      }
    }

    children_acc = process_children(children, inner_space, [])
    [box_element | children_acc] ++ acc
  end

  # Process button elements in new View DSL format (no :attrs key)
  def process_element(%{type: :button, text: text} = button, space, acc)
      when is_binary(text) do
    style_map = style_to_map(Map.get(button, :style, %{}))

    component_attrs = %{
      component_type: :button,
      label: text,
      on_click: Map.get(button, :on_click),
      fg: Map.get(button, :fg),
      bg: Map.get(button, :bg),
      style: style_map
    }

    build_button_elements(text, component_attrs, space) ++ acc
  end

  def process_element(%{type: :split_pane} = split, space, acc) do
    SplitPane.process(split, space, acc)
  end

  # Absolute layer: lay out the flow child against the parent's full available
  # space (overlays consume nothing), then resolve each overlay against the
  # same space at declared coordinates. Overlays render after flow content so
  # they sit on top in the renderer's last-write-wins cell merge.
  #
  # Element shape:
  #   %{
  #     type: :absolute_layer,
  #     flow_child: element | nil,
  #     overlays: [
  #       %{x: 0, y: 0, element: top_border},
  #       %{x: 0, y: :bottom, element: bottom_border},
  #       %{x: :right, y: :center, element: side_panel}
  #     ]
  #   }
  #
  # Overlay coordinates are relative to the layer's space. Symbolic
  # coordinates (`:left`, `:right`, `:top`, `:bottom`, `:center`) and negative
  # integers (offset from far edge) are resolved against the current space.
  def process_element(%{type: :absolute_layer} = layer, space, acc) do
    flow_child = Map.get(layer, :flow_child)
    overlays = Map.get(layer, :overlays, [])

    flow_acc =
      case flow_child do
        nil -> acc
        child -> process_element(child, space, acc)
      end

    Enum.reduce(overlays, flow_acc, fn overlay, current_acc ->
      process_overlay(overlay, space, current_acc)
    end)
  end

  def process_element(%{type: :table} = table_element, space, acc) do
    # Delegate table measurement and positioning to the dedicated module
    Table.measure_and_position(table_element, space, acc)
  end

  def process_element(%{type: :spacer} = spacer, space, acc) do
    size = Map.get(spacer, :size, 1)
    direction = Map.get(spacer, :direction, :vertical)

    {w, h} =
      case direction do
        :horizontal -> {size, space.height}
        _ -> {space.width, size}
      end

    [
      %{
        type: :spacer,
        x: space.x,
        y: space.y,
        width: w,
        height: h,
        style: Map.get(spacer, :style, %{})
      }
      | acc
    ]
  end

  def process_element(%{type: :image} = image_el, space, acc) do
    width = min(Map.get(image_el, :width, 20), space.width)
    height = min(Map.get(image_el, :height, 10), space.height)

    [
      %{
        type: :image,
        x: space.x,
        y: space.y,
        width: width,
        height: height,
        src: Map.get(image_el, :src),
        protocol: Map.get(image_el, :protocol),
        preserve_aspect: Map.get(image_el, :preserve_aspect, true),
        style: Map.get(image_el, :style, %{})
      }
      | acc
    ]
  end

  def process_element(%{type: :divider} = divider, space, acc) do
    char = Map.get(divider, :char, "-")

    [
      %{
        type: :divider,
        x: space.x,
        y: space.y,
        width: space.width,
        height: 1,
        char: char,
        style: Map.get(divider, :style, %{})
      }
      | acc
    ]
  end

  # Catch-all for unknown element types
  def process_element(%{type: type} = element, _space, acc) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "LayoutEngine: Unknown or unhandled element type: #{inspect(type)}. Element: #{inspect(element)}",
      %{}
    )

    acc
  end

  def process_element(other, _space, acc) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "LayoutEngine: Received non-element data: #{inspect(other)}",
      %{}
    )

    acc
  end

  defp build_button_elements(text, component_attrs, space) do
    [
      %{
        type: :box,
        x: space.x,
        y: space.y,
        width: min(Raxol.UI.TextMeasure.display_width(text) + 4, space.width),
        height: 3,
        attrs: component_attrs
      },
      %{
        type: :text,
        x: space.x + 2,
        y: space.y + 1,
        text: text,
        fg: Map.get(component_attrs, :fg),
        bg: Map.get(component_attrs, :bg),
        style: Map.get(component_attrs, :style, %{}),
        attrs: component_attrs
      }
    ]
  end

  # Process children of a container element (Helper).
  # Each child is dispatched through process_element/3 which already handles
  # all element types (containers, primitives, catch-all).
  defp process_children(children, space, acc) when is_list(children) do
    Enum.reduce(children, acc, fn child, current_acc ->
      process_element(child, space, current_acc)
    end)
  end

  # Resolve an overlay descriptor into a positioned space inside `parent_space`
  # and dispatch the overlay's element through process_element/3. Coordinates
  # outside the parent space are clipped (the overlay is silently dropped).
  defp process_overlay(%{element: nil}, _parent_space, acc), do: acc

  defp process_overlay(%{element: element} = overlay, parent_space, acc) do
    raw_x = Map.get(overlay, :x, 0)
    raw_y = Map.get(overlay, :y, 0)

    x = resolve_axis(raw_x, parent_space.x, parent_space.width)
    y = resolve_axis(raw_y, parent_space.y, parent_space.height)

    if in_bounds?(x, y, parent_space) do
      overlay_space = %{
        x: x,
        y: y,
        width: max(parent_space.x + parent_space.width - x, 0),
        height: max(parent_space.y + parent_space.height - y, 0)
      }

      process_element(element, overlay_space, acc)
    else
      acc
    end
  end

  defp process_overlay(_, _, acc), do: acc

  # Resolve a coordinate (integer, negative offset, or symbolic atom) against
  # the parent space's origin and length on a single axis.
  defp resolve_axis(:left, origin, _length), do: origin
  defp resolve_axis(:top, origin, _length), do: origin
  defp resolve_axis(:right, origin, length), do: origin + max(length - 1, 0)
  defp resolve_axis(:bottom, origin, length), do: origin + max(length - 1, 0)
  defp resolve_axis(:center, origin, length), do: origin + div(length, 2)

  defp resolve_axis(value, origin, length) when is_integer(value) and value < 0,
    do: origin + max(length + value, 0)

  defp resolve_axis(value, origin, _length) when is_integer(value),
    do: origin + value

  defp resolve_axis(_other, origin, _length), do: origin

  defp in_bounds?(x, y, %{x: px, y: py, width: w, height: h}) do
    x >= px and x < px + w and y >= py and y < py + h
  end

  # --- End Element Processing ---

  # --- Element Measurement Logic ---
  # Function Header for multi-clause function with defaults
  @doc """
  Calculates the intrinsic dimensions (width, height) of an element.

  This function determines the natural size of an element before layout constraints
  are applied. For containers, it might recursively measure children.

  ## Parameters

  * `element` - The element map to measure.
  * `available_space` - Map providing context (e.g., max width).
  - Defaults to an empty map.

  ## Returns

  A map representing the dimensions: `%{width: integer(), height: integer()}`.
  """
  @spec measure_element(element() | any(), map()) :: measurement()
  def measure_element(element, available_space \\ %{})

  # Handles valid elements (maps with :type and :attrs)
  def measure_element(%{type: type, attrs: attrs} = element, available_space)
      when is_atom(type) do
    # Convert keyword list to map if needed
    attrs_map = convert_attrs_to_map(attrs)
    measure_element_by_type(type, element, attrs_map, available_space)
  end

  # Handles new widget format (flat maps with :content/:children, no :attrs)
  def measure_element(%{type: :text, content: content}, _available_space)
      when is_binary(content) do
    case lookup_prepared(:text, content) do
      {w, h} ->
        %{width: w, height: h}

      nil ->
        lines = String.split(content, "\n")

        width =
          lines
          |> Enum.map(&Raxol.UI.TextMeasure.display_width/1)
          |> Enum.max(fn -> 0 end)

        %{width: width, height: length(lines)}
    end
  end

  # Button with top-level :text key (new View DSL format)
  def measure_element(%{type: :button, text: text} = _element, available_space) do
    label = text || "Button"
    Inputs.measure(:button, %{label: label}, available_space)
  end

  # Checkbox with top-level :label key (new View DSL format)
  def measure_element(
        %{type: :checkbox, label: label} = _element,
        _available_space
      ) do
    Elements.measure(:checkbox, %{label: label || ""})
  end

  # TextInput with top-level :value/:placeholder keys (new View DSL format)
  def measure_element(%{type: :text_input} = element, available_space) do
    attrs_map = %{
      value: Map.get(element, :value, ""),
      placeholder: Map.get(element, :placeholder, "")
    }

    Inputs.measure(:text_input, attrs_map, available_space)
  end

  # Box with single map child (View DSL produces map, not list, for single child)
  def measure_element(
        %{type: :box, children: %{} = child} = element,
        available_space
      ) do
    measure_element(%{element | children: [child]}, available_space)
  end

  # Box with top-level properties (new View DSL format from Box.new/1)
  def measure_element(
        %{type: :box, children: children} = element,
        available_space
      )
      when is_list(children) do
    style = resolve_style(element)
    overhead = box_overhead(element, style)
    {width, height} = resolve_box_dimensions(element, style, available_space)
    inner_space = shrink_space_by(available_space, overhead)

    resolve_box_size(width, height, children, inner_space, overhead)
  end

  # Flex with top-level properties (new View DSL format from Flex.row/1 etc.)
  def measure_element(
        %{type: :flex, children: children} = element,
        available_space
      )
      when is_list(children) do
    Flexbox.measure_flex(enrich_flex_attrs(element), available_space)
  end

  def measure_element(
        %{type: container_type, children: children} = element,
        available_space
      )
      when container_type in [:row, :column, :view] and is_list(children) do
    measure_element_by_type(container_type, element, %{}, available_space)
  end

  def measure_element(%{type: :spacer} = spacer, available_space) do
    size = Map.get(spacer, :size, 1)

    case Map.get(spacer, :direction, :vertical) do
      :horizontal -> %{width: size, height: available_space.height}
      _ -> %{width: available_space.width, height: size}
    end
  end

  def measure_element(%{type: :image} = image_el, _available_space) do
    %{
      width: Map.get(image_el, :width, 20),
      height: Map.get(image_el, :height, 10)
    }
  end

  def measure_element(%{type: :divider}, available_space) do
    %{width: available_space.width, height: 1}
  end

  # Catch-all for unknown element types
  def measure_element(other, _available_space) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "LayoutEngine: Cannot measure unknown element type: #{inspect(other)}",
      %{}
    )

    %{width: 0, height: 0}
  end

  defp measure_element_by_type(type, element, attrs_map, available_space) do
    case type do
      type when type in [:text, :label, :box, :checkbox] ->
        Elements.measure(type, attrs_map)

      type when type in [:button, :text_input] ->
        Inputs.measure(type, attrs_map, available_space)

      type
      when type in [
             :row,
             :column,
             :panel,
             :grid,
             :view,
             :flex,
             :css_grid,
             :responsive,
             :responsive_grid,
             :split_pane
           ] ->
        measure_container_element(type, element, available_space)

      :table ->
        Table.measure(attrs_map, available_space)

      _ ->
        handle_unknown_element(type)
    end
  end

  defp measure_container_element(:row, element, available_space),
    do: Containers.measure_row(element, available_space)

  defp measure_container_element(:column, element, available_space),
    do: Containers.measure_column(element, available_space)

  defp measure_container_element(:panel, element, available_space),
    do: Panels.measure_panel(element, available_space)

  defp measure_container_element(:grid, element, available_space),
    do: Grid.measure_grid(element, available_space)

  defp measure_container_element(:view, element, available_space),
    do: measure_view(element, available_space)

  defp measure_container_element(:flex, element, available_space),
    do: Flexbox.measure_flex(element, available_space)

  defp measure_container_element(:css_grid, element, available_space),
    do: CSSGrid.measure_css_grid(element, available_space)

  defp measure_container_element(:responsive, element, available_space),
    do: Responsive.measure_responsive(element, available_space)

  defp measure_container_element(:responsive_grid, element, available_space),
    do: Responsive.measure_responsive(element, available_space)

  defp measure_container_element(:split_pane, element, available_space),
    do: SplitPane.measure_split_pane(element, available_space)

  defp measure_view(element, available_space) do
    %{type: :column, children: Map.get(element, :children, [])}
    |> __MODULE__.measure_element(available_space)
  end

  defp handle_unknown_element(type) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "LayoutEngine: Cannot measure element type: #{inspect(type)}",
      %{}
    )

    %{width: 0, height: 0}
  end

  # --- Helper Functions ---

  @doc """
  Looks up cached measurements for an element from the prepare phase.

  Returns `{width, height}` if cached, or `nil` if not found.
  """
  def lookup_prepared(type, content) do
    case Process.get(:raxol_measurement_cache) do
      cache when is_map(cache) ->
        hash = :erlang.phash2(content)
        Map.get(cache, {type, hash})

      _ ->
        nil
    end
  end

  defp convert_attrs_to_map(attrs) when is_list(attrs), do: Map.new(attrs)
  defp convert_attrs_to_map(attrs), do: attrs

  defp style_to_map(styles) when is_list(styles) do
    Enum.reduce(styles, %{}, fn
      attr, acc when attr in @known_style_attrs -> Map.put(acc, attr, true)
      _other, acc -> acc
    end)
  end

  defp style_to_map(styles) when is_map(styles), do: styles
  defp style_to_map(_), do: %{}

  # Resolve style map from an element, defaulting to empty map.
  defp resolve_style(element) do
    case Map.get(element, :style) do
      s when is_map(s) -> s
      _ -> %{}
    end
  end

  # Calculate total border + padding overhead for a box (both sides).
  defp box_overhead(element, style) do
    border = Map.get(element, :border) || Map.get(style, :border, :none)
    padding = Map.get(element, :padding, 0)
    border_offset = if border in [:none, false], do: 0, else: 1
    pad = if is_integer(padding), do: padding, else: 0
    2 * (border_offset + pad)
  end

  # Build inner space for a box by subtracting border and padding from outer space.
  defp box_inner_space(space, border, padding) do
    border_offset = if border in [:none, false], do: 0, else: 1
    pad = if is_integer(padding), do: padding, else: 0
    inset = border_offset + pad

    %{
      x: space.x + inset,
      y: space.y + inset,
      width: max(0, space.width - 2 * inset),
      height: max(0, space.height - 2 * inset)
    }
  end

  # Resolve explicit width/height for a box, handling :fill expansion.
  defp resolve_box_dimensions(element, style, available_space) do
    raw_width =
      Map.get(element, :width) || Map.get(style, :width) ||
        Map.get(element, :size)

    raw_height = Map.get(element, :height) || Map.get(style, :height)

    width = if raw_width == :fill, do: available_space.width, else: raw_width

    height =
      if raw_height == :fill, do: available_space.height, else: raw_height

    {width, height}
  end

  # Shrink available space by a symmetric overhead amount.
  defp shrink_space_by(available_space, overhead) do
    avail_w =
      Map.get(available_space, :width, Raxol.Core.Defaults.terminal_width())

    avail_h =
      Map.get(available_space, :height, Raxol.Core.Defaults.terminal_height())

    Map.merge(available_space, %{
      width: max(0, avail_w - overhead),
      height: max(0, avail_h - overhead)
    })
  end

  # Determine final box size from explicit dimensions or measured children.
  defp resolve_box_size(w, h, _children, _inner_space, _overhead)
       when is_integer(w) and is_integer(h) do
    %{width: w, height: h}
  end

  defp resolve_box_size(w, _h, children, inner_space, overhead)
       when is_integer(w) do
    child_dims = measure_children_as_column(children, inner_space)
    %{width: w, height: child_dims.height + overhead}
  end

  defp resolve_box_size(_w, _h, children, inner_space, overhead) do
    child_dims = measure_children_as_column(children, inner_space)

    %{
      width: child_dims.width + overhead,
      height: child_dims.height + overhead
    }
  end

  defp measure_children_as_column(children, inner_space) do
    measure_container_element(
      :column,
      %{type: :column, children: children},
      inner_space
    )
  end

  # Build :attrs from top-level keys so Flexbox.parse_flex_properties works.
  # The View DSL (Flex.row/column) puts direction/gap/etc. at the top level,
  # but Flexbox reads from the :attrs map.
  defp enrich_flex_attrs(%{type: :flex} = flex) do
    existing = Map.get(flex, :attrs, %{})

    if map_size(existing) > 0 and Map.has_key?(existing, :flex_direction) do
      flex
    else
      Map.put(flex, :attrs, build_flex_attrs(flex))
    end
  end

  # Style values take priority over top-level defaults because
  # Flex.row/column always sets top-level gap/padding to 0 as defaults,
  # so `row style: %{gap: 1}` would otherwise be ignored.
  defp build_flex_attrs(flex) do
    style = resolve_style(flex)

    %{
      flex_direction: Map.get(flex, :direction, :row),
      justify_content:
        Map.get(style, :justify_content) ||
          Map.get(flex, :justify, :flex_start),
      align_items:
        Map.get(style, :align_items) || Map.get(flex, :align, :stretch),
      gap: Map.get(style, :gap) || Map.get(flex, :gap, 0),
      padding: Map.get(style, :padding) || Map.get(flex, :padding, 0)
    }
  end

  defp get_display_text("", placeholder), do: placeholder
  defp get_display_text(value, _placeholder), do: value

  defp get_checkbox_text(true), do: "[[OK]]"
  defp get_checkbox_text(false), do: "[ ]"

  # --- End Measurement Logic ---
end
