// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
//
// Efficient XDR implementation almost compatible with rpcgen (see run-rpcgen.sh)

// XDR in a nutshell:
//
// int: big endian 32bit
// unsigned: BE 32bit
// enum: BE 32bit
// bool: BE 32bit 0/1
// hyper: BE 64bit
// unsigned hyper: BE 64bit
// float: BE float
// double: BE double
// quadruple: BE long double
// opaque[n] (fixed-length): bytes, padded to !(n%4)
// opaque (variable-length): BE 32bit length, then n bytes, padded to !(n%4)
// string: same as opaque
// array<T>[n] (fixed-length): n items of type T
// vector<T> (variable-length): BE 32bit length, then n items of type T
// struct: components in the same order as specified
// union: BE 32bit variant id, then variant of the union
// void: nothing (empty, 0 byte data)
// optional (XDR T*): BE 32bit 1/0, then T or nothing
// linked list: sequence of optional entries
//
// RPC over TCP:
//
// BE 32bit length, then rpc_msg, then the procedure message itself
//
// RPC over RDMA:
// RFC 8166 - Remote Direct Memory Access Transport for Remote Procedure Call Version 1
// RFC 8267 - Network File System (NFS) Upper-Layer Binding to RPC-over-RDMA Version 1
// RFC 8797 - Remote Direct Memory Access - Connection Manager (RDMA-CM) Private Data for RPC-over-RDMA Version 1
// message is received in an RDMA Receive operation
// message: list of read chunks, list of write chunks, optional reply write chunk, then actual RPC body if present
// read chunk: BE 32bit position, BE 32bit registered memory key, BE 32bit length, BE 64bit offset
// write chunk: BE 32bit registered memory key, BE 32bit length, BE 64bit offset
// in reality for NFS 3.0: only 1 read chunk in write3 and symlink3, only 1 write chunk in read3 and readlink3
// read chunk is read by the server using RDMA Read from the client memory after receiving RPC request
// write chunk is pushed by the server using RDMA Write to the client memory before sending RPC reply
// connection is established using RDMA-CM at default port 20049

#pragma once

#include "xdr_impl.h"

#include <string.h>
#include <endian.h>
#include <assert.h>
#include <vector>

#include "malloc_or_die.h"

#define FALSE 0
#define TRUE 1
#define XDR_ENCODE 0
#define XDR_DECODE 1
#define BYTES_PER_XDR_UNIT 4
#define IXDR_PUT_U_LONG(a, b)
#define IXDR_GET_U_LONG(a) 0
#define IXDR_PUT_BOOL(a, b)
#define IXDR_GET_BOOL(a) 0
#define XDR_INLINE(xdrs, len) NULL

struct xdr_linked_list_t
{
    xdrproc_t fn;
    unsigned entry_size, size, cap;
    void *base;
    unsigned has_next, link_offset;
};

struct XDR
{
    int x_op;
    bool rdma = false;
    void *rdma_chunk = NULL;
    bool rdma_chunk_used = false;

    // For decoding:
    uint8_t *buf = NULL;
    unsigned avail = 0;
    std::vector<void*> allocs;
    std::vector<xdr_linked_list_t> in_linked_list;

    // For encoding:
    std::vector<uint8_t> cur_out;
    unsigned last_end = 0;
    std::vector<iovec> buf_list;
};

uint32_t inline len_pad4(uint32_t len)
{
    return ((len+3)/4) * 4;
}

inline int xdr_opaque(XDR *xdrs, void *data, uint32_t len)
{
    if (len <= 0)
    {
        return 1;
    }
    if (xdrs->x_op == XDR_DECODE)
    {
        uint32_t padded = len_pad4(len);
        if (xdrs->avail < padded)
            return 0;
        memcpy(data, xdrs->buf, len);
        xdrs->buf += padded;
        xdrs->avail -= padded;
    }
    else
    {
        unsigned old = xdrs->cur_out.size();
        uint32_t pad = (len & 3) ? (4 - (len & 3)) : 0;
        xdrs->cur_out.resize(old + len + pad);
        memcpy(xdrs->cur_out.data()+old, data, len);
        for (uint32_t i = 0; i < pad; i++)
            xdrs->cur_out[old+i] = 0;
    }
    return 1;
}

inline int xdr_bytes(XDR *xdrs, xdr_string_t *data, uint32_t maxlen, bool rdma_chunk = false)
{
    if (xdrs->x_op == XDR_DECODE)
    {
        if (xdrs->avail < 4)
            return 0;
        uint32_t len = be32toh(*((uint32_t*)xdrs->buf));
        if (rdma_chunk && xdrs->rdma && xdrs->rdma_chunk)
        {
            // Take (only a single) RDMA chunk from xdrs->rdma_chunk while decoding
            assert(!xdrs->rdma_chunk_used);
            xdrs->rdma_chunk_used = true;
            data->data = (char*)xdrs->rdma_chunk;
            data->size = len;
            return 1;
        }
        uint32_t padded = len_pad4(len);
        if (xdrs->avail < 4+padded)
            return 0;
        data->size = len;
        data->data = (char*)(xdrs->buf+4);
        xdrs->buf += 4+padded;
        xdrs->avail -= 4+padded;
    }
    else
    {
        // Always encode RDMA chunks as separate iovecs
        if (data->size < XDR_COPY_LENGTH && (!rdma_chunk || !xdrs->rdma))
        {
            unsigned old = xdrs->cur_out.size();
            xdrs->cur_out.resize(old + 4+data->size);
            *(uint32_t*)(xdrs->cur_out.data() + old) = htobe32(data->size);
            memcpy(xdrs->cur_out.data()+old+4, data->data, data->size);
        }
        else
        {
            unsigned old = xdrs->cur_out.size();
            xdrs->cur_out.resize(old + 4);
            *(uint32_t*)(xdrs->cur_out.data() + old) = htobe32(data->size);
            xdrs->buf_list.push_back((iovec){
                .iov_base = 0,
                .iov_len = xdrs->cur_out.size() - xdrs->last_end,
            });
            xdrs->last_end = xdrs->cur_out.size();
            xdrs->buf_list.push_back((iovec)
            {
                .iov_base = (void*)data->data,
                .iov_len = data->size,
            });
        }
        if ((data->size & 3) && (!rdma_chunk || !xdrs->rdma))
        {
            // No padding for RDMA chunks
            int pad = 4-(data->size & 3);
            unsigned old = xdrs->cur_out.size();
            xdrs->cur_out.resize(old+pad);
            for (int i = 0; i < pad; i++)
                xdrs->cur_out[old+i] = 0;
        }
    }
    return 1;
}

inline int xdr_string(XDR *xdrs, xdr_string_t *data, uint32_t maxlen, bool rdma_chunk = false)
{
    return xdr_bytes(xdrs, data, maxlen, rdma_chunk);
}

inline int xdr_u_int(XDR *xdrs, void *data)
{
    if (xdrs->x_op == XDR_DECODE)
    {
        if (xdrs->avail < 4)
            return 0;
        *((uint32_t*)data) = be32toh(*((uint32_t*)xdrs->buf));
        xdrs->buf += 4;
        xdrs->avail -= 4;
    }
    else
    {
        unsigned old = xdrs->cur_out.size();
        xdrs->cur_out.resize(old + 4);
        *(uint32_t*)(xdrs->cur_out.data() + old) = htobe32(*(uint32_t*)data);
    }
    return 1;
}

inline int xdr_uint32_t(XDR *xdrs, void *data)
{
    return xdr_u_int(xdrs, data);
}

inline int xdr_enum(XDR *xdrs, void *data)
{
    return xdr_u_int(xdrs, data);
}

inline int xdr_bool(XDR *xdrs, void *data)
{
    return xdr_u_int(xdrs, data);
}

inline int xdr_uint64_t(XDR *xdrs, void *data)
{
    if (xdrs->x_op == XDR_DECODE)
    {
        if (xdrs->avail < 8)
            return 0;
        *((uint64_t*)data) = be64toh(*((uint64_t*)xdrs->buf));
        xdrs->buf += 8;
        xdrs->avail -= 8;
    }
    else
    {
        unsigned old = xdrs->cur_out.size();
        xdrs->cur_out.resize(old + 8);
        *(uint64_t*)(xdrs->cur_out.data() + old) = htobe64(*(uint64_t*)data);
    }
    return 1;
}

// Parse inconvenient shitty linked lists as arrays
inline int xdr_pointer(XDR *xdrs, char **data, unsigned entry_size, xdrproc_t entry_fn)
{
    if (xdrs->x_op == XDR_DECODE)
    {
        if (xdrs->avail < 4)
            return 0;
        uint32_t has_next = be32toh(*((uint32_t*)xdrs->buf));
        xdrs->buf += 4;
        xdrs->avail -= 4;
        *data = NULL;
        if (!xdrs->in_linked_list.size() ||
            xdrs->in_linked_list.back().fn != entry_fn)
        {
            if (has_next)
            {
                unsigned cap = 2;
                void *base = malloc_or_die(entry_size * cap);
                xdrs->in_linked_list.push_back((xdr_linked_list_t){
                    .fn = entry_fn,
                    .entry_size = entry_size,
                    .size = 1,
                    .cap = cap,
                    .base = base,
                    .has_next = 0,
                    .link_offset = 0,
                });
                *data = (char*)base;
                if (!entry_fn(xdrs, base))
                    return 0;
                auto & ll = xdrs->in_linked_list.back();
                while (ll.has_next)
                {
                    ll.has_next = 0;
                    if (ll.size >= ll.cap)
                    {
                        ll.cap *= 2;
                        ll.base = realloc_or_die(ll.base, ll.entry_size * ll.cap);
                    }
                    if (!entry_fn(xdrs, (uint8_t*)ll.base + ll.entry_size*ll.size))
                        return 0;
                    ll.size++;
                }
                for (unsigned i = 0; i < ll.size-1; i++)
                {
                    *(void**)((uint8_t*)ll.base + i*ll.entry_size + ll.link_offset) =
                        (uint8_t*)ll.base + (i+1)*ll.entry_size;
                }
                *data = (char*)ll.base;
                xdrs->allocs.push_back(ll.base);
                xdrs->in_linked_list.pop_back();
            }
        }
        else
        {
            auto & ll = xdrs->in_linked_list.back();
            xdrs->in_linked_list.back().has_next = has_next;
            xdrs->in_linked_list.back().link_offset = (uint8_t*)data - (uint8_t*)ll.base - ll.entry_size*ll.size;
        }
    }
    else
    {
        unsigned old = xdrs->cur_out.size();
        xdrs->cur_out.resize(old + 4);
        *(uint32_t*)(xdrs->cur_out.data() + old) = htobe32(*data ? 1 : 0);
        if (*data)
            entry_fn(xdrs, *data);
    }
    return 1;
}

inline int xdr_array(XDR *xdrs, char **data, uint32_t* len, uint32_t maxlen, uint32_t entry_size, xdrproc_t fn)
{
    if (xdrs->x_op == XDR_DECODE)
    {
        if (xdrs->avail < 4)
            return 0;
        *len = be32toh(*((uint32_t*)xdrs->buf));
        if (*len > maxlen)
            return 0;
        xdrs->buf += 4;
        xdrs->avail -= 4;
        *data = (char*)malloc_or_die(entry_size * (*len));
        for (uint32_t i = 0; i < *len; i++)
            fn(xdrs, *data + entry_size*i);
        xdrs->allocs.push_back(*data);
    }
    else
    {
        unsigned old = xdrs->cur_out.size();
        xdrs->cur_out.resize(old + 4);
        *(uint32_t*)(xdrs->cur_out.data() + old) = htobe32(*len);
        for (uint32_t i = 0; i < *len; i++)
            fn(xdrs, *data + entry_size*i);
    }
    return 1;
}
