lib/needle_uid.ex

defmodule Needle.UID do
  @moduledoc "./README.md" |> File.stream!() |> Enum.drop(1) |> Enum.join()
  use Ecto.ParameterizedType
  import Untangle, except: [dump: 3]

  @pride_enabled Code.ensure_loaded?(Pride) and Application.compile_env(:needle_uid, :pride_enabled, false)
  @ulid_enabled Application.compile_env(:needle_uid, :ulid_enabled, true)

  @doc "translates alphanumerics into a sentinel ID value"
  def synthesise!(x) when is_binary(x) do
    # TODO with UUID
    Needle.ULID.synthesise!(x)
  end

  @doc """
  The underlying schema type.
  """
  @impl true
  def type(_params), do: :uuid

  @impl true
  def init(opts) do
    if @pride_enabled do
      if Keyword.get(opts, :prefix) do
        IO.warn("use prefixed UUIDv7 for #{inspect(opts)}")
        Pride.init(opts)
      else
        IO.warn("fallback to ULID for #{inspect(opts)}")
        Pride.init(opts ++ [allow_unprefixed: true])
      end
    else
      Map.new(opts)
    end
  end

  @impl true
  def equal?(a, b, params \\ nil)

  if @pride_enabled do
    def equal?(a, b, %{prefix: _} = params), do: Pride.equal?(a, b, params)
  end

  if @ulid_enabled do
    def equal?(a, b, _), do: Needle.ULID.equal?(a, b)
  end

  @impl true
  def embed_as(format, params \\ nil)

  if @pride_enabled do
    def embed_as(format, %{prefix: _} = params), do: Pride.embed_as(format, params)
  end

  if @ulid_enabled do
    def embed_as(format, _), do: Needle.ULID.embed_as(format)
  end

  @impl true
  def autogenerate(params), do: generate(params)
  def generate(params_or_timestamp \\ nil)

  if @pride_enabled do
    def generate(%{prefix: _} = params), do: Pride.autogenerate(params)
  end

  if @ulid_enabled do
    def generate(timestamp) when is_integer(timestamp), do: Needle.ULID.generate(timestamp)
    # def generate(%Date{} =timestamp), do: Needle.ULID.generate(timestamp) # TODO
    def generate(%DateTime{} =timestamp), do: Needle.ULID.generate(timestamp)
  end

  if @pride_enabled do
    def generate(schema) when is_atom(schema) and not is_nil(schema) do
      debug(schema, "gen schema")

      if function_exported?(schema, :__schema__, 1) do
        # hopefully ok to just take the first?
        case schema.__schema__(:primary_key) |> List.first() |> debug("gen primary_key first") do
          nil ->
            if @ulid_enabled do
              Needle.ULID.generate()
            end

          field ->
            generate(schema, field)
        end
      else
        if @ulid_enabled do
          Needle.ULID.generate()
        end
      end
    end
  end

  if @ulid_enabled do
    def generate(_), do: Needle.ULID.generate()
  end

  if @pride_enabled do
    def generate(schema, field) when is_atom(schema) do
      case Pride.params(schema, field) |> debug("gen prefix") do
        %{prefix: _} = params ->
          Pride.autogenerate(params)

        _ ->
          if @ulid_enabled do
            Needle.ULID.generate()
          end
      end
    end
  end

  @doc "Returns the timestamp of an encoded or unencoded UID"
  if @ulid_enabled do
    def timestamp(<<_::bytes-size(26)>> = encoded) do
      Needle.ULID.timestamp(encoded)
    end
  end

  def timestamp(encoded) do
    debug(encoded, "TODO")
    raise "#TODO for UUID"
  end

  @doc """
  Casts an encoded string to ID. Transforms outside data into runtime data.

  Used to (potentially) convert your data into an internal normalized representation which will be part of your changesets. It also used for verifying that something is actually valid input data for your type.
  """
  @impl true
  def cast(term, params \\ nil)
  def cast(nil, _params), do: {:ok, nil}

  if @pride_enabled do
    def cast(term, %{prefix: _} = params) do
      with {:error, _} <- Pride.cast(term, params) do
        # for old ULIDs in a prefixed table
        if @ulid_enabled and Needle.ULID.valid?(term) do
          {:ok, term}
        else
          {:error, message: "Not recognised as valid Prefixed UUIDv7 or ULID"}
        end
      end
    end
  end

  def cast(<<_::bytes-size(16)>> = value, _), do: {:ok, value}

  def cast(<<_::bytes-size(26)>> = value, params) do
    if @ulid_enabled and Needle.ULID.valid?(value) do
      {:ok, value}
    else
      if @pride_enabled do
        Pride.cast(value, params)
      else
        {:error, message: "Invalid ULID"}
      end
    end
  end

  if @pride_enabled do
    def cast(term, %{} = params), do: Pride.cast(term, params)
  end

  def cast(value, params) do
    debug(value, "Could not recognise value (with params: #{inspect(params)}) ")
    {:error, message: "Not recognised as valid ULID or Prefixed UUIDv7"}
  end

  @doc """
  Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
  """
  def cast!(value, params \\ nil) do
    case cast(value, params) do
      {:ok, uid} ->
        uid

      {:error, term} ->
        raise Ecto.CastError, type: __MODULE__, value: value, message: term[:message]

      :error ->
        raise Ecto.CastError, type: __MODULE__, value: value
    end
  end

  @doc """
  Converts an encoded ID into a binary. Used to get your data ready to be written to the database. Transforms anything (outside data or runtime data) into database column data
  """
  @impl true
  def dump(value, dumper \\ nil, params \\ nil)
  def dump(nil, _, _), do: {:ok, nil}

  if @pride_enabled do
    def dump(value, dumper, %{prefix: _} = params), do: Pride.dump(value, dumper, params)
  end

  if @ulid_enabled do
    def dump(<<_::bytes-size(26)>> = encoded, _, _), do: Needle.ULID.decode(encoded)
  end

  if @pride_enabled do
    def dump(value, dumper, %{} = params), do: Pride.dump(value, dumper, params)
  end

  def dump(_, _, _), do: :error

  def dump!(encoded, dumper \\ nil, params \\ nil) do
    case dump(encoded, dumper, params) do
      {:ok, uid} -> uid
      _ -> raise Ecto.CastError, type: __MODULE__, value: encoded
    end
  end

  @doc """
  Converts a binary ID into an encoded string. Transforms database column data into runtime data.
  """
  @impl true
  def load(value, loader \\ nil, params \\ nil)
  def load(nil, _, _), do: {:ok, nil}

  if @pride_enabled do
    def load(value, loader, %{prefix: _} = params), do: Pride.load(value, loader, params)
  end

  if @pride_enabled do
    def load(value, loader, %{} = params) do
      with :error <- Pride.load(value, loader, params) do
        if @ulid_enabled do
          Needle.ULID.encode(value)
        else
          :error
        end
      end
    end
  end

  if @ulid_enabled do
    def load(bytes, _, _) when is_binary(bytes) and byte_size(bytes) == 16,
      do: Needle.ULID.encode(bytes)
  end

  def load(_, _, _), do: :error

  @doc """
  Takes a string and returns true if it is a valid UUID or ULID.

  ## Examples
      iex> valid?("01J3MQ2Q4RVB1WTE3KT1D8ZNX1")
      true

      iex> valid?("550e8400-e29b-41d4-a716-446655440000")
      true

      > is_pride?("test_3TUIKuXX5mNO2jSA41bsDx") and is_uuid?("test_3TUIKuXX5mNO2jSA41bsDx")
      true

      iex> valid?("invalid_id")
      false
  """
  def valid?(str, params \\ nil) do
    is_ulid?(str) || is_uuid?(str, params)
  end

  @doc """
  Takes a string and returns true if it is a valid ULID (Universally Unique Lexicographically Sortable Identifier).

  ## Examples
      iex> is_ulid?("01J3MQ2Q4RVB1WTE3KT1D8ZNX1")
      true

      iex> is_ulid?("invalid_ulid")
      false
  """
  def is_ulid?(str) when is_binary(str) and byte_size(str) == 26 do
    Needle.ULID.valid?(str)
  end

  def is_ulid?(_), do: false

  @doc """
  Takes a string and returns true if it is a valid Object ID or Prefixed UUID (Universally Unique Identifier).

  ## Examples
      iex> is_uuid?("550e8400-e29b-41d4-a716-446655440000")
      true

      > is_pride?("test_3TUIKuXX5mNO2jSA41bsDx") and is_uuid?("test_3TUIKuXX5mNO2jSA41bsDx")
      true

      iex> is_uuid?("invalid_uuid")
      false
      
  """
  def is_uuid?(str, params \\ nil)

  def is_uuid?(str, params) when is_binary(str) and byte_size(str) == 36 do
    with {:ok, _} <- Ecto.UUID.cast(str) do
      true
    else
      _ ->
      is_pride?(str, params)
    end
  end

  def is_uuid?(str, params), do: is_pride?(str, params)

  def is_pride?(str, params \\ nil)

  def is_pride?(str, params) do
    @pride_enabled && Pride.valid?(str, params)
  end

end