defmodule Grizzly.Transport do
  @moduledoc """
  Behaviour and functions for communicating to `zipgateway`
  """

  alias Grizzly.ZIPGateway

  defmodule Response do
    @moduledoc """
    The response from parse response
    """

    alias Grizzly.ZWave.Command

    @type t() :: %__MODULE__{
            port: :inet.port_number() | nil,
            ip_address: :inet.ip_address() | nil,
            command: Command.t()
          }

    @enforce_keys [:command]
    defstruct port: nil, ip_address: nil, command: nil
  end

  alias Grizzly.ZWave.{Command, DecodeError}

  @opaque t() :: %__MODULE__{impl: module(), assigns: map()}

  @type socket() :: :ssl.sslsocket() | :inet.socket()

  @type args() :: [
          ip_address: :inet.ip_address(),
          port: :inet.port_number(),
          node_id: Grizzly.node_id(),
          transport: t()
        ]

  @type parse_opt() :: {:raw, boolean()}

  @typedoc """
  After starting a server options can be passed back to the caller so that the
  caller can do any other work it might seem fit.

  Options:

    * - `:strategy` - this informs the caller if the transport needs to wait for
        connects to accept or if the socket can just process incoming messages.
        If the strategy is `:accept` that is to mean the socket is okay to start
        accepting new connections.
  """
  @type listen_option() :: {:strategy, :none | :accept}

  @enforce_keys [:impl]
  defstruct assigns: %{}, impl: nil

  @callback open(keyword()) :: {:ok, t()} | {:error, :timeout}

  @callback listen(t()) :: {:ok, t(), [listen_option()]} | {:error, any()}

  @callback accept(t()) :: {:ok, t()} | {:error, any()}

  @callback handshake(t()) :: {:ok, t()} | {:error, any()}

  @callback send(t(), binary(), keyword()) :: :ok

  @callback peername(t()) :: {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, any()}

  @callback parse_response(any(), [parse_opt()]) ::
              {:ok, Response.t() | binary() | :connection_closed} | {:error, DecodeError.t()}

  @callback close(t()) :: :ok

  @doc """
  Make a new `Grizzly.Transport`

  If need to optionally assign some priv data you can map that into this function.
  """
  @spec new(module(), map()) :: t()
  def new(impl, assigns \\ %{}) do
    %__MODULE__{
      impl: impl,
      assigns: assigns
    }
  end

  @doc """
  Update the assigns with this field and value
  """
  @spec put(t(), atom(), any()) :: t()
  def put(transport, assign, assign_value) do
    new_assigns = Map.put(transport.assigns, assign, assign_value)

    %__MODULE__{transport | assigns: new_assigns}
  end

  @doc """
  Get the assign value for the field
  """
  @spec get(t(), atom(), any()) :: any()
  def get(transport, assign, default \\ nil),
    do: Map.get(transport.assigns, assign, default)

  @doc """
  Listen using a transport
  """
  @spec listen(t()) :: {:ok, t(), [listen_option()]} | {:error, any()}
  def listen(transport) do
    %__MODULE__{impl: transport_impl} = transport

    transport_impl.listen(transport)
  end

  @doc """
  Accept a new connection
  """
  @spec accept(t()) :: {:ok, t()} | {:error, any()}
  def accept(transport) do
    %__MODULE__{impl: transport_impl} = transport

    transport_impl.accept(transport)
  end

  @doc """
  Preform the handshake
  """
  @spec handshake(t()) :: {:ok, t()} | {:error, any()}
  def handshake(transport) do
    %__MODULE__{impl: transport_impl} = transport

    transport_impl.handshake(transport)
  end

  @doc """
  Open the transport
  """
  @spec open(module(), args()) :: {:ok, t()} | {:error, :timeout}
  def open(transport_module, args) do
    transport_module.open(args)
  end

  @doc """
  Send binary data using a transport
  """
  @spec send(t(), binary(), keyword()) :: :ok
  def send(transport, binary, opts \\ []) do
    %__MODULE__{impl: transport_impl} = transport

    transport_impl.send(transport, binary, opts)
  end

  @spec peername(t()) :: {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, any()}
  def peername(transport) do
    %__MODULE__{impl: transport_impl} = transport
    transport_impl.peername(transport)
  end

  @spec node_id(t()) :: {:ok, Grizzly.node_id()} | {:error, any()}
  def node_id(transport) do
    case get(transport, :node_id) || peername(transport) do
      {:ok, {ip, _}} -> {:ok, ZIPGateway.node_id_from_ip(ip)}
      {:error, reason} -> {:error, reason}
      node_id -> {:ok, node_id}
    end
  end

  @doc """
  Parse the response for the transport
  """
  @spec parse_response(t(), any()) ::
          {:ok, Response.t() | binary() | :connection_closed} | {:error, DecodeError.t()}
  def parse_response(transport, response, opts \\ []) do
    %__MODULE__{impl: transport_impl} = transport
    opts = [transport: transport] ++ opts

    transport_impl.parse_response(response, opts)
  end
end
