defmodule Absinthe.Type.Custom do
  use Absinthe.Schema.Notation

  @moduledoc """
  This module contains the following additional data types:
  - datetime (UTC)
  - naive_datetime
  - date
  - time
  - decimal (only if [Decimal](https://hex.pm/packages/decimal) is available)

  Further description of these types can be found in the source code.

  To use: `import_types Absinthe.Type.Custom`.
  """

  scalar :datetime, name: "DateTime" do
    description """
    The `DateTime` scalar type represents a date and time in the UTC
    timezone. The DateTime appears in a JSON response as an ISO8601 formatted
    string, including UTC timezone ("Z"). The parsed date and time string will
    be converted to UTC if there is an offset.
    """

    serialize &DateTime.to_iso8601/1
    parse &parse_datetime/1
  end

  scalar :naive_datetime, name: "NaiveDateTime" do
    description """
    The `Naive DateTime` scalar type represents a naive date and time without
    timezone. The DateTime appears in a JSON response as an ISO8601 formatted
    string.
    """

    serialize &NaiveDateTime.to_iso8601/1
    parse &parse_naive_datetime/1
  end

  scalar :date do
    description """
    The `Date` scalar type represents a date. The Date appears in a JSON
    response as an ISO8601 formatted string, without a time component.
    """

    serialize &Date.to_iso8601/1
    parse &parse_date/1
  end

  scalar :time do
    description """
    The `Time` scalar type represents a time. The Time appears in a JSON
    response as an ISO8601 formatted string, without a date component.
    """

    serialize &Time.to_iso8601/1
    parse &parse_time/1
  end

  if Code.ensure_loaded?(Decimal) do
    scalar :decimal do
      description """
      The `Decimal` scalar type represents signed double-precision fractional
      values parsed by the `Decimal` library. The Decimal appears in a JSON
      response as a string to preserve precision.
      """

      serialize &Absinthe.Type.Custom.Decimal.serialize/1
      parse &Absinthe.Type.Custom.Decimal.parse/1
    end
  end

  @spec parse_datetime(Absinthe.Blueprint.Input.String.t()) :: {:ok, DateTime.t()} | :error
  @spec parse_datetime(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
  defp parse_datetime(%Absinthe.Blueprint.Input.String{value: value}) do
    case DateTime.from_iso8601(value) do
      {:ok, datetime, _} -> {:ok, datetime}
      _error -> :error
    end
  end

  defp parse_datetime(%Absinthe.Blueprint.Input.Null{}) do
    {:ok, nil}
  end

  defp parse_datetime(_) do
    :error
  end

  @spec parse_naive_datetime(Absinthe.Blueprint.Input.String.t()) ::
          {:ok, NaiveDateTime.t()} | :error
  @spec parse_naive_datetime(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
  defp parse_naive_datetime(%Absinthe.Blueprint.Input.String{value: value}) do
    case NaiveDateTime.from_iso8601(value) do
      {:ok, naive_datetime} -> {:ok, naive_datetime}
      _error -> :error
    end
  end

  defp parse_naive_datetime(%Absinthe.Blueprint.Input.Null{}) do
    {:ok, nil}
  end

  defp parse_naive_datetime(_) do
    :error
  end

  @spec parse_date(Absinthe.Blueprint.Input.String.t()) :: {:ok, Date.t()} | :error
  @spec parse_date(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
  defp parse_date(%Absinthe.Blueprint.Input.String{value: value}) do
    case Date.from_iso8601(value) do
      {:ok, date} -> {:ok, date}
      _error -> :error
    end
  end

  defp parse_date(%Absinthe.Blueprint.Input.Null{}) do
    {:ok, nil}
  end

  defp parse_date(_) do
    :error
  end

  @spec parse_time(Absinthe.Blueprint.Input.String.t()) :: {:ok, Time.t()} | :error
  @spec parse_time(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
  defp parse_time(%Absinthe.Blueprint.Input.String{value: value}) do
    case Time.from_iso8601(value) do
      {:ok, time} -> {:ok, time}
      _error -> :error
    end
  end

  defp parse_time(%Absinthe.Blueprint.Input.Null{}) do
    {:ok, nil}
  end

  defp parse_time(_) do
    :error
  end
end
