#pragma once

#include "config.h"
#include "common.h"
#include "hpack.h"

#ifdef __cplusplus
extern "C" {
#endif

enum CNO_PEER_KIND {
    CNO_REMOTE,
    CNO_LOCAL,
};

enum CNO_CONNECTION_KIND {
    CNO_SERVER,
    CNO_CLIENT,
};

enum CNO_HTTP_VERSION {
    CNO_HTTP1,
    CNO_HTTP2,
};

enum CNO_FRAME_TYPE {
    CNO_FRAME_DATA          = 0x0,
    CNO_FRAME_HEADERS       = 0x1,
    CNO_FRAME_PRIORITY      = 0x2,
    CNO_FRAME_RST_STREAM    = 0x3,
    CNO_FRAME_SETTINGS      = 0x4,
    CNO_FRAME_PUSH_PROMISE  = 0x5,
    CNO_FRAME_PING          = 0x6,
    CNO_FRAME_GOAWAY        = 0x7,
    CNO_FRAME_WINDOW_UPDATE = 0x8,
    CNO_FRAME_CONTINUATION  = 0x9,
    CNO_FRAME_UNKNOWN       = 0xa,
};

enum CNO_RST_STREAM_CODE {
    CNO_RST_NO_ERROR            = 0x0,
    CNO_RST_PROTOCOL_ERROR      = 0x1,
    CNO_RST_INTERNAL_ERROR      = 0x2,
    CNO_RST_FLOW_CONTROL_ERROR  = 0x3,
    CNO_RST_SETTINGS_TIMEOUT    = 0x4,
    CNO_RST_STREAM_CLOSED       = 0x5,
    CNO_RST_FRAME_SIZE_ERROR    = 0x6,
    CNO_RST_REFUSED_STREAM      = 0x7,
    CNO_RST_CANCEL              = 0x8,
    CNO_RST_COMPRESSION_ERROR   = 0x9,
    CNO_RST_CONNECT_ERROR       = 0xa,
    CNO_RST_ENHANCE_YOUR_CALM   = 0xb,
    CNO_RST_INADEQUATE_SECURITY = 0xc,
    CNO_RST_HTTP_1_1_REQUIRED   = 0xd,
};

enum CNO_FRAME_FLAGS {
    CNO_FLAG_ACK         = 0x1,
    CNO_FLAG_END_STREAM  = 0x1,
    CNO_FLAG_END_HEADERS = 0x4,
    CNO_FLAG_PADDED      = 0x8,
    CNO_FLAG_PRIORITY    = 0x20,
};

enum CNO_CONNECTION_SETTINGS {
    CNO_SETTINGS_HEADER_TABLE_SIZE       = 0x1,
    CNO_SETTINGS_ENABLE_PUSH             = 0x2,
    CNO_SETTINGS_MAX_CONCURRENT_STREAMS  = 0x3,
    CNO_SETTINGS_INITIAL_WINDOW_SIZE     = 0x4,
    CNO_SETTINGS_MAX_FRAME_SIZE          = 0x5,
    CNO_SETTINGS_MAX_HEADER_LIST_SIZE    = 0x6,
    CNO_SETTINGS_UNKNOWN_1               = 0x7,
    CNO_SETTINGS_ENABLE_CONNECT_PROTOCOL = 0x8,
    CNO_SETTINGS_UNDEFINED               = 0x9,
};

struct cno_frame_t {
    uint8_t /* enum CNO_FRAME_TYPE  */ type;
    uint8_t /* enum CNO_FRAME_FLAGS */ flags;
    uint32_t stream;
    struct cno_buffer_t payload;
};

struct cno_message_t {
    int code;
    struct cno_buffer_t method; // reason string in HTTP/1.x mode responses
    struct cno_buffer_t path;
    struct cno_header_t *headers;
    size_t headers_len;
};

struct cno_tail_t {
    struct cno_header_t *headers;
    size_t headers_len;
};

struct cno_stream_t;

struct cno_settings_t {
    union {
        // TODO implement this in a way not dependent on alignment
        // TODO extensions
        struct {
            uint32_t header_table_size;
            uint32_t enable_push;
            uint32_t max_concurrent_streams;
            uint32_t initial_window_size;
            uint32_t max_frame_size;
            uint32_t max_header_list_size;
            uint32_t unknown;
            uint32_t enable_connect_protocol;
        };
        uint32_t array[8];
    };
};

struct cno_vtable_t {
    // There is something to send to the other side. Transport level is outside
    // the scope of this library. NOTE: calls to `on_writev` must be atomic w.r.t.
    // each other so that frames don't mix.
    int (*on_writev)(void *, const struct cno_buffer_t *, size_t count);
    // The transport should be closed and all further writes discarded.
    int (*on_close)(void *);
    // A new stream has been created due to sending/receiving a request or sending
    // a push promise. In the latter two cases, `on_message_head` will be called
    // shortly afterwards.
    int (*on_stream_start)(void *, uint32_t id);
    // Either a response has been sent/received fully, or the stream has been reset.
    int (*on_stream_end)(void *, uint32_t id, uint32_t code, enum CNO_PEER_KIND);
    // The other side has signaled that it is willing to accept more data.
    // There is a global limit (shared between all streams) and one for each stream;
    // `cno_write_data` will send as much data as the lowest of the two allows.
    // When the global limit is updated, this function is called with stream id = 0.
    int (*on_flow_increase)(void *, uint32_t id);
    // A request/response has been received (depending on whether this is a server
    // connection or not). Each stream carries exactly one request/response pair.
    int (*on_message_head)(void *, uint32_t id, const struct cno_message_t *);
    // Client only: server is intending to push a response to a request that
    // it anticipates in advance.
    int (*on_message_push)(void *, uint32_t id, const struct cno_message_t *, uint32_t parent);
    // A chunk of the payload has arrived.
    int (*on_message_data)(void *, uint32_t id, const char *, size_t);
    // All chunks of the payload (and possibly trailers) have arrived.
    int (*on_message_tail)(void *, uint32_t id, const struct cno_tail_t * /* NULL if none */);
    // An HTTP 2 frame has been received.
    int (*on_frame)(void *, const struct cno_frame_t *);
    // An HTTP 2 frame will be sent with `on_writev` soon.
    int (*on_frame_send)(void *, const struct cno_frame_t *);
    // An acknowledgment of one of the previously sent pings has arrived.
    int (*on_pong)(void *, const char[8]);
    // New connection-wide settings have been chosen by the peer.
    int (*on_settings)(void *);
    // HTTP 1 server only: the previous request (see `on_message_head`) has requested
    // an upgrade to a different protocol. If `cno_write_head` is called with code 101
    // before this function returns, the connection will be proxied as payload for
    // the last created stream (read -> on_message_data, cno_write_data -> write).
    // Otherwise, the upgrade is ignored.
    int (*on_upgrade)(void *, uint32_t id);
};

struct cno_connection_t {
// public:
    const struct cno_vtable_t *cb_code;
    // Passed as the first argument to all callbacks.
    void *cb_data;
    // Disable automatic sending of stream WINDOW_UPDATEs after receiving DATA; application
    // must call `cno_open_flow` after processing a chunk from `on_message_data`.
    uint8_t manual_flow_control : 1;
    // Disable special handling of the "Upgrade: h2c" header in HTTP/1.x mode.
    // NOTE: this is set by default because:
    //   1. when using tls, you *have* to set this to be compliant;
    //   2. most browsers/servers only support h2 over tls anyway;
    //   3. weird things may happen if you reset stream 1 without consuming the whole payload.
    uint8_t disallow_h2_upgrade : 1;
    // Disable special handling of the HTTP2 preface in HTTP/1.x mode.
    uint8_t disallow_h2_prior_knowledge : 1;
    // Whether `cno_init` was called with `CNO_CLIENT`.
    uint8_t /* read-only */ client : 1;
    // Whether `cno_begin` was called with `CNO_HTTP2` or an upgrade has beed performed.
    uint8_t /* read-only */ mode : 1;

// private:
    uint8_t  state;
    uint8_t  recently_reset_next;
    uint32_t recently_reset[CNO_STREAM_RESET_HISTORY];
    uint32_t last_stream[2]; // dereferencable with CNO_REMOTE/CNO_LOCAL
    uint32_t stream_count[2];
    uint32_t goaway[2]; // sent by {them, us}
    int64_t  window[2]; // advertised by {them, us}
    struct cno_settings_t settings[2];
    struct cno_buffer_dyn_t buffer;
    struct cno_hpack_t decoder;
    struct cno_hpack_t encoder;
    struct cno_stream_t *streams[CNO_STREAM_BUCKETS];
};

// Initialize a freshly constructed connection object. (Set up the callbacks after this.)
void cno_init(struct cno_connection_t *, enum CNO_CONNECTION_KIND);

// Free all resources associated with a connection. *Does not emit events*: if you store
// additional per-stream data, discard it.
void cno_fini(struct cno_connection_t *);

// Begin the message exchange using the negotiated protocol.
int cno_begin(struct cno_connection_t *, enum CNO_HTTP_VERSION);

// Handle some new data from the transport level. In h1 mode, this function may return
// CNO_ERRNO_WOULD_BLOCK; this means you need to handle some pipelined requests
// before accepting more of them (or raise the concurrent stream limit). In that case,
// the provided data is buffered and must not be passed again. Pass an empty chunk
// to have another go at parsing the buffered data if you've nothing to add.
int cno_consume(struct cno_connection_t *, const char *, size_t);

// Handle an EOF from a half-closed transport. Half-closed connections are not supported
// because they behave really inconveniently (e.g. you can't detect RST unless you write).
int cno_eof(struct cno_connection_t *);

// Gracefully shut down the connection. (After calling this, wait for remaining streams
// to end, then close the transport.)
int cno_shutdown(struct cno_connection_t *);

// Set a new configuration for HTTP 2. (The current one can be read as `c->settings[CNO_LOCAL]`.)
int cno_configure(struct cno_connection_t *, const struct cno_settings_t *);

// Obtain a new stream id to send a request with.
uint32_t cno_next_stream(const struct cno_connection_t *);

// Client: send a new request. Server: respond to a previously received one. If `final` is 0
// (only allowed for requests and non-1xx responses), one or more calls to `cno_write_data`
// must follow. For a 1xx response, call this function again with a proper code later.
// 101 responses must only be sent while `cno_consume` is blocked in `on_message_head`
// or `on_upgrade` of an HTTP 1 connection; see `on_upgrade`. This function may return
// CNO_ERRNO_WOULD_BLOCK if you've created too many streams already (client) or there
// are pipelined h1 requests that need to be handled before this one (server).
int cno_write_head(struct cno_connection_t *, uint32_t stream, const struct cno_message_t *, int final);

// Server: initiate a request in anticipation of the client doing it anyway. This should
// only be done for safe requests without payload. The function will silently do nothing
// if the client uses HTTP 1 or has specified that it does not want push messages.
int cno_write_push(struct cno_connection_t *, uint32_t stream, const struct cno_message_t *);

// Attach more data to the previously sent message. If `final` is 0, more calls must follow.
// Returns -1 on error, else the number of sent bytes, which may be less than requested
// due to HTTP 2 flow control. If that's the case, wait for an `on_flow_increase` on
// the same stream (or on stream 0) before retrying.
int cno_write_data(struct cno_connection_t *, uint32_t stream, const char *, size_t, int final);

// Write the trailers and close the stream. In h1 mode, only works if `content-length`
// was not specified due to protocol limitations; otherwise, the trailers are silently
// dropped. (The standard says they're optional!) For `NULL` or an empty list of trailers,
// equivalent to `cno_write_data(conn, stream, NULL, 0, 1)`.
int cno_write_tail(struct cno_connection_t *, uint32_t stream, const struct cno_tail_t *);

// Reject a stream. Has no effect in HTTP 1 mode (in which case you should simply
// close the transport) or if the stream has already finished because a response
// or another reset has been received/sent.
int cno_write_reset(struct cno_connection_t *, uint32_t stream, enum CNO_RST_STREAM_CODE);

// Send a ping with some data. See also `on_pong`. Only works in HTTP 2 mode.
int cno_write_ping(struct cno_connection_t *, const char[8]);

// Send a raw HTTP 2 frame. Only works in HTTP 2 mode, and only for extension frames.
int cno_write_frame(struct cno_connection_t *, const struct cno_frame_t *);

// Increase the flow window by the specified amount, allowing the peer to send more data.
//
// NOTE: if manual flow control is disabled, the window size is kept constant by increasing
//       it before emitting `on_message_data`. This function can still be used to make
//       the window bigger than the default.
int cno_open_flow(struct cno_connection_t *, uint32_t stream, uint32_t delta);

#ifdef __cplusplus
}  // extern "C"
#endif
