/*
 * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The OpenAirInterface Software Alliance licenses this file to You under
 * the OAI Public License, Version 1.1  (the "License"); you may not use this file
 * except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.openairinterface.org/?page_id=698
 *
 * Author and copyright: Laurent Thomas, open-cells.com
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *-------------------------------------------------------------------------------
 * For more information about the OpenAirInterface (OAI) Software Alliance:
 *      contact@openairinterface.org
 */

#include <arpa/inet.h>
#include "e1ap_api.h"
#include "UTIL/OSA/osa_defs.h"
#include "nr_pdcp/nr_pdcp_entity.h"
#include "openair2/LAYER2/nr_pdcp/nr_pdcp_e1_api.h"
#include "openair2/RRC/NR/cucp_cuup_if.h"
#include "openair2/RRC/LTE/MESSAGES/asn1_msg.h"
#include "openair3/ocp-gtpu/gtp_itf.h"
#include "e1ap_asnc.h"
#include "e1ap_common.h"
#include "e1ap.h"

struct NR_DRB_ToAddMod;
static void fill_DRB_configList_e1(NR_DRB_ToAddModList_t *DRB_configList, pdu_session_to_setup_t *pdu) {

  for (int i=0; i < pdu->numDRB2Setup; i++) {
    DRB_nGRAN_to_setup_t *drb = pdu->DRBnGRanList + i;
    asn1cSequenceAdd(DRB_configList->list, struct NR_DRB_ToAddMod, ie);
    ie->drb_Identity = drb->id;
    ie->cnAssociation = CALLOC(1, sizeof(*ie->cnAssociation));
    ie->cnAssociation->present = NR_DRB_ToAddMod__cnAssociation_PR_sdap_Config;

    // sdap_Config
    asn1cCalloc(ie->cnAssociation->choice.sdap_Config, sdap_config);
    sdap_config->pdu_Session = pdu->sessionId;
    sdap_config->sdap_HeaderDL = drb->sDAP_Header_DL;
    sdap_config->sdap_HeaderUL = drb->sDAP_Header_UL;
    sdap_config->defaultDRB = drb->defaultDRB;

    asn1cCalloc(sdap_config->mappedQoS_FlowsToAdd, FlowsToAdd);
    for (int j=0; j < drb->numQosFlow2Setup; j++) {
      asn1cSequenceAdd(FlowsToAdd->list, NR_QFI_t, qfi);
      *qfi = drb->qosFlows[j].fiveQI;
    }
    sdap_config->mappedQoS_FlowsToRelease = NULL;

    // pdcp_Config
    ie->reestablishPDCP = NULL;
    ie->recoverPDCP = NULL;
    asn1cCalloc(ie->pdcp_Config, pdcp_config);
    asn1cCalloc(pdcp_config->drb, drbCfg);
    asn1cCallocOne(drbCfg->discardTimer, drb->discardTimer);
    asn1cCallocOne(drbCfg->pdcp_SN_SizeUL, drb->pDCP_SN_Size_UL);
    asn1cCallocOne(drbCfg->pdcp_SN_SizeDL, drb->pDCP_SN_Size_DL);
    drbCfg->headerCompression.present = NR_PDCP_Config__drb__headerCompression_PR_notUsed;
    drbCfg->headerCompression.choice.notUsed = 0;

    drbCfg->integrityProtection = NULL;
    drbCfg->statusReportRequired = NULL;
    drbCfg->outOfOrderDelivery = NULL;
    pdcp_config->moreThanOneRLC = NULL;

    pdcp_config->t_Reordering = calloc(1, sizeof(*pdcp_config->t_Reordering));
    *pdcp_config->t_Reordering = drb->reorderingTimer;
    pdcp_config->ext1 = NULL;

    if (pdu->integrityProtectionIndication == 0 || // Required
        pdu->integrityProtectionIndication == 1) { // Preferred
      asn1cCallocOne(drbCfg->integrityProtection, NR_PDCP_Config__drb__integrityProtection_enabled);
    }

    if (pdu->confidentialityProtectionIndication == 0 || // Required
        pdu->confidentialityProtectionIndication == 1) { // Preferred
      asn1cCalloc(pdcp_config->ext1, ext1);
      asn1cCallocOne(ext1->cipheringDisabled, NR_PDCP_Config__ext1__cipheringDisabled_true);
    }
  }
}

static int drb_config_N3gtpu_create(e1ap_bearer_setup_req_t * const req,
                                    gtpv1u_gnb_create_tunnel_resp_t *create_tunnel_resp,
                                    instance_t instance) {

  gtpv1u_gnb_create_tunnel_req_t create_tunnel_req={0};

  NR_DRB_ToAddModList_t DRB_configList = {0};
  for (int i=0; i < req->numPDUSessions; i++) {
    pdu_session_to_setup_t *const pdu = &req->pduSession[i];
    create_tunnel_req.pdusession_id[i] = pdu->sessionId;
    create_tunnel_req.incoming_rb_id[i] = pdu->DRBnGRanList[0].id; // taking only the first DRB. TODO:change this
    memcpy(&create_tunnel_req.dst_addr[i].buffer,
           &pdu->tlAddress,
           sizeof(uint8_t)*4);
    create_tunnel_req.dst_addr[i].length = 32; // 8bits * 4bytes
    create_tunnel_req.outgoing_teid[i] = pdu->teId;
    fill_DRB_configList_e1(&DRB_configList, pdu);
  }
  create_tunnel_req.num_tunnels = req->numPDUSessions;
  create_tunnel_req.ue_id = (req->gNB_cu_cp_ue_id & 0xFFFF);

  // Create N3 tunnel
  int ret = gtpv1u_create_ngu_tunnel(instance, &create_tunnel_req, create_tunnel_resp, nr_pdcp_data_req_drb, sdap_data_req);
  if (ret != 0) {
    LOG_E(NR_RRC,"rrc_gNB_process_NGAP_PDUSESSION_SETUP_REQ : gtpv1u_create_ngu_tunnel failed,start to release UE id %ld\n",
          create_tunnel_req.ue_id);
    return ret;
  }

  // Configure DRBs
  uint8_t *kUPenc = NULL;
  uint8_t *kUPint = NULL;

  nr_derive_key(UP_ENC_ALG, req->cipheringAlgorithm, (uint8_t *)req->encryptionKey, &kUPenc);

  nr_derive_key(UP_INT_ALG, req->integrityProtectionAlgorithm, (uint8_t *)req->integrityProtectionKey, &kUPint);

  nr_pdcp_e1_add_drbs(true, // set this to notify PDCP that his not UE
                      create_tunnel_req.ue_id,
                      &DRB_configList,
                      (req->integrityProtectionAlgorithm << 4) | req->cipheringAlgorithm,
                      kUPenc,
                      kUPint);
  return ret;
}

void process_e1_bearer_context_setup_req(instance_t instance, e1ap_bearer_setup_req_t *const req)
{
  e1ap_upcp_inst_t *inst = getCxtE1(instance);
  AssertFatal(inst, "");
  gtpv1u_gnb_create_tunnel_resp_t create_tunnel_resp_N3={0};

  // GTP tunnel for UL
  drb_config_N3gtpu_create(req, &create_tunnel_resp_N3, inst->gtpInstN3);

  MessageDef *msg = itti_alloc_new_message(TASK_CUCP_E1, 0, E1AP_BEARER_CONTEXT_SETUP_RESP);
  e1ap_bearer_setup_resp_t *resp = &E1AP_BEARER_CONTEXT_SETUP_RESP(msg);

  in_addr_t my_addr;
  if (inet_pton(AF_INET, inst->setupReq.localAddressF1U, &my_addr) != 1)
    LOG_E(E1AP, "can't use the F1-U local interface: %s\n", inst->setupReq.localAddressF1U);
  fill_e1ap_bearer_setup_resp(resp, req, inst->gtpInstF1U, req->gNB_cu_cp_ue_id, inst->setupReq.remotePortF1U, my_addr);

  resp->gNB_cu_cp_ue_id = req->gNB_cu_cp_ue_id;
  resp->numPDUSessions = req->numPDUSessions;
  for (int i=0; i < req->numPDUSessions; i++) {
    pdu_session_setup_t *pduSetup = resp->pduSession + i;
    pdu_session_to_setup_t *pdu2Setup = req->pduSession + i;

    pduSetup->id = pdu2Setup->sessionId;
    if (inet_pton(AF_INET, inst->setupReq.localAddressN3, &pduSetup->tlAddress) != 1)
      LOG_E(E1AP, "can't use the N3 local interface: %s\n", inst->setupReq.localAddressN3);
    pduSetup->teId = create_tunnel_resp_N3.gnb_NGu_teid[i];
    pduSetup->numDRBSetup = pdu2Setup->numDRB2Setup;

    // At this point we don't have a way to know the DRBs that failed to setup
    // We assume all DRBs to setup have are setup successfully so we always send successful outcome in response
    // TODO: Modify nr_pdcp_add_drbs() to return DRB list that failed to setup to support E1AP
    pduSetup->numDRBFailed = 0;
  }

  e1apCUUP_send_BEARER_CONTEXT_SETUP_RESPONSE(inst, resp);
}

void CUUP_process_bearer_context_mod_req(instance_t instance, e1ap_bearer_setup_req_t *const req)
{
  e1ap_upcp_inst_t *inst = getCxtE1(instance);
  AssertFatal(inst, "");
  // assume we receive modification of F1-U but it is wrong, we can also get modification of N3 when HO will occur
  CU_update_UP_DL_tunnel(req, inst->gtpInstF1U, req->gNB_cu_cp_ue_id);
  // TODO: send bearer cxt mod response
}

void CUUP_process_bearer_release_command(instance_t instance, e1ap_bearer_release_cmd_t *const cmd)
{
  e1ap_upcp_inst_t *inst = getCxtE1(instance);
  AssertFatal(inst, "");
  newGtpuDeleteAllTunnels(inst->gtpInstN3, cmd->gNB_cu_up_ue_id);
  newGtpuDeleteAllTunnels(inst->gtpInstF1U, cmd->gNB_cu_up_ue_id);
  e1apCUUP_send_BEARER_CONTEXT_RELEASE_COMPLETE(inst, cmd);
}