defmodule Raxol.Terminal.Renderer do
  @moduledoc """
  Terminal renderer module.

  This module handles rendering of terminal output using ANSI escape codes,
  including:
  - Character cell rendering
  - Text styling (colors, bold, italic, underline)
  - Cursor rendering
  - Performance optimizations (style batching, caching)

  ## Integration with Other Modules

  The Renderer module works closely with several specialized modules:

  ### Manipulation Module
  - Receives text and style updates from the Manipulation module
  - Renders text with proper styling and positioning
  - Handles text insertion, deletion, and modification

  ### Selection Module
  - Renders text selections with visual highlighting
  - Supports multiple selections
  - Handles selection state changes

  ### Validation Module
  - Renders validation errors and warnings
  - Applies visual indicators for invalid input
  - Shows validation state through styling

  ## Performance Optimizations

  The renderer includes several optimizations:
  - Only renders changed cells
  - Batches style updates for consecutive cells
  - Minimizes terminal output
  - Caches rendered output when possible

  ## Usage

  ```elixir
  # Create a new renderer
  buffer = ScreenBuffer.new(80, 24)
  renderer = Renderer.new(buffer)

  # Render with selection
  selection = %{selection: {0, 0, 0, 5}}
  output = Renderer.render(renderer, selection: selection)

  # Render with validation
  validation = Validation.validate_input(buffer, 0, 0, "text")
  output = Renderer.render(renderer, validation: validation)
  ```
  """

  alias Raxol.Terminal.ScreenBuffer

  @type t :: %__MODULE__{
          screen_buffer: ScreenBuffer.t(),
          cursor: {non_neg_integer(), non_neg_integer()} | nil,
          theme: map(),
          font_settings: map(),
          style_cache: map()
        }

  defstruct [
    :screen_buffer,
    :cursor,
    :theme,
    :font_settings,
    :style_batching,
    style_cache: %{}
  ]

  require Logger

  # ANSI escape code constants
  @ansi_reset "\e[0m"
  @ansi_bold "\e[1m"
  @ansi_italic "\e[3m"
  @ansi_underline "\e[4m"

  # Standard foreground color codes
  @fg_color_codes %{
    black: "30",
    red: "31",
    green: "32",
    yellow: "33",
    blue: "34",
    magenta: "35",
    cyan: "36",
    white: "37",
    bright_black: "90",
    bright_red: "91",
    bright_green: "92",
    bright_yellow: "93",
    bright_blue: "94",
    bright_magenta: "95",
    bright_cyan: "96",
    bright_white: "97"
  }

  # Standard background color codes
  @bg_color_codes %{
    black: "40",
    red: "41",
    green: "42",
    yellow: "43",
    blue: "44",
    magenta: "45",
    cyan: "46",
    white: "47",
    bright_black: "100",
    bright_red: "101",
    bright_green: "102",
    bright_yellow: "103",
    bright_blue: "104",
    bright_magenta: "105",
    bright_cyan: "106",
    bright_white: "107"
  }

  @doc """
  Creates a new renderer with the given screen buffer.

  ## Examples

      iex> screen_buffer = ScreenBuffer.new(80, 24)
      iex> renderer = Renderer.new(screen_buffer)
      iex> renderer.screen_buffer
      %ScreenBuffer{}
  """
  def new(
        screen_buffer,
        theme \\ %{},
        font_settings \\ %{},
        style_batching \\ false
      ) do
    %__MODULE__{
      screen_buffer: screen_buffer,
      cursor: nil,
      theme: theme,
      font_settings: font_settings,
      style_batching: style_batching,
      style_cache: %{}
    }
  end

  @doc """
  Renders the terminal content without additional options.
  """
  def render(%__MODULE__{} = renderer) do
    render(renderer, %{}, %{})
  end

  @doc """
  Renders the terminal content.
  """
  def render(%__MODULE__{} = renderer, opts) do
    render(renderer, opts, %{})
  end

  @doc """
  Renders the terminal content with additional options.
  """
  def render(%__MODULE__{} = renderer, _opts \\ %{}, _additional_opts \\ %{}) do
    renderer.screen_buffer
    |> get_styled_content_optimized(renderer.theme, renderer.style_batching)
    |> apply_font_settings(renderer.font_settings)
    |> maybe_apply_cursor(renderer.cursor)
  end

  defp get_styled_content_optimized(buffer, theme, style_batching) do
    buffer.cells
    |> Enum.map_join("\n", fn row ->
      render_row_optimized(row, theme, style_batching)
    end)
  end

  defp render_row_optimized(row, theme, style_batching) do
    case style_batching do
      true -> render_batched_optimized(row, theme)
      false -> render_individual_optimized(row, theme)
    end
  end

  # Batched rendering: group consecutive cells with the same style
  defp render_batched_optimized(row, theme) do
    row
    |> Enum.chunk_by(& &1.style)
    |> Enum.map_join("", fn cells_with_same_style ->
      style = hd(cells_with_same_style).style
      chars = Enum.map_join(cells_with_same_style, "", & &1.char)
      ansi_prefix = build_ansi_prefix(style, theme)

      case ansi_prefix do
        "" -> chars
        prefix -> prefix <> chars <> @ansi_reset
      end
    end)
  end

  # Individual cell rendering
  defp render_individual_optimized(row, theme) do
    row
    |> Enum.map_join("", fn cell ->
      ansi_prefix = build_ansi_prefix(cell.style, theme)

      case ansi_prefix do
        "" -> cell.char
        prefix -> prefix <> cell.char <> @ansi_reset
      end
    end)
  end

  # Build ANSI escape prefix from style and theme
  defp build_ansi_prefix(style, theme) do
    style_map = normalize_style(style)
    codes = []

    # Foreground color
    codes =
      case Map.get(style_map, :foreground) do
        nil ->
          case get_default_fg_ansi(theme) do
            nil -> codes
            code -> [code | codes]
          end

        color ->
          case resolve_fg_ansi(color, theme) do
            nil -> codes
            code -> [code | codes]
          end
      end

    # Background color
    codes =
      case Map.get(style_map, :background) do
        nil ->
          case get_default_bg_ansi(theme) do
            nil -> codes
            code -> [code | codes]
          end

        color ->
          case resolve_bg_ansi(color, theme) do
            nil -> codes
            code -> [code | codes]
          end
      end

    # Text attributes
    codes =
      if Map.get(style_map, :bold, false),
        do: [@ansi_bold | codes],
        else: codes

    codes =
      if Map.get(style_map, :italic, false),
        do: [@ansi_italic | codes],
        else: codes

    codes =
      if Map.get(style_map, :underline, false),
        do: [@ansi_underline | codes],
        else: codes

    case codes do
      [] -> ""
      _ -> codes |> Enum.reverse() |> Enum.join("")
    end
  end

  # Resolve foreground color to ANSI code
  defp resolve_fg_ansi(color, theme) when is_atom(color) do
    # Check theme first
    theme_color = get_in(theme, [:foreground, color])

    case theme_color do
      nil ->
        # Use standard ANSI color code
        case Map.get(@fg_color_codes, color) do
          nil -> nil
          code -> "\e[#{code}m"
        end

      hex when is_binary(hex) ->
        hex_to_ansi_fg(hex)
    end
  end

  defp resolve_fg_ansi(%{r: r, g: g, b: b}, _theme) do
    "\e[38;2;#{r};#{g};#{b}m"
  end

  defp resolve_fg_ansi(color, _theme)
       when is_integer(color) and color >= 0 and color <= 255 do
    "\e[38;5;#{color}m"
  end

  defp resolve_fg_ansi(color, _theme) when is_binary(color) do
    hex_to_ansi_fg(color)
  end

  defp resolve_fg_ansi(_, _), do: nil

  # Resolve background color to ANSI code
  defp resolve_bg_ansi(color, theme) when is_atom(color) do
    theme_color = get_in(theme, [:background, color])

    case theme_color do
      nil ->
        case Map.get(@bg_color_codes, color) do
          nil -> nil
          code -> "\e[#{code}m"
        end

      hex when is_binary(hex) ->
        hex_to_ansi_bg(hex)
    end
  end

  defp resolve_bg_ansi(%{r: r, g: g, b: b}, _theme) do
    "\e[48;2;#{r};#{g};#{b}m"
  end

  defp resolve_bg_ansi(color, _theme)
       when is_integer(color) and color >= 0 and color <= 255 do
    "\e[48;5;#{color}m"
  end

  defp resolve_bg_ansi(color, _theme) when is_binary(color) do
    hex_to_ansi_bg(color)
  end

  defp resolve_bg_ansi(_, _), do: nil

  # Get default foreground ANSI from theme
  defp get_default_fg_ansi(theme) do
    case get_in(theme, [:foreground, :default]) do
      nil -> nil
      hex when is_binary(hex) -> hex_to_ansi_fg(hex)
      _ -> nil
    end
  end

  # Get default background ANSI from theme
  defp get_default_bg_ansi(theme) do
    case get_in(theme, [:background, :default]) do
      nil -> nil
      hex when is_binary(hex) -> hex_to_ansi_bg(hex)
      _ -> nil
    end
  end

  # Convert hex color string to ANSI 24-bit foreground escape
  defp hex_to_ansi_fg(hex) do
    case parse_hex_color(hex) do
      {:ok, r, g, b} -> "\e[38;2;#{r};#{g};#{b}m"
      :error -> nil
    end
  end

  # Convert hex color string to ANSI 24-bit background escape
  defp hex_to_ansi_bg(hex) do
    case parse_hex_color(hex) do
      {:ok, r, g, b} -> "\e[48;2;#{r};#{g};#{b}m"
      :error -> nil
    end
  end

  # Parse "#RRGGBB" or "#RGB" hex color strings
  defp parse_hex_color("#" <> hex), do: parse_hex_digits(hex)
  defp parse_hex_color(hex) when is_binary(hex), do: parse_hex_digits(hex)

  defp parse_hex_digits(<<r::binary-size(2), g::binary-size(2), b::binary-size(2)>>) do
    with {r_val, ""} <- Integer.parse(r, 16),
         {g_val, ""} <- Integer.parse(g, 16),
         {b_val, ""} <- Integer.parse(b, 16) do
      {:ok, r_val, g_val, b_val}
    else
      _ -> :error
    end
  end

  defp parse_hex_digits(<<r::binary-size(1), g::binary-size(1), b::binary-size(1)>>) do
    with {r_val, ""} <- Integer.parse(r <> r, 16),
         {g_val, ""} <- Integer.parse(g <> g, 16),
         {b_val, ""} <- Integer.parse(b <> b, 16) do
      {:ok, r_val, g_val, b_val}
    else
      _ -> :error
    end
  end

  defp parse_hex_digits(_), do: :error

  defp normalize_style(%{__struct__: _} = style) do
    Map.from_struct(style)
  end

  defp normalize_style(style) when is_map(style) do
    style
  end

  defp normalize_style(_style) do
    %{}
  end

  defp apply_font_settings(content, _font_settings), do: content
  defp maybe_apply_cursor(content, nil), do: content
  defp maybe_apply_cursor(content, _cursor), do: content

  @doc """
  Sets the cursor position.

  ## Examples

      iex> screen_buffer = ScreenBuffer.new(80, 24)
      iex> renderer = Renderer.new(screen_buffer)
      iex> renderer = Renderer.set_cursor(renderer, {10, 5})
      iex> renderer.cursor
      {10, 5}
  """
  def set_cursor(%__MODULE__{} = renderer, position) do
    %{renderer | cursor: position}
  end

  @doc """
  Clears the cursor position.

  ## Examples

      iex> screen_buffer = ScreenBuffer.new(80, 24)
      iex> renderer = Renderer.new(screen_buffer)
      iex> renderer = Renderer.set_cursor(renderer, {10, 5})
      iex> renderer = Renderer.clear_cursor(renderer)
      iex> renderer.cursor
      nil
  """
  def clear_cursor(%__MODULE__{} = renderer) do
    %{renderer | cursor: nil}
  end

  @doc """
  Updates the theme settings.

  ## Examples

      iex> screen_buffer = ScreenBuffer.new(80, 24)
      iex> renderer = Renderer.new(screen_buffer)
      iex> theme = %{foreground: %{default: "#FFF"}}
      iex> renderer = Renderer.set_theme(renderer, theme)
      iex> renderer.theme
      %{foreground: %{default: "#FFF"}}
  """
  def set_theme(%__MODULE__{} = renderer, theme) do
    %{renderer | theme: theme}
  end

  @doc """
  Updates the font settings.

  ## Examples

      iex> screen_buffer = ScreenBuffer.new(80, 24)
      iex> renderer = Renderer.new(screen_buffer)
      iex> settings = %{family: "Fira Code"}
      iex> renderer = Renderer.set_font_settings(renderer, settings)
      iex> renderer.font_settings
      %{family: "Fira Code"}
  """
  def set_font_settings(%__MODULE__{} = renderer, settings) do
    %{renderer | font_settings: settings}
  end

  @doc """
  Starts a new renderer process.
  """
  def start_link(opts \\ []) do
    screen_buffer = Keyword.get(opts, :screen_buffer, ScreenBuffer.new(80, 24))
    theme = Keyword.get(opts, :theme, %{})
    font_settings = Keyword.get(opts, :font_settings, %{})

    renderer = new(screen_buffer, theme, font_settings)
    {:ok, renderer}
  end

  @doc """
  Stops the renderer process.
  """
  def stop(_renderer) do
    :ok
  end

  @doc """
  Gets the current content of the screen buffer.

  ## Parameters
    * `renderer` - The renderer to get content from
    * `opts` - Options for content retrieval
      * `:include_style` - Whether to include style information (default: false)
      * `:include_cursor` - Whether to include cursor position (default: false)

  ## Returns
    * `{:ok, content}` - The current content
    * `{:error, reason}` - If content retrieval fails

  ## Examples
      iex> get_content(renderer)
      {:ok, "Hello, World!"}
  """
  def get_content(renderer, opts \\ [])

  def get_content(%__MODULE__{} = renderer, opts) do
    _include_style = Keyword.get(opts, :include_style, true)
    include_cursor = Keyword.get(opts, :include_cursor, true)

    content =
      renderer.screen_buffer
      |> ScreenBuffer.get_content()

    apply_cursor_option(content, renderer.cursor, include_cursor)
  end

  # Handle ScreenBuffer structs (updated after buffer consolidation)
  def get_content(%Raxol.Terminal.ScreenBuffer{} = buffer, opts) do
    _include_style = Keyword.get(opts, :include_style, false)
    include_cursor = Keyword.get(opts, :include_cursor, false)

    content =
      buffer
      |> Raxol.Terminal.ScreenBuffer.get_lines()
      |> Enum.map_join("\n", &Enum.join/1)
      |> maybe_add_cursor(buffer.cursor_position, include_cursor)

    {:ok, content}
  end

  # Handle buffer manager PIDs (legacy support - deprecated after buffer consolidation)
  def get_content(manager_pid, _opts) when is_pid(manager_pid) do
    {:error, :deprecated_buffer_manager}
  end

  defp apply_cursor_option(content, cursor, true) do
    content |> maybe_add_cursor(cursor, true)
  end

  defp apply_cursor_option(content, _cursor, false), do: content

  defp maybe_add_cursor(content, nil, _include_cursor), do: content
  defp maybe_add_cursor(content, cursor, true), do: {content, cursor}
  defp maybe_add_cursor(content, _cursor, false), do: content
end
