defmodule WebAssembly.Core do
  @moduledoc false

  defmodule Engine do
    @moduledoc """
    Elements-assembling engine used internally by `WebAssembly.Builder` and `WebAssembly.DSL`.
    """

    @s __MODULE__
    @pid_key :webassembly_engine_pid

    defstruct scopes: [], result: nil

    require WebAssembly.Types, as: T

    defp pid(), do: Process.get(@pid_key)


    @doc """
    Fire up the engine accessible within the current Erlang process.
    """
    @spec fire() :: :ok
    def fire() do
      {:ok, pid} = Agent.start_link(fn -> %@s{} end)
      Process.put(@pid_key, pid)
      :ok
    end

    @doc """
    Open new scope.
    """
    @spec new_scope() :: :ok
    def new_scope() do
      Agent.update(pid, fn %{scopes: scopes} = state -> 
        %{ state | scopes: [[]|scopes] }
      end)
    end

    @doc """
    Push `value` into the current scope.
    """
    @spec push(T.content) :: :ok
    def push(value) do
      Agent.update(pid, fn %{scopes: [scope|prevs]} = state ->
        %{ state | scopes: [[value|scope] | prevs] }
      end)
    end

    @doc """
    Release the current scope, merging it with the uplevel one.

    If this was the last scope, prepare the engine for `return/0`.
    """
    @spec release_scope() :: :ok
    def release_scope() do
      import Enum, only: [reverse: 1]

      Agent.update(pid, fn
        %{scopes: [scope1,scope0|prevs]} = state ->
          scope_merged = [reverse(scope1) | scope0]
          %{ state | scopes: [scope_merged | prevs] }

        %{scopes: [scope]} ->
          %@s{ result: reverse(scope), scopes: :released }
      end)
    end

    @doc """
    Shutdown the engine and return the assembled structure.
    """
    @spec return() :: [T.content]
    def return() do
      result = Agent.get(pid, fn
        %{scopes: :released, result: res} ->
        res
      end)
      :ok = Agent.stop(pid)
      result
    end
  end
end
