defmodule Raxol.Terminal.Buffer.DamageTracker do
  @moduledoc """
  Tracks damage regions in a terminal buffer for efficient rendering.
  Damage regions indicate areas that have changed and need to be redrawn.
  """

  @type region ::
          {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
  @type t :: %__MODULE__{
          regions: [region()],
          max_regions: non_neg_integer()
        }

  defstruct regions: [],
            max_regions: 100

  @doc """
  Creates a new damage tracker with a maximum region limit.
  """
  @spec new(non_neg_integer()) :: t()
  def new(max_regions \\ 100) do
    %__MODULE__{
      regions: [],
      max_regions: max_regions
    }
  end

  @doc """
  Adds a damage region to the tracker.
  """
  @spec add_damage_region(
          t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer()
        ) :: t()
  def add_damage_region(tracker, x, y, width, height) do
    new_region = {x, y, width, height}

    # Add region and enforce limit
    regions = [new_region | tracker.regions]

    regions =
      if length(regions) > tracker.max_regions do
        Enum.take(regions, tracker.max_regions)
      else
        regions
      end

    %{tracker | regions: regions}
  end

  @doc """
  Adds multiple damage regions at once.
  """
  @spec add_damage_regions(t(), [region()]) :: t()
  def add_damage_regions(tracker, new_regions) do
    Enum.reduce(new_regions, tracker, fn {x, y, width, height}, acc ->
      add_damage_region(acc, x, y, width, height)
    end)
  end

  @doc """
  Gets all damage regions.
  """
  @spec get_damage_regions(t()) :: [region()]
  def get_damage_regions(tracker) do
    tracker.regions
  end

  @doc """
  Clears all damage regions.
  """
  @spec clear_damage(t()) :: t()
  def clear_damage(tracker) do
    %{tracker | regions: []}
  end

  @doc """
  Returns the count of damage regions.
  """
  @spec damage_count(t()) :: non_neg_integer()
  def damage_count(tracker) do
    length(tracker.regions)
  end

  @doc """
  Checks if there are any damage regions.
  """
  @spec has_damage?(t()) :: boolean()
  def has_damage?(tracker) do
    tracker.regions != []
  end

  @doc """
  Merges overlapping or adjacent regions to reduce redundancy.
  """
  @spec merge_regions(t()) :: t()
  def merge_regions(tracker) do
    merged_regions =
      tracker.regions
      |> Enum.sort()
      |> merge_sorted_regions()

    %{tracker | regions: merged_regions}
  end

  # Private functions

  defp merge_sorted_regions([]), do: []
  defp merge_sorted_regions([region]), do: [region]

  defp merge_sorted_regions([r1, r2 | rest]) do
    if regions_overlap?(r1, r2) or regions_adjacent?(r1, r2) do
      merged = merge_two_regions(r1, r2)
      merge_sorted_regions([merged | rest])
    else
      [r1 | merge_sorted_regions([r2 | rest])]
    end
  end

  defp regions_overlap?({x1, y1, w1, h1}, {x2, y2, w2, h2}) do
    not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1)
  end

  defp regions_adjacent?({x1, y1, w1, h1}, {x2, y2, w2, h2}) do
    # Check if regions share an edge
    # Right edge
    # Left edge
    # Bottom edge
    # Top edge
    (x1 + w1 == x2 and y1 == y2 and h1 == h2) or
      (x2 + w2 == x1 and y1 == y2 and h1 == h2) or
      (y1 + h1 == y2 and x1 == x2 and w1 == w2) or
      (y2 + h2 == y1 and x1 == x2 and w1 == w2)
  end

  defp merge_two_regions({x1, y1, w1, h1}, {x2, y2, w2, h2}) do
    min_x = min(x1, x2)
    min_y = min(y1, y2)
    max_x = max(x1 + w1, x2 + w2)
    max_y = max(y1 + h1, y2 + h2)

    {min_x, min_y, max_x - min_x, max_y - min_y}
  end
end
