/*
 * Copyright (c) 2018-2019, Arm Limited and affiliates.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */

#include "nsconfig.h"
#include <string.h>
#include "ns_types.h"
#include "ns_list.h"
#include "ns_trace.h"
#include "nsdynmemLIB.h"
#include "fhss_config.h"
#include "NWK_INTERFACE/Include/protocol.h"
#include "mac_api.h"
#include "mac_mcps.h"
#include "Common_Protocols/ipv6_constants.h"
#include "socket_api.h"
#include "6LoWPAN/MAC/mac_helper.h"
#include "6LoWPAN/MAC/mpx_api.h"
#include "6LoWPAN/ws/ws_config.h"
#include "6LoWPAN/ws/ws_eapol_pdu.h"
#include "6LoWPAN/ws/ws_llc.h"

#ifdef HAVE_WS

#define TRACE_GROUP "wsep"

typedef struct {
    void *data_ptr;
    void *buffer;
    ws_eapol_pdu_tx_status *tx_status;
    uint8_t tx_identifier;
    uint8_t handle;
    ns_list_link_t link;
} eapol_pdu_msdu_t;

typedef NS_LIST_HEAD(eapol_pdu_msdu_t, link) eapol_pdu_msdu_list_t;

typedef struct {
    uint8_t priority;
    bool filter_requsted: 1;
    ws_eapol_pdu_address_check *addr_check;
    ws_eapol_pdu_receive *receive;
    ns_list_link_t link;
} eapol_pdu_recv_cb_t;

typedef NS_LIST_HEAD(eapol_pdu_recv_cb_t, link) eapol_pdu_recv_cb_list_t;

typedef struct {
    eapol_pdu_recv_cb_list_t recv_cb_list;                 /**< List of receive callbacks */
    eapol_pdu_msdu_list_t msdu_list;                       /**< MSDU list */
    ws_eapol_pdu_receive *receive;                         /**< data receive callback */
    protocol_interface_info_entry_t *interface_ptr;        /**< Interface pointer */
    mpx_api_t *mpx_api;                                    /**< MPX api */
    uint16_t mpx_user_id;                                  /**< MPX user identifier */
    uint8_t msdu_handle;                                   /**< MSDU handle */
    ns_list_link_t link;                                   /**< Link */
} eapol_pdu_data_t;

static void ws_eapol_pdu_mpx_data_confirm(const mpx_api_t *api, const struct mcps_data_conf_s *data);
static void ws_eapol_pdu_mpx_data_indication(const mpx_api_t *api, const struct mcps_data_ind_s *data);
static void ws_eapol_pdu_data_request_primitiv_set(mcps_data_req_t *dataReq, protocol_interface_info_entry_t *cur);
static eapol_pdu_data_t *ws_eapol_pdu_data_get(protocol_interface_info_entry_t *interface_ptr);

static NS_LIST_DEFINE(eapol_pdu_data_list, eapol_pdu_data_t, link);

int8_t ws_eapol_pdu_init(protocol_interface_info_entry_t *interface_ptr)
{
    if (!interface_ptr) {
        return -1;
    }

    if (ws_eapol_pdu_data_get(interface_ptr) != NULL) {
        return 0;
    }

    eapol_pdu_data_t *eapol_pdu_data = ns_dyn_mem_alloc(sizeof(eapol_pdu_data_t));
    if (!eapol_pdu_data) {
        return -1;
    }

    eapol_pdu_data->interface_ptr = interface_ptr;
    ns_list_init(&eapol_pdu_data->recv_cb_list);
    ns_list_init(&eapol_pdu_data->msdu_list);
    eapol_pdu_data->msdu_handle = 0;

    ns_list_add_to_end(&eapol_pdu_data_list, eapol_pdu_data);

    return 0;
}

int8_t ws_eapol_pdu_delete(protocol_interface_info_entry_t *interface_ptr)
{
    if (!interface_ptr) {
        return -1;
    }

    eapol_pdu_data_t *eapol_pdu_data = ws_eapol_pdu_data_get(interface_ptr);

    if (!eapol_pdu_data) {
        return -1;
    }

    ns_list_foreach_safe(eapol_pdu_recv_cb_t, cb_entry, &eapol_pdu_data->recv_cb_list) {
        ns_list_remove(&eapol_pdu_data->recv_cb_list, cb_entry);
        ns_dyn_mem_free(cb_entry);
    }

    ns_list_foreach_safe(eapol_pdu_msdu_t, msdu_entry, &eapol_pdu_data->msdu_list) {
        ns_list_remove(&eapol_pdu_data->msdu_list, msdu_entry);
        ns_dyn_mem_free(msdu_entry);
    }

    ns_list_remove(&eapol_pdu_data_list, eapol_pdu_data);
    ns_dyn_mem_free(eapol_pdu_data);

    return 0;
}

int8_t ws_eapol_pdu_cb_register(protocol_interface_info_entry_t *interface_ptr, const eapol_pdu_recv_cb_data_t *cb_data)
{
    if (!interface_ptr || !cb_data) {
        return -1;
    }

    eapol_pdu_data_t *eapol_pdu_data =  ws_eapol_pdu_data_get(interface_ptr);

    if (!eapol_pdu_data) {
        return -1;
    }

    eapol_pdu_recv_cb_t *new_cb = ns_dyn_mem_alloc(sizeof(eapol_pdu_recv_cb_t));
    if (!new_cb) {
        return -1;
    }

    new_cb->priority = cb_data->priority;
    new_cb->addr_check = cb_data->addr_check;
    new_cb->receive = cb_data->receive;
    new_cb->filter_requsted = cb_data->filter_requsted;

    ns_list_foreach(eapol_pdu_recv_cb_t, entry, &eapol_pdu_data->recv_cb_list) {
        if (new_cb->priority <= entry->priority) {
            ns_list_add_before(&eapol_pdu_data->recv_cb_list, entry, new_cb);
            return 0;
        }
    }

    ns_list_add_to_end(&eapol_pdu_data->recv_cb_list, new_cb);
    return 0;
}

int8_t ws_eapol_pdu_cb_unregister(protocol_interface_info_entry_t *interface_ptr, const eapol_pdu_recv_cb_data_t *cb_data)
{
    if (!interface_ptr || !cb_data) {
        return -1;
    }

    eapol_pdu_data_t *eapol_pdu_data = ws_eapol_pdu_data_get(interface_ptr);

    if (!eapol_pdu_data) {
        return -1;
    }

    ns_list_foreach_safe(eapol_pdu_recv_cb_t, entry, &eapol_pdu_data->recv_cb_list) {
        if (entry->receive == cb_data->receive) {
            ns_list_remove(&eapol_pdu_data->recv_cb_list, entry);
            ns_dyn_mem_free(entry);
            return 0;
        }
    }

    return -1;
}

int8_t ws_eapol_pdu_send_to_mpx(protocol_interface_info_entry_t *interface_ptr, const uint8_t *eui_64, void *data, uint16_t size, void *buffer, ws_eapol_pdu_tx_status *tx_status, uint8_t tx_identifier)
{
    eapol_pdu_data_t *eapol_pdu_data = ws_eapol_pdu_data_get(interface_ptr);

    if (!eapol_pdu_data) {
        return -1;
    }

    mcps_data_req_t data_request;
    ws_eapol_pdu_data_request_primitiv_set(&data_request, eapol_pdu_data->interface_ptr);

    eapol_pdu_msdu_t *msdu_entry = ns_dyn_mem_temporary_alloc(sizeof(eapol_pdu_msdu_t));
    if (!msdu_entry) {
        return -1;
    }
    msdu_entry->data_ptr = data;
    msdu_entry->buffer = buffer;
    msdu_entry->handle = eapol_pdu_data->msdu_handle;
    msdu_entry->tx_status = tx_status;
    msdu_entry->tx_identifier = tx_identifier;
    ns_list_add_to_start(&eapol_pdu_data->msdu_list, msdu_entry);

    memcpy(data_request.DstAddr, eui_64, 8);
    data_request.msdu = data;
    data_request.msduLength = size;
    data_request.msduHandle = eapol_pdu_data->msdu_handle;

    eapol_pdu_data->msdu_handle++;

    eapol_pdu_data->mpx_api->mpx_data_request(eapol_pdu_data->mpx_api, &data_request, eapol_pdu_data->mpx_user_id);
    return 0;
}

static void ws_eapol_pdu_data_request_primitiv_set(mcps_data_req_t *dataReq, protocol_interface_info_entry_t *cur)
{
    memset(dataReq, 0, sizeof(mcps_data_req_t));

    dataReq->InDirectTx = false;
    dataReq->TxAckReq = true;
    dataReq->SrcAddrMode = ADDR_802_15_4_LONG;
    dataReq->DstAddrMode = ADDR_802_15_4_LONG;
    dataReq->DstPANId = mac_helper_panid_get(cur);
}

int8_t ws_eapol_pdu_mpx_register(protocol_interface_info_entry_t *interface_ptr, struct mpx_api_s *mpx_api, uint16_t mpx_user_id)
{
    if (!interface_ptr) {
        return -1;
    }

    eapol_pdu_data_t *eapol_pdu_data = ws_eapol_pdu_data_get(interface_ptr);

    if (!eapol_pdu_data) {
        return -1;
    }

    if (!mpx_api && eapol_pdu_data->mpx_api) {
        //Disable Data Callbacks from MPX Class
        eapol_pdu_data->mpx_api->mpx_user_registration(eapol_pdu_data->mpx_api, NULL, NULL, eapol_pdu_data->mpx_user_id);
    }

    eapol_pdu_data->mpx_api = mpx_api;
    eapol_pdu_data->mpx_user_id = mpx_user_id;

    if (eapol_pdu_data->mpx_api) {
        eapol_pdu_data->mpx_api->mpx_user_registration(eapol_pdu_data->mpx_api, ws_eapol_pdu_mpx_data_confirm, ws_eapol_pdu_mpx_data_indication, eapol_pdu_data->mpx_user_id);
    }
    return 0;
}

static void ws_eapol_pdu_mpx_data_confirm(const mpx_api_t *api, const struct mcps_data_conf_s *data)
{
    eapol_pdu_data_t *eapol_pdu_data = NULL;

    ns_list_foreach(eapol_pdu_data_t, entry, &eapol_pdu_data_list) {
        if (entry->mpx_api == api) {
            eapol_pdu_data = entry;
            break;
        }
    }

    if (!eapol_pdu_data) {
        return;
    }

    ns_list_foreach(eapol_pdu_msdu_t, msdu, &eapol_pdu_data->msdu_list) {
        if (msdu->handle == data->msduHandle) {
            if (msdu->tx_status) {
                eapol_pdu_tx_status_e status = EAPOL_PDU_TX_ERR_UNSPEC;
                if (data->status == MLME_SUCCESS) {
                    status = EAPOL_PDU_TX_OK;
                } else if (data->status == MLME_TX_NO_ACK) {
                    status = EAPOL_PDU_TX_ERR_TX_NO_ACK;
                }
                msdu->tx_status(eapol_pdu_data->interface_ptr, status, msdu->tx_identifier);
            }
            ns_dyn_mem_free(msdu->buffer);
            ns_list_remove(&eapol_pdu_data->msdu_list, msdu);
            ns_dyn_mem_free(msdu);
            return;
        }
    }
}

static void ws_eapol_pdu_mpx_data_indication(const mpx_api_t *api, const struct mcps_data_ind_s *data)
{
    if (!data || !data->msduLength || !data->msdu_ptr) {
        return;
    }

    eapol_pdu_data_t *eapol_pdu_data = NULL;

    ns_list_foreach(eapol_pdu_data_t, entry, &eapol_pdu_data_list) {
        if (entry->mpx_api == api) {
            eapol_pdu_data = entry;
            break;
        }
    }

    if (!eapol_pdu_data) {
        return;
    }

    ns_list_foreach(eapol_pdu_recv_cb_t, entry, &eapol_pdu_data->recv_cb_list) {
        if (entry->addr_check(eapol_pdu_data->interface_ptr, data->SrcAddr) >= 0) {
            if (entry->filter_requsted && !ws_llc_eapol_relay_forward_filter(eapol_pdu_data->interface_ptr, data->SrcAddr, data->DSN, data->timestamp)) {
                tr_info("EAPOL relay filter drop");
                return;
            }

            entry->receive(eapol_pdu_data->interface_ptr, data->SrcAddr, data->msdu_ptr, data->msduLength);
            break;
        }
    }
}

static eapol_pdu_data_t *ws_eapol_pdu_data_get(protocol_interface_info_entry_t *interface_ptr)
{
    eapol_pdu_data_t *eapol_pdu_data = NULL;

    ns_list_foreach(eapol_pdu_data_t, entry, &eapol_pdu_data_list) {
        if (entry->interface_ptr == interface_ptr) {
            eapol_pdu_data = entry;
            break;
        }
    }

    return eapol_pdu_data;
}

#endif /* HAVE_WS */

