/*
 * Copyright (C) 2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 *
 * This file is part of Open5GS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "ogs-gtp.h"

int ogs_gtp1_gsn_addr_to_sockaddr(const ogs_gtp1_gsn_addr_t *gsnaddr,
        uint16_t gsnaddr_len, uint16_t port, ogs_sockaddr_t **list)
{
    ogs_sockaddr_t *addr = NULL, *addr6 = NULL;

    ogs_assert(gsnaddr);
    ogs_assert(list);

    switch (gsnaddr_len) {
    case OGS_GTP_GSN_ADDRESS_IPV4_LEN:
        addr = ogs_calloc(1, sizeof(ogs_sockaddr_t));
        if (!addr) {
            ogs_error("ogs_calloc() failed");
            return OGS_ERROR;
        }
        addr->ogs_sa_family = AF_INET;
        addr->ogs_sin_port = port;
        addr->sin.sin_addr.s_addr = gsnaddr->addr;
        *list = addr;
        break;
    case OGS_GTP_GSN_ADDRESS_IPV6_LEN:
        addr6 = ogs_calloc(1, sizeof(ogs_sockaddr_t));
        if (!addr6) {
            ogs_error("ogs_calloc() failed");
            return OGS_ERROR;
        }
        addr6->ogs_sa_family = AF_INET6;
        addr6->ogs_sin_port = port;
        memcpy(addr6->sin6.sin6_addr.s6_addr, gsnaddr->addr6, OGS_IPV6_LEN);
        *list = addr6;
        break;
    default:
        ogs_error("No IPv4 or IPv6");
        return OGS_ERROR;
    }

    return OGS_OK;
}

int ogs_gtp1_sockaddr_to_gsn_addr(const ogs_sockaddr_t *addr,
        const ogs_sockaddr_t *addr6, ogs_gtp1_gsn_addr_t *gsnaddr, int *len)
{
    ogs_assert(gsnaddr);

    if (addr && addr6) {
        ogs_error("GSN Address: Both IPv4 and IPv6 not supported");
        return OGS_ERROR;
    } else if (addr) {
        gsnaddr->addr = addr->sin.sin_addr.s_addr;
        *len = OGS_GTP_GSN_ADDRESS_IPV4_LEN;
    } else if (addr6) {
        memcpy(gsnaddr->addr6, addr6->sin6.sin6_addr.s6_addr, OGS_IPV6_LEN);
        *len = OGS_GTP_GSN_ADDRESS_IPV6_LEN;
    } else {
        ogs_error("No IPv4 or IPv6");
        return OGS_ERROR;
    }

    return OGS_OK;
}

int ogs_gtp1_gsn_addr_to_ip(const ogs_gtp1_gsn_addr_t *gsnaddr, uint16_t gsnaddr_len,
        ogs_ip_t *ip)
{
    ogs_assert(ip);
    ogs_assert(gsnaddr);

    memset(ip, 0, sizeof(ogs_ip_t));

    if (gsnaddr_len == OGS_GTP_GSN_ADDRESS_IPV4_LEN) {
        ip->ipv4 = 1;
        ip->ipv6 = 0;
        ip->addr = gsnaddr->addr;
    } else if (gsnaddr_len == OGS_GTP_GSN_ADDRESS_IPV6_LEN) {
        ip->ipv4 = 0;
        ip->ipv6 = 1;
        memcpy(ip->addr6, gsnaddr->addr6, OGS_IPV6_LEN);
    } else {
        ogs_error("No IPv4 or IPv6");
        return OGS_ERROR;
    }

    return OGS_OK;
}

int ogs_gtp1_pdu_session_type_to_eua_ietf_type(uint8_t session_type)
{
    switch (session_type) {
    case OGS_PDU_SESSION_TYPE_IPV4:
        return OGS_PDP_EUA_IETF_IPV4;
    case OGS_PDU_SESSION_TYPE_IPV6:
        return OGS_PDP_EUA_IETF_IPV6;
    case OGS_PDU_SESSION_TYPE_IPV4V6:
        return OGS_PDP_EUA_IETF_IPV4V6;
    default:
        return OGS_ERROR;
    }
}

int ogs_gtp1_eua_ietf_type_to_pdu_session_type(uint8_t eua_ietf_type)
{
    switch (eua_ietf_type) {
    case OGS_PDP_EUA_IETF_IPV4:
        return OGS_PDU_SESSION_TYPE_IPV4;
    case OGS_PDP_EUA_IETF_IPV6:
        return OGS_PDU_SESSION_TYPE_IPV6;
    case OGS_PDP_EUA_IETF_IPV4V6:
        return OGS_PDU_SESSION_TYPE_IPV4V6;
    default:
        return OGS_ERROR;
    }
}

int ogs_gtp1_eua_to_ip(const ogs_eua_t *eua, uint16_t eua_len, ogs_ip_t *ip,
        uint8_t *pdu_session_type)
{
    ogs_assert(eua);
    ogs_assert(ip);

    memset(ip, 0, sizeof *ip);

    switch (eua->organization) {
    case OGS_PDP_EUA_ORG_IETF:
        break;
    case OGS_PDP_EUA_ORG_ETSI:
    default:
        ogs_error("Unsupported EUA organization %u", eua->organization);
        return OGS_ERROR;
    }

    eua_len -= 2;
    switch (eua->type) {
    case OGS_PDP_EUA_IETF_IPV4:
        if (eua_len == OGS_IPV4_LEN) {
            ip->addr = eua->addr;
        } else if (eua_len != 0) {
            ogs_error("Wrong IPv4 EUA length %u", eua_len);
            return OGS_ERROR;
        }
        ip->ipv4 = 1;
        ip->ipv6 = 0;
        *pdu_session_type = OGS_PDU_SESSION_TYPE_IPV4;
        break;
    case OGS_PDP_EUA_IETF_IPV6:
        if (eua_len == OGS_IPV6_LEN) {
            memcpy(ip->addr6, eua->addr6, OGS_IPV6_LEN);
        } else if (eua_len != 0) {
            ogs_error("Wrong IPv6 EUA length %u", eua_len);
            return OGS_ERROR;
        }
        ip->ipv4 = 0;
        ip->ipv6 = 1;
        *pdu_session_type = OGS_PDU_SESSION_TYPE_IPV6;
        break;
    case OGS_PDP_EUA_IETF_IPV4V6:
        if (eua_len == OGS_IPV4_LEN) {
            ip->addr = eua->addr;
        } else if (eua_len == OGS_IPV6_LEN) {
            memcpy(ip->addr6, eua->addr6, OGS_IPV6_LEN);
        } else if (eua_len == OGS_IPV4_LEN + OGS_IPV6_LEN) {
            ip->addr = eua->both.addr;
            memcpy(ip->addr6, eua->both.addr6, OGS_IPV6_LEN);
        } else if (eua_len != 0) {
            ogs_error("Wrong IPv4v6 EUA length %u", eua_len);
            return OGS_ERROR;
        }
        ip->ipv4 = 1;
        ip->ipv6 = 1;
        *pdu_session_type = OGS_PDU_SESSION_TYPE_IPV4V6;
        break;
    default:
        ogs_error("No IPv4 or IPv6");
        return OGS_ERROR;
    }

    return OGS_OK;
}

int ogs_gtp1_ip_to_eua(uint8_t pdu_session_type, const ogs_ip_t *ip,
        ogs_eua_t *eua, uint8_t *eua_len)
{
    ogs_assert(eua);
    ogs_assert(ip);
    ogs_assert(eua_len);

    memset(eua, 0, sizeof *eua);

    eua->spare = 0xf; /* TS 29.060 Figure 35 */
    eua->organization = OGS_PDP_EUA_ORG_IETF;

    switch (pdu_session_type)
    {
    case OGS_PDU_SESSION_TYPE_IPV4:
        if (!ip->ipv4) {
            ogs_error("EUA type IPv4 but no IPv4 address available");
            return OGS_ERROR;
        }
        eua->addr = ip->addr;
        *eua_len = 2 + OGS_IPV4_LEN;
        eua->type = OGS_PDP_EUA_IETF_IPV4;
        break;
    case OGS_PDU_SESSION_TYPE_IPV6:
        if (!ip->ipv6) {
            ogs_error("EUA type IPv4 but no IPv6 address available");
            return OGS_ERROR;
        }
        memcpy(eua->addr6, ip->addr6, OGS_IPV6_LEN);
        *eua_len = 2 + OGS_IPV6_LEN;
        eua->type = OGS_PDP_EUA_IETF_IPV6;
        break;
    case OGS_PDU_SESSION_TYPE_IPV4V6:
        if (ip->ipv4 && ip->ipv6) {
            eua->both.addr = ip->addr;
            memcpy(eua->both.addr6, ip->addr6, OGS_IPV6_LEN);
            *eua_len = 2 + OGS_IPV4_LEN + OGS_IPV6_LEN;
        } else if (ip->ipv4) {
            eua->addr = ip->addr;
            *eua_len = 2 + OGS_IPV4_LEN;
        } else if (ip->ipv6) {
            memcpy(eua->addr6, ip->addr6, OGS_IPV6_LEN);
            *eua_len = 2 + OGS_IPV6_LEN;
        } else {
            ogs_error("EUA type IPv4 but no IPv4 nor IPv6 address available");
            return OGS_ERROR;
        }
        eua->type = OGS_PDP_EUA_IETF_IPV4V6;
        break;
    default:
        ogs_error("Unexpected session type");
        return OGS_ERROR;
    }
    return OGS_OK;
}

/* 3GPP TS 23.401 Table E.3 */
int ogs_gtp1_qos_profile_to_qci(const ogs_gtp1_qos_profile_decoded_t *decoded,
        uint8_t *qci)
{
    ogs_assert(decoded);
    ogs_assert(qci);

    if (!decoded->data_octet6_to_13_present) {
        /* traffic class not present, take QCI 9 as default */
        *qci = 9;
        return OGS_OK;
    }

    switch (decoded->qos_profile.data.traffic_class) {
    case OGS_GTP1_QOS_TRAFFIC_CLASS_CONVERSATIONAL:
        if (decoded->qos_profile.data.source_statistics_descriptor == OGS_GTP1_QOS_SRC_STATS_DESC_SPEECH)
            *qci = 1;
        else if (decoded->dec_transfer_delay >= 150)
            *qci = 2;
        else
            *qci = 3;
        break;
    case OGS_GTP1_QOS_TRAFFIC_CLASS_STREAMING:
        *qci = 4;
        break;
    case OGS_GTP1_QOS_TRAFFIC_CLASS_INTERACTIVE:
        switch (decoded->qos_profile.data.traffic_handling_priority) {
        case 1:
            *qci = decoded->qos_profile.data.signalling_indication ? 5 : 6;
            break;
        case 2:
            *qci = 7;
            break;
        case 3:
            *qci = 8;
            break;
        default:
            *qci = 9;
        }
        break;
    case OGS_GTP1_QOS_TRAFFIC_CLASS_SUBSCRIBED:
    case OGS_GTP1_QOS_TRAFFIC_CLASS_BACKGROUND:
    default:
        *qci = 9;
    }
    return OGS_OK;
}
