lib/qmi/codec/network_access.ex

defmodule QMI.Codec.NetworkAccess do
  @moduledoc """
  Codec for making network access service requests
  """
  import Bitwise

  require Logger

  @network_access_service_id 0x03

  # messages
  @get_signal_strength 0x0020
  @get_home_network 0x0025
  @get_rf_band_info 0x0031
  @set_system_selection_preference 0x0033
  @get_system_selection_preference 0x0034

  # indications
  @serving_system_indication 0x0024
  @operator_name_indication 0x003A

  @typedoc """
  The radio interface that is being reported
  """
  @type radio_interface() :: :no_service | :cdma_1x | :cdma_1x_evdo | :amps | :gsm | :umts | :lte

  @typedoc """
  Report from requesting the signal strength
  """
  @type signal_strength_report() :: %{
          rssi_reports: [%{radio: radio_interface(), rssi: integer()}]
        }

  @typedoc """
  Report from requesting the home network
  """
  @type home_network_report() :: %{
          mcc: char(),
          mnc: char(),
          provider: binary()
        }

  @typedoc """
  Information about an radio interface band
  """
  @type rf_band_information() :: %{
          interface: radio_interface(),
          band: binary(),
          channel: integer()
        }

  @type serving_system_registration_state() ::
          :not_registered | :registered | :registration_denied | :registration_unknown

  @type attach_state() :: :unknown | :attached | :detached

  @type network() :: :network_unknown | :network_3gpp2 | :network_3gpp

  @typedoc """
  Required fields:

  * `:name` - the name of the indication
  * `:service_id` - the service id
  * `:indication_id` - the indication id
  * `:serving_system_registration_state` - the state of the registration status
    to the serving system
  * `:serving_system_cs_attach_state` - the circuit-switched domain attach state
  * `:serving_system_ps_attach_state` - the packet-switched domain attach state
  * `:serving_system_selected_network` - the type of selected radio access network
  * `:serving_system_radio_interfaces` - a list of radio interfaces currently in use

  Optional fields:

  * `:cell_id` - the id of the cell being used by the connected tower
  * `:utc_offset` - the UTC offset in seconds
  * `:location_area_code` - the location area code of a tower
  * `:network_datetime` - the reported datetime of the network when connecting
  * `:roaming` - if you are in roaming or not
  * `:std_offset` - `Calendar.std_offset()` for daylight savings
    adjustment
  """
  @type serving_system_indication() :: %{
          required(:name) => :serving_system_indication,
          required(:service_id) => 0x03,
          required(:indication_id) => 0x0024,
          required(:serving_system_registration_state) => serving_system_registration_state(),
          required(:serving_system_cs_attach_state) => attach_state(),
          required(:serving_system_ps_attach_state) => attach_state(),
          required(:serving_system_selected_network) => network(),
          required(:serving_system_radio_interfaces) => [radio_interface()],
          optional(:cell_id) => integer(),
          optional(:utc_offset) => Calendar.utc_offset(),
          optional(:location_area_code) => integer(),
          optional(:network_datetime) => NaiveDateTime.t(),
          optional(:roaming) => boolean(),
          optional(:std_offset) => Calendar.std_offset()
        }

  @type operator_name_indication() :: %{
          name: :operator_name_indication,
          long_name: binary(),
          short_name: binary()
        }

  @typedoc """
  How long a system preference change should be applied

  * `:power_cycle` - only remains active until the next power cycle
  * `:permanent` - remains active through power cycles until changed by a client
  """
  @type preference_change_duration() :: :power_cycle | :permanent

  @typedoc """
  The options to configure the system selection preference

  * `:mode_preference` - which radio access technologies should the modem use
  * `:change_duration` - the `preference_change_duration()` for the applied
    settings. The default is `:permanent`.
  * `:roaming_preference` - the preferred roaming setting
  """
  @type set_system_selection_preference_opt() ::
          {:mode_preference, [radio_interface()]}
          | {:change_duration, preference_change_duration()}
          | {:roaming_preference, roaming_preference()}

  @doc """
  Make the `QMI.request()` for getting signal strength
  """
  @spec get_signal_strength() :: QMI.request()
  def get_signal_strength() do
    %{
      service_id: @network_access_service_id,
      payload:
        <<@get_signal_strength::16-little, 0x05::little-16, 0x10, 0x02::little-16,
          0xEF::little-16>>,
      decode: &parse_get_signal_strength_resp/1
    }
  end

  defp parse_get_signal_strength_resp(
         <<@get_signal_strength::little-16, _length::little-16, tlvs::binary>>
       ) do
    parse_get_signal_strength_tlvs(%{rssi_reports: []}, tlvs)
  end

  defp parse_get_signal_strength_tlvs(parsed, <<>>) do
    {:ok, parsed}
  end

  defp parse_get_signal_strength_tlvs(
         parsed,
         <<0x11, _length::little-16, num_sets::little-16,
           rssi_list::size(num_sets)-unit(16)-binary, rest::binary>>
       ) do
    rssi_reports =
      for <<rssi, radio_if <- rssi_list>>, do: %{rssi: -rssi, radio: radio_interface(radio_if)}

    parsed
    |> Map.put(:rssi_reports, rssi_reports)
    |> parse_get_signal_strength_tlvs(rest)
  end

  defp parse_get_signal_strength_tlvs(
         parsed,
         <<_type, length::little-16, _values::size(length)-unit(8)-binary, rest::binary>>
       ) do
    parse_get_signal_strength_tlvs(parsed, rest)
  end

  defp radio_interfaces(radio_ifs) do
    for <<radio_if <- radio_ifs>> do
      radio_interface(radio_if)
    end
  end

  defp radio_interface(0x00), do: :no_service
  defp radio_interface(0x01), do: :cdma_1x
  defp radio_interface(0x02), do: :cdma_1x_evdo
  defp radio_interface(0x03), do: :amps
  defp radio_interface(0x04), do: :gsm
  defp radio_interface(0x05), do: :umts
  defp radio_interface(0x08), do: :lte
  defp radio_interface(0x09), do: :td_scdma

  @typedoc """
  The roaming preference

  * `:off` - acquire only systems for which the roaming indicator is off
  * `:not_off` - acquire a system as long as its roaming indicator is not off
  * `:not_flashing` - acquire a system as for which the roaming indicator is
    off or solid on - CDMA only.
  * `:any` - acquire systems, regardless of their roaming indicator
  """
  @type roaming_preference() :: :off | :not_off | :not_flashing | :any

  @typedoc """
  The networking selection preference

  * `:automatic` - automatically select the network
  * `:manual` - manually select the network
  """
  @type network_selection_preference() :: :automatic | :manual

  @typedoc """
  The network registration restriction preference

  * `:unrestricted` - device follows the normal registration process
  * `:camped_only` - device will camp on a network but not register
  * `:limited` - device selects the network for limited service

  This can also be integer value that is specified by the specific modem
  provider
  """
  @type registration_restriction_preference() ::
          :unrestricted | :camped_only | :limited | non_neg_integer()

  @typedoc """
  The modem usage preference setting

  `:unknown` - device does not know the usage preference setting
  `:voice_centric` - the device is set for voice centric usage
  `:data_centric` - the device is set for data centric
  """
  @type usage_setting_preference() :: :unknown | :voice_centric | :data_centric

  @typedoc """
  The voice domain preference setting

  * `:cs_only` - circuit-switched (CS) voice only
  * `:ps_only` - packet-switched (PS) voice only
  * `:cs_preferred` - PS is secondary
  * `:ps_preferred` - CS is secondary
  """
  @type voice_domain_preference() :: :cs_only | :ps_only | :cs_preferred | :ps_preferred

  @typedoc """
  Preference settings for when a device selects a system

  * `:emergency_mode` - `:on` if the device is in emergency mode, `:off`
    otherwise
  * `:mode_preference` - a list of radio access technologies the device will
    try to use
  * `:roaming_preference` - the device roaming preference
  * `:network_selection_preference` - if the device will automatically select
    a network
  * `:acquisition_order` - the order in which the device will try to connect
    to a radio access technology
  * `:registration_restriction` - the system registration restriction
  * `:usage_settings` - the modem usage preference
  * `:voice_domain_preference` - the voice domain preference
  """
  @type get_system_selection_preference_response() :: %{
          optional(:emergency_mode) => :off | :on,
          optional(:mode_preference) => [radio_interface()],
          optional(:roaming_preference) => roaming_preference(),
          optional(:network_selection_preference) => network_selection_preference(),
          optional(:acquisition_order) => [radio_interface()],
          optional(:registration_restriction) => registration_restriction_preference(),
          optional(:usage_settings) => usage_setting_preference(),
          optional(:voice_domain) => voice_domain_preference()
        }

  @doc """
  Make `QMI.request()` to get the system selection preferences
  """
  @spec get_system_selection_preference() :: QMI.request()
  def get_system_selection_preference() do
    %{
      service_id: @network_access_service_id,
      payload: <<@get_system_selection_preference::little-16, 0x00, 0x00>>,
      decode: &parse_get_system_selection_preference/1
    }
  end

  defp parse_get_system_selection_preference(
         <<@get_system_selection_preference::little-16, size::little-16, tlvs::size(size)-binary>>
       ) do
    {:ok, do_parse_get_system_selection_preference(%{}, tlvs)}
  end

  defp parse_get_system_selection_preference(_other) do
    {:error, :unexpected_response}
  end

  defp do_parse_get_system_selection_preference(parsed_tlvs, <<>>) do
    parsed_tlvs
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x10, 0x01::little-16, emergency_mode, rest::binary>>
       ) do
    tlvs
    |> Map.put(:emergency_mode, parse_emergency_mode(emergency_mode))
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x11, 0x02::little-16, rat_mask::little-16, rest::binary>>
       ) do
    possible_rats = [
      {:cdma_1x, 0x01},
      {:cdma_1x_evdo, 0x02},
      {:gsm, 0x04},
      {:umts, 0x08},
      {:lte, 0x10},
      {:td_scdma, 0x20}
    ]

    rats_list =
      Enum.reduce(possible_rats, [], fn {rat, byte}, list ->
        if (rat_mask &&& byte) == byte do
          [rat | list]
        else
          list
        end
      end)

    tlvs
    |> Map.put(:mode_preference, rats_list)
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x14, 0x02::little-16, roam_pref::little-16, rest::binary>>
       ) do
    tlvs
    |> Map.put(:roaming_preference, parse_roaming_preference(roam_pref))
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x16, 0x01::little-16, network_selection_pref, rest::binary>>
       ) do
    tlvs
    |> Map.put(
      :network_selection_preference,
      parse_network_selection_preference(network_selection_pref)
    )
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x1C, _size::16, number_or_rats, rat_acquisition_order::binary-size(number_or_rats),
           rest::binary>>
       ) do
    rat_acquisition_order_list =
      rat_acquisition_order |> :erlang.binary_to_list() |> Enum.map(&radio_interface/1)

    tlvs
    |> Map.put(:acquisition_order, rat_acquisition_order_list)
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x1D, 0x04::little-16, registration_restriction::little-32, rest::binary>>
       ) do
    tlvs
    |> Map.put(
      :registration_restriction,
      parse_registration_restriction(registration_restriction)
    )
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x1F, 0x04::little-16, setting::little-32, rest::binary>>
       ) do
    tlvs
    |> Map.put(:usage_setting, parse_usage_setting(setting))
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<0x20, 0x04::little-16, domain_pref::little-32, rest::binary>>
       ) do
    tlvs
    |> Map.put(:void_domain_preference, parse_voice_domain_preference(domain_pref))
    |> do_parse_get_system_selection_preference(rest)
  end

  defp do_parse_get_system_selection_preference(
         tlvs,
         <<_type, size::little-16, _tlvs::binary-size(size), rest::binary>>
       ) do
    do_parse_get_system_selection_preference(tlvs, rest)
  end

  defp parse_registration_restriction(0x00), do: :unrestricted
  defp parse_registration_restriction(0x01), do: :camped_only
  defp parse_registration_restriction(0x02), do: :limited
  defp parse_registration_restriction(byte), do: byte

  defp parse_usage_setting(0), do: :unknown
  defp parse_usage_setting(1), do: :voice_centric
  defp parse_usage_setting(2), do: :data_centric

  defp parse_voice_domain_preference(0x00), do: :cs_only
  defp parse_voice_domain_preference(0x01), do: :ps_only
  defp parse_voice_domain_preference(0x02), do: :cs_preferred
  defp parse_voice_domain_preference(0x03), do: :ps_preferred

  defp parse_emergency_mode(0x00), do: :off
  defp parse_emergency_mode(0x01), do: :on

  defp parse_roaming_preference(0x01), do: :off
  defp parse_roaming_preference(0x02), do: :not_off
  defp parse_roaming_preference(0x03), do: :not_flashing
  defp parse_roaming_preference(0xFF), do: :any

  defp parse_network_selection_preference(0x00), do: :automatic
  defp parse_network_selection_preference(0x01), do: :manual

  @doc """
  Generate the `QMI.request()` for setting system selection preferences
  """
  @spec set_system_selection_preference([set_system_selection_preference_opt()]) :: QMI.request()
  def set_system_selection_preference(opts \\ []) do
    {tlvs, size} = make_tlvs(opts)
    payload = [<<@set_system_selection_preference::little-16, size::little-16>>, tlvs]

    %{
      service_id: @network_access_service_id,
      payload: payload,
      decode: &parse_set_system_selection_preference/1
    }
  end

  defp make_tlvs(opts) do
    opts = Keyword.put_new(opts, :change_duration, :permanent)

    do_make_tlvs(opts, [], 0)
  end

  defp do_make_tlvs([], tlvs, bytes), do: {tlvs, bytes}

  defp do_make_tlvs([{:change_duration, :permanent} | rest], tlvs, bytes) do
    tlv = <<0x17, 0x01::little-16, 0x01>>

    do_make_tlvs(rest, tlvs ++ [tlv], bytes + 4)
  end

  defp do_make_tlvs([{:change_duration, :power_cycle} | rest], tlvs, bytes) do
    tlv = <<0x17, 0x01::little-16, 0x00>>

    do_make_tlvs(rest, tlvs ++ [tlv], bytes + 4)
  end

  defp do_make_tlvs([{:mode_preference, radio_techs} | rest], tlvs, bytes) do
    radio_techs_mask =
      Enum.reduce(radio_techs, 0x00, fn
        :cdma_1x, mask -> mask ||| 0x01
        :cdma_1x_hrdp, mask -> mask ||| 0x02
        :gsm, mask -> mask ||| 0x04
        :umts, mask -> mask ||| 0x08
        :lte, mask -> mask ||| 0x10
        :td_scdma, mask -> mask ||| 0x20
        _other, mask -> mask
      end)

    tlv = <<0x11, 0x02::little-16, radio_techs_mask::little-16>>

    do_make_tlvs(rest, tlvs ++ [tlv], bytes + 5)
  end

  defp do_make_tlvs([{:roaming_preference, roaming_preference} | rest], tlvs, bytes) do
    roaming_byte = encode_roaming_preference(roaming_preference)

    tlv = <<0x14, 0x02::little-16, roaming_byte::little-16>>
    do_make_tlvs(rest, tlvs ++ [tlv], bytes + 5)
  end

  defp encode_roaming_preference(:off), do: 0x01
  defp encode_roaming_preference(:not_off), do: 0x02
  defp encode_roaming_preference(:not_flashing), do: 0x03
  defp encode_roaming_preference(:any), do: 0xFF

  defp parse_set_system_selection_preference(
         <<@set_system_selection_preference::little-16, _rest::binary>>
       ) do
    :ok
  end

  @doc """
  Make the request for getting the home network
  """
  @spec get_home_network() :: QMI.request()
  def get_home_network() do
    %{
      service_id: @network_access_service_id,
      payload: <<@get_home_network::little-16, 0x00, 0x00>>,
      decode: &parse_get_home_network_resp/1
    }
  end

  defp parse_get_home_network_resp(
         <<@get_home_network::little-16, size::little-16, tlvs::size(size)-binary>>
       ) do
    parse_get_home_network_tlvs(%{}, tlvs)
  end

  defp parse_get_home_network_resp(_other) do
    {:error, :unexpected_response}
  end

  defp parse_get_home_network_tlvs(parsed, <<>>) do
    {:ok, parsed}
  end

  defp parse_get_home_network_tlvs(
         parsed,
         <<0x01, size::little-16, values::size(size)-binary, rest::binary>>
       ) do
    <<mcc::little-16, mnc::little-16, description::binary>> = values

    parsed
    |> Map.put(:mcc, mcc)
    |> Map.put(:mnc, mnc)
    |> Map.put(:provider, extract_string(description))
    |> parse_get_home_network_tlvs(rest)
  end

  defp parse_get_home_network_tlvs(
         parsed,
         <<_type, length::little-16, _values::size(length)-binary, rest::binary>>
       ) do
    parse_get_home_network_tlvs(parsed, rest)
  end

  defp extract_string(<<length, string::binary-size(length)>>), do: string

  @doc """
  Parse an indication
  """
  @spec parse_indication(binary()) ::
          {:ok, serving_system_indication()} | {:error, :invalid_indication}
  def parse_indication(
        <<@serving_system_indication::16-little, size::16-little, tlvs::binary-size(size)>>
      ) do
    result =
      :serving_system_indication
      |> init_indication()
      |> parse_serving_system_indication(tlvs)

    {:ok, result}
  end

  def parse_indication(
        <<@operator_name_indication::16-little, size::16-little, tlvs::binary-size(size)>>
      ) do
    result =
      :operator_name_indication
      |> init_indication()
      |> parse_operator_name_indication(tlvs)

    {:ok, result}
  end

  def parse_indication(_other) do
    {:error, :invalid_indication}
  end

  defp init_indication(:serving_system_indication = name) do
    %{
      name: name,
      indication_id: @serving_system_indication,
      service_id: @network_access_service_id,
      serving_system_registration_state: :not_registered,
      serving_system_cs_attach_state: :unknown,
      serving_system_ps_attach_state: :unknown,
      serving_system_selected_network: :network_unknown,
      serving_system_radio_interfaces: []
    }
  end

  defp init_indication(:operator_name_indication = name) do
    %{
      name: name,
      long_name: "",
      short_name: ""
    }
  end

  defp parse_operator_name_indication(indication, <<>>), do: indication

  defp parse_operator_name_indication(
         indication,
         <<0x14, size::16-little, nitz_info::binary-size(size), rest::binary>>
       ) do
    <<name_encoding, _other_encodings::24, long_name_size, long_name::binary-size(long_name_size),
      short_name_size, short_name::binary-size(short_name_size)>> = nitz_info

    indication
    |> Map.put(:long_name, parse_operator_name(name_encoding, long_name))
    |> Map.put(:short_name, parse_operator_name(name_encoding, short_name))
    |> parse_operator_name_indication(rest)
  end

  defp parse_operator_name_indication(
         indication,
         <<type, size, values::binary-size(size), rest::binary>>
       ) do
    Logger.debug("[QMI]: Ignoring TLV from operator name indication #{type} #{size} #{values}")

    parse_operator_name(indication, rest)
  end

  # libQMI parses UCS2 as UTF-16.
  # https://gitlab.freedesktop.org/mobile-broadband/libqmi/-/blob/master/src/libqmi-glib/qmi-helpers.c#L404
  defp parse_operator_name(1, name), do: :unicode.characters_to_binary(name, {:utf16, :big})
  defp parse_operator_name(_other_encoding, name), do: name

  defp parse_serving_system_indication(parsed, <<>>) do
    parsed
  end

  defp parse_serving_system_indication(
         parsed,
         <<0x01, length::16-little, values::size(length)-binary, rest::binary>>
       ) do
    <<registration_state, cs_attach_state, ps_attach_state, selected_network, num_radio_if,
      radio_if::size(num_radio_if)-binary>> = values

    parsed = %{
      parsed
      | serving_system_registration_state: serving_system_registration_state(registration_state),
        serving_system_cs_attach_state: serving_system_attach_state(cs_attach_state),
        serving_system_ps_attach_state: serving_system_attach_state(ps_attach_state),
        serving_system_selected_network: serving_system_network(selected_network),
        serving_system_radio_interfaces: radio_interfaces(radio_if)
    }

    parse_serving_system_indication(parsed, rest)
  end

  defp parse_serving_system_indication(
         serving_system_ind,
         <<0x10, 0x01::little-16, roaming?, rest::binary>>
       ) do
    serving_system_ind
    |> Map.put(:roaming, roaming? == 0)
    |> parse_serving_system_indication(rest)
  end

  defp parse_serving_system_indication(
         serving_system_ind,
         <<0x1E, 0x04::little-16, cell_id::little-32, rest::binary>>
       ) do
    serving_system_ind
    |> Map.put(:cell_id, cell_id)
    |> parse_serving_system_indication(rest)
  end

  defp parse_serving_system_indication(
         serving_system_indication,
         <<0x1A, 0x01::little-16, offset::signed, rest::binary>>
       ) do
    indication = try_parse_std_offset(serving_system_indication, rest)

    indication
    |> Map.put(:utc_offset, calc_utc_offset(indication, offset))
    |> parse_serving_system_indication(rest)
  end

  defp parse_serving_system_indication(
         %{std_offset: _std_offset} = indication,
         <<0x1B, 0x01::little-16, _adjustment, rest::binary>>
       ) do
    parse_serving_system_indication(indication, rest)
  end

  defp parse_serving_system_indication(
         serving_system_indication,
         <<0x1B, 0x01::little-16, adjustment, rest::binary>>
       ) do
    serving_system_indication
    |> Map.put(:std_offset, adjustment * 3600)
    |> parse_serving_system_indication(rest)
  end

  defp parse_serving_system_indication(
         serving_system_indication,
         <<0x1D, 0x02::little-16, lac::little-16, rest::binary>>
       ) do
    serving_system_indication
    |> Map.put(:location_area_code, lac)
    |> parse_serving_system_indication(rest)
  end

  defp parse_serving_system_indication(
         serving_system_indication,
         <<0x1C, 0x08::little-16, year::little-16, month, day, hour, minute, second,
           tz_offset::signed, rest::binary>>
       ) do
    indication = try_parse_std_offset(serving_system_indication, rest)
    {:ok, datetime} = NaiveDateTime.new(year, month, day, hour, minute, second)

    indication
    |> Map.put(:network_datetime, datetime)
    |> Map.put(:utc_offset, calc_utc_offset(indication, tz_offset))
    |> parse_serving_system_indication(rest)
  end

  defp parse_serving_system_indication(
         parsed,
         <<_type, length::16-little, _values::size(length)-binary, rest::binary>>
       ) do
    parse_serving_system_indication(parsed, rest)
  end

  defp try_parse_std_offset(%{std_offset: _std_offset} = indication, _binary) do
    indication
  end

  defp try_parse_std_offset(indication, <<>>) do
    indication
  end

  defp try_parse_std_offset(indication, <<0x1B, 0x01::little-16, adjustment, _rest::binary>>) do
    indication
    |> Map.put(:std_offset, adjustment * 3600)
  end

  defp try_parse_std_offset(
         indication,
         <<_type, length::little-16, _values::binary-size(length), rest::binary>>
       ) do
    try_parse_std_offset(indication, rest)
  end

  defp serving_system_registration_state(0x00), do: :not_registered
  defp serving_system_registration_state(0x01), do: :registered
  defp serving_system_registration_state(0x02), do: :not_registered_searching
  defp serving_system_registration_state(0x03), do: :registration_denied
  defp serving_system_registration_state(0x04), do: :registration_unknown

  defp serving_system_attach_state(0x00), do: :unknown
  defp serving_system_attach_state(0x01), do: :attached
  defp serving_system_attach_state(0x02), do: :detached

  defp serving_system_network(0x00), do: :network_unknown
  defp serving_system_network(0x01), do: :network_3gpp2
  defp serving_system_network(0x02), do: :network_3gpp

  defp calc_utc_offset(%{std_offset: std_offset}, tz_offset) do
    # QMI reports the overall offset. Elixir splits the offset into the amount
    # that goes to standard time (utc_offset) + the daylight savings offset
    # from standard time (std_offset).
    utc_offset_to_seconds(tz_offset) - std_offset
  end

  defp calc_utc_offset(_indication, tz_offset) do
    utc_offset_to_seconds(tz_offset)
  end

  defp utc_offset_to_seconds(offset), do: offset * 15 * 60

  @doc """
  Get the radio band information
  """
  @spec get_rf_band_info() :: QMI.request()
  def get_rf_band_info() do
    %{
      service_id: @network_access_service_id,
      payload: <<@get_rf_band_info::little-16, 0x00::little-16>>,
      decode: &parse_get_rf_band_info_resp/1
    }
  end

  defp parse_get_rf_band_info_resp(
         <<@get_rf_band_info::little-16, length::little-16, values::binary-size(length)>>
       ) do
    result = parse_get_rf_band_values([], values)

    {:ok, result}
  end

  defp parse_get_rf_band_info_resp(_binary) do
    {:error, :unexpected_response}
  end

  defp parse_get_rf_band_values(rf_band_info_list, <<>>) do
    rf_band_info_list
  end

  defp parse_get_rf_band_values(
         rf_band_info_list,
         <<0x01, length::little-16, radio_band_information::binary-size(length), rest::binary>>
       ) do
    rf_band_info_list
    |> parse_radio_ifs(radio_band_information)
    |> parse_get_rf_band_values(rest)
  end

  defp parse_get_rf_band_values(
         rf_band_info_list,
         <<_type, length::little-16, _values::binary-size(length), rest::binary>>
       ) do
    parse_get_rf_band_values(rf_band_info_list, rest)
  end

  defp parse_radio_ifs(rf_band_info_list, <<number_of_instances, radio_ifs_bin::binary>>) do
    parse_radio_band_information(rf_band_info_list, number_of_instances, radio_ifs_bin)
  end

  defp parse_radio_band_information(radio_ifs, 0, <<>>) do
    radio_ifs
  end

  defp parse_radio_band_information(
         radio_ifs,
         n,
         <<radio_if, active_band::little-16, active_channel::little-16, rest::binary>>
       ) do
    rifs =
      radio_ifs ++
        [
          %{
            interface: radio_interface(radio_if),
            band: parse_active_band(active_band),
            channel: active_channel
          }
        ]

    parse_radio_band_information(rifs, n - 1, rest)
  end

  # Parsing logic uses libqmi enumeration definition as reference
  # https://gitlab.freedesktop.org/mobile-broadband/libqmi/-/blob/master/src/libqmi-glib/qmi-enums-nas.h#L173-269
  defp parse_active_band(band) when band >= 0 and band <= 19 do
    "CDMA Band Class #{band}"
  end

  defp parse_active_band(band) when band >= 40 and band <= 48 do
    "GSM #{gsm_band_name(band)}"
  end

  defp parse_active_band(band) when band >= 80 and band <= 91 and band != 89 do
    "WCDMA #{wcdma_band_name(band)}"
  end

  defp parse_active_band(band) when band >= 120 and band <= 162 do
    "LTE Band #{lte_band_name(band)}"
  end

  defp parse_active_band(band) when band >= 200 and band <= 205 do
    "TDS-CDMA Band #{tds_cdma_band_name(band)}"
  end

  defp parse_active_band(band) do
    "Unknown Band #{band}"
  end

  defp gsm_band_name(40), do: "450"
  defp gsm_band_name(41), do: "480"
  defp gsm_band_name(42), do: "750"
  defp gsm_band_name(43), do: "850"
  defp gsm_band_name(44), do: "900 Extended"
  defp gsm_band_name(45), do: "900 Primary"
  defp gsm_band_name(46), do: "900 Railways"
  defp gsm_band_name(47), do: "DCS 1800"
  defp gsm_band_name(48), do: "PCS 1900"

  defp wcdma_band_name(80), do: "2100"
  defp wcdma_band_name(81), do: "PCS 1900"
  defp wcdma_band_name(82), do: "DCS 1800"
  defp wcdma_band_name(83), do: "1700 US"
  defp wcdma_band_name(84), do: "850"
  defp wcdma_band_name(85), do: "800"
  defp wcdma_band_name(86), do: "2600"
  defp wcdma_band_name(87), do: "900"
  defp wcdma_band_name(88), do: "1700 Japan"
  defp wcdma_band_name(90), do: "1500 Japan"
  defp wcdma_band_name(91), do: "850 Japan"

  defp lte_band_name(n), do: Integer.to_string(n - 120 + 1)

  defp tds_cdma_band_name(200), do: "A"
  defp tds_cdma_band_name(201), do: "B"
  defp tds_cdma_band_name(202), do: "C"
  defp tds_cdma_band_name(203), do: "D"
  defp tds_cdma_band_name(204), do: "E"
  defp tds_cdma_band_name(205), do: "F"
end