lib/ton/cell/topological_order.ex

defmodule Ton.Cell.TopologicalOrder do
  @moduledoc """
  Cell ordering algorithm
  """

  alias Ton.Cell

  @spec sort(Cell.t()) :: [%{cell: Cell.t(), refs: [non_neg_integer()]}] | no_return()
  def sort(root_cell) do
    {all_cells, cell_hashes} = flatten_cells([root_cell])

    sorted_hashes = sort(cell_hashes, all_cells)

    indexes =
      sorted_hashes
      |> Enum.with_index()
      |> Map.new()

    Enum.map(sorted_hashes, fn hash ->
      cell = Map.get(all_cells, hash)

      ref_indexes =
        Enum.map(cell.refs, fn ref ->
          Map.get(indexes, ref)
        end)

      %{cell | refs: ref_indexes}
    end)
  end

  defp flatten_cells(cells, acc \\ {%{}, []})

  defp flatten_cells([], {new_all_cells, new_uniq_hashes}) do
    {new_all_cells, Enum.reverse(new_uniq_hashes)}
  end

  defp flatten_cells(cells, {all_cells, uniq_hashes}) do
    {new_all_cells, new_uniq_hashes, new_cells_reversed} =
      Enum.reduce(cells, {all_cells, uniq_hashes, []}, fn cell,
                                                          {all_cells_acc, uniq_hashes_acc,
                                                           current_cells_acc} ->
        hash = Cell.hash(cell)

        case Map.get(all_cells_acc, hash) do
          nil ->
            ref_hashes =
              Enum.map(cell.refs, fn ref ->
                Cell.hash(ref)
              end)

            cell_wrapper = %{cell: cell, refs: ref_hashes}
            new_all_cells_acc = Map.put(all_cells_acc, hash, cell_wrapper)
            new_uniq_hashes_acc = [hash | uniq_hashes_acc]
            new_current_cells_acc = Enum.concat(current_cells_acc, cell.refs)

            {new_all_cells_acc, new_uniq_hashes_acc, new_current_cells_acc}

          _ ->
            {all_cells_acc, uniq_hashes_acc, current_cells_acc}
        end
      end)

    flatten_cells(new_cells_reversed, {new_all_cells, new_uniq_hashes})
  end

  defp sort(hashes, all_cells, temp_mark \\ MapSet.new(), sorted \\ [])

  defp sort([], _all_cells, _temp_mark, sorted), do: sorted

  defp sort([cell_hash | _tail] = cell_hashes, all_cells, temp_mark, sorted) do
    {new_cell_hashes, new_temp_mark, new_sorted} =
      do_sort(cell_hash, cell_hashes, all_cells, temp_mark, sorted)

    sort(new_cell_hashes, all_cells, new_temp_mark, new_sorted)
  end

  defp do_sort(cell_hash, cell_hashes, all_cells, temp_mark, sorted_acc) do
    if Enum.member?(cell_hashes, cell_hash) do
      if Enum.member?(temp_mark, cell_hash) do
        raise "Not a DAG"
      end

      cell = Map.get(all_cells, cell_hash)
      temp_mark = MapSet.put(temp_mark, cell_hash)

      {new_cell_hashes, new_temp_mark, new_sorted_acc} =
        Enum.reduce(cell.refs, {cell_hashes, temp_mark, sorted_acc}, fn ref_cell_hash,
                                                                        {nested_cell_hashes,
                                                                         nested_temp_mark_acc,
                                                                         nested_sorted_acc} ->
          do_sort(
            ref_cell_hash,
            nested_cell_hashes,
            all_cells,
            nested_temp_mark_acc,
            nested_sorted_acc
          )
        end)

      new_sorted_acc = [cell_hash | new_sorted_acc]
      temp_mark = MapSet.delete(new_temp_mark, cell_hash)

      cell_hashes =
        Enum.reject(new_cell_hashes, fn current_cell_hash -> current_cell_hash == cell_hash end)

      {cell_hashes, temp_mark, new_sorted_acc}
    else
      {cell_hashes, temp_mark, sorted_acc}
    end
  end
end