lib/image/options/new.ex

defmodule Image.Options.New do
  @moduledoc """
  Options for new images.

  """
  alias Image.{Color, BandFormat, Interpretation}

  @type t :: [
          {:bands, pos_integer()}
          | {:format, Image.BandFormat.t()}
          | {:interpretation, Image.Interpretation.t()}
          | {:color, float() | Image.pixel()}
          | {:x_res, number()}
          | {:y_res, number()}
          | {:x_offset, number()}
          | {:y_offset, number()}
        ]

  @default_bands 3

  def default_options do
    [
      format: {:u, 8},
      interpretation: :srgb,
      color: 0,
      x_res: 0,
      y_res: 0,
      x_offset: 0,
      y_offset: 0
    ]
  end

  @doc """
  Validate the options for `Image.new/2`.

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

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

        options ->
          {:ok, options}
      end

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

      other ->
        other
    end
  end

  defp validate_option({:format, format}, options) when is_tuple(format) do
    case BandFormat.image_format_from_nx(format) do
      {:ok, format} ->
        {:cont, Keyword.put(options, :format, format)}

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

  defp validate_option({:format, format}, options) when is_atom(format) do
    case BandFormat.nx_format(format) do
      {:ok, _nx_type} ->
        {:cont, options}

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

  defp validate_option({:interpretation, interpretation}, options) do
    case Interpretation.validate_interpretation(interpretation) do
      {:ok, interpretation} ->
        {:cont, Keyword.put(options, :interpretation, interpretation)}

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

  @numeric_options [:x_res, :y_res, :x_offset, :y_offset, :bands]
  defp validate_option({option, value}, options)
       when option in @numeric_options and is_number(value) and value >= 0 do
    {:cont, options}
  end

  defp validate_option({:color, color}, options) 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({option, value}, _options) do
    {:halt, {:error, invalid_option(option, value)}}
  end

  def set_default_bands(%{bands: _bands} = options) do
    options
  end

  def set_default_bands(%{color: color} = options) when is_integer(color) do
    Map.put(options, :bands, @default_bands)
  end

  def set_default_bands(%{color: color} = options) when is_list(color) do
    Map.put(options, :bands, length(color))
  end

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

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

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