lib/pdf/layout.ex

defmodule Pdf.Layout do
  @moduledoc """
  Layout helpers for positioning content in PDF documents.

  Provides `box/4`, `row/4`, and `column/4` containers that manage
  coordinates, padding, margin, and borders based on `Pdf.Style`.
  """

  alias Pdf.{Page, Style}

  @doc """
  Render content inside a box with padding, margin, border, and optional background.

  The callback receives `(page, %{x, y, width, height})` with the inner content area
  (after padding/margin are applied) and must return the updated page.

  ## Example

      Layout.box(page, {50, 700}, {200, 100}, style: %{padding: 10, border: 1}, fn page, area ->
        Page.text_at(page, {area.x, area.y - 12}, "Inside box")
      end)
  """
  def box(page, {x, y}, {w, h}, opts \\ [], callback) do
    style = parse_style(opts)
    {mt, mr, mb, ml} = style.margin
    {pt, pr, pb, pl} = style.padding
    {bt, br, bb, bl} = style.border

    outer_x = x + ml
    outer_y = y - mt
    outer_w = w - ml - mr
    outer_h = h - mt - mb

    page =
      if style.background do
        page
        |> Page.save_state()
        |> Page.set_fill_color(style.background)
        |> Page.rectangle({outer_x, outer_y - outer_h}, {outer_w, outer_h})
        |> Page.fill()
        |> Page.restore_state()
      else
        page
      end

    page = draw_borders(page, {outer_x, outer_y, outer_w, outer_h}, style)

    inner_x = outer_x + bl + pl
    inner_y = outer_y - bt - pt
    inner_w = outer_w - bl - br - pl - pr
    inner_h = outer_h - bt - bb - pt - pb

    callback.(page, %{x: inner_x, y: inner_y, width: inner_w, height: inner_h})
  end

  @doc """
  Distribute content horizontally in columns.

  Takes a list of `{weight, callback}` tuples. The available width is
  split proportionally by weight.

  ## Example

      Layout.row(page, {50, 700}, {400, 100}, [
        {1, fn page, area -> Page.text_at(page, {area.x, area.y - 12}, "Left") end},
        {2, fn page, area -> Page.text_at(page, {area.x, area.y - 12}, "Center (2x wide)") end},
        {1, fn page, area -> Page.text_at(page, {area.x, area.y - 12}, "Right") end}
      ])
  """
  def row(page, {x, y}, {w, h}, columns, opts \\ []) do
    style = parse_style(opts)
    {_mt, _mr, _mb, _ml} = style.margin
    gap = Keyword.get(opts, :gap, 0)

    total_weight = columns |> Enum.map(&elem(&1, 0)) |> Enum.sum()
    gap_total = gap * max(length(columns) - 1, 0)
    available_w = w - gap_total

    {page, _x} =
      Enum.reduce(columns, {page, x}, fn {weight, callback}, {page, col_x} ->
        col_w = available_w * (weight / total_weight)
        page = callback.(page, %{x: col_x, y: y, width: col_w, height: h})
        {page, col_x + col_w + gap}
      end)

    page
  end

  @doc """
  Stack content vertically.

  Takes a list of `{height, callback}` tuples. Each item is placed
  below the previous one.

  ## Example

      Layout.column(page, {50, 700}, {400, 300}, [
        {20, fn page, area -> Page.text_at(page, {area.x, area.y - 12}, "Row 1") end},
        {20, fn page, area -> Page.text_at(page, {area.x, area.y - 12}, "Row 2") end}
      ])
  """
  def column(page, {x, y}, {w, _h}, rows, opts \\ []) do
    gap = Keyword.get(opts, :gap, 0)

    {page, _y} =
      Enum.reduce(rows, {page, y}, fn {row_h, callback}, {page, row_y} ->
        page = callback.(page, %{x: x, y: row_y, width: w, height: row_h})
        {page, row_y - row_h - gap}
      end)

    page
  end

  defp parse_style(opts) do
    case Keyword.get(opts, :style) do
      nil -> Style.new()
      %Style{} = s -> s
      map -> Style.new(map)
    end
  end

  defp draw_borders(page, {x, y, w, h}, style) do
    {bt, br, bb, bl} = style.border

    if bt == 0 and br == 0 and bb == 0 and bl == 0 do
      page
    else
      page = Page.save_state(page)
      page = Page.set_stroke_color(page, style.border_color)

      page =
        if bt > 0 do
          page |> Page.set_line_width(bt) |> Page.line({x, y}, {x + w, y}) |> Page.stroke()
        else
          page
        end

      page =
        if br > 0 do
          page
          |> Page.set_line_width(br)
          |> Page.line({x + w, y}, {x + w, y - h})
          |> Page.stroke()
        else
          page
        end

      page =
        if bb > 0 do
          page
          |> Page.set_line_width(bb)
          |> Page.line({x, y - h}, {x + w, y - h})
          |> Page.stroke()
        else
          page
        end

      page =
        if bl > 0 do
          page |> Page.set_line_width(bl) |> Page.line({x, y}, {x, y - h}) |> Page.stroke()
        else
          page
        end

      Page.restore_state(page)
    end
  end
end