defmodule Raxol.Terminal.Selection.Manager do
  @moduledoc """
  Manages text selection operations in the terminal.
  """

  defstruct start_pos: nil,
            end_pos: nil,
            active: false,
            # :normal, :word, :line
            mode: :normal,
            scrollback_included: false

  @type position :: {non_neg_integer(), non_neg_integer()}
  @type selection_mode :: :normal | :word | :line

  @type t :: %__MODULE__{
          start_pos: position() | nil,
          end_pos: position() | nil,
          active: boolean(),
          mode: selection_mode(),
          scrollback_included: boolean()
        }

  @doc """
  Creates a new selection manager instance.
  """
  def new do
    %__MODULE__{}
  end

  @doc """
  Starts a new selection at the given position.
  """
  def start_selection(%__MODULE__{} = state, pos, mode \\ :normal)
      when is_tuple(pos) and tuple_size(pos) == 2 and
             mode in [:normal, :word, :line] do
    %{state | start_pos: pos, end_pos: pos, active: true, mode: mode}
  end

  @doc """
  Updates the selection end position.
  """
  def update_selection(%__MODULE__{} = state, pos)
      when is_tuple(pos) and tuple_size(pos) == 2 do
    update_selection_if_active(state.active, state, pos)
  end

  @doc """
  Ends the current selection.
  """
  def end_selection(%__MODULE__{} = state) do
    %{state | active: false, start_pos: nil, end_pos: nil}
  end

  @doc """
  Gets the current selection range.
  """
  def get_selection_range(%__MODULE__{} = state) do
    get_range_if_valid(
      state.active and state.start_pos and state.end_pos,
      state
    )
  end

  @doc """
  Checks if a position is within the current selection.
  """
  def position_in_selection?(%__MODULE__{} = state, pos)
      when is_tuple(pos) and tuple_size(pos) == 2 do
    check_position_in_selection(
      state.active and state.start_pos and state.end_pos,
      state,
      pos
    )
  end

  @doc """
  Gets the selected text from the terminal buffer.
  """
  def get_selected_text(%__MODULE__{} = state, buffer) do
    get_text_if_valid_selection(
      state.active and state.start_pos and state.end_pos,
      state,
      buffer
    )
  end

  @doc """
  Includes scrollback buffer in selection.
  """
  def include_scrollback(%__MODULE__{} = state, include \\ true) do
    %{state | scrollback_included: include}
  end

  @doc """
  Checks if scrollback is included in selection.
  """
  def scrollback_included?(%__MODULE__{} = state) do
    state.scrollback_included
  end

  # Private helper functions

  defp update_selection_if_active(false, state, _pos), do: state
  defp update_selection_if_active(true, state, pos), do: %{state | end_pos: pos}

  defp get_range_if_valid(false, _state), do: nil
  defp get_range_if_valid(true, state), do: {state.start_pos, state.end_pos}

  defp check_position_in_selection(false, _state, _pos), do: false

  defp check_position_in_selection(true, state, pos) do
    {start_x, start_y} = state.start_pos
    {end_x, end_y} = state.end_pos
    {x, y} = pos

    check_line_type_selection(
      start_y == end_y,
      y,
      x,
      start_y,
      end_y,
      start_x,
      end_x
    )
  end

  defp check_line_type_selection(true, y, x, start_y, _end_y, start_x, end_x) do
    # Single line selection
    y == start_y and x >= min(start_x, end_x) and x <= max(start_x, end_x)
  end

  defp check_line_type_selection(false, y, x, start_y, end_y, start_x, end_x) do
    # Multi-line selection
    check_multiline_position(y, x, start_y, end_y, start_x, end_x)
  end

  defp get_text_if_valid_selection(false, _state, _buffer), do: ""

  defp get_text_if_valid_selection(true, state, buffer) do
    {start_x, start_y} = state.start_pos
    {end_x, end_y} = state.end_pos

    get_text_by_line_type(
      start_y == end_y,
      buffer,
      start_y,
      end_y,
      start_x,
      end_x
    )
  end

  defp get_text_by_line_type(true, buffer, start_y, _end_y, start_x, end_x) do
    get_line_selection(buffer, start_y, start_x, end_x)
  end

  defp get_text_by_line_type(false, buffer, start_y, end_y, start_x, end_x) do
    get_multiline_selection(buffer, start_y, end_y, start_x, end_x)
  end

  defp get_line_selection(buffer, y, start_x, end_x) do
    case Enum.at(buffer, y) do
      nil ->
        ""

      line ->
        start_idx = min(start_x, end_x)
        end_idx = max(start_x, end_x)
        String.slice(line, start_idx, end_idx - start_idx + 1)
    end
  end

  defp get_multiline_selection(buffer, start_y, end_y, start_x, end_x) do
    buffer
    |> Enum.slice(start_y..end_y)
    |> Enum.with_index()
    |> Enum.map_join("\n", fn {line, idx} ->
      slice_line_by_index(line, idx, end_y - start_y, start_x, end_x)
    end)
  end

  defp check_multiline_position(y, x, start_y, _end_y, start_x, _end_x)
       when y == start_y,
       do: x >= start_x

  defp check_multiline_position(y, x, _start_y, end_y, _start_x, end_x)
       when y == end_y,
       do: x <= end_x

  defp check_multiline_position(y, _x, start_y, end_y, _start_x, _end_x)
       when y > start_y and y < end_y,
       do: true

  defp check_multiline_position(_y, _x, _start_y, _end_y, _start_x, _end_x),
    do: false

  defp slice_line_by_index(line, 0, _last_index, start_x, _end_x) do
    String.slice(line, start_x..-1)
  end

  defp slice_line_by_index(line, idx, last_index, _start_x, end_x)
       when idx == last_index do
    String.slice(line, 0..end_x)
  end

  defp slice_line_by_index(line, _idx, _last_index, _start_x, _end_x), do: line
end
