lib/qr_nbu/renderer.ex

defmodule QRNBU.Renderer do
  @moduledoc """
  QR code rendering functionality for NBU payment QR codes.

  This module provides functions to convert NBU QR code strings into various visual formats:
  - PNG images (binary data)
  - SVG graphics (XML string) with optional logo overlay
  - Terminal output (Unicode blocks)

  Uses the `eqrcode` library for QR code generation with configurable error correction levels.

  ## Examples

      # Generate and render as PNG
      {:ok, qr_string} = QRNBU.generate(:v001, payment_data)
      {:ok, png_binary} = QRNBU.Renderer.to_png(qr_string)
      File.write!("payment.png", png_binary)

      # Generate and render as SVG (with UAH logo by default)
      {:ok, svg_string} = QRNBU.Renderer.to_svg(qr_string)
      File.write!("payment.svg", svg_string)

      # Generate SVG without logo
      {:ok, svg_string} = QRNBU.Renderer.to_svg(qr_string, logo: false)

      # Display in terminal
      QRNBU.Renderer.to_terminal(qr_string)

  ## Error Correction Levels

  QR codes support four error correction levels (from lowest to highest):
  - `:l` (L) - ~7% error recovery
  - `:m` (M) - ~15% error recovery (default)
  - `:q` (Q) - ~25% error recovery
  - `:h` (H) - ~30% error recovery

  Higher error correction levels create larger QR codes but are more resistant to damage.
  When a logo is embedded (SVG only), error correction is automatically set to `:h` for
  maximum damage tolerance.

  ## Logo Support (SVG Only)

  SVG output supports embedding a logo in the center of the QR code:
  - `logo: true` - Embed UAH currency sign (default)
  - `logo: false` - No logo
  - `logo: "/path/to/logo.svg"` - Custom SVG logo from file
  - `logo: {:svg, "<svg>...</svg>"}` - Inline SVG string

  PNG output does not support logo overlays.
  """

  alias QRNBU.Renderer.Logo

  @type qr_string :: String.t()
  @type error_correction :: :l | :m | :q | :h
  @type png_binary :: binary()
  @type svg_string :: String.t()
  @type logo_option :: Logo.logo_option()

  @doc """
  Renders an NBU QR code string as a PNG image.

  ## Parameters

  - `qr_string` - The QR code string from `QRNBU.generate/2`
  - `opts` - Optional keyword list:
    - `:error_correction` - Error correction level (default: `:medium`)
    - `:width` - Image width in pixels (default: 300)

  ## Returns

  - `{:ok, png_binary}` - PNG image as binary data
  - `{:error, reason}` - Error message

  ## Examples

      iex> {:ok, qr} = QRNBU.generate(:v001, %{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> })
      iex> {:ok, png} = QRNBU.Renderer.to_png(qr)
      iex> is_binary(png) and byte_size(png) > 0
      true

      iex> {:ok, png} = QRNBU.Renderer.to_png("test", width: 500, error_correction: :h)
      iex> is_binary(png)
      true
  """
  @spec to_png(qr_string(), keyword()) :: {:ok, png_binary()} | {:error, String.t()}
  def to_png(qr_string, opts \\ []) when is_binary(qr_string) do
    error_correction = Keyword.get(opts, :error_correction, :m)
    width = Keyword.get(opts, :width, 300)

    with {:ok, qr_code} <- create_qr_code(qr_string, error_correction) do
      png_binary = EQRCode.png(qr_code, width: width)
      {:ok, png_binary}
    end
  rescue
    e -> {:error, "PNG rendering failed: #{Exception.message(e)}"}
  end

  @doc """
  Renders an NBU QR code string as an SVG image.

  ## Parameters

  - `qr_string` - The QR code string from `QRNBU.generate/2`
  - `opts` - Optional keyword list:
    - `:error_correction` - Error correction level (default: `:m`, auto-upgraded to `:h` when logo is enabled)
    - `:width` - SVG width (default: 300)
    - `:logo` - Logo option (default: `true`):
      - `true` - Embed UAH currency sign
      - `false` - No logo
      - `"/path/to/logo.svg"` - Custom SVG logo from file
      - `{:svg, "<svg>...</svg>"}` - Inline SVG string

  ## Returns

  - `{:ok, svg_string}` - SVG XML as string
  - `{:error, reason}` - Error message

  ## Examples

      iex> {:ok, qr} = QRNBU.generate(:v001, %{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> })
      iex> {:ok, svg} = QRNBU.Renderer.to_svg(qr)
      iex> String.contains?(svg, "<svg")
      true

      iex> {:ok, svg} = QRNBU.Renderer.to_svg("test", width: 400, logo: false)
      iex> String.contains?(svg, "width=\\"400.0\\"")
      true

      iex> {:ok, svg} = QRNBU.Renderer.to_svg("test", logo: true)
      iex> String.contains?(svg, "uah-logo")
      true
  """
  @spec to_svg(qr_string(), keyword()) :: {:ok, svg_string()} | {:error, String.t()}
  def to_svg(qr_string, opts \\ []) when is_binary(qr_string) do
    logo_option = Keyword.get(opts, :logo, true)
    width = Keyword.get(opts, :width, 300)

    # Auto-upgrade error correction to :h when logo is enabled for better scanability
    error_correction =
      if logo_enabled?(logo_option) do
        Keyword.get(opts, :error_correction, :h)
      else
        Keyword.get(opts, :error_correction, :m)
      end

    with {:ok, qr_code} <- create_qr_code(qr_string, error_correction),
         svg_string <- EQRCode.svg(qr_code, width: width),
         {:ok, svg_with_logo} <- Logo.embed(svg_string, logo_option, width) do
      {:ok, svg_with_logo}
    end
  rescue
    e -> {:error, "SVG rendering failed: #{Exception.message(e)}"}
  end

  @spec logo_enabled?(Logo.logo_option()) :: boolean()
  defp logo_enabled?(false), do: false
  defp logo_enabled?(_), do: true

  @doc """
  Renders an NBU QR code string to the terminal using Unicode block characters.

  Displays the QR code directly in the terminal using filled and empty Unicode blocks.

  ## Parameters

  - `qr_string` - The QR code string from `QRNBU.generate/2`
  - `opts` - Optional keyword list:
    - `:error_correction` - Error correction level (default: `:medium`)

  ## Returns

  - `:ok` - Successfully printed to terminal
  - `{:error, reason}` - Error message

  ## Examples

      iex> {:ok, qr} = QRNBU.generate(:v001, %{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> })
      iex> QRNBU.Renderer.to_terminal(qr)
      :ok
  """
  @spec to_terminal(qr_string(), keyword()) :: :ok | {:error, String.t()}
  def to_terminal(qr_string, opts \\ []) when is_binary(qr_string) do
    error_correction = Keyword.get(opts, :error_correction, :m)

    with {:ok, qr_code} <- create_qr_code(qr_string, error_correction) do
      terminal_output = EQRCode.render(qr_code)
      IO.puts(terminal_output)
      :ok
    end
  rescue
    e -> {:error, "Terminal rendering failed: #{Exception.message(e)}"}
  end

  @doc """
  Generates and saves an NBU QR code as a PNG file.

  Convenience function that combines generation and PNG rendering.

  ## Parameters

  - `version` - QR code version (`:v001`, `:v002`, or `:v003`)
  - `data` - Payment data map
  - `file_path` - Path to save the PNG file
  - `opts` - Optional rendering options (see `to_png/2`)

  ## Returns

  - `:ok` - File successfully saved
  - `{:error, reason}` - Error message

  ## Examples

      iex> QRNBU.Renderer.save_png(:v001, %{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> }, "/tmp/payment.png")
      :ok
  """
  @spec save_png(QRNBU.version(), QRNBU.qr_data(), Path.t(), keyword()) ::
          :ok | {:error, String.t()}
  def save_png(version, data, file_path, opts \\ []) do
    with {:ok, qr_string} <- QRNBU.generate(version, data),
         {:ok, png_binary} <- to_png(qr_string, opts),
         :ok <- File.write(file_path, png_binary) do
      :ok
    else
      {:error, reason} when is_binary(reason) -> {:error, reason}
      {:error, reason} -> {:error, "Failed to save PNG: #{inspect(reason)}"}
    end
  end

  @doc """
  Generates and saves an NBU QR code as an SVG file.

  Convenience function that combines generation and SVG rendering.
  By default, embeds the UAH currency sign logo in the center.

  ## Parameters

  - `version` - QR code version (`:v001`, `:v002`, or `:v003`)
  - `data` - Payment data map
  - `file_path` - Path to save the SVG file
  - `opts` - Optional rendering options (see `to_svg/2`), including:
    - `:logo` - Logo option (default: `true` for UAH sign)

  ## Returns

  - `:ok` - File successfully saved
  - `{:error, reason}` - Error message

  ## Examples

      iex> QRNBU.Renderer.save_svg(:v002, %{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів",
      ...>   amount: Decimal.new("100.50")
      ...> }, "/tmp/payment.svg")
      :ok
  """
  @spec save_svg(QRNBU.version(), QRNBU.qr_data(), Path.t(), keyword()) ::
          :ok | {:error, String.t()}
  def save_svg(version, data, file_path, opts \\ []) do
    with {:ok, qr_string} <- QRNBU.generate(version, data),
         {:ok, svg_string} <- to_svg(qr_string, opts),
         :ok <- File.write(file_path, svg_string) do
      :ok
    else
      {:error, reason} when is_binary(reason) -> {:error, reason}
      {:error, reason} -> {:error, "Failed to save SVG: #{inspect(reason)}"}
    end
  end

  @doc """
  Generates and displays an NBU QR code in the terminal.

  Convenience function that combines generation and terminal rendering.

  ## Parameters

  - `version` - QR code version (`:v001`, `:v002`, or `:v003`)
  - `data` - Payment data map
  - `opts` - Optional rendering options (see `to_terminal/2`)

  ## Returns

  - `:ok` - Successfully displayed
  - `{:error, reason}` - Error message

  ## Examples

      iex> QRNBU.Renderer.display(:v001, %{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> })
      :ok
  """
  @spec display(QRNBU.version(), QRNBU.qr_data(), keyword()) :: :ok | {:error, String.t()}
  def display(version, data, opts \\ []) do
    with {:ok, qr_string} <- QRNBU.generate(version, data),
         :ok <- to_terminal(qr_string, opts) do
      :ok
    end
  end

  # Private helpers

  @spec create_qr_code(qr_string(), error_correction()) :: {:ok, EQRCode.Matrix.t()}
  defp create_qr_code(qr_string, error_correction) do
    # EQRCode.encode/2 returns a matrix directly, not a tuple
    qr_code = EQRCode.encode(qr_string, error_correction)
    {:ok, qr_code}
  end
end