lib/library.ex

defmodule Lunar.Library do
  @moduledoc """
    Defines a library that will extend the Lua runtime.

    ## Example usage

      defmodule MyLibrary do
        use Lunar.Library, scope: "my_library"

        deflua hello(name) do
          "Hello, \#{name}!"
        end
      end

    In our Lua code, we can now call `my_library.hello("Robert")` and get back `"Hello, Robert!"`.
  """
  @type t :: __MODULE__

  @callback install(tuple()) :: tuple()
  @callback table() :: [tuple()]
  @callback scope() :: String.t()

  defmacro __using__(opts) do
    scope = Keyword.fetch!(opts, :scope)

    quote do
      require Record
      Record.defrecord(:erl_mfa, Record.extract(:erl_mfa, from_lib: "luerl/include/luerl.hrl"))

      Module.register_attribute(__MODULE__, :lua_functions, accumulate: true, persist: true)

      import Lunar.Library, only: [deflua: 2]

      @before_compile unquote(__MODULE__)

      @behaviour Lunar.Library

      @impl Lunar.Library
      def install(luerl_state) do
        :luerl_heap.alloc_table(table(), luerl_state)
      end

      @impl Lunar.Library
      def scope, do: unquote(scope)
    end
  end

  defmacro __before_compile__(_) do
    quote do
      @impl Lunar.Library
      def table do
        functions =
          :functions
          |> __MODULE__.__info__()
          |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))

        Enum.flat_map(@lua_functions, fn {func, wrapped_func} ->
          functions
          |> Map.get(func, [])
          |> Enum.map(&{to_string(func), erl_mfa(m: __MODULE__, f: wrapped_func, a: &1)})
        end)
      end
    end
  end

  defmacro deflua(call, do: body) do
    {func, _line, _args} = call

    # credo:disable-for-next-line
    wrapped_func = String.to_atom("__wrapped_#{func}")

    quote do
      @lua_functions {unquote(func), unquote(wrapped_func)}
      def unquote(call) do
        unquote(body)
      end

      def unquote(wrapped_func)(_arity, args, state) do
        :telemetry.execute([:lunar, :deflua, :invocation], %{count: 1}, %{
          args: args,
          function_name: unquote(func),
          scope: __MODULE__.scope(),
          module_name: __MODULE__
        })

        res = apply(__MODULE__, unquote(func), args)
        {[res], state}
      end
    end
  end
end