defmodule Destructure do
  @moduledoc """
  Provides helpers for destructuring Elixir data structures. See the `d/1` macro
  for more information.
  """

  @doc """
  Easy destructuring of `map`, `structs`, `keyword`, with atom keys.
  String keys are supported via a special syntax `d(s%{param})`.

  ## Examples

  The macro can be used in simple match statements:

      iex> d(%{name}) = %{name: "Joe"}
      ...> name
      "Joe"

  Or in case/for/with statements.

      iex> case %{name: "Mike"} do
      ...>   d%{name} -> name
      ...> end
      "Mike"

  It can be used inside a complex match:

      iex> %{body: d%{name}} = %{body: %{name: "Robert"}}
      ...> name
      "Robert"

  With multiple keys:

      iex> d(%{first, last}) = %{first: "Daniel", last: "Berkompas"}
      ...> {first, last}
      {"Daniel", "Berkompas"}

  With multiple keys and custom variable naming:

      iex> d(%{first, last, email: mail}) = %{first: "Daniel", last: "Berkompas", email: "top@secret.com"}
      ...> {first, last, mail}
      {"Daniel", "Berkompas", "top@secret.com"}

  For Maps with String Keys:

      iex> s(%{name}) = %{"name" => "Daniel Berkompas"}
      ...> name
      "Daniel Berkompas"

  With multiple keys:

      iex> s(%{name, email}) = %{"name" => "Daniel Berkompas", "email" => "top@secret.com"}
      ...> {name, email}
      {"Daniel Berkompas", "top@secret.com"}

  With multiple keys and custom variable naming:

      iex> s(%{name, "email" => mail}) = %{"name" => "Daniel Berkompas", "email" => "top@secret.com"}
      ...> {name, mail}
      {"Daniel Berkompas", "top@secret.com"}


  For structs:

      iex> d(%Person{name}) = %Person{name: "Daniel Berkompas"}
      ...> name
      "Daniel Berkompas"

  With multiple keys:

      iex> d(%Person{name, email}) = %Person{name: "Daniel Berkompas", email: "top@secret.com"}
      ...> {name, email}
      {"Daniel Berkompas", "top@secret.com"}

  With multiple keys and custom variable naming:

      iex> d(%Person{name, email: mail}) = %Person{name: "Daniel Berkompas", email: "top@secret.com"}
      ...> {name, mail}
      {"Daniel Berkompas", "top@secret.com"}

  For keyword lists:

      iex> d([name]) = [name: "Daniel"]
      ...> name
      "Daniel"

  With multiple keys:

      iex> d([first, last]) = [first: "Daniel", last: "Berkompas"]
      ...> [first, last]
      ["Daniel", "Berkompas"]

  With multiple keys and custom assignment:

      iex> d([first, last, email: mail]) = [first: "Daniel", last: "Berkompas", email: "top@secret.com"]
      ...> [first, last, mail]
      ["Daniel", "Berkompas", "top@secret.com"]

  And in function definitions:

      iex> defmodule Test do
      ...>   import Destructure
      ...>   def test(d%{name}) do
      ...>     name
      ...>   end
      ...> end
      ...> Test.test(%{name: "Daniel"})
      "Daniel"
  """
  # Handle maps, including ones with multiple keys
  # {%{}, [], [{:first, [], Elixir}, {:second, [], Elixir}]}
  defmacro d({:%{}, context, args}) do
    {:%{}, context, Enum.map(args, &pattern/1)}
  end

  # Handle structs, including ones with multiple keys
  # {:%, [],
  #  [{:__aliases__, [alias: false], [:Namespace]},
  #   {:%{}, [], [{:first, [], Elixir}, {:second, [], Elixir}]}]}
  defmacro d({:%, x_context, [namespace, {:%{}, y_context, args}]}) do
    {:%, x_context, [namespace, {:%{}, y_context, Enum.map(args, &pattern/1)}]}
  end

  # Handle 1 and 3+ element tuples
  # {:{}, [], [{:first, [], Elixir}]}
  defmacro d({:{}, _, args}) do
    Enum.map(args, &pattern/1)
  end

  # Handle the special case of two-element tuples, whose ASTs look like
  # {{:first, [], Elixir}, {:last, [], Elixir}}
  defmacro d({first, second}) do
    [pattern(first), pattern(second)]
  end

  # Handle keyword list using square bracket
  defmacro d(list) when is_list(list) do
    Enum.map(list, &pattern/1)
  end

  # Handle string maps, including ones with multiple keys
  # {:%{}, [], [{:stuff, [], Elixir}, {:things, [], Elixir}]}
  defmacro s({:%{}, context, args}) do
    {:%{}, context, Enum.map(args, &s_pattern/1)}
  end

  defp s_pattern({key, _, _} = variable) do
    {key |> Atom.to_string(), variable}
  end

  defp s_pattern(otherwise), do: pattern(otherwise)

  defp pattern({key, _, _} = variable) do
    {key, variable}
  end

  defp pattern([arg]) do
    arg
  end

  defp pattern(other) do
    other
  end
end
