defmodule Raxol.Terminal.Buffer.Selection do
  @moduledoc """
  Manages text selection operations for the terminal.
  This module handles all selection-related operations including:
  - Starting and updating selections
  - Getting selected text
  - Checking if positions are within selections
  - Managing selection boundaries
  - Extracting text from regions
  """

  alias Raxol.Terminal.ScreenBuffer

  @doc """
  Creates a new selection with start and end positions.
  """
  @spec new(
          {non_neg_integer(), non_neg_integer()},
          {non_neg_integer(), non_neg_integer()}
        ) ::
          {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
  def new({start_x, start_y}, {end_x, end_y}) do
    {start_x, start_y, end_x, end_y}
  end

  @doc """
  Starts a text selection at the specified position.
  """
  @spec start(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
          ScreenBuffer.t()
  def start(buffer, x, y) do
    # Clear any existing selection first
    buffer = clear(buffer)
    %{buffer | selection: {x, y, x, y}}
  end

  @doc """
  Updates the current text selection to the specified position.
  """
  @spec update(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
          ScreenBuffer.t()
  def update(buffer, x, y) do
    case buffer.selection do
      {start_x, start_y, _, _} ->
        %{buffer | selection: {start_x, start_y, x, y}}

      nil ->
        start(buffer, x, y)
    end
  end

  @doc """
  Gets the currently selected text.
  """
  @spec get_text(ScreenBuffer.t()) :: String.t()
  def get_text(buffer) do
    case buffer.selection do
      nil ->
        ""

      {start_x, start_y, end_x, end_y} ->
        get_text_in_region(buffer, start_x, start_y, end_x, end_y)
    end
  end

  @doc """
  Checks if a position is within the current selection.
  """
  @spec contains?(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
          boolean()
  def contains?(buffer, x, y) do
    case buffer.selection do
      nil ->
        false

      {start_x, start_y, end_x, end_y} ->
        # Normalize coordinates to ensure start <= end
        {min_x, max_x} = {min(start_x, end_x), max(start_x, end_x)}
        {min_y, max_y} = {min(start_y, end_y), max(start_y, end_y)}

        x >= min_x and x <= max_x and y >= min_y and y <= max_y
    end
  end

  @doc """
  Gets the current selection boundaries.
  """
  @spec get_boundaries(ScreenBuffer.t()) ::
          {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
          | nil
  def get_boundaries(buffer) do
    case buffer.selection do
      nil ->
        nil

      {start_x, start_y, end_x, end_y} ->
        # Normalize coordinates to ensure start <= end
        {min_x, max_x} = {min(start_x, end_x), max(start_x, end_x)}
        {min_y, max_y} = {min(start_y, end_y), max(start_y, end_y)}

        {min_x, min_y, max_x, max_y}
    end
  end

  @doc """
  Gets text from a specified region in the buffer.
  """
  @spec get_text_in_region(
          ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer()
        ) :: String.t()
  def get_text_in_region(buffer, start_x, start_y, end_x, end_y) do
    # Check if coordinates are out of bounds
    case out_of_bounds?(buffer, start_x, start_y, end_x, end_y) do
      true ->
        ""

      false ->
        # Ensure start coordinates are less than or equal to end coordinates
        {start_x, end_x} = {min(start_x, end_x), max(start_x, end_x)}
        {start_y, end_y} = {min(start_y, end_y), max(start_y, end_y)}

        # Handle empty region (same start and end coordinates)
        handle_region_extraction(buffer, start_x, start_y, end_x, end_y)
    end
  end

  defp handle_region_extraction(_buffer, start_x, start_y, end_x, end_y)
       when start_x == end_x and start_y == end_y do
    ""
  end

  defp handle_region_extraction(buffer, start_x, start_y, end_x, end_y) do
    extract_region_text(buffer, start_x, start_y, end_x, end_y)
  end

  defp out_of_bounds?(buffer, start_x, start_y, end_x, end_y) do
    start_x >= buffer.width or end_x >= buffer.width or
      start_y >= buffer.height or end_y >= buffer.height or
      start_x < 0 or start_y < 0 or end_x < 0 or end_y < 0
  end

  defp extract_region_text(buffer, start_x, start_y, end_x, end_y) do
    case buffer.cells do
      nil ->
        # Return empty string if cells is nil
        ""

      cells ->
        text =
          for y <- start_y..end_y do
            line = Enum.at(cells, y) || []

            chars =
              for x <- start_x..end_x do
                cell = Enum.at(line, x)
                get_cell_char(cell)
              end

            Enum.join(chars)
          end

        Enum.join(text, "\n")
    end
  end

  @doc """
  Clears the current selection.
  """
  @spec clear(ScreenBuffer.t()) :: ScreenBuffer.t()
  def clear(buffer) do
    %{buffer | selection: nil}
  end

  @doc """
  Checks if there is an active selection.
  """
  @spec active?(ScreenBuffer.t()) :: boolean()
  def active?(buffer) do
    buffer.selection != nil
  end

  @doc """
  Gets the selection start position.
  """
  @spec get_start_position(ScreenBuffer.t()) ::
          {non_neg_integer(), non_neg_integer()} | nil
  def get_start_position(buffer) do
    case buffer.selection do
      {start_x, start_y, _, _} -> {start_x, start_y}
      nil -> nil
    end
  end

  @doc """
  Gets the selection end position.
  """
  @spec get_end_position(ScreenBuffer.t()) ::
          {non_neg_integer(), non_neg_integer()} | nil
  def get_end_position(buffer) do
    case buffer.selection do
      {start_x, start_y, end_x, end_y} ->
        get_position_if_different(start_x, start_y, end_x, end_y)

      nil ->
        nil
    end
  end

  defp get_position_if_different(start_x, start_y, end_x, end_y)
       when start_x == end_x and start_y == end_y do
    nil
  end

  defp get_position_if_different(_start_x, _start_y, end_x, end_y) do
    {end_x, end_y}
  end

  @doc """
  Gets a line from a list of strings at the specified index.
  """
  @spec get_line(list(String.t()), non_neg_integer()) :: String.t()
  def get_line(lines_list, row) when is_list(lines_list) and is_integer(row) do
    Enum.at(lines_list, row, "")
  end

  @doc """
  Gets the buffer text for the current selection.
  This is an alias for get_text/1 for compatibility.
  """
  @spec get_buffer_text(ScreenBuffer.t()) ::
          {:ok, String.t()} | {:error, term()}
  def get_buffer_text(buffer) do
    case Raxol.Core.ErrorHandling.safe_call(fn -> get_text(buffer) end) do
      {:ok, text} -> {:ok, text}
      {:error, e} -> {:error, e}
    end
  end

  defp get_cell_char(nil), do: " "
  defp get_cell_char(cell), do: cell.char
end
