/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc.  See LICENSE. */
/*
 * perf_server.c -- Implements the "perf" server, see
 *      https://tools.ietf.org/html/draft-banks-quic-performance-00
 */

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include <time.h>

#ifndef WIN32
#include <unistd.h>
#include <fcntl.h>
#else
#include "vc_compat.h"
#include "getopt.h"
#endif

#include <event2/event.h>

#include "lsquic.h"
#include "test_common.h"
#include "../src/liblsquic/lsquic_hash.h"
#include "test_cert.h"
#include "prog.h"

#include "../src/liblsquic/lsquic_byteswap.h"
#include "../src/liblsquic/lsquic_logger.h"

static uint64_t s_max_pacing_rate = 0;

static lsquic_conn_ctx_t *
perf_server_on_new_conn (void *stream_if_ctx, lsquic_conn_t *conn)
{
    LSQ_INFO("New connection!");

    if (s_max_pacing_rate > 0)
    {
        if (0 == lsquic_conn_set_param(conn, LSQCP_MAX_PACING_RATE,
                                       &s_max_pacing_rate,
                                       sizeof(s_max_pacing_rate)))
        {
            LSQ_INFO("max pacing rate set to %"PRIu64" bytes/sec (%.2f Mbps)",
                     s_max_pacing_rate,
                     (s_max_pacing_rate * 8.0) / 1000000.0);
        }
        else
        {
            LSQ_WARN("failed to set max pacing rate");
        }
    }

    return NULL;
}


static void
perf_server_on_conn_closed (lsquic_conn_t *conn)
{
    LSQ_INFO("Connection closed");
}


struct lsquic_stream_ctx
{
    union {
        uint64_t        left;   /* Number of bytes left to write */
        unsigned char   buf[sizeof(uint64_t)];  /* Read client header in */
    }                   u;
    unsigned            n_h_read;   /* Number of header bytes read in */
};


static struct lsquic_stream_ctx *
perf_server_on_new_stream (void *unused, struct lsquic_stream *stream)
{
    struct lsquic_stream_ctx *stream_ctx;

    stream_ctx = calloc(1, sizeof(*stream_ctx));
    if (stream_ctx)
    {
        lsquic_stream_wantread(stream, 1);
        return stream_ctx;
    }
    else
    {
        perror("calloc");
        exit(EXIT_FAILURE);
    }
}


static size_t
perf_read_and_discard (void *user_data, const unsigned char *buf,
                                                        size_t count, int fin)
{
    return count;
}


static void
perf_server_on_read (struct lsquic_stream *stream,
                                        struct lsquic_stream_ctx *stream_ctx)
{
    ssize_t nr;
    size_t toread;

    if (stream_ctx->n_h_read < sizeof(stream_ctx->u.buf))
    {
        /* Read the header */
        toread = sizeof(stream_ctx->u.buf) - stream_ctx->n_h_read;
        nr = lsquic_stream_read(stream, stream_ctx->u.buf
                            + sizeof(stream_ctx->u.buf) - toread, toread);
        if (nr > 0)
        {
            stream_ctx->n_h_read += nr;
            if (stream_ctx->n_h_read == sizeof(stream_ctx->u.left))
            {
#if __BYTE_ORDER == __LITTLE_ENDIAN
                stream_ctx->u.left = bswap_64(stream_ctx->u.left);
#endif
                LSQ_INFO("client requests %"PRIu64" bytes on stream %"PRIu64,
                    stream_ctx->u.left, lsquic_stream_id(stream));
            }
        }
        else if (nr < 0)
        {
            LSQ_WARN("error reading from stream: %s", strerror(errno));
            lsquic_stream_close(stream);
        }
        else
        {
            LSQ_WARN("incomplete header on stream %"PRIu64", abort connection",
                lsquic_stream_id(stream));
            lsquic_stream_wantread(stream, 0);
            lsquic_conn_abort(lsquic_stream_conn(stream));
        }
    }
    else
    {
        /* Read up until FIN, discarding whatever the client is sending */
        nr = lsquic_stream_readf(stream, perf_read_and_discard, NULL);
        if (nr == 0)
        {
            lsquic_stream_wantread(stream, 0);
            lsquic_stream_wantwrite(stream, 1);
        }
        else if (nr < 0)
        {
            LSQ_WARN("error reading from stream: %s", strerror(errno));
            lsquic_stream_close(stream);
        }
    }
}


static size_t
buffer_size (void *lsqr_ctx)
{
    struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx;
    return stream_ctx->u.left;
}


static size_t
buffer_read (void *lsqr_ctx, void *buf, size_t count)
{
    struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx;
    size_t left;

    left = buffer_size(stream_ctx);
    if (count > left)
        count = left;
    memset(buf, 0, count);
    stream_ctx->u.left -= count;
    return count;
}


static void
perf_server_on_write (struct lsquic_stream *stream,
                                        struct lsquic_stream_ctx *stream_ctx)
{
    struct lsquic_reader reader;
    ssize_t nw;

    reader = (struct lsquic_reader) { buffer_read, buffer_size, stream_ctx, };
    nw = lsquic_stream_writef(stream, &reader);
    if (nw >= 0)
        LSQ_DEBUG("%s: wrote %zd bytes", __func__, nw);
    else
        LSQ_WARN("%s: cannot write to stream: %s", __func__, strerror(errno));

    if (stream_ctx->u.left == 0)
        lsquic_stream_shutdown(stream, 1);
}


static void
perf_server_on_close (lsquic_stream_t *stream, lsquic_stream_ctx_t *stream_ctx)
{
    LSQ_DEBUG("stream closed");
    free(stream_ctx);
}


const struct lsquic_stream_if perf_server_stream_if = {
    .on_new_conn            = perf_server_on_new_conn,
    .on_conn_closed         = perf_server_on_conn_closed,
    .on_new_stream          = perf_server_on_new_stream,
    .on_read                = perf_server_on_read,
    .on_write               = perf_server_on_write,
    .on_close               = perf_server_on_close,
};


static void
usage (const char *prog)
{
    const char *const slash = strrchr(prog, '/');
    if (slash)
        prog = slash + 1;
    printf(
"Usage: %s [opts]\n"
"\n"
"Options:\n"
"   -x RATE     Maximum pacing rate in bytes per second (throttle bandwidth)\n"
"\n"
                , prog);
}


int
main (int argc, char **argv)
{
    int opt, s;
    struct prog prog;
    struct sport_head sports;

    TAILQ_INIT(&sports);
    prog_init(&prog, LSENG_SERVER, &sports, &perf_server_stream_if, NULL);

    while (-1 != (opt = getopt(argc, argv, PROG_OPTS "x:h")))
    {
        switch (opt) {
        case 'x':
            s_max_pacing_rate = strtoull(optarg, NULL, 10);
            break;
        case 'h':
            usage(argv[0]);
            prog_print_common_options(&prog, stdout);
            exit(0);
        default:
            if (0 != prog_set_opt(&prog, opt, optarg))
                exit(1);
        }
    }

    add_alpn("perf");
    if (0 != prog_prep(&prog))
    {
        LSQ_ERROR("could not prep");
        exit(EXIT_FAILURE);
    }

    LSQ_DEBUG("entering event loop");

    s = prog_run(&prog);
    prog_cleanup(&prog);

    exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE);
}
