/*
 * 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
 *
 * 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 <pthread.h>
#include <stdint.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <sys/ioctl.h>
#include <net/if.h>

#include <netinet/in.h>
#include <netinet/sctp.h>

#include <arpa/inet.h>

#include "assertions.h"
#include "common/utils/system.h"
#include "queue.h"

#include "intertask_interface.h"

#include "sctp_default_values.h"
#include "sctp_common.h"
#include "sctp_eNB_itti_messaging.h"

/* Used to format an uint32_t containing an ipv4 address */
#define IPV4_ADDR    "%u.%u.%u.%u"
#define IPV4_ADDR_FORMAT(aDDRESS)               \
    (uint8_t)((aDDRESS)  & 0x000000ff),         \
    (uint8_t)(((aDDRESS) & 0x0000ff00) >> 8 ),  \
    (uint8_t)(((aDDRESS) & 0x00ff0000) >> 16),  \
    (uint8_t)(((aDDRESS) & 0xff000000) >> 24)


enum sctp_connection_type_e {
    SCTP_TYPE_CLIENT,
    SCTP_TYPE_SERVER,
    SCTP_TYPE_MULTI_SERVER,
    SCTP_TYPE_MAX
};

typedef struct sctp_cnx_list_elm_s {
    STAILQ_ENTRY(sctp_cnx_list_elm_s) entries;

    /* Type of this association
     */
    enum sctp_connection_type_e connection_type;

    int        sd;              ///< Socket descriptor of connection */
    uint16_t   local_port;
    uint16_t   in_streams;      ///< Number of input streams negociated for this connection
    uint16_t   out_streams;     ///< Number of output streams negotiated for this connection
    uint16_t   ppid;            ///< Payload protocol Identifier
    sctp_assoc_t assoc_id;      ///< SCTP association id for the connection     (note4debug host byte order)
    uint32_t   messages_recv;   ///< Number of messages received on this connection
    uint32_t   messages_sent;   ///< Number of messages sent on this connection
    task_id_t  task_id;         ///< Task id of the task who asked for this connection
    instance_t instance;        ///< Instance
    uint16_t   cnx_id;          ///< Upper layer identifier

    struct   sockaddr *peer_addresses;///< A list of peer addresses for server socket
    int      nb_peer_addresses; ///< For server socket
} sctp_cnx_list_elm_t;


static STAILQ_HEAD(sctp_cnx_list_head, sctp_cnx_list_elm_s) sctp_cnx_list;
static uint16_t sctp_nb_cnx = 0;

//------------------------------------------------------------------------------
struct sctp_cnx_list_elm_s *sctp_get_cnx(sctp_assoc_t assoc_id, int sd)
{
    struct sctp_cnx_list_elm_s *elm;

    STAILQ_FOREACH(elm, &sctp_cnx_list, entries) {
        if (assoc_id != -1) {
            if (elm->assoc_id == assoc_id) {
                return elm;
            }
        } else {
            if (elm->sd == sd) {
                return elm;
            }
        }
    }

    return NULL;
}

//------------------------------------------------------------------------------
static inline
void
sctp_eNB_accept_associations_multi(
    struct sctp_cnx_list_elm_s *sctp_cnx)
{
    int                    ns;
    int                    n;
    int                    flags = 0;
    socklen_t              from_len;
    struct sctp_sndrcvinfo sinfo;

    struct sockaddr_in addr;
    uint8_t buffer[SCTP_RECV_BUFFER_SIZE];

    DevAssert(sctp_cnx != NULL);

    memset((void *)&addr, 0, sizeof(struct sockaddr_in));
    from_len = (socklen_t)sizeof(struct sockaddr_in);
    memset((void *)&sinfo, 0, sizeof(struct sctp_sndrcvinfo));

    n = sctp_recvmsg(sctp_cnx->sd, (void *)buffer, SCTP_RECV_BUFFER_SIZE,
                     (struct sockaddr *)&addr, &from_len,
                     &sinfo, &flags);

    if (n < 0) {
        if (errno == ENOTCONN) {
            SCTP_DEBUG("Received not connected for sd %d\n", sctp_cnx->sd);
            close(sctp_cnx->sd);
        } else {
            SCTP_DEBUG("An error occured during read\n");
            SCTP_ERROR("sctp_recvmsg (fd %d, len %d ): %s:%d\n", sctp_cnx->sd, n, strerror(errno), errno);
        }
        return;
    }

    if (flags & MSG_NOTIFICATION) {
        union sctp_notification *snp;
        snp = (union sctp_notification *)buffer;

        SCTP_DEBUG("Received notification for sd %d, type %u\n",
                   sctp_cnx->sd, snp->sn_header.sn_type);

        /* Association has changed. */
        if (SCTP_ASSOC_CHANGE == snp->sn_header.sn_type) {
            struct sctp_assoc_change *sctp_assoc_changed;
            sctp_assoc_changed = &snp->sn_assoc_change;

            SCTP_DEBUG("Client association changed: %d\n", sctp_assoc_changed->sac_state);

            /* New physical association requested by a peer */
            switch (sctp_assoc_changed->sac_state) {
            case SCTP_COMM_UP: {
                SCTP_DEBUG("Comm up notified for sd %d, assigned assoc_id %d\n",
                           sctp_cnx->sd, sctp_assoc_changed->sac_assoc_id);
                struct sctp_cnx_list_elm_s *new_cnx;

                new_cnx = calloc(1, sizeof(*sctp_cnx));

                DevAssert(new_cnx != NULL);

                new_cnx->connection_type = SCTP_TYPE_CLIENT;

                ns = sctp_peeloff(sctp_cnx->sd, sctp_assoc_changed->sac_assoc_id);

                new_cnx->sd         = ns;
                new_cnx->task_id    = sctp_cnx->task_id;
                new_cnx->cnx_id     = 0;
                new_cnx->ppid       = sctp_cnx->ppid;
                new_cnx->instance   = sctp_cnx->instance;
                new_cnx->local_port = sctp_cnx->local_port;
                new_cnx->assoc_id   = sctp_assoc_changed->sac_assoc_id;

                if (sctp_get_sockinfo(ns, &new_cnx->in_streams, &new_cnx->out_streams,
                                      &new_cnx->assoc_id) != 0) {
                    SCTP_ERROR("sctp_get_sockinfo failed\n");
                    close(ns);
                    free(new_cnx);
                    return;
                }

                /* Insert new element at end of list */
                STAILQ_INSERT_TAIL(&sctp_cnx_list, new_cnx, entries);
                sctp_nb_cnx++;

                /* Add the socket to list of fd monitored by ITTI */
                itti_subscribe_event_fd(TASK_SCTP, ns);

                sctp_itti_send_association_ind(new_cnx->task_id, new_cnx->instance,
                                               new_cnx->assoc_id, new_cnx->local_port,
                                               new_cnx->out_streams, new_cnx->in_streams);
            }
            break;

            default:
                break;
            }
        }
    } else {
        SCTP_DEBUG("No notification from SCTP\n");
    }
}

//------------------------------------------------------------------------------
static void
sctp_handle_new_association_req_multi(
    const instance_t instance,
    const task_id_t requestor,
    const sctp_new_association_req_multi_t * const sctp_new_association_req_p)
{
    int                           ns;
    int sd;

    sctp_assoc_t assoc_id = 0;

    struct sctp_cnx_list_elm_s   *sctp_cnx = NULL;
    enum sctp_connection_type_e   connection_type = SCTP_TYPE_CLIENT;

    /* Prepare a new SCTP association as requested by upper layer and try to connect
     * to remote host.
     */
    DevAssert(sctp_new_association_req_p != NULL);

    sd = sctp_new_association_req_p->multi_sd;

    /* Create new socket with IPv6 affinity */
//#warning "SCTP may Force IPv4 only, here"

    /* Mark the socket as non-blocking */
    //if (fcntl(sd, F_SETFL, O_NONBLOCK) < 0) {
    //SCTP_ERROR("fcntl F_SETFL O_NONBLOCK failed: %s\n",
    //         strerror(errno));
    //close(sd);
    //return;
    //}

    /* SOCK_STREAM socket type requires an explicit connect to the remote host
     * address and port.
     * Only use IPv4 for the first connection attempt
     */
    if ((sctp_new_association_req_p->remote_address.ipv6 != 0) ||
            (sctp_new_association_req_p->remote_address.ipv4 != 0)) {
        uint8_t address_index = 0;
        uint8_t used_address  = sctp_new_association_req_p->remote_address.ipv6 +
                                sctp_new_association_req_p->remote_address.ipv4;
        struct sockaddr_in addr[used_address];

        memset(addr, 0, used_address * sizeof(struct sockaddr_in));

        if (sctp_new_association_req_p->remote_address.ipv6 == 1) {
            if (inet_pton(AF_INET6, sctp_new_association_req_p->remote_address.ipv6_address,
                          &addr[address_index].sin_addr.s_addr) != 1) {
                SCTP_ERROR("Failed to convert ipv6 address %*s to network type\n",
                           (int)strlen(sctp_new_association_req_p->remote_address.ipv6_address),
                           sctp_new_association_req_p->remote_address.ipv6_address);
                //close(sd);
                //return;
                exit_fun("sctp_handle_new_association_req_multi fatal: inet_pton error");
            }

            SCTP_DEBUG("Converted ipv6 address %*s to network type\n",
                       (int)strlen(sctp_new_association_req_p->remote_address.ipv6_address),
                       sctp_new_association_req_p->remote_address.ipv6_address);

            addr[address_index].sin_family = AF_INET6;
            addr[address_index].sin_port   = htons(sctp_new_association_req_p->port);
            address_index++;
        }

        if (sctp_new_association_req_p->remote_address.ipv4 == 1) {
            if (inet_pton(AF_INET, sctp_new_association_req_p->remote_address.ipv4_address,
                          &addr[address_index].sin_addr.s_addr) != 1) {
                SCTP_ERROR("Failed to convert ipv4 address %*s to network type\n",
                           (int)strlen(sctp_new_association_req_p->remote_address.ipv4_address),
                           sctp_new_association_req_p->remote_address.ipv4_address);
                //close(sd);
                //return;
                exit_fun("sctp_handle_new_association_req_multi fatal: inet_pton error");
            }

            SCTP_DEBUG("Converted ipv4 address %*s to network type\n",
                       (int)strlen(sctp_new_association_req_p->remote_address.ipv4_address),
                       sctp_new_association_req_p->remote_address.ipv4_address);

            addr[address_index].sin_family = AF_INET;
            addr[address_index].sin_port   = htons(sctp_new_association_req_p->port);
            address_index++;
        }

        /* Connect to remote host and port */
        if (sctp_connectx(sd, (struct sockaddr *)addr, 1, &assoc_id) < 0) {
            /* sctp_connectx on non-blocking socket return EINPROGRESS */
            if (errno != EINPROGRESS) {
                SCTP_ERROR("Connect failed: %s\n", strerror(errno));
                sctp_itti_send_association_resp(
                    requestor, instance, -1, sctp_new_association_req_p->ulp_cnx_id,
                    SCTP_STATE_UNREACHABLE, 0, 0);
                /* Add the socket to list of fd monitored by ITTI */
                //itti_unsubscribe_event_fd(TASK_SCTP, sd);
                //close(sd);
                return;
            } else {
                SCTP_DEBUG("connectx assoc_id  %d in progress..., used %d addresses\n",
                           assoc_id, used_address);
            }
        } else {
            SCTP_DEBUG("sctp_connectx SUCCESS, socket %d used %d addresses assoc_id %d\n",
		       sd,
                       used_address,
                       assoc_id);
        }
    }

    ns = sctp_peeloff(sd, assoc_id);
    if (ns == -1) {
      perror("sctp_peeloff");
      printf("sctp_peeloff: sd=%d assoc_id=%d\n", sd, assoc_id);
      exit_fun("sctp_handle_new_association_req_multi fatal: sctp_peeloff error");
    }

    sctp_cnx = calloc(1, sizeof(*sctp_cnx));

    sctp_cnx->connection_type = connection_type;

    sctp_cnx->sd       = ns;
    sctp_cnx->task_id  = requestor;
    sctp_cnx->cnx_id   = sctp_new_association_req_p->ulp_cnx_id;
    sctp_cnx->ppid     = sctp_new_association_req_p->ppid;
    sctp_cnx->instance = instance;
    sctp_cnx->assoc_id = assoc_id;

    /* Add the socket to list of fd monitored by ITTI */
    itti_subscribe_event_fd(TASK_SCTP, ns);

    /* Insert new element at end of list */
    STAILQ_INSERT_TAIL(&sctp_cnx_list, sctp_cnx, entries);
    sctp_nb_cnx++;

    SCTP_DEBUG("Inserted new descriptor for sd %d in list, nb elements %u, assoc_id %d\n",
               ns, sctp_nb_cnx, assoc_id);
}

static const char *print_ip(struct addrinfo *p, char *buf, size_t buf_len)
{
  void *addr;
  if (p->ai_family == AF_INET) {
    struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
    addr = &ipv4->sin_addr;
  } else {
    struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
    addr = &ipv6->sin6_addr;
  }

  // convert the IP to a string and print it:
  return inet_ntop(p->ai_family, addr, buf, buf_len);
}

static void sctp_handle_new_association_req(const instance_t instance,
                                            const task_id_t requestor,
                                            const sctp_new_association_req_t *const req)
{
  enum sctp_connection_type_e connection_type = SCTP_TYPE_CLIENT;

  /* local address: IPv4 has priority, but can also handle IPv6 */
  const char *local = NULL;
  if (req->local_address.ipv6) {
    local = req->local_address.ipv6_address;
    SCTP_WARN("please specify IPv6 addresses in the IPv4 field, IPv4 handles both\n");
  }
  if (req->local_address.ipv4)
    local = req->local_address.ipv4_address;

  /* Prepare a new SCTP association as requested by upper layer and try to connect
   * to remote host.
   */
  DevAssert(req != NULL);
  struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_SCTP};
  if (local == NULL)
    hints.ai_flags = AI_PASSIVE;
  struct addrinfo *serv;

  int status = getaddrinfo(local, NULL, &hints, &serv);
  AssertFatal(status == 0, "getaddrinfo() failed: %s\n", gai_strerror(status));

  int sd;
  struct addrinfo *p = NULL;
  for (p = serv; p != NULL; p = p->ai_next) {
    char buf[512];
    const char *ip = print_ip(p, buf, sizeof(buf));
    SCTP_DEBUG("Trying %s for client socket creation\n", ip);

    if ((sd = socket(serv->ai_family, serv->ai_socktype, serv->ai_protocol)) == -1) {
      SCTP_WARN("Socket creation failed: %s\n", strerror(errno));
      continue;
    }

    /* we assume we can set options, or something is likely broken; the
     * connection won't operate properly */
    int ret = sctp_set_init_opt(sd, req->in_streams, req->out_streams, SCTP_MAX_ATTEMPTS, SCTP_TIMEOUT);
    AssertFatal(ret == 0, "sctp_set_init_opt() failed\n");

    /* Subscribe to all events */
    struct sctp_event_subscribe events = {0};
    events.sctp_data_io_event = 1;
    events.sctp_association_event = 1;
    events.sctp_address_event = 1;
    events.sctp_send_failure_event = 1;
    events.sctp_peer_error_event = 1;
    events.sctp_shutdown_event = 1;
    events.sctp_partial_delivery_event = 1;

    /* as above */
    ret = setsockopt(sd, serv->ai_protocol, SCTP_EVENTS, &events, 8);
    AssertFatal(ret == 0, "setsockopt() IPPROTO_SCTP_EVENTS failed: %s\n", strerror(errno));

    /* if that fails, we will try the next address */
    ret = sctp_bindx(sd, p->ai_addr, 1, SCTP_BINDX_ADD_ADDR);
    if (ret != 0) {
      SCTP_WARN("sctp_bindx() SCTP_BINDX_ADD_ADDR failed: errno %d %s\n", errno, strerror(errno));
      close(sd);
      continue;
    }

    SCTP_DEBUG("sctp_bindx() SCTP_BINDX_ADD_ADDR: socket bound to %s/%s\n", local, ip);
    break;
  }

  freeaddrinfo(serv);

  if (p == NULL) {
    SCTP_ERROR("could not open socket, no SCTP connection established\n");
    return;
  }

  /* SOCK_STREAM socket type requires an explicit connect to the remote host
   * address and port. */
  /* remote address: IPv4 has priority, but can also handle IPv6 */
  const char *remote = NULL;
  if (req->remote_address.ipv6) {
    remote = req->remote_address.ipv6_address;
    SCTP_WARN("please specify IPv6 addresses in the IPv4 field, IPv4 handles both\n");
  }
  if (req->remote_address.ipv4)
    remote = req->remote_address.ipv4_address;
  sctp_assoc_t assoc_id = 0;
  if (remote != NULL) {
    struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_SCTP};
    struct addrinfo *serv;

    char port[12];
    snprintf(port, sizeof(port), "%d", req->port);
    int status = getaddrinfo(remote, port, &hints, &serv);
    AssertFatal(status == 0, "getaddrinfo() failed: %s\n", gai_strerror(status));

    struct addrinfo *p = NULL;
    for (p = serv; p != NULL; p = p->ai_next) {
      char buf[512];
      const char *ip = print_ip(p, buf, sizeof(buf));
      SCTP_DEBUG("Trying to connect to %s for remote end %s\n", ip, remote);

      if (sctp_connectx(sd, p->ai_addr, 1, &assoc_id) < 0) {
        /* sctp_connectx on non-blocking socket return EINPROGRESS */
        if (errno != EINPROGRESS) {
          SCTP_ERROR("Connect failed: %s\n", strerror(errno));
          sctp_itti_send_association_resp(requestor, instance, -1, req->ulp_cnx_id, SCTP_STATE_UNREACHABLE, 0, 0);
          close(sd);
          return;
        } else {
          SCTP_DEBUG("sctp_connectx(): assoc_id %d in progress...\n", assoc_id);
        }
      } else {
        SCTP_DEBUG("sctp_connectx() SUCCESS: used assoc_id %d\n", assoc_id);
      }
      break;
    }

    freeaddrinfo(serv);
  } else {
    /* I am not sure that this is relevant; we already did sctp_bindx() above */
    connection_type = SCTP_TYPE_SERVER;

    /* No remote address provided -> only bind the socket for now.
     * Connection will be accepted in the main event loop
     */
    /*
    struct sockaddr_in6 addr6;


    addr6.sin6_family = AF_INET6;
    addr6.sin6_addr = in6addr_any;
    addr6.sin6_port = htons(req->port);
    addr6.sin6_flowinfo = 0;

    if (bind(sd, (struct sockaddr *)&addr6, sizeof(addr6)) < 0) {
      SCTP_ERROR("Failed to bind the socket to address any (v4/v6): %s\n", strerror(errno));
      close(sd);
      return;
    }
    */
  }

  struct sctp_cnx_list_elm_s *sctp_cnx = calloc(1, sizeof(*sctp_cnx));
  AssertFatal(sctp_cnx != NULL, "out of memory\n");
  sctp_cnx->connection_type = connection_type;

  sctp_cnx->sd = sd;
  sctp_cnx->task_id = requestor;
  sctp_cnx->cnx_id = req->ulp_cnx_id;
  sctp_cnx->ppid = req->ppid;
  sctp_cnx->instance = instance;
  sctp_cnx->assoc_id = assoc_id;

  /* Insert new element at end of list */
  STAILQ_INSERT_TAIL(&sctp_cnx_list, sctp_cnx, entries);
  sctp_nb_cnx++;

  /* Add the socket to list of fd monitored by ITTI */
  itti_subscribe_event_fd(TASK_SCTP, sd);

  SCTP_DEBUG("Inserted new descriptor for sd %d in list, nb elements %u, assoc_id %d\n", sd, sctp_nb_cnx, assoc_id);
}

//------------------------------------------------------------------------------
static void sctp_send_data(sctp_data_req_t *sctp_data_req_p)
{
    struct sctp_cnx_list_elm_s *sctp_cnx = NULL;

    DevAssert(sctp_data_req_p != NULL);
    DevAssert(sctp_data_req_p->buffer != NULL);
    DevAssert(sctp_data_req_p->buffer_length > 0);

    sctp_cnx = sctp_get_cnx(sctp_data_req_p->assoc_id, 0);

    if (sctp_cnx == NULL) {
        SCTP_ERROR("Failed to find SCTP description for assoc_id %d\n",
                   sctp_data_req_p->assoc_id);
        /* TODO: notify upper layer */
        return;
    }

    if (sctp_data_req_p->stream >= sctp_cnx->out_streams) {
        SCTP_ERROR("Requested stream (%"PRIu16") >= nb out streams (%"PRIu16")\n",
                   sctp_data_req_p->stream, sctp_cnx->out_streams);
        return;
    }

    /* Send message on specified stream of the sd association
     * NOTE: PPID should be defined in network order
     */
    if (sctp_sendmsg(sctp_cnx->sd, sctp_data_req_p->buffer,
                     sctp_data_req_p->buffer_length, NULL, 0,
                     htonl(sctp_cnx->ppid), 0, sctp_data_req_p->stream, 0, 0) < 0) {
        SCTP_ERROR("Sctp_sendmsg failed: %s\n", strerror(errno));
        /* TODO: notify upper layer */
        return;
    }
    free(sctp_data_req_p->buffer); // assuming it has been malloced
    SCTP_DEBUG("Successfully sent %u bytes on stream %d for assoc_id %d\n",
               sctp_data_req_p->buffer_length, sctp_data_req_p->stream,
               sctp_cnx->assoc_id);
}

//------------------------------------------------------------------------------
static int sctp_close_association(sctp_close_association_t *close_association_p)
{

    struct sctp_cnx_list_elm_s *sctp_cnx = NULL;

    DevAssert(close_association_p != NULL);
    sctp_cnx = sctp_get_cnx(close_association_p->assoc_id, 0);

    if (sctp_cnx == NULL) {
        SCTP_ERROR("Failed to find SCTP description for assoc_id %d\n",
                   close_association_p->assoc_id);
        /* TODO: notify upper layer */
        return -1;
    } else {
        close(sctp_cnx->sd);
        STAILQ_REMOVE(&sctp_cnx_list, sctp_cnx, sctp_cnx_list_elm_s, entries);
        SCTP_DEBUG("Removed assoc_id %d (closed socket %u)\n",
                   sctp_cnx->assoc_id, (unsigned int)sctp_cnx->sd);
    }

    return 0;
}

static int sctp_create_new_listener(const instance_t instance, const task_id_t requestor, sctp_init_t *init_p, int server_type)
{
  DevAssert(init_p != NULL);
  DevAssert(init_p->bind_address != NULL);
  int in_streams = 1;
  int out_streams = 1;

  /* local address: IPv4 has priority, but can also handle IPv6 */
  const char *local = init_p->bind_address;

  struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_SCTP};
  struct addrinfo *serv;

  char port[12];
  snprintf(port, sizeof(port), "%d", init_p->port);
  int status = getaddrinfo(local, port, &hints, &serv);
  AssertFatal(status == 0, "getaddrinfo() failed: %s\n", gai_strerror(status));

  int sd;
  struct addrinfo *p = NULL;
  for (p = serv; p != NULL; p = p->ai_next) {
    char buf[512];
    const char *ip = print_ip(p, buf, sizeof(buf));
    SCTP_DEBUG("Trying %s for server socket creation\n", ip);

    /* SOCK_SEQPACKET to be able to reuse(?) socket through SCTP_INIT_MSG_MULTI_REQ */
    int socktype = server_type ? SOCK_SEQPACKET : SOCK_STREAM;
    if ((sd = socket(serv->ai_family, socktype, serv->ai_protocol)) == -1) {
      SCTP_WARN("Socket creation failed: %s\n", strerror(errno));
      continue;
    }

    /* we assume we can set options, or something is likely broken; the
     * connection won't operate properly */
    int ret = sctp_set_init_opt(sd, in_streams, out_streams, 0, 0);
    AssertFatal(ret == 0, "sctp_set_init_opt() failed\n");

    struct sctp_event_subscribe event = {0};
    event.sctp_data_io_event = 1;
    event.sctp_association_event = 1;
    event.sctp_address_event = 1;
    event.sctp_send_failure_event = 1;
    event.sctp_peer_error_event = 1;
    event.sctp_shutdown_event = 1;
    event.sctp_partial_delivery_event = 1;

    /* as above */
    ret = setsockopt(sd, serv->ai_protocol, SCTP_EVENTS, &event, 8);
    AssertFatal(ret == 0, "setsockopt() IPPROTO_SCTP_EVENTS failed: %s\n", strerror(errno));

    /* if that fails, we will try the next address */
    ret = sctp_bindx(sd, p->ai_addr, 1, SCTP_BINDX_ADD_ADDR);
    if (ret != 0) {
      SCTP_WARN("sctp_bindx() SCTP_BINDX_ADD_ADDR failed: errno %d %s\n", errno, strerror(errno));
      close(sd);
      continue;
    }

    if (listen(sd, 5) < 0) {
      SCTP_WARN("listen() failed: %s:%d\n", strerror(errno), errno);
      close(sd);
      return -1;
    }

    SCTP_DEBUG("Created listen socket %d on %s:%s local %s\n", sd, ip, port, local);

    break;
  }

  freeaddrinfo(serv);

  if (p == NULL) {
    SCTP_ERROR("could not open server socket, no SCTP listener active\n");
    return -1;
  }

  struct sctp_cnx_list_elm_s *sctp_cnx = calloc(1, sizeof(*sctp_cnx));
  AssertFatal(sctp_cnx != NULL, "out of memory\n");

  sctp_cnx->connection_type = server_type ? SCTP_TYPE_MULTI_SERVER : SCTP_TYPE_SERVER;
  sctp_cnx->sd = sd;
  sctp_cnx->local_port = init_p->port;
  sctp_cnx->in_streams = in_streams;
  sctp_cnx->out_streams = out_streams;
  sctp_cnx->ppid = init_p->ppid;
  sctp_cnx->task_id = requestor;
  sctp_cnx->instance = instance;
  /* Insert new element at end of list */
  STAILQ_INSERT_TAIL(&sctp_cnx_list, sctp_cnx, entries);
  sctp_nb_cnx++;

  /* Add the socket to list of fd monitored by ITTI */
  itti_subscribe_event_fd(TASK_SCTP, sd);

  return sd;
}

//------------------------------------------------------------------------------
static inline
void
sctp_eNB_accept_associations(
    struct sctp_cnx_list_elm_s *sctp_cnx)
{
    int             client_sd;
    struct sockaddr_in6 saddr;
    socklen_t       saddr_size;

    DevAssert(sctp_cnx != NULL);

    saddr_size = sizeof(saddr);

    /* There is a new client connecting. Accept it...
     */
    if ((client_sd = accept(sctp_cnx->sd, (struct sockaddr*)&saddr, &saddr_size)) < 0) {
        SCTP_ERROR("[%d] accept failed: %s:%d\n", sctp_cnx->sd, strerror(errno), errno);
    } else {
        struct sctp_cnx_list_elm_s *new_cnx;
        uint16_t port;

        /* This is an ipv6 socket */
        port = saddr.sin6_port;

        /* Contrary to BSD, client socket does not inherit O_NONBLOCK option */
        if (fcntl(client_sd, F_SETFL, O_NONBLOCK) < 0) {
            SCTP_ERROR("fcntl F_SETFL O_NONBLOCK failed: %s\n",
                       strerror(errno));
            close(client_sd);
            return;
        }

        new_cnx = calloc(1, sizeof(*sctp_cnx));

        DevAssert(new_cnx != NULL);

        new_cnx->connection_type = SCTP_TYPE_CLIENT;

        new_cnx->sd         = client_sd;
        new_cnx->task_id    = sctp_cnx->task_id;
        new_cnx->cnx_id     = 0;
        new_cnx->ppid       = sctp_cnx->ppid;
        new_cnx->instance   = sctp_cnx->instance;
        new_cnx->local_port = sctp_cnx->local_port;

        if (sctp_get_sockinfo(client_sd, &new_cnx->in_streams, &new_cnx->out_streams,
                              &new_cnx->assoc_id) != 0) {
            SCTP_ERROR("sctp_get_sockinfo failed\n");
            close(client_sd);
            free(new_cnx);
            return;
        }

        /* Insert new element at end of list */
        STAILQ_INSERT_TAIL(&sctp_cnx_list, new_cnx, entries);
        sctp_nb_cnx++;

        /* Add the socket to list of fd monitored by ITTI */
        itti_subscribe_event_fd(TASK_SCTP, client_sd);

        sctp_itti_send_association_ind(new_cnx->task_id, new_cnx->instance,
                                       new_cnx->assoc_id, port,
                                       new_cnx->out_streams, new_cnx->in_streams);
    }
}

//------------------------------------------------------------------------------
static inline
void
sctp_eNB_read_from_socket(
    struct sctp_cnx_list_elm_s *sctp_cnx)
{
    DevAssert(sctp_cnx != NULL);

    int    flags = 0;
    struct sctp_sndrcvinfo sinfo={0};
    uint8_t buffer[SCTP_RECV_BUFFER_SIZE];

    int n = sctp_recvmsg(sctp_cnx->sd, (void *)buffer, SCTP_RECV_BUFFER_SIZE,
                    NULL, NULL, 
                     &sinfo, &flags);

    if (n < 0) {
        if( (errno == ENOTCONN) || (errno == ECONNRESET) || (errno == ETIMEDOUT) || (errno == ECONNREFUSED) )
        {
            itti_unsubscribe_event_fd(TASK_SCTP, sctp_cnx->sd);

            SCTP_DEBUG("Received not connected for sd %d\n", sctp_cnx->sd);
            SCTP_ERROR("sctp_recvmsg (fd %d, len %d ): %s:%d\n", sctp_cnx->sd, n, strerror(errno), errno);

            sctp_itti_send_association_resp(
                sctp_cnx->task_id, sctp_cnx->instance, -1,
                sctp_cnx->cnx_id, SCTP_STATE_UNREACHABLE, 0, 0);

            close(sctp_cnx->sd);
            STAILQ_REMOVE(&sctp_cnx_list, sctp_cnx, sctp_cnx_list_elm_s, entries);
            sctp_nb_cnx--;
            free(sctp_cnx);
        } else {
            SCTP_DEBUG("An error occured during read\n");
            SCTP_ERROR("sctp_recvmsg (fd %d, len %d ): %s:%d\n", sctp_cnx->sd, n, strerror(errno), errno);
        }

        return;
    } else if (n == 0) {
        SCTP_DEBUG("return of sctp_recvmsg is 0...\n");
        return;
    }

    if (!(flags & MSG_EOR)) {
      SCTP_ERROR("fatal: partial SCTP messages are not handled\n");
      exit_fun("fatal: partial SCTP messages are not handled" );
    }

    if (flags & MSG_NOTIFICATION) {
        union sctp_notification *snp;
        snp = (union sctp_notification *)buffer;

        SCTP_DEBUG("Received notification for sd %d, type %u\n",
                   sctp_cnx->sd, snp->sn_header.sn_type);

        /* Client deconnection */
        if (SCTP_SHUTDOWN_EVENT == snp->sn_header.sn_type) {
            itti_unsubscribe_event_fd(TASK_SCTP, sctp_cnx->sd);

            SCTP_WARN("Received SCTP SHUTDOWN EVENT\n");
            close(sctp_cnx->sd);

            sctp_itti_send_association_resp(
                sctp_cnx->task_id, sctp_cnx->instance, sctp_cnx->assoc_id,
                sctp_cnx->cnx_id, SCTP_STATE_SHUTDOWN,
                0, 0);

            STAILQ_REMOVE(&sctp_cnx_list, sctp_cnx, sctp_cnx_list_elm_s, entries);
            sctp_nb_cnx--;

            free(sctp_cnx);
        }
        /* Association has changed. */
        else if (SCTP_ASSOC_CHANGE == snp->sn_header.sn_type) {
            struct sctp_assoc_change *sctp_assoc_changed;
            sctp_assoc_changed = &snp->sn_assoc_change;

            SCTP_DEBUG("Client association changed: %d\n", sctp_assoc_changed->sac_state);

            /* New physical association requested by a peer */
            switch (sctp_assoc_changed->sac_state) {
            case SCTP_COMM_UP: {
                if (sctp_get_peeraddresses(sctp_cnx->sd, NULL, NULL) != 0) {
                    /* TODO Failure -> notify upper layer */
                } else {
                    sctp_get_sockinfo(sctp_cnx->sd, &sctp_cnx->in_streams,
                                      &sctp_cnx->out_streams, &sctp_cnx->assoc_id);
                }

                SCTP_DEBUG("Comm up notified for sd %d, assigned assoc_id %d\n",
                           sctp_cnx->sd, sctp_cnx->assoc_id);

                sctp_itti_send_association_resp(
                    sctp_cnx->task_id, sctp_cnx->instance, sctp_cnx->assoc_id,
                    sctp_cnx->cnx_id, SCTP_STATE_ESTABLISHED,
                    sctp_cnx->out_streams, sctp_cnx->in_streams);

            }
            break;

            default:
                if ( sctp_assoc_changed->sac_state == SCTP_SHUTDOWN_COMP) 
                    SCTP_WARN("SCTP_ASSOC_CHANGE to SSCTP_SHUTDOWN_COMP\n");
                if ( sctp_assoc_changed->sac_state == SCTP_RESTART) 
                    SCTP_WARN("SCTP_ASSOC_CHANGE to SCTP_RESTART\n");
                if ( sctp_assoc_changed->sac_state == SCTP_CANT_STR_ASSOC) 
                    SCTP_ERROR("SCTP_ASSOC_CHANGE to SCTP_CANT_STR_ASSOC\n");
                if ( sctp_assoc_changed->sac_state == SCTP_COMM_LOST) 
                    SCTP_ERROR("SCTP_ASSOC_CHANGE to SCTP_COMM_LOST\n");
                break;
            }
        }
    } else {
        sctp_cnx->messages_recv++;

        if (ntohl(sinfo.sinfo_ppid) != sctp_cnx->ppid) {
            /* Mismatch in Payload Protocol Identifier,
             * may be we received unsollicited traffic from stack other than S1AP.
             */
            SCTP_ERROR("Received data from peer with unsollicited PPID %d, expecting %d\n",
                       ntohl(sinfo.sinfo_ppid),
                       sctp_cnx->ppid);
        }

        SCTP_DEBUG("[%d][%d] Msg of length %d received, on stream %d, PPID %d\n",
                   sinfo.sinfo_assoc_id, sctp_cnx->sd, n, 
                   sinfo.sinfo_stream, ntohl(sinfo.sinfo_ppid));

        sctp_itti_send_new_message_ind(sctp_cnx->task_id,
                                       sctp_cnx->instance,
                                       sinfo.sinfo_assoc_id,
                                       buffer,
                                       n,
                                       sinfo.sinfo_stream);
    }
}

//------------------------------------------------------------------------------
void
static sctp_eNB_flush_sockets(
    struct epoll_event *events, int nb_events)
{
    int i;
    struct sctp_cnx_list_elm_s *sctp_cnx = NULL;

    for (i = 0; i < nb_events; i++) {
        sctp_cnx = sctp_get_cnx(-1, events[i].data.fd);

        if (sctp_cnx == NULL) {
            continue;
        }

        SCTP_DEBUG("Found data for descriptor %d\n", events[i].data.fd);

        if (sctp_cnx->connection_type == SCTP_TYPE_CLIENT) {
            sctp_eNB_read_from_socket(sctp_cnx);
        }
        else if (sctp_cnx->connection_type == SCTP_TYPE_MULTI_SERVER) {
            sctp_eNB_accept_associations_multi(sctp_cnx);
        }
        else {
            sctp_eNB_accept_associations(sctp_cnx);
        }
    }
}

//------------------------------------------------------------------------------
static void sctp_eNB_init(void)
{
    SCTP_DEBUG("Starting SCTP layer\n");

    STAILQ_INIT(&sctp_cnx_list);

    itti_mark_task_ready(TASK_SCTP);
}

//------------------------------------------------------------------------------
static void sctp_eNB_process_itti_msg()
{
    int                 nb_events;
    MessageDef         *received_msg = NULL;
    int                 result;

    itti_receive_msg(TASK_SCTP, &received_msg);

    /* Check if there is a packet to handle */
    if (received_msg != NULL) {
      LOG_D(SCTP,"Received message %d:%s\n",
		 ITTI_MSG_ID(received_msg), ITTI_MSG_NAME(received_msg));
        switch (ITTI_MSG_ID(received_msg)) {
        case SCTP_INIT_MSG: {
            if (sctp_create_new_listener(
                        ITTI_MSG_DESTINATION_INSTANCE(received_msg),
                        ITTI_MSG_ORIGIN_ID(received_msg),
                        &received_msg->ittiMsg.sctp_init,0) < 0) {
                /* SCTP socket creation or bind failed... */
                SCTP_ERROR("Failed to create new SCTP listener\n");
            }
        }
        break;

        case SCTP_INIT_MSG_MULTI_REQ: {
           int multi_sd = sctp_create_new_listener(
                           ITTI_MSG_DESTINATION_INSTANCE(received_msg),
                           ITTI_MSG_ORIGIN_ID(received_msg),
                           &received_msg->ittiMsg.sctp_init_multi,1);
            /* We received a new connection request */
            if (multi_sd < 0) {
                /* SCTP socket creation or bind failed... */
                SCTP_ERROR("Failed to create new SCTP listener\n");
            }
            sctp_itti_send_init_msg_multi_cnf(
                ITTI_MSG_ORIGIN_ID(received_msg),
                ITTI_MSG_DESTINATION_INSTANCE(received_msg),
                multi_sd);
        }
        break;

        case SCTP_NEW_ASSOCIATION_REQ: {
            sctp_handle_new_association_req(ITTI_MSG_DESTINATION_INSTANCE(received_msg),
                                            ITTI_MSG_ORIGIN_ID(received_msg),
                                            &received_msg->ittiMsg.sctp_new_association_req);
        }
        break;

        case SCTP_NEW_ASSOCIATION_REQ_MULTI: {
            sctp_handle_new_association_req_multi(ITTI_MSG_DESTINATION_INSTANCE(received_msg),
                                                  ITTI_MSG_ORIGIN_ID(received_msg),
                                                  &received_msg->ittiMsg.sctp_new_association_req_multi);
        }
        break;

        case SCTP_CLOSE_ASSOCIATION:
          sctp_close_association(&received_msg->ittiMsg.sctp_close_association);
          break;

        case TERMINATE_MESSAGE:
            SCTP_WARN("*** Exiting SCTP thread\n");
            itti_exit_task();
            break;

        case SCTP_DATA_REQ: {
          sctp_send_data(&received_msg->ittiMsg.sctp_data_req);
        }
        break;

        default:
            SCTP_ERROR("Received unhandled message %d:%s\n",
                       ITTI_MSG_ID(received_msg), ITTI_MSG_NAME(received_msg));
            break;
        }

        result = itti_free(ITTI_MSG_ORIGIN_ID(received_msg), received_msg);
        AssertFatal (result == EXIT_SUCCESS, "Failed to free memory (%d)!\n", result);
        received_msg = NULL;
    }
    struct epoll_event events[20];
    nb_events = itti_get_events(TASK_SCTP, events, 20);
    /* Now handle notifications for other sockets */
    sctp_eNB_flush_sockets(events, nb_events);

    return;
}

//------------------------------------------------------------------------------
void *sctp_eNB_task(void *arg)
{
    sctp_eNB_init();

    while (1) {
        sctp_eNB_process_itti_msg();
    }

    return NULL;
}
