defmodule Raxol.Terminal.Buffer.ScrollRegion do
  @moduledoc """
  Handles scroll region operations for the screen buffer.
  This module manages the scroll region boundaries and provides functions
  for scrolling content within the defined region.

  ## Scroll Region

  A scroll region defines a subset of the screen buffer where scrolling operations
  are confined. The region is defined by its top and bottom boundaries, and all
  scrolling operations (up/down) will only affect the content within these boundaries.

  ## Operations

  * Setting and clearing scroll regions
  * Scrolling content up and down within the region
  * Getting region boundaries
  * Validating region boundaries
  * Managing content within the region
  """

  require Raxol.Core.Runtime.Log

  alias Raxol.Terminal.Cell
  alias Raxol.Terminal.ScreenBuffer
  alias Raxol.Terminal.ScreenBuffer.Core, as: ScreenBufferCore

  @doc """
  Sets the scroll region boundaries.
  The region must be valid (top < bottom) and within screen bounds.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `top` - The top boundary of the scroll region
  * `bottom` - The bottom boundary of the scroll region

  ## Returns

  The updated screen buffer with new scroll region boundaries.
  If the region is invalid, the scroll region is cleared.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 5, 15)
      iex> ScrollRegion.get_region(buffer)
      {5, 15}

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 15, 5)  # Invalid region
      iex> ScrollRegion.get_region(buffer)
      {5, 15}
  """
  @spec set_region(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
          ScreenBuffer.t()
  def set_region(buffer, top, bottom) when top < 0 or bottom < 0,
    do: %{buffer | scroll_region: nil, scroll_position: 0}

  def set_region(buffer, top, bottom) when top > bottom,
    do: %{buffer | scroll_region: nil, scroll_position: 0}

  def set_region(buffer, _top, bottom) when bottom >= buffer.height,
    do: %{buffer | scroll_region: nil, scroll_position: 0}

  def set_region(buffer, top, bottom),
    do: %{buffer | scroll_region: {top, bottom}, scroll_position: top}

  @doc """
  Clears the scroll region, resetting to full screen.

  ## Parameters

  * `buffer` - The screen buffer to modify

  ## Returns

  The updated screen buffer with scroll region cleared.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 5, 15)
      iex> buffer = ScrollRegion.clear(buffer)
      iex> ScrollRegion.get_region(buffer)
      nil
  """
  @spec clear(ScreenBuffer.t()) :: ScreenBuffer.t()
  def clear(buffer) do
    %{buffer | scroll_region: nil}
  end

  @doc """
  Clears the scroll region, resetting to full screen.
  Alias for clear/1 for backward compatibility.
  """
  @spec clear_region(ScreenBuffer.t()) :: ScreenBuffer.t()
  def clear_region(buffer) do
    clear(buffer)
  end

  @doc """
  Gets the current scroll region boundaries.

  ## Parameters

  * `buffer` - The screen buffer to query

  ## Returns

  A tuple {top, bottom} representing the scroll region boundaries.
  Returns nil if no region is set.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> ScrollRegion.get_region(buffer)
      nil

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 5, 15)
      iex> ScrollRegion.get_region(buffer)
      {5, 15}
  """
  @spec get_region(ScreenBuffer.t()) ::
          {non_neg_integer(), non_neg_integer()} | nil
  def get_region(%ScreenBuffer{scroll_region: nil}) do
    nil
  end

  def get_region(%ScreenBuffer{scroll_region: {top, bottom}}) do
    {top, bottom}
  end

  @doc """
  Gets the current scroll region boundaries.
  Returns {0, height-1} if no region is set.

  ## Parameters

  * `buffer` - The screen buffer to query

  ## Returns

  A tuple {top, bottom} representing the effective scroll region boundaries.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> ScrollRegion.get_boundaries(buffer)
      {0, 23}

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 5, 15)
      iex> ScrollRegion.get_boundaries(buffer)
      {5, 15}
  """
  @spec get_boundaries(ScreenBuffer.t()) ::
          {non_neg_integer(), non_neg_integer()}
  def get_boundaries(%ScreenBuffer{scroll_region: nil, height: height}) do
    {0, height - 1}
  end

  def get_boundaries(%ScreenBuffer{scroll_region: {top, bottom}}),
    do: {top, bottom}

  @doc """
  Scrolls the content up within the scroll region.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `lines` - The number of lines to scroll up
  * `scroll_region_arg` - Optional scroll region override

  ## Returns

  The updated screen buffer with content scrolled up.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 5, 15)
      iex> buffer = ScrollRegion.scroll_up(buffer, 1)
      iex> # Content is scrolled up within region 5-15
  """
  @spec scroll_up(ScreenBuffer.t(), integer(), {integer(), integer()} | nil) ::
          {ScreenBuffer.t(), list()}
  def scroll_up(buffer, lines, scroll_region_arg \\ nil)

  def scroll_up(buffer, lines, scroll_region_arg) when lines > 0 do
    {scroll_start, scroll_end} = get_effective_region(buffer, scroll_region_arg)
    visible_lines = scroll_end - scroll_start + 1

    case lines >= visible_lines do
      true ->
        scrolled_lines =
          extract_lines_from_region(buffer, scroll_start, scroll_end)

        updated_buffer = clear_region(buffer, scroll_start, scroll_end)
        {updated_buffer, scrolled_lines}

      false ->
        {updated_buffer, scrolled_lines} =
          scroll_region_up_with_lines(buffer, scroll_start, scroll_end, lines)

        {updated_buffer, scrolled_lines}
    end
  end

  def scroll_up(buffer, lines, _scroll_region_arg) when lines <= 0 do
    {buffer, []}
  end

  defp clear_region(buffer, start, ending) do
    visible_lines = ending - start + 1

    empty_region_cells =
      List.duplicate(List.duplicate(Cell.new(), buffer.width), visible_lines)

    updated_cells =
      replace_region_content(buffer.cells, start, ending, empty_region_cells)

    %{buffer | cells: updated_cells}
  end

  defp scroll_region_up_with_lines(buffer, scroll_start, scroll_end, lines) do
    # Pre-create a single empty line to reuse
    empty_line = List.duplicate(Cell.new(), buffer.width)

    # Split cells into before, region, and after_part
    {before, region_and_after} = Enum.split(buffer.cells, scroll_start)

    {region, after_part} =
      Enum.split(region_and_after, scroll_end - scroll_start + 1)

    # Move region content up by lines
    {scrolled_out, remaining} = Enum.split(region, lines)

    # Create empty lines for the bottom
    empty_lines = List.duplicate(empty_line, lines)

    # Combine: before + (remaining + empty_lines) + after_part
    new_cells = before ++ (remaining ++ empty_lines) ++ after_part

    {%{buffer | cells: new_cells}, scrolled_out}
  end

  @doc """
  Scrolls the content down within the scroll region.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `lines` - The number of lines to scroll down
  * `scroll_region_arg` - Optional scroll region override

  ## Returns

  The updated screen buffer with content scrolled down.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> buffer = ScrollRegion.set_region(buffer, 5, 15)
      iex> buffer = ScrollRegion.scroll_down(buffer, 1)
      iex> # Content is scrolled down within region 5-15
  """
  @spec scroll_down(ScreenBuffer.t(), integer(), {integer(), integer()} | nil) ::
          ScreenBuffer.t()
  def scroll_down(buffer, lines, scroll_region_arg \\ nil)

  def scroll_down(buffer, lines, scroll_region_arg) when lines > 0 do
    {scroll_start, scroll_end} = get_effective_region(buffer, scroll_region_arg)
    visible_lines = scroll_end - scroll_start + 1

    case lines >= visible_lines do
      true -> clear_region(buffer, scroll_start, scroll_end)
      false -> scroll_region_down(buffer, scroll_start, scroll_end, lines)
    end
  end

  def scroll_down(buffer, lines, _scroll_region_arg) when lines <= 0 do
    buffer
  end

  defp scroll_region_down(buffer, scroll_start, scroll_end, lines) do
    # Pre-create a single empty line to reuse
    empty_line = List.duplicate(Cell.new(), buffer.width)

    # Split cells into before, region, and after
    {before, region_and_after} = Enum.split(buffer.cells, scroll_start)

    {region, after_part} =
      Enum.split(region_and_after, scroll_end - scroll_start + 1)

    # Create empty lines for the top
    empty_lines = List.duplicate(empty_line, lines)

    # For down scroll: add empty lines at top, remove from bottom
    {remaining, _scrolled_out} = Enum.split(region, length(region) - lines)

    # Combine: before + (empty_lines + remaining) + after
    new_cells = before ++ (empty_lines ++ remaining) ++ after_part

    %{buffer | cells: new_cells}
  end

  @doc """
  Replaces the content of a region in the buffer with new content.

  ## Parameters

  * `cells` - The current cells in the buffer
  * `start_line` - The starting line of the region to replace
  * `end_line` - The ending line of the region to replace
  * `new_content` - The new content to insert in the region

  ## Returns

  The updated cells with the region replaced.

  ## Examples

      iex> cells = [[%Cell{char: "A"}, %Cell{char: "B"}], [%Cell{char: "C"}, %Cell{char: "D"}]]
      iex> new_content = [[%Cell{char: "X"}, %Cell{char: "Y"}], [%Cell{char: "Z"}, %Cell{char: "W"}]]
      iex> ScrollRegion.replace_region_content(cells, 0, 1, new_content)
      [[%Cell{char: "X"}, %Cell{char: "Y"}], [%Cell{char: "Z"}, %Cell{char: "W"}]]
  """
  @spec replace_region_content(
          list(list(Cell.t())),
          non_neg_integer(),
          non_neg_integer(),
          list(list(Cell.t()))
        ) :: list(list(Cell.t()))
  def replace_region_content(cells, start_line, end_line, new_content) do
    {before, after_part} = Enum.split(cells, start_line)
    {_, after_part} = Enum.split(after_part, end_line - start_line + 1)
    before ++ new_content ++ after_part
  end

  defp extract_lines_from_region(buffer, start_line, end_line) do
    Enum.map(start_line..end_line, fn i ->
      case i < length(buffer.cells) do
        true -> Enum.at(buffer.cells, i, [])
        false -> []
      end
    end)
    |> Enum.filter(fn line -> line != [] end)
  end

  defp get_effective_region(buffer, scroll_region_arg) do
    case scroll_region_arg do
      {start, ending}
      when is_integer(start) and start >= 0 and is_integer(ending) and
             ending >= start ->
        clamp_region({start, ending}, buffer.height)

      _ ->
        get_buffer_region(buffer)
    end
  end

  defp clamp_region({start, ending}, height),
    do: {start, min(height - 1, ending)}

  defp get_buffer_region(%ScreenBuffer{scroll_region: region, height: height}) do
    case region do
      {start, ending}
      when is_integer(start) and start >= 0 and is_integer(ending) and
             ending >= start ->
        clamp_region({start, ending}, height)

      _ ->
        {0, height - 1}
    end
  end

  defp get_buffer_region(%ScreenBufferCore{
         scroll_region: region,
         height: height
       }) do
    case region do
      {start, ending}
      when is_integer(start) and start >= 0 and is_integer(ending) and
             ending >= start ->
        clamp_region({start, ending}, height)

      _ ->
        {0, height - 1}
    end
  end

  @spec scroll_to(ScreenBuffer.t(), integer(), integer(), integer()) ::
          ScreenBuffer.t()
  def scroll_to(buffer, top, bottom, line) do
    {top, bottom} = clamp_region({top, bottom}, buffer.height)
    line = max(top, min(line, bottom))
    %{buffer | scroll_position: line}
  end

  @doc """
  Gets the current scroll position within the scroll region.
  """
  @spec get_scroll_position(ScreenBuffer.t()) :: non_neg_integer()
  def get_scroll_position(%ScreenBuffer{scroll_position: position}) do
    position
  end

  @doc """
  Gets the dimensions of the buffer.
  """
  @spec get_dimensions(ScreenBuffer.t()) ::
          {non_neg_integer(), non_neg_integer()}
  def get_dimensions(%ScreenBuffer{width: width, height: height}) do
    {width, height}
  end

  @doc """
  Gets the height of the buffer.
  """
  @spec get_height(ScreenBuffer.t()) :: non_neg_integer()
  def get_height(%ScreenBuffer{height: height}) do
    height
  end

  @doc """
  Gets the width of the buffer.
  """
  @spec get_width(ScreenBuffer.t()) :: non_neg_integer()
  def get_width(%ScreenBuffer{width: width}) do
    width
  end

  @doc """
  Gets the scroll bottom boundary.
  """
  @spec get_scroll_bottom(ScreenBuffer.t()) :: non_neg_integer()
  def get_scroll_bottom(%ScreenBuffer{scroll_region: nil, height: height}) do
    height - 1
  end

  def get_scroll_bottom(%ScreenBuffer{scroll_region: {_top, bottom}}) do
    bottom
  end

  @doc """
  Gets the scroll top boundary.
  """
  @spec get_scroll_top(ScreenBuffer.t()) :: non_neg_integer()
  def get_scroll_top(%ScreenBuffer{scroll_region: nil}) do
    0
  end

  def get_scroll_top(%ScreenBuffer{scroll_region: {top, _bottom}}) do
    top
  end

  @doc """
  Shifts the content in the scroll region so that the content of the given target line appears at the top of the region.
  Fills with blank lines as needed if the shift would go out of bounds.
  """
  @spec shift_region_to_line(
          ScreenBuffer.t(),
          {non_neg_integer(), non_neg_integer()},
          non_neg_integer()
        ) :: ScreenBuffer.t()
  def shift_region_to_line(buffer, {top, bottom}, target_line) do
    {top, bottom} = clamp_region({top, bottom}, buffer.height)
    target_line = max(top, min(target_line, bottom))
    shift = target_line - top

    new_cells = shift_region_content(buffer.cells, top, bottom, shift)
    %{buffer | cells: new_cells, scroll_position: target_line}
  end

  defp shift_region_content(cells, top, bottom, shift) do
    case cells do
      [] ->
        []

      [first | _] ->
        region_height = bottom - top + 1
        {before, region_and_after} = Enum.split(cells, top)
        {region, after_part} = Enum.split(region_and_after, region_height)

        case region do
          [] ->
            cells

          _ ->
            {_to_shift, remaining} = Enum.split(region, shift)
            empty_line = List.duplicate(Cell.new(), length(first))
            empty_lines = List.duplicate(empty_line, shift)
            new_region = remaining ++ empty_lines

            before ++ new_region ++ after_part
        end
    end
  end
end
