lib/image/options/draw.ex

defmodule Image.Options.Draw do
  @moduledoc """
  Options and options validation for the
  drawing functionss.

  """

  alias Image.Color
  alias Image.CombineMode

  @type circle :: [
          {:fill, boolean()}
          | {:color, Color.t()}
        ]

  @type rect :: [
          {:fill, boolean()}
          | {:color, Color.t()}
          | {:stroke_width, pos_integer()}
        ]

  @type point :: [
          {:color, Color.t()}
        ]

  @type flood :: [
          {:equal, boolean()}
          | {:color, Color.t()}
        ]

  @type mask :: [
          {:color, Color.t()}
        ]

  @type line :: [
          {:color, Color.t()}
        ]

  @type smudge :: []

  @type image :: [
          {:mode, CombineMode.t()}
        ]

  @doc false
  def default_options(:circle) do
    [
      color: :black,
      fill: true,
      stroke_width: 1
    ]
  end

  @doc false
  def default_options(:rect) do
    [
      color: :black,
      fill: true,
      stroke_width: 1
    ]
  end

  @doc false
  def default_options(:line) do
    [
      color: :black
    ]
  end

  @doc false
  def default_options(:point) do
    [
      color: :black
    ]
  end

  @doc false
  def default_options(:mask) do
    [
      color: :black
    ]
  end

  @doc false
  def default_options(:flood) do
    [
      color: :black,
      equal: false
    ]
  end

  @doc false
  def default_options(:image) do
    [
      mode: :VIPS_COMBINE_MODE_SET
    ]
  end

  @doc false
  def default_options(:smudge) do
    []
  end

  @doc """
  Validate the options for `Image.Draw`.

  """
  def validate_options(type, options) do
    options = Keyword.merge(default_options(type), options)

    options =
      case Enum.reduce_while(options, options, &validate_option(type, &1, &2)) do
        {:error, value} ->
          {:error, value}

        options ->
          {:ok, options}
      end

    case options do
      {:ok, options} ->
        options
        |> Map.new()
        |> wrap(:ok)

      other ->
        other
    end
  end

  defp validate_option(type, {:fill, fill}, options) when type in [:circle, :rect] do
    if fill do
      {:cont, Keyword.put(options, :fill, true)}
    else
      {:cont, Keyword.put(options, :fill, false)}
    end
  end

  defp validate_option(type, {:color, color}, options)
       when type in [:mask, :point, :circle, :rect, :line, :flood] do
    case Color.rgb_color(color) do
      {:ok, color} ->
        rgb = if Keyword.keyword?(color), do: Keyword.fetch!(color, :rgb), else: color
        {:cont, Keyword.put(options, :color, rgb)}

      {:error, reason} ->
        {:halt, {:error, reason}}
    end
  end

  defp validate_option(:flood, {:equal, equal}, options) do
    if equal do
      {:cont, Keyword.put(options, :equal, true)}
    else
      {:cont, Keyword.put(options, :equal, false)}
    end
  end

  defp validate_option(:image, {:mode, mode}, options) do
    case Image.CombineMode.validate(mode) do
      {:ok, mode} ->
        {:cont, Keyword.put(options, :mode, mode)}

      {:error, reason} ->
        {:halt, {:error, reason}}
    end
  end

  defp validate_option(type, {:stroke_width, stroke_width}, options)
       when type in [:rect, :circle]
       when is_integer(stroke_width) and stroke_width > 0 do
    {:cont, options}
  end

  defp validate_option(type, {option, value}, _options) do
    {:halt, {:error, invalid_option(type, option, value)}}
  end

  @doc false
  def invalid_option(type, option) do
    "Invalid option or option value for draw_#{type}: #{inspect(option)}"
  end

  @doc false
  def invalid_option(type, option, value) do
    "Invalid option or option value for draw_#{type}: #{option}: #{inspect(value)}"
  end

  defp wrap(term, atom) do
    {atom, term}
  end
end