defmodule ExJsonSchema.Validator.Type do
  @moduledoc """
  `ExJsonSchema.Validator` implementation for `"type"` attributes.

  See:
  https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.2
  https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-6.25
  https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
  """

  alias ExJsonSchema.Validator
  alias ExJsonSchema.Validator.Error

  @behaviour ExJsonSchema.Validator

  @impl ExJsonSchema.Validator
  def validate(%{version: version}, _, {"type", type}, data, _) do
    do_validate(version, type, data)
  end

  def validate(_, _, _, _, _) do
    []
  end

  @spec do_validate(
          version :: non_neg_integer,
          type :: ExJsonSchema.data(),
          data :: ExJsonSchema.data()
        ) :: Validator.errors()
  defp do_validate(version, type, data) do
    if valid?(version, type, data) do
      []
    else
      [%Error{error: %Error.Type{expected: List.wrap(type), actual: data_type(data)}}]
    end
  end

  defp valid?(_, "number", data), do: is_number(data)
  defp valid?(_, "array", data), do: is_list(data)
  defp valid?(_, "object", data), do: is_map(data)
  defp valid?(_, "null", data), do: is_nil(data)
  defp valid?(_, "boolean", data), do: is_boolean(data)
  defp valid?(_, "string", data), do: is_binary(data)
  defp valid?(_, "integer", data) when is_integer(data), do: true
  defp valid?(4, "integer", _), do: false

  defp valid?(version, "integer", data) when version >= 6 do
    is_float(data) and Float.round(data) == data
  end

  defp valid?(version, type, data) when is_list(type) do
    Enum.any?(type, &valid?(version, &1, data))
  end

  defp data_type(nil), do: "null"
  defp data_type(data) when is_binary(data), do: "string"
  defp data_type(data) when is_boolean(data), do: "boolean"
  defp data_type(data) when is_integer(data), do: "integer"
  defp data_type(data) when is_list(data), do: "array"
  defp data_type(data) when is_map(data), do: "object"
  defp data_type(data) when is_number(data), do: "number"
  defp data_type(_), do: "unknown"
end
