defmodule Paddle.Filters do
  @moduledoc ~S"""
  Module used internally by Paddle to manipulate LDAP filters.
  """

  @type easy_filter :: keyword | %{optional(atom | binary) => binary}
  @type eldap_filter :: tuple
  @type filter :: easy_filter | eldap_filter | nil
  @type t :: filter

  @spec construct_filter(filter) :: eldap_filter

  @doc ~S"""
  Construct a eldap filter from the given keyword list or map.

  If given an `:eldap` filter (a tuple), it is returned as is.

  If given `nil`, it will return an empty filter (`:eldap.and([])`).

  Examples:

      iex> Paddle.Filters.construct_filter(uid: "testuser")
      {:equalityMatch, {:AttributeValueAssertion, 'uid', 'testuser'}}

      iex> Paddle.Filters.construct_filter(%{uid: "testuser"})
      {:equalityMatch, {:AttributeValueAssertion, 'uid', 'testuser'}}

      iex> Paddle.Filters.construct_filter(%{"uid" => "testuser"})
      {:equalityMatch, {:AttributeValueAssertion, 'uid', 'testuser'}}

      iex> Paddle.Filters.construct_filter(:eldap.substrings('uid', initial: 'b'))
      {:substrings, {:SubstringFilter, 'uid', [initial: 'b']}}

      iex> Paddle.Filters.construct_filter(nil)
      {:and, []}

      iex> Paddle.Filters.construct_filter([])
      {:and, []}

      iex> Paddle.Filters.construct_filter(uid: "testuser", cn: "Test User")
      {:and,
       [equalityMatch: {:AttributeValueAssertion, 'uid', 'testuser'},
        equalityMatch: {:AttributeValueAssertion, 'cn', 'Test User'}]}
  """
  def construct_filter(filter) when is_tuple(filter), do: filter
  def construct_filter(nil), do: :eldap.and([])

  def construct_filter(filter) when is_map(filter), do: filter
                                                        |> Enum.into([])
                                                        |> construct_filter

  def construct_filter([{key, value}]) when is_binary(value) do
    :eldap.equalityMatch('#{key}', '#{value}')
  end

  def construct_filter(kwdn) when is_list(kwdn) do
    criteria = kwdn
               |> Enum.map(fn {key, value} -> :eldap.equalityMatch('#{key}', '#{value}') end)
    :eldap.and(criteria)
  end

  @spec merge_filter(filter, filter) :: filter

  @doc ~S"""
  Merge two filters with an "and" operation.

  Examples:

      iex> Paddle.Filters.merge_filter([uid: "testuser"], [cn: "Test User"])
      {:and,
       [equalityMatch: {:AttributeValueAssertion, 'uid', 'testuser'},
        equalityMatch: {:AttributeValueAssertion, 'cn', 'Test User'}]}

      iex> Paddle.Filters.merge_filter([uid: "testuser"], :eldap.substrings('cn', [initial: 'Tes']))
      {:and,
       [equalityMatch: {:AttributeValueAssertion, 'uid', 'testuser'},
        substrings: {:SubstringFilter, 'cn', [initial: 'Tes']}]}

      iex> Paddle.Filters.merge_filter([uid: "testuser"], [])
      [uid: "testuser"]

      iex> Paddle.Filters.merge_filter([], [cn: "Test User"])
      [cn: "Test User"]

      iex> Paddle.Filters.merge_filter([], nil)
      {:and, []}
  """
  def merge_filter(lhs, rhs)

  for lhs <- [[], nil], rhs <- [[], nil] do
    def merge_filter(unquote(lhs), unquote(rhs)), do: :eldap.and([])
  end

  for null_filter <- [[], nil] do
    def merge_filter(filter, unquote(null_filter)), do: filter
    def merge_filter(unquote(null_filter), filter), do: filter
  end

  def merge_filter({:and, lcond}, {:and, rcond}), do: {:and, lcond ++ rcond}
  def merge_filter({:and, lcond}, rhs), do: {:and, [construct_filter(rhs) | lcond]}
  def merge_filter(lhs, {:and, rcond}), do: {:and, [construct_filter(lhs) | rcond]}

  def merge_filter(lhs, rhs) do
    :eldap.and([construct_filter(lhs), construct_filter(rhs)])
  end

  @spec class_filter([binary]) :: eldap_filter

  @doc ~S"""
  Construct a filter that matches a list of objectClasses.

  Examples:

      iex> Paddle.Filters.class_filter ["posixAccount", "account"]
      {:and,
       [equalityMatch: {:AttributeValueAssertion, 'objectClass', 'posixAccount'},
        equalityMatch: {:AttributeValueAssertion, 'objectClass', 'account'}]}
  """
  def class_filter(classes) when is_list(classes) do
    classes
    |> Enum.map(&:eldap.equalityMatch('objectClass', '#{&1}'))
    |> :eldap.and
  end

  def class_filter(class), do: :eldap.equalityMatch('objectClass', '#{class}')

end
