/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc.  See LICENSE. */
/*
 * lsquic_di_hash.c -- Copy incoming data into a hash
 *
 * While this implementation copies the data, its memory use is limited,
 * which makes it a good choice when we have a lot of stream frames
 * coming in.
 *
 * Another difference is that incoming STREAM frames are allowed to overlap.
 */


#include <assert.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>

#include "lsquic.h"
#include "lsquic_int_types.h"
#include "lsquic_types.h"
#include "lsquic_conn_flow.h"
#include "lsquic_packet_common.h"
#include "lsquic_packet_in.h"
#include "lsquic_rtt.h"
#include "lsquic_sfcw.h"
#include "lsquic_varint.h"
#include "lsquic_hq.h"
#include "lsquic_hash.h"
#include "lsquic_stream.h"
#include "lsquic_mm.h"
#include "lsquic_malo.h"
#include "lsquic_conn.h"
#include "lsquic_conn_public.h"
#include "lsquic_data_in_if.h"


#define LSQUIC_LOGGER_MODULE LSQLM_DI
#define LSQUIC_LOG_CONN_ID lsquic_conn_log_cid(hdi->hdi_conn_pub->lconn)
#define LSQUIC_LOG_STREAM_ID hdi->hdi_stream_id
#include "lsquic_logger.h"


#define N_DB_SETS 57

#define DB_DATA_SIZE (0x1000 - sizeof(TAILQ_ENTRY(data_block)) - \
                                sizeof(uint64_t) - N_DB_SETS * sizeof(uint64_t))

struct data_block
{
    TAILQ_ENTRY(data_block) db_next;
    uint64_t                db_off;
    uint64_t                db_set[N_DB_SETS];  /* bit for each valid byte */
    unsigned char           db_data[DB_DATA_SIZE];
};

typedef char db_set_covers_all_db_data[(N_DB_SETS * 64 >= DB_DATA_SIZE) ?1: - 1];
typedef char db_set_no_waste[(N_DB_SETS * 64 - 64 <= DB_DATA_SIZE)?1: - 1];
typedef char db_block_is_4K[(sizeof(struct data_block) == 0x1000) ?1:- 1];


TAILQ_HEAD(dblock_head, data_block);


static const struct data_in_iface *di_if_hash_ptr;


struct hash_data_in
{
    struct data_in              hdi_data_in;
    struct lsquic_conn_public  *hdi_conn_pub;
    uint64_t                    hdi_fin_off;
    struct dblock_head         *hdi_buckets;
    struct data_block          *hdi_last_block;
    struct data_frame           hdi_data_frame;
    lsquic_stream_id_t          hdi_stream_id;
    unsigned                    hdi_count;
    unsigned                    hdi_nbits;
    enum {
            HDI_FIN = (1 << 0),
    }                           hdi_flags;
};


#define HDI_PTR(data_in) (struct hash_data_in *) \
    ((unsigned char *) (data_in) - offsetof(struct hash_data_in, hdi_data_in))


#define N_BUCKETS(n_bits) (1U << (n_bits))
#define BUCKNO(n_bits, off) ((off / DB_DATA_SIZE) & (N_BUCKETS(n_bits) - 1))


static unsigned
my_log2 /* silly name to suppress compiler warning */ (unsigned sz)
{
#if __GNUC__
    unsigned clz = __builtin_clz(sz);
    return 32 - clz;
#else
    unsigned clz;
    size_t y;
    clz = 32;
    y = sz >> 16;   if (y) { clz -= 16; sz = y; }
    y = sz >>  8;   if (y) { clz -=  8; sz = y; }
    y = sz >>  4;   if (y) { clz -=  4; sz = y; }
    y = sz >>  2;   if (y) { clz -=  2; sz = y; }
    y = sz >>  1;   if (y) return 32 - clz + 1;
    return 32 - clz + sz;
#endif
}


struct data_in *
lsquic_data_in_hash_new (struct lsquic_conn_public *conn_pub,
                        lsquic_stream_id_t stream_id, uint64_t byteage)
{
    struct hash_data_in *hdi;
    unsigned n;

    hdi = malloc(sizeof(*hdi));
    if (!hdi)
        return NULL;

    hdi->hdi_data_in.di_if    = di_if_hash_ptr;
    hdi->hdi_data_in.di_flags = 0;
    hdi->hdi_conn_pub         = conn_pub;
    hdi->hdi_stream_id        = stream_id;
    hdi->hdi_fin_off          = 0;
    hdi->hdi_flags            = 0;
    hdi->hdi_last_block       = NULL;
    if (byteage >= DB_DATA_SIZE /* __builtin_clz is undefined if
                                   argument is 0 */)
        hdi->hdi_nbits        = my_log2(byteage / DB_DATA_SIZE) + 2;
    else
        hdi->hdi_nbits        = 3;
    hdi->hdi_count            = 0;
    hdi->hdi_buckets          = malloc(sizeof(hdi->hdi_buckets[0]) *
                                                    N_BUCKETS(hdi->hdi_nbits));
    if (!hdi->hdi_buckets)
    {
        free(hdi);
        return NULL;
    }

    for (n = 0; n < N_BUCKETS(hdi->hdi_nbits); ++n)
        TAILQ_INIT(&hdi->hdi_buckets[n]);

    return &hdi->hdi_data_in;
}


static void
hash_di_destroy (struct data_in *data_in)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    struct data_block *block;
    unsigned n;

    for (n = 0; n < N_BUCKETS(hdi->hdi_nbits); ++n)
    {
        while ((block = TAILQ_FIRST(&hdi->hdi_buckets[n])))
        {
            TAILQ_REMOVE(&hdi->hdi_buckets[n], block, db_next);
            free(block);
        }
    }
    free(hdi->hdi_buckets);
    free(hdi);
}


static int
hash_grow (struct hash_data_in *hdi)
{
    struct dblock_head *new_buckets, *new[2];
    struct data_block *block;
    unsigned n, old_nbits;
    int idx;

    old_nbits = hdi->hdi_nbits;
    LSQ_DEBUG("doubling number of buckets to %u", N_BUCKETS(old_nbits + 1));
    new_buckets = malloc(sizeof(hdi->hdi_buckets[0])
                                                * N_BUCKETS(old_nbits + 1));
    if (!new_buckets)
    {
        LSQ_WARN("malloc failed: potential trouble ahead");
        return -1;
    }

    for (n = 0; n < N_BUCKETS(old_nbits); ++n)
    {
        new[0] = &new_buckets[n];
        new[1] = &new_buckets[n + N_BUCKETS(old_nbits)];
        TAILQ_INIT(new[0]);
        TAILQ_INIT(new[1]);
        while ((block = TAILQ_FIRST(&hdi->hdi_buckets[n])))
        {
            TAILQ_REMOVE(&hdi->hdi_buckets[n], block, db_next);
            idx = (BUCKNO(old_nbits + 1, block->db_off) >> old_nbits) & 1;
            TAILQ_INSERT_TAIL(new[idx], block, db_next);
        }
    }
    free(hdi->hdi_buckets);
    hdi->hdi_nbits   = old_nbits + 1;
    hdi->hdi_buckets = new_buckets;
    return 0;
}


static int
hash_insert (struct hash_data_in *hdi, struct data_block *block)
{
    unsigned buckno;

    if (hdi->hdi_count >= N_BUCKETS(hdi->hdi_nbits) / 2 && 0 != hash_grow(hdi))
        return -1;

    buckno = BUCKNO(hdi->hdi_nbits, block->db_off);
    TAILQ_INSERT_TAIL(&hdi->hdi_buckets[buckno], block, db_next);
    ++hdi->hdi_count;
    return 0;
}


static struct data_block *
hash_find (const struct hash_data_in *hdi, uint64_t off)
{
    struct data_block *block;
    unsigned buckno;

    buckno = BUCKNO(hdi->hdi_nbits, off);
    TAILQ_FOREACH(block, &hdi->hdi_buckets[buckno], db_next)
        if (off == block->db_off)
            return block;
    return NULL;
}


static void
hash_remove (struct hash_data_in *hdi, struct data_block *block)
{
    unsigned buckno;

    buckno = BUCKNO(hdi->hdi_nbits, block->db_off);
    TAILQ_REMOVE(&hdi->hdi_buckets[buckno], block, db_next);
    --hdi->hdi_count;
}


static struct data_block *
new_block (struct hash_data_in *hdi, uint64_t off)
{
    struct data_block *block;

    assert(0 == off % DB_DATA_SIZE);

    block = malloc(sizeof(*block));
    if (!block)
        return NULL;

    block->db_off = off;
    if (0 != hash_insert(hdi, block))
    {
        free(block);
        return NULL;
    }

    memset(block->db_set, 0, sizeof(block->db_set));
    return block;
}


static unsigned
block_write (struct data_block *block, unsigned block_off,
                             const unsigned char *data, unsigned data_sz)
{
    const unsigned char *begin, *end;
    unsigned set, bit, n_full_sets, n;
    uint64_t mask;

    assert(block_off < DB_DATA_SIZE);
    if (data_sz > DB_DATA_SIZE - block_off)
        data_sz = DB_DATA_SIZE - block_off;

    begin = data;
    end = begin + data_sz;
    set = block_off >> 6;
    bit = block_off & 0x3F;

    assert(set < N_DB_SETS);

    if (bit)
    {
        n = 64 - bit;
        if (n > data_sz)
            n = data_sz;
        mask = ~((1ULL <<  bit     ) - 1)
             &  ((1ULL << (bit + n - 1)) | ((1ULL << (bit + n - 1)) - 1));
        block->db_set[ set ] |= mask;
        memcpy(block->db_data + block_off, data, n);
        data      += n;
        block_off += n;
        ++set;
    }

    n_full_sets = (end - data) >> 6;
    if (n_full_sets)
    {
        memcpy(block->db_data + block_off, data, n_full_sets * 64);
        data      += n_full_sets * 64;
        block_off += n_full_sets * 64;
        memset(&block->db_set[ set ], 0xFF, n_full_sets * 8);
        set += n_full_sets;
    }

    if (data < end)
    {
        assert(end - data < 64);
        block->db_set[ set ] |= ((1ULL << (end - data)) - 1);
        memcpy(block->db_data + block_off, data, end - data);
        data = end;
    }

    assert(set <= N_DB_SETS);

    return data - begin;
}


static int
has_bytes_after (const struct data_block *block, unsigned off)
{
    unsigned bit, set;
    int has;

    set = off >> 6;
    bit = off & 0x3F;

    has = 0 != (block->db_set[ set ] >> bit);
    ++set;

    for ( ; set < N_DB_SETS; ++set)
        has += 0 != block->db_set[ set ];

    return has > 0;
}


enum ins_frame
lsquic_data_in_hash_insert_data_frame (struct data_in *data_in,
                const struct data_frame *data_frame, uint64_t read_offset)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    struct data_block *block;
    uint64_t key, off, diff, fin_off;
    const unsigned char *data;
    unsigned size, nw;

    if (data_frame->df_offset + data_frame->df_size < read_offset)
    {
        if (data_frame->df_fin)
            return INS_FRAME_ERR;
        else
            return INS_FRAME_DUP;
    }

    if ((hdi->hdi_flags & HDI_FIN) &&
         (
          (data_frame->df_fin &&
             data_frame->df_offset + data_frame->df_size != hdi->hdi_fin_off)
          ||
          data_frame->df_offset + data_frame->df_size > hdi->hdi_fin_off
         )
       )
    {
        return INS_FRAME_ERR;
    }

    if (data_frame->df_offset < read_offset)
    {
        diff = read_offset - data_frame->df_offset;
        assert(diff <= data_frame->df_size);
        size = data_frame->df_size   - diff;
        off  = data_frame->df_offset + diff;
        data = data_frame->df_data   + diff;
    }
    else
    {
        size = data_frame->df_size;
        off  = data_frame->df_offset;
        data = data_frame->df_data;
    }

    key = off - (off % DB_DATA_SIZE);
    do
    {
        block = hash_find(hdi, key);
        if (!block)
        {
            block = new_block(hdi, key);
            if (!block)
                return INS_FRAME_ERR;
        }
        nw = block_write(block, off % DB_DATA_SIZE, data, size);
        size -= nw;
        off  += nw;
        data += nw;
        key  += DB_DATA_SIZE;
    }
    while (size > 0);

    if (data_frame->df_fin)
    {
        fin_off = data_frame->df_offset + data_frame->df_size;
        if (has_bytes_after(block, fin_off - block->db_off) ||
                                                        hash_find(hdi, key))
        {
            return INS_FRAME_ERR;
        }
        hdi->hdi_flags  |= HDI_FIN;
        hdi->hdi_fin_off = fin_off;
    }

    return INS_FRAME_OK;
}


static enum ins_frame
hash_di_insert_frame (struct data_in *data_in,
                        struct stream_frame *new_frame, uint64_t read_offset)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    const struct data_frame *const data_frame = &new_frame->data_frame;
    enum ins_frame ins;

    ins = lsquic_data_in_hash_insert_data_frame(data_in, data_frame,
                                                                read_offset);
    assert(ins != INS_FRAME_OVERLAP);
    /* NOTE: Only release packet and frame for INS_FRAME_OK,
     *        other cases are handled by caller */
    if (ins == INS_FRAME_OK)
    {
        lsquic_packet_in_put(hdi->hdi_conn_pub->mm, new_frame->packet_in);
    }
    return ins;
}


#if __GNUC__
#   define ctz __builtin_ctzll
#else
static unsigned
ctz (unsigned long long x)
{
    unsigned n = 0;
    if (0 == (x & ((1ULL << 32) - 1))) { n += 32; x >>= 32; }
    if (0 == (x & ((1ULL << 16) - 1))) { n += 16; x >>= 16; }
    if (0 == (x & ((1ULL <<  8) - 1))) { n +=  8; x >>=  8; }
    if (0 == (x & ((1ULL <<  4) - 1))) { n +=  4; x >>=  4; }
    if (0 == (x & ((1ULL <<  2) - 1))) { n +=  2; x >>=  2; }
    if (0 == (x & ((1ULL <<  1) - 1))) { n +=  1; x >>=  1; }
    return n;
}
#endif


static unsigned
n_avail_bytes (const struct data_block *block, unsigned set, unsigned bit)
{
    unsigned count;
    uint64_t part;

    part = ~(block->db_set[ set ] >> bit);
    if (part)
    {
        count = ctz(part);
        if (count < 64 - bit)
            return count;
    }
    else
        count = 64;
    ++set;

    for ( ; set < N_DB_SETS && ~0ULL == block->db_set[ set ]; ++set)
        count += 64;

    if (set < N_DB_SETS)
    {
        part = ~block->db_set[ set ];
        if (part)
            count += ctz(part);
        else
            count += 64;
    }

    return count;
}


/* Data block is readable if there is at least one readable byte at
 * `read_offset' or there is FIN at that offset.
 */
static int
setup_data_frame (struct hash_data_in *hdi, const uint64_t read_offset,
                                                    struct data_block *block)
{
    unsigned set, bit;
    uint64_t offset;

    offset = read_offset % DB_DATA_SIZE;
    set = offset >> 6;
    bit = offset & 0x3F;

    if (block->db_set[ set ] & (1ULL << bit))
    {
        hdi->hdi_last_block             = block;
        hdi->hdi_data_frame.df_data     = block->db_data;
        hdi->hdi_data_frame.df_offset   = block->db_off;
        hdi->hdi_data_frame.df_read_off = offset;
        hdi->hdi_data_frame.df_size     = offset +
                                                n_avail_bytes(block, set, bit);
        hdi->hdi_data_frame.df_fin      =
            (hdi->hdi_flags & HDI_FIN) &&
                hdi->hdi_data_frame.df_read_off +
                    hdi->hdi_data_frame.df_size == hdi->hdi_fin_off;
        return 1;
    }
    else if ((hdi->hdi_flags & HDI_FIN) && read_offset == hdi->hdi_fin_off)
    {
        hdi->hdi_last_block             = block;
        hdi->hdi_data_frame.df_data     = NULL;
        hdi->hdi_data_frame.df_offset   = block->db_off;
        hdi->hdi_data_frame.df_read_off = offset;
        hdi->hdi_data_frame.df_size     = offset;
        hdi->hdi_data_frame.df_fin      = 1;
        return 1;
    }
    else
        return 0;
}


static struct data_frame *
hash_di_get_frame (struct data_in *data_in, uint64_t read_offset)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    struct data_block *block;
    uint64_t key;
    
    key = read_offset - (read_offset % DB_DATA_SIZE);
    block = hash_find(hdi, key);
    if (!block)
    {
        if ((hdi->hdi_flags & HDI_FIN) && read_offset == hdi->hdi_fin_off)
        {
            hdi->hdi_last_block             = NULL;
            hdi->hdi_data_frame.df_data     = NULL;
            hdi->hdi_data_frame.df_offset   = read_offset -
                                                    read_offset % DB_DATA_SIZE;
            hdi->hdi_data_frame.df_read_off = 0;
            hdi->hdi_data_frame.df_size     = 0;
            hdi->hdi_data_frame.df_fin      = 1;
            return &hdi->hdi_data_frame;
        }
        else
            return NULL;
    }

    if (setup_data_frame(hdi, read_offset, block))
        return &hdi->hdi_data_frame;
    else
        return NULL;
}


static void
hash_di_frame_done (struct data_in *data_in, struct data_frame *data_frame)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    struct data_block *const block = hdi->hdi_last_block;

    if (block)
    {
        if (data_frame->df_read_off == DB_DATA_SIZE ||
                            !has_bytes_after(block, data_frame->df_read_off))
        {
            hash_remove(hdi, block);
            free(block);
            if (0 == hdi->hdi_count && 0 == (hdi->hdi_flags & HDI_FIN))
            {
                LSQ_DEBUG("hash empty, want to switch");
                hdi->hdi_data_in.di_flags |= DI_SWITCH_IMPL;
            }
        }
    }
    else
        assert(data_frame->df_fin && data_frame->df_size == 0);
}


static int
hash_di_empty (struct data_in *data_in)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    return hdi->hdi_count == 0;
}


static struct data_in *
hash_di_switch_impl (struct data_in *data_in, uint64_t read_offset)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    struct data_in *new_data_in;

    assert(hdi->hdi_count == 0);

    new_data_in = lsquic_data_in_nocopy_new(hdi->hdi_conn_pub,
                                                    hdi->hdi_stream_id);
    data_in->di_if->di_destroy(data_in);

    return new_data_in;
}


static size_t
hash_di_mem_used (struct data_in *data_in)
{
    struct hash_data_in *const hdi = HDI_PTR(data_in);
    const struct data_block *block;
    size_t size;
    unsigned n;

    size = sizeof(*data_in);

    for (n = 0; n < N_BUCKETS(hdi->hdi_nbits); ++n)
        TAILQ_FOREACH(block, &hdi->hdi_buckets[n], db_next)
            size += sizeof(*block);

    size += N_BUCKETS(hdi->hdi_nbits) * sizeof(hdi->hdi_buckets[0]);

    return size;
}


static void
hash_di_dump_state (struct data_in *data_in)
{
    const struct hash_data_in *const hdi = HDI_PTR(data_in);
    const struct data_block *block;
    unsigned n;

    LSQ_DEBUG("hash state: flags: %X; fin off: %"PRIu64"; count: %u",
        hdi->hdi_flags, hdi->hdi_fin_off, hdi->hdi_count);
    for (n = 0; n < N_BUCKETS(hdi->hdi_nbits); ++n)
        TAILQ_FOREACH(block, &hdi->hdi_buckets[n], db_next)
            LSQ_DEBUG("block: off: %"PRIu64, block->db_off);
}


static uint64_t
hash_di_readable_bytes (struct data_in *data_in, uint64_t read_offset)
{
    const struct data_frame *data_frame;
    uint64_t starting_offset;

    starting_offset = read_offset;
    while (data_frame = hash_di_get_frame(data_in, read_offset),
                data_frame && data_frame->df_size - data_frame->df_read_off)
        read_offset += data_frame->df_size - data_frame->df_read_off;

    return read_offset - starting_offset;
}


static const struct data_in_iface di_if_hash = {
    .di_destroy      = hash_di_destroy,
    .di_dump_state   = hash_di_dump_state,
    .di_empty        = hash_di_empty,
    .di_frame_done   = hash_di_frame_done,
    .di_get_frame    = hash_di_get_frame,
    .di_insert_frame = hash_di_insert_frame,
    .di_mem_used     = hash_di_mem_used,
    .di_own_on_ok    = 0,
    .di_readable_bytes
                     = hash_di_readable_bytes,
    .di_switch_impl  = hash_di_switch_impl,
};

static const struct data_in_iface *di_if_hash_ptr = &di_if_hash;
