{#- Package slaplte provides helpers for configuring Amarisoft LTE services in SlapOS.

    - load_iru_and_icell initializes RU and cell registries.
    - load_ipeercell initializes peer-cell registry.
    - load_ipeer initializes peer registry.
    - load_iue initializes UE registry.
    - ru_config emits RF driver configuration for specified Radio Units.

    In the code iX denotes shared instance of type X, while X denotes
    parameters passed to iX. For example iru denotes Radio Unit shared
    instance, while ru denotes parameters of that Radio Unit instance.

    The following utilities are also provided:

    - B escapes string to be safe to use in buildout code.
    - J should be used around macro calls to retrieve returned objects.
    - error reports instantiation error.
    - ierror reports instantiation error caused by shared instance configuration.
-#}


{#- defaults provide default values for lte parameters.
    it should be kept in sync with "default" in json schemas

    TODO automatically load defaults from JSON schemas  #}
{%- set defaults = {
      'ru': {
        'txrx_active': 'INACTIVE',
      },
      'ru/cpri_link': {
        'mult':     16,
        'rx_delay':  0,
        'tx_delay':  0,
        'tx_dbm':    0,
      },

      'ru/sunwave': {
        'n_antenna_dl': 2,
        'n_antenna_ul': 1,
      },
      'ru/sunwave/cpri_link': {
        'mapping': 'bf1',
        'rx_delay': 11.0,
        'tx_dbm':   42.0,
      },

      'cell/lte': {
        'inactivity_timer': 10000,
      },
      'cell/lte/fdd': {
      },
      'cell/lte/tdd': {
        'tdd_ul_dl_config': '[Configuration 2] 5ms 2UL 6DL (default)',
      },

      'cell/nr': {
        'inactivity_timer': 10000,
      },
      'cell/nr/fdd': {
        'ssb_pos_bitmap':   '1000',
      },
      'cell/nr/tdd': {
        'ssb_pos_bitmap':   '10000000',
        'tdd_ul_dl_config': '5ms 2UL 7DL 4/6 (default)',
      },

      'ue': {
        'sim_algo': 'milenage',
        'opc':      'milenage',
        'amf':      '0x9001',
        'sqn':      '000000000000',
        'impu':     '',
        'impi':     '',
        'imsi':     '001010123456789',
        'k':        '00112233445566778899aabbccddeeff',
      },
    }
%}


{#- B(name) escapes name to be safe to use in buildout code.

    It escapes buildout control characters in the string so that the result
    could be used in buildout code without the risk of buildout profile to
    become broken and/or with injected code when handling string input from
    outside.

    The most often case when B needs to be used is when handling references of
    shared instances in generated buildout code.

    See xbuildout.encode documentation for details.
#}
{%- set B = xbuildout.encode  %}

{#- J is used around macro calls to retrieve returned objects.

    It is needed to workaround jinja2 limitation that macro can return only
    strings - not arbitrary objects: we return objects as JSON-encoded string
    and J decodes them.

    By convention macros that return JSON-encoded objects start with "j" prefix.

    Usage example:

      set obj = J(jmymacro(...))
#}
{%- set J = json_module.loads %}

{#- jdefault_ul_earfcn returns default UL EARFCN corresponding to DL EARFCN. #}
{%- macro jdefault_ul_earfcn(dl_earfcn) %}
{{-   xearfcn_module.dl2ul(dl_earfcn) | tojson }}
{%- endmacro %}

{#- jdefault_ul_nr_arfcn returns default UL NR ARFCN corresponding to DL NR ARFCN and band. #}
{%- macro jdefault_ul_nr_arfcn(dl_nr_arfcn, nr_band)  %}
{{-   xnrarfcn_module.dl2ul(dl_nr_arfcn, nr_band) | tojson  }}
{%- endmacro %}

{#- jdefault_ssb_nr_arfcn returns default SSB NR ARFCN corresponding to DL NR ARFCN
    and band. #}
{%- macro jdefault_ssb_nr_arfcn(dl_nr_arfcn, nr_band) %}
{#- NOTE: computations rechecked wrt https://tech-academy.amarisoft.com/OutOfBox_UEsim_SA.html#Tips_SSB_Frequency #}
{%-   set ssb_nr_arfcn, max_ssb_scs_khz = xnrarfcn_module.dl2ssb(dl_nr_arfcn, nr_band) %}
{{-   ssb_nr_arfcn | tojson }}
{%- endmacro %}


{#- tap indicates tap interface, that slapos told us to use,
    or 'xxx-notap-xxx' if slapos provided us either nothing or empty string. #}
{%- set tap = slap_configuration.get('tap-name', '')   %}
{%- if tap == '' %}
{%-   set tap = 'xxx-notap-xxx'   %}
{%- endif %}


{#- bug indicates an error in template logic.
    it should not happen. #}
{%- macro bug(msg)  %}
{%-   do assert(False, 'BUG: %s' % (msg,)) %}
{%- endmacro  %}

{#- error reports instantiation error. #}
{%- macro error(msg)  %}
{%-   set msg = 'Instantiation Error: %s\n' % msg %}
{%-   do assert(False, msg) %}
{%- endmacro  %}

{#- ierror reports instantiation error caused by shared instance configuration. #}
{%- macro ierror(ishared, msg)  %}
{%-   do error('%s: %s' % (J(jref_of_shared(ishared)), msg)) %}
{%- endmacro  %}


{#- ---- loading ---- #}

{#- jref_of_shared returns original reference used to request shared instance.

    slapproxy puts the reference into slave_reference and slave_title as <partition_id>_<reference>.
    slapos master puts the reference into slave_title as-is and assigns to slave_reference SOFTINST-XXX.

    -> we extract the reference from slave_title.
#}
{%- macro jref_of_shared(ishared) %}
{#- do  print('jref_of_shared %r' % (ishared,)) #}
{%-   set ref = ishared['slave_title']  %}
{%-   set partition_id = slap_configuration['slap-computer-partition-id'] %}
{%-   if ref.startswith(partition_id) %}
{%-     set ref = ref[len(partition_id):] %}
{%-   endif %}
{%-   set ref = ref.removeprefix('_') %}
{{-   ref | tojson  }}
{%- endmacro  %}

{#- qshared_instance_list queues not yet loaded shared instances.
    load_* routines process this queue and move loaded instances to i<type>_dict registries. #}
{%- set qshared_instance_list = slap_configuration.get('slave-instance-list', []) %}

{#- protect against duplicate slave_title -- see jref_of_shared for why we need this #}
{%- for i, ishared in enumerate(qshared_instance_list)  %}
{%-   for k, kshared in enumerate(qshared_instance_list)  %}
{%-     if i != k  and  ishared.slave_title == kshared.slave_title  %}
{%-       do ierror(ishared, 'duplicate title wrt %s' % kshared.slave_reference)  %}
{%-     endif %}
{%-   endfor  %}
{%- endfor  %}

{#- check_loaded_everything verifies that all shared instances were handling during the load. #}
{%- macro check_loaded_everything() %}
{%-   for ishared in qshared_instance_list  %}
{%-     do ierror(ishared, "shared instance of unsupported type") %}
{%-   endfor  %}
{%- endmacro  %}

{#- json-decode _ in all shared instances #}
{%- for ishared in qshared_instance_list  %}
{%-   do ishared.update({'_': J(ishared['_'])}) %}
{%- endfor  %}


{#- load_iru_and_icell initializes RU and cell registries.

    icell_dict keeps cell shared instances:  reference -> icell
    iru_dict   keeps RU   shared instances + RU whose definition is embedded into a cell:  reference -> iRU
    in the kept instances _ is automatically json-decoded

    icell_kind=enb  - load cells definition to serve them from enb
    icell_kind=ue   - load cells definition to connect to them
#}
{%- macro load_iru_and_icell(iru_dict, icell_dict, icell_kind)  %}
{%-   set qother = [] %}
{%-   for ishared in qshared_instance_list  %}
{%-     set ref = J(jref_of_shared(ishared))  %}
{%-     set _ = ishared['_']  %}
{%-     if   'ru_type'   in _ %}
{%-       set iru = ishared %}
{%-       do _ru_set_defaults(_)  %}
{%-       do iru_dict.update({ref: iru})  %}
{%-     elif 'cell_type' in _  and  _.get('cell_kind') == icell_kind  %}
{%-       set icell = ishared %}
{%-       do _cell_set_defaults(_, icell_kind, icell_dict)  %}
{%-       do icell_dict.update({ref: icell})  %}
{%-       set ru = _['ru']  %}
{%-       if ru.ru_type not in ('ru_ref', 'ruincell_ref') %}
{#-         embedded ru definition -> expose it as synthethic `_<cell_ref>_ru` #}
{%-         do _ru_set_defaults(ru) %}
{%-         do iru_dict.update({'_%s_ru' % ref: {
                  '_':                ru,
                  'slave_title':      '%s. RU' % icell.slave_title,
                  'slave_reference':  False,
               }})  %}
{%-       endif %}
{%-     else %}
{%-       do qother.append(ishared) %}
{%-     endif %}
{%-   endfor  %}
{%-   do qshared_instance_list.clear()  %}
{%-   do qshared_instance_list.extend(qother) %}

{#-   do print('\n>>> iru_dict:'),   pprint(iru_dict)   #}
{#-   do print('\n>>> icell_dict:'), pprint(icell_dict) #}

{#-   verify that there is no dangling cell -> cell refs in ruincell_ref #}
{%-   for _, icell in icell_dict|dictsort %}
{%-     set ru = icell['_']['ru'] %}
{%-     if ru.ru_type == 'ruincell_ref' %}
{%-       if ru.ruincell_ref not in icell_dict  %}
{%-         do ierror(icell, "referred cell %r does not exist" % ru.ruincell_ref) %}
{%-       endif %}
{%-     endif %}
{%-   endfor %}

{#-   verify that there is no dangling cell->ru references #}
{%-   for _, icell in icell_dict|dictsort %}
{%-     set ru_ref = J(jcell_ru_ref(icell, icell_dict)) %}
{%-     if ru_ref not in iru_dict %}
{%-       do ierror(icell, "referred RU %r does not exist" % ru_ref)  %}
{%-     endif %}
{%-   endfor  %}

{#-   assign RUs rf_port and tx/rx channel indices  #}
{%-   set rf_chan = namespace(tx=0, rx=0) %}
{%-   for rf_port, (ru_ref, iru) in enumerate(iru_dict|dictsort)  %}
{%-     set ru = iru['_'] %}
{%-     do ru.update({'_rf_port':    rf_port,
                      '_rf_chan_tx': rf_chan.tx,
                      '_rf_chan_rx': rf_chan.rx}) %}
{%-     set rf_chan.tx = rf_chan.tx + ru.n_antenna_dl %}
{%-     set rf_chan.rx = rf_chan.rx + ru.n_antenna_ul %}
{%-   endfor  %}

{#-   assign TAP interfaces to cpri RUs #}
{%-   set iru_vcpri = list(iru_dict|dictsort | selectattr('1._.ru_link_type', '==', 'cpri')) %}
{%-   for i, (ru_ref, iru) in enumerate(iru_vcpri)  %}
{%-     if len(iru_vcpri) > 1 %}
{%-       set ru_tap = "%s-%d" % (tap, i+1) %}
{%-     else  %}
{%-       set ru_tap = tap  %}
{%-     endif %}
{%-     do iru._.cpri_link.update({'_tap': ru_tap}) %}
{%-   endfor  %}
{%- endmacro  %}

{%- macro _ru_set_defaults(ru)  %}
{%-   for k, v in defaults['ru'].items()  %}
{%-     do ru.setdefault(k, v) %}
{%-   endfor  %}
{%-   for k, v in defaults.get('ru/'+ru.ru_type, {}).items() %}
{%-     do ru.setdefault(k, v) %}
{%-   endfor  %}
{%-   if ru.ru_link_type == 'cpri' %}
{%-     set link = ru.cpri_link  %}
{%-     for k, v in defaults['ru/cpri_link'].items() %}
{%-       do link.setdefault(k, v)  %}
{%-     endfor  %}
{%-     for k, v in defaults['ru/%s/cpri_link' % ru.ru_type].items() %}
{%-       do link.setdefault(k, v)  %}
{%-     endfor  %}
{%-   endif %}
{%- endmacro  %}

{%- macro _cell_set_defaults(cell, icell_kind, icell_dict)  %}
{%-   if icell_kind == 'enb'  %}
{%-     for k, v in defaults['cell/%s' % cell.cell_type].items() %}
{%-       do cell.setdefault(k, v) %}
{%-     endfor  %}
{%-     for k, v in defaults['cell/%s/%s' % (cell.cell_type, cell.rf_mode)].items() %}
{%-       do cell.setdefault(k, v) %}
{%-     endfor  %}
{%-     set n = len(list(icell_dict|dictsort | selectattr('1._.cell_type', '==', cell.cell_type)))  %}
{%-     do cell.setdefault('root_sequence_index', 1 + 203*(cell.cell_type == 'lte') + n)  %}
{%-   endif %}
{%-   if cell.cell_type == 'lte'  %}
{%-     do cell.setdefault('ul_earfcn',    J(jdefault_ul_earfcn(cell.dl_earfcn))) %}
{%-   elif cell.cell_type == 'nr' %}
{%-     do cell.setdefault('ul_nr_arfcn',  J(jdefault_ul_nr_arfcn(cell.dl_nr_arfcn, cell.nr_band)))  %}
{%-     do cell.setdefault('subcarrier_spacing', 30  if cell.rf_mode == 'tdd'  else 15)  %}
{%-     do cell.setdefault('ssb_nr_arfcn', J(jdefault_ssb_nr_arfcn(cell.dl_nr_arfcn, cell.nr_band)))  %}
{%-   else  %}
{%-     do bug('unreachable') %}
{%-   endif %}
{%- endmacro %}


{#- jcell_ru_ref returns RU reference linked from a cell.
    if the cell embeds RU definition, its reference comes as `_<cell_ref>_ru`. #}
{%- macro jcell_ru_ref(icell, icell_dict) %}
{{-   _jcell_ru_ref(icell, icell_dict, [])  }}
{%- endmacro  %}
{%- macro _jcell_ru_ref(icell, icell_dict, seen)  %}
{%-   set cell_ref = J(jref_of_shared(icell)) %}
{%-   if cell_ref in seen %}
{%-     for x in seen %}
{%-       do ierror(x, "%s form a cycle via RU references" % seen)  %}
{%-     endfor  %}
{{-     None | tojson }}
{%-   else  %}
{%-     do seen.append(cell_ref)  %}
{%-     set ru = icell['_']['ru'] %}
{%-     if ru.ru_type == 'ru_ref' %}
{{-       ru.ru_ref | tojson  }}
{%-     elif ru.ru_type == 'ruincell_ref' %}
{{-       _jcell_ru_ref(icell_dict[ru.ruincell_ref], icell_dict, seen)  }}
{%-     else  %}
{#-       ru definition is embedded into cell #}
{{-       ('_%s_ru' % J(jref_of_shared(icell))) | tojson  }}
{%-     endif %}
{%-   endif %}
{%- endmacro  %}


{#- load_ipeer initializes peer registry.

    ipeer_dict keeps peer shared instances:  reference -> ipeer
#}
{%- macro load_ipeer(ipeer_dict)  %}
{%-   set qother = [] %}
{%-   for ishared in qshared_instance_list  %}
{%-     set ref = J(jref_of_shared(ishared))  %}
{%-     set _ = ishared['_']  %}
{%-     if 'peer_type' in _ %}
{%-       set ipeer = ishared %}
{%-       do assert(_.peer_type in ('lte', 'nr')) %}
{%-       do ipeer_dict.update({ref: ipeer})  %}
{%-     else  %}
{%-       do qother.append(ishared) %}
{%-     endif %}
{%-   endfor  %}
{%-   do qshared_instance_list.clear()  %}
{%-   do qshared_instance_list.extend(qother) %}
{%- endmacro  %}


{#- load_ipeercell initializes peer-cell registry.

    ipeercell_dict keeps peer cell shared instances:  reference -> ipeercell
#}
{%- macro load_ipeercell(ipeercell_dict)  %}
{%-   set qother = [] %}
{%-   for ishared in qshared_instance_list  %}
{%-     set ref = J(jref_of_shared(ishared))  %}
{%-     set _ = ishared['_']  %}
{%-     if 'cell_type' in _  and  _.get('cell_kind') == 'enb_peer'  %}
{%-       set ipeercell = ishared %}
{%-       if _.cell_type == 'lte'  %}
{%-         do _.setdefault('ul_earfcn',    J(jdefault_ul_earfcn(_.dl_earfcn))) %}
{%-       elif _.cell_type == 'nr' %}
{%-         do _.setdefault('ul_nr_arfcn',  J(jdefault_ul_nr_arfcn(_.dl_nr_arfcn, _.nr_band)))  %}
{%-         do _.setdefault('subcarrier_spacing',
                    30  if nrarfcn_module.get_duplex_mode(_.nr_band).lower() == 'tdd'  else
                    15) %}
{%-         do _.setdefault('ssb_nr_arfcn', J(jdefault_ssb_nr_arfcn(_.dl_nr_arfcn, _.nr_band)))  %}
{%-       else  %}
{%-         do bug('unreachable') %}
{%-       endif %}
{%-       do ipeercell_dict.update({ref: ipeercell})  %}
{%-     else  %}
{%-       do qother.append(ishared) %}
{%-     endif %}
{%-   endfor  %}
{%-   do qshared_instance_list.clear()  %}
{%-   do qshared_instance_list.extend(qother) %}
{%- endmacro  %}


{#- load_iue initializes UE registry.

    iue_dict keeps ue shared instance:  reference -> iue
#}
{%- macro load_iue(iue_dict)  %}
{%-   set qother = [] %}
{%-   for ishared in qshared_instance_list  %}
{%-     set ref = J(jref_of_shared(ishared))  %}
{%-     set _ = ishared['_']  %}
{%-     if   'ue_type'   in _ or (ors and 'imsi' in _) %}
{%-       set iue = ishared %}
{%-       if   ors %}
{%-         do _.update({'ue_type': 'lte'}) %}
{%-       endif %}
{%-       for k, v in defaults['ue'].items()  %}
{%-         do _.setdefault(k, v) %}
{%-       endfor  %}
{%-       do iue_dict.update({ref: iue})  %}
{%-     else  %}
{%-       do qother.append(ishared) %}
{%-     endif %}
{%-   endfor  %}
{%-   do qshared_instance_list.clear()  %}
{%-   do qshared_instance_list.extend(qother) %}
{%- endmacro  %}


{#- ---- building configuration ---- #}

{#- ru_config emits RF driver configuration for specified Radio Units. #}
{%- macro ru_config(iru_dict, slapparameter_dict, support_gain_in_list) %}
  // Radio Units
  rf_driver: {
  {%- set dev_argv = []                                                       %}
  {%- set ru_sdr_dict = {}  %}  {#- dev -> ru  for ru with ru_type = sdr      #}
  {%- set ru_cpri_dict = {} %}  {#- dev -> ru  for ru with link_type = cpri   #}
  {%- set tx_gainv = [] %}      {#- tx_gain by tx channel                     #}
  {%- set rx_gainv = [] %}      {#- rx_gain by rx channel                     #}
  {%- for (ru_ref, iru) in iru_dict.items() | sort(attribute="1._._rf_port")  %}
  {%-   set ru = iru['_'] %}
      // {{ B(ru_ref) }} {{ ru.n_antenna_dl }}T{{ ru.n_antenna_ul }}R  ({{ ru.ru_type }})
  {%-   if ru.ru_link_type == 'sdr'  %}
  {%-     do ru_sdr_dict.update({len(dev_argv): ru})  %}
  {%-     for n in ru.sdr_dev_list  %}
  {%-       do dev_argv.append("dev%d=/dev/sdr%d" % (len(dev_argv), n)) %}
  {%-     endfor  %}
  {%-   elif ru.ru_link_type == 'cpri'  %}
  {%-     do ru_cpri_dict.update({len(dev_argv): ru}) %}
  {%-     set link = ru.cpri_link %}
  {%-     do dev_argv.append("dev%d=/dev/sdr%d@%d" % (len(dev_argv), link.sdr_dev, link.sfp_port))  %}
  {%-   else  %}
  {%-     do bug('unreachable') %}
  {%-   endif %}
  {%-   set ru_tx_gain = ru.tx_gain  if ru.txrx_active == 'ACTIVE'  else -1000 %}
  {%-   do tx_gainv.extend([ru_tx_gain]*ru.n_antenna_dl)  %}
  {%-   do rx_gainv.extend([ru.rx_gain]*ru.n_antenna_ul)  %}
  {%- endfor %}

  {#- emit big error if both sdr and cpri are present
      to protect users from unclear eNB failures in such unsupported combination  #}
  {%- set do_sdr  = len(ru_sdr_dict)  > 0 %}
  {%- set do_cpri = len(ru_cpri_dict) > 0 %}
  {%- if do_sdr and do_cpri %}
  {%-   do error('Mixing SDR + CPRI is not supported and breaks subtly.

    SDR  Radio Units: %r
    CPRI Radio Units: %r

    See https://support.amarisoft.com/issues/26021 for details' % (
          iru_dict |dictsort |selectattr('1._.ru_type',      '==', 'sdr')  |map(attribute='0') |list,
          iru_dict |dictsort |selectattr('1._.ru_link_type', '==', 'cpri') |map(attribute='0') |list
        )) %}
  {%- endif %}

  {#- disable trx completely if all we have is only inactive sdr(s).
      do not disable if there is cpri, because for cpri whether to activate
      radio or not is natively controlled via RU-specific config.
      See e.g. ru/lopcomm/cu_config.jinja2.xml for details #}
  {%- if do_sdr  and  (not do_cpri)  and
         len(iru_dict|dictsort | selectattr('1._.txrx_active', '==', 'ACTIVE') | list) == 0 %}
      name: "dummy",
  {%- else %}
      name: "sdr",
  {%- endif %}

  {%- if slapparameter_dict.get('gps_sync', False) %}
      sync: "gps",
  {%- endif %}

  {#- below we continue as if sdr and cpri are both supported by enb simultaneously #}
      args: "{{(dev_argv | join(',')) or '---'}}",

  {%- if ors %}
      rx_antenna:"tx_rx",
      tdd_tx_mod: 1,
  {%- endif %}

  {#- emit cpri_* options if a cpri ru is present #}
  {#- NOTE values for non-cpri links come as empty  #}
  {%- if do_cpri  %}
  {%-   set vcpri = [None]*len(dev_argv)  %}
  {%-   for dev, ru in ru_cpri_dict|dictsort  %}
  {%-     do vcpri.__setitem__(dev, ru.cpri_link) %}
  {%-   endfor  %}
        cpri_mapping:  "{{ vcpri | map(attribute='mapping')  | map('default', '') | join(',') }}",
        cpri_mult:     "{{ vcpri | map(attribute='mult')     | map('default', '') | join(',') }}",
        cpri_rx_delay: "{{ vcpri | map(attribute='rx_delay') | map('default', '') | join(',') }}",
        cpri_tx_delay: "{{ vcpri | map(attribute='tx_delay') | map('default', '') | join(',') }}",
        cpri_tx_dbm:   "{{ vcpri | map(attribute='tx_dbm')   | map('default', '') | join(',') }}",
        ifname:        "{{ vcpri | map(attribute='_tap')     | map('default', '') | join(',') }}",
  {%- endif %}
  },

  {#- emit tx/rx gain for all channels #}
  {%- if support_gain_in_list  %}
  tx_gain: {{ tx_gainv }},
  rx_gain: {{ rx_gainv }},
  {%- else %}
  tx_gain: {{ tx_gainv[0] }},
  rx_gain: {{ rx_gainv[0] }},
  {%- endif %}

{%- endmacro  %}
