lib/cte/utils.ex

defmodule CTE.Utils do
  @moduledoc """
  Basic utilities for helping developing functionality about the `CTE` data structures
  """

  @doc """
  render a path hierarchy as a .dot string that you could use for drawing your results, using graphviz.  w⦿‿⦿t!

  Upon receiving something like this:

      %{
        nodes: %{
          6 => %{
            author: "Rolie",
            comment: "Everything is easier, than with the Nested Sets.",
            id: 6
          },
          8 => %{
            author: "Olie",
            comment: "I’m sold! And I’ll use its Elixir implementation! <3",
            id: 8
          },
          9 => %{author: "Polie", comment: "w⦿‿⦿t!", id: 9}
        },
        paths: [
                 [6, 6, 0],
                 [6, 8, 1],
                 [8, 8, 0],
                 [6, 9, 1],
                 [9, 9, 0]
               ]
      }

  this will output a .dot formatted string that you could use later for generating
  an image: `dot -Tpng <filename>.dot -o <filename>.png`

  """
  @spec print_dot(map, Keyword.t()) :: String.t()
  def print_dot(tree, opts \\ [])

  def print_dot(%{paths: paths, nodes: nodes}, opts)
      when is_list(paths) and is_map(nodes) do
    labels = Keyword.get(opts, :labels, [])
    [[root, _, _] | paths] = paths

    root = Map.get(nodes, root)
    acc = "digraph #{dot_bubble(root, labels)} {"

    Enum.reduce(paths, acc, fn [ancestor, descendant, _depth], acc ->
      parent = Map.get(nodes, ancestor)
      child = Map.get(nodes, descendant)
      acc <> "\n  " <> build_dot(parent, child, labels)
    end) <>
      "\n}\n"
  end

  def print_dot(_, _), do: {:error, :invalid_argument}

  @doc """
  print the tree at the console, using a custom function for selecting the info to be displayed

  The print_tree/3 function receives the tree structure returned by the CTE, the id of an existing node we
  want to start printing the tree with, followed by the options.

  ## Options

    * `:callback` - a function that it is invoked for every node in the tree. Has two parameters:

      * id - the id of the node we render, ao a node specific unique identifier i.e. node name, etc.
      * nodes - the nodes received the tree structure, a map %{node_id => node}...any()

      This function must return a tuple with two elements. First element is the name of the node we render,
      the second one being any optional info you want to add.

    * `:raw` - return a list of tuples if true. Each tuple will contain the depth of
      the text returned from the callback. Useful for custom formatting the output of the print.

  Example, using the default options:

  iex» {:ok, tree} = CTT.tree(1)
  iex» CTE.Utils.print_tree(tree,1, callback: &({&2[&1].author <> ":", &2[&1].comment}))

  Olie: Is Closure Table better than the Nested Sets?
  └── Rolie: It depends. Do you need referential integrity?
   └── Olie: Yeah.

  """
  def print_tree(tree, id, opts \\ [])

  def print_tree(%{paths: paths, nodes: nodes}, id, opts) do
    user_callback = Keyword.get(opts, :callback, fn id, _nodes -> {id, "info..."} end)
    direct_children = direct_children(paths)

    callback = fn
      node_id when not is_nil(node_id) ->
        {name, info} = user_callback.(node_id, nodes)
        {{name, info}, Map.get(direct_children, node_id, [])}
    end

    _print_tree([id], callback, opts)
  end

  @doc """
  converts the given tree into a nested map

  This function receives a tree structure returned by the CTE and
  the id of an existing node we want to start from. It returns
  a nested map representation of the tree.

  ## Options

    * `:callback` - a function of arity one that accepts the current
      node as input and outputs a formatted node. Defaults to `& &1`.

  Example:

  iex» {:ok, tree} = CTT.tree(1)
  iex» CTE.Utils.tree_to_map(tree, 6, callback: &Map.take(&1, [:text]))

  %{
    "children" => [
      %{
        "children" => [],
        "id" => 8,
        "node" => %{text: "I’m sold! And I’ll use its Elixir implementation! <3"}
      },
      %{"children" => [], "id" => 9, "node" => %{text: "w⦿‿⦿t!"}}
    ],
    "id" => 6,
    "node" => %{text: "Everything is easier, than with the Nested Sets."}
  }

  """
  def tree_to_map(%{paths: paths, nodes: nodes}, id, opts \\ []) do
    case nodes do
      %{^id => _} -> :ok
      _ -> raise ArgumentError, "id `#{id}` is not in the provided tree"
    end

    callback = opts[:callback] || (& &1)
    direct_children = direct_children(paths)
    _tree_to_map(id, direct_children, nodes, callback)
  end

  defp direct_children(paths) do
    paths
    |> Enum.filter(fn [a, d, depth] -> a != d && depth == 1 end)
    |> Enum.group_by(fn [a, _, _] -> a end, fn [_, d, _] -> d end)
    |> Enum.reduce(%{}, fn {parent, children}, acc ->
      descendants = children || []
      Map.put(acc, parent, Enum.uniq(descendants))
    end)
  end

  defp _print_tree(nodes, callback, opts) do
    case print_tree(nodes, _depth = [], _seen = %{}, callback, opts, []) do
      {_seen, [] = out} -> out
      {_, out} -> Enum.reverse(out)
    end
  end

  # credits where credits due:
  # - adapted from a Mix.Utils similar method
  defp print_tree(nodes, depth, seen, callback, opts, out) do
    {nodes, seen} =
      Enum.flat_map_reduce(nodes, seen, fn node, seen ->
        {{name, info}, children} = callback.(node)

        if Map.has_key?(seen, name) do
          {[{name, info, []}], seen}
        else
          {[{name, info, children}], Map.put(seen, name, true)}
        end
      end)

    print_every_node(nodes, depth, seen, callback, opts, out)
  end

  defp print_every_node([], _depth, seen, _callback, _opts, out), do: {seen, out}

  defp print_every_node([{_name, info, children} | nodes], depth, seen, callback, opts, out) do
    raw? = Keyword.get(opts, :raw, false)
    info = if(info, do: info, else: "")

    out =
      if raw? do
        [{length(depth), info} | out]
      else
        # info = if(info, do: " #{info}", else: "")
        # IO.puts("#{depth(depth)}#{prefix(depth, nodes)}#{name}#{info}")
        IO.puts("#{depth(depth)}#{prefix(depth, nodes)}#{info}")
        out
      end

    {seen, out} = print_tree(children, [nodes != [] | depth], seen, callback, opts, out)

    print_every_node(nodes, depth, seen, callback, opts, out)
  end

  defp depth([]), do: ""
  defp depth(depth), do: Enum.reverse(depth) |> tl |> Enum.map(&entry(&1))

  defp entry(true), do: "│  "
  defp entry(false), do: "   "

  defp prefix([], _), do: ""
  defp prefix(_, []), do: "└── "
  defp prefix(_, _), do: "├── "

  @spec build_dot(String.t(), String.t(), list) :: String.t()
  defp build_dot(parent, child, []), do: "#{parent} -> #{child}"

  defp build_dot(parent, child, labels) do
    "#{dot_bubble(parent, labels)} -> #{dot_bubble(child, labels)}"
  end

  defp dot_bubble(node, labels) do
    bubble_text =
      labels
      |> Enum.map(fn
        l when is_function(l) ->
          l.(node) || l

        l ->
          Map.get(node, l, l)
      end)
      |> Enum.join("")

    "\"#{bubble_text}\""
  end

  defp _tree_to_map(root_id, direct_children, nodes, callback) do
    root_node = nodes |> Map.get(root_id) |> callback.()
    child_ids = Map.get(direct_children, root_id) || []
    child_nodes = Enum.map(child_ids, &_tree_to_map(&1, direct_children, nodes, callback))

    %{"id" => root_id, "node" => root_node, "children" => child_nodes}
  end
end