defmodule Map.Helpers do
  @moduledoc """
  Helper Functions for working with `Map`.

  Based on [this helpful gist](https://gist.github.com/kipcole9/0bd4c6fb6109bfec9955f785087f53fb)
  """

  @doc """
  Convert map string camelCase keys to underscore_keys
  """
  def underscore_keys(nil), do: nil

  def underscore_keys(%MapSet{} = map_set), do: Enum.to_list(map_set) |> underscore_keys

  def underscore_keys(%{} = map),
    do:
      Enum.into(map, %{}, fn {k, v} ->
        {k |> Macro.underscore() |> String.replace("-", "_"), underscore_keys(v)}
      end)

  # Walk the list and atomize the keys of
  # of any map members
  def underscore_keys([head | rest]) do
    [underscore_keys(head) | underscore_keys(rest)]
  end

  def underscore_keys(not_a_map) do
    not_a_map
  end

  @doc """
  Convert map string keys to :atom keys
  """
  def atomize_keys(nil), do: nil

  # Structs don't do enumerable and anyway the keys are already
  # atoms
  def atomize_keys(%{__struct__: _} = struct) do
    struct
  end

  def atomize_keys(%{} = map),
    do: Enum.into(map, %{}, fn {k, v} -> {atomize_string(k), atomize_keys(v)} end)

  # Walk the list and atomize the keys of
  # of any map members
  def atomize_keys([head | rest]) do
    [atomize_keys(head) | atomize_keys(rest)]
  end

  def atomize_keys(not_a_map) do
    not_a_map
  end

  defp atomize_string(string) when is_binary(string), do: String.to_atom(string)

  defp atomize_string(not_a_string) do
    not_a_string
  end

  @doc """
  Convert map atom keys to strings
  """
  def stringify_keys(nil), do: nil

  def stringify_keys(%MapSet{} = map_set), do: Enum.to_list(map_set) |> stringify_keys

  def stringify_keys(%{} = map),
    do: Enum.into(map, %{}, fn {k, v} -> {stringify_atom(k), stringify_keys(v)} end)

  def stringify_keys(%{__meta__: _} = schema), do: schema
  def stringify_keys(%{struct: _} = struct), do: struct

  # Walk the list and stringify the keys of
  # of any map members
  def stringify_keys([head | rest]) do
    [stringify_keys(head) | stringify_keys(rest)]
  end

  def stringify_keys(not_a_map) do
    not_a_map
  end

  defp stringify_atom(atom) when is_atom(atom) do
    Atom.to_string(atom)
  end

  defp stringify_atom(not_a_atom), do: not_a_atom

  @doc """
  Deep merge two maps
  """
  def deep_merge(left, right) do
    Map.merge(left, right, &deep_resolve/3)
  end

  # Key exists in both maps, and both values are maps as well.
  # These can be merged recursively.
  defp deep_resolve(_key, %{} = left, %{} = right) do
    deep_merge(left, right)
  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 deep_resolve(_key, _left, right) do
    right
  end
end
