defmodule Raxol.Core.Renderer.View.Components.Box do
  @moduledoc """
  Handles box layout functionality for the Raxol view system.
  Provides box model layout with content, padding, border, and margin.
  """

  @doc """
  Creates a new box view.

  ## Options
    * `:children` - List of child views
    * `:title` - Optional title rendered in the top border
    * `:padding` - Padding around content (integer or {top, right, bottom, left})
    * `:margin` - Margin around box (integer or {top, right, bottom, left})
    * `:border` - Border style (:none, :single, :double, :rounded, :bold, :dashed)
    * `:fg` - Foreground color
    * `:bg` - Background color
    * `:size` - Box size {width, height}

  ## Examples

      Box.new(children: [view1, view2], padding: 1)
      Box.new(padding: {1, 2, 1, 2}, border: :single)
  """
  def new(opts \\ []) do
    style = Keyword.get(opts, :style, [])
    style_map = if is_map(style), do: style, else: Map.new(style)
    border = Map.get(style_map, :border, Keyword.get(opts, :border, :none))
    padding = Map.get(style_map, :padding, Keyword.get(opts, :padding, 0))

    animation_hints = build_animation_hints(opts)

    %{
      type: :box,
      children: Keyword.get(opts, :children, []),
      title: Keyword.get(opts, :title),
      padding: normalize_spacing(padding),
      margin: normalize_spacing(Keyword.get(opts, :margin, 0)),
      border: border,
      fg: Keyword.get(opts, :fg),
      bg: Keyword.get(opts, :bg),
      size: Keyword.get(opts, :size),
      style: style,
      animation_hints: animation_hints
    }
  end

  @doc """
  Calculates the layout of a box and its children.
  """
  def calculate_layout(box, available_size) do
    # Calculate content size by subtracting padding and border
    content_size = calculate_content_size(box, available_size)

    # Layout children within content area
    children_layout = layout_children(box.children, content_size)

    # Apply padding and border
    layout = apply_box_model(box, children_layout, available_size)

    case layout do
      [] ->
        # If no children, return the box itself as a layout element with position and size
        [Map.merge(box, %{position: {0, 0}, size: available_size})]

      _ ->
        layout
    end
  end

  defp calculate_content_size(box, {width, height}) do
    {padding_left, padding_right, padding_top, padding_bottom} = box.padding
    border_width = if box.border == :none, do: 0, else: 2

    content_width = width - padding_left - padding_right - border_width
    content_height = height - padding_top - padding_bottom - border_width

    {content_width, content_height}
  end

  defp layout_children(children, {width, height}) do
    # Get layout mode from box style or default to vertical
    layout_mode = get_layout_mode(children)

    case layout_mode do
      :horizontal -> layout_horizontal(children, {width, height})
      :stack -> layout_stack(children, {width, height})
      # Default
      _ -> layout_vertical(children, {width, height})
    end
  end

  defp get_layout_mode(children) do
    # Check if any child has a layout mode specified
    Enum.find_value(children, :vertical, fn child ->
      Map.get(child, :layout_mode)
    end)
  end

  defp layout_vertical(children, {width, height}) do
    children
    |> Enum.scan({0, 0}, fn child, {_prev_x, prev_y} ->
      child_height = get_child_height(child, height)
      child_width = get_child_width(child, width)

      # Position child at top of remaining space
      _positioned_child =
        child
        |> Map.put(:position, {0, prev_y})
        |> Map.put(:size, {child_width, child_height})

      {0, prev_y + child_height}
    end)
    |> Enum.map(fn {_pos, child} -> child end)
  end

  defp layout_horizontal(children, {width, height}) do
    children
    |> Enum.scan({0, 0}, fn child, {prev_x, _prev_y} ->
      child_width = get_child_width(child, width)
      child_height = get_child_height(child, height)

      # Position child to the right of previous child
      _positioned_child =
        child
        |> Map.put(:position, {prev_x, 0})
        |> Map.put(:size, {child_width, child_height})

      {prev_x + child_width, 0}
    end)
    |> Enum.map(fn {_pos, child} -> child end)
  end

  defp layout_stack(children, {width, height}) do
    # Stack all children at the same position, only the last one visible
    children
    |> Enum.with_index()
    |> Enum.map(fn {child, index} ->
      child_height = get_child_height(child, height)
      child_width = get_child_width(child, width)

      # All children get the same position, but only the last one is visible
      visible = index == length(children) - 1

      _positioned_child =
        child
        |> Map.put(:position, {0, 0})
        |> Map.put(:size, {child_width, child_height})
        |> Map.put(:visible, visible)
    end)
  end

  defp get_child_width(child, available_width) do
    case Map.get(child, :width) do
      nil -> available_width
      width when is_integer(width) -> min(width, available_width)
      :auto -> available_width
      _ -> available_width
    end
  end

  defp get_child_height(child, available_height) do
    case Map.get(child, :height) do
      # Default height for text-like content
      nil -> 1
      height when is_integer(height) -> min(height, available_height)
      :auto -> 1
      _ -> 1
    end
  end

  defp apply_box_model(box, children_layout, {_width, _height}) do
    {margin_top, margin_right, margin_bottom, margin_left} = box.margin
    {padding_top, padding_right, padding_bottom, padding_left} = box.padding

    # Apply margins
    layout =
      apply_margins(
        children_layout,
        {margin_top, margin_right, margin_bottom, margin_left}
      )

    # Apply padding
    layout =
      apply_padding(
        layout,
        {padding_top, padding_right, padding_bottom, padding_left}
      )

    # Apply border if needed
    case box.border do
      :none -> layout
      _ -> apply_border(layout, box.border)
    end
  end

  defp apply_margins(layout, {top, _right, _bottom, left}) do
    # Apply margins by adjusting the overall box position
    # This affects the box's position relative to its parent
    Enum.map(layout, fn child ->
      {child_x, child_y} = Map.get(child, :position, {0, 0})
      {child_width, child_height} = Map.get(child, :size, {0, 0})

      # Adjust position by margins
      new_x = child_x + left
      new_y = child_y + top

      child
      |> Map.put(:position, {new_x, new_y})
      |> Map.put(:size, {child_width, child_height})
      |> Map.put(:margined, true)
    end)
  end

  defp apply_padding(layout, {top, right, bottom, left}) do
    # Apply padding by adjusting child positions
    Enum.map(layout, fn child ->
      {child_x, child_y} = Map.get(child, :position, {0, 0})
      {child_width, child_height} = Map.get(child, :size, {0, 0})

      # Adjust position by padding
      new_x = child_x + left
      new_y = child_y + top

      # Adjust size to account for padding
      new_width = max(0, child_width - left - right)
      new_height = max(0, child_height - top - bottom)

      child
      |> Map.put(:position, {new_x, new_y})
      |> Map.put(:size, {new_width, new_height})
      |> Map.put(:padded, true)
    end)
  end

  defp apply_border(layout, style) do
    # Get border characters for the style
    border_chars = get_border_characters(style)

    # Apply border by adjusting content area and adding border elements
    layout
    |> Enum.map(fn child ->
      {child_x, child_y} = Map.get(child, :position, {0, 0})
      {child_width, child_height} = Map.get(child, :size, {0, 0})

      # Adjust position to account for border
      # Left border
      new_x = child_x + 1
      # Top border
      new_y = child_y + 1

      # Adjust size to account for borders
      # Left and right borders
      new_width = max(0, child_width - 2)
      # Top and bottom borders
      new_height = max(0, child_height - 2)

      child
      |> Map.put(:position, {new_x, new_y})
      |> Map.put(:size, {new_width, new_height})
      |> Map.put(:bordered, true)
      |> Map.put(:border_style, style)
      |> Map.put(:border_chars, border_chars)
    end)
  end

  defp get_border_characters(style) do
    case style do
      :single ->
        %{
          top_left: "┌",
          top: "─",
          top_right: "┐",
          left: "│",
          right: "│",
          bottom_left: "└",
          bottom: "─",
          bottom_right: "┘"
        }

      :double ->
        %{
          top_left: "╔",
          top: "═",
          top_right: "╗",
          left: "║",
          right: "║",
          bottom_left: "╚",
          bottom: "═",
          bottom_right: "╝"
        }

      :rounded ->
        %{
          top_left: "╭",
          top: "─",
          top_right: "╮",
          left: "│",
          right: "│",
          bottom_left: "╰",
          bottom: "─",
          bottom_right: "╯"
        }

      :bold ->
        %{
          top_left: "┏",
          top: "━",
          top_right: "┓",
          left: "┃",
          right: "┃",
          bottom_left: "┗",
          bottom: "━",
          bottom_right: "┛"
        }

      :dashed ->
        %{
          top_left: "┌",
          top: "┄",
          top_right: "┐",
          left: "┆",
          right: "┆",
          bottom_left: "└",
          bottom: "┄",
          bottom_right: "┘"
        }

      _ ->
        %{
          top_left: "┌",
          top: "─",
          top_right: "┐",
          left: "│",
          right: "│",
          bottom_left: "└",
          bottom: "─",
          bottom_right: "┘"
        }
    end
  end

  # Helper function to normalize spacing values
  defp build_animation_hints(opts) do
    existing = Keyword.get(opts, :animation_hints, [])

    if Keyword.get(opts, :border_beam, false) do
      beam_opts = Keyword.get(opts, :border_beam_opts, [])

      hint = %{
        type: :border_beam,
        effect: Keyword.get(beam_opts, :effect, :stroke),
        variant: Keyword.get(beam_opts, :variant, :colorful),
        size: Keyword.get(beam_opts, :size, :full),
        strength: Keyword.get(beam_opts, :strength, 0.8),
        duration_ms: Keyword.get(beam_opts, :duration, 2000),
        period_ms: Keyword.get(beam_opts, :period_ms, 1800),
        density: Keyword.get(beam_opts, :density, 0.75),
        frequency: Keyword.get(beam_opts, :frequency, 25),
        bucket_ms: Keyword.get(beam_opts, :bucket_ms, 60),
        softness: Keyword.get(beam_opts, :softness, 0.45),
        brightness: Keyword.get(beam_opts, :brightness, 1.3),
        saturation: Keyword.get(beam_opts, :saturation, 1.2),
        hue_range: Keyword.get(beam_opts, :hue_range, 30),
        active: Keyword.get(beam_opts, :active, true),
        static_colors: Keyword.get(beam_opts, :static_colors, false)
      }

      [hint | existing]
    else
      existing
    end
  end

  defp normalize_spacing(n) when is_integer(n) and n >= 0, do: {n, n, n, n}
  defp normalize_spacing({n}) when is_integer(n) and n >= 0, do: {n, n, n, n}

  defp normalize_spacing({h, v})
       when is_integer(h) and is_integer(v) and h >= 0 and v >= 0,
       do: {h, v, h, v}

  defp normalize_spacing({t, r, b, l})
       when is_integer(t) and is_integer(r) and is_integer(b) and is_integer(l) and
              t >= 0 and r >= 0 and b >= 0 and l >= 0,
       do: {t, r, b, l}

  defp normalize_spacing(_), do: {0, 0, 0, 0}
end
