/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * 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 "nstackx_socket.h"
#include "nstackx_log.h"
#include "nstackx_error.h"
#include "nstackx_util.h"
#include "nstackx_dev.h"
#include "securec.h"

#define DEFAULT_UDP_MSS 1472
#define DEFAULT_MAX_BUF 4096
#define IOV_CNT 2

#define TAG "nStackXSocket"

static int32_t g_gsoSupport = 0;

int32_t SupportGSO(void)
{
    return g_gsoSupport;
}

void SocketModuleClean(void)
{
}

int32_t SocketModuleInit(void)
{
    return NSTACKX_EOK;
}

int32_t SetSocketNonBlock(SocketDesc fd)
{
    int32_t flag;

    flag = fcntl(fd, F_GETFL, 0);
    if (flag < 0) {
        LOGE(TAG, "fcntl GETFL error");
        return NSTACKX_EFAILED;
    }

    if (fcntl(fd, F_SETFL, (unsigned int)flag | O_NONBLOCK) < 0) {
        LOGE(TAG, "fcntl SETFL error");
        return NSTACKX_EFAILED;
    }
    return NSTACKX_EOK;
}

#ifndef UDP_SEGMENT
#define UDP_SEGMENT     103
#endif

static inline void SetupCmsg(struct cmsghdr *cm, uint16_t mss)
{
    cm->cmsg_level = SOL_UDP;
    cm->cmsg_type = UDP_SEGMENT;
    cm->cmsg_len = CMSG_LEN(sizeof(mss));
    *(uint16_t *)(void *)CMSG_DATA(cm) = mss;
}

static inline int32_t IsSocketValid(const Socket *s)
{
    return !(s == NULL || s->protocol != NSTACKX_PROTOCOL_UDP);
}

int32_t SocketSendEx(const Socket *s, uint16_t mss, const struct iovec *iov, uint32_t cnt)
{
    int32_t ret = NSTACKX_EFAILED;
    char ctrl[CMSG_SPACE(sizeof(uint16_t))] = {0};
    struct msghdr mh;

    if (!IsSocketValid(s)) {
        LOGE(TAG, "invalid socket input\n");
        return ret;
    }

    mh.msg_name = (struct sockaddr *)&s->dstAddr;
    mh.msg_namelen = sizeof(struct sockaddr_in);
    mh.msg_iov = (struct iovec *)iov;
    mh.msg_iovlen = (size_t)cnt;
    mh.msg_control = ctrl;
    mh.msg_controllen = sizeof(ctrl);
    mh.msg_flags = 0;

    SetupCmsg(CMSG_FIRSTHDR(&mh), mss);

    ret = (int32_t)sendmsg(s->sockfd, &mh, 0);
    if (ret <= 0) {
        ret = CheckSocketError();
    }

    return ret;
}
#ifndef NSTACKX_WITH_LINUX
static int32_t SendUdpSegment(struct sockaddr_in *sa)
{
    int32_t err;
    char ctrl[CMSG_SPACE(sizeof(uint16_t))] = {0};
    struct msghdr mh;
    char buf[DEFAULT_UDP_MSS];
    struct iovec iov[IOV_CNT] = {
        {
            .iov_base = buf,
            .iov_len = sizeof(buf),
        },
        {
            .iov_base = buf,
            .iov_len = sizeof(buf),
        }
    };
    int32_t fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        return NSTACKX_EFAILED;
    }

    mh.msg_name = (struct sockaddr *)sa;
    mh.msg_namelen = sizeof(struct sockaddr_in);
    mh.msg_iov = iov;
    mh.msg_iovlen = IOV_CNT;
    mh.msg_control = ctrl;
    mh.msg_controllen = sizeof(ctrl);
    mh.msg_flags = 0;

    SetupCmsg(CMSG_FIRSTHDR(&mh), DEFAULT_UDP_MSS);

    err = (int32_t)sendmsg(fd, &mh, 0);
    if (close(fd) < 0) {
        return NSTACKX_EFAILED;
    }
    return (err == (IOV_CNT * DEFAULT_UDP_MSS)) ? NSTACKX_EOK : NSTACKX_EFAILED;
}

static void RecvUdpSegment(int32_t fd)
{
    ssize_t err;
    char buf[DEFAULT_MAX_BUF];

    err = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
    if (err == DEFAULT_UDP_MSS) {
        err = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        if (err == DEFAULT_UDP_MSS) {
            g_gsoSupport = 1;
            LOGI(TAG, "kernel support UDP GSO");
        } else {
            LOGI(TAG, "kernel does not support UDP GSO");
        }
    } else {
        LOGI(TAG, "kernel does not support UDP GSO");
    }
}

static int32_t LocalAddrBindAndGet(int32_t fd, struct sockaddr_in *sa)
{
    int32_t err;
    socklen_t len = sizeof(*sa);

    sa->sin_family = AF_INET;
    sa->sin_port = 0;
    sa->sin_addr.s_addr = inet_addr("127.0.0.1");
    err = bind(fd, (struct sockaddr *)sa, len);
    if (err) {
        return NSTACKX_EFAILED;
    }

    err = getsockname(fd, (struct sockaddr *)sa, &len);
    if (err) {
        return NSTACKX_EFAILED;
    }
    return NSTACKX_EOK;
}
#endif

void CheckGSOSupport(void)
{
#ifndef NSTACKX_WITH_LINUX
    int32_t fd;
    struct sockaddr_in sa = {0};

    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        return;
    }
    if (LocalAddrBindAndGet(fd, &sa) != NSTACKX_EOK) {
        goto L_OUT;
    }

    if (SendUdpSegment(&sa) != NSTACKX_EOK) {
        goto L_OUT;
    }

    RecvUdpSegment(fd);

L_OUT:
    CloseSocketInner(fd);
#endif
}

int32_t SocketOpInProgress(void)
{
    return errno == EINPROGRESS;
}

int32_t SocketOpWouldBlock(void)
{
    return errno == EAGAIN || errno == EWOULDBLOCK;
}