lib/kalevala/character/conn.ex

defmodule Kalevala.Character.Conn.Private do
  @moduledoc false

  alias Kalevala.World.Room

  defstruct [
    :event_router,
    :next_controller,
    :request_id,
    :update_character,
    actions: [],
    channel_changes: [],
    halt?: false
  ]

  @doc """
  Generate a request id to track actions past usage in the conn
  """
  def generate_request_id() do
    bytes =
      Enum.reduce(1..16, <<>>, fn _, bytes ->
        bytes <> <<Enum.random(0..255)>>
      end)

    Base.encode16(bytes, case: :lower)
  end

  @doc false
  def character(conn) do
    character = conn.private.update_character || conn.character

    case is_nil(character) do
      true ->
        nil

      false ->
        meta = Kalevala.Meta.trim(character.meta)
        %{character | brain: :trimmed, inventory: :trimmed, meta: meta}
    end
  end

  @doc false
  def default_event_router(conn) do
    case character(conn) do
      nil ->
        nil

      character ->
        {:global, name} = Room.global_name(character.room_id)
        :global.whereis_name(name)
    end
  end
end

defmodule Kalevala.Character.Conn.Event do
  @moduledoc """
  Send an out of band Event
  """

  @derive Jason.Encoder
  defstruct [:topic, :data]
end

defmodule Kalevala.Character.Conn.EventText do
  @moduledoc """
  An event that also includes text output

  Geared towards text suppression in the web client, to allow for
  text to be rendered for telnet and send an event in the web client.
  """

  @derive Jason.Encoder
  defstruct [:data, :text, :topic]
end

defmodule Kalevala.Character.Conn.IncomingEvent do
  @moduledoc """
  Receive an out of band event

  Telnet: via GMCP
  Websocket: anything not `system/send`
  """

  @derive Jason.Encoder
  defstruct [:topic, :data]
end

defmodule Kalevala.Character.Conn.Text do
  @moduledoc """
  Struct to print text

  Used to determine if a new line should be sent before sending out
  new text.
  """

  defstruct [:data, newline: false, go_ahead: false]
end

defmodule Kalevala.Character.Conn.Option do
  @moduledoc """
  Telnet option
  """

  defstruct [:name, :value]
end

defmodule Kalevala.Character.Conn do
  @moduledoc """
  Struct for tracking data being processed in a controller or command
  """

  @type t() :: %__MODULE__{}

  alias Kalevala.Character.Conn.Private
  alias Kalevala.Meta

  defstruct [
    :character,
    :params,
    assigns: %{},
    events: [],
    output: [],
    options: [],
    private: %Private{},
    session: %{}
  ]

  @doc false
  def event_router(conn = %{private: %{event_router: nil}}),
    do: Private.default_event_router(conn)

  def event_router(%{private: %{event_router: event_router}}), do: event_router

  # Push text back to the user
  defp push(conn, event = %Kalevala.Character.Conn.Event{}, _newline) do
    Map.put(conn, :output, conn.output ++ [event])
  end

  defp push(conn, event = %Kalevala.Character.Conn.EventText{}, newline) do
    text = %Kalevala.Character.Conn.Text{
      data: event.text,
      newline: newline
    }

    event = %{event | text: text}

    Map.put(conn, :output, conn.output ++ [event])
  end

  defp push(conn, data, newline) do
    text = %Kalevala.Character.Conn.Text{
      data: data,
      newline: newline
    }

    Map.put(conn, :output, conn.output ++ [text])
  end

  defp merge_assigns(conn, assigns) do
    conn.session
    |> Map.put(:character, Private.character(conn))
    |> Map.merge(conn.assigns)
    |> Map.merge(assigns)
  end

  @doc """
  Get the character out of the conn

  If the character has been updated, this character will be returned
  """
  def character(conn, opts \\ [])

  def character(conn, trim: true), do: Private.character(conn)

  def character(conn, _), do: conn.private.update_character || conn.character

  @doc """
  Render text to the conn
  """
  def render(conn, view, template, assigns \\ %{}) do
    assigns = merge_assigns(conn, assigns)
    data = view.render(template, assigns)
    push(conn, data, false)
  end

  @doc """
  Render a prompt to the conn
  """
  def prompt(conn, view, template, assigns \\ %{}) do
    assigns = merge_assigns(conn, assigns)
    data = view.render(template, assigns)
    push(conn, data, true)
  end

  @doc """
  Add to the assignment map on the conn
  """
  def assign(conn, key, value) do
    assigns = Map.put(conn.assigns, key, value)
    Map.put(conn, :assigns, assigns)
  end

  @doc """
  Put a value into the session data
  """
  def put_session(conn, key, value) do
    session = Map.put(conn.session, key, value)
    Map.put(conn, :session, session)
  end

  @doc """
  Get a value out of the session data
  """
  def get_session(conn, key), do: Map.get(conn.session, key)

  @doc """
  Put the new controller that the foreman should swap to
  """
  def put_controller(conn, controller) do
    put_private(conn, :next_controller, controller)
  end

  @doc """
  Mark the connection for termination
  """
  def halt(conn) do
    private = Map.put(conn.private, :halt?, true)
    Map.put(conn, :private, private)
  end

  @doc """
  Send the foreman an in-game event
  """
  def event(conn, topic, data \\ %{}) do
    event = %Kalevala.Event{
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: topic,
      data: data
    }

    Map.put(conn, :events, conn.events ++ [event])
  end

  @doc """
  Delay an event
  """
  def delay_event(conn, delay, topic, data \\ %{}) do
    event = %Kalevala.Event.Delayed{
      delay: delay,
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: topic,
      data: data
    }

    Map.put(conn, :events, conn.events ++ [event])
  end

  @doc """
  Send a telnet option
  """
  def send_option(conn, name, value) when is_boolean(value) do
    option = %Kalevala.Character.Conn.Option{name: name, value: value}
    Map.put(conn, :options, conn.options ++ [option])
  end

  @doc """
  Request the room to move via the exit
  """
  def request_movement(conn, exit_name) do
    event = %Kalevala.Event{
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: Kalevala.Event.Movement.Request,
      data: %Kalevala.Event.Movement.Request{
        character: Private.character(conn),
        exit_name: exit_name
      }
    }

    event = Kalevala.Event.set_start_time(event)

    Map.put(conn, :events, conn.events ++ [event])
  end

  @doc """
  Creates an even to move from one room to another
  """
  def move(conn, direction, room_id, view, template, assigns) do
    assigns = merge_assigns(conn, assigns)
    data = view.render(template, assigns)

    event = %Kalevala.Event{
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: Kalevala.Event.Movement,
      data: %Kalevala.Event.Movement{
        character: Private.character(conn),
        direction: direction,
        reason: data,
        room_id: room_id
      }
    }

    Map.put(conn, :events, conn.events ++ [event])
  end

  @doc """
  Sends a request to drop an item into the room
  """
  def request_item_drop(conn, item_instance) do
    event = %Kalevala.Event{
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: Kalevala.Event.ItemDrop.Request,
      data: %Kalevala.Event.ItemDrop.Request{
        item_instance: item_instance
      }
    }

    Map.put(conn, :events, conn.events ++ [event])
  end

  @doc """
  Sends a request to get an item from the room
  """
  def request_item_pickup(conn, item_name) do
    event = %Kalevala.Event{
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: Kalevala.Event.ItemPickUp.Request,
      data: %Kalevala.Event.ItemPickUp.Request{
        item_name: item_name
      }
    }

    Map.put(conn, :events, conn.events ++ [event])
  end

  @doc """
  Update the character in state
  """
  def put_character(conn, character) do
    put_private(conn, :update_character, character)
  end

  @doc """
  Request to subscribe to a channel
  """
  def subscribe(conn, channel_name, options, error_fun) do
    options = Keyword.merge([character: Private.character(conn)], options)

    channel_changes = [
      {:subscribe, channel_name, options, error_fun} | conn.private.channel_changes
    ]

    put_private(conn, :channel_changes, channel_changes)
  end

  @doc """
  Request to unsubscribe from a channel
  """
  def unsubscribe(conn, channel_name, options, error_fun) do
    options = Keyword.merge([character: Private.character(conn)], options)

    channel_changes = [
      {:unsubscribe, channel_name, options, error_fun} | conn.private.channel_changes
    ]

    put_private(conn, :channel_changes, channel_changes)
  end

  @doc """
  Request to publish a message to a channel

  Available options for altering the message, these also get passed along to
  the channel callbacks.

  - `type`, defaults to `"speech"`
  - `meta`, defaults to `%{}`
  """
  def publish_message(conn, channel_name, text, options, error_fun) do
    event = %Kalevala.Event{
      acting_character: Private.character(conn),
      from_pid: self(),
      topic: Kalevala.Event.Message,
      data: %Kalevala.Event.Message{
        channel_name: channel_name,
        character: Private.character(conn),
        id: Kalevala.Event.Message.generate_id(),
        meta: Keyword.get(options, :meta, %{}),
        text: text,
        type: Keyword.get(options, :type, "speech")
      }
    }

    publish_channel_message(conn, channel_name, event, options, error_fun)
  end

  defp publish_channel_message(conn, channel_name, event, options, error_fun) do
    options = Keyword.merge([character: Private.character(conn)], options)

    channel_changes = [
      {:publish, channel_name, event, options, error_fun} | conn.private.channel_changes
    ]

    put_private(conn, :channel_changes, channel_changes)
  end

  @doc """
  Put an action to be performed

  This should be a `Kalevala.Character.Action` struct to save which
  action module should be run, along with any delay and params.
  """
  def put_action(conn, action = %Kalevala.Character.Action{}) do
    action = %{action | request_id: conn.private.request_id}
    put_private(conn, :actions, conn.private.actions ++ [action])
  end

  @doc """
  Update a key in a character's meta map
  """
  def put_meta(conn, key, value) do
    character = character(conn)
    meta = Meta.put(character.meta, key, value)
    character = %{character | meta: meta}
    put_character(conn, character)
  end

  defp put_private(conn, key, value) do
    private = Map.put(conn.private, key, value)
    Map.put(conn, :private, private)
  end
end