defmodule Raxol.Terminal.ScreenBuffer.Attributes do
  @moduledoc """
  Manages buffer attributes including formatting, charset, and cursor state.
  Consolidates: Formatting, TextFormatting, Charset, Cursor functionality.
  """

  alias Raxol.Terminal.ScreenBuffer.Core
  alias Raxol.Terminal.ANSI.TextFormatting

  # Cursor operations

  @doc """
  Sets the cursor position.
  """
  @spec set_cursor_position(Core.t(), non_neg_integer(), non_neg_integer()) ::
          Core.t()
  def set_cursor_position(buffer, x, y) do
    x = max(0, min(x, buffer.width - 1))
    y = max(0, min(y, buffer.height - 1))
    %{buffer | cursor_position: {x, y}}
  end

  @doc """
  Gets the cursor position.
  """
  @spec get_cursor_position(Core.t()) :: {non_neg_integer(), non_neg_integer()}
  def get_cursor_position(buffer) do
    buffer.cursor_position
  end

  @doc """
  Moves the cursor relative to its current position.
  """
  @spec move_cursor(Core.t(), integer(), integer()) :: Core.t()
  def move_cursor(buffer, dx, dy) do
    {x, y} = buffer.cursor_position
    set_cursor_position(buffer, x + dx, y + dy)
  end

  @doc """
  Sets cursor visibility.
  """
  @spec set_cursor_visible(Core.t(), boolean()) :: Core.t()
  def set_cursor_visible(buffer, visible) do
    %{buffer | cursor_visible: visible}
  end

  @doc """
  Sets cursor style.
  """
  @spec set_cursor_style(Core.t(), atom()) :: Core.t()
  def set_cursor_style(buffer, style)
      when style in [:block, :underline, :bar] do
    %{buffer | cursor_style: style}
  end

  def set_cursor_style(buffer, _), do: buffer

  @doc """
  Sets cursor blink state.
  """
  @spec set_cursor_blink(Core.t(), boolean()) :: Core.t()
  def set_cursor_blink(buffer, blink) do
    %{buffer | cursor_blink: blink}
  end

  @doc """
  Saves the current cursor position.
  """
  @spec save_cursor(Core.t()) :: Core.t()
  def save_cursor(buffer) do
    Map.put(buffer, :saved_cursor_position, buffer.cursor_position)
  end

  @doc """
  Restores the saved cursor position.
  """
  @spec restore_cursor(Core.t()) :: Core.t()
  def restore_cursor(buffer) do
    case Map.get(buffer, :saved_cursor_position) do
      {x, y} -> set_cursor_position(buffer, x, y)
      nil -> buffer
    end
  end

  # Formatting operations

  @doc """
  Sets the default text style for the buffer.
  """
  @spec set_default_style(Core.t(), map()) :: Core.t()
  def set_default_style(buffer, style) do
    %{buffer | default_style: style}
  end

  @doc """
  Gets the default text style.
  """
  @spec get_default_style(Core.t()) :: map()
  def get_default_style(buffer) do
    buffer.default_style || TextFormatting.new()
  end

  @doc """
  Creates a text style from SGR parameters.
  """
  @spec create_style(list(integer())) :: map()
  def create_style(params) do
    # Create a default style and apply SGR parameters
    initial_style = TextFormatting.new()

    Enum.reduce(params, initial_style, fn param, style ->
      TextFormatting.parse_sgr_param(param, style)
    end)
  end

  @doc """
  Merges two styles, with the second taking precedence.
  """
  @spec merge_styles(map(), map()) :: map()
  def merge_styles(base, override) do
    Map.merge(base, override)
  end

  # Charset operations

  @doc """
  Sets the active charset (G0, G1, G2, G3).
  """
  @spec set_charset(Core.t(), atom(), atom()) :: Core.t()
  def set_charset(buffer, slot, charset) when slot in [:g0, :g1, :g2, :g3] do
    charsets =
      Map.get(buffer, :charsets, %{
        g0: :ascii,
        g1: :ascii,
        g2: :ascii,
        g3: :ascii
      })

    new_charsets = Map.put(charsets, slot, charset)
    Map.put(buffer, :charsets, new_charsets)
  end

  def set_charset(buffer, _, _), do: buffer

  @doc """
  Gets the active charset for a slot.
  """
  @spec get_charset(Core.t(), atom()) :: atom()
  def get_charset(buffer, slot) when slot in [:g0, :g1, :g2, :g3] do
    charsets =
      Map.get(buffer, :charsets, %{
        g0: :ascii,
        g1: :ascii,
        g2: :ascii,
        g3: :ascii
      })

    Map.get(charsets, slot, :ascii)
  end

  def get_charset(_buffer, _), do: :ascii

  @doc """
  Selects which charset slot is active.
  """
  @spec select_charset(Core.t(), atom()) :: Core.t()
  def select_charset(buffer, slot) when slot in [:g0, :g1, :g2, :g3] do
    Map.put(buffer, :active_charset, slot)
  end

  def select_charset(buffer, _), do: buffer

  @doc """
  Gets the currently active charset.
  """
  @spec get_active_charset(Core.t()) :: atom()
  def get_active_charset(buffer) do
    slot = Map.get(buffer, :active_charset, :g0)
    get_charset(buffer, slot)
  end

  @doc """
  Translates a character according to the active charset.
  """
  @spec translate_char(Core.t(), String.t()) :: String.t()
  def translate_char(buffer, char) do
    charset = get_active_charset(buffer)
    translate_with_charset(char, charset)
  end

  # Screen mode operations

  @doc """
  Switches between main and alternate screen buffers.
  """
  @spec set_alternate_screen(Core.t(), boolean()) :: Core.t()
  def set_alternate_screen(buffer, use_alternate) do
    %{buffer | alternate_screen: use_alternate}
  end

  @doc """
  Checks if using alternate screen.
  """
  @spec using_alternate_screen?(Core.t()) :: boolean()
  def using_alternate_screen?(buffer) do
    buffer.alternate_screen
  end

  # Tab stop operations

  @doc """
  Sets a tab stop at the current cursor position.
  """
  @spec set_tab_stop(Core.t()) :: Core.t()
  def set_tab_stop(buffer) do
    {x, _y} = buffer.cursor_position
    tab_stops = Map.get(buffer, :tab_stops, default_tab_stops(buffer.width))
    new_tab_stops = MapSet.put(tab_stops, x)
    Map.put(buffer, :tab_stops, new_tab_stops)
  end

  @doc """
  Clears a tab stop at the current cursor position.
  """
  @spec clear_tab_stop(Core.t()) :: Core.t()
  def clear_tab_stop(buffer) do
    {x, _y} = buffer.cursor_position
    tab_stops = Map.get(buffer, :tab_stops, default_tab_stops(buffer.width))
    new_tab_stops = MapSet.delete(tab_stops, x)
    Map.put(buffer, :tab_stops, new_tab_stops)
  end

  @doc """
  Clears all tab stops.
  """
  @spec clear_all_tab_stops(Core.t()) :: Core.t()
  def clear_all_tab_stops(buffer) do
    Map.put(buffer, :tab_stops, MapSet.new())
  end

  @doc """
  Resets tab stops to default (every 8 columns).
  """
  @spec reset_tab_stops(Core.t()) :: Core.t()
  def reset_tab_stops(buffer) do
    Map.put(buffer, :tab_stops, default_tab_stops(buffer.width))
  end

  @doc """
  Finds the next tab stop position from the current cursor.
  """
  @spec next_tab_stop(Core.t()) :: non_neg_integer()
  def next_tab_stop(buffer) do
    {x, _y} = buffer.cursor_position
    tab_stops = Map.get(buffer, :tab_stops, default_tab_stops(buffer.width))

    # Find next tab stop after current position
    tab_stops
    |> Enum.filter(fn stop -> stop > x end)
    |> Enum.min(fn -> buffer.width - 1 end)
  end

  # Private helper functions

  defp translate_with_charset(char, :ascii), do: char

  defp translate_with_charset(char, :dec_special) do
    # DEC Special Graphics character set mapping
    case char do
      "`" -> "◆"
      "a" -> "▒"
      "b" -> "␉"
      "c" -> "␌"
      "d" -> "␍"
      "e" -> "␊"
      "f" -> "°"
      "g" -> "±"
      "h" -> "␤"
      "i" -> "␋"
      "j" -> "┘"
      "k" -> "┐"
      "l" -> "┌"
      "m" -> "└"
      "n" -> "┼"
      "o" -> "⎺"
      "p" -> "⎻"
      "q" -> "─"
      "r" -> "⎼"
      "s" -> "⎽"
      "t" -> "├"
      "u" -> "┤"
      "v" -> "┴"
      "w" -> "┬"
      "x" -> "│"
      "y" -> "≤"
      "z" -> "≥"
      "{" -> "π"
      "|" -> "≠"
      "}" -> "£"
      "~" -> "·"
      _ -> char
    end
  end

  defp translate_with_charset(char, _), do: char

  defp default_tab_stops(width) do
    # Default tab stops every 8 columns
    0..(width - 1)
    |> Enum.filter(fn x -> rem(x, 8) == 0 end)
    |> MapSet.new()
  end

  # === Stub Implementations for Test Compatibility ===
  # These functions are referenced by delegations but not critical for core functionality

  @doc """
  Checks if cursor is visible (stub).
  """
  @spec cursor_visible?(Core.t()) :: boolean()
  def cursor_visible?(buffer), do: buffer.cursor_visible

  @doc """
  Checks if cursor is blinking (stub).
  """
  @spec cursor_blinking?(Core.t()) :: boolean()
  def cursor_blinking?(buffer), do: Map.get(buffer, :cursor_blink, true)

  @doc """
  Gets cursor style (stub).
  """
  @spec get_cursor_style(Core.t()) :: atom()
  def get_cursor_style(buffer), do: buffer.cursor_style

  @doc """
  Gets text style (stub).
  """
  @spec get_style(Core.t()) :: TextFormatting.text_style()
  def get_style(buffer), do: buffer.default_style

  @doc """
  Updates text style (stub).
  """
  @spec update_style(Core.t(), TextFormatting.text_style()) :: Core.t()
  def update_style(buffer, style), do: %{buffer | default_style: style}

  @doc """
  Gets foreground color (stub).
  """
  @spec get_foreground(Core.t()) :: atom() | tuple()
  def get_foreground(buffer), do: buffer.default_style.foreground

  @doc """
  Gets background color (stub).
  """
  @spec get_background(Core.t()) :: atom() | tuple()
  def get_background(buffer), do: buffer.default_style.background

  @doc """
  Starts selection (stub).
  """
  @spec start_selection(Core.t(), non_neg_integer(), non_neg_integer()) ::
          Core.t()
  def start_selection(buffer, x, y), do: %{buffer | selection: {x, y, nil, nil}}

  @doc """
  Updates selection (stub).
  """
  @spec update_selection(Core.t(), non_neg_integer(), non_neg_integer()) ::
          Core.t()
  def update_selection(buffer, x, y) do
    case buffer.selection do
      {sx, sy, _, _} -> %{buffer | selection: {sx, sy, x, y}}
      nil -> buffer
    end
  end

  @doc """
  Clears selection (stub).
  """
  @spec clear_selection(Core.t()) :: Core.t()
  def clear_selection(buffer), do: %{buffer | selection: nil}

  @doc """
  Gets selection (stub).
  """
  @spec get_selection(Core.t()) ::
          {integer(), integer(), integer(), integer()} | nil
  def get_selection(buffer), do: buffer.selection

  @doc """
  Gets selection start (stub).
  """
  @spec get_selection_start(Core.t()) :: {integer(), integer()} | nil
  def get_selection_start(buffer) do
    case buffer.selection do
      {sx, sy, _, _} -> {sx, sy}
      nil -> nil
    end
  end

  @doc """
  Gets selection end (stub).
  """
  @spec get_selection_end(Core.t()) :: {integer(), integer()} | nil
  def get_selection_end(buffer) do
    case buffer.selection do
      {_, _, nil, nil} -> nil
      {_, _, ex, ey} -> {ex, ey}
      nil -> nil
    end
  end

  @doc """
  Gets selection boundaries (stub).
  """
  @spec get_selection_boundaries(Core.t()) ::
          {integer(), integer(), integer(), integer()} | nil
  def get_selection_boundaries(buffer), do: buffer.selection

  @doc """
  Checks if position is in selection (stub).
  """
  @spec in_selection?(Core.t(), non_neg_integer(), non_neg_integer()) ::
          boolean()
  def in_selection?(buffer, x, y) do
    case buffer.selection do
      {sx, sy, ex, ey} when ex != nil and ey != nil ->
        {start_x, start_y, end_x, end_y} = normalize_selection(sx, sy, ex, ey)
        position_in_selection?(x, y, start_x, start_y, end_x, end_y)

      _ ->
        false
    end
  end

  defp normalize_selection(x1, y1, x2, y2) do
    if y1 < y2 or (y1 == y2 and x1 <= x2) do
      {x1, y1, x2, y2}
    else
      {x2, y2, x1, y1}
    end
  end

  defp position_in_selection?(x, y, start_x, start_y, end_x, end_y) do
    cond do
      y < start_y or y > end_y -> false
      y > start_y and y < end_y -> true
      y == start_y and y == end_y -> x >= start_x and x <= end_x
      y == start_y -> x >= start_x
      y == end_y -> x <= end_x
      true -> false
    end
  end

  @doc """
  Gets text in region.
  """
  @spec get_text_in_region(Core.t(), integer(), integer(), integer(), integer()) ::
          String.t()
  def get_text_in_region(buffer, x1, y1, x2, y2) do
    # Extract text from the specified region
    case buffer.cells do
      nil ->
        ""

      cells when is_list(cells) ->
        # Ensure coordinates are within bounds and properly ordered
        start_y = max(0, min(y1, y2))
        end_y = min(length(cells) - 1, max(y1, y2))

        if start_y > end_y do
          ""
        else
          cells
          |> Enum.slice(start_y..end_y)
          |> Enum.map(fn line when is_list(line) ->
            start_x = max(0, min(x1, x2))
            end_x = min(length(line) - 1, max(x1, x2))

            if start_x > end_x do
              ""
            else
              line
              |> Enum.slice(start_x..end_x)
              |> Enum.map(fn
                %{char: char} -> char
                _ -> " "
              end)
              |> Enum.join("")
              |> String.trim_trailing()
            end
          end)
          |> Enum.reject(&(&1 == ""))
          |> Enum.join("\n")
        end

      _ ->
        ""
    end
  end

  @doc """
  Checks if attribute is set (stub).
  """
  @spec attribute_set?(Core.t(), atom()) :: boolean()
  def attribute_set?(_buffer, _attr), do: false

  @doc """
  Gets set attributes (stub).
  """
  @spec get_set_attributes(Core.t()) :: list(atom())
  def get_set_attributes(_buffer), do: []

  @doc """
  Applies single shift (stub).
  """
  @spec apply_single_shift(Core.t(), integer()) :: Core.t()
  def apply_single_shift(buffer, _set), do: buffer

  @doc """
  Gets single shift state (stub).
  """
  @spec get_single_shift(Core.t()) :: integer() | nil
  def get_single_shift(_buffer), do: nil

  @doc """
  Gets current G set (stub).
  """
  @spec get_current_g_set(Core.t()) :: integer()
  def get_current_g_set(_buffer), do: 0

  @doc """
  Designates charset (stub).
  """
  @spec designate_charset(Core.t(), integer(), atom()) :: Core.t()
  def designate_charset(buffer, _set, _charset), do: buffer

  @doc """
  Gets designated charset (stub).
  """
  @spec get_designated_charset(Core.t(), integer()) :: atom()
  def get_designated_charset(_buffer, _set), do: :us

  @doc """
  Invokes G set (stub).
  """
  @spec invoke_g_set(Core.t(), integer()) :: Core.t()
  def invoke_g_set(buffer, _set), do: buffer

  @doc """
  Resets all attributes to defaults (stub).
  """
  @spec reset_all_attributes(Core.t()) :: Core.t()
  def reset_all_attributes(buffer) do
    %{buffer | default_style: TextFormatting.default_style()}
  end

  @doc """
  Resets specific attribute (stub).
  """
  @spec reset_attribute(Core.t(), atom()) :: Core.t()
  def reset_attribute(buffer, _attr), do: buffer

  @doc """
  Resets charset state (stub).
  """
  @spec reset_charset_state(Core.t()) :: Core.t()
  def reset_charset_state(buffer), do: buffer

  @doc """
  Checks if selection is active (stub).
  """
  @spec selection_active?(Core.t()) :: boolean()
  def selection_active?(buffer), do: buffer.selection != nil

  @doc """
  Sets specific attribute (stub).
  """
  @spec set_attribute(Core.t(), atom()) :: Core.t()
  def set_attribute(buffer, _attr), do: buffer

  @doc """
  Sets background color (stub).
  """
  @spec set_background(Core.t(), atom() | tuple()) :: Core.t()
  def set_background(buffer, color) do
    style = Map.put(buffer.default_style, :background, color)
    %{buffer | default_style: style}
  end

  @doc """
  Sets cursor visibility (stub).
  """
  @spec set_cursor_visibility(Core.t(), boolean()) :: Core.t()
  def set_cursor_visibility(buffer, visible) do
    %{buffer | cursor_visible: visible}
  end

  @doc """
  Sets foreground color (stub).
  """
  @spec set_foreground(Core.t(), atom() | tuple()) :: Core.t()
  def set_foreground(buffer, color) do
    style = Map.put(buffer.default_style, :foreground, color)
    %{buffer | default_style: style}
  end
end
