/*******************************************************************************
    OpenAirInterface
    Copyright(c) 1999 - 2014 Eurecom

    OpenAirInterface is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.


    OpenAirInterface is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with OpenAirInterface.The full GNU General Public License is
   included in this distribution in the file called "COPYING". If not,
   see <http://www.gnu.org/licenses/>.

  Contact Information
  OpenAirInterface Admin: openair_admin@eurecom.fr
  OpenAirInterface Tech : openair_tech@eurecom.fr
  OpenAirInterface Dev  : openair4g-devel@eurecom.fr

  Address      : Eurecom, Compus SophiaTech 450, route des chappes, 06451 Biot, France.

 *******************************************************************************/
#if HAVE_CONFIG_H
# include "config.h"
#endif

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#if !defined(MME_CLIENT_TEST)
# include "intertask_interface.h"
#endif

#include "assertions.h"
#include "queue.h"

#include "s1ap_mme.h"
#include "s1ap_mme_decoder.h"
#include "s1ap_mme_handlers.h"
#include "s1ap_ies_defs.h"

#include "s1ap_mme_nas_procedures.h"
#include "s1ap_mme_retransmission.h"

#define S1AP_DEBUG_LIST
#ifdef S1AP_DEBUG_LIST
# define eNB_LIST_OUT(x, args...) S1AP_DEBUG("[eNB]%*s"x"\n", 4*indent, "", ##args)
# define UE_LIST_OUT(x, args...)  S1AP_DEBUG("[UE] %*s"x"\n", 4*indent, "", ##args)
#else
# define eNB_LIST_OUT(x, args...)
# define UE_LIST_OUT(x, args...)
#endif

#if !defined(MME_CLIENT_TEST)
// static pthread_t s1ap_task_thread;

int         hss_associated = 0;
uint32_t    nb_eNB_associated = 0;
STAILQ_HEAD(eNB_list_s, eNB_description_s)
            eNB_list_head;

static int  indent = 0;

void *s1ap_mme_thread(void *args);

static int s1ap_send_init_sctp(void) {
    // Create and alloc new message
    MessageDef *message_p;
    message_p = itti_alloc_new_message(TASK_S1AP, SCTP_INIT_MSG);
    message_p->ittiMsg.sctpInit.port = S1AP_PORT_NUMBER;
    message_p->ittiMsg.sctpInit.ppid = S1AP_SCTP_PPID;
    message_p->ittiMsg.sctpInit.ipv4 = 1;
    message_p->ittiMsg.sctpInit.ipv6 = 0;
    message_p->ittiMsg.sctpInit.nb_ipv4_addr = 1;
    message_p->ittiMsg.sctpInit.ipv4_address[0]
    = mme_config.ipv4.mme_ip_address_for_S1_MME;
    /* SR WARNING: ipv6 multi-homing fails sometimes for localhost.
     * Disable it for now.*/
    message_p->ittiMsg.sctpInit.nb_ipv6_addr = 0;
    message_p->ittiMsg.sctpInit.ipv6_address[0] = "0:0:0:0:0:0:0:1";

    return itti_send_msg_to_task(TASK_SCTP, INSTANCE_DEFAULT, message_p);
}

void *s1ap_mme_thread(void *args)
{
    MessageDef *received_message_p;

    itti_mark_task_ready(TASK_S1AP);

    while(1) {
        /* Trying to fetch a message from the message queue.
         * If the queue is empty, this function will block till a
         * message is sent to the task.
         */
        itti_receive_msg(TASK_S1AP, &received_message_p);
        DevAssert(received_message_p != NULL);

        switch (ITTI_MSG_ID(received_message_p))
        {
            case ACTIVATE_MESSAGE: {
                hss_associated = 1;
            } break;

            case SCTP_DATA_IND: {
                /* New message received from SCTP layer.
                 * Decode and handle it.
                 */
                s1ap_message message;

                memset((void *)&message, 0, sizeof(s1ap_message));

                /* Invoke S1AP message decoder */
                if (s1ap_mme_decode_pdu(&message,
                                        SCTP_DATA_IND(received_message_p).buffer,
                                        SCTP_DATA_IND(received_message_p).buf_length) < 0)
                {
                    // TODO: Notify eNB of failure with right cause
                    S1AP_ERROR("Failed to decode new buffer\n");
                } else {
                    s1ap_mme_handle_message(SCTP_DATA_IND(received_message_p).assoc_id,
                                            SCTP_DATA_IND(received_message_p).stream,
                                            &message);
                }
                /* Free received PDU array */
                free(SCTP_DATA_IND(received_message_p).buffer);
            } break;

            /* SCTP layer notifies S1AP of disconnection of a peer. */
            case SCTP_CLOSE_ASSOCIATION: {
                s1ap_handle_sctp_deconnection(SCTP_CLOSE_ASSOCIATION(received_message_p).assoc_id);
            } break;

            case SCTP_NEW_ASSOCIATION: {
                s1ap_handle_new_association(&received_message_p->ittiMsg.sctp_new_peer);
            } break;

            case NAS_DOWNLINK_DATA_REQ: {
                /* New message received from NAS task.
                 * This corresponds to a S1AP downlink nas transport message.
                 */
                s1ap_generate_downlink_nas_transport(NAS_DL_DATA_REQ(received_message_p).UEid,
                                                     NAS_DL_DATA_REQ(received_message_p).nasMsg.data,
                                                     NAS_DL_DATA_REQ(received_message_p).nasMsg.length);
            } break;

#if defined(DISABLE_USE_NAS)
            case NAS_ATTACH_ACCEPT: {
                s1ap_handle_attach_accepted(&received_message_p->ittiMsg.nas_attach_accept);
            } break;
#else
            // handled by MME_APP know
            //case NAS_CONNECTION_ESTABLISHMENT_CNF: {
            //    s1ap_handle_conn_est_cnf(&NAS_CONNECTION_ESTABLISHMENT_CNF(received_message_p));
            //} break;

            case MME_APP_CONNECTION_ESTABLISHMENT_CNF: {
                s1ap_handle_conn_est_cnf(&MME_APP_CONNECTION_ESTABLISHMENT_CNF(received_message_p));
            } break;
#endif

            case TIMER_HAS_EXPIRED: {
                s1ap_handle_timer_expiry(&received_message_p->ittiMsg.timer_has_expired);
            } break;

            case TERMINATE_MESSAGE: {
                itti_exit_task();
            } break;

            default: {
                S1AP_DEBUG("Unkwnon message ID %d:%s\n",
                           ITTI_MSG_ID(received_message_p), ITTI_MSG_NAME(received_message_p));
            } break;
        }
        itti_free(ITTI_MSG_ORIGIN_ID(received_message_p), received_message_p);
        received_message_p = NULL;
    }
    return NULL;
}

int s1ap_mme_init(const mme_config_t *mme_config_p) {
    S1AP_DEBUG("Initializing S1AP interface\n");

    if (get_asn1c_environment_version() < ASN1_MINIMUM_VERSION) {
        S1AP_ERROR("ASN1C version %d fount, expecting at least %d\n",
                   get_asn1c_environment_version(), ASN1_MINIMUM_VERSION);
        return -1;
    } else {
        S1AP_DEBUG("ASN1C version %d\n", get_asn1c_environment_version());
    }
#if defined(UPDATE_RELEASE_10)
    S1AP_DEBUG("S1AP Release v10.5\n");
#else
# if defined(UPDATE_RELEASE_9)
    S1AP_DEBUG("S1AP Release v9.8\n");
# else
    S1AP_DEBUG("S1AP Release v8.10\n");
# endif
#endif

    STAILQ_INIT(&eNB_list_head);

    if (itti_create_task(TASK_S1AP, &s1ap_mme_thread, NULL) < 0) {
        S1AP_ERROR("Error while creating S1AP task\n");
        return -1;
    }
    if (s1ap_send_init_sctp() < 0) {
        S1AP_ERROR("Error while sendind SCTP_INIT_MSG to SCTP \n");
        return -1;
    }
    S1AP_DEBUG("Initializing S1AP interface: DONE\n");
    return 0;
}

void s1ap_dump_eNB_list(void) {
    eNB_description_t *eNB_ref;

    STAILQ_FOREACH(eNB_ref, &eNB_list_head, eNB_entries)
    {
        s1ap_dump_eNB(eNB_ref);
    }
}

void s1ap_dump_eNB(eNB_description_t *eNB_ref) {
#ifdef S1AP_DEBUG_LIST
    ue_description_t *ue_ref;
    //Reset indentation
    indent = 0;

    if (eNB_ref == NULL) {
        return;
    }

    eNB_LIST_OUT("");
    eNB_LIST_OUT("eNB name:          %s",   eNB_ref->eNB_name == NULL ? "not present" : eNB_ref->eNB_name);
    eNB_LIST_OUT("eNB ID:            %07x", eNB_ref->eNB_id);
    eNB_LIST_OUT("SCTP assoc id:     %d",   eNB_ref->sctp_assoc_id);
    eNB_LIST_OUT("SCTP instreams:    %d",   eNB_ref->instreams);
    eNB_LIST_OUT("SCTP outstreams:   %d",   eNB_ref->outstreams);
    eNB_LIST_OUT("UE attache to eNB: %d",   eNB_ref->nb_ue_associated);

    indent++;

    STAILQ_FOREACH(ue_ref, &eNB_ref->ue_list_head, ue_entries)
    {
        s1ap_dump_ue(ue_ref);
    }
    indent--;
    eNB_LIST_OUT("");
#else
    s1ap_dump_ue(NULL);
#endif
}

void s1ap_dump_ue(ue_description_t *ue_ref) {
#ifdef S1AP_DEBUG_LIST
    if (ue_ref == NULL) return;
    UE_LIST_OUT("eNB UE s1ap id:   0x%06x", ue_ref->eNB_ue_s1ap_id);
    UE_LIST_OUT("MME UE s1ap id:   0x%08x", ue_ref->mme_ue_s1ap_id);
    UE_LIST_OUT("SCTP stream recv: 0x%04x", ue_ref->sctp_stream_recv);
    UE_LIST_OUT("SCTP stream send: 0x%04x", ue_ref->sctp_stream_send);
#else
    ue_ref = ue_ref;
#endif
}

eNB_description_t* s1ap_is_eNB_id_in_list(uint32_t eNB_id) {

    eNB_description_t *eNB_ref;

    STAILQ_FOREACH(eNB_ref, &eNB_list_head, eNB_entries)
    {
        if (eNB_ref->eNB_id == eNB_id) {
            /* We fount a matching reference, return it */
            return eNB_ref;
        }
    }
    /* No matching eNB, return NULL */
    return eNB_ref;
}

eNB_description_t* s1ap_is_eNB_assoc_id_in_list(uint32_t sctp_assoc_id) {

    eNB_description_t *eNB_ref;

    STAILQ_FOREACH(eNB_ref, &eNB_list_head, eNB_entries)
    {
        if (eNB_ref->sctp_assoc_id == sctp_assoc_id) {
            /* We fount a matching reference, return it */
            return eNB_ref;
        }
    }
    /* No matching eNB or no eNB in list, return NULL */
    return NULL;
}

ue_description_t *s1ap_is_ue_eNB_id_in_list(eNB_description_t *eNB_ref,
                                            uint32_t eNB_ue_s1ap_id) {
    ue_description_t *ue_ref;

    DevAssert(eNB_ref != NULL);

    STAILQ_FOREACH(ue_ref, &eNB_ref->ue_list_head, ue_entries)
    {
        if (ue_ref->eNB_ue_s1ap_id == eNB_ue_s1ap_id) {
            return ue_ref;
        }
    }

    return NULL;
}

ue_description_t* s1ap_is_ue_mme_id_in_list(uint32_t mme_ue_s1ap_id) {

    ue_description_t *ue_ref;
    eNB_description_t *eNB_ref;

    STAILQ_FOREACH(eNB_ref, &eNB_list_head, eNB_entries)
    {
        STAILQ_FOREACH(ue_ref, &eNB_ref->ue_list_head, ue_entries)
        {
            // We fount a matching reference, return it
            if (ue_ref->mme_ue_s1ap_id == mme_ue_s1ap_id) {
                return ue_ref;
            }
        }
    }

    return NULL;
}

ue_description_t* s1ap_is_teid_in_list(uint32_t teid) {

    ue_description_t *ue_ref;
    eNB_description_t *eNB_ref;

    STAILQ_FOREACH(eNB_ref, &eNB_list_head, eNB_entries)
    {
        STAILQ_FOREACH(ue_ref, &eNB_ref->ue_list_head, ue_entries)
        {
            // We fount a matching reference, return it
            if (ue_ref->teid == teid) {
                return ue_ref;
            }
        }
    }

    return NULL;
}

eNB_description_t* s1ap_new_eNB(void) {

    eNB_description_t *eNB_ref = NULL;

    eNB_ref = calloc(1, sizeof(eNB_description_t));
    /* Something bad happened during malloc...
     * May be we are running out of memory.
     * TODO: Notify eNB with a cause like Hardware Failure.
     */
    DevAssert(eNB_ref != NULL);

    // Update number of eNB associated
    nb_eNB_associated++;

    STAILQ_INIT(&eNB_ref->ue_list_head);

    eNB_ref->nb_ue_associated = 0;

    STAILQ_INSERT_TAIL(&eNB_list_head, eNB_ref, eNB_entries);

    return eNB_ref;
}

ue_description_t* s1ap_new_ue(uint32_t sctp_assoc_id) {

    eNB_description_t *eNB_ref = NULL;
    ue_description_t  *ue_ref  = NULL;

    eNB_ref = s1ap_is_eNB_assoc_id_in_list(sctp_assoc_id);
    DevAssert(eNB_ref != NULL);

    ue_ref = calloc(1, sizeof(ue_description_t));
    /* Something bad happened during malloc...
     * May be we are running out of memory.
     * TODO: Notify eNB with a cause like Hardware Failure.
     */
    DevAssert(ue_ref != NULL);

    ue_ref->eNB = eNB_ref;

    // Increment number of UE
    eNB_ref->nb_ue_associated++;
    STAILQ_INSERT_TAIL(&eNB_ref->ue_list_head, ue_ref, ue_entries);

    return ue_ref;
}

void s1ap_remove_ue(ue_description_t *ue_ref)
{
    eNB_description_t *eNB_ref;

    /* NULL reference... */
    if (ue_ref == NULL) return;

    eNB_ref = ue_ref->eNB;

    /* Updating number of UE */
    eNB_ref->nb_ue_associated--;
    /* Remove any attached timer */
//     s1ap_timer_remove_ue(ue_ref->mme_ue_s1ap_id);

    /* Freeing memory */
    free(ue_ref);
}

void s1ap_remove_eNB(eNB_description_t *eNB_ref)
{
    ue_description_t *ue_ref;

    if (eNB_ref == NULL) return;

    while (!STAILQ_EMPTY(&eNB_ref->ue_list_head))
    {
        ue_ref = STAILQ_FIRST(&eNB_ref->ue_list_head);
        eNB_ref->nb_ue_associated--;
        STAILQ_REMOVE_HEAD(&eNB_ref->ue_list_head, ue_entries);
        free(ue_ref);
    }

    STAILQ_REMOVE(&eNB_list_head, eNB_ref, eNB_description_s, eNB_entries);

    free(eNB_ref);
    nb_eNB_associated--;
}

#endif
