defmodule XmlToMap do
  @moduledoc """
  Simple convenience module for getting a map out of an XML string.
  """

  alias XmlToMap.{NaiveMap, NestedMap}

  @doc """
  `naive_map/1` utility is inspired by `Rails Hash.from_xml()` but is
  "naive" in that it is convenient (requires no setup) but carries the same
  drawbacks.

  For example no validation over what should be a collection.  If and only if
  nodes are repeated at the same level will they become a list.  If a node has
  attributes we'll prepend a "-" in front of them and merge them into the map
  and take the node value and nest that inside "#content" key.
  """
  @defaults %{namespace_match_fn: &__MODULE__.default_namespace_match_fn/0}

  def naive_map(xml, opts \\ []) do
    params = Enum.into(opts, @defaults)
    tree = get_generic_data_structure(xml, params)
    NaiveMap.parse(tree)
  end

  def nested_map(xml, opts \\ []) do
    params = Enum.into(opts, @defaults)
    {purge_empty, params} = Map.pop(params, :purge_empty, false)
    tree = get_generic_data_structure(xml, params)
    NestedMap.parse(tree, purge_empty)
  end

  # Default function to handle namespace in xml
  # This fuction invokes an anonymous function that has this parameters: Name, Namespace, Prefix.
  # Usually, the namespace appears at the top of xml file, e.g `xmlns:g="http://base.google.com/ns/1.0"`,
  # where `g` is the Prefix and `"http://base.google.com/ns/1.0"` the Namespace,
  # the Name is the key tag in xml.
  # It should return a term. It is called for each tag and attribute name.
  # The result will be used in the output.
  # When a namespace and prefix is found, it'll return a term like "#{prefix}:#{name}",
  # otherwise it will return only the name.
  # The default behavior in :erlsom.simple_form/2 is Name if Namespace == undefined, and a string {Namespace}Name otherwise.
  def default_namespace_match_fn do
    fn name, namespace, prefix ->
      cond do
        namespace != [] && prefix != [] -> "#{prefix}:#{name}"
        true -> name
      end
    end
  end

  defp get_generic_data_structure(xml, %{namespace_match_fn: namespace_match_fn}) do
    {:ok, element, _tail} = :erlsom.simple_form(xml, [{:nameFun, namespace_match_fn.()}])
    element
  end

  # erlsom simple_form returns a kind of tree:
  # Result = {ok, Element, Tail},
  # where Element = {Tag, Attributes, Content},
  # Tag is a string
  # Attributes = [{AttributeName, Value}],
  # Content is a list of Elements and/or strings.
  defp get_generic_data_structure(xml, _opts) do
    {:ok, element, _tail} = :erlsom.simple_form(xml)
    element
  end
end
