if !Code.ensure_loaded?(Comp) do
  defmodule Comp do
    @moduledoc """
    Provides utilities to implement and work with `Comparable` types
    """

    @type left :: term
    @type right :: term

    defmacro gt, do: :gt
    defmacro lt, do: :lt
    defmacro eq, do: :eq

    @doc """
    Helper to define ordering relation for pair of types,
    accepts two `term :: type` pairs
    and block of code where relation is described.

    ## Examples

        iex> quote do
        ...>   use Comp
        ...>   defmodule Foo do
        ...>     defstruct [:value, :meta]
        ...>   end
        ...>   defmodule Bar do
        ...>     defstruct [:value, :meta]
        ...>   end
        ...>   defcomparable %Foo{value: left} :: Foo, %Foo{value: right} :: Foo do
        ...>     Comp.compare(left, right)
        ...>   end
        ...>   defcomparable %Foo{value: left} :: Foo, %Bar{value: right} :: Bar do
        ...>     Comp.compare(left, right)
        ...>   end
        ...>   defcomparable %Foo{value: left} :: Foo, right :: Integer do
        ...>     Comp.compare(left, right)
        ...>   end
        ...> end
        ...> |> Code.compile_quoted
        iex> quote do
        ...>   x = %Foo{value: 1, meta: 1}
        ...>   y = %Foo{value: 1, meta: 2}
        ...>   Comp.equal?(x, y) && Comp.equal?(y, x)
        ...> end
        ...> |> Code.eval_quoted
        ...> |> elem(0)
        true
        iex> quote do
        ...>   x = %Foo{value: 1, meta: 1}
        ...>   y = %Bar{value: 1, meta: 2}
        ...>   Comp.equal?(x, y) && Comp.equal?(y, x)
        ...> end
        ...> |> Code.eval_quoted
        ...> |> elem(0)
        true
        iex> quote do
        ...>   x = %Foo{value: 1, meta: 1}
        ...>   y = 1
        ...>   Comp.equal?(x, y) && Comp.equal?(y, x)
        ...> end
        ...> |> Code.eval_quoted
        ...> |> elem(0)
        true
    """
    defmacro defcomparable(
               {:"::", _, [left_expression, quoted_left_type]},
               {:"::", _, [right_expression, quoted_right_type]},
               do: code
             ) do
      {left_type, []} = Code.eval_quoted(quoted_left_type, [], __CALLER__)

      {right_type, []} = Code.eval_quoted(quoted_right_type, [], __CALLER__)

      lr_type =
        [Comparable, Type, left_type, To, right_type]
        |> Module.concat()

      rl_type =
        [Comparable, Type, right_type, To, left_type]
        |> Module.concat()

      lr_impl =
        quote do
          defmodule unquote(lr_type) do
            @fields [:left, :right]
            @enforce_keys @fields
            defstruct @fields
          end

          defimpl Comparable, for: unquote(lr_type) do
            def compare(%unquote(lr_type){
                  left: unquote(left_expression),
                  right: unquote(right_expression)
                }) do
              unquote(code)
            end
          end
        end

      if lr_type == rl_type do
        lr_impl
      else
        quote do
          unquote(lr_impl)

          defmodule unquote(rl_type) do
            @fields [:left, :right]
            @enforce_keys @fields
            defstruct @fields
          end

          defimpl Comparable, for: unquote(rl_type) do
            def compare(%unquote(rl_type){
                  left: unquote(right_expression),
                  right: unquote(left_expression)
                }) do
              unquote(code)
            end
          end
        end
      end
    end

    @doc """
    Is left term equal to right term?

    ## Examples

        iex> Comp.equal?(1, 1)
        true
        iex> Comp.equal?(1, :hello)
        false
    """
    @spec equal?(left, right) :: boolean
    def equal?(left, right) do
      left
      |> new(right)
      |> Comparable.compare() == eq()
    end

    @doc """
    Is left term not equal to right term?

    ## Examples

        iex> Comp.not_equal?(1, 1)
        false
        iex> Comp.not_equal?(1, :hello)
        true
    """
    @spec not_equal?(left, right) :: boolean
    def not_equal?(left, right) do
      left
      |> new(right)
      |> Comparable.compare() != eq()
    end

    @doc """
    Is left term greater than right term?

    ## Examples

        iex> Comp.greater_than?(1, 1)
        false
        iex> Comp.greater_than?(1, 2)
        false
        iex> Comp.greater_than?(2, 1)
        true
    """
    @spec greater_than?(left, right) :: boolean
    def greater_than?(left, right) do
      left
      |> new(right)
      |> Comparable.compare() == gt()
    end

    @doc """
    Is left term less than right term?

    ## Examples

        iex> Comp.less_than?(1, 1)
        false
        iex> Comp.less_than?(1, 2)
        true
        iex> Comp.less_than?(2, 1)
        false
    """
    @spec less_than?(left, right) :: boolean
    def less_than?(left, right) do
      left
      |> new(right)
      |> Comparable.compare() == lt()
    end

    @doc """
    Is left term greater or equal to right term?

    ## Examples

        iex> Comp.greater_or_equal?(1, 1)
        true
        iex> Comp.greater_or_equal?(1, 2)
        false
        iex> Comp.greater_or_equal?(2, 1)
        true
    """
    @spec greater_or_equal?(left, right) :: boolean
    def greater_or_equal?(left, right) do
      left
      |> new(right)
      |> Comparable.compare() != lt()
    end

    @doc """
    Is left term less or equal to right term?

    ## Examples

        iex> Comp.less_or_equal?(1, 1)
        true
        iex> Comp.less_or_equal?(1, 2)
        true
        iex> Comp.less_or_equal?(2, 1)
        false
    """
    @spec less_or_equal?(left, right) :: boolean
    def less_or_equal?(left, right) do
      left
      |> new(right)
      |> Comparable.compare() != gt()
    end

    @doc """
    Returns the biggest of the two given terms, if terms are equal - then the first one is returned

    ## Examples

        iex> Comp.max(1, 1)
        1
        iex> Comp.max(1, 2)
        2
        iex> Comp.max(2, 1)
        2
    """
    @spec max(left, right) :: left | right
    def max(left, right) do
      left
      |> new(right)
      |> Comparable.compare()
      |> case do
        gt() -> left
        lt() -> right
        eq() -> left
      end
    end

    @doc """
    Returns the smallest of the two given terms, if terms are equal - then the first one is returned

    ## Examples

        iex> Comp.min(1, 1)
        1
        iex> Comp.min(1, 2)
        1
        iex> Comp.min(2, 1)
        1
    """
    @spec min(left, right) :: left | right
    def min(left, right) do
      left
      |> new(right)
      |> Comparable.compare()
      |> case do
        gt() -> right
        lt() -> left
        eq() -> left
      end
    end

    def type_of(v) when is_atom(v), do: Atom
    def type_of(v) when is_bitstring(v), do: BitString
    def type_of(v) when is_float(v), do: Float
    def type_of(v) when is_function(v), do: Function
    def type_of(v) when is_integer(v), do: Integer
    def type_of(v) when is_pid(v), do: PID
    def type_of(v) when is_port(v), do: Port
    def type_of(v) when is_reference(v), do: Reference
    def type_of(v) when is_tuple(v), do: Tuple
    def type_of(v) when is_list(v), do: List
    def type_of(%t{}), do: t
    def type_of(v) when is_map(v), do: Map

    @doc """
    Compare left and right term

    ## Examples

        iex> Comp.compare(1, 2)
        :lt
        iex> Comp.compare(2, 1)
        :gt
        iex> Comp.compare(1, 1)
        :eq
    """
    @spec compare(left, right) :: Comparable.ord()
    def compare(left, right) do
      left
      |> new(right)
      |> Comparable.compare()
    end

    defp new(left, right) do
      lr_type =
        try do
          [Comparable, Type, type_of(left), To, type_of(right)]
          |> Module.safe_concat()
        rescue
          ArgumentError ->
            [Comparable, Type, Any, To, Any]
            |> Module.safe_concat()
        end

      %{__struct__: lr_type, left: left, right: right}
    end
  end
end
