defmodule Xandra.Transport do
  @moduledoc false

  # Internal module to abstract away operations on transport sockets, independent from
  # the transport (TCP or SSL).

  @type options() :: [{atom(), term()} | atom()]
  @type error_reason() :: :inet.posix() | term()

  @type t() :: %__MODULE__{
          module: :gen_tcp | :ssl,
          options: options(),
          socket: :gen_tcp.socket() | :ssl.sslsocket() | nil
        }

  @enforce_keys [:module, :options]
  defstruct [:module, :options, :socket]

  defguard is_data_message(transport, msg)
           when is_tuple(msg) and tuple_size(msg) == 3 and elem(msg, 0) in [:tcp, :ssl] and
                  elem(msg, 1) == transport.socket

  defguard is_closed_message(transport, msg)
           when is_tuple(msg) and tuple_size(msg) == 2 and
                  elem(msg, 0) in [:tcp_closed, :ssl_closed] and
                  elem(msg, 1) == transport.socket

  defguard is_error_message(transport, msg)
           when is_tuple(msg) and tuple_size(msg) == 3 and
                  elem(msg, 0) in [:tcp_error, :ssl_error] and
                  elem(msg, 1) == transport.socket

  @spec connect(t(), :inet.ip_address() | charlist(), :inet.port_number(), timeout()) ::
          {:ok, t()} | {:error, error_reason()}
  def connect(%__MODULE__{socket: nil} = transport, address, port, timeout) do
    with {:ok, socket} <- transport.module.connect(address, port, transport.options, timeout) do
      {:ok, %__MODULE__{transport | socket: socket}}
    end
  end

  @spec send(t(), iodata()) :: :ok | {:error, error_reason()}
  def send(%__MODULE__{socket: socket, module: module}, data) when not is_nil(socket) do
    module.send(socket, data)
  end

  @spec recv(t(), integer(), timeout()) :: {:ok, binary()} | {:error, error_reason()}
  def recv(%__MODULE__{socket: socket, module: module}, bytes, timeout)
      when not is_nil(socket) and is_integer(bytes) do
    module.recv(socket, bytes, timeout)
  end

  @spec setopts(t(), options()) :: :ok | {:error, error_reason}
  def setopts(%__MODULE__{} = transport, options) when is_list(options) do
    inet_mod(transport.module).setopts(transport.socket, options)
  end

  @spec address_and_port(t()) ::
          {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, error_reason}
  def address_and_port(%__MODULE__{socket: socket} = transport) when not is_nil(socket) do
    inet_mod(transport.module).peername(socket)
  end

  @spec close(t()) :: t()
  def close(%__MODULE__{} = transport) do
    _ = transport.module.close(transport.socket)
    %__MODULE__{transport | socket: nil}
  end

  defp inet_mod(:gen_tcp), do: :inet
  defp inet_mod(:ssl), do: :ssl
end
