lib/bbox/client.ex

defmodule Bbox.Client do
  use GenServer
  alias Bbox.Peers
  require Logger

  @ip {127, 0, 0, 1}

  def start_link(state) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @impl true
  def init(state) do
    send(self(), :connect)
    {:ok, state}
  end

  @impl true
  def handle_info(:connect, state) do
    port = Keyword.fetch!(state, :port)

    peers =
      Peers.list!()
      |> Enum.map(&(&1.address |> String.split(":") |> Enum.at(1) |> String.to_integer()))
      |> Enum.filter(&(&1 != port))

    peers =
      peers
      |> Enum.map(fn peer ->
        case :gen_tcp.connect(@ip, peer, [:binary, active: true]) do
          {:ok, socket} ->
            {:ok, socket}

          {:error, reason} ->
            disconnect(state, reason)

            {:error, reason}
        end
      end)
      |> Enum.filter(&match?({:ok, _}, &1))
      |> Enum.map(fn {:ok, peer} -> peer end)

    Logger.info("connected to #{length(peers)} peers")

    {:noreply, state |> Keyword.put(:peers, peers)}
  end

  @impl true
  def handle_info({:tcp, _, packet}, state) do
    Logger.info("Received #{packet}")

    {:noreply, state}
  end

  @impl true
  def handle_info({:tcp_closed, _}, state) do
    {:stop, :normal, state}
  end

  @impl true
  def handle_info({:tcp_error, _}, state), do: {:stop, :normal, state}

  @impl true
  def handle_cast({:message_by_socket, {socket, message}}, state) do
    Logger.info("Sending #{message}")

    :ok = :gen_tcp.send(socket, message)
    {:noreply, state}
  end

  @impl true
  def handle_cast({:message_by_port, {port, message}}, state) do
    Logger.info("Sending #{message}")

    peer = get_peer(port)

    :gen_tcp.send(peer, message)

    {:noreply, state}
  end

  @impl true
  def handle_cast({:broadcast_message, message}, state) do
    list_peers()
    |> Enum.each(fn peer ->
      :gen_tcp.send(peer, message)
    end)

    {:noreply, state}
  end

  @impl true
  def handle_call({:get_peer, port}, _from, state) do
    peer =
      list_peers()
      |> Enum.find(fn peer ->
        {:ok, {_, p}} = :inet.peername(peer)

        p == port
      end)

    {:reply, peer, state}
  end

  @impl true
  def handle_call(:list_peers, _from, state), do: {:reply, Keyword.fetch!(state, :peers), state}

  def disconnect(state, reason) do
    Logger.info("Disconnected: #{reason}")
    {:stop, :normal, state}
  end

  def send_message_by_socket(params) do
    GenServer.cast(__MODULE__, {:message_by_socket, params})
  end

  def send_message_by_port(params) do
    GenServer.cast(__MODULE__, {:message_by_port, params})
  end

  def broadcast_message(message) do
    GenServer.cast(__MODULE__, {:broadcast_message, message})
  end

  def get_peer(port) do
    GenServer.call(__MODULE__, {:get_peer, port})
  end

  def list_peers() do
    GenServer.call(__MODULE__, :list_peers)
  end
end