lib/engine/engine.ex

defmodule SnapFramework.Engine do
  @moduledoc """
  The EEx template engine.

  # Overview

  The SnapFramework Engine is responsible for parsing EEx templates and building graphs from them.

  You should always start a template with the a graph, and then add any components and primitives immediately after it.

  ``` elixir
  <%= graph font_size: 20 %>

  <%= component Scenic.Component.Button,
      "button text",
      id: :btn
  %>

  <%= primitive Scenic.Primitive.Rectangle,
      {100, 100},
      id: :rect,
      fill: :steel_blue
  %>
  ```

  In the above example you can see how simple it is to render component and primitives.

  # Layouts

  The templating engine also supports layouts.

  ``` elixir
  <%= graph font_size: 20 %>

  <%= layout padding: 100, width: 600, height: 600, translate: {100, 10} do %>
      <%= component Scenic.Component.Button, "test btn", id: :test_btn %>
      <%= component Scenic.Component.Button, "test btn", id: :test_btn %>
      <%= component Scenic.Component.Button, "test btn", id: :test_btn %>
      <%= component Scenic.Component.Button, "test btn", id: :test_btn %>
      <%= component Scenic.Component.Button, "test btn", id: :test_btn %>
      <%= component Scenic.Component.Button, "test btn", id: :test_btn %>

      <%= layout padding: 0, width: 600, height: 300, translate: {10, 10} do %>
          <%= component Scenic.Component.Input.Dropdown, {
                  @dropdown_opts,
                  @dropdown_value
              },
              id: :dropdown_1,
              z_index: 100
          %>

          <%= component Scenic.Component.Input.Dropdown, {
                  @dropdown_opts,
                  @dropdown_value
              },
              id: :dropdown_2
          %>
      <% end %>
  <% end %>
  ```

  The only required options on templates are `width` and `height`.
  Any nested layouts will have the padding and translate of the previous layout added onto it.

  Any components rendered within a layout are added directly to the graph. Which means you can modify them directly in the scene you're working in.
  There is no parent component that is rendered.
  """

  @behaviour EEx.Engine
  require Logger

  def encode_to_iodata!({:safe, body}), do: body
  def encode_to_iodata!(body) when is_binary(body), do: body

  def compile(path, assigns, info, _env) do
    quoted = EEx.compile_file(path, info)
    {result, _binding} = Code.eval_quoted(quoted, assigns)
    result
  end

  def compile_string(string, assigns, info, env) do
    quoted = EEx.compile_string(string, info)
    {result, _binding} = Code.eval_quoted(quoted, assigns, env)
    result
  end

  @doc false
  def init(opts) do
    %{
      iodata: [],
      dynamic: [],
      vars_count: 0,
      assigns: opts[:assigns] || []
    }
  end

  @doc false
  def handle_begin(state) do
    Macro.var(:assigns, __MODULE__)
    %{state | iodata: [], dynamic: []}
  end

  @doc false
  def handle_end(quoted) do
    quoted
    |> handle_body()
  end

  @doc false
  def handle_body(state) do
    %{iodata: iodata, dynamic: dynamic} = state
    safe = Enum.reverse(iodata)
    {:__block__, [], Enum.reverse([safe | dynamic])}
  end

  @doc false
  def handle_text(state, text) do
    handle_text(state, [], text)
  end

  @doc false
  def handle_text(state, _meta, text) do
    %{iodata: iodata} = state
    %{state | iodata: [text | iodata]}
  end

  @doc false
  def handle_expr(state, "=", ast) do
    ast = traverse(ast, state.assigns)
    %{iodata: iodata, dynamic: dynamic, vars_count: vars_count} = state
    var = Macro.var(:"arg#{vars_count}", __MODULE__)
    ast = quote do: unquote(var) = unquote(ast)
    %{state | dynamic: [ast | dynamic], iodata: [var | iodata], vars_count: vars_count + 1}
  end

  def handle_expr(state, "", ast) do
    ast = traverse(ast, state.assigns)
    %{dynamic: dynamic} = state
    %{state | dynamic: [ast | dynamic]}
  end

  def handle_expr(state, marker, ast) do
    EEx.Engine.handle_expr(state, marker, ast)
  end

  ## Traversal

  defp traverse(expr, assigns) do
    expr
    |> Macro.prewalk(&SnapFramework.Parser.Assigns.run(&1, assigns))
    # |> Macro.prewalk(&SnapFramework.Parser.Enumeration.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Layout.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Graph.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Component.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Primitive.run/1)
    # |> Macro.prewalk(&SnapFramework.Parser.Outlet.run(&1, assigns))
  end

  @doc false
  def fetch_assign!(assigns, key) do
    case Access.fetch(assigns, key) do
      {:ok, val} ->
        val

      :error ->
        raise ArgumentError, """
          assign @#{key} not available in eex template.

          Please make sure all proper assigns have been set. If this
          is a child template, ensure assigns are given explicitly by
          the parent template as they are not automatically forwarded.

          Available assigns: #{inspect(Enum.map(assigns, &elem(&1, 0)))}
          """
    end
  end
end