// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
 *   Copyright (c) 2019 Jean-Marie Verdun         jmverdun3@gmail.com      *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Library General Public           *
 *   License as published by the Free Software Foundation; either          *
 *   version 2 of the License, or (at your option) any later version.      *
 *                                                                         *
 *   This library  is distributed in the hope that it will be useful,      *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; see the file COPYING.LIB. If not,    *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
 *   Suite 330, Boston, MA  02111-1307, USA                                *
 *                                                                         *
 ***************************************************************************/

#include <FCConfig.h>


#include <limits>

#if defined(FC_OS_WIN32)
# include <Windows.h>
# include <cstdint>
#endif

#include <openssl/hmac.h>
#include <openssl/md5.h>
#include <openssl/pem.h>
#include <openssl/sha.h>

#include <curl/curl.h>

#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/DocumentPy.h>
#include <Base/Console.h>
#include <Base/Tools.h>
#include <Base/PyObjectBase.h>

#include <CXX/Extensions.hxx>
#include <CXX/Objects.hxx>

#include "AppCloud.h"


using namespace App;
using namespace std;
using namespace boost::placeholders;
XERCES_CPP_NAMESPACE_USE

/* Python entry */
PyMOD_INIT_FUNC(Cloud)
{
    PyObject* mod = Cloud::initModule();
    Base::Console().log("Loading Cloud module... done\n");
    PyMOD_Return(mod);
}

Py::Object Cloud::Module::sCloudURL(const Py::Tuple& args)
{
    char* URL;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &URL)) {
        throw Py::Exception();
    }

    std::string strURL = URL;
    PyMem_Free(URL);
    if (this->URL.getStrValue() != strURL) {
        this->URL.setValue(strURL);
    }

    return Py::None();
}

Py::Object Cloud::Module::sCloudTokenAuth(const Py::Tuple& args)
{
    char* TokenAuth;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &TokenAuth)) {
        throw Py::Exception();
    }

    std::string strTokenAuth = TokenAuth;
    PyMem_Free(TokenAuth);
    if (this->TokenAuth.getStrValue() != strTokenAuth) {
        this->TokenAuth.setValue(strTokenAuth);
    }

    return Py::None();
}

Py::Object Cloud::Module::sCloudTokenSecret(const Py::Tuple& args)
{
    char* TokenSecret;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &TokenSecret)) {
        throw Py::Exception();
    }

    std::string strTokenSecret = TokenSecret;
    PyMem_Free(TokenSecret);
    if (this->TokenSecret.getStrValue() != strTokenSecret) {
        this->TokenSecret.setValue(strTokenSecret);
    }

    return Py::None();
}

Py::Object Cloud::Module::sCloudTCPPort(const Py::Tuple& args)
{
    char* TCPPort;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &TCPPort)) {
        throw Py::Exception();
    }

    std::string strTCPPort = TCPPort;
    PyMem_Free(TCPPort);
    if (this->TCPPort.getStrValue() != strTCPPort) {
        this->TCPPort.setValue(strTCPPort);
    }

    return Py::None();
}

Py::Object Cloud::Module::sCloudSave(const Py::Tuple& args)
{
    char* pDoc;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &pDoc)) {
        throw Py::Exception();
    }

    std::string strpDoc = pDoc;
    PyMem_Free(pDoc);
    cloudSave(strpDoc.c_str());

    return Py::None();
}


Py::Object Cloud::Module::sCloudRestore(const Py::Tuple& args)
{
    char* pDoc;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &pDoc)) {
        throw Py::Exception();
    }

    std::string strpDoc = pDoc;
    PyMem_Free(pDoc);
    cloudRestore(strpDoc.c_str());

    return Py::None();
}

Py::Object Cloud::Module::sCloudProtocolVersion(const Py::Tuple& args)
{
    char* ProtocolVersion;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &ProtocolVersion)) {
        throw Py::Exception();
    }

    std::string strProtocolVersion = ProtocolVersion;
    PyMem_Free(ProtocolVersion);
    if (this->ProtocolVersion.getStrValue() != strProtocolVersion) {
        this->ProtocolVersion.setValue(strProtocolVersion);
    }

    return Py::None();
}

Py::Object Cloud::Module::sCloudRegion(const Py::Tuple& args)
{
    char* Region;
    if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &Region)) {
        throw Py::Exception();
    }

    std::string strRegion = Region;
    PyMem_Free(Region);
    if (this->Region.getStrValue() != strRegion) {
        this->Region.setValue(strRegion);
    }

    return Py::None();
}

struct data_buffer
{
    const char* ptr;
    size_t remaining_size;
};

static size_t read_callback(void* ptr, size_t size, size_t nmemb, void* stream)
{

    struct data_buffer* local_ptr = (struct data_buffer*)stream;
    size_t data_to_transfer = size * nmemb;

    if (local_ptr->remaining_size) {
        // copy as much as possible from the source to the destination
        size_t copy_this_much = local_ptr->remaining_size;
        if (copy_this_much > data_to_transfer) {
            copy_this_much = data_to_transfer;
        }
        memcpy(ptr, local_ptr->ptr, copy_this_much);

        local_ptr->ptr += copy_this_much;
        local_ptr->remaining_size -= copy_this_much;
        return copy_this_much;
    }

    return 0;
}

void Cloud::CloudWriter::checkXML(DOMNode* node)
{
    if (node) {
        switch (node->getNodeType()) {
            case DOMNode::ELEMENT_NODE:
                checkElement(static_cast<DOMElement*>(node));
                break;
            case DOMNode::TEXT_NODE:
                checkText(static_cast<DOMText*>(node));
                break;
            default:
                break;
        }
        DOMNode* child = node->getFirstChild();
        while (child) {
            DOMNode* next = child->getNextSibling();
            checkXML(child);
            child = next;
        }
    }
}

void Cloud::CloudWriter::checkElement(DOMElement* element)
{
    char* name = XMLString::transcode(element->getTagName());
    if (strcmp(name, "Code") == 0) {
        print = 1;
    }
    XMLString::release(&name);
}

void Cloud::CloudWriter::checkText(DOMText* text)
{

    XMLCh* buffer = new XMLCh[XMLString::stringLen(text->getData()) + 1];
    XMLString::copyString(buffer, text->getData());
    XMLString::trim(buffer);
    char* content = XMLString::transcode(buffer);
    delete[] buffer;
    if (print) {
        strcpy(errorCode, content);
    }
    print = 0;
    XMLString::release(&content);
}


void Cloud::CloudWriter::createBucket()
{
    struct Cloud::AmzData* RequestData;
    struct Cloud::AmzDatav4* RequestDatav4;
    CURL* curl;
    CURLcode res;

    struct data_buffer curl_buffer;

    char path[1024];
    sprintf(path, "/%s/", this->Bucket);
    std::string strURL(this->URL);
    eraseSubStr(strURL, "http://");
    eraseSubStr(strURL, "https://");

    if (this->ProtocolVersion == "2") {
        RequestData
            = Cloud::ComputeDigestAmzS3v2("PUT", "application/xml", path, this->TokenSecret, nullptr, 0);
    }
    else {
        RequestDatav4 = Cloud::ComputeDigestAmzS3v4(
            "PUT",
            strURL.c_str(),
            "application/xml",
            path,
            this->TokenSecret,
            nullptr,
            0,
            nullptr,
            this->Region
        );
    }

    // Let's build the Header and call to curl
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
#ifdef ALLOW_SELF_SIGNED_CERTIFICATE
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
#endif

    if (curl) {
        struct curl_slist* chunk = nullptr;
        char URL[256];
        // Let's build our own header
        std::string strURL(this->URL);
        eraseSubStr(strURL, "http://");
        eraseSubStr(strURL, "https://");
        if (this->ProtocolVersion == "2") {
            chunk = Cloud::BuildHeaderAmzS3v2(strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData);
            delete RequestData;
        }
        else {
            chunk = Cloud::BuildHeaderAmzS3v4(strURL.c_str(), this->TokenAuth, RequestDatav4);
            delete RequestDatav4;
        }

        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

        // Lets build the URL for our Curl call

        sprintf(URL, "%s:%s/%s/", this->URL, this->TCPPort, this->Bucket);
        curl_easy_setopt(curl, CURLOPT_URL, URL);

        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_PUT, 1L);
        // curl read a file not a memory buffer (it shall be able to do it)
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);

        curl_buffer.ptr = nullptr;
        curl_buffer.remaining_size = (size_t)0;

        curl_easy_setopt(curl, CURLOPT_READDATA, &curl_buffer);

        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)0);
        res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }
        curl_easy_cleanup(curl);
    }
}


struct Cloud::AmzDatav4* Cloud::ComputeDigestAmzS3v4(
    char* operation,
    const char* server,
    char* data_type,
    const char* target,
    const char* Secret,
    const char* ptr,
    long size,
    char* parameters,
    std::string Region
)
{
    struct AmzDatav4* returnData;
    returnData = new Cloud::AmzDatav4;
    struct tm* tm;
    char* canonical_request;
    char* canonicalRequestHash;
    char* stringToSign;

    strcpy(returnData->ContentType, data_type);

#if defined(FC_OS_WIN32)
    _putenv("TZ=GMT");
    time_t rawtime;

    time(&rawtime);
    tm = localtime(&rawtime);
#else
    struct timeval tv;
    setenv("TZ", "GMT", 1);
    gettimeofday(&tv, nullptr);
    tm = localtime(&tv.tv_sec);
#endif
    strftime(returnData->dateFormattedD, 256, "%Y%m%d", tm);
    strftime(returnData->dateFormattedS, 256, "%Y%m%dT%H%M%SZ", tm);
    returnData->MD5 = nullptr;

    // We must evaluate the canonical request
    canonical_request = (char*)malloc(4096 * (sizeof(char*)));
    strcpy(canonical_request, operation);
    strcat(canonical_request, "\n");
    strcat(canonical_request, target);
    strcat(canonical_request, "\n");
    if (parameters == nullptr) {
        strcat(canonical_request, "\n");
    }
    else {
        strcat(canonical_request, parameters);
        strcat(canonical_request, "\n");
    }
    strcat(canonical_request, "host:");
    strcat(canonical_request, server);
    strcat(canonical_request, "\n");
    strcat(canonical_request, "x-amz-date:");
    strcat(canonical_request, returnData->dateFormattedS);
    strcat(canonical_request, "\n\n");
    strcat(canonical_request, "host;x-amz-date\n");
    // We must add there the file SHA256 Hash
    returnData->SHA256Sum = nullptr;
    if (strcmp(operation, "PUT") == 0) {
        if (ptr != nullptr) {
            returnData->SHA256Sum = Cloud::SHA256Sum(ptr, size);
            strcat(canonical_request, returnData->SHA256Sum);
        }
        else {
            strcat(canonical_request, "UNSIGNED-PAYLOAD");
        }
    }
    else {
        strcat(canonical_request, "UNSIGNED-PAYLOAD");
    }
    canonicalRequestHash = Cloud::SHA256Sum(canonical_request, strlen(canonical_request));
    // returnData->digest = string(digest);


    // We need now to sign a string which contain the digest
    // The format is as follow
    // ${authType}
    // ${dateValueL}
    // ${dateValueS}/${Region}/${service}/aws4_request
    // ${canonicalRequestHash}"

    stringToSign = (char*)malloc(4096 * sizeof(char));
    strcat(stringToSign, "AWS4-HMAC-SHA256");
    strcat(stringToSign, "\n");
    strcat(stringToSign, returnData->dateFormattedS);
    strcat(stringToSign, "\n");
    strcat(stringToSign, returnData->dateFormattedD);
    strcat(stringToSign, "/us/s3/aws4_request");
    strcat(stringToSign, "\n");
    strcat(stringToSign, canonicalRequestHash);
    strcat(stringToSign, "\0");

    // We must now compute the signature
    // Everything starts with the secret key and an SHA256 HMAC encryption
    char kSecret[256];
    unsigned char *kDate, *kRegion, *kService, *kSigned, *kSigning;

    strcpy(kSecret, "AWS4");
    strcat(kSecret, Secret);
    unsigned int HMACLength;

    std::string temporary;

    kDate = HMAC(
        EVP_sha256(),
        kSecret,
        strlen(kSecret),
        (const unsigned char*)returnData->dateFormattedD,
        strlen(returnData->dateFormattedD),
        nullptr,
        &HMACLength
    );

    temporary = getHexValue(kDate, HMACLength);
    temporary = getHexValue(kDate, HMACLength);

    // We can now compute the remaining parts
    kRegion = HMAC(
        EVP_sha256(),
        kDate,
        HMACLength,
        (const unsigned char*)Region.c_str(),
        strlen(Region.c_str()),
        nullptr,
        &HMACLength
    );

    temporary = getHexValue(kRegion, HMACLength);

    kService = HMAC(
        EVP_sha256(),
        kRegion,
        HMACLength,
        (const unsigned char*)"s3",
        strlen("s3"),
        nullptr,
        &HMACLength
    );

    temporary = getHexValue(kService, HMACLength);

    kSigning = HMAC(
        EVP_sha256(),
        kService,
        HMACLength,
        (const unsigned char*)"aws4_request",
        strlen("aws4_request"),
        nullptr,
        &HMACLength
    );

    temporary = getHexValue(kService, HMACLength);


    kSigned = HMAC(
        EVP_sha256(),
        kSigning,
        HMACLength,
        (const unsigned char*)stringToSign,
        strlen(stringToSign),
        nullptr,
        &HMACLength
    );

    temporary = getHexValue(kSigned, HMACLength);

    returnData->digest = string(temporary);
    returnData->Region = Region;
    free(canonical_request);
    return (returnData);
}

std::string Cloud::getHexValue(unsigned char* input, unsigned int HMACLength)
{
    char* Hex;
    unsigned char* ptr = input;
    std::string resultReadable;
    Hex = (char*)malloc(HMACLength * 2 * sizeof(char) + 1);
    for (unsigned int i = 0; i < HMACLength; i++) {
        sprintf(Hex, "%02x", *ptr);
        ptr++;
        Hex[2] = '\0';
        resultReadable += Hex;
    }
    return resultReadable;
}

struct Cloud::AmzData* Cloud::ComputeDigestAmzS3v2(
    char* operation,
    char* data_type,
    const char* target,
    const char* Secret,
    const char* ptr,
    long size
)
{
    struct AmzData* returnData;
    // struct timeval tv;
    struct tm* tm;
    char date_formatted[256];
    char StringToSign[1024];
    unsigned char* digest;
    unsigned int HMACLength;
    // Amazon S3 and Swift require the timezone to be define to GMT.
    // As to simplify the conversion this is performed through the TZ
    // environment variable and a call to localtime as to convert output of gettimeofday
    returnData = new Cloud::AmzData;
    strcpy(returnData->ContentType, data_type);
#if defined(FC_OS_WIN32)
    _putenv("TZ=GMT");
    time_t rawtime;

    time(&rawtime);
    tm = localtime(&rawtime);
#else
    struct timeval tv;
    setenv("TZ", "GMT", 1);
    gettimeofday(&tv, nullptr);
    tm = localtime(&tv.tv_sec);
#endif
    strftime(date_formatted, 256, "%a, %d %b %Y %T %z", tm);
    returnData->MD5 = nullptr;
    if (strcmp(operation, "PUT") == 0) {
        if (ptr != nullptr) {
            returnData->MD5 = Cloud::MD5Sum(ptr, size);
            sprintf(
                StringToSign,
                "%s\n%s\n%s\n%s\n%s",
                operation,
                returnData->MD5,
                data_type,
                date_formatted,
                target
            );
        }
        else {
            sprintf(StringToSign, "%s\n\n%s\n%s\n%s", operation, data_type, date_formatted, target);
        }
    }
    else {
        sprintf(StringToSign, "%s\n\n%s\n%s\n%s", operation, data_type, date_formatted, target);
    }
    // We have to use HMAC encoding and SHA1
    digest = HMAC(
        EVP_sha1(),
        Secret,
        strlen(Secret),
        (const unsigned char*)&StringToSign,
        strlen(StringToSign),
        nullptr,
        &HMACLength
    );
    returnData->digest = Base::base64_encode(digest, HMACLength);
    strcpy(returnData->dateFormatted, date_formatted);
    return returnData;
}

char* Cloud::SHA256Sum(const char* ptr, long size)
{
    char* output;
    std::string local;
    std::string resultReadable;
    unsigned char result[SHA256_DIGEST_LENGTH];
    output = (char*)malloc(2 * SHA256_DIGEST_LENGTH * sizeof(char) + 1);
    SHA256((unsigned char*)ptr, size, result);

    strcpy(output, getHexValue(result, SHA256_DIGEST_LENGTH).c_str());

    return (output);
}

char* Cloud::MD5Sum(const char* ptr, long size)
{
    char* output;
    std::string local;
    unsigned char result[MD5_DIGEST_LENGTH];
    output = (char*)malloc(2 * MD5_DIGEST_LENGTH * sizeof(char) + 1);
    MD5((unsigned char*)ptr, size, result);
    local = Base::base64_encode(result, MD5_DIGEST_LENGTH);
    strcpy(output, local.c_str());
    return (output);
}

struct curl_slist* Cloud::BuildHeaderAmzS3v4(
    const char* URL,
    const char* PublicKey,
    struct Cloud::AmzDatav4* Data
)
{
    char header_data[1024];
    struct curl_slist* chunk = nullptr;

    // Build the Host: entry
    sprintf(header_data, "Host: %s", URL);
    chunk = curl_slist_append(chunk, header_data);

    // Build the Date entry

    sprintf(header_data, "X-Amz-Date: %s", Data->dateFormattedS);
    chunk = curl_slist_append(chunk, header_data);

    // Build the Content-Type entry

    sprintf(header_data, "Content-Type:%s", Data->ContentType);
    chunk = curl_slist_append(chunk, header_data);

    // If ptr is not null we must compute the MD5-Sum as to validate later the ETag
    // and add the MD5-Content: entry to the header
    if (Data->MD5 != nullptr) {
        sprintf(header_data, "Content-MD5: %s", Data->MD5);
        chunk = curl_slist_append(chunk, header_data);
        // We don't need it anymore we can free it
        free((void*)Data->MD5);
    }

    if (Data->SHA256Sum != nullptr) {
        sprintf(header_data, "x-amz-content-sha256: %s", Data->SHA256Sum);
        chunk = curl_slist_append(chunk, header_data);
        // We don't need it anymore we can free it
        free((void*)Data->SHA256Sum);
    }
    else {
        chunk = curl_slist_append(chunk, "x-amz-content-sha256: UNSIGNED-PAYLOAD");
    }

    // build the Auth entry
    sprintf(
        header_data,
        "Authorization: AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/aws4_request, "
        "SignedHeaders=%s, Signature=%s",
        PublicKey,
        Data->dateFormattedD,
        "us",
        "s3",
        "host;x-amz-date",
        Data->digest.c_str()
    );
    chunk = curl_slist_append(chunk, header_data);

    return chunk;
}

struct curl_slist* Cloud::BuildHeaderAmzS3v2(
    const char* URL,
    const char* TCPPort,
    const char* PublicKey,
    struct Cloud::AmzData* Data
)
{
    char header_data[1024];
    struct curl_slist* chunk = nullptr;

    // Build the Host: entry

    sprintf(header_data, "Host: %s:%s", URL, TCPPort);
    chunk = curl_slist_append(chunk, header_data);
    // Build the Date entry

    sprintf(header_data, "Date: %s", Data->dateFormatted);
    chunk = curl_slist_append(chunk, header_data);
    // Build the Content-Type entry

    sprintf(header_data, "Content-Type:%s", Data->ContentType);
    chunk = curl_slist_append(chunk, header_data);

    // If ptr is not null we must compute the MD5-Sum as to validate later the ETag
    // and add the MD5-Content: entry to the header
    if (Data->MD5 != nullptr) {
        sprintf(header_data, "Content-MD5: %s", Data->MD5);
        chunk = curl_slist_append(chunk, header_data);
        // We don't need it anymore we can free it
        free((void*)Data->MD5);
    }

    // build the Auth entry

    sprintf(header_data, "Authorization: AWS %s:%s", PublicKey, Data->digest.c_str());
    chunk = curl_slist_append(chunk, header_data);

    return chunk;
}

Cloud::CloudWriter::CloudWriter(
    const char* URL,
    const char* TokenAuth,
    const char* TokenSecret,
    const char* TCPPort,
    const char* Bucket,
    std::string ProtocolVersion,
    std::string Region
)
{
    struct Cloud::AmzData* RequestData;
    struct Cloud::AmzDatav4* RequestDatav4;
    CURL* curl;
    CURLcode res;

    std::string s;

    this->URL = URL;
    this->TokenAuth = TokenAuth;
    this->TokenSecret = TokenSecret;
    this->TCPPort = TCPPort;
    this->Bucket = Bucket;
    if (!ProtocolVersion.empty()) {
        this->ProtocolVersion = ProtocolVersion;
    }
    else {
        this->ProtocolVersion = "2";
    }
    this->Region = Region;
    this->FileName = "";
    char path[1024];
    sprintf(path, "/%s/", this->Bucket);
    std::string strURL(this->URL);
    eraseSubStr(strURL, "http://");
    eraseSubStr(strURL, "https://");
    if (this->ProtocolVersion == "2") {
        RequestData
            = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->TokenSecret, nullptr, 0);
    }
    else {
        RequestDatav4 = Cloud::ComputeDigestAmzS3v4(
            "GET",
            strURL.c_str(),
            "application/xml",
            path,
            this->TokenSecret,
            nullptr,
            0,
            nullptr,
            this->Region
        );
    }
    // Let's build the Header and call to curl
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
#ifdef ALLOW_SELF_SIGNED_CERTIFICATE
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
#endif
    if (curl) {
        // Let's build our own header
        struct curl_slist* chunk = nullptr;
        char URL[256];
        std::string strURL(this->URL);
        eraseSubStr(strURL, "http://");
        eraseSubStr(strURL, "https://");
        if (this->ProtocolVersion == "2") {
            chunk = Cloud::BuildHeaderAmzS3v2(strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData);
            delete RequestData;
        }
        else {
            chunk = Cloud::BuildHeaderAmzS3v4(strURL.c_str(), this->TokenAuth, RequestDatav4);
            delete RequestDatav4;
        }

        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

        // Lets build the URL for our Curl call

        sprintf(URL, "%s:%s/%s/", this->URL, this->TCPPort, this->Bucket);
        curl_easy_setopt(curl, CURLOPT_URL, URL);

        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
        // curl read a file not a memory buffer (it shall be able to do it)
        res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }
        curl_easy_cleanup(curl);
        createBucket();
        // Lets dump temporarily for debug purposes of s3v4 implementation
        std::stringstream input(s);

        try {
            XMLPlatformUtils::Initialize();
        }
        catch (const XMLException& toCatch) {
            char* message = XMLString::transcode(toCatch.getMessage());
            cout << "Error during initialization! :\n" << message << "\n";
            XMLString::release(&message);
            return;
        }

        XercesDOMParser* parser = new XercesDOMParser();
        parser->setValidationScheme(XercesDOMParser::Val_Always);
        parser->setDoNamespaces(true);

        xercesc::MemBufInputSource myxml_buf(
            (const XMLByte* const)s.c_str(),
            s.size(),
            "myxml (in memory)"
        );

        parser->parse(myxml_buf);
        auto* dom = parser->getDocument();
        // Is there an Error entry into the document ?
        // if yes, then we must create the Bucket
        checkXML(dom);
        if (strcmp(errorCode, "NoSuchBucket") == 0) {
            // we must create the Bucket using a PUT request
            createBucket();
        }
    }
}

Cloud::CloudWriter::~CloudWriter()
{}

size_t Cloud::CurlWrite_CallbackFunc_StdString(void* contents, size_t size, size_t nmemb, std::string* s)
{
    size_t newLength = size * nmemb;
    try {
        s->append((char*)contents, newLength);
    }
    catch (std::bad_alloc&) {
        // handle memory problem
        return 0;
    }
    return newLength;
}

void Cloud::CloudReader::checkElement(DOMElement* element)
{
    char* name = XMLString::transcode(element->getTagName());
    if (strcmp(name, "Key") == 0) {
        file = 1;
    }
    if (strcmp(name, "NextContinuationToken") == 0) {
        continuation = 1;
    }
    if (strcmp(name, "IsTruncated") == 0) {
        truncated = 1;
    }
    XMLString::release(&name);
}

void Cloud::CloudReader::checkText(DOMText* text)
{

    XMLCh* buffer = new XMLCh[XMLString::stringLen(text->getData()) + 1];
    XMLString::copyString(buffer, text->getData());
    XMLString::trim(buffer);
    struct Cloud::CloudReader::FileEntry* new_entry;
    char* content = XMLString::transcode(buffer);
    delete[] buffer;
    if (file) {
        new_entry = new Cloud::CloudReader::FileEntry;
        strcpy(new_entry->FileName, content);
        Cloud::CloudReader::FileList.push_back(new_entry);
    }
    file = 0;
    if (continuation == 1) {
        strcpy(Cloud::CloudReader::NextFileName, content);
        continuation = 0;
    }
    if (truncated == 1) {
        if (strncmp(content, "true", 4) != 0) {
            truncated = 0;
        }
        else {
            truncated = 2;
        }
    }
    XMLString::release(&content);
}

void Cloud::CloudReader::addFile(struct Cloud::CloudReader::FileEntry* new_entry)
{
    Cloud::CloudReader::FileList.push_back(new_entry);
}

void Cloud::CloudReader::checkXML(DOMNode* node)
{
    if (node) {
        switch (node->getNodeType()) {
            case DOMNode::ELEMENT_NODE:
                checkElement(static_cast<DOMElement*>(node));
                break;
            case DOMNode::TEXT_NODE:
                checkText(static_cast<DOMText*>(node));
                break;
            default:
                break;
        }
        DOMNode* child = node->getFirstChild();
        while (child) {
            DOMNode* next = child->getNextSibling();
            checkXML(child);
            child = next;
        }
    }
}


Cloud::CloudReader::~CloudReader()
{}

Cloud::CloudReader::CloudReader(
    const char* URL,
    const char* TokenAuth,
    const char* TokenSecret,
    const char* TCPPort,
    const char* Bucket,
    std::string ProtocolVersion,
    std::string Region
)
{
    struct Cloud::AmzData* RequestData;
    struct Cloud::AmzDatav4* RequestDatav4;
    CURL* curl;
    CURLcode res;
    bool GetBucketContentList = true;
    struct curl_slist* chunk = nullptr;
    char parameters[1024];


    this->URL = URL;
    this->TokenAuth = TokenAuth;
    this->TokenSecret = TokenSecret;
    this->TCPPort = TCPPort;
    this->Bucket = Bucket;
    if (!ProtocolVersion.empty()) {
        this->ProtocolVersion = ProtocolVersion;
    }
    else {
        this->ProtocolVersion = "2";
    }
    this->Region = Region;

    char path[1024];
    sprintf(path, "/%s/", this->Bucket);
    Cloud::CloudReader::NextFileName = (char*)malloc(sizeof(char) * 1024);
    for (int i = 0; i < 1024; i++) {
        NextFileName[i] = '\0';
    }


    // Let's build the Header and call to curl
    curl_global_init(CURL_GLOBAL_ALL);
    while (GetBucketContentList) {
        std::string s;
        curl = curl_easy_init();
#ifdef ALLOW_SELF_SIGNED_CERTIFICATE
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
#endif
        if (curl) {
            // Let's build our own header
            char URL[256];
            std::string strURL(this->URL);
            eraseSubStr(strURL, "http://");
            eraseSubStr(strURL, "https://");
            if (strlen(NextFileName) == 0) {
                sprintf(parameters, "list-type=2");
            }
            else {
                sprintf(parameters, "list-type=2&continuation-token=%s", NextFileName);
            }
            sprintf(URL, "%s:%s/%s/?%s", this->URL, this->TCPPort, this->Bucket, parameters);
            curl_easy_setopt(curl, CURLOPT_URL, URL);

            if (this->ProtocolVersion == "2") {
                RequestData = Cloud::ComputeDigestAmzS3v2(
                    "GET",
                    "application/xml",
                    path,
                    this->TokenSecret,
                    nullptr,
                    0
                );
                chunk = Cloud::BuildHeaderAmzS3v2(
                    strURL.c_str(),
                    this->TCPPort,
                    this->TokenAuth,
                    RequestData
                );
                delete RequestData;
            }
            else {
                RequestDatav4 = Cloud::ComputeDigestAmzS3v4(
                    "GET",
                    strURL.c_str(),
                    "application/xml",
                    path,
                    this->TokenSecret,
                    nullptr,
                    0,
                    (char*)&parameters[0],
                    this->Region
                );
                chunk = Cloud::BuildHeaderAmzS3v4(strURL.c_str(), this->TokenAuth, RequestDatav4);
                delete RequestDatav4;
            }
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
            // curl read a file not a memory buffer (it shall be able to do it)

            res = curl_easy_perform(curl);

            for (int i = 0; i < 1024; i++) {
                NextFileName[i] = '\0';
            }
            if (res != CURLE_OK) {
                fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
            }
            curl_easy_cleanup(curl);


            std::stringstream input(s);

            try {
                XMLPlatformUtils::Initialize();
            }
            catch (const XMLException& toCatch) {
                char* message = XMLString::transcode(toCatch.getMessage());
                cout << "Error during initialization! :\n" << message << "\n";
                XMLString::release(&message);
                return;
            }

            XercesDOMParser* parser = new XercesDOMParser();
            parser->setValidationScheme(XercesDOMParser::Val_Always);
            parser->setDoNamespaces(true);

            xercesc::MemBufInputSource myxml_buf(
                (const XMLByte* const)s.c_str(),
                s.size(),
                "myxml (in memory)"
            );

            parser->parse(myxml_buf);
            auto* dom = parser->getDocument();
            checkXML(dom);
        }
        if (truncated == 0) {
            GetBucketContentList = false;
        }
        else {
            truncated = 0;
            continuation = 0;
            file = 0;
        }
    }
}

void Cloud::CloudReader::DownloadFile(Cloud::CloudReader::FileEntry* entry)
{
    struct Cloud::AmzData* RequestData;
    struct Cloud::AmzDatav4* RequestDatav4;
    CURL* curl;
    CURLcode res;

    std::string s;

    // We must get the directory content
    char path[1024];
    sprintf(path, "/%s/%s", this->Bucket, entry->FileName);
    std::string strURL(this->URL);
    eraseSubStr(strURL, "http://");
    eraseSubStr(strURL, "https://");
    if (this->ProtocolVersion == "2") {
        RequestData = Cloud::ComputeDigestAmzS3v2(
            "GET",
            "application/octet-stream",
            path,
            this->TokenSecret,
            nullptr,
            0
        );
    }
    else {
        RequestDatav4 = Cloud::ComputeDigestAmzS3v4(
            "GET",
            strURL.c_str(),
            "application/octet-stream",
            path,
            this->TokenSecret,
            nullptr,
            0,
            nullptr,
            this->Region
        );
    }

    // Let's build the Header and call to curl
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
#ifdef ALLOW_SELF_SIGNED_CERTIFICATE
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
#endif
    if (curl) {
        struct curl_slist* chunk = nullptr;
        char URL[256];
        // Let's build our own header
        std::string strURL(this->URL);
        eraseSubStr(strURL, "http://");
        eraseSubStr(strURL, "https://");
        if (this->ProtocolVersion == "2") {
            chunk = Cloud::BuildHeaderAmzS3v2(strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData);
            delete RequestData;
        }
        else {
            chunk = Cloud::BuildHeaderAmzS3v4(strURL.c_str(), this->TokenAuth, RequestDatav4);
            delete RequestDatav4;
        }

        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

        sprintf(URL, "%s:%s/%s/%s", this->URL, this->TCPPort, this->Bucket, entry->FileName);
        curl_easy_setopt(curl, CURLOPT_URL, URL);

        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
        // curl read a file not a memory buffer (it shall be able to do it)

        res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }
        curl_easy_cleanup(curl);

        entry->FileStream << s;
    }
}

struct Cloud::CloudReader::FileEntry* Cloud::CloudReader::GetEntry(std::string FileName)
{
    struct Cloud::CloudReader::FileEntry* current_entry = nullptr;
    list<FileEntry*>::const_iterator it1;

    for (it1 = FileList.begin(); it1 != FileList.end(); ++it1) {
        if (strcmp(FileName.c_str(), (*it1)->FileName) == 0) {
            current_entry = (*it1);
            break;
        }
    }

    if (current_entry != nullptr) {
        (*it1)->touch = 1;
        DownloadFile(*it1);
    }

    return (current_entry);
}

int Cloud::CloudReader::isTouched(std::string FileName)
{
    list<FileEntry*>::const_iterator it1;
    for (it1 = FileList.begin(); it1 != FileList.end(); ++it1) {
        if (strcmp(FileName.c_str(), (*it1)->FileName) == 0) {
            if ((*it1)->touch) {
                return (1);
            }
            else {
                return (0);
            }
        }
    }
    return (0);
}

void Cloud::eraseSubStr(std::string& Str, const std::string& toErase)
{
    size_t pos = Str.find(toErase);
    if (pos != std::string::npos) {
        Str.erase(pos, toErase.length());
    }
}


void Cloud::CloudWriter::putNextEntry(const char* file)
{
    this->FileName = file;
    this->FileStream.str("");
    this->FileStream << std::fixed;
    this->FileStream.precision(std::numeric_limits<double>::digits10 + 1);
    this->FileStream.setf(ios::fixed, ios::floatfield);
    this->FileStream.imbue(std::locale::classic());
}

bool Cloud::CloudWriter::shouldWrite(const std::string&, const Base::Persistence*) const
{
    return true;
}

void Cloud::CloudWriter::pushCloud(const char* FileName, const char* data, long size)
{
    struct Cloud::AmzData* RequestData;
    struct Cloud::AmzDatav4* RequestDatav4;
    CURL* curl;
    CURLcode res;
    struct data_buffer curl_buffer;

    char path[1024];
    sprintf(path, "/%s/%s", this->Bucket, FileName);

    std::string strURL(this->URL);
    eraseSubStr(strURL, "http://");
    eraseSubStr(strURL, "https://");
    if (this->ProtocolVersion == "2") {
        RequestData = Cloud::ComputeDigestAmzS3v2(
            "PUT",
            "application/octet-stream",
            path,
            this->TokenSecret,
            data,
            size
        );
    }
    else {
        RequestDatav4 = Cloud::ComputeDigestAmzS3v4(
            "PUT",
            strURL.c_str(),
            "application/octet-stream",
            path,
            this->TokenSecret,
            data,
            size,
            nullptr,
            this->Region
        );
    }

    // Let's build the Header and call to curl
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
#ifdef ALLOW_SELF_SIGNED_CERTIFICATE
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
#endif
    if (curl) {
        struct curl_slist* chunk = nullptr;
        char URL[256];
        // Let's build our own header
        std::string strURL(this->URL);
        eraseSubStr(strURL, "http://");
        eraseSubStr(strURL, "https://");
        if (this->ProtocolVersion == "2") {
            chunk = Cloud::BuildHeaderAmzS3v2(strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData);
            delete RequestData;
        }
        else {
            chunk = Cloud::BuildHeaderAmzS3v4(strURL.c_str(), this->TokenAuth, RequestDatav4);
            delete RequestDatav4;
        }

        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

        // Lets build the URL for our Curl call

        sprintf(URL, "%s:%s/%s/%s", this->URL, this->TCPPort, this->Bucket, FileName);
        curl_easy_setopt(curl, CURLOPT_URL, URL);

        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_PUT, 1L);
        // curl read a file not a memory buffer (it shall be able to do it)
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
        curl_buffer.ptr = data;
        curl_buffer.remaining_size = (size_t)size;

        curl_easy_setopt(curl, CURLOPT_READDATA, &curl_buffer);

        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)size);


        res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }
        curl_easy_cleanup(curl);
    }
}

void Cloud::CloudWriter::writeFiles(void)
{
    // use a while loop because it is possible that while
    // processing the files, new ones can be added
    std::string tmp = "";
    char* cstr;
    size_t index = 0;
    if (strlen(this->FileName.c_str()) > 1) {
        // We must push the current buffer
        const std::string tmp = this->FileStream.str();
        const char* cstr = tmp.data();
        pushCloud((const char*)this->FileName.c_str(), cstr, tmp.size());
    }
    while (index < FileList.size()) {
        FileEntry entry = FileList.begin()[index];

        if (shouldWrite(entry.FileName, entry.Object)) {
            this->FileStream.str("");
            this->FileStream.precision(std::numeric_limits<double>::digits10 + 1);
            this->FileStream.setf(ios::fixed, ios::floatfield);
            this->FileStream.imbue(std::locale::classic());
            entry.Object->SaveDocFile(*this);
            tmp = this->FileStream.str();
            cstr = (char*)tmp.data();
            pushCloud((const char*)entry.FileName.c_str(), (const char*)cstr, tmp.size());
            this->FileStream.str("");
        }

        index++;
    }
}


bool Cloud::Module::cloudSave(const char* BucketName)
{
    Document* doc = GetApplication().getActiveDocument();

    auto hGrp = App::GetApplication().GetParameterGroupByPath(
        "User parameter:BaseApp/Preferences/Document"
    );

    // Save the name of the tip object in order to handle in Restore()
    if (doc->Tip.getValue()) {
        doc->TipName.setValue(doc->Tip.getValue()->getNameInDocument());
    }

    std::string LastModifiedDateString = Base::Tools::currentDateTimeString();
    doc->LastModifiedDate.setValue(LastModifiedDateString.c_str());
    // set author if needed
    bool saveAuthor = App::GetApplication()
                          .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
                          ->GetBool("prefSetAuthorOnSave", false);
    if (saveAuthor) {
        std::string Author = App::GetApplication()
                                 .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
                                 ->GetASCII("prefAuthor", "");
        doc->LastModifiedBy.setValue(Author.c_str());
    }
    if (strcmp(BucketName, doc->Label.getValue()) != 0) {
        doc->Label.setValue(BucketName);
    }

    Cloud::CloudWriter mywriter(
        (const char*)this->URL.getStrValue().c_str(),
        (const char*)this->TokenAuth.getStrValue().c_str(),
        (const char*)this->TokenSecret.getStrValue().c_str(),
        (const char*)this->TCPPort.getStrValue().c_str(),
        BucketName,
        (const char*)this->ProtocolVersion.getStrValue().c_str(),
        this->Region.getStrValue()
    );

    mywriter.putNextEntry("Document.xml");

    if (hGrp->GetBool("SaveBinaryBrep", false)) {
        mywriter.setMode("BinaryBrep");
    }
    mywriter.Stream() << "<?xml version='1.0' encoding='utf-8'?>" << endl
                      << "<!--" << endl
                      << " FreeCAD Document, see https://www.freecad.org for more information..."
                      << endl
                      << "-->" << endl;
    doc->Save(mywriter);

    // Special handling for Gui document.
    doc->signalSaveDocument(mywriter);

    // write additional files
    mywriter.writeFiles();

    return (true);
}

void readFiles(Cloud::CloudReader reader, Base::XMLReader* xmlreader)
{
    // It's possible that not all objects inside the document could be created, e.g. if a module
    // is missing that would know these object types. So, there may be data files inside the Cloud
    // file that cannot be read. We simply ignore these files.
    // On the other hand, however, it could happen that a file should be read that is not part of
    // the zip file. This happens e.g. if a document is written without GUI up but is read with GUI
    // up. In this case the associated GUI document asks for its file which is not part of the
    // file, then.
    // In either case it's guaranteed that the order of the files is kept.

    std::vector<Base::XMLReader::FileEntry>::const_iterator it = xmlreader->FileList.begin();
    while (it != xmlreader->FileList.end()) {
        if (reader.isTouched(it->FileName.c_str()) == 0) {
            Base::Reader localreader(
                reader.GetEntry(it->FileName.c_str())->FileStream,
                it->FileName,
                xmlreader->FileVersion
            );
            // for debugging only purpose
            if (false) {
                std::stringstream ss;
                ss << localreader.getStream().rdbuf();
                auto aString = ss.str();
                aString = "";
            }
            it->Object->RestoreDocFile(localreader);
            if (localreader.getLocalReader() != nullptr) {
                readFiles(reader, localreader.getLocalReader().get());
            }
        }
        it++;
    }
}

void Cloud::Module::LinkXSetValue(std::string filename)
{
    // Need to check if the document exist
    // if not we have to create a new one
    // and Restore the associated Document
    //
    std::vector<Document*> Documents;
    Documents = App::GetApplication().getDocuments();
    for (std::vector<Document*>::iterator it = Documents.begin(); it != Documents.end(); it++) {
        if ((*it)->FileName.getValue() == filename) {
            // The document exist
            // we can exit
            return;
        }
    }

    size_t header = filename.find_first_of(":");
    string protocol = filename.substr(0, header);
    string url_new = filename.substr(header + 3);  // url_new is the url excluding the http part
    size_t part2 = url_new.find_first_of("/");
    string path = url_new.substr(part2 + 1);
    // Starting from here the Document doesn't exist we must create it
    // and make it the active Document before Restoring
    App::Document* newDoc;
    string newName;
    Document* currentDoc = GetApplication().getActiveDocument();
    newName = GetApplication().getUniqueDocumentName("unnamed");
    newDoc = GetApplication().newDocument(newName.c_str(), (const char*)path.c_str(), true);
    GetApplication().setActiveDocument(newDoc);
    this->cloudRestore((const char*)path.c_str());
    GetApplication().setActiveDocument(currentDoc);
}

bool Cloud::Module::cloudRestore(const char* BucketName)
{

    Document* doc = GetApplication().getActiveDocument();
    // clean up if the document is not empty
    // !TODO: mind exceptions while restoring!
    doc->signalLinkXsetValue.connect(boost::bind(&Cloud::Module::LinkXSetValue, this, _1));

    doc->clearUndos();

    doc->clearDocument();

    std::stringstream oss;

    Cloud::CloudReader myreader(
        (const char*)this->URL.getStrValue().c_str(),
        (const char*)this->TokenAuth.getStrValue().c_str(),
        (const char*)this->TokenSecret.getStrValue().c_str(),
        (const char*)this->TCPPort.getStrValue().c_str(),
        BucketName,
        (const char*)this->ProtocolVersion.getStrValue().c_str(),
        this->Region.getStrValue()
    );

    // we shall pass there the initial Document.xml file

    Base::XMLReader reader("Document.xml", myreader.GetEntry("Document.xml")->FileStream);

    if (!reader.isValid()) {
        throw Base::FileException("Error reading Document.xml file", "Document.xml");
    }


    GetApplication().signalStartRestoreDocument(*doc);
    doc->setStatus(Document::Restoring, true);

    try {
        doc->Restore(reader);
    }
    catch (const Base::Exception& e) {
        Base::Console().error("Invalid Document.xml: %s\n", e.what());
    }

    // Special handling for Gui document, the view representations must already
    // exist, what is done in Restore().
    // Note: This file doesn't need to be available if the document has been created
    // without GUI. But if available then follow after all data files of the App document.

    doc->signalRestoreDocument(reader);

    readFiles(myreader, &reader);

    // reset all touched

    doc->afterRestore(true);

    GetApplication().signalFinishRestoreDocument(*doc);
    doc->setStatus(Document::Restoring, false);
    doc->Label.setValue(BucketName);
    // The FileName shall be an URI format
    string uri;
    uri = this->URL.getStrValue() + ":" + this->TCPPort.getStrValue() + "/" + string(BucketName);
    doc->FileName.setValue(uri);
    doc->signalLinkXsetValue.disconnect(boost::bind(&Cloud::Module::LinkXSetValue, this, _1));
    return (true);
}
