//
// Things to worry about:
//   Expect: 100-Continue



#include "http.hpp"
#include "wrap-sstream.hpp"
#include "wrap-string.hpp"
#include "util.hpp"
#include "luastack.hpp"
#include "json.hpp"

#include <cstdint>

using string_view = std::string_view;

struct { int code; const char *name; } status_codes[] = {
    // Commonly-used codes duplicated at front of list.
    { 200, "OK" },
    { 201, "Created" },
    { 404, "Not Found" },
    { 400, "Bad Request" },
    { 500, "Internal Server Error" },

    // All valid codes, including the ones above.
    { 101, "Switching Protocols" },
    { 102, "Processing" },
    { 103, "Early Hints" },

    { 200, "OK" },
    { 201, "Created" },
    { 202, "Accepted" },
    { 203, "Non-Authoritative Information" },
    { 204, "No Content" },
    { 205, "Reset Content" },
    { 206, "Partial Content" },
    { 207, "Multi-Status" },
    { 208, "Already Reported" },
    
    { 300, "Multiple Choices" },
    { 301, "Moved Permanently" },
    { 302, "Found" },
    { 303, "See Other" },
    { 304, "Not Modified" },
    { 305, "Use Proxy" },
    { 306, "Switch Proxy" },
    { 307, "Temporary Redirect" },
    { 308, "Permanent Redirect" },

    { 400, "Bad Request" },
    { 401, "Unauthorized" },
    { 402, "Payment Required" },
    { 403, "Forbidden" },
    { 404, "Not Found" },
    { 405, "Method Not Allowed" },
    { 406, "Not Acceptable" },
    { 407, "Proxy Authentication Required" },
    { 408, "Request Timeout" },
    { 409, "Conflict" },
    { 410, "Gone" },
    { 411, "Length Required" },
    { 412, "Precondition Failed" },
    { 413, "Payload Too Large" },
    { 414, "URI Too Long" },
    { 415, "Unsupported Media Type" },
    { 416, "Range Not Satisfiable" },
    { 417, "Expectation Failed" },
    { 418, "I'm a teapot" },
    { 421, "Misdirected Request" },
    { 422, "Unprocessable Entity" },
    { 423, "Locked" },
    { 424, "Failed Dependency" },
    { 425, "Too Early" },
    { 426, "Upgrade Required" },
    { 428, "Precondition Required" },
    { 429, "Too Many Requests" },
    { 431, "Request Header Fields Too Large" },
    { 451, "Unavailable For Legal Reasons" },

    { 500, "Internal Server Error" },
    { 501, "Not Implemented" },
    { 502, "Bad Gateway" },
    { 503, "Service Unavailable" },
    { 504, "Gateway Timeout" },
    { 505, "HTTP Version Not Supported" },
    { 506, "Variant Also Negotiates" },
    { 507, "Insufficient Storage" },
    { 508, "Loop Detected" },
    { 510, "Not Extended" },
    { 511, "Network Authentication Required" },

    { 0, "" },
};

static const char *status_code_to_string(int code) {
    for (int i = 0; status_codes[i].code != 0; i++) {
        if (status_codes[i].code == code) {
            return status_codes[i].name;
        }
    }
    switch (code / 100) {
        case 1: return "Unknown Informational Status";
        case 2: return "Unknown Successful Status";
        case 3: return "Unknown Redirect";
        case 4: return "Unknown Client Error";
        case 5: return "Unknown Server Error";
        default: return "Unknown Error";
    }
}

static int status_code_from_string(std::string_view s) {
    for (int i = 0; status_codes[i].code != 0; i++) {
        if (sv::case_insensitive_eq(status_codes[i].name, s)) {
            return status_codes[i].code;
        }
    }
    return 0;
}

// Our client always sends HTTP/1.1 queries, but it
// accepts HTTP/1.0 responses.  Our server accepts both protocols.
static bool is_supported_protocol(string_view protocol) {
    return (protocol == "HTTP/1.0") || (protocol == "HTTP/1.1");
}

// Our client can make GET, HEAD, and POST requests.
static bool is_supported_client_method(string_view method) {
    return ((method == "GET") || (method == "HEAD") || (method == "POST"));
}

// Our server is only allowed to return certain status codes.
static bool is_supported_server_status(int status) {
    if (status == 200) return true;
    if ((status >= 400) && (status <= 599)) return true;
    return false;
}

bool contains_newline(std::string_view s) {
    if (s.find('\n') != std::string_view::npos) return true;
    return false;
}

bool words_separated_by_dashes(string_view v) {
    while (true) {
        string_view word = sv::read_simple_identifier(v);
        if (word.empty()) return false;
        if (v.empty()) return true;
        char c = v.front();
        if (c != '-') return false;
        v.remove_prefix(1);
    }
}

// This doesn't check whether the mime type is actually
// registered, obviously.  It only checks that it's in
// the desired notation.
bool valid_mime_type(string_view method) {
    string_view part1 = sv::read_simple_identifier(method);
    if (part1.empty()) return false;
    if (sv::zfront(method) != '/') return false;
    method.remove_prefix(1);
    while (true) {
        string_view word = sv::read_simple_identifier(method);
        if (word.empty()) return false;
        if (method.empty()) return true;
        char c = method.front();
        if ((c != '-') && (c != '.') && (c != '+')) return false;
        method.remove_prefix(1);
    }
}

// Technically, this is a true, correct URL encode routine.
static eng::string url_encode_param(string_view value) {
    eng::ostringstream result;
    const char *hexdigits = "0123456789ABCDEF";
    for (int i = 0; i < int(value.size()); i++) {
        char c = value[i];

        if (sv::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            result << c;
        } else if (c == ' ') {
            result << '+';
        } else {
            result << '%' << hexdigits[c>>4] << hexdigits[c&15];
        }
    }
    return result.str();
}

// This URL encode routine leaves slashes intact.  That's not
// technically correct, but it's really what you want for paths.
static eng::string url_encode_path(string_view value) {
    eng::ostringstream result;
    const char *hexdigits = "0123456789ABCDEF";
    for (int i = 0; i < int(value.size()); i++) {
        char c = value[i];

        if (sv::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') {
            result << c;
        } else if (c == ' ') {
            result << '+';
        } else {
            result << '%' << hexdigits[c>>4] << hexdigits[c&15];
        }
    }
    return result.str();
}

static eng::string url_decode(string_view eurl) {
    eng::ostringstream result;
    int i = 0;
    int len = eurl.size();
    while (i < len) {
        char c = eurl[i];
        if (c == '+') {
            result << ' ';
            i += 1;
        } else if ((c == '%') && (i + 2 < len)) {
            std::string_view code = eurl.substr(i + 1, 2);
            uint64_t value = sv::to_hex64(code);
            if (value > 255) {
                result << '?';
            } else {
                result << char(value);
            }
            i += 3;
        } else {
            result << c;
            i += 1;
        }
    }
    return result.str();
}

static void send_encoded_path(std::string_view path, const UrlParameters &params, StreamBuffer *sb) {
    sb->write_bytes(url_encode_path(path));
    bool first_param = true;
    for (const auto &pair : params) {
        sb->write_char(first_param ? '?' : '&');
        sb->write_bytes(url_encode_param(pair.first));
        sb->write_char('=');
        sb->write_bytes(url_encode_param(pair.second));
        first_param = false;
    }
}

static void send_host_and_port(std::string_view host, int port, StreamBuffer *sb) {
    sb->write_bytes(host);
    if (port != 0) {
        sb->write_bytes(util::ss(":", port));
    }
}

static void send_protocol(bool http11, StreamBuffer *sb) {
    if (http11) {
        sb->write_bytes("HTTP/1.1");
    } else {
        sb->write_bytes("HTTP/1.0");
    }
}

static void send_content_length_header(bool http11, int size, StreamBuffer *sb) {
    if (http11) {
        sb->write_bytes(util::ss("Content-length: ", size, "\r\n"));
    }
}

static void send_content_type_header(bool http11, const eng::string &mime, StreamBuffer *sb) {
    sb->write_bytes("Content-type: ");
    sb->write_bytes(mime);
    if (http11) {
        if (sv::has_prefix(mime, "text/")) {
            sb->write_bytes(" ; charset=utf-8");
        }
    }
    sb->write_bytes("\r\n");
}

static void send_cache_control_header(bool http11, int max_age, StreamBuffer *sb) {
    if (http11) {
        if (max_age == 0) {
            sb->write_bytes("Cache-control: no-cache\r\n");
        } else {
            sb->write_bytes(util::ss("Cache-control: max-age=", max_age, "\r\n"));
        }
    } else {
        sb->write_bytes("Pragma: no-cache\r\n");
    }
}

static void send_connection_header(bool http11, bool keep_alive, StreamBuffer *sb) {
    if (http11) {
        if (keep_alive) {
            sb->write_bytes("Connection: keep-alive\r\n");
        } else {
            sb->write_bytes("Connection: close\r\n");
        }
    }
}

static void send_error_response(bool http11, int code, eng::string extrainfo, StreamBuffer *sb) {
    send_protocol(http11, sb);
    eng::string errstr = status_code_to_string(code);
    sb->write_bytes(util::ss(" ", code, " ", errstr));
    if ((!extrainfo.empty()) && (!contains_newline(extrainfo))) {
        sb->write_bytes(": ");
        sb->write_bytes(extrainfo);
    }
    sb->write_bytes("\r\n");
    sb->write_bytes("Content-Type: text/plain\r\n");
    sb->write_bytes("\r\n");
    sb->write_bytes(errstr);
    if (!extrainfo.empty()) {
        sb->write_bytes(": ");
        sb->write_bytes(extrainfo);
    }
}

// HTTP 1.0: An entity body is included with a request message only when the request method calls for one.
// The presence of an entity body in a request is signaled by the inclusion of a Content-Length header field
// in the request message headers. HTTP/1.0 requests containing an entity body must include a valid
// Content-Length header field.
//
// HTTP 1.1: The presence of a message-body in a request is signaled by the inclusion of a Content-Length or
// Transfer-Encoding header field in the request's message-headers
//
static bool request_contains_content(int content_length, std::string_view transfer_encoding) {
    return (content_length >= 0) || (!transfer_encoding.empty());
}

// HTTP 1.0: For response messages, whether or not an entity body is included with a message is dependent on
// both the request method and the response code. All responses to the HEAD request method must not include a
// body, even though the presence of entity header fields may lead one to believe they do. All 1xx (informational),
// 204 (no content), and 304 (not modified) responses must not include a body. All other responses must include an
// entity body or a Content-Length header field defined with a value of zero (0).
//
// HTTP 1.1: For response messages, whether or not a message-body is included with a message is dependent on both
// the request method and the response status code (section 6.1.1). All responses to the HEAD request method MUST
// NOT include a message-body, even though the presence of entity- header fields might lead one to believe they
// do. All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body.
// All other responses do include a message-body, although it MAY be of zero length.
//
static bool response_contains_content(std::string_view method, int status) {
    if (method == "HEAD") return false;
    if ((status >= 100) && (status <= 199)) return false;
    if (status == 204) return false;
    if (status == 304) return false;
    return true;
}

// In a properly-formed url, the hostname and path are url encoded.
// This parser expects an encoded URL.
struct ParsedURL {
public:
    bool valid;
    eng::string proto;
    eng::string host;
    int port;
    eng::string path;
    UrlParameters params;

public:
    void clear() {
        valid = false;
        proto.clear();
        host.clear();
        port = 0;
        path.clear();
        params.clear();
    }

    eng::string str() {
        StreamBuffer sb;
        sb.write_bytes(proto);
        sb.write_bytes("://");
        send_host_and_port(host, port, &sb);
        send_encoded_path(path, params, &sb);
        return eng::string(sb.view());
    }

    ParsedURL(std::string_view url) {
        clear();

        if (!sv::has_prefix(url, "/")) {
            proto = util::ascii_tolower(sv::read_to_sep(url, ':'));
            if (!sv::has_prefix(url, "//")) { clear(); return; }
            url.remove_prefix(2);
            if (!words_separated_by_dashes(proto)) { clear(); return; }
            
            // Extract the host and port as a single string.
            string_view turl = url;
            string_view hostport = sv::read_to_sep(turl, '/');
            url.remove_prefix(hostport.size());

            // Split the host and port from each other and parse them.
            host = util::ascii_tolower(sv::read_to_sep(hostport, ':'));
            if (host.empty()) { clear(); return; }
            if (!hostport.empty()) {
                int64_t iport = sv::to_int64(hostport);
                if ((iport < 1) || (iport > 65535)) {
                    clear(); return;
                }
                port = iport;
            }
        } else {
            // Stick in some defaults for unspecified fields.
            host = "host";
            proto = "https";
        }

        // Split off the path.
        path = url_decode(sv::read_to_sep(url, '?'));
        if (path.empty()) {
            path = "/";
        }

        // Process url parameters.
        while (!sv::isnull(url)) {
            std::string_view keyval = sv::read_to_sep(url, '&');
            if (keyval.empty()) { clear(); return; }
            std::string_view key = sv::read_to_sep(keyval, '=');
            if (key.empty()) { clear(); return; }
            if (sv::isnull(keyval)) { clear(); return; }
            eng::string dkey = url_decode(key);
            eng::string dval = url_decode(keyval);
            params[dkey] = dval;
        }

        // If we made it here, we have a valid URL
        valid = true;
    }
};

void HttpClientRequest::check_fail(string_view s) {
    if (check_fail_.empty()) {
        check_fail_ = s;
    }
}

void HttpClientRequest::send_internal(StreamBuffer *sb, bool debug_string) const {
    // If there's an error in the request, handle it.  In debug string mode,
    // we just put the error into the output.  In production mode, we assert
    // fail.
    eng::string error = check();
    if (debug_string) {
        if (!error.empty()) {
            sb->write_bytes(error);
            return;
        }
    } else {
        assert(error.empty());
    }

    // Send the command.
    sb->write_bytes(method_);
    sb->write_char(' ');
    send_encoded_path(path_, params_, sb);
    sb->write_bytes(" HTTP/1.1");
    sb->write_bytes("\r\n");

    // Send the host header.
    sb->write_bytes("Host: ");
    send_host_and_port(host_, port_, sb);
    sb->write_bytes("\r\n");

    // The empty accept-encoding header notifies the
    // server that we don't support gzip, deflate, or
    // other content compression.
    sb->write_bytes("Accept-encoding:");
    sb->write_bytes("\r\n");

    // Add a user-agent header.  Not sure why.
    sb->write_bytes("User-agent: Mozilla 5.0 (luprex)");
    sb->write_bytes("\r\n");

    // Add the requester IDs (debug string only)
    if (debug_string && ((request_id_ != 0) || (place_id_ != 0) || (thread_id_ != 0))) {
        sb->write_bytes("X-requester-ids: ");
        sb->write_bytes(util::ss("rid=", request_id_, "; pid=", place_id_, "; tid=", thread_id_));
        sb->write_bytes("\r\n");
    }

    // Send the content length and the content type.
    if (content_assigned_) {
        send_content_length_header(true, content_.size(), sb);
        send_content_type_header(true, mime_type_, sb);
    }

    // Send the extra linebreak.
    sb->write_bytes("\r\n");

    // Send the content.
    if (content_assigned_) {
        sb->write_bytes(content_);
    }
}

HttpClientRequest::HttpClientRequest() {
    verify_certificate_ = false;
    port_ = 0;
    content_assigned_ = false;
    request_id_ = 0;
    place_id_ = 0;
    thread_id_ = 0;
}

void HttpClientRequest::set_verify_certificate(bool flag) {
    verify_certificate_ = flag;
}

void HttpClientRequest::set_method(const eng::string &s) {
    eng::string method = util::ascii_toupper(s);
    if (!is_supported_client_method(method)) {
        check_fail(util::ss("method not implemented: ", method, "."));
        return;
    }
    if ((!method_.empty()) && (method_ != method)) {
        check_fail(util::ss("method specified twice: ", method_, " and ", method));
        return;
    }
    method_ = method;
}

void HttpClientRequest::set_host(const eng::string &s) {
    eng::string host = util::ascii_tolower(s);
    if (host.empty()) {
        check_fail(util::ss("hostname cannot be empty string."));
        return;
    }
    // This is not quite strict, but it's close.  I believe
    // the DNS lookup will fail for invalid hostnames anyway.
    for (char c : host) {
        if ((c != '-') && (c != '.') && (!sv::ascii_isalnum(c))) {
            check_fail(util::ss("hostnames can only contain letters, digits, and hyphen: ", host));
            return;
        }
    }
    if (!host_.empty()) {
        check_fail(util::ss("hostname specified twice: ", host_, " and ", host));
        return;
    }
    host_ = host;
}

void HttpClientRequest::set_port(int port) {
    if ((port < 1) || (port > 65535)) {
        check_fail(util::ss("port must be between 1 and 65535: ", port));
        return;
    }
    if (port_ != 0) {
        check_fail(util::ss("port specified twice: ", port_, " and ", port));
        return;
    }
    port_ = port;
}

void HttpClientRequest::set_path(string_view path) {
    if (!sv::has_prefix(path, "/")) {
        check_fail(util::ss("path must start with slash"));
        return;
    }
    if (!path_.empty()) {
        check_fail(util::ss("path specified twice: ", path_, " and ", path));
        return;
    }
    path_ = path;
}

void HttpClientRequest::set_param(const eng::string &key, const eng::string &val) {
    if (params_.find(key) != params_.end()) {
        check_fail(util::ss("url parameter specified twice: ", key));
        return;
    }
    if (key.empty()) {
        check_fail(util::ss("parameter key cannot be empty"));
        return;
    }
    params_[key] = val;
}

void HttpClientRequest::set_url(string_view url) {
    ParsedURL parsed_url(url);
    if (!parsed_url.valid) {
        check_fail(util::ss("syntactically invalid URL: ", url));
        return;
    }
    if (parsed_url.proto != "https") {
        check_fail(util::ss("unsupported protocol: ", parsed_url.proto));
        return;
    }
    set_host(parsed_url.host);
    if (parsed_url.port) set_port(parsed_url.port);
    set_path(parsed_url.path);
    for (const auto &pair : parsed_url.params) {
        set_param(pair.first, pair.second);
    }
}

void HttpClientRequest::set_mime_type(const eng::string &mime_type) {
    if (!valid_mime_type(mime_type)) {
        check_fail(util::ss("Not a valid mime type: ", mime_type));
        return;
    }
    if (!mime_type_.empty()) {
        check_fail(util::ss("Mime type specified twice: ", mime_type_, " and ", mime_type));
        return;
    }
    mime_type_ = mime_type;
}

void HttpClientRequest::set_content(const eng::string &content) {
    if (!content_.empty()) {
        check_fail(util::ss("Content specified twice"));
        return;
    }
    if (content_assigned_) {
        check_fail("Content specified twice.");
        return;
    }
    content_ = content;
    content_assigned_ = true;
}

void HttpClientRequest::set_verify_certificate(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.tryboolean(val);
    if (!tval) {
        check_fail(util::ss("verifycertificate must be a boolean"));
        return;
    }
    set_verify_certificate(*tval);
}

void HttpClientRequest::set_method(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.trystring(val);
    if (!tval) {
        check_fail(util::ss("method must be a string"));
        return;
    }
    set_method(*tval);
}

void HttpClientRequest::set_host(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.trystring(val);
    if (!tval) {
        check_fail(util::ss("host must be a string"));
        return;
    }
    set_host(*tval);
}

void HttpClientRequest::set_port(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.tryint(val);
    if (!tval) {
        check_fail(util::ss("port must be an int"));
        return;
    }
    set_port(*tval);
}

void HttpClientRequest::set_path(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.trystring(val);
    if (!tval) {
        check_fail(util::ss("path must be a string"));
        return;
    }
    set_path(*tval);
}

void HttpClientRequest::set_param(LuaCoreStack &LS, LuaSlot key, LuaSlot val) {
    auto tkey = LS.trystring(key);
    auto tval = LS.trystring(val);
    if (!tkey) {
        check_fail(util::ss("url parameter key must be a string"));
        return;
    }
    if (!tval) {
        check_fail(util::ss("url parameter val must be a string"));
        return;
    }
    set_param(*tkey, *tval);
}

void HttpClientRequest::set_params(LuaCoreStack &LS0, LuaSlot tab) {
    if (!LS0.trytable(tab)) {
        check_fail(util::ss("params must be a table"));
        return;
    }
    LuaVar key, val;
    LuaExtStack LS(LS0.state(), key, val);
    LS.set(key, LuaNil);
    while (LS.next(tab, key, val)) {
        set_param(LS, key, val);
    }
}

void HttpClientRequest::set_url(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.trystring(val);
    if (!tval) {
        check_fail(util::ss("url must be a string"));
        return;
    }
    set_url(*tval);
}

void HttpClientRequest::set_mime_type(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.trystring(val);
    if (!tval) {
        check_fail(util::ss("mime type must be a string"));
        return;
    }
    set_mime_type(*tval);
}

void HttpClientRequest::set_content(LuaCoreStack &LS, LuaSlot val) {
    auto tval = LS.trystring(val);
    if (!tval) {
        check_fail(util::ss("content must be a string"));
        return;
    }
    set_content(*tval);
}

void HttpClientRequest::set_jsonvalue(LuaCoreStack &LS, LuaSlot val) {
    eng::string out;
    eng::string err = json::encode(LS, val, out, false, HttpParser::MAX_CONTENT_LENGTH);
    if (!err.empty()) {
        check_fail(util::ss("json encode failure: ", err));
        return;
    }
    set_content(out);
    set_mime_type("application/json");
}

void HttpClientRequest::set_defaults() {
    if (method_.empty()) {
        method_ = "GET";
    }
    if (port_ == 0) {
        port_ = 443;
    }
}

void HttpClientRequest::configure(LuaKeywordParser &kp) {
    LuaVar val;
    LuaExtStack LS(kp.state(), val);
    if (kp.optional(val, "method")) {
        set_method(LS, val);
    }
    if (kp.optional(val, "host")) {
        set_host(LS, val);
    }
    if (kp.optional(val, "port")) {
        set_port(LS, val);
    }
    if (kp.optional(val, "path")) {
        set_path(LS, val);
    }
    if (kp.optional(val, "params")) {
        set_params(LS, val);
    }
    if (kp.optional(val, "url")) {
        set_url(LS, val);
    }
    if (kp.optional(val, "verifycertificate")) {
        set_verify_certificate(LS, val);
    }
    if (kp.optional(val, "mimetype")) {
        set_mime_type(LS, val);
    }
    if (kp.optional(val, "content")) {
        set_content(LS, val);
    }
    if (kp.optional(val, "html")) {
        set_content(LS, val);
        set_mime_type("text/html");
    }

    if (kp.optional(val, "text")) {
        set_content(LS, val); 
        set_mime_type("text/plain");
    }
    if (kp.optional(val, "json")) {
        set_content(LS, val);
        set_mime_type("application/json");
    }
    if (kp.optional(val, "bytes")) {
        set_content(LS, val);
        set_mime_type("application/octet-stream");
    }
    if (kp.optional(val, "jsonvalue")) {
        set_jsonvalue(LS, val);
    }
}

eng::string HttpClientRequest::target() const {
    assert(check().empty());
    eng::ostringstream oss;
    oss << (verify_certificate_ ? "cert" : "nocert");
    oss << ':' << host_ << ':' << port_;
    return oss.str();
}


eng::string HttpClientRequest::check() const {
    if (!check_fail_.empty()) {
        return check_fail_;
    }
    if (method_.empty()) {
        return "method has not been set";
    }
    if (host_.empty()) {
        return "host has not been set";
    }
    if (port_ == 0) {
        return "port has not been set";
    }
    if (path_.empty()) {
        return "url has not been set";
    }
    if (method_ == "POST") {
        if (!content_assigned_) {
            return "content not set for POST request";
        }
        if (mime_type_.empty()) {
            return "mime type not set for POST request";
        }
    }
    return "";
}

void HttpClientRequest::serialize(StreamBuffer *sb) const {
    sb->write_int64(request_id_);
    sb->write_int64(place_id_);
    sb->write_int64(thread_id_);
    sb->write_string(check_fail_);
    sb->write_bool(verify_certificate_);
    sb->write_string(method_);
    sb->write_string(host_);
    sb->write_int32(port_);
    sb->write_string(path_);
    sb->write_int32(params_.size());
    for (const auto &pair : params_) {
        sb->write_string(pair.first);
        sb->write_string(pair.second);
    }
}

void HttpClientRequest::deserialize(StreamBuffer *sb) {
    request_id_ = sb->read_int64();
    place_id_ = sb->read_int64();
    thread_id_ = sb->read_int64();
    check_fail_ = sb->read_string();
    verify_certificate_ = sb->read_bool();
    method_ = sb->read_string();
    host_ = sb->read_string();
    port_ = sb->read_int32();
    path_ = sb->read_string();
    int32_t nparams = sb->read_int32();
    params_.clear();
    for (int i = 0; i < nparams; i++) {
        eng::string k = sb->read_string();
        eng::string v = sb->read_string();
        params_[k] = v;
    }
}

eng::string HttpClientRequest::debug_string() {
    StreamBuffer sb;
    send_internal(&sb, true);
    return eng::string(sb.view());
}

void HttpServerResponse::check_fail(string_view s) {
    if (check_fail_.empty()) {
        check_fail_ = s;
    }
}


void HttpServerResponse::send_internal(StreamBuffer *sb, bool debug_string) const {
    eng::string error = check();
    if (!error.empty()) {
        send_error_response(http11_, 500, error, sb);
        return;
    }

    // Determine if we're going to be sending content.
    bool contains_content = response_contains_content("GET", status_);

    // The status message is the human-readable status code.
    eng::string statusmsg = status_code_to_string(status_);

    // Annotate error messages.
    const eng::string *content = &content_;
    if (errstatus() && contains_content &&
        (mime_type_ == "text/plain") && !contains_newline(content_)) {
        if (sv::has_prefix(content_, statusmsg)) {
            statusmsg = content_;
        } else if (!content_.empty()) {
            statusmsg += ": ";
            statusmsg += content_;
        }
        content = &statusmsg;
    }

    // Send the status line.
    send_protocol(http11_, sb);
    sb->write_bytes(util::ss(" ", status_, " ", statusmsg, "\r\n"));

    // Send the connection header.
    if (!errstatus()) {
        send_connection_header(http11_, keep_alive_, sb);
    }

    // Send the headers that make sense if we're sending content.
    if (contains_content) {
        if (!errstatus()) {
            send_cache_control_header(http11_, max_age_, sb);
        }
        send_content_length_header(http11_, content->size(), sb);
        send_content_type_header(http11_, mime_type_, sb);
    }

    // Send the extra linebreak.
    sb->write_bytes("\r\n");

    // Send the content.
    if (contains_content) {
        sb->write_bytes(*content);
    }
}

HttpServerResponse::HttpServerResponse() {
    status_ = 0;
    max_age_ = 0;
    keep_alive_ = false;
    content_assigned_ = false;
    http11_ = false;
}

void HttpServerResponse::set_status(int status) {
    if (!is_supported_server_status(status)) {
        check_fail(util::ss("status code not supported yet: ", status));
        return;
    }
    if (status_ != 0) {
        check_fail(util::ss("status specified twice: ", status_, " and ", status));
        return;
    }
    status_ = status;
}

void HttpServerResponse::set_max_age(int max_age) {
    if (max_age < 0) {
        check_fail(util::ss("max age must be a positive integer: ", max_age));
        return;
    }
    if (max_age_ != 0) {
        check_fail(util::ss("max age specified twice: ", max_age_, " and ", max_age));
        return;
    }
    max_age_ = max_age;
}

void HttpServerResponse::set_mime_type(const eng::string &mime_type) {
    if (!valid_mime_type(mime_type)) {
        check_fail(util::ss("mime type not syntactically valid: ", mime_type));
        return;
    }
    if (!mime_type_.empty()) {
        check_fail(util::ss("mime type specified twice: ", mime_type_, " and ", mime_type));
        return;
    }
    mime_type_ = mime_type;
}

void HttpServerResponse::set_content(const eng::string &content) {
    if (content_assigned_) {
        check_fail(util::ss("content specified twice"));
        return;
    }
    content_ = content;
    content_assigned_ = true;
}

void HttpServerResponse::set_status(LuaCoreStack &LS, LuaSlot val) {
    int status = 0;
    auto vstring = LS.trystring(val);
    auto vint = LS.tryint(val);
    if (vstring) {
        status = status_code_from_string(*vstring);
        if (status == 0) {
            check_fail(util::ss("unrecognized status code: ", *vstring));
            return;
        }
    } else if (vint) {
        status = *vint;
    } else {
        check_fail(util::ss("status must be an integer"));
        return;
    }
    set_status(status);
}

void HttpServerResponse::set_max_age(LuaCoreStack &LS, LuaSlot val) {
    auto vint = LS.tryint(val);
    if (!vint) {
        check_fail(util::ss("max-age must be an int"));
        return;
    }
    set_max_age(*vint);
}

void HttpServerResponse::set_mime_type(LuaCoreStack &LS, LuaSlot val) {
    auto vstring = LS.trystring(val); 
    if (!vstring) {
        check_fail(util::ss("mime type must be a string"));
        return;
    }
    set_mime_type(*vstring);
}

void HttpServerResponse::set_content(LuaCoreStack &LS, LuaSlot val) {
    auto vstring = LS.trystring(val);
    if (!vstring) {
        check_fail(util::ss("content must be a string"));
        return;
    }
    set_content(*vstring);
}

void HttpServerResponse::set_jsonvalue(LuaCoreStack &LS, LuaSlot val) {
    eng::string out;
    eng::string err = json::encode(LS, val, out, false, HttpParser::MAX_CONTENT_LENGTH);
    if (!err.empty()) {
        check_fail(util::ss("json encode failure: ", err));
        return;
    }
    set_content(out);
    set_mime_type("application/json");
}

void HttpServerResponse::configure(LuaKeywordParser &kp) {
    LuaVar val;
    LuaExtStack LS(kp.state(), val);
    if (kp.optional(val, "status")) {
        set_status(LS, val);
    }
    if (kp.optional(val, "maxage")) {
        set_max_age(LS, val);
    }
    if (kp.optional(val, "mimetype")) {
        set_mime_type(LS, val);
    }
    if (kp.optional(val, "content")) {
        set_content(LS, val);
    }
    if (kp.optional(val, "html")) {
        set_content(LS, val);
        set_mime_type("text/html");
    }
    if (kp.optional(val, "text")) {
        set_content(LS, val);
        set_mime_type("text/plain");
    }
    if (kp.optional(val, "json")) {
        set_content(LS, val);
        set_mime_type("application/json");
    }
    if (kp.optional(val, "bytes")) {
        set_content(LS, val);
        set_mime_type("application/octet-stream");
    }
    if (kp.optional(val, "jsonvalue")) {
        set_jsonvalue(LS, val);
    }
}

void HttpServerResponse::fail(int status, const eng::string &errmessage) {
    status_ = status;
    content_ = errmessage;
    content_assigned_ = true;
    mime_type_ = "text/plain";
    max_age_ = 0;
}

void HttpServerResponse::set_defaults() {
    // If you specified content, and didn't specify
    // a status code, then assume it's a success.
    if ((status_ == 0) && (content_assigned_)) {
        status_ = 200;
    }

    // If the response status indicates that we're sending
    // content, and there's no content, generate blank content.
    if (response_contains_content("GET", status_) &&
        (!content_assigned_) && (mime_type_.empty())) {
        content_assigned_ = true;
        content_ = "";
        mime_type_ = "text/plain";
    }
}

eng::string HttpServerResponse::check() const {
    if (!check_fail_.empty()) {
        return check_fail_;
    }

    // There needs to be a status code.
    if (status_ == 0) {
        return "status code not specified";
    }

    // If you assigned a mime type and didn't specify
    // content, that's bad.
    if ((!content_assigned_) && (!mime_type_.empty())) {
        return "mime type specified without content";
    }

    // If you assigned content, but didn't assign
    // a mime type, that's bad.
    if (content_assigned_ && (mime_type_.empty())) {
        return "content specified without mime type";
    }
    return "";
}

eng::string HttpServerResponse::debug_string() {
    StreamBuffer sb;
    send_internal(&sb, true);
    return eng::string(sb.view());
}

HttpParser::HttpParser() {
    request_id_ = 0;
    is_request_ = false;
    status_ = 0;
    mime_type_ = "";
    content_length_ = -1;
    comm_length_ = 0;
    http11_ = false;
}

eng::string HttpParser::first_path_component(std::string_view defval) const {
    std::string_view v = path_;
    assert(sv::zfront(v) == '/');
    v.remove_prefix(1);
    std::string_view first = sv::read_to_sep(v, '/');
    if (first.empty()) {
        return eng::string(defval);
    } else {
        return eng::string(first);
    }
}

eng::string HttpParser::to_lua_identifier(std::string_view pathcomp) {
    eng::ostringstream oss;
    for (char c : pathcomp) {
        if (sv::ascii_islower(c)) {
            oss << c;
        } else if (sv::ascii_isupper(c)) {
            oss << char(c + 'a' - 'A');
        } else if (sv::ascii_isdigit(c)) {
            oss << c;
        } else if ((c == '.') || (c == '_')) {
            oss << '_';
        } else {
            return "";
        }
    }
    return oss.str();
}

void HttpParser::fail(int code, std::string_view message) {
    status_ = code;
    error_ = message;
    mime_type_ = "";
    charset_ = "";
    content_ = "";
}

void HttpParser::syntax(std::string_view detail) {
    if (is_request_) {
        fail(400, util::ss("Bad Request: ", detail));
    } else {
        fail(500, util::ss("Bad Response: ", detail));
    }
}

void HttpParser::incomplete(bool closed) {
    if (closed) {
        syntax("response truncated");
    } else {
        fail(0, "response not yet fully received");
    }
}

void HttpParser::oversized() {
    fail(413, util::ss("Payload Too Large: Limit=", MAX_CONTENT_LENGTH));
}

bool HttpParser::parse_request_line(std::string_view &view, bool closed) {
    // Extract the request line.
    //
    string_view request = sv::trim(sv::read_to_line(view));
    if (sv::isnull(view)) {
        incomplete(closed);
        return false;
    }

    // Break down the request line.
    //
    eng::string method = util::ascii_toupper(sv::read_to_space(request));
    string_view path = sv::read_to_space(request);
    eng::string protocol = util::ascii_toupper(sv::read_to_space(request));
    if ((!request.empty()) || (protocol.empty())) {
        syntax("invalid request line");
        return false;
    }
    if (method != "GET") {
        fail(405, util::ss("Method Not Allowed: ", method));
        return false;
    }
    if (!is_supported_protocol(protocol)) {
        fail(505, util::ss("HTTP Version Not Supported: ", protocol));
        return false;
    }
    http11_ = (protocol == "HTTP/1.1");

    // Parse the url.
    //
    ParsedURL url(path);
    if (!url.valid) {
        syntax(util::ss("invalid URL path: ", path));
        return false;
    }

    method_ = method;
    path_ = url.path;
    params_ = url.params;
    return true;
}

bool HttpParser::parse_status_line(std::string_view &view, bool closed) {
    // Extract the status line.
    //
    string_view status = sv::trim(sv::read_to_line(view));
    if (sv::isnull(view)) {
        incomplete(closed);
        return false;
    }

    // Get the protocol version from the response line.
    //
    string_view protoversion = sv::read_to_space(status);
    if (!is_supported_protocol(protoversion)) {
        syntax(util::ss("unsupported protocol: ", protoversion));
        return false;
    }
    http11_ = (protoversion == "HTTP/1.1");

    // Get the status code from the response line.
    //
    string_view scode = sv::read_to_space(status);
    int64_t code = sv::to_int64(scode, 0);
    if ((code < 100) || (code > 599)) {
        syntax(util::ss("invalid response code: ", scode));
        return false;
    }
    status_ = code;

    // Responses outside the range 200-299 are errors,
    // and therefore must store a nonempty error message.
    //
    if ((code < 200) || (code > 299)) {
        if (status.empty()) {
            error_ = status_code_to_string(code);
        } else {
            error_ = status;
        }
    }
    return true;
}

void HttpParser::parse_content_encoding(string_view value) {
    content_encoding_ = util::ascii_tolower(value);
}

void HttpParser::parse_content_length(string_view value) {
    int64_t code = sv::to_int64(value);
    if ((code < 0) || (code > INT_MAX)) {
        syntax(util::ss("unparseable content-length: ", value));
    }
    content_length_ = code;
}

void HttpParser::parse_content_type(string_view value) {
    eng::string ctype = util::ascii_tolower(value);
    string_view ctview(ctype);
    mime_type_ = sv::trim(sv::read_to_sep(ctview, ';'));
    if (mime_type_.empty()) {
        syntax(util::ss("unparseable content-type: ", value));
        return;
    }
    while (true) {
        string_view feature = sv::trim(sv::read_to_sep(ctview, ';'));
        if (feature.empty()) {
            return;
        }
        string_view ftype = sv::trim(sv::read_to_sep(feature, '='));
        if (ftype == "charset") {
            charset_ = sv::trim(feature);
        }
    }
}

void HttpParser::parse_location(string_view value) {
    location_ = url_decode(value);
}

void HttpParser::parse_transfer_encoding(string_view value) {
    transfer_encoding_ = util::ascii_tolower(value);
}

void HttpParser::parse_header(string_view header, string_view value) {
    if (header == "content-encoding") {
        parse_content_encoding(value);
    } else if (header == "content-length") {
        parse_content_length(value);
    } else if (header == "content-type") {
        parse_content_type(value);
    } else if (header == "location") {
        parse_location(value);
    } else if (header == "transfer-encoding") {
        parse_transfer_encoding(value);
    } else if (header == "content-range") {
        fail(416, util::ss("Range Not Satisfiable: unsupported header: ", header));
    }
}

bool HttpParser::parse_headers(std::string_view &view, bool closed) {
    // Parse the headers.
    while (true) {
        string_view header = sv::read_to_line(view);
        if (sv::isnull(view)) {
            incomplete(closed);
            return false;
        }
        if (header.empty()) {
            return true;
        }
        eng::string command = util::ascii_tolower(sv::trim(sv::read_to_sep(header, ':')));
        if (sv::isnull(header)) {
            syntax(util::ss("no colon in header line: ", command));
            return false;
        }
        if (!words_separated_by_dashes(command)) {
            syntax(util::ss("invalid header: ", command));
            return false;
        }
        parse_header(command, sv::trim(header));
    }
}

bool HttpParser::parse_content_basic(std::string_view &view, bool closed) {
    if (content_length_ >= 0) {
        if (content_length_ > MAX_CONTENT_LENGTH) {
            oversized();
            return false;
        }
        if (int(view.size()) < content_length_) {
            incomplete(closed);
            return false;
        }
        content_ = sv::read_nbytes(view, content_length_);
    } else {
        if (int64_t(view.size()) > MAX_CONTENT_LENGTH) {
            oversized();
            return false;
        }
        if (!closed) {
            incomplete(closed);
            return false;
        }
        content_ = sv::read_nbytes(view, view.size());
    }
    return true;
}

bool HttpParser::parse_content_chunked(std::string_view &view, bool closed) {
    int64_t total_size = 0;
    std::vector<string_view> chunks;
    while (true) {
        std::string_view chunk_header = sv::trim(sv::read_to_line(view));
        if (sv::isnull(view)) {
            incomplete(closed);
            return false;
        }
        int64_t chunk_size = sv::to_hex64(chunk_header, -1);
        if (chunk_size < 0) {
            syntax("unparseable chunk header");
            return false;
        }
        if (chunk_size > MAX_CONTENT_LENGTH) {
            oversized();
            return false;
        }
        if (chunk_size == 0) break;
        total_size += chunk_size;
        if (total_size > MAX_CONTENT_LENGTH) {
            oversized();
            return false;
        }
        std::string_view chunk = sv::read_nbytes(view, chunk_size);
        if (int64_t(chunk.size()) != chunk_size) {
            incomplete(closed);
            return false;
        }
        std::string_view newline = sv::read_to_line(view);
        if (!newline.empty()) {
            syntax("corrupted chunk encoding");
            return false;
        }
        if (sv::isnull(view)) {
            incomplete(closed);
            return false;
        }
        chunks.push_back(chunk);
    }
    content_.resize(total_size);
    size_t offset = 0;
    for (string_view chunk : chunks) {
        content_.replace(offset, chunk.size(), chunk);
        offset += chunk.size();
    }
    return true;
}

bool HttpParser::parse_content(std::string_view &view, bool closed) {
    // If there's no body, just return true.
    if (is_request_) {
        if (!request_contains_content(content_length_, transfer_encoding_)) {
            return true;
        }
    } else {
        if (!response_contains_content(method_, status_)) {
            return true;
        }
    }

    // Parse the content.
    if (transfer_encoding_ == "") {
        if (!parse_content_basic(view, closed)) {
            return false;
        }
    } else if (transfer_encoding_ == "chunked") {
        if (!parse_content_chunked(view, closed)) {
            return false;
        }
    } else {
        syntax(util::ss("unsupported transfer-encoding: ", transfer_encoding_));
        return false;
    }

    // Uncompress the content.
    if ((content_encoding_ == "") || (content_encoding_ == "identity")) {
    } else {
        syntax(util::ss("content-encoding not supported: ", content_encoding_));
        return true;
    }

    // If the sender didn't specify content-type, make a guess based on the content.
    if (mime_type_.empty()) {
        if (sv::valid_utf8(content_)) {
            mime_type_ = "text/html";
            charset_ = "utf-8";
        } else {
            mime_type_ = "application/octet-stream";
            charset_ = "";
        }
    }

    // Switch the charset to utf-8, if it's text.
    if (sv::has_prefix(mime_type_, "text/")) {
        if (charset_.empty() || (charset_ == "ascii") || (charset_ == "utf-8")) {
            // we're already good.
        } else {
            // We can't convert charsets yet.
            syntax(util::ss("charset not supported: ", charset_));
            return true;
        }
    } else {
        // Not text.  No need to specify charset.
        charset_.clear();
    }
    return true;
}

void HttpParser::store(LuaCoreStack &LS0, LuaSlot tab) const {
    LuaVar ptab, djson;
    LuaExtStack LS(LS0.state(), ptab, djson);

    LS.newtable(tab);
    if (!is_request_) {
        LS.rawset(tab, "status", status_);
    }
    if (!error_.empty()) {
        LS.rawset(tab, "error", error_);
    }
    if (!location_.empty()) {
        LS.rawset(tab, "location", location_);
    }
    if (!mime_type_.empty() || !content_.empty()) {
        LS.rawset(tab, "mimetype", mime_type_);
        LS.rawset(tab, "content", content_);
        if (mime_type_ == "application/json") {
            json::decode(LS, djson, content_);
            LS.rawset(tab, "jsonvalue", djson);
        }
    }
    // We currently don't store the method, because
    // our server only supports GET.  Even if we
    // do support more methods in the future,  are
    // unlikely to want to handle this from inside lua.
    // 
    // if (!method_.empty()) {
    //     LS.rawset(tab, "method", method_);
    // }
    if (!path_.empty()) {
        LS.rawset(tab, "path", path_);
        LS.newtable(ptab);
        LS.rawset(tab, "params", ptab);
        for (const auto &pair : params_) {
            LS.rawset(ptab, pair.first, pair.second);
        }
    }

    // Debugging fields.  Do not use for lua programming.
    if (content_length_ >= 0) {
        LS.rawset(tab, "dbg_contentlength", content_length_);
    }
    if (!transfer_encoding_.empty()) {
        LS.rawset(tab, "dbg_transferencoding", transfer_encoding_);
    }
    if (!charset_.empty()) {
        LS.rawset(tab, "dbg_charset", charset_);
    }
    if (comm_length_ != 0) {
        LS.rawset(tab, "dbg_commlength", comm_length_);
    }
}

eng::string HttpParser::debug_string() const {
    eng::ostringstream oss;
    if (request_id_ != 0) {
        oss << "  request_id: " << request_id_ << std::endl;
    }
    oss << "  status_code: " << status_ << std::endl;
    oss << "  error: " << error_ << std::endl;
    if (content_length_ >= 0) {
        oss << "  content_length: " << content_length_ << std::endl;
    }
    if (!transfer_encoding_.empty()) {
        oss << "  transfer_encoding: " << transfer_encoding_ << std::endl;
    }
    if (!location_.empty()) {
        oss << "  location: " << location_ << std::endl;
    }
    if (!mime_type_.empty()) {
        oss << "  mime_type: " << mime_type_ << std::endl;
    }
    if (!charset_.empty()) {
        oss << "  charset: " << charset_ << std::endl;
    }
    if (!content_.empty()) {
        oss << "  content: " << content_ << std::endl;
    }
    if (!method_.empty()) {
        oss << "  method: " << method_ << std::endl;
    }
    if (!path_.empty()) {
        oss << "  path: " << path_ << std::endl;
    }
    for (const auto &pair : params_) {
        oss << "  param: " << pair.first << "=" << pair.second << std::endl;
    }
    if (comm_length_ > 0) {
        oss << "  comm_length: " << comm_length_ << std::endl;
    }
    return oss.str();
}

void HttpParser::parse_response(std::string_view view, bool closed, std::string_view method) {
    std::string_view original_view = view;
    is_request_ = false;
    method_ = method;

    // Parse the status line.
    if (!parse_status_line(view, closed)) {
        return;
    }

    // Parse the headers.
    if (!parse_headers(view, closed)) {
        return;
    }

    // Process the content.
    if (!parse_content(view, closed)) {
        return;
    }

    // Calculate the response length.
    comm_length_ = original_view.size() - view.size();

    // If it's not a redirect, ignore location.
    if ((status_ < 300) || (status_ > 399)) {
        location_.clear();
    }

    // If it's multipart, reject it.
    if (sv::has_prefix(mime_type_, "multipart/")) {
        syntax("multipart messages not supported");
        return;
    }
}

void HttpParser::parse_request(std::string_view view, bool closed) {
    std::string_view original_view = view;
    is_request_ = true;

    // Parse the request line.
    if (!parse_request_line(view, closed)) {
        return;
    }

    // Parse the headers.
    if (!parse_headers(view, closed)) {
        return;
    }

    // Process the content, if any.
    if (!parse_content(view, closed)) {
        return;
    }

    // Calculate the comm length.
    comm_length_ = original_view.size() - view.size();

    // Always ignore location.
    location_.clear();

    // If it's multipart, reject it.
    if (sv::has_prefix(mime_type_, "multipart/")) {
        syntax("multipart messages not supported");
        return;
    }

    // If we've made it this far, and there's no
    // status code, set it to 200 OK.
    if (status_ == 0) status_ = 200;
}

void HttpParser::store_fail(LuaCoreStack &LS, LuaSlot tab, int status_code, std::string_view error) {
    HttpParser parser;
    parser.fail(status_code, error);
    parser.store(LS, tab);
}

void HttpClientRequestMap::serialize(StreamBuffer *sb) const {
    sb->write_int32(size());
    for (const auto &pair : *this) {
        pair.second.serialize(sb);
    }
}

void HttpClientRequestMap::deserialize(StreamBuffer *sb) {
    int32_t count = sb->read_int32();
    clear();
    HttpClientRequest req;
    for (int i = 0; i < count; i++) {
        req.deserialize(sb);
        (*this)[req.request_id()] = req;
    }
}

LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL syntax") {
    LuaArg url;
    LuaRet fixed;
    LuaDefStack LS(L, url, fixed);
    ParsedURL parsed(LS.ckstring(url));
    if (!parsed.valid) {
        luaL_error(L, "invalid URL, not fixable");
        return LS.result();
    }
    LS.set(fixed, parsed.str());
    return LS.result();
}


LuaDefine(http_clientrequest, "request",
    "|Takes an HTTP client request in the form of a lua table."
    "|The table may contain these fields:"
    "|"
    "|  method             (ie, 'GET', 'POST', etc)"
    "|  host               (ie, 'google.com')"
    "|  port               (default: 443)"
    "|  path               (ie, '/index.html')"
    "|  params             (a table of url parameters)"
    "|  verifycertificate  (default: true)"
    "|  content            (the body to send to the server, if any)"
    "|  mimetype           (mime type of the body, if any)"
    "|"
    "|The table can also contain this field, which sets"
    "|host, port, path, and params in a single directive."
    "|"
    "|  url                (ie, 'https://host:port/path.html?a=b&c=d')"
    "|"
    "|The table can also contain these fields, which set"
    "|both the content and the mimetype in a single directive:"
    "|"
    "|  html      - content as a string (text/html)"
    "|  text      - content as a string (text/plain)"
    "|  json      - content as a string (application/json)"
    "|  bytes     - content as a string (application/octet-stream)"
    "|"
    "|You can specify url components separately (host, port, path,"
    "|and params), or you can specify the entire url as a unit. "
    "|If you specify components, they must not be url-encoded. "
    "|If you specify the url as a whole, it must already be url-encoded."
    "|"
    "|You can omit the port, in which case it defaults to the"
    "|standard https port.  You can omit verifycertificate, in which"
    "|case it defaults to true.  You can omit the method if the"
    "|method is implied by the function you called (eg, 'http.get')."
    "|"
    "|Note that unencrypted http is not supported - we only allow https."
    "|However, you can talk to a server that has a dummy certificate"
    "|by specifying verifycertificate=false."
    "|"
    "|This routine, http.clientrequest, returns a debug string for the "
    "|request.  The debug string looks like the actual http headers"
    "|that would be sent.") {
    LuaArg tab;
    LuaRet str;
    LuaDefStack LS(L, tab, str);
    LuaKeywordParser kp(LS, tab);
    HttpClientRequest req;
    req.configure(kp);
    kp.final_check_throw();
    req.set_defaults();
    eng::string error = req.check();
    if (!error.empty()) {
        luaL_error(L, "%s", error.c_str());
        return 0;
    }
    LS.set(str, req.debug_string());
    return LS.result();
}

LuaDefine(http_clientresponse, "response",
    "|Returns an HTTP client response in the form of a lua table."
    "|The table will contain these important fields:"
    "|"
    "|  status    - 3-digit HTTP response code."
    "|  error     - an error message, or nil if no error."
    "|  content   - on success, the content, as a string."
    "|  mimetype  - on success, the mime type of the content."
    "|  location  - for HTTP redirects, the target url."
    "|"
    "|If the mimetype is a text mimetype, then the content"
    "|is automatically converted to utf-8."
    "|"
    "|The table may also contain these debugging-only fields."
    "|"
    "|  dbg_transferencoding - If there was a Transfer-Encoding header."
    "|  dbg_contentlength    - If there was a Content-length header."
    "|  dbg_charset          - Original character set for text mime types."
    "|  dbg_commlength       - Total bytes in the communication."
    "|"
    "|None of the dbg fields is needed to understand the response."
    "|For example, consider dbg_charset.  When text content is"
    "|passed to lua, the content is automatically converted to utf-8."
    "|So dbg_charset only tells you what the character set used"
    "|to be, before it was converted to utf-8."
    "|"
    "|If an http routine generates an error, that error will be"
    "|expressed as a status code.  These locally-generated status"
    "|codes can be:"
    "|"
    "|   400 (bad request)          - the request was malformed"
    "|   503 (service unavailable)  - dns fail, connect fail, or ssl fail"
    "|   500 (internal server error)- the response contains invalid HTTP"
    "|   413 (payload too large)    - we refuse to download something so big"
    "|   425 (can't resume)         - reloaded a save game with a pending request"
    "|"
    "|Error messages that are generated locally consist of "
    "|the standard message (eg, 'bad request') followed by more "
    "|detailed information."
    "|"
    "|This routine, http.clientresponse, generates a response by parsing"
    "|an actual HTTP response string.  This is for debugging only.") {
    LuaArg text;
    LuaRet tab;
    LuaDefStack LS(L, text, tab);
    HttpParser parser;
    parser.parse_response(LS.ckstring(text), true, "GET");
    parser.store(LS, tab);
    return LS.result();
}

LuaDefine(http_serverrequest, "request",
    "|Returns an HTTP server request in the form of a lua table."
    "|The table will contain these important fields:"
    "|"
    "|  status    - 3-digit HTTP response code."
    "|  error     - an error message, or nil if no error."
    "|  method    - GET, HEAD, or POST"
    "|  path      - the url-decoded path, eg, '/index.html'"
    "|  params    - a table of url-decoded URL parameters"
    "|  content   - the content, as a string (POST only)"
    "|  mimetype  - the mime type of the content (POST only)"
    "|"
    "|If the mimetype is a text mimetype, then the content"
    "|is automatically converted to utf-8."
    "|"
    "|The table may also contain these debugging-only fields."
    "|"
    "|  dbg_transferencoding - If there was a Transfer-Encoding header."
    "|  dbg_contentlength    - If there was a Content-length header."
    "|  dbg_charset          - Original character set for text mime types."
    "|  dbg_commlength       - Total bytes in the communication."
    "|"
    "|None of the dbg fields is needed to understand the request."
    "|For example, consider dbg_charset.  When text content is"
    "|passed to lua, the content is automatically converted to utf-8."
    "|So dbg_charset only tells you what the character set used"
    "|to be, before it was converted to utf-8."
    "|"
    "|When the engine is functioning as a webserver, bad requests "
    "|are never passed to lua.  Therefore, a request that is passed "
    "|to lua will always contain status=200 and error=nil.  However, "
    "|when debugging server requests using http.serverrequest, "
    "|it is possible to see certain other errors:"
    "|"
    "|   400 (bad request)          - the request was malformed"
    "|   503 (service unavailable)  - dns fail, connect fail, or ssl fail"
    "|   500 (internal server error)- the response contains invalid HTTP"
    "|   413 (payload too large)    - we refuse to download something so big"
    "|   425 (can't resume)         - reloaded a save game with a pending request"
    "|"
    "|Error messages that are generated locally consist of "
    "|the standard message (eg, 'bad request') followed by more "
    "|detailed information."
    "|"
    "|This routine, http.serverrequest, generates a request by parsing"
    "|an actual HTTP request string.  This is for debugging only.") {
    LuaArg text;
    LuaRet tab;
    LuaDefStack LS(L, text, tab);
    HttpParser parser;
    parser.parse_request(LS.ckstring(text), true);
    parser.store(LS, tab);
    return LS.result();
}

LuaDefine(http_serverresponse, "response",
    "|Takes an HTTP server response in the form of a lua table."
    "|The table may contain these fields:"
    "|"
    "|  status    - status result"
    "|  content   - content as a string"
    "|  mimetype  - mime type of the content"
    "|  maxage    - enables client-side caching"
    "|"
    "|The table can also contain these fields, which set"
    "|both the content and the mimetype in a single directive:"
    "|"
    "|  html      - content as a string (text/html)"
    "|  text      - content as a string (text/plain)"
    "|  json      - content as a string (application/json)"
    "|  bytes     - content as a string (application/octet-stream)"
    "|"
    "|To send a successful response to a GET or HEAD request,"
    "|you must supply content and mimetype (you can supply both"
    "|in a single field, if desired).  We recommend omitting the"
    "|status code."
    "|"
    "|To send a successful response to a POST request, you only"
    "|need to supply status=201 or status='Created'."
    "|"
    "|To send an error response, you must supply a status"
    "|code indicating the error.  The status must be one of the"
    "|standard HTTP status codes, expressed either as a 3-digit"
    "|number (eg, 404), or a string (eg, 'Not Found')."
    "|"
    "|Not every HTTP status code is currently supported:"
    "|"
    "|  * Success codes 200 and 201 are supported"
    "|  * Other success codes (202-299) are not supported"
    "|  * Informational codes (100-199) are not supported"
    "|  * Redirects (300-399) are not supported"
    "|  * All error codes (400-599) are supported"
    "|"
    "|By default, this server asks the client not to cache"
    "|anything.  If you specify maxage, however, then you"
    "|are giving the client permission to cache."
    "|"
    "|If the lua server throws an error, or returns an"
    "|invalid response, the error will be reported through"
    "|the browser, as a 500 Internal Server Error."
    "|"
    "|This routine, http.serverresponse, returns a debug string for the "
    "|response.  The debug string looks like the actual http response"
    "|that would be sent.") {
    LuaArg tab;
    LuaRet str;
    LuaDefStack LS(L, tab, str);
    LuaKeywordParser kp(LS, tab);
    HttpServerResponse resp;
    resp.configure(kp);
    kp.final_check_throw();
    resp.set_defaults();
    LS.set(str, resp.debug_string());
    return LS.result();
}


LuaDefine(http_validmime, "(mt)", "") {
    LuaArg str;
    LuaRet ok;
    LuaDefStack LS(L, str, ok);
    LS.set(ok, valid_mime_type(LS.ckstring(str)));
    return LS.result();
}

LuaDefine(http_statusstring, "(statuscode)", "Convert a 3-digit status code to a string") {
    LuaArg code;
    LuaRet str;
    LuaDefStack LS(L, code, str);
    int icode = LS.ckint(code);
    LS.set(str, status_code_to_string(icode));
    return LS.result();
}

LuaDefine(http_statuscode, "(statusstring)", "Convert a string to a 3-digit status code") {
    LuaArg str;
    LuaRet code;
    LuaDefStack LS(L, code, str);
    eng::string sstr = LS.ckstring(str);
    LS.set(code, status_code_from_string(sstr));
    int iresult = LS.result();
    return iresult;
}
