defmodule Ash.CiString do
  @moduledoc """
  Represents a case insensitive string

  While some data layers are aware of case insensitive string types, in order for values
  of this type to be used in other parts of Ash Framework, it has to be embedded in a module
  this allows us to implement the `Comparable` protocol for it.

  For the type implementation, see `Ash.Type.CiString`
  """

  defstruct [:string, casted?: false, case: nil]

  @type casing :: nil | :lower | :upper

  @type t :: %__MODULE__{
          string: String.t(),
          casted?: boolean(),
          case: casing()
        }
  @type string_type :: t() | String.t()

  @doc "Creates a case insensitive string"
  @spec sigil_i(string_type() | nil, charlist()) :: t()
  def sigil_i(value, mods) do
    cond do
      ?l in mods ->
        new(value, :lower)

      ?u in mods ->
        new(value, :upper)

      true ->
        new(value)
    end
  end

  defimpl Jason.Encoder do
    def encode(ci_string, opts) do
      ci_string
      |> Ash.CiString.value()
      |> Jason.Encode.string(opts)
    end
  end

  defimpl String.Chars do
    def to_string(ci_string) do
      Ash.CiString.value(ci_string)
    end
  end

  defimpl Inspect do
    import Inspect.Algebra

    def inspect(%Ash.CiString{string: string}, opts) do
      concat(["#Ash.CiString<", to_doc(string, opts), ">"])
    end
  end

  @doc """
  Returns a Ash.CiString from a String, or `nil` if the value is `nil`.
  """
  @spec new(string_type() | nil, casing()) :: t() | nil
  def new(value, casing \\ nil)

  def new(nil, _), do: nil

  def new(%__MODULE__{string: value}, casing) do
    new(value, casing)
  end

  def new(value, casing) do
    case casing do
      :upper ->
        %Ash.CiString{casted?: true, string: value && String.upcase(value)}

      :lower ->
        %Ash.CiString{casted?: true, string: value && String.downcase(value)}

      nil ->
        %Ash.CiString{casted?: false, string: value}
    end
  end

  @doc """
  Converts a `Ash.CiString` into a normal Elixir String.
  """
  @spec value(t()) :: String.t()
  def value(%Ash.CiString{string: value, casted?: false, case: :lower}) do
    value && String.downcase(value)
  end

  def value(%Ash.CiString{string: value, casted?: false, case: :upper}) do
    value && String.upcase(value)
  end

  def value(%Ash.CiString{string: value}) do
    value
  end

  @doc """
  Compares an Elixir String or Ash.CiString. It will return `:eq` if equal, `:lt`, if
  the string is ordered alphabetically before the second string, and `:gt` if after.
  """
  @spec compare(string_type(), string_type()) :: :gt | :lt | :eq
  def compare(left, right) do
    do_compare(to_comparable_string(left), to_comparable_string(right))
  end

  defp do_compare(left, right) when left == right, do: :eq
  defp do_compare(left, right) when left < right, do: :lt
  defp do_compare(_, _), do: :gt

  @doc "Returns the downcased value, only downcasing if it hasn't already been done"
  @spec to_comparable_string(string_type() | nil) :: String.t() | nil
  def to_comparable_string(value) when is_binary(value) do
    String.downcase(value)
  end

  def to_comparable_string(%__MODULE__{case: :lower, casted?: true, string: value}) do
    value
  end

  def to_comparable_string(%__MODULE__{string: value}) do
    value && String.downcase(value)
  end

  def to_comparable_string(nil), do: nil
end

import Ash.Type.Comparable

defcomparable left :: Ash.CiString, right :: BitString do
  Ash.CiString.compare(left, right)
end

defcomparable left :: Ash.CiString, right :: Ash.CiString do
  Ash.CiString.compare(left, right)
end
