#include "Request.h"
#include <ctime>
#include <cstdlib>
#include <type_traits>

namespace panda { namespace protocol { namespace http {

static bool _init () {
    std::srand(std::time(NULL));
    return true;
}

static const bool _inited = _init();

static inline string _method_str (Request::Method rm) {
    using Method = Request::Method;
    switch (rm) {
        case Method::Options : return "OPTIONS";
        case Method::Get     : return "GET";
        case Method::Head    : return "HEAD";
        case Method::Post    : return "POST";
        case Method::Put     : return "PUT";
        case Method::Delete  : return "DELETE";
        case Method::Trace   : return "TRACE";
        case Method::Connect : return "CONNECT";
        default: return "[UNKNOWN]";
    }
}

string Request::_generate_boundary() noexcept {
    const constexpr size_t SZ = (string::MAX_SSO_CHARS / sizeof (int)) + (string::MAX_SSO_CHARS % sizeof (int) == 0 ? 0 : 1);
    const constexpr char alphabet[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    const constexpr size_t alphabet_sz = sizeof (alphabet) - 1;
    int dices[SZ];
    string r(40, '-');
    for(size_t i = 0; i <SZ; ++i) { dices[i] = std::rand(); }

    const char* random_bytes = (const char*)dices;
    for(size_t i = r.size() - 17; i < r.size(); ++i) {
        r[i] = alphabet[*random_bytes++ % alphabet_sz];
    }

    return r;
}

Request::Method Request::method() const noexcept {
    if (_method == Method::Unspecified) {
        bool use_post = (form && form.enc_type() == EncType::Multipart && (!form.empty() || (uri && !uri->query().empty()))) // complete form
                     || _form_streaming != FormStreaming::None;
        return use_post ? Method::Post : Method::Get;
    }
    return _method;
}

static inline bool _method_has_meaning_for_body (Request::Method method) {
    return method == Request::Method::Post || method == Request::Method::Put;
}

string Request::_http_header (SerializationContext& ctx) const {
    //part 1: precalc pieces
    auto eff_method  = method();
    bool body_method = _method_has_meaning_for_body(eff_method);
    auto out_meth    = _method_str(eff_method);
    auto eff_uri     = ctx.uri;

    auto out_reluri  = eff_uri ? eff_uri->relative() : string("/");

    auto tmp_http_ver = !ctx.http_version ? 11 : ctx.http_version;
    string out_content_length;
    bool calc_content_length
              = !ctx.chunked
            && (ctx.body->parts.size() || body_method)
            && !headers.has("Content-Length");
    if (calc_content_length) out_content_length = panda::to_string(ctx.body->length());

    size_t sz_host = 0;
    size_t sz_host_port = 0;
    if (!headers.has("Host") && eff_uri && eff_uri->host()) {
        // Host field builder
        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host
        sz_host = eff_uri->host().length();
        auto& scheme = eff_uri->scheme();
        auto port = eff_uri->port();
        if ((!scheme) || (scheme == "http" && port != 80) || (scheme == "https" && port != 443)) {
            sz_host_port = 6;
        }
    }

    string out_accept_encoding;
    if (compression_prefs && compression_prefs != static_cast<compression::storage_t>(Compression::IDENTITY) && !headers.has("Accept-Encoding")) {
        string comp_pos, comp_neg;
        int index_pos = 0, index_neg = 0;
        compression::for_each(compression_prefs, [&](auto value, bool negation){
            const char* val = nullptr;
            switch (value) {
                case Compression::GZIP   : val = "gzip"; break;
                case Compression::BROTLI : val = "br";   break;
                default: return;
            }
            if (negation) {
                if (index_neg) { comp_neg += ", "; }
                comp_neg += val;
                comp_neg += ";q=0";
                ++index_neg;
            }  else {
                if (index_pos) { comp_pos += ", "; }
                comp_pos += val;
                ++index_pos;
            }
        });
        if (index_neg) {
            if (index_pos) { comp_pos += ", "; }
             comp_pos += comp_neg;
        }
        if (comp_pos) { out_accept_encoding = comp_pos; }
    }

    auto out_content_encoding = _content_encoding(ctx);
    size_t sz_cookies = 0;
    if (cookies.size()) {
        for (auto& f : cookies.fields) sz_cookies += f.name.length() + f.value.length() + 3; // 3 for ' ', '=' and ';' for each pair
    }

    // part 2: summarize pieces size
    size_t reserved = out_meth.length();
    reserved += out_reluri.length();
    reserved += 5 + 6 + 2 + 1;   /* http-version  + trailer */

    if (out_content_length)   reserved += 14 + 2 + out_content_length.length()   + 2;
    if (sz_host)              reserved += 4  + 2 + sz_host + sz_host_port        + 2;
    if (out_accept_encoding)  reserved += 15 + 2 + out_accept_encoding.length()  + 2;
    if (out_content_encoding) reserved += 16 + 2 + out_content_encoding.length() + 2;
    if (sz_cookies)           reserved += 6  + 2 + sz_cookies                    + 2;

    for (auto& h: ctx.handled_headers) { reserved += h.name.length() + 2 + h.value.length() + 2; }
    for (auto& h: headers)  {
        if (ctx.handled_headers.has(h.name)) continue;
        reserved += h.name.length() + 2 + h.value.length() + 2;
    }

    // part 3: write out pieces
    string s(reserved);
    s += out_meth;
    s += ' ';
    s += out_reluri;
    s += " HTTP/";
    if (tmp_http_ver == 11) s += "1.1\r\n";
    else                    s += "1.0\r\n";

    if (sz_host) {
        s += "Host: " ;
        s += eff_uri->host();
        if (sz_host_port) { s+= ":";  s += panda::to_string(eff_uri->port()); }
        s += "\r\n";
    };

    if (out_content_length)   { s += "Content-Length: "  ; s += out_content_length  ; s += "\r\n" ;};
    if (out_accept_encoding)  { s += "Accept-Encoding: " ; s += out_accept_encoding ; s += "\r\n" ;};
    if (out_content_encoding) { s += "Content-Encoding: "; s += out_content_encoding; s += "\r\n" ;};

    if (sz_cookies) {
        s += "Cookie: ";
        auto sz = cookies.size();
        for (size_t i = 0; i < sz; ++i) {
            if (i) { s += "; "; }
            const auto& f = cookies.fields[i];
            s += f.name;
            s += '=';
            s += f.value;
        }
        s += "\r\n";
    }

    for (auto& h: ctx.handled_headers) { s += h.name; s += ": "; s += h.value; s+= "\r\n"; }
    for (auto& h: headers)  {
        if (ctx.handled_headers.has(h.name)) continue;
        s += h.name; s += ": "; s += h.value; s+= "\r\n";
    }
    s += "\r\n";

    //assert((sz_cookies + headers.size()) ==  0);
    //assert(reserved >= s.length());

    return s;
}

std::vector<string> Request::to_vector () const {
    SerializationContext ctx;
    if (_form_streaming != FormStreaming::None && _form_streaming != FormStreaming::Started)
        throw "form streaming wasn't finished";

    bool form_streaming =  _form_streaming == FormStreaming::Started;
    /* it seems nobody supports muliptart + gzip + chunk */
    ctx.compression = !form_streaming ? compression.type : Compression::Type::IDENTITY;
    ctx.body        = &body;
    ctx.uri         = uri.get();
    ctx.chunked     = this->chunked || form_streaming;

    auto add_form_header = [&](auto& boundary) {
        string ct = "multipart/form-data; boundary=";
        ct += boundary;
        ctx.handled_headers.add("Content-Type", ct);
    };

    Body form_body;
    URI form_uri;
    if (form) {
        if (form.enc_type() == EncType::Multipart) {
            if (!form.empty() || (uri && !uri->query().empty())) {
                auto boundary = form_streaming ? _form_boundary : _generate_boundary();
                ctx.uri  = form.to_body(form_body, form_uri, uri, boundary);
                ctx.body = &form_body;
                add_form_header(boundary);
            } else if (form_streaming) {
                add_form_header(_form_boundary);
            }
        }
        else if((form.enc_type() == EncType::UrlEncoded) && !form.empty()) {
            form.to_uri(form_uri, uri);
            ctx.uri = &form_uri;
        }
    }
    else if (form_streaming) {
        add_form_header(_form_boundary);
    }
    return _to_vector(ctx, [&]() { return _compile_prepare(ctx); }, [&]() { return _http_header(ctx); });
}

bool Request::expects_continue () const {
    for (auto& val : headers.get_multi("Expect")) if (val == "100-continue") return true;
    return false;
}

std::uint8_t Request::allowed_compression (bool inverse) const noexcept {
    std::uint8_t result = 0;
    compression::for_each(compression_prefs, [&](auto value, bool negation){
        if (inverse == negation) {
            result |= value;
        }
    });
    return result;
}

static string form_trailer(const string& boundary) noexcept {
    auto sz = boundary.size() + 6;
    string r(sz);
    r += "--";
    r += boundary;
    r += "--\r\n";
    return r;
}

namespace tag {
    using uri = std::integral_constant<int, 0>;
    using form = std::integral_constant<int, 1>;
}

template<typename Tag> struct Helper;

template<> struct Helper<tag::form> {
    using Field =  Request::Form::value_type;

    struct PatrialField {
        string name;
        string mime_type;
        string filename;
        bool complete = false;
    };

    struct FullField: PatrialField {
        FullField(const string& name_, const string& filename_, const string& mime_type_, const string& value_):
            PatrialField{name_, mime_type_, filename_, true}, value{value_}{}
        string value;
    };

    static size_t buffer_size(const string &boundary, const Request::Form& container) noexcept {
        auto fields_count = container.size();
        size_t size = (
                boundary.length() + 4   /* "--" prefix and "\r\n" */
                + 37 + 2                /* Content-Disposition: form-data; name="" + \r\n */
            ) * fields_count  + 2;      /* -- */
        for(auto it : container) {
            size += it.second.value.length();
            auto& name = it.second.name;
            if (name) {
                size += name.length() + 14;   //; filename=""
            }
            auto& ct = it.second.content_type;
            if (ct) {
                size += ct.size() + 18; //Content-Type: xxx\r\n
            }
        }
        return size;
    }

    static void append_header(string& r, const string& header, const string& value) noexcept {
        r += header;
        r += ": ";
        r += value;
        r += "\r\n";
    }

    static void append(string& r, const PatrialField& field, const string& boundary) noexcept {
        r += "--";
        r += boundary;
        r += "\r\n";
        r += "Content-Disposition: form-data; name=\"";
        r += field.name;
        r += "\"";
        if (field.filename) {
            r+= "; filename=\"";
            r += field.filename;
            r += "\"";
        }
        r += "\r\n";

        if (field.mime_type) {
            r += "Content-Type: ";
            r += field.mime_type;
            r += "\r\n";
        }

        if (field.complete) r += "\r\n";
    }

    static void append(string& r, const FullField& field, const string& boundary) noexcept {
        append(r, (const PatrialField&)field, boundary);
        r += field.value;
        r += "\r\n";
    }

    static void append(string& r, const Field& it, const string& boundary) noexcept {
        append(r, FullField{it.first, it.second.name, it.second.content_type, it.second.value}, boundary);
    }

};

template<> struct Helper<tag::uri> {
    using Field =  string_multimap<string, string>::value_type;

    static size_t buffer_size(const string &boundary, const string_multimap<string, string>& container) noexcept {
        auto fields_count= container.size();
        size_t size = (
                boundary.length() + 4   /* "--" prefix and "\r\n" */
                + 37 + 2                /* Content-Disposition: form-data; name="" + \r\n */
            ) * fields_count  + 2;      /* -- */
        for(auto it : container) {
            size += it.second.length();
        }
        return size;
    }

    static void append(string& r, const Field& it, const string& boundary) noexcept {
        r += "--";
        r += boundary;
        r += "\r\n";
        r += "Content-Disposition: form-data; name=\"";
        r += it.first;
        r += "\"";
        r += "\r\n";
        r += "\r\n";
        r += it.second;
        r += "\r\n";
    }
};

void Request::form_file_finalize(string& out) noexcept {
    if (_form_streaming == FormStreaming::File) {
        if (compressor) {
            out += compressor->flush();
            compressor.reset();
        }
        out += "\r\n"; /* finalize file */
    }
}


Request::wrapped_chunk Request::form_finish() {
    if (_form_streaming == FormStreaming::None) throw "form streaming was not started";
    if (_form_streaming == FormStreaming::Done) throw "form streaming already complete";

    string data;
    form_file_finalize(data);
    data += form_trailer(_form_boundary);
    _form_streaming = FormStreaming::Done;
    return final_chunk(data);
}

Request::wrapped_chunk Request::form_field(const string& name, const string& content, const string& filename, const string& mime_type) {
    using H = Helper<tag::form>;
    if (_form_streaming == FormStreaming::None) throw "form streaming was not started";
    if (_form_streaming == FormStreaming::Done) throw "form streaming already complete";

    string data;
    form_file_finalize(data);
    H::append(data, H::FullField{name, filename, mime_type, content}, _form_boundary);
    return make_chunk(data, compression::CompressorPtr{});  // we don't compress
}

Request::wrapped_chunk Request::form_file(const string& name, const string filename, const string& mime_type) {
    using H = Helper<tag::form>;
    if (_form_streaming == FormStreaming::None) throw "form streaming was not started";
    if (_form_streaming == FormStreaming::Done) throw "form streaming already complete";

    string data;
    form_file_finalize(data);
    _form_streaming = FormStreaming::File;

    H::append(data, H::PatrialField{name, mime_type, filename, false}, _form_boundary);
    data += "\r\n";

    return make_chunk(data, compression::CompressorPtr{});  // we don't compress
}

Request::wrapped_chunk Request::form_data(const string& content) {
    if (_form_streaming != FormStreaming::File) throw "form file streaming was not started";
    return make_chunk(content);
}

template<typename Tag, typename Container>
void _serialize(Body& body, const string &boundary, const Container& container) {
    using H = Helper<Tag>;
    string r(H::buffer_size(boundary, container));
    for(auto it : container) { H::append(r, it, boundary); }
    r += form_trailer(boundary);
    body.parts.emplace_back(r);
}

const uri::URI *Request::Form::to_body(Body& body, uri::URI &uri, const uri::URISP original_uri, const string &boundary) const noexcept {
    if (empty()) {
        if (!original_uri) return {};
        auto& q = original_uri->query();
        _serialize<tag::uri>(body, boundary, q);
        uri = URI(*original_uri);
        uri.query().clear();
        return &uri;
    } else {
        _serialize<tag::form>(body, boundary, *this);
        return original_uri.get();
    }
}

void Request::Form::to_uri  (uri::URI &uri, const URISP original_uri) const  {
    if (original_uri) uri = *original_uri;
    else              uri = "/";
    auto& q = uri.query();
    for(auto it : *this) {
        auto& named = it.second;
        if (named.name) throw string("form contains named field (filename) " + it.second.value + ", it cannot be converted to URI");
        q.insert({it.first, it.second.value});
    }
}


}}}
