lib/engine/builder.ex

defmodule SnapFramework.Engine.Builder do
  @moduledoc """
  ## Overview

  This module is responsible for taking the parsed EEx template and building the graph.
  """

  def build_graph(list, acc \\ %{}) do
    Enum.reduce(list, acc, fn item, acc ->
      case item do
        [type: :graph, opts: opts] ->
          Scenic.Graph.build(opts)

        [type: :component, module: module, data: data, opts: opts] ->
          children = if opts[:do], do: opts[:do], else: nil
          acc |> module.add_to_graph(data, Keyword.put_new(opts, :children, children))

        [type: :component, module: module, data: data, opts: opts, children: children] ->
          acc |> module.add_to_graph(data, Keyword.put_new(opts, :children, children))

        [type: :primitive, module: module, data: data, opts: opts] ->
          acc |> module.add_to_graph(data, opts)

        [
          type: :layout,
          children: children,
          padding: padding,
          width: width,
          height: height,
          translate: translate
        ] ->
          do_layout(acc, children, padding, width, height, translate).graph

        "\n" ->
          acc

        list ->
          if is_list(list) do
            build_graph(list, acc)
          else
            acc
          end
      end
    end)
  end

  defp do_layout(%Scenic.Graph{} = graph, children, padding, width, height, {x, y} = translate) do
    layout = %{
      last_x: x,
      last_y: y,
      padding: padding,
      width: width,
      height: height,
      largest_width: 0,
      largest_height: 0,
      graph: graph,
      translate: translate
    }

    Enum.reduce(children, layout, fn child, layout ->
      case child do
        [type: :component, module: module, data: data, opts: opts] ->
          children = if opts[:do], do: opts[:do], else: nil
          translate_and_render(layout, module, data, Keyword.put_new(opts, :children, children))

        [type: :component, module: module, data: data, opts: opts, children: children] ->
          translate_and_render(layout, module, data, Keyword.put_new(opts, :children, children))

        [type: :primitive, module: _module, data: _data, opts: _opts] ->
          layout

        [
          type: :layout,
          children: children,
          padding: padding,
          width: width,
          height: height,
          translate: translate
        ] ->
          {x, y} = translate
          {prev_x, prev_y} = layout.translate
          nested_layout = %{
            layout
            | last_x: x + prev_x + layout.padding,
              last_y: layout.last_y + y + layout.largest_height + layout.padding,
              padding: padding,
              width: width,
              height: height,
              translate: {x + prev_x, y + prev_y}
          }

          graph = do_layout(nested_layout, children).graph
          %{layout | graph: graph}

        "\n" ->
          layout

        list ->
          if is_list(list) do
            do_layout(layout, list)
          else
            layout
          end
      end
    end)
  end

  defp do_layout(layout, children) do
    Enum.reduce(children, layout, fn child, layout ->
      case child do
        [type: :component, module: module, data: data, opts: opts] ->
          children = if opts[:do], do: opts[:do], else: nil
          translate_and_render(layout, module, data, Keyword.put_new(opts, :children, children))

        [type: :component, module: module, data: data, opts: opts, children: children] ->
          translate_and_render(layout, module, data, Keyword.put_new(opts, :children, children))

        [type: :primitive, module: _module, data: _data, opts: _opts] ->
          layout

        [
          type: :layout,
          children: children,
          padding: padding,
          width: width,
          height: height,
          translate: translate
        ] ->
          {x, y} = translate
          {prev_x, prev_y} = layout.translate
          nested_layout = %{
            layout
            | last_x: x + prev_x + layout.padding,
              last_y: layout.last_y + y + layout.largest_height + layout.padding,
              padding: padding,
              width: width,
              height: height,
              translate: {x + prev_x, y + prev_y}
          }

          do_layout(nested_layout, children)

        "\n" ->
          layout

        list ->
          if is_list(list) do
            do_layout(layout, list)
          else
            layout
          end
      end
    end)
  end

  defp translate_and_render(layout, module, data, opts) do
    {l, t, r, b} = get_bounds(module, data, opts)
    {tx, _ty} = layout.translate

    layout =
      case fits_in_x?(r + layout.last_x + layout.padding, layout.width) do
        true ->
          x = l + layout.last_x + layout.padding
          y = layout.last_y

          %{
            layout
            | last_x: r + layout.last_x + layout.padding,
              graph:
                module.add_to_graph(layout.graph, data, Keyword.merge(opts, translate: {x, y}))
          }

        false ->
          x = l + tx + layout.padding
          y = t + layout.last_y + layout.largest_height + layout.padding

          %{
            layout
            | last_x: l + tx + r + layout.padding,
              last_y: t + layout.last_y + layout.largest_height + layout.padding,
              graph:
                module.add_to_graph(layout.graph, data, Keyword.merge(opts, translate: {x, y}))
          }
      end

    layout = if r > layout.largest_width, do: %{layout | largest_width: r}, else: layout
    if b > layout.largest_height, do: %{layout | largest_height: b}, else: layout
  end

  defp get_bounds(mod, data, opts) do
    mod.bounds(data, opts)
  end

  defp fits_in_x?(potential_x, max_x), do: potential_x <= max_x

  # defp fits_in_y?(potential_y, max_y), do: potential_y <= max_y
end