#pragma once
#include "Body.h"
#include "error.h"
#include "Headers.h"
#include "compression/Compressor.h"
#include <array>
#include <panda/refcnt.h>
#include <panda/uri/URI.h>

namespace panda { namespace protocol { namespace http {

enum class State {headers, body, chunk, chunk_body, chunk_trailer, done, error};

using compression::Compression;
using compression::is_valid_compression;

struct Request;
using panda::uri::URI;
using panda::uri::URISP;

struct Message : virtual Refcnt {
    template <class, class> struct Builder;
    using wrapped_chunk = std::array<string, 3>;

    Headers headers;
    Body    body;
    bool    chunked      = false;
    int     http_version = 0;

    struct CompressionParams {
        Compression::Type  type  = Compression::IDENTITY;
        Compression::Level level = Compression::Level::min;
    } compression;

    mutable compression::CompressorPtr compressor;

    Message () {}

    Message (Headers&& headers, Body&& body, bool chunked = false, int http_version = 0) :
        headers(std::move(headers)), body(std::move(body)), chunked(chunked), http_version(http_version)
    {}

    bool keep_alive () const;
    void keep_alive (bool val) { val ? headers.connection("keep-alive") : headers.connection("close"); }

    void compress (Compression::Type type, Compression::Level level = Compression::Level::min) {
        compression = CompressionParams{type, level};
    }

    wrapped_chunk make_chunk (const string& s) const { return make_chunk(s, compressor); }
    wrapped_chunk final_chunk (const string& s = "") const { return final_chunk(s, compressor); }

protected:
    struct SerializationContext {
        int                         http_version;
        const Body*                 body;
        GenericHeaders<2>           handled_headers;
        Compression::Type           compression;
        compression::CompressorPtr  compressor;
        const Request*              request;
        bool                        chunked;
    };

    static string to_string (const std::vector<string>& pieces);

    static inline string _compression_string(Compression::Type type) noexcept {
        switch (type) {
        case Compression::GZIP:     return "gzip";
        case Compression::DEFLATE:  return "deflate";
        case Compression::BROTLI:   return "br";
        case Compression::IDENTITY: return string{};
        }
        std::terminate();
    }

    inline string _content_encoding(const SerializationContext& ctx) const noexcept {
        if (!headers.has("Content-Encoding") && ctx.compressor) {
            return _compression_string(ctx.compression);
        }
        return string{};
    }

    inline void _compile_prepare (SerializationContext& ctx) const {
        if (ctx.chunked) {
            ctx.http_version = 11;
            // special header, to prevent multiple TEnc headers
            ctx.handled_headers.set("Transfer-Encoding", "chunked");
        } else {
            ctx.http_version = http_version;
        }

        // content-length logic is in request/response because it slightly differs
    }


    template <typename PrepareHeaders, typename CompileHeaders>
    inline std::vector<string> _to_vector (SerializationContext& ctx, PrepareHeaders&& prepare, CompileHeaders&& compile) const {
        _prepare_compressor(ctx, compression.level);

        Body mutable_body;
        if (ctx.compressor && !ctx.chunked) {
            compress_body(*ctx.compressor, *ctx.body, mutable_body);
            ctx.body = &mutable_body;
        }

        prepare();
        auto compiled_headers = compile();
        if (!ctx.body->length()) return  {compiled_headers};
        auto sz = ctx.body->parts.size();

        std::vector<string> result;
        if (ctx.chunked) {
            result.reserve(1 + sz * 3 + 1);
            result.emplace_back(compiled_headers);
            auto append_piecewise = [&](auto& piece) { result.emplace_back(piece); };
            _serialize_body(ctx, append_piecewise);
        } else {
            result.reserve(1 + sz);
            result.emplace_back(compiled_headers);
            for (auto& part : ctx.body->parts) result.emplace_back(part);
        }

        return result;
    }

    static inline compression::CompressorPtr _get_compressor (Compression::Type type, Compression::Level level) {
        if (type != Compression::IDENTITY) {
            auto c = compression::instantiate(type);
            if (c) {
                c->prepare_compress(level);
                return c;
            }
        }
        return compression::CompressorPtr{};
    }

    inline wrapped_chunk make_chunk (const string& s, const compression::CompressorPtr& compr) const {
        if (!s) return {"", "", ""};
        if(compr) {
            return wrap_into_chunk(compr->compress(s));
        } else {
            return wrap_into_chunk(s);
        }
    }

private:
    inline wrapped_chunk wrap_into_chunk (const string& s) const {
        if (!s) return {"", "", ""};
        return {string::from_number(s.length(), 16) + "\r\n", s, "\r\n"};
    }

    inline wrapped_chunk final_chunk (const string& s, const compression::CompressorPtr& compr) const {
        if (compr) {
            string content;
            if (s) { content += compr->compress(s); }
            content += compr->flush();
            auto chunk = wrap_into_chunk(content);
            chunk[2] += "0\r\n\r\n";
            return chunk;
        } else {
            if (s) {
                return {
                    string::from_number(s.length(), 16) + "\r\n",
                    s,
                    "\r\n0\r\n\r\n"
                };
            }
            else return {"", "","0\r\n\r\n"};
        }
    }

    void compress_body(compression::Compressor& compressor, const Body& src, Body& dst) const;

    template<typename Fn>
    inline void _append_chunk (wrapped_chunk chunk, Fn&& fn) const {
        for (auto& piece : chunk) if (piece) { fn(piece); }
    }

    template<typename Fn>
    inline void _serialize_body (SerializationContext& ctx, Fn&& fn) const {
        for (auto& part : ctx.body->parts) {
            _append_chunk(make_chunk(part, ctx.compressor), fn);
        }
        _append_chunk(final_chunk("", ctx.compressor), fn);
    }

    inline void _prepare_compressor (SerializationContext& ctx, Compression::Level level) const {
        auto value = _get_compressor(ctx.compression, level);
        if (value) { compressor = ctx.compressor = value; }
        else { ctx.compression = Compression::IDENTITY; } // reset back
    }

    //friend struct Parser; friend struct RequestParser; friend struct ResponseParser;
};
using MessageSP = iptr<Message>;

template <class T, class MP>
struct Message::Builder {
    T& headers (Headers&& headers) {
        _message->headers = std::move(headers);
        return self();
    }

    T& header (const string& k, const string& v) {
        _message->headers.add(k, v);
        return self();
    }

    T& body (Body&& val, const string& content_type = "") {
        _message->body = std::move(val);
        if (content_type) _message->headers.add("Content-Type", content_type);
        return self();
    }

    T& body (const string& body, const string& content_type = "") {
        _message->body = body;
        if (content_type) _message->headers.add("Content-Type", content_type);
        return self();
    }

    T& http_version (int http_version) {
        _message->http_version = http_version;
        return self();
    }

    T& chunked (const string& content_type = "") {
        _message->chunked = true;
        if (content_type) _message->headers.add("Content-Type", content_type);
        return self();
    }

    T& compress (Compression::Type method, Compression::Level level = Compression::Level::min) {
        _message->compress(method, level);
        return self();
    }

    MP build () { return _message; }

protected:
    MP _message;

    Builder (const MP& msg) : _message(msg) {}

    T& self () { return static_cast<T&>(*this); }
};


}}}