/**
 * @file addons/http.c
 * @brief HTTP addon.
 *
 * This is a heavily modified version of the EmbeddableWebServer (see copyright
 * below). This version has been stripped from everything not strictly necessary
 * for receiving/replying to simple HTTP requests, and has been modified to use
 * the Flecs OS API.
 *
 * EmbeddableWebServer Copyright (c) 2016, 2019, 2020 Forrest Heller, and 
 * CONTRIBUTORS (see below) - All rights reserved.
 *
 * CONTRIBUTORS:
 * Martin Pulec - bug fixes, warning fixes, IPv6 support
 * Daniel Barry - bug fix (ifa_addr != NULL)
 * 
 * Released under the BSD 2-clause license:
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution. THIS SOFTWARE IS 
 * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 
 * NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "../private_api.h"

#ifdef FLECS_HTTP

#ifdef ECS_TARGET_MSVC
#pragma comment(lib, "Ws2_32.lib")
#endif

#if defined(ECS_TARGET_WINDOWS)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
typedef SOCKET ecs_http_socket_t;
#else
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <strings.h>
#include <signal.h>
#include <fcntl.h>
#ifdef __FreeBSD__
#include <netinet/in.h>
#endif
typedef int ecs_http_socket_t;

#if !defined(MSG_NOSIGNAL)
#define MSG_NOSIGNAL (0)
#endif

#endif

/* Max length of request method */
#define ECS_HTTP_METHOD_LEN_MAX (8) 

/* Timeout (s) before connection purge */
#define ECS_HTTP_CONNECTION_PURGE_TIMEOUT (1.0)

/* Number of dequeues before purging */
#define ECS_HTTP_CONNECTION_PURGE_RETRY_COUNT (5)

/* Number of retries receiving request */
#define ECS_HTTP_REQUEST_RECV_RETRY (10)

/* Minimum interval between dequeueing requests (ms) */
#define ECS_HTTP_MIN_DEQUEUE_INTERVAL (50)

/* Minimum interval between printing statistics (ms) */
#define ECS_HTTP_MIN_STATS_INTERVAL (10 * 1000)

/* Receive buffer size */
#define ECS_HTTP_SEND_RECV_BUFFER_SIZE (16 * 1024)

/* Max length of request (path + query + headers + body) */
#define ECS_HTTP_REQUEST_LEN_MAX (10 * 1024 * 1024)

/* Total number of outstanding send requests */
#define ECS_HTTP_SEND_QUEUE_MAX (256)

/* Cache invalidation timeout (s) */
#define ECS_HTTP_CACHE_TIMEOUT ((ecs_ftime_t)1.0)

/* Cache entry purge timeout (s) */
#define ECS_HTTP_CACHE_PURGE_TIMEOUT ((ecs_ftime_t)10.0)

/* Global statistics */
int64_t ecs_http_request_received_count = 0;
int64_t ecs_http_request_invalid_count = 0;
int64_t ecs_http_request_handled_ok_count = 0;
int64_t ecs_http_request_handled_error_count = 0;
int64_t ecs_http_request_not_handled_count = 0;
int64_t ecs_http_request_preflight_count = 0;
int64_t ecs_http_send_ok_count = 0;
int64_t ecs_http_send_error_count = 0;
int64_t ecs_http_busy_count = 0;

/* Send request queue */
typedef struct ecs_http_send_request_t {
    ecs_http_socket_t sock;
    char *headers;
    int32_t header_length;
    char *content;
    int32_t content_length;
} ecs_http_send_request_t;

typedef struct ecs_http_send_queue_t {
    ecs_http_send_request_t requests[ECS_HTTP_SEND_QUEUE_MAX];
    int32_t head;
    int32_t tail;
    ecs_os_thread_t thread;
    int32_t wait_ms;
} ecs_http_send_queue_t;

typedef struct ecs_http_request_key_t {
    const char *array;
    ecs_size_t count;
} ecs_http_request_key_t;

typedef struct ecs_http_request_entry_t {
    char *content;
    int32_t content_length;
    ecs_ftime_t time;
} ecs_http_request_entry_t;

/* HTTP server struct */
struct ecs_http_server_t {
    bool should_run;
    bool running;

    ecs_http_socket_t sock;
    ecs_os_mutex_t lock;
    ecs_os_thread_t thread;

    ecs_http_reply_action_t callback;
    void *ctx;

    ecs_sparse_t connections; /* sparse<http_connection_t> */
    ecs_sparse_t requests; /* sparse<http_request_t> */

    bool initialized;

    uint16_t port;
    const char *ipaddr;

    double dequeue_timeout; /* used to not lock request queue too often */
    double stats_timeout; /* used for periodic reporting of statistics */

    double request_time; /* time spent on requests in last stats interval */
    double request_time_total; /* total time spent on requests */
    int32_t requests_processed; /* requests processed in last stats interval */
    int32_t requests_processed_total; /* total requests processed */
    int32_t dequeue_count; /* number of dequeues in last stats interval */ 
    ecs_http_send_queue_t send_queue;

    ecs_hashmap_t request_cache;
};

/** Fragment state, used by HTTP request parser */
typedef enum  {
    HttpFragStateBegin,
    HttpFragStateMethod,
    HttpFragStatePath,
    HttpFragStateVersion,
    HttpFragStateHeaderStart,
    HttpFragStateHeaderName,
    HttpFragStateHeaderValueStart,
    HttpFragStateHeaderValue,
    HttpFragStateCR,
    HttpFragStateCRLF,
    HttpFragStateCRLFCR,
    HttpFragStateBody,
    HttpFragStateDone
} HttpFragState;

/** A fragment is a partially received HTTP request */
typedef struct {
    HttpFragState state;
    ecs_strbuf_t buf;
    ecs_http_method_t method;
    int32_t body_offset;
    int32_t query_offset;
    int32_t header_offsets[ECS_HTTP_HEADER_COUNT_MAX];
    int32_t header_value_offsets[ECS_HTTP_HEADER_COUNT_MAX];
    int32_t header_count;
    int32_t param_offsets[ECS_HTTP_QUERY_PARAM_COUNT_MAX];
    int32_t param_value_offsets[ECS_HTTP_QUERY_PARAM_COUNT_MAX];
    int32_t param_count;
    int32_t content_length;
    char *header_buf_ptr;
    char header_buf[32];
    bool parse_content_length;
    bool invalid;
} ecs_http_fragment_t;

/** Extend public connection type with fragment data */
typedef struct {
    ecs_http_connection_t pub;
    ecs_http_socket_t sock;

    /* Connection is purged after both timeout expires and connection has
     * exceeded retry count. This ensures that a connection does not immediately
     * timeout when a frame takes longer than usual */
    double dequeue_timeout;
    int32_t dequeue_retries;    
} ecs_http_connection_impl_t;

typedef struct {
    ecs_http_request_t pub;
    uint64_t conn_id; /* for sanity check */
    char *res;
    int32_t req_len;
} ecs_http_request_impl_t;

static
ecs_size_t http_send(
    ecs_http_socket_t sock, 
    const void *buf, 
    ecs_size_t size, 
    int flags)
{
    ecs_assert(size >= 0, ECS_INTERNAL_ERROR, NULL);
#ifdef ECS_TARGET_POSIX
    ssize_t send_bytes = send(sock, buf, flecs_itosize(size), 
        flags | MSG_NOSIGNAL);
    return flecs_itoi32(send_bytes);
#else
    int send_bytes = send(sock, buf, size, flags);
    return flecs_itoi32(send_bytes);
#endif
}

static
ecs_size_t http_recv(
    ecs_http_socket_t sock,
    void *buf,
    ecs_size_t size,
    int flags)
{
    ecs_size_t ret;
#ifdef ECS_TARGET_POSIX
    ssize_t recv_bytes = recv(sock, buf, flecs_itosize(size), flags);
    ret = flecs_itoi32(recv_bytes);
#else
    int recv_bytes = recv(sock, buf, size, flags);
    ret = flecs_itoi32(recv_bytes);
#endif
    if (ret == -1) {
        ecs_dbg("recv failed: %s (sock = %d)", ecs_os_strerror(errno), sock);
    } else if (ret == 0) {
        ecs_dbg("recv: received 0 bytes (sock = %d)", sock);
    }

    return ret;
}

static
void http_sock_set_timeout(
    ecs_http_socket_t sock,
    int32_t timeout_ms)
{
    int r;
#ifdef ECS_TARGET_POSIX
    struct timeval tv;
    tv.tv_sec = timeout_ms * 1000;
    tv.tv_usec = 0;
    r = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
#else
    DWORD t = (DWORD)timeout_ms;
    r = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&t, sizeof t);
#endif
    if (r) {
        ecs_warn("http: failed to set socket timeout: %s", 
            ecs_os_strerror(errno));
    }
}

static
void http_sock_keep_alive(
    ecs_http_socket_t sock)
{
    int v = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (const char*)&v, sizeof v)) {
        ecs_warn("http: failed to set socket KEEPALIVE: %s",
            ecs_os_strerror(errno));
    }
}

static
void http_sock_nonblock(ecs_http_socket_t sock, bool enable) {
    (void)sock;
    (void)enable;
#ifdef ECS_TARGET_POSIX
    int flags;
    flags = fcntl(sock,F_GETFL,0);
    if (flags == -1) {
        ecs_warn("http: failed to set socket NONBLOCK: %s",
            ecs_os_strerror(errno));
        return;
    }
    if (enable) {
        flags = fcntl(sock, F_SETFL, flags | O_NONBLOCK);
    } else {
        flags = fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
    }
    if (flags == -1) {
        ecs_warn("http: failed to set socket NONBLOCK: %s",
            ecs_os_strerror(errno));
        return;
    }
#endif
}

static
int http_getnameinfo(
    const struct sockaddr* addr,
    ecs_size_t addr_len,
    char *host,
    ecs_size_t host_len,
    char *port,
    ecs_size_t port_len,
    int flags)
{
    ecs_assert(addr_len > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(host_len > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(port_len > 0, ECS_INTERNAL_ERROR, NULL);
#if defined(ECS_TARGET_WINDOWS)
    return getnameinfo(addr, addr_len, host, 
        flecs_ito(uint32_t, host_len), port, flecs_ito(uint32_t, port_len), 
        flags);
#else
    return getnameinfo(addr, flecs_ito(uint32_t, addr_len), host, 
        flecs_ito(uint32_t, host_len), port, flecs_ito(uint32_t, port_len), 
        flags);
#endif
}

static
int http_bind(
    ecs_http_socket_t sock,
    const struct sockaddr* addr,
    ecs_size_t addr_len)
{
    ecs_assert(addr_len > 0, ECS_INTERNAL_ERROR, NULL);
#if defined(ECS_TARGET_WINDOWS)
    return bind(sock, addr, addr_len);
#else
    return bind(sock, addr, flecs_ito(uint32_t, addr_len));
#endif
}

static
bool http_socket_is_valid(
    ecs_http_socket_t sock)
{
#if defined(ECS_TARGET_WINDOWS)
    return sock != INVALID_SOCKET;
#else
    return sock >= 0;
#endif
}

#if defined(ECS_TARGET_WINDOWS)
#define HTTP_SOCKET_INVALID INVALID_SOCKET
#else
#define HTTP_SOCKET_INVALID (-1)
#endif

static
void http_close(
    ecs_http_socket_t *sock)
{
    ecs_assert(sock != NULL, ECS_INTERNAL_ERROR, NULL);

#if defined(ECS_TARGET_WINDOWS)
    closesocket(*sock);
#else
    ecs_dbg_2("http: closing socket %u", *sock);
    shutdown(*sock, SHUT_RDWR);
    close(*sock);
#endif
    *sock = HTTP_SOCKET_INVALID;
}

static
ecs_http_socket_t http_accept(
    ecs_http_socket_t sock,
    struct sockaddr* addr,
    ecs_size_t *addr_len)
{
    socklen_t len = (socklen_t)addr_len[0];
    ecs_http_socket_t result = accept(sock, addr, &len);
    addr_len[0] = (ecs_size_t)len;
    return result;
}

static
void http_reply_fini(ecs_http_reply_t* reply) {
    ecs_assert(reply != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(reply->body.content);
}

static
void http_request_fini(ecs_http_request_impl_t *req) {
    ecs_assert(req != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->server != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->id == req->conn_id, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(req->res);
    flecs_sparse_remove_t(&req->pub.conn->server->requests, 
        ecs_http_request_impl_t, req->pub.id);
}

static
void http_connection_free(ecs_http_connection_impl_t *conn) {
    ecs_assert(conn != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(conn->pub.id != 0, ECS_INTERNAL_ERROR, NULL);
    uint64_t conn_id = conn->pub.id;

    if (http_socket_is_valid(conn->sock)) {
        http_close(&conn->sock);
    }

    flecs_sparse_remove_t(&conn->pub.server->connections, 
        ecs_http_connection_impl_t, conn_id);
}

// https://stackoverflow.com/questions/10156409/convert-hex-string-char-to-int
static
char http_hex_2_int(char a, char b){
    a = (a <= '9') ? (char)(a - '0') : (char)((a & 0x7) + 9);
    b = (b <= '9') ? (char)(b - '0') : (char)((b & 0x7) + 9);
    return (char)((a << 4) + b);
}

static
void http_decode_url_str(
    char *str) 
{
    char ch, *ptr, *dst = str;
    for (ptr = str; (ch = *ptr); ptr++) {
        if (ch == '%') {
            dst[0] = http_hex_2_int(ptr[1], ptr[2]);
            dst ++;
            ptr += 2;
        } else {
            dst[0] = ptr[0];
            dst ++;
        }
    }
    dst[0] = '\0';
}

static
void http_parse_method(
    ecs_http_fragment_t *frag)
{
    char *method = ecs_strbuf_get_small(&frag->buf);
    if (!ecs_os_strcmp(method, "GET")) frag->method = EcsHttpGet;
    else if (!ecs_os_strcmp(method, "POST")) frag->method = EcsHttpPost;
    else if (!ecs_os_strcmp(method, "PUT")) frag->method = EcsHttpPut;
    else if (!ecs_os_strcmp(method, "DELETE")) frag->method = EcsHttpDelete;
    else if (!ecs_os_strcmp(method, "OPTIONS")) frag->method = EcsHttpOptions;
    else {
        frag->method = EcsHttpMethodUnsupported;
        frag->invalid = true;
    }
    ecs_strbuf_reset(&frag->buf);
}

static
bool http_header_writable(
    ecs_http_fragment_t *frag)
{
    return frag->header_count < ECS_HTTP_HEADER_COUNT_MAX;
}

static
void http_header_buf_reset(
    ecs_http_fragment_t *frag)
{
    frag->header_buf[0] = '\0';
    frag->header_buf_ptr = frag->header_buf;
}

static
void http_header_buf_append(
    ecs_http_fragment_t *frag,
    char ch)
{
    if ((frag->header_buf_ptr - frag->header_buf) < 
        ECS_SIZEOF(frag->header_buf)) 
    {
        frag->header_buf_ptr[0] = ch;
        frag->header_buf_ptr ++;
    } else {
        frag->header_buf_ptr[0] = '\0';
    }
}

static
uint64_t http_request_key_hash(const void *ptr) {
    const ecs_http_request_key_t *key = ptr;
    const char *array = key->array;
    int32_t count = key->count;
    return flecs_hash(array, count * ECS_SIZEOF(char));
}

static
int http_request_key_compare(const void *ptr_1, const void *ptr_2) {
    const ecs_http_request_key_t *type_1 = ptr_1;
    const ecs_http_request_key_t *type_2 = ptr_2;

    int32_t count_1 = type_1->count;
    int32_t count_2 = type_2->count;

    if (count_1 != count_2) {
        return (count_1 > count_2) - (count_1 < count_2);
    }

    return ecs_os_memcmp(type_1->array, type_2->array, count_1);
}

static
ecs_http_request_entry_t* http_find_request_entry(
    ecs_http_server_t *srv,
    const char *array,
    int32_t count)
{
    ecs_http_request_key_t key;
    key.array = array;
    key.count = count;

    ecs_time_t t = {0, 0};
    ecs_http_request_entry_t *entry = flecs_hashmap_get(
        &srv->request_cache, &key, ecs_http_request_entry_t);

    if (entry) {
        ecs_ftime_t tf = (ecs_ftime_t)ecs_time_measure(&t);
        if ((tf - entry->time) < ECS_HTTP_CACHE_TIMEOUT) {
            return entry;
        }
    }
    return NULL;
}

static
void http_insert_request_entry(
    ecs_http_server_t *srv,
    ecs_http_request_impl_t *req,
    ecs_http_reply_t *reply)
{
    int32_t content_length = ecs_strbuf_written(&reply->body);
    if (!content_length) {
        return;
    }

    ecs_http_request_key_t key;
    key.array = req->res;
    key.count = req->req_len;
    ecs_http_request_entry_t *entry = flecs_hashmap_get(
        &srv->request_cache, &key, ecs_http_request_entry_t);
    if (!entry) {
        flecs_hashmap_result_t elem = flecs_hashmap_ensure(
            &srv->request_cache, &key, ecs_http_request_entry_t);
        ecs_http_request_key_t *elem_key = elem.key;
        elem_key->array = ecs_os_memdup_n(key.array, char, key.count);
        entry = elem.value;
    } else {
        ecs_os_free(entry->content);
    }

    ecs_time_t t = {0, 0};
    entry->time = (ecs_ftime_t)ecs_time_measure(&t);
    entry->content_length = ecs_strbuf_written(&reply->body);
    entry->content = ecs_strbuf_get(&reply->body);
    ecs_strbuf_appendstrn(&reply->body, 
            entry->content, entry->content_length);
}

static
char* http_decode_request(
    ecs_http_request_impl_t *req,
    ecs_http_fragment_t *frag)
{
    ecs_os_zeromem(req);

    char *res = ecs_strbuf_get(&frag->buf);
    if (!res) {
        return NULL;
    }

    req->pub.method = frag->method;
    req->pub.path = res + 1;
    http_decode_url_str(req->pub.path);

    if (frag->body_offset) {
        req->pub.body = &res[frag->body_offset];
    }
    int32_t i, count = frag->header_count;
    for (i = 0; i < count; i ++) {
        req->pub.headers[i].key = &res[frag->header_offsets[i]];
        req->pub.headers[i].value = &res[frag->header_value_offsets[i]];
    }
    count = frag->param_count;
    for (i = 0; i < count; i ++) {
        req->pub.params[i].key = &res[frag->param_offsets[i]];
        req->pub.params[i].value = &res[frag->param_value_offsets[i]];
        /* Safe, member is only const so that end-user can't change it */
        http_decode_url_str(ECS_CONST_CAST(char*, req->pub.params[i].value));
    }

    req->pub.header_count = frag->header_count;
    req->pub.param_count = frag->param_count;
    req->res = res;
    req->req_len = frag->header_offsets[0];

    return res;
}

static
ecs_http_request_entry_t* http_enqueue_request(
    ecs_http_connection_impl_t *conn,
    uint64_t conn_id,
    ecs_http_fragment_t *frag)
{
    ecs_http_server_t *srv = conn->pub.server;

    ecs_os_mutex_lock(srv->lock);
    bool is_alive = conn->pub.id == conn_id;

    if (!is_alive || frag->invalid) { 
        /* Don't enqueue invalid requests or requests for purged connections */
        ecs_strbuf_reset(&frag->buf);
    } else {
        ecs_http_request_impl_t req;
        char *res = http_decode_request(&req, frag);
        if (res) {
            req.pub.conn = (ecs_http_connection_t*)conn;

            /* Check cache for GET requests */
            if (frag->method == EcsHttpGet) {
                ecs_http_request_entry_t *entry = 
                    http_find_request_entry(srv, res, frag->header_offsets[0]);
                if (entry) {
                    /* If an entry is found, don't enqueue a request. Instead
                     * return the cached response immediately. */
                    ecs_os_free(res);
                    return entry;
                }
            }

            ecs_http_request_impl_t *req_ptr = flecs_sparse_add_t(
                &srv->requests, ecs_http_request_impl_t);
            *req_ptr = req;
            req_ptr->pub.id = flecs_sparse_last_id(&srv->requests);
            req_ptr->conn_id = conn->pub.id;
            ecs_os_linc(&ecs_http_request_received_count);
        }
    }

    ecs_os_mutex_unlock(srv->lock);
    return NULL;
}

static
bool http_parse_request(
    ecs_http_fragment_t *frag,
    const char* req_frag, 
    ecs_size_t req_frag_len) 
{
    int32_t i;
    for (i = 0; i < req_frag_len; i++) {
        char c = req_frag[i];
        switch (frag->state) {
        case HttpFragStateBegin:
            ecs_os_memset_t(frag, 0, ecs_http_fragment_t);
            frag->buf.max = ECS_HTTP_METHOD_LEN_MAX;
            frag->state = HttpFragStateMethod;
            frag->header_buf_ptr = frag->header_buf;
            
            /* fall through */
        case HttpFragStateMethod:
            if (c == ' ') {
                http_parse_method(frag);
                frag->state = HttpFragStatePath;
                frag->buf.max = ECS_HTTP_REQUEST_LEN_MAX;
            } else {
                ecs_strbuf_appendch(&frag->buf, c);
            }
            break;
        case HttpFragStatePath:
            if (c == ' ') {
                frag->state = HttpFragStateVersion;
                ecs_strbuf_appendch(&frag->buf, '\0');
            } else {
                if (c == '?' || c == '=' || c == '&') {
                    ecs_strbuf_appendch(&frag->buf, '\0');
                    int32_t offset = ecs_strbuf_written(&frag->buf);
                    if (c == '?' || c == '&') {
                        frag->param_offsets[frag->param_count] = offset;
                    } else {
                        frag->param_value_offsets[frag->param_count] = offset;
                        frag->param_count ++;
                    }
                } else {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateVersion:
            if (c == '\r') {
                frag->state = HttpFragStateCR;
            } /* version is not stored */
            break;
        case HttpFragStateHeaderStart:
            if (http_header_writable(frag)) {
                frag->header_offsets[frag->header_count] = 
                    ecs_strbuf_written(&frag->buf);
            }
            http_header_buf_reset(frag);
            frag->state = HttpFragStateHeaderName;

            /* fall through */
        case HttpFragStateHeaderName:
            if (c == ':') {
                frag->state = HttpFragStateHeaderValueStart;
                http_header_buf_append(frag, '\0');
                frag->parse_content_length = !ecs_os_strcmp(
                    frag->header_buf, "Content-Length");

                if (http_header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, '\0');
                    frag->header_value_offsets[frag->header_count] =
                        ecs_strbuf_written(&frag->buf);
                }
            } else if (c == '\r') {
                frag->state = HttpFragStateCR;
            } else  {
                http_header_buf_append(frag, c);
                if (http_header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateHeaderValueStart:
            http_header_buf_reset(frag);
            frag->state = HttpFragStateHeaderValue;
            if (c == ' ') { /* skip first space */
                break;
            }

            /* fall through */
        case HttpFragStateHeaderValue:
            if (c == '\r') {
                if (frag->parse_content_length) {
                    http_header_buf_append(frag, '\0');
                    int32_t len = atoi(frag->header_buf);
                    if (len < 0) {
                        frag->invalid = true;
                    } else {
                        frag->content_length = len;
                    }
                    frag->parse_content_length = false;
                }
                if (http_header_writable(frag)) {
                    int32_t cur = ecs_strbuf_written(&frag->buf);
                    if (frag->header_offsets[frag->header_count] < cur &&
                        frag->header_value_offsets[frag->header_count] < cur)
                    {
                        ecs_strbuf_appendch(&frag->buf, '\0');
                        frag->header_count ++;
                    }
                }
                frag->state = HttpFragStateCR;
            } else {
                if (frag->parse_content_length) {
                    http_header_buf_append(frag, c);
                }
                if (http_header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateCR:
            if (c == '\n') {
                frag->state = HttpFragStateCRLF;
            } else {
                frag->state = HttpFragStateHeaderStart;
            }
            break;
        case HttpFragStateCRLF:
            if (c == '\r') {
                frag->state = HttpFragStateCRLFCR;
            } else {
                frag->state = HttpFragStateHeaderStart;
                i--;
            }
            break;
        case HttpFragStateCRLFCR:
            if (c == '\n') {
                if (frag->content_length != 0) {
                    frag->body_offset = ecs_strbuf_written(&frag->buf);
                    frag->state = HttpFragStateBody;
                } else {
                    frag->state = HttpFragStateDone;
                }
            } else {
                frag->state = HttpFragStateHeaderStart;
            }
            break;
        case HttpFragStateBody: {
                ecs_strbuf_appendch(&frag->buf, c);
                if ((ecs_strbuf_written(&frag->buf) - frag->body_offset) == 
                    frag->content_length) 
                {
                    frag->state = HttpFragStateDone;
                }
            }
            break;
        case HttpFragStateDone:
            break;
        }
    }

    if (frag->state == HttpFragStateDone) {
        return true;
    } else {
        return false;
    }
}

static
ecs_http_send_request_t* http_send_queue_post(
    ecs_http_server_t *srv)
{
    /* This function should only be called while the server is locked. Before 
     * the lock is released, the returned element should be populated. */
    ecs_http_send_queue_t *sq = &srv->send_queue;
    int32_t next = (sq->head + 1) % ECS_HTTP_SEND_QUEUE_MAX;
    if (next == sq->tail) {
        return NULL;
    }

    /* Don't enqueue new requests if server is shutting down */
    if (!srv->should_run) {
        return NULL;
    }

    /* Return element at end of the queue */
    ecs_http_send_request_t *result = &sq->requests[sq->head];
    sq->head = next;
    return result;
}

static
ecs_http_send_request_t* http_send_queue_get(
    ecs_http_server_t *srv)
{
    ecs_os_mutex_lock(srv->lock);
    ecs_http_send_queue_t *sq = &srv->send_queue;
    if (sq->tail == sq->head) {
        return NULL;
    }

    int32_t next = (sq->tail + 1) % ECS_HTTP_SEND_QUEUE_MAX;
    ecs_http_send_request_t *result = &sq->requests[sq->tail];
    sq->tail = next;
    return result;
}

static
void* http_server_send_queue(void* arg) {
    ecs_http_server_t *srv = arg;
    int32_t wait_ms = srv->send_queue.wait_ms;

    /* Run for as long as the server is running or there are messages. When the
     * server is stopping, no new messages will be enqueued */
    while (srv->should_run || (srv->send_queue.head != srv->send_queue.tail)) {
        ecs_http_send_request_t* r = http_send_queue_get(srv);
        if (!r) {
            ecs_os_mutex_unlock(srv->lock);
            /* If the queue is empty, wait so we don't run too fast */
            if (srv->should_run) {
                ecs_os_sleep(0, wait_ms * 1000 * 1000);
            }
        } else {
            ecs_http_socket_t sock = r->sock;
            char *headers = r->headers;
            int32_t headers_length = r->header_length;
            char *content = r->content;
            int32_t content_length = r->content_length;
            ecs_os_mutex_unlock(srv->lock);

            if (http_socket_is_valid(sock)) {
                bool error = false;

                http_sock_nonblock(sock, false);

                /* Write headers */
                ecs_size_t written = http_send(sock, headers, headers_length, 0);
                if (written != headers_length) {
                    ecs_err("http: failed to write HTTP response headers: %s",
                        ecs_os_strerror(errno));
                    ecs_os_linc(&ecs_http_send_error_count);
                    error = true;
                } else if (content_length >= 0) {
                    /* Write content */
                    written = http_send(sock, content, content_length, 0);
                    if (written != content_length) {
                        ecs_err("http: failed to write HTTP response body: %s",
                            ecs_os_strerror(errno));
                        ecs_os_linc(&ecs_http_send_error_count);
                        error = true;
                    }
                }
                if (!error) {
                    ecs_os_linc(&ecs_http_send_ok_count);
                }

                http_close(&sock);
            } else {
                ecs_err("http: invalid socket\n");
            }

            ecs_os_free(content);
            ecs_os_free(headers);
        }
    }
    return NULL;
}

static
void http_append_send_headers(
    ecs_strbuf_t *hdrs,
    int code, 
    const char* status, 
    const char* content_type,  
    ecs_strbuf_t *extra_headers,
    ecs_size_t content_len,
    bool preflight)
{
    ecs_strbuf_appendlit(hdrs, "HTTP/1.1 ");
    ecs_strbuf_appendint(hdrs, code);
    ecs_strbuf_appendch(hdrs, ' ');
    ecs_strbuf_appendstr(hdrs, status);
    ecs_strbuf_appendlit(hdrs, "\r\n");

    if (content_type) {
        ecs_strbuf_appendlit(hdrs, "Content-Type: ");
        ecs_strbuf_appendstr(hdrs, content_type);
        ecs_strbuf_appendlit(hdrs, "\r\n");
    }

    if (content_len >= 0) {
        ecs_strbuf_appendlit(hdrs, "Content-Length: ");
        ecs_strbuf_append(hdrs, "%d", content_len);
        ecs_strbuf_appendlit(hdrs, "\r\n");
    }

    ecs_strbuf_appendlit(hdrs, "Access-Control-Allow-Origin: *\r\n");
    if (preflight) {
        ecs_strbuf_appendlit(hdrs, "Access-Control-Allow-Private-Network: true\r\n");
        ecs_strbuf_appendlit(hdrs, "Access-Control-Allow-Methods: GET, PUT, OPTIONS\r\n");
        ecs_strbuf_appendlit(hdrs, "Access-Control-Max-Age: 600\r\n");
    }

    ecs_strbuf_mergebuff(hdrs, extra_headers);

    ecs_strbuf_appendlit(hdrs, "\r\n");
}

static
void http_send_reply(
    ecs_http_connection_impl_t* conn, 
    ecs_http_reply_t* reply,
    bool preflight) 
{
    ecs_strbuf_t hdrs = ECS_STRBUF_INIT;
    char *content = ecs_strbuf_get(&reply->body);
    int32_t content_length = reply->body.length - 1;

    /* Use asynchronous send queue for outgoing data so send operations won't
     * hold up main thread */
    ecs_http_send_request_t *req = NULL;

    if (!preflight) {
        req = http_send_queue_post(conn->pub.server);
        if (!req) {
            reply->code = 503; /* queue full, server is busy */
            ecs_os_linc(&ecs_http_busy_count);
        }
    }

    http_append_send_headers(&hdrs, reply->code, reply->status, 
        reply->content_type, &reply->headers, content_length, preflight);
    char *headers = ecs_strbuf_get(&hdrs);
    ecs_size_t headers_length = ecs_strbuf_written(&hdrs);

    if (!req) {
        ecs_size_t written = http_send(conn->sock, headers, headers_length, 0);
        if (written != headers_length) {
            ecs_err("http: failed to send reply to '%s:%s': %s",
                conn->pub.host, conn->pub.port, ecs_os_strerror(errno));
            ecs_os_linc(&ecs_http_send_error_count);
        }
        ecs_os_free(content);
        ecs_os_free(headers);
        http_close(&conn->sock);
        return;
    }

    /* Second, enqueue send request for response body */
    req->sock = conn->sock;
    req->headers = headers;
    req->header_length = headers_length;
    req->content = content;
    req->content_length = content_length;

    /* Take ownership of values */
    reply->body.content = NULL;
    conn->sock = HTTP_SOCKET_INVALID;
}

static
void http_recv_connection(
    ecs_http_server_t *srv,
    ecs_http_connection_impl_t *conn, 
    uint64_t conn_id,
    ecs_http_socket_t sock)
{
    ecs_size_t bytes_read;
    char recv_buf[ECS_HTTP_SEND_RECV_BUFFER_SIZE];
    ecs_http_fragment_t frag = {0};
    int32_t retries = 0;

    do {
        if ((bytes_read = http_recv(
            sock, recv_buf, ECS_SIZEOF(recv_buf), 0)) > 0) 
        {
            bool is_alive = conn->pub.id == conn_id;
            if (!is_alive) {
                /* Connection has been purged by main thread */
                goto done;
            }

            if (http_parse_request(&frag, recv_buf, bytes_read)) {
                if (frag.method == EcsHttpOptions) {
                    ecs_http_reply_t reply;
                    reply.body = ECS_STRBUF_INIT;
                    reply.code = 200;
                    reply.content_type = NULL;
                    reply.headers = ECS_STRBUF_INIT;
                    reply.status = "OK";
                    http_send_reply(conn, &reply, true);
                    ecs_os_linc(&ecs_http_request_preflight_count);
                } else {
                    ecs_http_request_entry_t *entry =
                        http_enqueue_request(conn, conn_id, &frag);
                    if (entry) {
                        ecs_http_reply_t reply;
                        reply.body = ECS_STRBUF_INIT;
                        reply.code = 200;
                        reply.content_type = NULL;
                        reply.headers = ECS_STRBUF_INIT;
                        reply.status = "OK";
                        ecs_strbuf_appendstrn(&reply.body, 
                            entry->content, entry->content_length);
                        http_send_reply(conn, &reply, false);
                        http_connection_free(conn);

                        /* Lock was transferred from enqueue_request */
                        ecs_os_mutex_unlock(srv->lock);
                    }
                }
            } else {
                ecs_os_linc(&ecs_http_request_invalid_count);
            }
        }

        ecs_os_sleep(0, 10 * 1000 * 1000);
    } while ((bytes_read == -1) && (++retries < ECS_HTTP_REQUEST_RECV_RETRY));

    if (retries == ECS_HTTP_REQUEST_RECV_RETRY) {
        http_close(&sock);
    }

done:
    ecs_strbuf_reset(&frag.buf);
}

typedef struct {
    ecs_http_connection_impl_t *conn;
    uint64_t id;
} http_conn_res_t;

static
http_conn_res_t http_init_connection(
    ecs_http_server_t *srv, 
    ecs_http_socket_t sock_conn,
    struct sockaddr_storage *remote_addr, 
    ecs_size_t remote_addr_len) 
{
    http_sock_set_timeout(sock_conn, 100);
    http_sock_keep_alive(sock_conn);
    http_sock_nonblock(sock_conn, true);

    /* Create new connection */
    ecs_os_mutex_lock(srv->lock);
    ecs_http_connection_impl_t *conn = flecs_sparse_add_t(
        &srv->connections, ecs_http_connection_impl_t);
    uint64_t conn_id = conn->pub.id = flecs_sparse_last_id(&srv->connections);
    conn->pub.server = srv;
    conn->sock = sock_conn;
    ecs_os_mutex_unlock(srv->lock);

    char *remote_host = conn->pub.host;
    char *remote_port = conn->pub.port;

    /* Fetch name & port info */
    if (http_getnameinfo((struct sockaddr*) remote_addr, remote_addr_len,
        remote_host, ECS_SIZEOF(conn->pub.host),
        remote_port, ECS_SIZEOF(conn->pub.port),
            NI_NUMERICHOST | NI_NUMERICSERV))
    {
        ecs_os_strcpy(remote_host, "unknown");
        ecs_os_strcpy(remote_port, "unknown");
    }

    ecs_dbg_2("http: connection established from '%s:%s' (socket %u)", 
        remote_host, remote_port, sock_conn);
    
    return (http_conn_res_t){ .conn = conn, .id = conn_id };
}

static
void http_accept_connections(
    ecs_http_server_t* srv, 
    const struct sockaddr* addr, 
    ecs_size_t addr_len) 
{
#ifdef ECS_TARGET_WINDOWS
    /* If on Windows, test if winsock needs to be initialized */
    SOCKET testsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (INVALID_SOCKET == testsocket && WSANOTINITIALISED == WSAGetLastError()){
        WSADATA data = { 0 };
        int result = WSAStartup(MAKEWORD(2, 2), &data);
        if (result) {
            ecs_warn("http: WSAStartup failed with GetLastError = %d\n", 
                GetLastError());
            return;
        }
    } else {
        http_close(&testsocket);
    }
#endif

    /* Resolve name + port (used for logging) */
    char addr_host[256];
    char addr_port[20];

    ecs_http_socket_t sock = HTTP_SOCKET_INVALID;
    ecs_assert(srv->sock == HTTP_SOCKET_INVALID, ECS_INTERNAL_ERROR, NULL);

    if (http_getnameinfo(
        addr, addr_len, addr_host, ECS_SIZEOF(addr_host), addr_port, 
        ECS_SIZEOF(addr_port), NI_NUMERICHOST | NI_NUMERICSERV))
    {
        ecs_os_strcpy(addr_host, "unknown");
        ecs_os_strcpy(addr_port, "unknown");
    }

    ecs_os_mutex_lock(srv->lock);
    if (srv->should_run) {
        ecs_dbg_2("http: initializing connection socket");

        sock = socket(addr->sa_family, SOCK_STREAM, IPPROTO_TCP);
        if (!http_socket_is_valid(sock)) {
            ecs_err("http: unable to create new connection socket: %s", 
                ecs_os_strerror(errno));
            ecs_os_mutex_unlock(srv->lock);
            goto done;
        }

        int reuse = 1, result;
        result = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 
            (char*)&reuse, ECS_SIZEOF(reuse)); 
        if (result) {
            ecs_warn("http: failed to setsockopt: %s", ecs_os_strerror(errno));
        }

        if (addr->sa_family == AF_INET6) {
            int ipv6only = 0;
            if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, 
                (char*)&ipv6only, ECS_SIZEOF(ipv6only)))
            {
                ecs_warn("http: failed to setsockopt: %s", 
                    ecs_os_strerror(errno));
            }
        }

        result = http_bind(sock, addr, addr_len);
        if (result) {
            ecs_err("http: failed to bind to '%s:%s': %s", 
                addr_host, addr_port, ecs_os_strerror(errno));
            ecs_os_mutex_unlock(srv->lock);
            goto done;
        }

        http_sock_set_timeout(sock, 1000);

        srv->sock = sock;

        result = listen(srv->sock, SOMAXCONN);
        if (result) {
            ecs_warn("http: could not listen for SOMAXCONN (%d) connections: %s", 
                SOMAXCONN, ecs_os_strerror(errno));
        }

        ecs_trace("http: listening for incoming connections on '%s:%s'",
            addr_host, addr_port);
    } else {
        ecs_dbg_2("http: server shut down while initializing");
    }
    ecs_os_mutex_unlock(srv->lock);

    struct sockaddr_storage remote_addr;
    ecs_size_t remote_addr_len = 0;

    while (srv->should_run) {
        remote_addr_len = ECS_SIZEOF(remote_addr);
        ecs_http_socket_t sock_conn = http_accept(srv->sock, (struct sockaddr*) &remote_addr, 
            &remote_addr_len);

        if (!http_socket_is_valid(sock_conn)) {
            if (srv->should_run) {
                ecs_dbg("http: connection attempt failed: %s", 
                    ecs_os_strerror(errno));
            }
            continue;
        }

        http_conn_res_t conn = http_init_connection(srv, sock_conn, &remote_addr, remote_addr_len);
        http_recv_connection(srv, conn.conn, conn.id, sock_conn);
    }

done:
    ecs_os_mutex_lock(srv->lock);
    if (http_socket_is_valid(sock) && errno != EBADF) {
        http_close(&sock);
        srv->sock = sock;
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_trace("http: no longer accepting connections on '%s:%s'",
        addr_host, addr_port);
}

static
void* http_server_thread(void* arg) {
    ecs_http_server_t *srv = arg;
    struct sockaddr_in addr;
    ecs_os_zeromem(&addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(srv->port);

    if (!srv->ipaddr) {
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
    } else {
        inet_pton(AF_INET, srv->ipaddr, &(addr.sin_addr));
    }

    http_accept_connections(srv, (struct sockaddr*)&addr, ECS_SIZEOF(addr));
    return NULL;
}

static
void http_do_request(
    ecs_http_server_t *srv,
    ecs_http_reply_t *reply,
    const ecs_http_request_impl_t *req)
{
    if (srv->callback(ECS_CONST_CAST(ecs_http_request_t*, req), reply, 
        srv->ctx) == false) 
    {
        reply->code = 404;
        reply->status = "Resource not found";
        ecs_os_linc(&ecs_http_request_not_handled_count);
    } else {
        if (reply->code >= 400) {
            ecs_os_linc(&ecs_http_request_handled_error_count);
        } else {
            ecs_os_linc(&ecs_http_request_handled_ok_count);
        }
    }
}

static
void http_handle_request(
    ecs_http_server_t *srv,
    ecs_http_request_impl_t *req)
{
    ecs_http_reply_t reply = ECS_HTTP_REPLY_INIT;
    ecs_http_connection_impl_t *conn = 
        (ecs_http_connection_impl_t*)req->pub.conn;

    if (req->pub.method != EcsHttpOptions) {
        if (srv->callback((ecs_http_request_t*)req, &reply, srv->ctx) == false) {
            reply.code = 404;
            reply.status = "Resource not found";
            ecs_os_linc(&ecs_http_request_not_handled_count);
        } else {
            if (reply.code >= 400) {
                ecs_os_linc(&ecs_http_request_handled_error_count);
            } else {
                ecs_os_linc(&ecs_http_request_handled_ok_count);
            }
        }

        if (req->pub.method == EcsHttpGet) {
            http_insert_request_entry(srv, req, &reply);
        }

        http_send_reply(conn, &reply, false);
        ecs_dbg_2("http: reply sent to '%s:%s'", conn->pub.host, conn->pub.port);
    } else {
        /* Already taken care of */
    }

    http_reply_fini(&reply);
    http_request_fini(req);
    http_connection_free(conn);
}

static
void http_purge_request_cache(
    ecs_http_server_t *srv,
    bool fini)
{
    ecs_time_t t = {0, 0};
    ecs_ftime_t time = (ecs_ftime_t)ecs_time_measure(&t);
    ecs_map_iter_t it = ecs_map_iter(&srv->request_cache.impl);
    while (ecs_map_next(&it)) {
        ecs_hm_bucket_t *bucket = ecs_map_ptr(&it);
        int32_t i, count = ecs_vec_count(&bucket->values);
        ecs_http_request_key_t *keys = ecs_vec_first(&bucket->keys);
        ecs_http_request_entry_t *entries = ecs_vec_first(&bucket->values);
        for (i = count - 1; i >= 0; i --) {
            ecs_http_request_entry_t *entry = &entries[i];
            if (fini || ((time - entry->time) > ECS_HTTP_CACHE_PURGE_TIMEOUT)) {
                ecs_http_request_key_t *key = &keys[i];
                /* Safe, code owns the value */
                ecs_os_free(ECS_CONST_CAST(char*, key->array));
                ecs_os_free(entry->content);
                flecs_hm_bucket_remove(&srv->request_cache, bucket, 
                    ecs_map_key(&it), i);
            }
        }
    }

    if (fini) {
        flecs_hashmap_fini(&srv->request_cache);
    }
}

static
int32_t http_dequeue_requests(
    ecs_http_server_t *srv,
    double delta_time)
{
    ecs_os_mutex_lock(srv->lock);

    int32_t i, request_count = flecs_sparse_count(&srv->requests);
    for (i = request_count - 1; i >= 1; i --) {
        ecs_http_request_impl_t *req = flecs_sparse_get_dense_t(
            &srv->requests, ecs_http_request_impl_t, i);
        http_handle_request(srv, req);
    }

    int32_t connections_count = flecs_sparse_count(&srv->connections);
    for (i = connections_count - 1; i >= 1; i --) {
        ecs_http_connection_impl_t *conn = flecs_sparse_get_dense_t(
            &srv->connections, ecs_http_connection_impl_t, i);

        conn->dequeue_timeout += delta_time;
        conn->dequeue_retries ++;
        
        if ((conn->dequeue_timeout > 
            (double)ECS_HTTP_CONNECTION_PURGE_TIMEOUT) &&
             (conn->dequeue_retries > ECS_HTTP_CONNECTION_PURGE_RETRY_COUNT)) 
        {
            ecs_dbg("http: purging connection '%s:%s' (sock = %d)", 
                conn->pub.host, conn->pub.port, conn->sock);
            http_connection_free(conn);
        }
    }

    http_purge_request_cache(srv, false);
    ecs_os_mutex_unlock(srv->lock);

    return request_count - 1;
}

const char* ecs_http_get_header(
    const ecs_http_request_t* req,
    const char* name) 
{
    for (ecs_size_t i = 0; i < req->header_count; i++) {
        if (!ecs_os_strcmp(req->headers[i].key, name)) {
            return req->headers[i].value;
        }
    }
    return NULL;
}

const char* ecs_http_get_param(
    const ecs_http_request_t* req,
    const char* name) 
{
    for (ecs_size_t i = 0; i < req->param_count; i++) {
        if (!ecs_os_strcmp(req->params[i].key, name)) {
            return req->params[i].value;
        }
    }
    return NULL;
}

ecs_http_server_t* ecs_http_server_init(
    const ecs_http_server_desc_t *desc) 
{
    ecs_check(ecs_os_has_threading(), ECS_UNSUPPORTED, 
        "missing OS API implementation");

    ecs_http_server_t* srv = ecs_os_calloc_t(ecs_http_server_t);
    srv->lock = ecs_os_mutex_new();
    srv->sock = HTTP_SOCKET_INVALID;

    srv->should_run = false;
    srv->initialized = true;

    srv->callback = desc->callback;
    srv->ctx = desc->ctx;
    srv->port = desc->port;
    srv->ipaddr = desc->ipaddr;
    srv->send_queue.wait_ms = desc->send_queue_wait_ms;
    if (!srv->send_queue.wait_ms) {
        srv->send_queue.wait_ms = 1;
    }

    flecs_sparse_init_t(&srv->connections, NULL, NULL, ecs_http_connection_impl_t);
    flecs_sparse_init_t(&srv->requests, NULL, NULL, ecs_http_request_impl_t);

    /* Start at id 1 */
    flecs_sparse_new_id(&srv->connections);
    flecs_sparse_new_id(&srv->requests);

    /* Initialize request cache */
    flecs_hashmap_init(&srv->request_cache, 
        ecs_http_request_key_t, ecs_http_request_entry_t,
        http_request_key_hash, http_request_key_compare, NULL);

#ifndef ECS_TARGET_WINDOWS
    /* Ignore pipe signal. SIGPIPE can occur when a message is sent to a client
     * but te client already disconnected. */
    signal(SIGPIPE, SIG_IGN);
#endif

    return srv;
error:
    return NULL;
}

void ecs_http_server_fini(
    ecs_http_server_t* srv) 
{
    if (srv->should_run) {
        ecs_http_server_stop(srv);
    }
    ecs_os_mutex_free(srv->lock);
    http_purge_request_cache(srv, true);
    flecs_sparse_fini(&srv->requests);
    flecs_sparse_fini(&srv->connections);
    ecs_os_free(srv);
}

int ecs_http_server_start(
    ecs_http_server_t *srv)
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!srv->should_run, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!srv->thread, ECS_INVALID_PARAMETER, NULL);

    srv->should_run = true;

    ecs_dbg("http: starting server thread");

    srv->thread = ecs_os_thread_new(http_server_thread, srv);
    if (!srv->thread) {
        goto error;
    }

    srv->send_queue.thread = ecs_os_thread_new(http_server_send_queue, srv);
    if (!srv->send_queue.thread) {
        goto error;
    }

    return 0;
error:
    return -1;
}

void ecs_http_server_stop(
    ecs_http_server_t* srv) 
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_OPERATION, NULL);
    ecs_check(srv->should_run, ECS_INVALID_PARAMETER, NULL);

    /* Stop server thread */
    ecs_dbg("http: shutting down server thread");

    ecs_os_mutex_lock(srv->lock);
    srv->should_run = false;
    if (http_socket_is_valid(srv->sock)) {
        http_close(&srv->sock);
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_os_thread_join(srv->thread);
    ecs_os_thread_join(srv->send_queue.thread);
    ecs_trace("http: server threads shut down");

    /* Cleanup all outstanding requests */
    int i, count = flecs_sparse_count(&srv->requests);
    for (i = count - 1; i >= 1; i --) {
        http_request_fini(flecs_sparse_get_dense_t(
            &srv->requests, ecs_http_request_impl_t, i));
    }

    /* Close all connections */
    count = flecs_sparse_count(&srv->connections);
    for (i = count - 1; i >= 1; i --) {
        http_connection_free(flecs_sparse_get_dense_t(
            &srv->connections, ecs_http_connection_impl_t, i));
    }

    ecs_assert(flecs_sparse_count(&srv->connections) == 1, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_sparse_count(&srv->requests) == 1,
        ECS_INTERNAL_ERROR, NULL);

    srv->thread = 0;
error:
    return;
}

void ecs_http_server_dequeue(
    ecs_http_server_t* srv,
    ecs_ftime_t delta_time)
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->should_run, ECS_INVALID_PARAMETER, NULL);
    
    srv->dequeue_timeout += (double)delta_time;
    srv->stats_timeout += (double)delta_time;

    if ((1000 * srv->dequeue_timeout) > (double)ECS_HTTP_MIN_DEQUEUE_INTERVAL) {
        srv->dequeue_timeout = 0;

        ecs_time_t t = {0};
        ecs_time_measure(&t);
        int32_t request_count = http_dequeue_requests(srv, srv->dequeue_timeout);
        srv->requests_processed += request_count;
        srv->requests_processed_total += request_count;
        double time_spent = ecs_time_measure(&t);
        srv->request_time += time_spent;
        srv->request_time_total += time_spent;
        srv->dequeue_count ++;
    }

    if ((1000 * srv->stats_timeout) > (double)ECS_HTTP_MIN_STATS_INTERVAL) {
        srv->stats_timeout = 0;
        ecs_dbg("http: processed %d requests in %.3fs (avg %.3fs / dequeue)",
            srv->requests_processed, srv->request_time, 
            (srv->request_time / (double)srv->dequeue_count));
        srv->requests_processed = 0;
        srv->request_time = 0;
        srv->dequeue_count = 0;
    }

error:
    return;
}

int ecs_http_server_http_request(
    ecs_http_server_t* srv,
    const char *req,
    ecs_size_t len,
    ecs_http_reply_t *reply_out)
{
    if (!len) {
        len = ecs_os_strlen(req);
    }

    ecs_http_fragment_t frag = {0};
    if (!http_parse_request(&frag, req, len)) {
        ecs_strbuf_reset(&frag.buf);
        reply_out->code = 400;
        return -1;
    }

    ecs_http_request_impl_t request;
    char *res = http_decode_request(&request, &frag);
    if (!res) {
        reply_out->code = 400;
        return -1;
    }

    http_do_request(srv, reply_out, &request);
    ecs_os_free(res);

    return (reply_out->code >= 400) ? -1 : 0;
}

int ecs_http_server_request(
    ecs_http_server_t* srv,
    const char *method,
    const char *req,
    ecs_http_reply_t *reply_out)
{
    ecs_strbuf_t reqbuf = ECS_STRBUF_INIT;
    ecs_strbuf_appendstr_zerocpy_const(&reqbuf, method);
    ecs_strbuf_appendlit(&reqbuf, " ");
    ecs_strbuf_appendstr_zerocpy_const(&reqbuf, req);
    ecs_strbuf_appendlit(&reqbuf, " HTTP/1.1\r\n\r\n");
    int32_t len = ecs_strbuf_written(&reqbuf);
    char *reqstr = ecs_strbuf_get(&reqbuf);
    int result = ecs_http_server_http_request(srv, reqstr, len, reply_out);
    ecs_os_free(reqstr);
    return result;
}

void* ecs_http_server_ctx(
    ecs_http_server_t* srv)
{
    return srv->ctx;
}

#endif
