#include "../nr_rlc_entity.h"
#include "../nr_rlc_entity_am.h"
#include "../nr_rlc_entity_um.h"
#include "../nr_rlc_entity_tm.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef _STANDALONE_TESTING_
#include "common/utils/LOG/log.h"
#endif

/*
 * GNB_AM <rx_maxsize> <tx_maxsize> <t_poll_retransmit> <t_reassembly>
 *       <t_status_prohibit> <poll_pdu> <poll_byte> <max_retx_threshold>
 *       <sn_field_length>
 *       create the gNB RLC AM entity with given parameters
 *
 * UE_AM <rx_maxsize> <tx_maxsize> <t_poll_retransmit> <t_reassembly>
 *      <t_status_prohibit> <poll_pdu> <poll_byte> <max_retx_threshold>
 *      <sn_field_length>
 *       create the UE RLC AM entity with given parameters
 *
 * GNB_UM <rx_maxsize> <tx_maxsize> <t_reassembly> <sn_field_length>
 *     create the gNB RLC UM entity with given parameters
 *
 * UE_UM <rx_maxsize> <tx_maxsize> <t_reassembly> <sn_field_length>
 *     create the UE RLC UM entity with given parameters
 *
 * GNB_TM <tx_maxsize>
 *     create the gNB RLC TM entity with given parameters
 *
 * UE_UM <tx_maxsize>
 *     create the UE RLC TM entity with given parameters
 *
 * TIME <time>
 *     following actions to be performed at time <time>
 *     <time> starts at 1
 *     You must end your test definition with a line 'TIME, -1'.
 *
 * GNB_SDU <id> <size>
 *     send an SDU to gNB with id <i> and size <size>
 *     the SDU is [00 01 ... ff 01 ...]
 *     (ie. start byte is 00 then we increment for each byte, loop if needed)
 *
 * UE_SDU <id> <size>
 *     same as GNB_SDU but the SDU is sent to the UE
 *
 * GNB_PDU <size> <'size' bytes>
 *     send a custom PDU from gNB to UE (gNB does not see this PDU at all)
 *
 * UE_PDU <size> <'size' bytes>
 *     send a custom PDU from UE to gNB (UE does not see this PDU at all)
 *
 * GNB_PDU_SIZE <size>
 *     set 'gnb_pdu_size'
 *
 * UE_PDU_SIZE <size>
 *     set 'ue_pdu_size'
 *
 * GNB_RECV_FAILS <fails>
 *     set the 'gnb_recv_fails' flag to <fails>
 *     (1: recv will fail, 0: recv will succeed)
 *
 * UE_RECV_FAILS <fails>
 *     same as GNB_RECV_FAILS but for 'ue_recv_fails'
 *
 * MUST_FAIL
 *     to be used as first command after the first TIME to indicate
 *     that the test must fail (ie. exit with non zero, crash not allowed)
 *
 * GNB_BUFFER_STATUS
 *     call buffer_status for gNB and print result
 *
 * UE_BUFFER_STATUS
 *     call buffer_status for UE and print result
 *
 * GNB_DISCARD_SDU <sdu ID>
 *     discards given SDU
 *
 * UE_DISCARD_SDU <sdu ID>
 *     discards given SDU
 *
 * RE_ESTABLISH
 *     re-establish both gNB and UE
 */

enum action {
  GNB_AM, UE_AM,
  GNB_UM, UE_UM,
  GNB_TM, UE_TM,
  TIME, GNB_SDU, UE_SDU, GNB_PDU, UE_PDU,
  GNB_PDU_SIZE, UE_PDU_SIZE,
  GNB_RECV_FAILS, UE_RECV_FAILS,
  MUST_FAIL,
  GNB_BUFFER_STATUS, UE_BUFFER_STATUS,
  GNB_DISCARD_SDU, UE_DISCARD_SDU,
  RE_ESTABLISH
};

int test[] = {
/* TEST is defined at compilation time */
#include TEST
};

void deliver_sdu_gnb_am(void *deliver_sdu_data,
                        struct nr_rlc_entity_t *_entity,
                        char *buf, int size)
{
  nr_rlc_entity_am_t *entity = (nr_rlc_entity_am_t *)_entity;
  printf("TEST: GNB: %"PRIu64": deliver SDU size %d [",
         entity->t_current, size);
  for (int i = 0; i < size; i++) printf(" %2.2x", (unsigned char)buf[i]);
  printf("]\n");
}

void deliver_sdu_gnb_um(void *deliver_sdu_data,
                        struct nr_rlc_entity_t *_entity,
                        char *buf, int size)
{
  nr_rlc_entity_um_t *entity = (nr_rlc_entity_um_t *)_entity;
  printf("TEST: GNB: %"PRIu64": deliver SDU size %d [",
         entity->t_current, size);
  for (int i = 0; i < size; i++) printf(" %2.2x", (unsigned char)buf[i]);
  printf("]\n");
}

void deliver_sdu_gnb_tm(void *deliver_sdu_data,
                        struct nr_rlc_entity_t *_entity,
                        char *buf, int size)
{
  nr_rlc_entity_tm_t *entity = (nr_rlc_entity_tm_t *)_entity;
  printf("TEST: GNB: %"PRIu64": deliver SDU size %d [",
         entity->t_current, size);
  for (int i = 0; i < size; i++) printf(" %2.2x", (unsigned char)buf[i]);
  printf("]\n");
}

void successful_delivery_gnb(void *successful_delivery_data,
                             nr_rlc_entity_t *_entity, int sdu_id)
{
  nr_rlc_entity_am_t *entity = (nr_rlc_entity_am_t *)_entity;
  printf("TEST: GNB: %"PRIu64": SDU %d was successfully delivered.\n",
         entity->t_current, sdu_id);
}

void max_retx_reached_gnb(void *max_retx_reached_data,
                          nr_rlc_entity_t *_entity)
{
  nr_rlc_entity_am_t *entity = (nr_rlc_entity_am_t *)_entity;
  printf("TEST: GNB: %"PRIu64": max RETX reached! radio link failure!\n",
         entity->t_current);
  exit(1);
}

void deliver_sdu_ue_am(void *deliver_sdu_data,
                       struct nr_rlc_entity_t *_entity,
                       char *buf, int size)
{
  nr_rlc_entity_am_t *entity = (nr_rlc_entity_am_t *)_entity;
  printf("TEST: UE: %"PRIu64": deliver SDU size %d [",
         entity->t_current, size);
  for (int i = 0; i < size; i++) printf(" %2.2x", (unsigned char)buf[i]);
  printf("]\n");
}

void deliver_sdu_ue_um(void *deliver_sdu_data,
                       struct nr_rlc_entity_t *_entity,
                       char *buf, int size)
{
  nr_rlc_entity_um_t *entity = (nr_rlc_entity_um_t *)_entity;
  printf("TEST: UE: %"PRIu64": deliver SDU size %d [",
         entity->t_current, size);
  for (int i = 0; i < size; i++) printf(" %2.2x", (unsigned char)buf[i]);
  printf("]\n");
}

void deliver_sdu_ue_tm(void *deliver_sdu_data,
                       struct nr_rlc_entity_t *_entity,
                       char *buf, int size)
{
  nr_rlc_entity_tm_t *entity = (nr_rlc_entity_tm_t *)_entity;
  printf("TEST: UE: %"PRIu64": deliver SDU size %d [",
         entity->t_current, size);
  for (int i = 0; i < size; i++) printf(" %2.2x", (unsigned char)buf[i]);
  printf("]\n");
}

void successful_delivery_ue(void *successful_delivery_data,
                            nr_rlc_entity_t *_entity, int sdu_id)
{
  nr_rlc_entity_am_t *entity = (nr_rlc_entity_am_t *)_entity;
  printf("TEST: UE: %"PRIu64": SDU %d was successfully delivered.\n",
         entity->t_current, sdu_id);
}

void max_retx_reached_ue(void *max_retx_reached_data,
                         nr_rlc_entity_t *_entity)
{
  nr_rlc_entity_am_t *entity = (nr_rlc_entity_am_t *)_entity;
  printf("TEST: UE: %"PRIu64", max RETX reached! radio link failure!\n",
         entity->t_current);
  exit(1);
}

int test_main(void)
{
  nr_rlc_entity_t *gnb = NULL;
  nr_rlc_entity_t *ue = NULL;
  int i;
  int k;
  char *sdu;
  char *pdu;
  nr_rlc_entity_buffer_status_t buffer_status;
  int gnb_do_buffer_status = 0;
  int ue_do_buffer_status = 0;
  int size;
  int pos;
  int next_byte_gnb = 0;
  int next_byte_ue = 0;
  int gnb_recv_fails = 0;
  int ue_recv_fails = 0;
  int gnb_pdu_size = 1000;
  int ue_pdu_size = 1000;

#ifndef _STANDALONE_TESTING_
  logInit();
#endif

  printf("TEST: start\n");

  sdu = malloc(16001);
  pdu = malloc(3000);
  if (sdu == NULL || pdu == NULL) {
    printf("out of memory\n");
    exit(1);
  }

  for (i = 0; i < 16001; i++)
    sdu[i] = i & 255;

  pos = 0;
  if (test[pos] != TIME) {
    printf("%s:%d:%s: fatal\n", __FILE__, __LINE__, __FUNCTION__);
    exit(1);
  }

  for (i = 1; i < 1000; i++) {
    if (i == test[pos+1]) {
      pos += 2;
      while (test[pos] != TIME)
        switch (test[pos]) {
        default: printf("fatal: unknown action\n"); exit(1);
        case GNB_AM:
          gnb = new_nr_rlc_entity_am(test[pos+1], test[pos+2],
                                     deliver_sdu_gnb_am, NULL,
                                     successful_delivery_gnb, NULL,
                                     max_retx_reached_gnb, NULL,
                                     test[pos+3], test[pos+4], test[pos+5],
                                     test[pos+6], test[pos+7], test[pos+8],
                                     test[pos+9]);
          pos += 10;
          break;
        case UE_AM:
          ue = new_nr_rlc_entity_am(test[pos+1], test[pos+2],
                                    deliver_sdu_ue_am, NULL,
                                    successful_delivery_ue, NULL,
                                    max_retx_reached_ue, NULL,
                                    test[pos+3], test[pos+4], test[pos+5],
                                    test[pos+6], test[pos+7], test[pos+8],
                                    test[pos+9]);
          pos += 10;
          break;
        case GNB_UM:
          gnb = new_nr_rlc_entity_um(test[pos+1], test[pos+2],
                                     deliver_sdu_gnb_um, NULL,
                                     test[pos+3], test[pos+4]);
          pos += 5;
          break;
        case UE_UM:
          ue = new_nr_rlc_entity_um(test[pos+1], test[pos+2],
                                    deliver_sdu_ue_um, NULL,
                                    test[pos+3], test[pos+4]);
          pos += 5;
          break;
        case GNB_TM:
          gnb = new_nr_rlc_entity_tm(test[pos+1], deliver_sdu_gnb_tm, NULL);
          pos += 2;
          break;
        case UE_TM:
          ue = new_nr_rlc_entity_tm(test[pos+1], deliver_sdu_ue_tm, NULL);
          pos += 2;
          break;
        case GNB_SDU:
          for (k = 0; k < test[pos+2]; k++, next_byte_gnb++)
            sdu[k] = next_byte_gnb;
          printf("TEST: GNB: %d: recv_sdu (id %d): size %d: [",
                 i, test[pos+1], test[pos+2]);
          for (k = 0; k < test[pos+2]; k++)
            printf(" %2.2x", (unsigned char)sdu[k]);
          printf("]\n");
          gnb->recv_sdu(gnb, sdu, test[pos+2], test[pos+1]);
          pos += 3;
          break;
        case UE_SDU:
          for (k = 0; k < test[pos+2]; k++, next_byte_ue++)
            sdu[k] = next_byte_ue;
          printf("TEST: UE: %d: recv_sdu (id %d): size %d: [",
                 i, test[pos+1], test[pos+2]);
          for (k = 0; k < test[pos+2]; k++)
            printf(" %2.2x", (unsigned char)sdu[k]);
          printf("]\n");
          ue->recv_sdu(ue, sdu, test[pos+2], test[pos+1]);
          pos += 3;
          break;
        case GNB_PDU:
          for (k = 0; k < test[pos+1]; k++)
            pdu[k] = test[pos+2+k];
          printf("TEST: GNB: %d: custom PDU: size %d: [", i, test[pos+1]);
          for (k = 0; k < test[pos+1]; k++) printf(" %2.2x", (unsigned char)pdu[k]);
          printf("]\n");
          if (!ue_recv_fails)
            ue->recv_pdu(ue, pdu, test[pos+1]);
          pos += 2 + test[pos+1];
          break;
        case UE_PDU:
          for (k = 0; k < test[pos+1]; k++)
            pdu[k] = test[pos+2+k];
          printf("TEST: UE: %d: custom PDU: size %d: [", i, test[pos+1]);
          for (k = 0; k < test[pos+1]; k++) printf(" %2.2x", (unsigned char)pdu[k]);
          printf("]\n");
          if (!gnb_recv_fails)
            gnb->recv_pdu(gnb, pdu, test[pos+1]);
          pos += 2 + test[pos+1];
          break;
        case GNB_PDU_SIZE:
          gnb_pdu_size = test[pos+1];
          pos += 2;
          break;
        case UE_PDU_SIZE:
          ue_pdu_size = test[pos+1];
          pos += 2;
          break;
        case GNB_RECV_FAILS:
          gnb_recv_fails = test[pos+1];
          pos += 2;
          break;
        case UE_RECV_FAILS:
          ue_recv_fails = test[pos+1];
          pos += 2;
          break;
        case MUST_FAIL:
          /* do nothing, only used by caller */
          pos++;
          break;
        case GNB_BUFFER_STATUS:
          gnb_do_buffer_status = 1;
          pos++;
          break;
        case UE_BUFFER_STATUS:
          ue_do_buffer_status = 1;
          pos++;
          break;
        case GNB_DISCARD_SDU:
          printf("TEST: GNB: %d: discard SDU %d\n", i, test[pos+1]);
          gnb->discard_sdu(gnb, test[pos+1]);
          pos += 2;
          break;
        case UE_DISCARD_SDU:
          printf("TEST: UE: %d: discard SDU %d\n", i, test[pos+1]);
          ue->discard_sdu(ue, test[pos+1]);
          pos += 2;
          break;
        case RE_ESTABLISH:
          printf("TEST: %d: re-establish gNB and UE\n", i);
          gnb->reestablishment(gnb);
          ue->reestablishment(ue);
          pos++;
          break;
        }
    }

    gnb->set_time(gnb, i);
    ue->set_time(ue, i);

    if (gnb_do_buffer_status) {
      gnb_do_buffer_status = 0;
      buffer_status = gnb->buffer_status(gnb, gnb_pdu_size);
      printf("TEST: GNB: %d: buffer_status: status_size %d tx_size %d retx_size %d\n",
             i,
             buffer_status.status_size,
             buffer_status.tx_size,
             buffer_status.retx_size);
    }

    size = gnb->generate_pdu(gnb, pdu, gnb_pdu_size);
    if (size) {
      printf("TEST: GNB: %d: generate_pdu: size %d: [", i, size);
      for (k = 0; k < size; k++) printf(" %2.2x", (unsigned char)pdu[k]);
      printf("]\n");
      if (!ue_recv_fails)
        ue->recv_pdu(ue, pdu, size);
    }

    if (ue_do_buffer_status) {
      ue_do_buffer_status = 0;
      buffer_status = ue->buffer_status(ue, ue_pdu_size);
      printf("TEST: UE: %d: buffer_status: status_size %d tx_size %d retx_size %d\n",
             i,
             buffer_status.status_size,
             buffer_status.tx_size,
             buffer_status.retx_size);
    }

    size = ue->generate_pdu(ue, pdu, ue_pdu_size);
    if (size) {
      printf("TEST: UE: %d: generate_pdu: size %d: [", i, size);
      for (k = 0; k < size; k++) printf(" %2.2x", (unsigned char)pdu[k]);
      printf("]\n");
      if (!gnb_recv_fails)
        gnb->recv_pdu(gnb, pdu, size);
    }
  }

  gnb->delete_entity(gnb);
  ue->delete_entity(ue);

  free(sdu);
  free(pdu);

  return 0;
}

void usage(void)
{
  printf("options:\n");
  printf("    -no-fork\n");
  printf("        don't fork (to ease debugging with gdb)\n");
  exit(0);
}

int main(int n, char **v)
{
  int must_fail = 0;
  int son;
  int status;
  int i;
  int no_fork = 0;

  for (i = 1; i < n; i++) {
    if (!strcmp(v[i], "-no-fork")) { no_fork = 1; continue; }
    usage();
  }

  if (test[2] == MUST_FAIL)
    must_fail = 1;

  if (no_fork) return test_main();

  son = fork();
  if (son == -1) {
    perror("fork");
    return 1;
  }

  if (son == 0)
    return test_main();

  if (wait(&status) == -1) {
    perror("wait");
    return 1;
  }

  /* child must quit properly */
  if (!WIFEXITED(status))
    return 1;

  /* child must fail if expected to */
  if (must_fail && WEXITSTATUS(status) == 0)
    return 1;

  /* child must not fail if not expected to */
  if (!must_fail && WEXITSTATUS(status))
    return 1;

  return 0;
}
