/**
 *
 *  @file HttpRequest.cc
 *  An Tao
 *
 *  Copyright 2018, An Tao.  All rights reserved.
 *  https://github.com/an-tao/drogon
 *  Use of this source code is governed by a MIT license
 *  that can be found in the License file.
 *
 *  Drogon
 *
 */

#include "http_request.h"

#include "http_file_upload_request.h"
//#include "HttpAppFrameworkImpl.h"

#include <core/utilities.h>

#include "core/application.h"

#include <fstream>
#include <iostream>
#ifndef _WIN32
#include <unistd.h>
#endif

using namespace drogon;

void HttpRequest::parseJson() const
{
    auto input = contentView();
    if (input.empty())
        return;
    if (contentType_ == CT_APPLICATION_JSON ||
        getHeaderBy("content-type").find("application/json") !=
            std::string::npos)
    {
        static std::once_flag once;
        static Json::CharReaderBuilder builder;
        std::call_once(once, []() { builder["collectComments"] = false; });
        jsonPtr_ = std::make_shared<Json::Value>();
        JSONCPP_STRING errs;
        std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
        if (!reader->parse(input.data(),
                           input.data() + input.size(),
                           jsonPtr_.get(),
                           &errs))
        {
            LOG_ERROR << errs;
            jsonPtr_.reset();
            jsonParsingErrorPtr_ =
                std::make_unique<std::string>(std::move(errs));
        }
        else
        {
            jsonParsingErrorPtr_.reset();
        }
    }
    else
    {
        jsonPtr_.reset();
        jsonParsingErrorPtr_ =
            std::make_unique<std::string>("content type error");
    }
}
void HttpRequest::parseParameters() const
{
    auto input = queryView();
    if (!input.empty())
    {
        string_view::size_type pos = 0;
        while ((input[pos] == '?' || isspace(input[pos])) &&
               pos < input.length())
        {
            ++pos;
        }
        auto value = input.substr(pos);
        while ((pos = value.find('&')) != string_view::npos)
        {
            auto coo = value.substr(0, pos);
            auto epos = coo.find('=');
            if (epos != string_view::npos)
            {
                auto key = coo.substr(0, epos);
                string_view::size_type cpos = 0;
                while (cpos < key.length() && isspace(key[cpos]))
                    ++cpos;
                key = key.substr(cpos);
                auto pvalue = coo.substr(epos + 1);
                std::string pdecode = utils::urlDecode(pvalue);
                std::string keydecode = utils::urlDecode(key);
                parameters_[keydecode] = pdecode;
            }
            value = value.substr(pos + 1);
        }
        if (value.length() > 0)
        {
            auto &coo = value;
            auto epos = coo.find('=');
            if (epos != string_view::npos)
            {
                auto key = coo.substr(0, epos);
                string_view::size_type cpos = 0;
                while (cpos < key.length() && isspace(key[cpos]))
                    ++cpos;
                key = key.substr(cpos);
                auto pvalue = coo.substr(epos + 1);
                std::string pdecode = utils::urlDecode(pvalue);
                std::string keydecode = utils::urlDecode(key);
                parameters_[keydecode] = pdecode;
            }
        }
    }

    input = contentView();
    if (input.empty())
        return;
    std::string type = getHeaderBy("content-type");
    std::transform(type.begin(), type.end(), type.begin(), tolower);
    if (type.empty() ||
        type.find("application/x-www-form-urlencoded") != std::string::npos)
    {
        string_view::size_type pos = 0;
        while ((input[pos] == '?' || isspace(input[pos])) &&
               pos < input.length())
        {
            ++pos;
        }
        auto value = input.substr(pos);
        while ((pos = value.find('&')) != string_view::npos)
        {
            auto coo = value.substr(0, pos);
            auto epos = coo.find('=');
            if (epos != string_view::npos)
            {
                auto key = coo.substr(0, epos);
                string_view::size_type cpos = 0;
                while (cpos < key.length() && isspace(key[cpos]))
                    ++cpos;
                key = key.substr(cpos);
                auto pvalue = coo.substr(epos + 1);
                std::string pdecode = utils::urlDecode(pvalue);
                std::string keydecode = utils::urlDecode(key);
                parameters_[keydecode] = pdecode;
            }
            value = value.substr(pos + 1);
        }
        if (value.length() > 0)
        {
            auto &coo = value;
            auto epos = coo.find('=');
            if (epos != string_view::npos)
            {
                auto key = coo.substr(0, epos);
                string_view::size_type cpos = 0;
                while (cpos < key.length() && isspace(key[cpos]))
                    ++cpos;
                key = key.substr(cpos);
                auto pvalue = coo.substr(epos + 1);
                std::string pdecode = utils::urlDecode(pvalue);
                std::string keydecode = utils::urlDecode(key);
                parameters_[keydecode] = pdecode;
            }
        }
    }
}

void HttpRequest::appendToBuffer(trantor::MsgBuffer *output) const
{
    switch (method_)
    {
        case Get:
            output->append("GET ");
            break;
        case Post:
            output->append("POST ");
            break;
        case Head:
            output->append("HEAD ");
            break;
        case Put:
            output->append("PUT ");
            break;
        case Delete:
            output->append("DELETE ");
            break;
        case Options:
            output->append("OPTIONS ");
            break;
        case Patch:
            output->append("PATCH ");
            break;
        default:
            return;
    }

    if (!path_.empty())
    {
        output->append(utils::urlEncode(path_));
    }
    else
    {
        output->append("/");
    }

    std::string content;

    if (!passThrough_ &&
        (!parameters_.empty() && contentType_ != CT_MULTIPART_FORM_DATA))
    {
        for (auto const &p : parameters_)
        {
            content.append(utils::urlEncodeComponent(p.first));
            content.append("=");
            content.append(utils::urlEncodeComponent(p.second));
            content.append("&");
        }
        content.resize(content.length() - 1);
        if (method_ == Get || method_ == Delete || method_ == Head)
        {
            auto ret = std::find(output->peek(),
                                 (const char *)output->beginWrite(),
                                 '?');
            if (ret != output->beginWrite())
            {
                if (ret != output->beginWrite() - 1)
                {
                    output->append("&");
                }
            }
            else
            {
                output->append("?");
            }
            output->append(content);
            content.clear();
        }
        else if (contentType_ == CT_APPLICATION_JSON)
        {
            /// Can't set parameters in content in this case
            LOG_ERROR
                << "You can't set parameters in the query string when the "
                   "request content type is JSON and http method "
                   "is POST or PUT";
            LOG_ERROR << "Please put these parameters into the path or "
                         "into the json "
                         "string";
            content.clear();
        }
    }

    output->append(" ");
    if (version_ == Version::kHttp11)
    {
        output->append("HTTP/1.1");
    }
    else if (version_ == Version::kHttp10)
    {
        output->append("HTTP/1.0");
    }
    else
    {
        return;
    }
    output->append("\r\n");

    if (!passThrough_ && contentType_ == CT_MULTIPART_FORM_DATA)
    {
        auto mReq = dynamic_cast<const HttpFileUploadRequest *>(this);
        if (mReq)
        {
            for (auto &param : mReq->getParameters())
            {
                content.append("--");
                content.append(mReq->boundary());
                content.append("\r\n");
                content.append("Content-Disposition: form-data; name=\"");
                content.append(param.first);
                content.append("\"\r\n\r\n");
                content.append(param.second);
                content.append("\r\n");
            }
            for (auto &file : mReq->files())
            {
                content.append("--");
                content.append(mReq->boundary());
                content.append("\r\n");
                content.append("Content-Disposition: form-data; name=\"");
                content.append(file.itemName());
                content.append("\"; filename=\"");
                content.append(file.fileName());
                content.append("\"\r\n\r\n");
                std::ifstream infile(file.path(), std::ifstream::binary);
                if (!infile)
                {
                    LOG_ERROR << file.path() << " not found";
                }
                else
                {
                    std::streambuf *pbuf = infile.rdbuf();
                    std::streamsize filesize = pbuf->pubseekoff(0, infile.end);
                    pbuf->pubseekoff(0, infile.beg);  // rewind
                    std::string str;
                    str.resize(filesize);
                    pbuf->sgetn(&str[0], filesize);
                    content.append(std::move(str));
                }
                content.append("\r\n");
            }
            content.append("--");
            content.append(mReq->boundary());
            content.append("--");
        }
    }
    assert(!(!content.empty() && !content_.empty()));
    if (!passThrough_ && (!content.empty() || !content_.empty()))
    {
        char buf[64];
        auto len = snprintf(buf,
                            sizeof(buf),
                            contentLengthFormatString<decltype(content.length())>(),
                            content.length() + content_.length());
        output->append(buf, len);
        if (contentTypeString_.empty())
        {
            auto &type = webContentTypeToString(contentType_);
            output->append(type.data(), type.length());
        }
    }
    if (!passThrough_ && !contentTypeString_.empty())
    {
        output->append(contentTypeString_);
    }
    for (auto it = headers_.begin(); it != headers_.end(); ++it)
    {
        output->append(it->first);
        output->append(": ");
        output->append(it->second);
        output->append("\r\n");
    }
    if (cookies_.size() > 0)
    {
        output->append("Cookie: ");
        for (auto it = cookies_.begin(); it != cookies_.end(); ++it)
        {
            output->append(it->first);
            output->append("= ");
            output->append(it->second);
            output->append(";");
        }
        output->unwrite(1);  // delete last ';'
        output->append("\r\n");
    }

    output->append("\r\n");
    if (!content.empty())
        output->append(content);
    if (!content_.empty())
        output->append(content_);
}

void HttpRequest::addHeader(const char *start,
                                const char *colon,
                                const char *end)
{
    std::string field(start, colon);
    // Field name is case-insensitive.so we transform it to lower;(rfc2616-4.2)
    std::transform(field.begin(), field.end(), field.begin(), ::tolower);
    ++colon;
    while (colon < end && isspace(*colon))
    {
        ++colon;
    }
    std::string value(colon, end);
    while (!value.empty() && isspace(value[value.size() - 1]))
    {
        value.resize(value.size() - 1);
    }
    if (field.length() == 6 && field == "cookie")
    {
        LOG_TRACE << "cookies!!!:" << value;
        std::string::size_type pos;
        while ((pos = value.find(';')) != std::string::npos)
        {
            std::string coo = value.substr(0, pos);
            auto epos = coo.find('=');
            if (epos != std::string::npos)
            {
                std::string cookie_name = coo.substr(0, epos);
                std::string::size_type cpos = 0;
                while (cpos < cookie_name.length() &&
                       isspace(cookie_name[cpos]))
                    ++cpos;
                cookie_name = cookie_name.substr(cpos);
                std::string cookie_value = coo.substr(epos + 1);
                cookies_[std::move(cookie_name)] = std::move(cookie_value);
            }
            value = value.substr(pos + 1);
        }
        if (value.length() > 0)
        {
            std::string &coo = value;
            auto epos = coo.find('=');
            if (epos != std::string::npos)
            {
                std::string cookie_name = coo.substr(0, epos);
                std::string::size_type cpos = 0;
                while (cpos < cookie_name.length() &&
                       isspace(cookie_name[cpos]))
                    ++cpos;
                cookie_name = cookie_name.substr(cpos);
                std::string cookie_value = coo.substr(epos + 1);
                cookies_[std::move(cookie_name)] = std::move(cookie_value);
            }
        }
    }
    else
    {
        switch (field.length())
        {
            case 6:
                if (field == "expect")
                {
                    expectPtr_ = std::make_unique<std::string>(value);
                }
                break;
            case 10:
            {
                if (field == "connection")
                {
                    if (version_ == Version::kHttp11)
                    {
                        if (value.length() == 5 && value == "close")
                            keepAlive_ = false;
                    }
                    else if (value.length() == 10 &&
                             (value == "Keep-Alive" || value == "keep-alive"))
                    {
                        keepAlive_ = true;
                    }
                }
            }
            break;

            default:
                break;
        }
        headers_.emplace(std::move(field), std::move(value));
    }
}

HttpRequestPtr HttpRequest::newHttpRequest()
{
    auto req = std::make_shared<HttpRequest>(nullptr);
    req->setMethod(drogon::Get);
    req->setVersion(drogon::Version::kHttp11);
    return req;
}

HttpRequestPtr HttpRequest::newHttpFormPostRequest()
{
    auto req = std::make_shared<HttpRequest>(nullptr);
    req->setMethod(drogon::Post);
    req->setVersion(drogon::Version::kHttp11);
    req->contentType_ = CT_APPLICATION_X_FORM;
    req->flagForParsingContentType_ = true;
    return req;
}

HttpRequestPtr HttpRequest::newHttpJsonRequest(const Json::Value &data)
{
    static std::once_flag once;
    static Json::StreamWriterBuilder builder;
    std::call_once(once, []() {
        builder["commentStyle"] = "None";
        builder["indentation"] = "";

/*
        if (!Application::get_instance()->isUnicodeEscapingUsedInJson())
        {
            builder["emitUTF8"] = true;
        }*/

    });
    auto req = std::make_shared<HttpRequest>(nullptr);
    req->setMethod(drogon::Get);
    req->setVersion(drogon::Version::kHttp11);
    req->contentType_ = CT_APPLICATION_JSON;
    req->setContent(writeString(builder, data));
    req->flagForParsingContentType_ = true;
    return req;
}

HttpRequestPtr HttpRequest::newFileUploadRequest(
    const std::vector<UploadFile> &files)
{
    return std::make_shared<HttpFileUploadRequest>(files);
}

void HttpRequest::swap(HttpRequest &that) noexcept
{
    using std::swap;
    swap(method_, that.method_);
    swap(version_, that.version_);
    swap(flagForParsingJson_, that.flagForParsingJson_);
    swap(flagForParsingParameters_, that.flagForParsingParameters_);
    swap(matchedPathPattern_, that.matchedPathPattern_);
    swap(path_, that.path_);
    swap(query_, that.query_);
    swap(headers_, that.headers_);
    swap(cookies_, that.cookies_);
    swap(parameters_, that.parameters_);
    swap(jsonPtr_, that.jsonPtr_);
    swap(sessionPtr_, that.sessionPtr_);
    swap(attributesPtr_, that.attributesPtr_);
    swap(cacheFilePtr_, that.cacheFilePtr_);
    swap(peer_, that.peer_);
    swap(local_, that.local_);
    swap(creationDate_, that.creationDate_);
    swap(content_, that.content_);
    swap(expectPtr_, that.expectPtr_);
    swap(contentType_, that.contentType_);
    swap(contentTypeString_, that.contentTypeString_);
    swap(keepAlive_, that.keepAlive_);
    swap(loop_, that.loop_);
    swap(flagForParsingContentType_, that.flagForParsingContentType_);
    swap(jsonParsingErrorPtr_, that.jsonParsingErrorPtr_);
}

const char *HttpRequest::methodString() const
{
    const char *result = "UNKNOWN";
    switch (method_)
    {
        case Get:
            result = "GET";
            break;
        case Post:
            result = "POST";
            break;
        case Head:
            result = "HEAD";
            break;
        case Put:
            result = "PUT";
            break;
        case Delete:
            result = "DELETE";
            break;
        case Options:
            result = "OPTIONS";
            break;
        case Patch:
            result = "PATCH";
            break;
        default:
            break;
    }
    return result;
}

bool HttpRequest::setMethod(const char *start, const char *end)
{
    assert(method_ == Invalid);
    string_view m(start, end - start);
    switch (m.length())
    {
        case 3:
            if (m == "GET")
            {
                method_ = Get;
            }
            else if (m == "PUT")
            {
                method_ = Put;
            }
            else
            {
                method_ = Invalid;
            }
            break;
        case 4:
            if (m == "POST")
            {
                method_ = Post;
            }
            else if (m == "HEAD")
            {
                method_ = Head;
            }
            else
            {
                method_ = Invalid;
            }
            break;
        case 5:
            if (m == "PATCH")
            {
                method_ = Patch;
            }
            else
            {
                method_ = Invalid;
            }
            break;
        case 6:
            if (m == "DELETE")
            {
                method_ = Delete;
            }
            else
            {
                method_ = Invalid;
            }
            break;
        case 7:
            if (m == "OPTIONS")
            {
                method_ = Options;
            }
            else
            {
                method_ = Invalid;
            }
            break;
        default:
            method_ = Invalid;
            break;
    }

    // if (method_ != Invalid)
    // {
    //     content_ = "";
    //     query_ = "";
    //     cookies_.clear();
    //     parameters_.clear();
    //     headers_.clear();
    // }
    return method_ != Invalid;
}

HttpRequest::~HttpRequest()
{
}

void HttpRequest::reserveBodySize(size_t length)
{
    if (length <= Application::get_instance()->getClientMaxMemoryBodySize())
    {
        content_.reserve(length);
    }
    else
    {
        // Store data of body to a temperary file
        createTmpFile();
    }
}

void HttpRequest::appendToBody(const char *data, size_t length)
{
    if (cacheFilePtr_)
    {
        cacheFilePtr_->append(data, length);
    }
    else
    {
        if (content_.length() + length <=
            Application::get_instance()->getClientMaxMemoryBodySize())
        {
            content_.append(data, length);
        }
        else
        {
            createTmpFile();
            cacheFilePtr_->append(content_);
            cacheFilePtr_->append(data, length);
            content_.clear();
        }
    }
}

void HttpRequest::createTmpFile()
{
    /*
    auto tmpfile = Application::get_instance()->getUploadPath();


    auto fileName = utils::getUuid();
    tmpfile.append("/tmp/")
        .append(1, fileName[0])
        .append(1, fileName[1])
        .append("/")
        .append(fileName);
    cacheFilePtr_ = std::make_unique<CacheFile>(tmpfile);
    */
}
