defmodule Romeo.Connection do
  @moduledoc false

  @timeout 5_000
  @default_transport Romeo.Transports.TCP

  alias Romeo.Connection.Features

  defstruct component: false,
            features: %Features{},
            host: nil,
            jid: nil,
            legacy_tls: false,
            nickname: "",
            owner: nil,
            parser: nil,
            password: nil,
            port: nil,
            preferred_auth_mechanisms: [],
            require_tls: false,
            resource: "",
            rooms: [],
            ssl_opts: [],
            socket: nil,
            socket_opts: [],
            stream_id: nil,
            timeout: nil,
            transport: nil

  use Connection

  require Logger

  ### PUBLIC API ###

  @doc """
  Start the connection process and connect to an XMPP server.

  ## Options

    * `:component` - Connect as an [XMPP Component][0] (default: `false`);
    * `:host` - Server hostname (default: inferred by the JID);
    * `:jid` - User jabber ID;
    * `:password` - User password;
    * `:port` - Server port (default: based on the transport);
    * `:require_tls` - Set to `false` if ssl should not be used (default: `true`);
    * `:ssl_opts` - A list of ssl options, see ssl docs;
    * `:socket_opts` - Options to be given to the underlying socket;
    * `:timeout` - Connect timeout in milliseconds (default: `#{@timeout}`);
    * `:transport` - Transport handles the protocol (default: `#{@default_transport}`);

  [0]: http://xmpp.org/extensions/xep-0114.html
  """
  def start_link(args, options \\ []) do
    args =
      args
      |> Keyword.put_new(:timeout, @timeout)
      |> Keyword.put_new(:transport, @default_transport)
      |> Keyword.put(:owner, self())

    Connection.start_link(__MODULE__, struct(__MODULE__, args), options)
  end

  @doc """
  Send a message via the underlying transport.
  """
  def send(pid, data) do
    Connection.call(pid, {:send, data})
  end

  @doc """
  Stop the process and disconnect.

  ## Options

    * `:timeout` - Call timeout (default: `#{@timeout}`)
  """
  @spec close(pid, Keyword.t) :: :ok
  def close(pid, opts \\ []) do
    Connection.call(pid, :close, opts[:timeout] || @timeout)
  end

  ## Connection callbacks

  def init(conn) do
    {:connect, :init, conn}
  end

  def connect(_, %{transport: transport, timeout: timeout} = conn) do
    case transport.connect(conn) do
      {:ok, conn} ->
        {:ok, conn}
      {:error, _} ->
        {:backoff, timeout, conn}
    end
  end

  def disconnect(info, %{socket: socket, transport: transport} = conn) do
    transport.disconnect(info, socket)
    {:connect, :reconnect, reset_connection(conn)}
  end

  defp reset_connection(conn) do
    %{conn | features: %Features{}, parser: nil, socket: nil}
  end

  def handle_call(_, _, %{socket: nil} = conn) do
    {:reply, {:error, :closed}, conn}
  end
  def handle_call({:send, data}, _, %{transport: transport} = conn) do
    case transport.send(conn, data) do
      {:ok, conn} ->
        {:reply, :ok, conn}
      {:error, _} = error ->
        {:disconnect, error, error, conn}
    end
  end
  def handle_call(:close, from, %{socket: socket, transport: transport} = conn) do
    transport.disconnect({:close, from}, socket)
    {:reply, :ok, conn}
  end

  def handle_info(info, %{owner: owner, transport: transport} = conn) do
    case transport.handle_message(info, conn) do
      {:ok, conn, :more} ->
        {:noreply, conn}
      {:ok, conn, stanza} ->
        stanza = Romeo.Stanza.Parser.parse(stanza)
        Kernel.send(owner, {:stanza, stanza})
        {:noreply, conn}
      {:error, _} = error ->
        {:disconnect, error, conn}
      :unknown ->
        Logger.debug fn ->
          [inspect(__MODULE__), ?\s, inspect(self()), " received message: " | inspect(info)]
        end
        {:noreply, conn}
    end
  end
end
