Skip to main content

lib/pote/format/ansi.ex

defmodule Pote.Format.ANSI do
  @moduledoc """
  ANSI format for basic terminal colours.

  **Deprecated**: Use `Pote.ColorInfo.nearest_basic_color/1` instead for finding
  the nearest basic color, and `Pote.ColorInfo.to_ansi/1` for ANSI escape codes.
  This module will be removed in a future version.
  """

  @behaviour Pote.Format

  alias Pote.Conversions
  alias Pote.Colors.Basic

  @type parsed :: atom()

  @spec parse(any()) :: {:ok, parsed()} | :error
  @spec valid?(any()) :: boolean()
  @spec to_rgb(parsed()) :: {0..255, 0..255, 0..255}
  @spec to_hex(parsed()) :: String.t()
  @spec to_argb(parsed()) :: {0..255, 0..255, 0..255, 0..255}
  @spec to_hsl(parsed()) :: {float(), float(), float()}
  @spec to_hsv(parsed()) :: {float(), float(), float()}
  @spec to_cmyk(parsed()) :: {float(), float(), float(), float()}
  @spec to_xterm256(parsed()) :: non_neg_integer()
  @spec name(parsed()) :: String.t() | nil
  @spec from_rgb({0..255, 0..255, 0..255}) :: parsed()
  @spec info(parsed()) :: map()

  @basic_colors %{
    black: {0, 0, 0},
    red: {255, 0, 0},
    green: {0, 255, 0},
    yellow: {255, 255, 0},
    blue: {0, 0, 255},
    magenta: {255, 0, 255},
    cyan: {0, 255, 255},
    white: {255, 255, 255},
    bright_black: {128, 128, 128},
    bright_red: {255, 128, 128},
    bright_green: {128, 255, 128},
    bright_yellow: {255, 255, 128},
    bright_blue: {128, 128, 255},
    bright_magenta: {255, 128, 255},
    bright_cyan: {128, 255, 255},
    bright_white: {255, 255, 255},
    # Extended colors (common aliases)
    orange: {255, 165, 0},
    purple: {128, 0, 128},
    pink: {255, 192, 203},
    violet: {238, 130, 238},
    indigo: {75, 0, 130},
    teal: {0, 128, 128},
    lime: {0, 255, 0},
    navy: {0, 0, 128},
    maroon: {128, 0, 0},
    olive: {128, 128, 0},
    aqua: {0, 255, 255},
    fuchsia: {255, 0, 255},
    silver: {192, 192, 192},
    gray: {128, 128, 128},
    grey: {128, 128, 128}
  }

  def parse(color) when is_atom(color) do
    if Map.has_key?(@basic_colors, color) do
      {:ok, color}
    else
      :error
    end
  end

  def parse(color) when is_binary(color), do: parse_binary_color(color)
  def parse(_), do: :error

  defp parse_binary_color(color) do
    atom_color = String.to_existing_atom(color)
    if Map.has_key?(@basic_colors, atom_color), do: {:ok, atom_color}, else: :error
  rescue
    ArgumentError -> :error
  end

  def valid?(color) when is_atom(color), do: Map.has_key?(@basic_colors, color)
  def valid?(color) when is_binary(color), do: valid_binary_color?(color)
  def valid?(_), do: false

  defp valid_binary_color?(color) do
    atom_color = String.to_existing_atom(color)
    Map.has_key?(@basic_colors, atom_color)
  rescue
    ArgumentError -> false
  end

  def to_rgb(color) do
    Map.get(@basic_colors, color, {128, 128, 128})
  end

  def to_hex(color) do
    color |> to_rgb() |> Conversions.rgb_to_hex()
  end

  def to_argb(color) do
    {r, g, b} = to_rgb(color)
    {255, r, g, b}
  end

  def to_hsl(color) do
    color |> to_rgb() |> Conversions.rgb_to_hsl()
  end

  def to_hsv(color) do
    color |> to_rgb() |> Conversions.rgb_to_hsv()
  end

  def to_cmyk(color) do
    color |> to_rgb() |> Conversions.rgb_to_cmyk()
  end

  def to_xterm256(color) do
    # Mapeo básico de ANSI a XTerm
    ansi_to_xterm = %{
      black: 0,
      red: 1,
      green: 2,
      yellow: 3,
      blue: 4,
      magenta: 5,
      cyan: 6,
      white: 7,
      bright_black: 8,
      bright_red: 9,
      bright_green: 10,
      bright_yellow: 11,
      bright_blue: 12,
      bright_magenta: 13,
      bright_cyan: 14,
      bright_white: 15,
      # Extended colors - approximate XTerm256 equivalents
      orange: 208,
      purple: 128,
      pink: 217,
      violet: 177,
      indigo: 62,
      teal: 44,
      lime: 10,
      navy: 18,
      maroon: 88,
      olive: 142,
      aqua: 50,
      fuchsia: 201,
      silver: 250,
      gray: 240,
      grey: 240
    }

    Map.get(ansi_to_xterm, color, 245)
  end

  def name(color), do: color

  def from_rgb(rgb) do
    Enum.min_by(Basic.basic_colors(), fn {_name, color_rgb} ->
      Conversions.color_distance(rgb, color_rgb)
    end)
    |> elem(0)
  end

  def info(parsed) do
    %{
      format: :ansi,
      original: parsed,
      parsed: parsed,
      rgb: to_rgb(parsed),
      hex: to_hex(parsed),
      argb: to_argb(parsed),
      hsl: to_hsl(parsed),
      hsv: to_hsv(parsed),
      cmyk: to_cmyk(parsed),
      xterm256: to_xterm256(parsed),
      name: name(parsed)
    }
  end
end