alias Raxol.Style.Colors.Color

defmodule Raxol.Style.Colors.Gradient do
  @moduledoc """
  Creates and manages color gradients for terminal applications.

  This module provides functionality for creating gradients between colors and
  applying them to text, creating visually striking terminal effects.

  ## Examples

  ```elixir
  # Create a simple linear gradient between red and blue
  alias Raxol.Style.Colors.{Color, Gradient}

  red = Color.from_hex("#FF0000")
  blue = Color.from_hex("#0000FF")
  gradient = Gradient.linear(red, blue, 10)

  # Apply the gradient to text
  colored_text = Gradient.apply_to_text(gradient, "Hello, World!")

  # Create a rainbow gradient
  rainbow = Gradient.rainbow(20)
  rainbow_text = Gradient.apply_to_text(rainbow, "Rainbow Text")

  # Create a multi-stop gradient
  colors = [
    Color.from_hex("#FF0000"),  # Red
    Color.from_hex("#FFFF00"),  # Yellow
    Color.from_hex("#00FF00")   # Green
  ]
  multi = Gradient.multi_stop(colors, 15)
  ```
  """

  defstruct [
    # List of color stops
    :colors,
    # Number of discrete steps
    :steps,
    # Linear, radial, etc.
    :type
  ]

  @type gradient_type :: :linear | :rainbow | :heat_map | :multi_stop

  @type t :: %__MODULE__{
          colors: [Color.t()],
          steps: non_neg_integer(),
          type: gradient_type()
        }

  @doc """
  Creates a linear gradient between two colors.

  ## Parameters

  - `start_color` - The starting color
  - `end_color` - The ending color
  - `steps` - The number of color steps in the gradient (including start and end)

  ## Examples

      iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
      iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
      iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 5)
      iex> length(gradient.colors)
      5
  """
  def linear(%Color{} = start_color, %Color{} = end_color, steps)
      when is_integer(steps) and steps >= 2 do
    colors = generate_gradient_colors(start_color, end_color, steps)

    %__MODULE__{
      colors: colors,
      steps: steps,
      type: :linear
    }
  end

  @doc """
  Creates a multi-stop gradient with multiple color stops.

  ## Parameters

  - `color_stops` - A list of colors to transition between
  - `steps` - The total number of color steps in the gradient

  ## Examples

      iex> colors = [
      ...>   Raxol.Style.Colors.Color.from_hex("#FF0000"),
      ...>   Raxol.Style.Colors.Color.from_hex("#00FF00"),
      ...>   Raxol.Style.Colors.Color.from_hex("#0000FF")
      ...> ]
      iex> gradient = Raxol.Style.Colors.Gradient.multi_stop(colors, 10)
      iex> length(gradient.colors)
      10
  """
  def multi_stop(color_stops, steps)
      when is_list(color_stops) and length(color_stops) >= 2 and
             is_integer(steps) and steps >= 2 do
    # Calculate how many steps to allocate for each segment
    segment_count = length(color_stops) - 1

    # For even distribution, we need to calculate how many steps per segment
    # We add segment_count - 1 to account for the shared points between segments
    segments_steps = distribute_steps(steps, segment_count)

    # Generate the colors for each segment
    colors =
      color_stops
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.zip(segments_steps)
      |> Enum.flat_map(fn {[start_color, end_color], segment_steps} ->
        # Generate colors for this segment, excluding the last one (except for the final segment)
        segment_colors =
          generate_gradient_colors(start_color, end_color, segment_steps)

        handle_segment_end(
          end_color == List.last(color_stops),
          segment_colors
        )
      end)

    %__MODULE__{
      colors: colors,
      steps: steps,
      type: :multi_stop
    }
  end

  @doc """
  Creates a rainbow gradient with the given number of steps.

  ## Parameters

  - `steps` - The number of color steps in the rainbow

  ## Examples

      iex> gradient = Raxol.Style.Colors.Gradient.rainbow(6)
      iex> length(gradient.colors)
      6
  """
  def rainbow(steps) when is_integer(steps) and steps >= 2 do
    # Create a list of rainbow colors
    rainbow_colors = [
      # Red
      Color.from_hex("#FF0000"),
      # Orange
      Color.from_hex("#FF7F00"),
      # Yellow
      Color.from_hex("#FFFF00"),
      # Green
      Color.from_hex("#00FF00"),
      # Blue
      Color.from_hex("#0000FF"),
      # Indigo
      Color.from_hex("#4B0082"),
      # Violet
      Color.from_hex("#9400D3")
    ]

    # Create a multi-stop gradient with these colors
    multi_stop(rainbow_colors, steps)
    |> Map.put(:type, :rainbow)
  end

  @doc """
  Creates a heat map gradient from cool to hot colors.

  ## Parameters

  - `steps` - The number of color steps in the heat map

  ## Examples

      iex> gradient = Raxol.Style.Colors.Gradient.heat_map(5)
      iex> length(gradient.colors)
      5
  """
  def heat_map(steps) when is_integer(steps) and steps >= 2 do
    # Create a list of heat map colors from cool to hot
    heat_colors = [
      # Blue (coldest)
      Color.from_hex("#0000FF"),
      # Cyan
      Color.from_hex("#00FFFF"),
      # Green
      Color.from_hex("#00FF00"),
      # Yellow
      Color.from_hex("#FFFF00"),
      # Red (hottest)
      Color.from_hex("#FF0000")
    ]

    # Create a multi-stop gradient with these colors
    multi_stop(heat_colors, steps)
    |> Map.put(:type, :heat_map)
  end

  @doc """
  Gets the color at a specific position in the gradient.

  ## Parameters

  - `gradient` - The gradient to sample from
  - `position` - A value between 0.0 and 1.0 representing the position in the gradient

  ## Examples

      iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
      iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
      iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 5)
      iex> color = Raxol.Style.Colors.Gradient.at_position(gradient, 0.5)
      iex> color.hex
      "#800080"  # Purple (mix of red and blue)
  """
  def at_position(%__MODULE__{colors: colors}, position)
      when is_float(position) and position >= 0.0 and position <= 1.0 do
    # Determine the index based on position
    index = calculate_position_index(position == 1.0, colors, position)

    Enum.at(colors, index)
  end

  @doc """
  Reverses the direction of a gradient.

  ## Parameters

  - `gradient` - The gradient to reverse

  ## Examples

      iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
      iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
      iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 3)
      iex> reversed = Raxol.Style.Colors.Gradient.reverse(gradient)
      iex> hd(reversed.colors).hex
      "#0000FF"
  """
  def reverse(%__MODULE__{colors: colors} = gradient) do
    %{gradient | colors: Enum.reverse(colors)}
  end

  @doc """
  Applies a gradient to text, returning an ANSI-formatted string.

  ## Parameters

  - `gradient` - The gradient to apply
  - `text` - The text to colorize

  ## Examples

      iex> red = Raxol.Style.Colors.Color.from_hex("#FF0000")
      iex> blue = Raxol.Style.Colors.Color.from_hex("#0000FF")
      iex> gradient = Raxol.Style.Colors.Gradient.linear(red, blue, 5)
      iex> Raxol.Style.Colors.Gradient.apply_to_text(gradient, "Hello")
      "\e[38;2;255;0;0mH\e[0m\e[38;2;191;0;64me\e[0m\e[38;2;128;0;128ml\e[0m\e[38;2;64;0;191ml\e[0m\e[38;2;0;0;255mo\e[0m"
  """
  def apply_to_text(%__MODULE__{colors: colors}, text) when is_binary(text) do
    # Split the text into graphemes
    graphemes = String.graphemes(text)

    # Calculate how to distribute colors across characters
    {chars_per_color, remainder} = distribute_colors(colors, graphemes)

    # Combine colors with text
    colored_chars =
      combine_colors_with_text(colors, graphemes, chars_per_color, remainder)

    # Join the colored characters
    Enum.join(colored_chars)
  end

  # Also provide to_ansi_sequence as an alias for apply_to_text for API compatibility
  @doc """
  Alias for apply_to_text/2.
  """
  def to_ansi_sequence(gradient, text), do: apply_to_text(gradient, text)

  # Private functions

  # Generate interpolated colors between start and end
  defp generate_gradient_colors(start_color, _end_color, 1), do: [start_color]

  defp generate_gradient_colors(start_color, end_color, steps) do
    0..(steps - 1)
    |> Enum.map(fn step ->
      # Calculate the interpolation factor
      factor = step / (steps - 1)
      # Interpolate between start and end colors
      interpolate_color(start_color, end_color, factor)
    end)
  end

  # Interpolate between two colors
  defp interpolate_color(
         %Color{r: r1, g: g1, b: b1},
         %Color{r: r2, g: g2, b: b2},
         factor
       ) do
    r = round(r1 + (r2 - r1) * factor)
    g = round(g1 + (g2 - g1) * factor)
    b = round(b1 + (b2 - b1) * factor)

    Color.from_rgb(r, g, b)
  end

  # Distribute steps across segments
  defp distribute_steps(total_steps, segment_count) do
    # Calculate the number of intervals to distribute
    total_intervals = max(0, total_steps - 1)

    # Calculate base intervals per segment
    base_intervals = div(total_intervals, segment_count)
    # Calculate remaining intervals
    remainder_intervals = rem(total_intervals, segment_count)

    # Distribute intervals, giving one extra to segments until remainder is used
    # Then add 1 to get the number of *colors* needed for each segment's generation
    Enum.map(1..segment_count, fn segment_index ->
      intervals_for_segment =
        calculate_segment_intervals(
          segment_index <= remainder_intervals,
          base_intervals
        )

      # Need intervals + 1 colors to cover the intervals
      intervals_for_segment + 1
    end)
  end

  # Distribute colors across text
  defp distribute_colors(colors, graphemes) do
    color_count = length(colors)
    char_count = length(graphemes)

    handle_color_distribution(
      color_count >= char_count,
      color_count,
      char_count
    )
  end

  # Combine colors with text characters
  defp combine_colors_with_text(colors, graphemes, chars_per_color, remainder) do
    # If we have more colors than characters, just use the first n colors
    handle_color_text_combination(
      {chars_per_color == 1, remainder == 0},
      colors,
      graphemes,
      chars_per_color,
      remainder
    )
  end

  # Apply a color to a text string
  defp colorize_text(text, %Color{r: r, g: g, b: b}) do
    # Format as true-color ANSI escape sequence
    "\e[38;2;#{r};#{g};#{b}m#{text}\e[0m"
  end

  # Pattern matching helper functions for refactored if statements

  # Handle segment end decision instead of if statement
  defp handle_segment_end(true, segment_colors), do: segment_colors

  defp handle_segment_end(false, segment_colors),
    do: Enum.drop(segment_colors, -1)

  # Calculate position index instead of if statement
  defp calculate_position_index(true, colors, _position), do: length(colors) - 1

  defp calculate_position_index(false, colors, position),
    do: trunc(position * length(colors))

  # Calculate segment intervals instead of if statement
  defp calculate_segment_intervals(true, base_intervals), do: base_intervals + 1
  defp calculate_segment_intervals(false, base_intervals), do: base_intervals

  # Handle color distribution instead of if statement
  defp handle_color_distribution(true, _color_count, _char_count) do
    # If we have more or equal colors than characters, we can assign one color per character
    {1, 0}
  end

  defp handle_color_distribution(false, color_count, char_count) do
    # Otherwise, calculate how many characters per color
    chars_per_color = div(char_count, color_count)
    remainder = rem(char_count, color_count)
    {chars_per_color, remainder}
  end

  # Handle color text combination instead of if statement
  defp handle_color_text_combination(
         {true, true},
         colors,
         graphemes,
         _chars_per_color,
         _remainder
       ) do
    Enum.zip(graphemes, colors)
    |> Enum.map(fn {char, color} -> colorize_text(char, color) end)
  end

  defp handle_color_text_combination(
         _condition,
         colors,
         graphemes,
         chars_per_color,
         remainder
       ) do
    # Distribute colors across characters
    colors
    |> Enum.with_index()
    |> Enum.flat_map(fn {color, index} ->
      # Calculate how many characters for this color
      extra = calculate_extra_chars(index < remainder)
      count = chars_per_color + extra

      # Calculate the starting position in the graphemes list
      start_pos = index * chars_per_color + min(index, remainder)

      # Extract the characters for this color
      chars = Enum.slice(graphemes, start_pos, count)

      # Apply color to each character
      Enum.map(chars, fn char -> colorize_text(char, color) end)
    end)
  end

  # Calculate extra characters instead of if statement
  defp calculate_extra_chars(true), do: 1
  defp calculate_extra_chars(false), do: 0
end
