defmodule SMPPEX.Pdu.Factory do
  @moduledoc """
  Module for convenient generation of the most common PDUs.
  """

  alias SMPPEX.Protocol.CommandNames
  alias SMPPEX.Pdu
  alias SMPPEX.Pdu.MessageState
  alias SMPPEX.Pdu.TONNPIDefaults

  @spec bind_transmitter(String.t(), String.t(), map) :: Pdu.t()

  def bind_transmitter(system_id, password, opts \\ %{}) do
    bind(:bind_transmitter, system_id, password, opts)
  end

  @spec bind_receiver(String.t(), String.t(), map) :: Pdu.t()

  def bind_receiver(system_id, password, opts \\ %{}) do
    bind(:bind_receiver, system_id, password, opts)
  end

  @spec bind_transceiver(String.t(), String.t(), map) :: Pdu.t()

  def bind_transceiver(system_id, password, opts \\ %{}) do
    bind(:bind_transceiver, system_id, password, opts)
  end

  @spec bind(atom, String.t(), String.t(), map) :: Pdu.t()

  def bind(command_name, system_id, password, opts \\ %{})
      when command_name == :bind_transceiver or command_name == :bind_transmitter or
             command_name == :bind_receiver do
    mandatory =
      opts
      |> Map.put(:system_id, system_id)
      |> Map.put(:password, password)

    bind(command_name, mandatory)
  end

  @spec bind(atom, map) :: Pdu.t()

  def bind(command_name, opts)
      when command_name == :bind_transceiver or command_name == :bind_transmitter or
             command_name == :bind_receiver do
    {:ok, command_id} = CommandNames.id_by_name(command_name)

    Pdu.new(
      command_id,
      opts
    )
  end

  @spec bind_transmitter_resp(non_neg_integer, String.t()) :: Pdu.t()

  def bind_transmitter_resp(command_status, system_id \\ "") do
    {:ok, command_id} = CommandNames.id_by_name(:bind_transmitter_resp)
    bind_resp(command_id, command_status, system_id)
  end

  @spec bind_receiver_resp(non_neg_integer, String.t()) :: Pdu.t()

  def bind_receiver_resp(command_status, system_id \\ "") do
    {:ok, command_id} = CommandNames.id_by_name(:bind_receiver_resp)
    bind_resp(command_id, command_status, system_id)
  end

  @spec bind_transceiver_resp(non_neg_integer, String.t()) :: Pdu.t()

  def bind_transceiver_resp(command_status, system_id \\ "") do
    {:ok, command_id} = CommandNames.id_by_name(:bind_transceiver_resp)
    bind_resp(command_id, command_status, system_id)
  end

  @spec bind_resp(non_neg_integer, non_neg_integer, String.t()) :: Pdu.t()

  def bind_resp(command_id, command_status, system_id) do
    Pdu.new({command_id, command_status, 0}, %{
      system_id: system_id
    })
  end

  @spec unbind :: Pdu.t()

  def unbind do
    {:ok, command_id} = CommandNames.id_by_name(:unbind)
    Pdu.new(command_id)
  end

  @spec unbind_resp(non_neg_integer) :: Pdu.t()

  def unbind_resp(command_status \\ 0) do
    {:ok, command_id} = CommandNames.id_by_name(:unbind_resp)
    Pdu.new({command_id, command_status, 0})
  end

  @spec enquire_link :: Pdu.t()

  def enquire_link do
    {:ok, command_id} = CommandNames.id_by_name(:enquire_link)
    Pdu.new(command_id)
  end

  @spec enquire_link_resp(non_neg_integer) :: Pdu.t()

  def enquire_link_resp(command_status \\ 0) do
    {:ok, command_id} = CommandNames.id_by_name(:enquire_link_resp)
    Pdu.new({command_id, command_status, 0})
  end

  @type message :: String.t() | {data_coding :: non_neg_integer, String.t()}
  @spec submit_sm(Pdu.addr() | String.t(), Pdu.addr() | String.t(), message, non_neg_integer) ::
          Pdu.t()

  def submit_sm(source, dest, message, registered_delivery \\ 0)

  def submit_sm(source, dest, message, registered_delivery) when is_binary(source) do
    {ton, npi} = TONNPIDefaults.ton_npi(source)
    submit_sm({source, ton, npi}, dest, message, registered_delivery)
  end

  def submit_sm(source, dest, message, registered_delivery) when is_binary(dest) do
    {ton, npi} = TONNPIDefaults.ton_npi(dest)
    submit_sm(source, {dest, ton, npi}, message, registered_delivery)
  end

  def submit_sm(
        {source_addr, source_addr_ton, source_addr_npi},
        {dest_addr, dest_addr_ton, dest_addr_npi},
        {data_coding, message},
        registered_delivery
      ) do
    {:ok, command_id} = CommandNames.id_by_name(:submit_sm)

    Pdu.new(command_id, %{
      source_addr: source_addr,
      source_addr_ton: source_addr_ton,
      source_addr_npi: source_addr_npi,
      destination_addr: dest_addr,
      dest_addr_ton: dest_addr_ton,
      dest_addr_npi: dest_addr_npi,
      short_message: message,
      data_coding: data_coding,
      registered_delivery: registered_delivery
    })
  end

  def submit_sm(
        {source_addr, source_addr_ton, source_addr_npi},
        {dest_addr, dest_addr_ton, dest_addr_npi},
        message,
        registered_delivery
      )
      when is_binary(message) do
    submit_sm(
      {source_addr, source_addr_ton, source_addr_npi},
      {dest_addr, dest_addr_ton, dest_addr_npi},
      {0, message},
      registered_delivery
    )
  end

  @spec submit_sm_resp(non_neg_integer, String.t()) :: Pdu.t()

  def submit_sm_resp(command_status, message_id \\ "") do
    {:ok, command_id} = CommandNames.id_by_name(:submit_sm_resp)

    Pdu.new({command_id, command_status, 0}, %{
      message_id: message_id
    })
  end

  @spec deliver_sm(Pdu.addr() | String.t(), Pdu.addr() | String.t(), message) :: Pdu.t()

  def deliver_sm(
        {source_addr, source_addr_ton, source_addr_npi},
        {dest_addr, dest_addr_ton, dest_addr_npi},
        {data_coding, message}
      ) do
    {:ok, command_id} = CommandNames.id_by_name(:deliver_sm)

    Pdu.new(command_id, %{
      source_addr: source_addr,
      source_addr_ton: source_addr_ton,
      source_addr_npi: source_addr_npi,
      destination_addr: dest_addr,
      dest_addr_ton: dest_addr_ton,
      dest_addr_npi: dest_addr_npi,
      short_message: message,
      data_coding: data_coding
    })
  end

  def deliver_sm(
        {source_addr, source_addr_ton, source_addr_npi},
        {dest_addr, dest_addr_ton, dest_addr_npi},
        message
      )
      when is_binary(message) do
    deliver_sm(
      {source_addr, source_addr_ton, source_addr_npi},
      {dest_addr, dest_addr_ton, dest_addr_npi},
      {0, message}
    )
  end

  def deliver_sm(source, dest, message) when is_binary(source) do
    {ton, npi} = TONNPIDefaults.ton_npi(source)
    deliver_sm({source, ton, npi}, dest, message)
  end

  def deliver_sm(source, dest, message) when is_binary(dest) do
    {ton, npi} = TONNPIDefaults.ton_npi(dest)
    deliver_sm(source, {dest, ton, npi}, message)
  end

  @type message_state :: non_neg_integer | atom

  @spec delivery_report(String.t(), Pdu.addr(), Pdu.addr(), String.t(), message_state) :: Pdu.t()

  def delivery_report(
        message_id,
        source,
        dest,
        message \\ "",
        message_state \\ :DELIVERED
      )

  def delivery_report(
        message_id,
        {_source_addr, _source_addr_ton, _source_addr_npi} = source,
        {_dest_addr, _dest_addr_ton, _dest_addr_npi} = dest,
        message,
        message_state
      )
      when is_atom(message_state) do
    message_state_code = MessageState.code_by_name(message_state)
    delivery_report(message_id, source, dest, message, message_state_code)
  end

  def delivery_report(
        message_id,
        {source_addr, source_addr_ton, source_addr_npi},
        {dest_addr, dest_addr_ton, dest_addr_npi},
        message,
        message_state
      )
      when is_integer(message_state) do
    {:ok, command_id} = CommandNames.id_by_name(:deliver_sm)

    Pdu.new(
      command_id,
      %{
        source_addr: source_addr,
        source_addr_ton: source_addr_ton,
        source_addr_npi: source_addr_npi,
        destination_addr: dest_addr,
        dest_addr_ton: dest_addr_ton,
        dest_addr_npi: dest_addr_npi,
        esm_class: 0b00000100,
        short_message: message
      },
      %{
        message_state: message_state,
        receipted_message_id: message_id
      }
    )
  end

  @spec delivery_report_for_submit_sm(String.t(), Pdu.t(), String.t(), message_state) :: Pdu.t()

  def delivery_report_for_submit_sm(
        message_id,
        submit_sm,
        message \\ "",
        message_state \\ :DELIVERED
      ) do
    source = Pdu.source(submit_sm)
    dest = Pdu.dest(submit_sm)
    delivery_report(message_id, dest, source, message, message_state)
  end

  @spec deliver_sm_resp(non_neg_integer) :: Pdu.t()

  def deliver_sm_resp(command_status \\ 0) do
    {:ok, command_id} = CommandNames.id_by_name(:deliver_sm_resp)
    Pdu.new({command_id, command_status, 0})
  end
end
