lib/kalevala/character/foreman.ex

defmodule Kalevala.Character.Foreman do
  @moduledoc """
  Session Foreman

  Manages data flowing from the player into the game.
  """

  use GenServer

  require Logger

  alias Kalevala.Character.Conn
  alias Kalevala.Event
  alias Kalevala.Character.Foreman.Channel

  @type t() :: %__MODULE__{}

  defstruct [
    :callback_module,
    :character,
    :communication_module,
    :controller,
    :supervisor_name,
    processing_action: nil,
    action_queue: [],
    private: %{},
    session: %{}
  ]

  @doc """
  Start a new foreman for a connecting player
  """
  def start_player(protocol_pid, options) do
    options =
      Keyword.merge(options,
        callback_module: Kalevala.Character.Foreman.Player,
        protocol: protocol_pid
      )

    DynamicSupervisor.start_child(options[:supervisor_name], {__MODULE__, options})
  end

  @doc """
  Start a new foreman for a non-player (character run by the world)
  """
  def start_non_player(options) do
    options = Keyword.merge(options, callback_module: Kalevala.Character.Foreman.NonPlayer)
    DynamicSupervisor.start_child(options[:supervisor_name], {__MODULE__, options})
  end

  @doc false
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, [])
  end

  @impl true
  def init(opts) do
    opts = Enum.into(opts, %{})

    state = %__MODULE__{
      callback_module: opts.callback_module,
      communication_module: opts.communication_module,
      controller: opts.initial_controller,
      supervisor_name: opts.supervisor_name
    }

    state = opts.callback_module.init(state, opts)

    {:ok, state, {:continue, :init_controller}}
  end

  @doc false
  def new_conn(state) do
    %Conn{
      character: state.character,
      session: state.session,
      private: %Conn.Private{
        request_id: Conn.Private.generate_request_id()
      }
    }
  end

  @impl true
  def handle_continue(:init_controller, state) do
    new_conn(state)
    |> state.controller.init()
    |> handle_conn(state)
  end

  @impl true
  def handle_info({:recv, :text, data}, state) do
    new_conn(state)
    |> state.controller.recv(data)
    |> handle_conn(state)
  end

  def handle_info({:recv, :event, event}, state) do
    new_conn(state)
    |> state.controller.recv_event(event)
    |> handle_conn(state)
  end

  def handle_info(event = %Event{}, state) do
    new_conn(state)
    |> state.controller.event(event)
    |> handle_conn(state)
  end

  def handle_info({:route, event = %Event{}}, state) do
    new_conn(state)
    |> Map.put(:events, [event])
    |> handle_conn(state)
  end

  def handle_info(event = %Event.Delayed{}, state) do
    event = Event.Delayed.to_event(event)

    new_conn(state)
    |> Map.put(:events, [event])
    |> handle_conn(state)
  end

  def handle_info(event = %Event.Display{}, state) do
    new_conn(state)
    |> state.controller.display(event)
    |> handle_conn(state)
  end

  def handle_info({:process_action, action}, state) do
    case state.processing_action == action do
      true ->
        Logger.info(
          "Processing #{inspect(action.type)}, #{Enum.count(state.action_queue)} left in the queue.",
          request_id: action.request_id
        )

        state = Map.put(state, :processing_action, nil)

        new_conn(state)
        |> action.type.run(action.params)
        |> handle_conn(state)

      false ->
        Logger.warn("Character tried processing an action that was not next", type: :foreman)

        {:noreply, state}
    end
  end

  def handle_info(:terminate, state) do
    state.callback_module.terminating(state)
    DynamicSupervisor.terminate_child(state.supervisor_name, self())
    {:noreply, state}
  end

  @doc """
  Handle the conn struct after processing
  """
  def handle_conn(conn, state) do
    conn
    |> Channel.handle_channels(state)
    |> send_options(state)
    |> send_output(state)
    |> send_events()

    session = Map.merge(state.session, conn.session)

    state =
      state
      |> Map.put(:session, session)
      |> Map.put(:action_queue, state.action_queue ++ conn.private.actions)

    case conn.private.halt? do
      true ->
        state.callback_module.terminate(state)
        {:noreply, state}

      false ->
        state
        |> handle_actions()
        |> update_character(conn)
        |> update_controller(conn)
    end
  end

  defp handle_actions(state = %{processing_action: nil, action_queue: [action | actions]}) do
    Logger.info(
      "Delaying #{inspect(action.type)} for #{action.delay}ms with #{inspect(action.params)}",
      request_id: action.request_id
    )

    Process.send_after(self(), {:process_action, action}, action.delay)

    state
    |> Map.put(:processing_action, action)
    |> Map.put(:action_queue, actions)
  end

  defp handle_actions(state), do: state

  defp send_options(conn, state) do
    state.callback_module.send_options(state, conn.options)

    conn
  end

  defp send_output(conn, state) do
    state.callback_module.send_output(state, conn.output)

    conn
  end

  @doc false
  def send_events(conn) do
    {events, delayed_events} =
      Enum.split_with(conn.events, fn event ->
        match?(%Kalevala.Event{}, event)
      end)

    Enum.each(delayed_events, fn delayed_event ->
      Process.send_after(self(), delayed_event, delayed_event.delay)
    end)

    case Conn.event_router(conn) do
      nil ->
        conn

      event_router ->
        Enum.each(events, fn event ->
          send(event_router, event)
        end)

        conn
    end
  end

  defp update_character(state, conn) do
    case is_nil(conn.private.update_character) do
      true ->
        state

      false ->
        state.callback_module.track_presence(state, conn)
        %{state | character: conn.private.update_character}
    end
  end

  defp update_controller(state, conn) do
    case is_nil(conn.private.next_controller) do
      true ->
        {:noreply, state}

      false ->
        state = %{state | controller: conn.private.next_controller}
        {:noreply, state, {:continue, :init_controller}}
    end
  end
end

defmodule Kalevala.Character.Foreman.Callbacks do
  @moduledoc """
  Callbacks for a integrating with the character foreman process
  """

  alias Kalevala.Character.Conn
  alias Kalevala.Character.Foreman

  @type state() :: Foreman.t()

  @typedoc "Options for starting the foreman process"
  @type opts() :: Keyword.t()

  @doc """
  Fill in state with any passed in options
  """
  @callback init(state(), opts()) :: state()

  @doc """
  Called when the foreman process is halted through a conn

  Perform whatever actions are required to start terminating.
  """
  @callback terminate(state()) :: :ok

  @doc """
  The process is terminating from a `:terminate` message

  Perform whatever is required before terminating.
  """
  @callback terminating(state()) :: :ok

  @doc """
  Send options to a connection process
  """
  @callback send_options(state(), list()) :: :ok

  @doc """
  Send text to a connection process
  """
  @callback send_output(state(), list()) :: :ok

  @doc """
  The character updated and presence should be tracked
  """
  @callback track_presence(state, Conn.t()) :: :ok
end

defmodule Kalevala.Character.Foreman.Player do
  @moduledoc """
  Callbacks for a player character
  """

  alias Kalevala.Character.Conn
  alias Kalevala.Character.Foreman
  alias Kalevala.Event

  @behaviour Kalevala.Character.Foreman.Callbacks

  defstruct [:protocol, :presence_module, :quit_view]

  @impl true
  def init(state, opts) do
    private = %__MODULE__{
      protocol: opts.protocol,
      presence_module: opts.presence_module,
      quit_view: opts.quit_view
    }

    %{state | private: private}
  end

  @impl true
  def terminate(state) do
    send(state.private.protocol, :terminate)
  end

  @impl true
  def terminating(%{character: nil}), do: :ok

  def terminating(state) do
    {quit_view, quit_template} = state.private.quit_view

    conn = Foreman.new_conn(state)

    event = %Event{
      topic: Event.Movement,
      data: %Event.Movement{
        character: Conn.Private.character(conn),
        direction: :from,
        reason: quit_view.render(quit_template, %{character: state.character}),
        room_id: state.character.room_id
      }
    }

    conn
    |> Map.put(:events, [event])
    |> Foreman.send_events()
  end

  @impl true
  def send_options(state, options) do
    Enum.each(options, fn option ->
      send(state.private.protocol, {:send, option})
    end)
  end

  @impl true
  def send_output(state, text) do
    Enum.each(text, fn line ->
      send(state.private.protocol, {:send, line})
    end)
  end

  @impl true
  def track_presence(state, conn) do
    state.private.presence_module.track(Conn.character(conn, trim: true))
  end
end

defmodule Kalevala.Character.Foreman.NonPlayer do
  @moduledoc """
  Callbacks for a non-player character
  """

  require Logger

  alias Kalevala.Character.Conn
  alias Kalevala.Character.Foreman
  alias Kalevala.Event

  @behaviour Kalevala.Character.Foreman.Callbacks

  defstruct [:quit_view]

  @impl true
  def init(state, opts) do
    Logger.info("Character starting - #{opts.character.id}")

    private = %__MODULE__{
      quit_view: opts.quit_view
    }

    %{state | character: %{opts.character | pid: self()}, private: private}
  end

  @impl true
  def terminate(state), do: state

  @impl true
  def terminating(%{character: nil}), do: :ok

  def terminating(state) do
    {quit_view, quit_template} = state.private.quit_view

    conn = Foreman.new_conn(state)

    event = %Event{
      topic: Event.Movement,
      data: %Event.Movement{
        character: Conn.Private.character(conn),
        direction: :from,
        reason: quit_view.render(quit_template, %{character: state.character}),
        room_id: state.character.room_id
      }
    }

    conn
    |> Map.put(:events, [event])
    |> Foreman.send_events()
  end

  @impl true
  def send_options(_state, _options), do: :ok

  @impl true
  def send_output(_state, _output), do: :ok

  @impl true
  def track_presence(_state, _conn), do: :ok
end