lib/kalevala/telnet/protocol.ex

defmodule Kalevala.Telnet.Protocol do
  @moduledoc """
  ranch protocol for handling telnet connection
  """

  alias Kalevala.Character.Conn.Event
  alias Kalevala.Character.Conn.EventText
  alias Kalevala.Character.Conn.IncomingEvent
  alias Kalevala.Character.Conn.Text
  alias Kalevala.Character.Conn.Option
  alias Kalevala.Character.Foreman
  alias Kalevala.Output
  alias Telnet.Options

  require Logger

  @behaviour :ranch_protocol

  @impl true
  def start_link(ref, _socket, transport, opts) do
    # Use the special start link to get around a ranch deadlock
    # on `:ranch.handshake/1`
    pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, transport, opts])
    {:ok, pid}
  end

  @doc false
  def init(ref, transport, options) do
    # See deadlock comment above
    {:ok, socket} = :ranch.handshake(ref)
    :ok = transport.setopts(socket, active: true)
    send(self(), :init)

    protocol_options = Enum.into(options.protocol, %{})

    state = %{
      socket: socket,
      transport: transport,
      output_processors: protocol_options.output_processors,
      buffer: <<>>,
      foreman_pid: nil,
      foreman_options: options[:foreman],
      options: %{
        newline: false
      }
    }

    :gen_server.enter_loop(__MODULE__, [], state)
  end

  def handle_info(:init, state) do
    {:ok, foreman_pid} = Foreman.start_player(self(), state.foreman_options)
    state = Map.put(state, :foreman_pid, foreman_pid)
    {:noreply, state, {:continue, :initial_iacs}}
  end

  def handle_info({:tcp, _socket, data}, state) do
    process_data(state, data)
  end

  def handle_info({:ssl, _socket, data}, state) do
    process_data(state, data)
  end

  def handle_info({:tcp_closed, _socket}, state) do
    handle_info(:terminate, state)
  end

  def handle_info({:ssl_closed, _socket}, state) do
    handle_info(:terminate, state)
  end

  def handle_info(:terminate, state) do
    Logger.info("Session terminating")
    send(state.foreman_pid, :terminate)
    {:stop, :normal, state}
  end

  def handle_info({:send, data}, state) do
    data = List.wrap(data)

    state =
      Enum.reduce(data, state, fn data, state ->
        push(state, data)
      end)

    {:noreply, state}
  end

  def handle_continue(:initial_iacs, state) do
    # WILL GMCP
    state.transport.send(state.socket, <<255, 251, 201>>)
    # DO OAuth
    state.transport.send(state.socket, <<255, 253, 165>>)
    # DO NEW-ENVIRON
    state.transport.send(state.socket, <<255, 253, 39>>)
    {:noreply, state}
  end

  defp push(state, output = %Event{}) do
    data = <<255, 250, 201>>
    data = data <> output.topic <> " "
    data = data <> Jason.encode!(output.data)
    data = data <> <<255, 240>>

    state.transport.send(state.socket, data)
    state
  end

  defp push(state, output = %EventText{}) do
    event = %Event{
      topic: output.topic,
      data: output.data
    }

    state
    |> push(output.text)
    |> push(event)
  end

  defp push(state, output = %Text{}) do
    push_text(state, output.data)
    if output.go_ahead, do: state.transport.send(state.socket, <<255, 249>>)
    update_newline(state, output.newline)
  end

  defp push(state, output = %Option{name: :echo}) do
    case output.value do
      true ->
        state.transport.send(state.socket, <<255, 251, 1>>)

      false ->
        state.transport.send(state.socket, <<255, 252, 1>>)
    end

    state
  end

  defp push_text(state, text) do
    text =
      Enum.reduce(state.output_processors, text, fn processor, text ->
        Output.process(text, processor)
      end)

    case state.options.newline do
      true ->
        state.transport.send(state.socket, ["\n", text])

      false ->
        state.transport.send(state.socket, text)
    end
  end

  defp process_data(state, data) do
    {options, string, buffer} = Options.parse(state.buffer <> data)
    state = %{state | buffer: buffer}

    Enum.each(options, fn option ->
      process_option(state, option)
    end)

    send(state.foreman_pid, {:recv, :text, string})

    {:noreply, update_newline(state, String.length(string) == 0)}
  end

  defp process_option(state, {:gmcp, topic, data}) do
    send(state.foreman_pid, {:recv, :event, %IncomingEvent{topic: topic, data: data}})
  end

  defp process_option(_state, _option), do: :ok

  defp update_newline(state, status) do
    %{state | options: %{state.options | newline: status}}
  end
end