lib/cldr/utils/map.ex

defmodule Cldr.Map do
  @moduledoc """
  Functions for transforming maps, keys and values.
  """

  @doc """
  Returns am argument unchanged.

  Useful when a `noop` function is required.
  """
  def identity(x), do: x

  @default_deep_map_options [level: 1..1_000_000, filter: [], reject: [], skip: [], only: [], except: []]
  @starting_level 1
  @max_level 1_000_000

  @doc """
  Recursively traverse a map and invoke a function
  that transforms the mapfor each key/value pair.

  ## Arguments

  * `map` is any `t:map/0`

  * `function` is a `1-arity` function or function reference that
    is called for each key/value pair of the provided map. It can
    also be a 2-tuple of the form `{key_function, value_function}`

    * In the case where `function` is a single function it will be
      called with the 2-tuple argument `{key, value}`

    * In the case where function is of the form `{key_function, value_function}`
      the `key_function` will be called with the argument `key` and the value
      function will be called with the argument `value`

  * `options` is a keyword list of options. The default is
    `#{inspect @default_deep_map_options}`

  ## Options

  * `:level` indicates the starting (and optionally ending) levels of
    the map at which the `function` is executed. This can
    be an integer representing one `level` or a range
    indicating a range of levels. The default is `1..#{@max_level}`

  * `:only` is a term or list of terms or a `check function`. If it is a term
    or list of terms, the `function` is only called if the `key` of the
    map is equal to the term or in the list of terms. If `:only` is a
    `check function` then the `check function` is passed the `{k, v}` of
    the current branch in the `map`. It is expected to return a `truthy`
    value that if `true` signals that the argument `function` will be executed.

  * `:except` is a term or list of terms or a `check function`. If it is a term
    or list of terms, the `function` is only called if the `key` of the
    map is not equal to the term or not in the list of terms. If `:except` is a
    `check function` then the `check function` is passed the `{k, v}` of
    the current branch in the `map`. It is expected to return a `truthy`
    value that if `true` signals that the argument `function` will not be executed.

  * `:filter` is a term or list of terms or a `check function`. If the
    `key` currently being processed equals the term (or is in the list of
    terms, or the `check_function` returns a truthy value) then this branch
    of the map is processed by `function` and its output is included in the result.

  * `:reject` is a term or list of terms or a `check function`. If the
    `key` currently being processed equals the term (or is in the list of
    terms, or the `check_function` returns a truthy value) then this branch
    of the map is omitted from the mapped output.

  * `:skip` is a term or list of terms or a `check function`. If the
    `key` currently being processed equals the term (or is in the list of
    terms, or the `check_function` returns a truthy value) then this branch
    of the map is *not* processed by `function` but it is included in
    the mapped result.

  ## Notes

  * `:only` and `:except` operate on individual keys whereas `:filter`
    and `:filter` and `:reject` operator on *entire branches* of a map

  * If both the options `:only` and `:except` are provided then the `function`
    is called only when a `term` meets both criteria. That means that `:except`
    has priority over `:only`.

  * If both the options `:filter` and `:reject` are provided then `:reject`
    has priority over `:filter`.

  ## Returns

  * The `map` transformed by the recursive application of
    `function`

  ## Examples

      iex> map = %{a: :a, b: %{c: :c}}
      iex> fun = fn
      ...>   {k, v} when is_atom(k) -> {Atom.to_string(k), v}
      ...>   other -> other
      ...> end
      iex> Cldr.Map.deep_map map, fun
      %{"a" => :a, "b" => %{"c" => :c}}
      iex> map = %{a: :a, b: %{c: :c}}
      iex> Cldr.Map.deep_map map, fun, only: :c
      %{a: :a, b: %{"c" => :c}}
      iex> Cldr.Map.deep_map map, fun, except: [:a, :b]
      %{a: :a, b: %{"c" => :c}}
      iex> Cldr.Map.deep_map map, fun, level: 2
      %{a: :a, b: %{"c" => :c}}

  """
  @spec deep_map(
          map() | list(),
          function :: function() | {function(), function()},
          options :: list()
        ) ::
          map() | list()

  def deep_map(map, function, options \\ @default_deep_map_options)

  # Don't deep map structs since they have atom keys anyway and they
  # also don't support enumerable
  def deep_map(%_struct{} = map, _function, _options) when is_map(map) do
    map
  end

  def deep_map(map_or_list, function, options)
      when (is_map(map_or_list) or is_list(map_or_list)) and is_list(options) do
    options = validate_options(function, options)
    deep_map(map_or_list, function, options, @starting_level)
  end

  defp deep_map(nil, _fun, _options, _level) do
    nil
  end

  # If the level is greater than the return
  # just return the map or list
  defp deep_map(map_or_list, _function, %{level: %{last: last}}, level) when level > last do
    map_or_list
  end

  # If the level is less than the range then keep recursing
  # without executing the function
  defp deep_map(map, function, %{level: %{first: first}} = options, level)
       when is_map(map) and level < first do
    Enum.map(map, fn
      {k, v} when is_map(v) or is_list(v) ->
        {k, deep_map(v, function, options, level + 1)}

      {k, v} ->
        {k, v}
    end)
    |> Map.new()
  end

  # Here we are in range so we conditionally execute the function
  # if the options for `:only` and `:except` are matched
  defp deep_map(map, function, options, level) when is_map(map) and is_function(function) do
    Enum.reduce(map, [], fn
      {k, v}, acc when is_map(v) or is_list(v) ->
        case process_type({k, v}, options) do
          :continue ->
            v = deep_map(v, function, Map.put(options, :filtering, true), level + 1)
            [{k, v} | acc]
          :process ->
            v = deep_map(v, function, Map.put(options, :filtering, true), level + 1)
            [function.({k, v}) | acc]
          :except ->
            v = deep_map(v, function, options, level + 1)
            [{k, v} | acc]
          :skip ->
            [function.({k, v}) | acc]
          :reject ->
            acc
        end

      {k, v}, acc ->
        case process_type({k, v}, options) do
          :continue ->
            [{k, v} | acc]
          :process ->
            [function.({k, v}) | acc]
          :except ->
            [{k, v} | acc]
          :skip ->
            [function.({k, v}) | acc]
          :reject ->
            acc
        end
    end)
    |> Map.new()
  end

  defp deep_map(map, {key_function, value_function}, options, level) when is_map(map) do
    Enum.reduce(map, [], fn
      {k, v}, acc when is_map(v) or is_list(v) ->
        case process_type({k, v}, options) do
          :continue ->
            v = deep_map(v, {key_function, value_function}, Map.put(options, :filtering, true), level + 1)
            [{k, v} | acc]
          :process ->
            v = deep_map(v, {key_function, value_function}, Map.put(options, :filtering, true), level + 1)
            [{key_function.(k), value_function.(v)} | acc]
          :except ->
            v = deep_map(v, {key_function, value_function}, options, level + 1)
            [{k, v} | acc]
          :skip ->
            [{key_function.(k), v} | acc]
          :reject ->
            acc
        end

      {k, v}, acc ->
        case process_type({k, v}, options) do
          :continue ->
            [{k, v} | acc]
          :process ->
            [{key_function.(k), value_function.(v)} | acc]
          :except ->
            [{k, v} | acc]
          :skip ->
            [{key_function.(k), v} | acc]
          :reject ->
            acc
        end
    end)
    |> Map.new()
  end

  defp deep_map([], _function, _options, _level) do
    []
  end

  defp deep_map([head | rest], function, options, level) do
    case process_type(head, options) do
      :continue ->
        [head | deep_map(rest, function, Map.put(options, :filtering, true), level + 1)]
      :process ->
        [deep_map(head, function, Map.put(options, :filtering, true), level + 1) |
          deep_map(rest, function, options, level + 1)]
      :except ->
        [deep_map(head, function, options, level + 1) |
          deep_map(rest, function, options, level + 1)]
      :skip ->
        [head | deep_map(rest, function, options, level + 1)]
      :reject ->
        deep_map(rest, function, options, level + 1)
    end
  end

  defp deep_map(value, function, options, _level) when is_function(function) do
    case process_type(value, options) do
      :continue ->
        value
      :process ->
        function.(value)
      :except ->
        value
      :skip ->
        value
      :reject ->
        nil
    end
  end

  defp deep_map(value, {_key_function, value_function}, options, _level) do
    case process_type(value, options) do
      :continue ->
        value
      :process ->
        value_function.(value)
      :skip ->
        value
      :reject ->
        nil
    end
  end

  @doc """
  Transforms a `map`'s `String.t` keys to `atom()` keys.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`. One additional option apples
    to this function directly:

    * `:only_existing` which is set to `true` will
      only convert the binary value to an atom if the atom
      already exists.  The default is `false`.

  ## Example

      iex> Cldr.Map.atomize_keys %{"a" => %{"b" => %{1 => "c"}}}
      %{a: %{b: %{1 => "c"}}}

  """
  @default_atomize_options [only_existing: false]
  def atomize_keys(map, options \\ [])

  def atomize_keys(map, options) when is_map(map) or is_list(map) do
    options = @default_atomize_options ++ options
    map_options = Map.new(options)

    deep_map(map, &atomize_key(&1, map_options), options)
  end

  def atomize_keys({k, value}, options) when is_map(value) or is_list(value) do
    options = @default_atomize_options ++ options
    map_options = Map.new(options)

    {atomize_key(k, map_options), deep_map(value, &atomize_key(&1, map_options), options)}
  end

  # For compatibility with older
  # versions of ex_cldr that expect this
  # behaviour

  # TODO Fix Cldr.Config.get_locale to not make this assumption
  def atomize_keys(other, _options) do
    other
  end

  @doc """
  Transforms a `map`'s `String.t` values to `atom()` values.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`. One additional option apples
    to this function directly:

    * `:only_existing` which is set to `true` will
      only convert the binary value to an atom if the atom
      already exists.  The default is `false`.

  ## Examples

      iex> Cldr.Map.atomize_values %{"a" => %{"b" => %{1 => "c"}}}
      %{"a" => %{"b" => %{1 => :c}}}

  """
  def atomize_values(map, options \\ [only_existing: false])

  def atomize_values(map, options) when is_map(map) or is_list(map) do
    options = @default_atomize_options ++ options
    map_options = Map.new(options)

    deep_map(map, &atomize_value(&1, map_options), options)
  end

  def atomize_values({k, value}, options) when is_map(value) or is_list(value) do
    options = @default_atomize_options ++ options
    map_options = Map.new(options)

    {k, deep_map(value, &atomize_value(&1, map_options), options)}
  end

  @doc """
  Transforms a `map`'s `String.t` keys to `Integer.t` keys.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  The map key is converted to an `integer` from
  either an `atom` or `String.t` only when the
  key is comprised of `integer` digits.

  Keys which cannot be converted to an `integer`
  are returned unchanged.

  ## Example

      iex> Cldr.Map.integerize_keys %{a: %{"1" => "value"}}
      %{a: %{1 => "value"}}

  """
  def integerize_keys(map, options \\ [])

  def integerize_keys(map, options) when is_map(map) or is_list(map) do
    deep_map(map, &integerize_key/1, options)
  end

  def integerize_keys({k, value}, options) when is_map(value) or is_list(value) do
    {integerize_key(k), deep_map(value, &integerize_key/1, options)}
  end

  @doc """
  Transforms a `map`'s `String.t` values to `Integer.t` values.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  The map value is converted to an `integer` from
  either an `atom` or `String.t` only when the
  value is comprised of `integer` digits.

  Keys which cannot be converted to an integer
  are returned unchanged.

  ## Example

      iex> Cldr.Map.integerize_values %{a: %{b: "1"}}
      %{a: %{b: 1}}

  """
  def integerize_values(map, options \\ []) do
    deep_map(map, &integerize_value/1, options)
  end

  @doc """
  Transforms a `map`'s `String.t` keys to `Float.t` values.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  The map key is converted to a `float` from
  a `String.t` only when the key is comprised of
  a valid float form.

  Keys which cannot be converted to a `float`
  are returned unchanged.

  ## Examples

      iex> Cldr.Map.floatize_keys %{a: %{"1.0" => "value"}}
      %{a: %{1.0 => "value"}}

      iex> Cldr.Map.floatize_keys %{a: %{"1" => "value"}}
      %{a: %{1.0 => "value"}}

  """
  def floatize_keys(map, options \\ [])

  def floatize_keys(map, options) when is_map(map) or is_list(map) do
    deep_map(map, &floatize_key/1, options)
  end

  def floatize_keys({k, value}, options) when is_map(value) or is_list(value) do
    {floatize_key(k), deep_map(value, &floatize_key/1, options)}
  end

  @doc """
  Transforms a `map`'s `String.t` values to `Float.t` values.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  The map value is converted to a `float` from
  a `String.t` only when the
  value is comprised of a valid float form.

  Values which cannot be converted to a `float`
  are returned unchanged.

  ## Examples

      iex> Cldr.Map.floatize_values %{a: %{b: "1.0"}}
      %{a: %{b: 1.0}}

      iex> Cldr.Map.floatize_values %{a: %{b: "1"}}
      %{a: %{b: 1.0}}

  """
  def floatize_values(map, options \\ []) do
    deep_map(map, &floatize_value/1, options)
  end

  @doc """
  Transforms a `map`'s `atom()` keys to `String.t` keys.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  ## Example

      iex> Cldr.Map.stringify_keys %{a: %{"1" => "value"}}
      %{"a" => %{"1" => "value"}}

  """
  def stringify_keys(map, options \\ [])

  def stringify_keys(map, options) when is_map(map) or is_list(map) do
    deep_map(map, &stringify_key/1, options)
  end

  def stringify_keys({k, value}, options) when is_map(value) or is_list(value) do
    {stringify_key(k), deep_map(value, &stringify_key/1, options)}
  end

  @doc """
  Transforms a `map`'s `atom()` keys to `String.t` keys.

  ## Arguments

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  ## Example

      iex> Cldr.Map.stringify_values %{a: %{"1" => :value}}
      %{a: %{"1" => "value"}}

  """
  def stringify_values(map, options \\ []) do
    deep_map(map, &stringify_value/1, options)
  end

  @doc """
  Convert map `String.t` keys from `camelCase` to `snake_case`

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  ## Example

      iex> Cldr.Map.underscore_keys %{"a" => %{"thisOne" => "value"}}
      %{"a" => %{"this_one" => "value"}}

  """
  def underscore_keys(map, options \\ [])

  def underscore_keys(map, options) when is_map(map) or is_nil(map) do
    deep_map(map, &underscore_key/1, options)
  end

  def underscore_keys({k, value}, options) when is_map(value) or is_list(value) do
    {underscore_key(k), deep_map(value, &underscore_key/1, options)}
  end

  @doc """
  Rename map keys from `from` to `to`

  * `map` is any `t:map/0`

  * `from` is any value map key

  * `to` is any valid map key

  * `options` is a keyword list of options passed
    to `deep_map/3`

  ## Example

      iex> Cldr.Map.rename_keys %{"a" => %{"this_one" => "value"}}, "this_one", "that_one"
      %{"a" => %{"that_one" => "value"}}

  """
  def rename_keys(map, from, to, options \\ []) when is_map(map) or is_list(map) do
    renamer = fn
      {^from, v} -> {to, v}
      other -> other
    end

    deep_map(map, renamer, options)
  end

  @doc """
  Convert a camelCase string or atom to a snake_case

  * `string` is a `String.t` or `atom()` to be
    transformed

  This is the code of Macro.underscore with modifications.
  The change is to cater for strings in the format:

    This_That

  which in Macro.underscore gets formatted as

    this__that (note the double underscore)

  when we actually want

    that_that

  ## Examples

      iex> Cldr.Map.underscore "thisThat"
      "this_that"

      iex> Cldr.Map.underscore "This_That"
      "this_that"

  """
  @spec underscore(string :: String.t() | atom()) :: String.t()
  def underscore(<<h, t::binary>>) do
    <<to_lower_char(h)>> <> do_underscore(t, h)
  end

  def underscore(other) do
    other
  end

  # h is upper case, next char is not uppercase, or a _ or .  => and prev != _
  defp do_underscore(<<h, t, rest::binary>>, prev)
       when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and t != ?. and t != ?_ and t != ?- and
              prev != ?_ do
    <<?_, to_lower_char(h), t>> <> do_underscore(rest, t)
  end

  # h is uppercase, previous was not uppercase or _
  defp do_underscore(<<h, t::binary>>, prev)
       when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ do
    <<?_, to_lower_char(h)>> <> do_underscore(t, h)
  end

  # h is dash "-" -> replace with underscore "_"
  defp do_underscore(<<?-, t::binary>>, _) do
    <<?_>> <> underscore(t)
  end

  # h is .
  defp do_underscore(<<?., t::binary>>, _) do
    <<?/>> <> underscore(t)
  end

  # Any other char
  defp do_underscore(<<h, t::binary>>, _) do
    <<to_lower_char(h)>> <> do_underscore(t, h)
  end

  defp do_underscore(<<>>, _) do
    <<>>
  end

  defp to_lower_char(char) when char == ?-, do: ?_
  defp to_lower_char(char) when char >= ?A and char <= ?Z, do: char + 32
  defp to_lower_char(char), do: char

  @doc """
  Removes any leading underscores from `map`
  `String.t` keys.

  * `map` is any `t:map/0`

  * `options` is a keyword list of options passed
    to `deep_map/3`

  ## Examples

      iex> Cldr.Map.remove_leading_underscores %{"a" => %{"_b" => "b"}}
      %{"a" => %{"b" => "b"}}

  """
  def remove_leading_underscores(map, options \\ []) do
    remover = fn
      {k, v} when is_binary(k) -> {String.trim_leading(k, "_"), v}
      other -> other
    end

    deep_map(map, remover, options)
  end

  @doc """
  Returns the result of deep merging a list of maps

  ## Examples

      iex> Cldr.Map.merge_map_list [%{a: "a", b: "b"}, %{c: "c", d: "d"}]
      %{a: "a", b: "b", c: "c", d: "d"}

  """
  def merge_map_list(list, resolver \\ &standard_deep_resolver/3)

  def merge_map_list([h | []], _resolver) do
    h
  end

  def merge_map_list([h | t], resolver) do
    deep_merge(h, merge_map_list(t, resolver), resolver)
  end

  def merge_map_list([], _resolver) do
    []
  end

  @doc """
  Deep merge two maps

  * `left` is any `t:map/0`

  * `right` is any `t:map/0`

  ## Examples

      iex> Cldr.Map.deep_merge %{a: "a", b: "b"}, %{c: "c", d: "d"}
      %{a: "a", b: "b", c: "c", d: "d"}

      iex> Cldr.Map.deep_merge %{a: "a", b: "b"}, %{c: "c", d: "d", a: "aa"}
      %{a: "aa", b: "b", c: "c", d: "d"}

  """
  def deep_merge(left, right, resolver \\ &standard_deep_resolver/3) when is_map(left) and is_map(right) do
    Map.merge(left, right, resolver)
  end

  # Key exists in both maps, and both values are maps as well.
  # These can be merged recursively.
  defp standard_deep_resolver(_key, left, right) when is_map(left) and is_map(right) do
    deep_merge(left, right, &standard_deep_resolver/3)
  end

  # Key exists in both maps, but at least one of the values is
  # NOT a map. We fall back to standard merge behavior, preferring
  # the value on the right.
  defp standard_deep_resolver(_key, _left, right) do
    right
  end

  def combine_list_resolver(_key, left, right) when is_list(left) and is_list(right) do
    left ++ right
  end

  @doc """
  Delete all members of a map that have a
  key in the list of keys

  ## Examples

      iex> Cldr.Map.delete_in %{a: "a", b: "b"}, [:a]
      %{b: "b"}

  """
  def delete_in(%{} = map, keys) when is_list(keys) do
    Enum.reject(map, fn {k, _v} -> k in keys end)
    |> Enum.map(fn {k, v} -> {k, delete_in(v, keys)} end)
    |> Map.new()
  end

  def delete_in(map, keys) when is_list(map) and is_binary(keys) do
    delete_in(map, [keys])
  end

  def delete_in(map, keys) when is_list(map) do
    Enum.reject(map, fn {k, _v} -> k in keys end)
    |> Enum.map(fn {k, v} -> {k, delete_in(v, keys)} end)
  end

  def delete_in(%{} = map, keys) when is_binary(keys) do
    delete_in(map, [keys])
  end

  def delete_in(other, _keys) do
    other
  end

  def from_keyword([] = list) do
    Map.new(list)
  end

  def from_keyword([{key, _value} | _rest] = keyword_list) when is_atom(key) do
    Map.new(keyword_list)
  end

  @doc """
  Extract strings from a map or list

  Recursively process the map or list
  and extract string values from maps
  and string elements from lists

  """
  def extract_strings(map_or_list, options \\ [])

  def extract_strings([], _options) do
    []
  end

  def extract_strings(map, _options) when is_map(map) do
    Enum.reduce(map, [], fn
      {_k, v}, acc when is_binary(v) -> [v | acc]
      {_k, v}, acc when is_map(v) -> [extract_strings(v) | acc]
      {_k, v}, acc when is_list(v) -> [extract_strings(v) | acc]
      _other, acc -> acc
    end)
    |> List.flatten
  end

  def extract_strings(list, _options) when is_list(list) do
    Enum.reduce(list, [], fn
      v, acc when is_binary(v) -> [v | acc]
      v, acc when is_map(v) -> extract_strings(v, acc)
      v, acc when is_list(v) -> extract_strings(v, acc)
      _other, acc -> acc
    end)
    |> List.flatten
  end

  @doc """
  Prune a potentially deeply nested map of some of
  its branches

  """
  def prune(map, fun) when is_map(map) and is_function(fun, 1) do
    deep_map(map, &(&1), reject: fun)
  end

  @doc """
  Invert a map

  Requires that the map is a simple map of
  keys and a list of values or a single
  non-map value

  ## Options

  * `:duplicates` which determines how duplicate values
    are handled:
    * `nil` or `false` which is the default and means only
      one value is kept. `Map.new/1` is used meanng the
      selected value is non-deterministic.
    * `:keep` meaning duplicate values are returned in a list
    * `:shortest` means the shortest duplicate is kept.
      This operates on string or atom values.
    * `:longest` means the shortest duplicate is kept.
      This operates on string or atom values.

  """
  def invert(map, options \\ [])

  def invert(map, options) when is_map(map) do
    map
    |> Enum.map(fn
      {k, v} when is_list(v) -> Enum.map(v, fn vv -> {vv, k} end)
      {k, v} when not is_map(v) -> {v, k}
    end)
    |> List.flatten
    |> process_duplicates(options[:duplicates])
  end

  defp process_duplicates(list, keep) when is_nil(keep) or keep == false do
    Map.new(list)
  end

  defp process_duplicates(list, :keep) do
    list
    |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
  end

  defp process_duplicates(list, :shortest) do
    list
    |> process_duplicates(:keep)
    |> Enum.map(fn {k, v} -> {k, shortest(v)} end)
    |> Map.new()
  end

  defp process_duplicates(list, :longest) do
    list
    |> process_duplicates(:keep)
    |> Enum.map(fn {k, v} -> {k, longest(v)} end)
    |> Map.new()
  end

  defp shortest(list) when is_list(list) do
    Enum.min_by(list, &len/1)
  end

  defp longest(list) when is_list(list) do
    Enum.max_by(list, &len/1)
  end

  defp len(e) when is_binary(e) do
    String.length(e)
  end

  defp len(e) when is_atom(e) do
    e
    |> Atom.to_string
    |> len()
  end

  #
  # Helpers
  #

  defp validate_options(function, options) when is_function(function) do
    validate_options(options)
  end

  defp validate_options({key_function, value_function}, options)
       when is_function(key_function) and is_function(value_function) do
    validate_options(options)
  end

  defp validate_options(function, _options) do
    raise ArgumentError,
          "function parameter must be a function or a 2-tuple " <>
            "consisting of a key_function and a value_function. Found #{inspect(function)}"
  end

  defp validate_options(options) do
    @default_deep_map_options
    |> Keyword.merge(options)
    |> Map.new()
    |> Map.update!(:level, fn
      level when is_integer(level) ->
        level..level

      %Range{} = level ->
        level

      other ->
        raise ArgumentError, ":level must be an integer or a range. Found #{inspect(other)}"
    end)
  end

  defp atomize_key({k, v}, %{only_existing: true}) when is_binary(k) do
    {String.to_existing_atom(k), v}
  rescue
    ArgumentError -> {k, v}
  end

  defp atomize_key({k, v}, %{only_existing: false}) when is_binary(k) do
    {String.to_atom(k), v}
  end

  defp atomize_key(other, _options) do
    other
  end

  defp atomize_value({k, v}, %{only_existing: true}) when is_binary(v) do
    {k, String.to_existing_atom(v)}
  rescue
    ArgumentError -> {k, v}
  end

  defp atomize_value({k, v}, %{only_existing: false}) when is_binary(v) do
    {k, String.to_atom(v)}
  end

  defp atomize_value(v, %{only_existing: false}) when is_binary(v) do
    String.to_atom(v)
  end

  defp atomize_value(other, _options) do
    other
  end

  defp integerize_key({k, v}) when is_binary(k) do
    case Integer.parse(k) do
      {integer, ""} -> {integer, v}
      _other -> {k, v}
    end
  end

  defp integerize_key(other) do
    other
  end

  defp integerize_value({k, v}) when is_binary(v) do
    case Integer.parse(v) do
      {integer, ""} -> {k, integer}
      _other -> {k, v}
    end
  end

  defp integerize_value(other) do
    other
  end

  defp floatize_key({k, v}) when is_binary(k) do
    case Float.parse(k) do
      {float, ""} -> {float, v}
      _other -> {k, v}
    end
  end

  defp floatize_key(other) do
    other
  end

  defp floatize_value({k, v}) when is_binary(v) do
    case Float.parse(v) do
      {float, ""} -> {k, float}
      _other -> {k, v}
    end
  end

  defp floatize_value(other) do
    other
  end

  defp stringify_key({k, v}) when is_atom(k), do: {Atom.to_string(k), v}
  defp stringify_key(other), do: other

  defp stringify_value({k, v}) when is_atom(v), do: {k, Atom.to_string(v)}
  defp stringify_value(other) when is_atom(other), do: Atom.to_string(other)
  defp stringify_value(other), do: other

  defp underscore_key({k, v}) when is_binary(k), do: {underscore(k), v}
  defp underscore_key(other), do: other

  # process_element?/2 determines whether the
  # calling function should apply to a given
  # value

  def process_type(x, options) do
    filter? = filter?(x, options) # |> IO.inspect(label: "Filter: #{inspect x}")
    reject? = reject?(x, options) # |> IO.inspect(label: "Reject: #{inspect x}")
    # IO.inspect only?(x, options), label: "Only: #{inspect x}"
    # IO.inspect except?(x, options), label: "Except: #{inspect x}"
    # IO.inspect skip?(x, options), label: "Skip: #{inspect x}"

    cond do
      reject? -> :reject
      skip?(x, options) -> :skip
      filter? && only?(x, options) && !except?(x, options) -> :process
      filter? -> :continue
      true -> :except
    end
    # |> IO.inspect(label: inspect(x))
  end

  # Keep this branch but don't process it
  defp skip?(x, %{skip: skip}) when is_function(skip) do
    skip.(x)
  end

  defp skip?({k, _v}, %{skip: skip}) when is_list(skip) do
    k in skip
  end

  defp skip?({k, _v}, %{skip: skip}) do
    k == skip
  end

  defp skip?(k, %{skip: skip}) when is_list(skip) do
    k in skip
  end

  defp skip?(k, %{skip: skip}) do
    k == skip
  end

  # Keep this branch is the result
  defp filter?(x, %{filter: filter}) when is_function(filter) do
    filter.(x)
  end

  defp filter?(_x, %{filtering: true}) do
    true
  end

  defp filter?(_x, %{filter: []}) do
    true
  end

  defp filter?({k, _v}, %{filter: filter}) when is_list(filter) do
    k in filter
  end

  defp filter?({k, _v}, %{filter: filter}) do
    k == filter
  end

  defp filter?(k, %{filter: filter}) when is_list(filter) do
    k in filter
  end

  defp filter?(k, %{filter: filter}) do
    k == filter
  end

  # Don't include this branch is the result
  defp reject?(x, %{reject: reject}) when is_function(reject) do
    reject.(x)
  end

  defp reject?({k, _v}, %{reject: reject}) when is_list(reject) do
    k in reject
  end

  defp reject?({k, _v}, %{reject: reject})  do
    k == reject
  end

  defp reject?(k, %{reject: reject}) when is_list(reject) do
    k in reject
  end

  defp reject?(k, %{reject: reject})  do
    k == reject
  end

  # Process this item
  defp only?(x, %{only: only}) when is_function(only) do
    only.(x)
  end

  defp only?(_x, %{only: []}) do
    true
  end

  defp only?({k, _v}, %{only: only}) when is_list(only) do
    k in only
  end

  defp only?({k, _v}, %{only: only}) do
    k == only
  end

  defp only?(k, %{only: only}) when is_list(only) do
    k in only
  end

  defp only?(k, %{only: only}) do
    k == only
  end

  # Don;t process this item
  defp except?(x, %{except: except}) when is_function(except) do
    except.(x)
  end

  defp except?({k, _v}, %{except: except}) when is_list(except) do
    k in except
  end

  defp except?({k, _v}, %{except: except}) do
    k == except
  end

  defp except?(k, %{except: except}) when is_list(except) do
    k in except
  end

  defp except?(k, %{except: except}) do
    k == except
  end

end