defmodule Chorex.Runtime do
  alias Chorex.RuntimeState
  import Chorex.RuntimeState

  defguard correct_session(m, s) when elem(m, 0) == s.session_token

  #
  # ----- GenServer functions -----
  #

  def init({actor_name, impl_name, return_to, session_token}) do
    state = %RuntimeState{
      # network configuration
      config: nil,
      # name of this actor
      actor: actor_name,
      # name of implementing module
      impl: impl_name,
      # session token
      session_token: session_token,
      # local variables
      vars: %{},
      # waiting messages
      inbox: :queue.new(),
      # call stack
      stack: [{:return, :finish_choreography, %{parent: return_to}}],
      # Number of barriers; pretends to be a stack
      barrier_depth: 0,
    }

    {:ok, state}
  end

  def handle_info({:config, config, init_args}, %RuntimeState{} = state) do
    args = init_args ++ [%{state | config: config}]
    apply(state.actor, :run, args)
  end

  def handle_info({:chorex, civ_tok, msg}, %RuntimeState{} = state)
      when correct_session(civ_tok, state) do
    {:noreply, push_inbox({civ_tok, msg}, state), {:continue, :try_recv}}
  end

  def handle_info({:choice, civ_tok, selection}, %RuntimeState{} = state)
      when correct_session(civ_tok, state) do
    {:noreply, push_inbox({civ_tok, {:choice, selection}}, state), {:continue, :try_recv}}
  end

  def handle_info({:recover, session_token, new_network, barrier_token, vars}, %RuntimeState{} = state)
      when session_token == state.session_token do
    # Unwind the stack to the corresponding recover frame
    new_stack =
      state.stack
      |> Enum.drop_while(fn
        ^barrier_token -> false
        _ -> true
      end)
      # knock off the recover frame
      |> Enum.drop(1)

    {:barrier, _, _, last_depth} = barrier_token
    new_depth = last_depth

    # Restore vars if given; see note in RuntimeMonitor.recover
    vars = if is_map(vars), do: vars, else: state.vars

    state = %{state | config: new_network, stack: new_stack, vars: vars, barrier_depth: new_depth}

    continue_on_stack(nil, state)
  end

  def handle_info({:revive, new_state}, _state) do
    continue_on_stack(nil, new_state)
  end

  def handle_info({:barrier, session_token, barrier_id, barrier_depth}, %RuntimeState{} = state)
      when session_token == state.session_token do
    # If we're getting the barrier early, something is *really* wrong.
    # Therefore, hard match here and blowup on failure.
    [{:barrier, ^session_token, ^barrier_id, ^barrier_depth}, {:recover, _} | rst_stack] = state.stack
    continue_on_stack(state.waiting_value, %{state | stack: rst_stack, barrier_depth: barrier_depth - 1})
  end

  def handle_continue(:try_recv, %RuntimeState{stack: [{:recv, _, _, _, _} | _]} = state) do
    # Run through state.inbox looking for something matching `(car state.stack)`
    [{:recv, civ_tok, match_func, cont_tok, vars} | rst_stack] = state.stack

    # Find the first thing in the queue matching `msg_pat` and drop it
    # the match_func gets generated by Chorex.project_sequence(Alice.(e1) ~> Bob.(e2))
    matcher =
      state.inbox
      |> :queue.to_list()
      |> Enum.find(fn
        {^civ_tok, _m} -> true
        _ -> false
      end)

    if matcher do
      # match found: drop from queue, continue on the frame with the new message

      matched_vars = match_func.(elem(matcher, 1))

      # Fold the matched vars into the saved variables from the stack
      # and update the state with the new variable map. Drop the matched message.
      vars = Map.merge(vars, matched_vars)
      new_state = %{drop_inbox(matcher, state) | stack: rst_stack, vars: vars}

      {:noreply, new_state, {:continue, {cont_tok, elem(matcher, 1)}}}
    else
      # No match found; keep waiting
      {:noreply, state}
    end
  end

  def handle_continue(:try_recv, %RuntimeState{} = state), do: {:noreply, state}

  def handle_continue({:return, ret_val}, %RuntimeState{} = state) do
    [{:return, cont_tok, vars} | rest_stack] = state.stack
    {:noreply, %{state | stack: rest_stack, vars: vars}, {:continue, {cont_tok, ret_val}}}
  end

  def handle_continue({:finish_choreography, ret_val}, %RuntimeState{} = state) do
    send(state.vars.parent, {:chorex_return, state.actor, ret_val})
    {:stop, :normal, nil}
  end

  defmacro __using__(_args) do
    quote do
      use GenServer
      alias Chorex.Runtime
      import Chorex.Runtime
      import Chorex.RuntimeState

      @impl true
      defdelegate init(start), to: Runtime

      @impl true
      defdelegate handle_info(msg, state), to: Runtime

      # Need to special-case these ones because they're defined by
      # Runtime. Can't use defdelegate because the impl needs to add
      # its own function clauses.
      @impl true
      def handle_continue(:try_recv, state), do: Runtime.handle_continue(:try_recv, state)
      def handle_continue({:return, _} = m, state), do: Runtime.handle_continue(m, state)

      def handle_continue({:finish_choreography, _} = m, state),
        do: Runtime.handle_continue(m, state)
    end
  end

  #
  # ----- Helper functions -----
  #

  # Looks at the stack and emits the proper return tuple
  @doc """
  Look at the top thing on the `RuntimeState` stack and return the
  proper response tuple.

  This function gets called every time a function in the Chorex actor
  GenServer needs to finish.
  """
  @spec continue_on_stack(any(), RuntimeState.t()) ::
          {:noreply, RuntimeState.t(), {:continue, any()}}
  def continue_on_stack(ret_val, state) do
    case state.stack do
      [{:recv, _, _, _, _} | _] ->
        # Variable splicing handled in the `handle_info` clause below.
        {:noreply, state, {:continue, :try_recv}}

      [{:return, _, _} | _] ->
        # Variable splicing handled in the `handle_info` clause below.
        {:noreply, state, {:continue, {:return, ret_val}}}

      [{:continue, ktok, vars} | rst] ->
        # This case is really simple: reset the variables.
        #
        # We reset the variables because the variables in this frame
        # were the ones in scope in the source that's become the
        # continuation we will be jumping to next.
        {:noreply, %{state | stack: rst, vars: vars}, {:continue, ktok}}

      [{:recover, recover_token} | rst] ->
        {:noreply, %{state | stack: rst}, {:continue, {:recover, recover_token}}}

      [{:barrier, _session, _id, _stack_depth} | _] ->
        # Don't do anything yet; wait for barrier to be lifted
        {:noreply, state}
    end
  end

  # DO NOT USE; DEMONSTRATION ONLY
  defmacro chorex_send(sender, receiver, civ, message) do
    config = Macro.var(:config, Chorex)
    state = Macro.var(:state, Chorex)

    quote do
      send(
        unquote(config)[unquote(receiver)],
        {:chorex,
         {unquote(state).session_token, unquote(civ), unquote(sender), unquote(receiver)},
         unquote(message)}
      )
    end
  end
end
