lib/component.ex

defmodule SnapFramework.Component do
  alias Scenic.Graph
  alias Scenic.Primitive
  require SnapFramework.Macros
  require Logger

  @moduledoc """
  ## Overview

  SnapFramework.Component is nearly identical to a Scene. The main different is the addition of the defcomponent macro,
  as well as the addition of the scenic opts key.
  defcomponent build out your scenic validate function and helper functions, automatically so you don't have to.

  ``` elixir
  defmodule Example.Component.MyComponent do
    use SnapFramework.Component,
      name: :my_component,
      template: "lib/scenes/my_component.eex",
      controller: :none,
      assigns: [],
      opts: []

    defcomponent :my_component, :tuple
  end
  ```

  The above example defines a component that takes in a tuple for data. and build your helper function defined as ```my_component/3```

  ## Templates

  Component templates also have an additional feature that primitives or scene templates do not have. You can inject children into them.
  Lets write a basic icon button component that takes an icon child.

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

  <%= primitive Scenic.Primitive.Circle,
      15,
      id: :bg,
      translate: {23, 23}
  %>

  @children

  # component module
  defmodule Example.Component.IconButton do
    use SnapFramework.Component,
      name: :icon_button,
      template: "lib/icons/icon_button/icon_button.eex",
      controller: :none,
      assigns: [],
      opts: []

    defcomponent :icon_button, :any
  end
  ```

  Now lets see how to use this component with children in a scene. This assumes we've already made an icon component.

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

    <%= component Example.Component.IconButton,
        nil,
        id: :icon_button
    do %>
      <%= component Example.Component.Icon,
          @icon,
          id: :icon
      %>
    <% end %>
  ```

  That's all there is to putting children in components!
  """

  defmacro __using__([name: name, template: template, controller: controller, assigns: assigns, opts: opts]) do
    quote do
      use SnapFramework.Scene,
        name: unquote(name),
        template: unquote(template),
        controller: unquote(controller),
        assigns: unquote(assigns),
        opts: unquote(opts)

      import SnapFramework.Component
      alias Scenic.Primitives
      require SnapFramework.Macros
      require EEx
      require Logger

      Module.register_attribute(__MODULE__, :assigns, persist: true)

      Module.register_attribute(__MODULE__, :preload, persist: true)
      @preload unquote(opts[:preload]) || true

      @before_compile SnapFramework.Component
    end
  end

  defmacro defcomponent(name, data_type) do
    quote location: :keep do
      case unquote(data_type) do
        :string ->
          def validate(data) when is_bitstring(data), do: {:ok, data}
          def validate(_), do: :invalid_data
        :number ->
          def validate(data) when is_number(data), do: {:ok, data}
          def validate(_), do: :invalid_data
        :list ->
          def validate(data) when is_list(data), do: {:ok, data}
          def validate(_), do: :invalid_data
        :map ->
          def validate(data) when is_map(data), do: {:ok, data}
          def validate(_), do: :invalid_data
        :atom ->
          def validate(data) when is_atom(data), do: {:ok, data}
          def validate(_), do: :invalid_data
        :tuple ->
          def validate(data) when is_tuple(data), do: {:ok, data}
          def validate(_), do: :invalid_data
        :any ->
          def validate(data), do: {:ok, data}
        _ ->
          def validate(data), do: {:ok, data}
      end

      def unquote(name)(graph, data, options \\ [])

      def unquote(name)(%Graph{} = g, data, options) do
        add_to_graph(g, data, options)
      end

      def unquote(name)(%Scenic.Primitive{module: Scenic.Primitive.Component, data: {mod, _, id}} = p, data, options) do
        data =
          case mod.validate(data) do
            {:ok, data} -> data
            {:error, msg} -> raise msg
          end

        Primitive.put(p, {__MODULE__, data, id}, options)
      end

      def unquote(name)(%Scenic.Primitive{module: mod} = p, data, opts) do
        data =
          case mod.validate(data) do
            {:ok, data} -> data
            {:error, error} -> raise Exception.message(error)
          end

        Primitive.put(p, data, opts)
      end
    end
  end

  defmacro __before_compile__(env) do
    caller = __CALLER__
    template = Module.get_attribute(env.module, :template)
    preload = Module.get_attribute(env.module, :preload)
    file = if preload, do: File.read!(template), else: nil

    quote location: :keep do
      def init(scene, data, opts \\ []) do
        assigns = Keyword.merge(@assigns, [
          module: unquote(caller.module),
          data: data,
          opts: opts,
          children: opts[:children]
        ])
        scene =
          scene
          |> assign(assigns)
          |> setup()
          |> compile()
          |> mounted()

        {:ok, scene}
      end

      def compile(scene) do
        info =
          Keyword.merge(
            [
              assigns: scene.assigns,
              engine: SnapFramework.Engine
            ],
            [
              file: unquote(caller.file),
              line: unquote(caller.line), trim: true
            ]
          )
        compiled_list =
          SnapFramework.Engine.compile_string(unquote(file), [
            assigns: scene.assigns
          ], info, __ENV__)

        graph = SnapFramework.Engine.Builder.build_graph(compiled_list)

        scene
        |> assign(graph: graph)
        |> push_graph(graph)
      end

      # SnapFramework.Macros.effect_handlers()
      SnapFramework.Macros.effect_handlers()
    end
  end
end