defmodule JsonSchema.Parser.UnionParser do
  @behaviour JsonSchema.Parser.ParserBehaviour
  @moduledoc """
  Parses a JSON schema union type:
      {
        "type": ["number", "integer", "null"]
      }
  Into an `JsonSchema.Types.UnionType`.
  """
  require Logger
  alias JsonSchema.{Parser, Types}
  alias Parser.{ErrorUtil, ParserResult, Util}
  alias Types.UnionType
  @doc """
  Returns true if the json subschema represents a union type.
  ## Examples
  iex> type?(%{})
  false
  iex> type?(%{"type" => ["number", "integer", "string"]})
  true
  """
  @impl JsonSchema.Parser.ParserBehaviour
  @spec type?(Types.schemaNode()) :: boolean
  def type?(%{"type" => types}) when is_list(types), do: true
  def type?(_schema_node), do: false
  @doc """
  Parses a JSON schema union type into an `JsonSchema.Types.UnionType`.
  """
  @impl JsonSchema.Parser.ParserBehaviour
  @spec parse(map, URI.t(), URI.t(), URI.t(), String.t()) ::
          ParserResult.t()
  def parse(%{"type" => types} = schema_node, _parent_id, id, path, name) do
    description = Map.get(schema_node, "description")
    default = Map.get(schema_node, "default")
    unknown_type =
      types
      |> Enum.find(fn type ->
        type not in [
          "null",
          "boolean",
          "number",
          "integer",
          "string",
          "array",
          "object"
        ]
      end)
    errors =
      cond do
        unknown_type != nil ->
          [ErrorUtil.unknown_union_type(path, unknown_type)]
        default != nil && not default_value_has_proper_type?(default, types) ->
          [ErrorUtil.invalid_type(path, "default", to_string(types), default)]
        true ->
          []
      end
    union_type = %UnionType{
      name: name,
      description: description,
      default: default,
      path: path,
      types: types |> Enum.map(&value_type_from_string/1)
    }
    union_type
    |> Util.create_type_dict(path, id)
    |> ParserResult.new([], errors)
  end
  @spec value_type_from_string(String.t()) :: UnionType.value_type()
  defp value_type_from_string(type) do
    case type do
      "null" -> :null
      "boolean" -> :boolean
      "integer" -> :integer
      "number" -> :number
      "string" -> :string
    end
  end
  @spec default_value_has_proper_type?(UnionType.default_value(), [String.t()]) ::
          boolean
  defp default_value_has_proper_type?(default, types) do
    default_type = Util.get_type(default)
    Enum.member?(types, default_type)
  end
end