#include <HardwareSerial.h>
/**
 * HTTPClient.cpp
 *
 * Created on: 02.11.2015
 *
 * Copyright (c) 2015 Markus Sattler. All rights reserved.
 * This file is part of the HTTPClient for Arduino.
 * Port to ESP32 by Evandro Luis Copercini (2017), 
 * changed fingerprints to CA verification. 												 
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Adapted in October 2018
 */

#include <Arduino.h>
#include <esp32-hal-log.h>  

#ifdef HTTPCLIENT_1_1_COMPATIBLE
#include <WiFi.h>
#include <WiFiClientSecure.h>
#endif

#include <StreamString.h>
#include <base64.h>

#include "HTTPClient.h"

/// Cookie jar support
#include <time.h>

#ifdef HTTPCLIENT_1_1_COMPATIBLE
class TransportTraits
{
public:
    virtual ~TransportTraits()
    {
    }

    virtual std::unique_ptr<WiFiClient> create()
    {
        return std::unique_ptr<WiFiClient>(new WiFiClient());
    }

    virtual bool verify(WiFiClient& client, const char* host)
    {
        return true;
    }
};

class TLSTraits : public TransportTraits
{
public:
    TLSTraits(const char* CAcert, const char* clicert = nullptr, const char* clikey = nullptr) :
        _cacert(CAcert), _clicert(clicert), _clikey(clikey)
    {
    }

    std::unique_ptr<WiFiClient> create() override
    {
        return std::unique_ptr<WiFiClient>(new WiFiClientSecure());
    }

    bool verify(WiFiClient& client, const char* host) override
    {
        WiFiClientSecure& wcs = static_cast<WiFiClientSecure&>(client);
        if (_cacert == nullptr) {
            wcs.setInsecure();
        } else {
            wcs.setCACert(_cacert);
            wcs.setCertificate(_clicert);
            wcs.setPrivateKey(_clikey);
        }
        return true;
    }

protected:
    const char* _cacert;
    const char* _clicert;
    const char* _clikey;
};
#endif // HTTPCLIENT_1_1_COMPATIBLE

/**
 * constructor
 */
HTTPClient::HTTPClient()
{
}

/**
 * destructor
 */
HTTPClient::~HTTPClient()
{
    if(_client) {
        _client->stop();
    }
    if(_currentHeaders) {
        delete[] _currentHeaders;
    }
    if(_tcpDeprecated) {
        _tcpDeprecated.reset(nullptr);
    }
    if(_transportTraits) {
        _transportTraits.reset(nullptr);
    }
}

void HTTPClient::clear()
{
    _returnCode = 0;
    _size = -1;
    _headers = "";
}


/**
 * parsing the url for all needed parameters
 * @param client Client&
 * @param url String
 * @param https bool
 * @return success bool
 */
bool HTTPClient::begin(WiFiClient &client, String url) {
#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if(_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }
#endif

    _client = &client;

    // check for : (http: or https:)
    int index = url.indexOf(':');
    if(index < 0) {
        log_d("failed to parse protocol");
        return false;
    }

    String protocol = url.substring(0, index);
    if(protocol != "http" && protocol != "https") {
        log_d("unknown protocol '%s'", protocol.c_str());
        return false;
    }

    _port = (protocol == "https" ? 443 : 80);
    _secure = (protocol == "https");
    return beginInternal(url, protocol.c_str());
}


/**
 * directly supply all needed parameters
 * @param client Client&
 * @param host String
 * @param port uint16_t
 * @param uri String
 * @param https bool
 * @return success bool
 */
bool HTTPClient::begin(WiFiClient &client, String host, uint16_t port, String uri, bool https)
{
#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if(_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }
#endif

    _client = &client;

     clear();
    _host = host;
    _port = port;
    _uri = uri;
    _protocol = (https ? "https" : "http");
    _secure = https;
    return true;
}


#ifdef HTTPCLIENT_1_1_COMPATIBLE
bool HTTPClient::begin(String url, const char* CAcert)
{
    if(_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _port = 443;
    if (!beginInternal(url, "https")) {
        return false;
    }
    _secure = true;
    _transportTraits = TransportTraitsPtr(new TLSTraits(CAcert));
    if(!_transportTraits) {
        log_e("could not create transport traits");
        return false;
    }

    return true;
}

/**
 * parsing the url for all needed parameters
 * @param url String
 */
bool HTTPClient::begin(String url)
{
    if(_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _port = 80;
    if (!beginInternal(url, "http")) {
        return begin(url, (const char*)NULL);
    }
    _transportTraits = TransportTraitsPtr(new TransportTraits());
    if(!_transportTraits) {
        log_e("could not create transport traits");
        return false;
    }

    return true;
}
#endif // HTTPCLIENT_1_1_COMPATIBLE

bool HTTPClient::beginInternal(String url, const char* expectedProtocol)
{
    log_v("url: %s", url.c_str());

    // check for : (http: or https:
    int index = url.indexOf(':');
    if(index < 0) {
        log_e("failed to parse protocol");
        return false;
    }

    _protocol = url.substring(0, index);
    if (_protocol != expectedProtocol) {
        log_d("unexpected protocol: %s, expected %s", _protocol.c_str(), expectedProtocol);
        return false;
    }

    url.remove(0, (index + 3)); // remove http:// or https://

    index = url.indexOf('/');
    if (index == -1) {
        index = url.length();
        url += '/';
    }
    String host = url.substring(0, index);
    url.remove(0, index); // remove host part

    // get Authorization
    index = host.indexOf('@');
    if(index >= 0) {
        // auth info
        String auth = host.substring(0, index);
        host.remove(0, index + 1); // remove auth part including @
        _base64Authorization = base64::encode(auth);
    }

    // get port
    index = host.indexOf(':');
    String the_host;
    if(index >= 0) {
        the_host = host.substring(0, index); // hostname
        host.remove(0, (index + 1)); // remove hostname + :
        _port = host.toInt(); // get port
    } else {
        the_host = host;
    }
    if(_host != the_host && connected()){
        log_d("switching host from '%s' to '%s'. disconnecting first", _host.c_str(), the_host.c_str());
        _canReuse = false;
        disconnect(true);
    }
    _host = the_host;
    _uri = url;
    log_d("protocol: %s, host: %s port: %d url: %s", _protocol.c_str(), _host.c_str(), _port, _uri.c_str());
    return true;
}

#ifdef HTTPCLIENT_1_1_COMPATIBLE
bool HTTPClient::begin(String host, uint16_t port, String uri)
{
    if(_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _host = host;
    _port = port;
    _uri = uri;
    _transportTraits = TransportTraitsPtr(new TransportTraits());
    log_d("host: %s port: %d uri: %s", host.c_str(), port, uri.c_str());
    return true;
}

bool HTTPClient::begin(String host, uint16_t port, String uri, const char* CAcert)
{
    if(_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _host = host;
    _port = port;
    _uri = uri;

    if (strlen(CAcert) == 0) {
        return false;
    }
    _secure = true;
    _transportTraits = TransportTraitsPtr(new TLSTraits(CAcert));
    return true;
}

bool HTTPClient::begin(String host, uint16_t port, String uri, const char* CAcert, const char* cli_cert, const char* cli_key)
{
    if(_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _host = host;
    _port = port;
    _uri = uri;

    if (strlen(CAcert) == 0) {
        return false;
    }
    _secure = true;
    _transportTraits = TransportTraitsPtr(new TLSTraits(CAcert, cli_cert, cli_key));
    return true;
}
#endif // HTTPCLIENT_1_1_COMPATIBLE

/**
 * end
 * called after the payload is handled
 */
void HTTPClient::end(void)
{
    disconnect(false);
    clear();
}



/**
 * disconnect
 * close the TCP socket
 */
void HTTPClient::disconnect(bool preserveClient)
{
    if(connected()) {
        if(_client->available() > 0) {
            log_d("still data in buffer (%d), clean up.\n", _client->available());
                _client->flush();
        }

        if(_reuse && _canReuse) {
            log_d("tcp keep open for reuse");
        } else {
            log_d("tcp stop");
            _client->stop();
            if(!preserveClient) {
                _client = nullptr;
#ifdef HTTPCLIENT_1_1_COMPATIBLE
                if(_tcpDeprecated) {
                    _transportTraits.reset(nullptr);
                    _tcpDeprecated.reset(nullptr);
                }
#endif
            }
        }
    } else {
        log_d("tcp is closed\n");
    }
}


/**
 * connected
 * @return connected status
 */
bool HTTPClient::connected()
{
    if(_client) {
        return ((_client->available() > 0) || _client->connected());
    }
    return false;
}

/**
 * try to reuse the connection to the server
 * keep-alive
 * @param reuse bool
 */
void HTTPClient::setReuse(bool reuse)
{
    _reuse = reuse;
}

/**
 * set User Agent
 * @param userAgent const char *
 */
void HTTPClient::setUserAgent(const String& userAgent)
{
    _userAgent = userAgent;
}

/**
 * set the Authorizatio for the http request
 * @param user const char *
 * @param password const char *
 */
void HTTPClient::setAuthorization(const char * user, const char * password)
{
    if(user && password) {
        String auth = user;
        auth += ":";
        auth += password;
        _base64Authorization = base64::encode(auth);
    }
}

/**
 * set the Authorizatio for the http request
 * @param auth const char * base64
 */
void HTTPClient::setAuthorization(const char * auth)
{
    if(auth) {
        _base64Authorization = auth;
    }
}

/**
 * set the Authorization type for the http request
 * @param authType const char *
 */
void HTTPClient::setAuthorizationType(const char * authType)
{
    if(authType) {
        _authorizationType = authType;
    }
}

/**
 * set the timeout (ms) for establishing a connection to the server
 * @param connectTimeout int32_t
 */
void HTTPClient::setConnectTimeout(int32_t connectTimeout)
{
    _connectTimeout = connectTimeout;
}

/**
 * set the timeout for the TCP connection
 * @param timeout unsigned int
 */
void HTTPClient::setTimeout(uint16_t timeout)
{
    _tcpTimeout = timeout;
    if(connected()) {
        _client->setTimeout((timeout + 500) / 1000);
    }
}

/**
 * use HTTP1.0
 * @param use
 */
void HTTPClient::useHTTP10(bool useHTTP10)
{
    _useHTTP10 = useHTTP10;
    _reuse = !useHTTP10;
}

/**
 * send a GET request
 * @return http code
 */
int HTTPClient::GET()
{
    return sendRequest("GET");
}

/**
 * sends a post request to the server
 * @param payload uint8_t *
 * @param size size_t
 * @return http code
 */
int HTTPClient::POST(uint8_t * payload, size_t size)
{
    return sendRequest("POST", payload, size);
}

int HTTPClient::POST(String payload)
{
    return POST((uint8_t *) payload.c_str(), payload.length());
}

/**
 * sends a patch request to the server
 * @param payload uint8_t *
 * @param size size_t
 * @return http code
 */
int HTTPClient::PATCH(uint8_t * payload, size_t size)
{
    return sendRequest("PATCH", payload, size);
}

int HTTPClient::PATCH(String payload)
{
    return PATCH((uint8_t *) payload.c_str(), payload.length());
}

/**
 * sends a put request to the server
 * @param payload uint8_t *
 * @param size size_t
 * @return http code
 */
int HTTPClient::PUT(uint8_t * payload, size_t size) {
    return sendRequest("PUT", payload, size);
}

int HTTPClient::PUT(String payload) {
    return PUT((uint8_t *) payload.c_str(), payload.length());
}

/**
 * sendRequest
 * @param type const char *     "GET", "POST", ....
 * @param payload String        data for the message body
 * @return
 */
int HTTPClient::sendRequest(const char * type, String payload)
{
    return sendRequest(type, (uint8_t *) payload.c_str(), payload.length());
}

/**
 * sendRequest
 * @param type const char *     "GET", "POST", ....
 * @param payload uint8_t *     data for the message body if null not send
 * @param size size_t           size for the message body if 0 not send
 * @return -1 if no info or > 0 when Content-Length is set by server
 */
int HTTPClient::sendRequest(const char * type, uint8_t * payload, size_t size)
{
    int code;
    bool redirect = false;
    uint16_t redirectCount = 0;
    do {
        // wipe out any existing headers from previous request
        for(size_t i = 0; i < _headerKeysCount; i++) {
            if (_currentHeaders[i].value.length() > 0) {
                _currentHeaders[i].value.clear();
            }
        }

        log_d("request type: '%s' redirCount: %d\n", type, redirectCount);
        
        // connect to server
        if(!connect()) {
            return returnError(HTTPC_ERROR_CONNECTION_REFUSED);
        }

        if(payload && size > 0) {
            addHeader(F("Content-Length"), String(size));
        }

        // add cookies to header, if present
        String cookie_string;
        if(generateCookieString(&cookie_string)) {
            addHeader("Cookie", cookie_string);
        }

        // send Header
        if(!sendHeader(type)) {
            return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
        }

        // send Payload if needed
        if(payload && size > 0) {
            if(_client->write(&payload[0], size) != size) {
                return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
            }
        }

        code = handleHeaderResponse();
        log_d("sendRequest code=%d\n", code);

        // Handle redirections as stated in RFC document:
        // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
        //
        // Implementing HTTP_CODE_FOUND as redirection with GET method,
        // to follow most of existing user agent implementations.
        //
        redirect = false;
        if (
            _followRedirects != HTTPC_DISABLE_FOLLOW_REDIRECTS && 
            redirectCount < _redirectLimit &&
            _location.length() > 0
        ) {
            switch (code) {
                // redirecting using the same method
                case HTTP_CODE_MOVED_PERMANENTLY:
                case HTTP_CODE_TEMPORARY_REDIRECT: {
                    if (
                        // allow to force redirections on other methods
                        // (the RFC require user to accept the redirection)
                        _followRedirects == HTTPC_FORCE_FOLLOW_REDIRECTS ||
                        // allow GET and HEAD methods without force
                        !strcmp(type, "GET") || 
                        !strcmp(type, "HEAD")
                    ) {
                        redirectCount += 1;
                        log_d("following redirect (the same method): '%s' redirCount: %d\n", _location.c_str(), redirectCount);
                        if (!setURL(_location)) {
                            log_d("failed setting URL for redirection\n");
                            // no redirection
                            break;
                        }
                        // redirect using the same request method and payload, diffrent URL
                        redirect = true;
                    }
                    break;
                }
                // redirecting with method dropped to GET or HEAD
                // note: it does not need `HTTPC_FORCE_FOLLOW_REDIRECTS` for any method
                case HTTP_CODE_FOUND:
                case HTTP_CODE_SEE_OTHER: {
                    redirectCount += 1;
                    log_d("following redirect (dropped to GET/HEAD): '%s' redirCount: %d\n", _location.c_str(), redirectCount);
                    if (!setURL(_location)) {
                        log_d("failed setting URL for redirection\n");
                        // no redirection
                        break;
                    }
                    // redirect after changing method to GET/HEAD and dropping payload
                    type = "GET";
                    payload = nullptr;
                    size = 0;
                    redirect = true;
                    break;
                }

                default:
                    break;
            }
        }

    } while (redirect);
    // handle Server Response (Header)
    return returnError(code);
}

/**
 * sendRequest
 * @param type const char *     "GET", "POST", ....
 * @param stream Stream *       data stream for the message body
 * @param size size_t           size for the message body if 0 not Content-Length is send
 * @return -1 if no info or > 0 when Content-Length is set by server
 */
int HTTPClient::sendRequest(const char * type, Stream * stream, size_t size)
{

    if(!stream) {
        return returnError(HTTPC_ERROR_NO_STREAM);
    }

    // connect to server
    if(!connect()) {
        return returnError(HTTPC_ERROR_CONNECTION_REFUSED);
    }

    if(size > 0) {
        addHeader("Content-Length", String(size));
    }

    // add cookies to header, if present
    String cookie_string;
    if(generateCookieString(&cookie_string)) {
        addHeader("Cookie", cookie_string);
    }

    // send Header
    if(!sendHeader(type)) {
        return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
    }

    int buff_size = HTTP_TCP_BUFFER_SIZE;

    int len = size;
    int bytesWritten = 0;

    if(len == 0) {
        len = -1;
    }

    // if possible create smaller buffer then HTTP_TCP_BUFFER_SIZE
    if((len > 0) && (len < HTTP_TCP_BUFFER_SIZE)) {
        buff_size = len;
    }

    // create buffer for read
    uint8_t * buff = (uint8_t *) malloc(buff_size);

    if(buff) {
        // read all data from stream and send it to server
        while(connected() && (stream->available() > -1) && (len > 0 || len == -1)) {

            // get available data size
            int sizeAvailable = stream->available();

            if(sizeAvailable) {

                int readBytes = sizeAvailable;

                // read only the asked bytes
                if(len > 0 && readBytes > len) {
                    readBytes = len;
                }

                // not read more the buffer can handle
                if(readBytes > buff_size) {
                    readBytes = buff_size;
                }

                // read data
                int bytesRead = stream->readBytes(buff, readBytes);

                // write it to Stream
                int bytesWrite = _client->write((const uint8_t *) buff, bytesRead);
                bytesWritten += bytesWrite;

                // are all Bytes a writen to stream ?
                if(bytesWrite != bytesRead) {
                    log_d("short write, asked for %d but got %d retry...", bytesRead, bytesWrite);

                    // check for write error
                    if(_client->getWriteError()) {
                        log_d("stream write error %d", _client->getWriteError());

                        //reset write error for retry
                        _client->clearWriteError();
                    }

                    // some time for the stream
                    delay(1);

                    int leftBytes = (readBytes - bytesWrite);

                    // retry to send the missed bytes
                    bytesWrite = _client->write((const uint8_t *) (buff + bytesWrite), leftBytes);
                    bytesWritten += bytesWrite;

                    if(bytesWrite != leftBytes) {
                        // failed again
                        log_d("short write, asked for %d but got %d failed.", leftBytes, bytesWrite);
                        free(buff);
                        return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
                    }
                }

                // check for write error
                if(_client->getWriteError()) {
                    log_d("stream write error %d", _client->getWriteError());
                    free(buff);
                    return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
                }

                // count bytes to read left
                if(len > 0) {
                    len -= readBytes;
                }

                delay(0);
            } else {
                delay(1);
            }
        }

        free(buff);

        if(size && (int) size != bytesWritten) {
            log_d("Stream payload bytesWritten %d and size %d mismatch!.", bytesWritten, size);
            log_d("ERROR SEND PAYLOAD FAILED!");
            return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
        } else {
            log_d("Stream payload written: %d", bytesWritten);
        }

    } else {
        log_d("too less ram! need %d", HTTP_TCP_BUFFER_SIZE);
        return returnError(HTTPC_ERROR_TOO_LESS_RAM);
    }

    // handle Server Response (Header)
    return returnError(handleHeaderResponse());
}

/**
 * size of message body / payload
 * @return -1 if no info or > 0 when Content-Length is set by server
 */
int HTTPClient::getSize(void)
{
    return _size;
}

/**
 * returns the stream of the tcp connection
 * @return WiFiClient
 */
WiFiClient& HTTPClient::getStream(void)
{
    if (connected()) {
        return *_client;
    }

    log_w("getStream: not connected");
    static WiFiClient empty;
    return empty;
}

/**
 * returns a pointer to the stream of the tcp connection
 * @return WiFiClient*
 */
WiFiClient* HTTPClient::getStreamPtr(void)
{
    if(connected()) {
        return _client;
    }

    log_w("getStreamPtr: not connected");
    return nullptr;
}

/**
 * write all  message body / payload to Stream
 * @param stream Stream *
 * @return bytes written ( negative values are error codes )
 */
int HTTPClient::writeToStream(Stream * stream)
{

    if(!stream) {
        return returnError(HTTPC_ERROR_NO_STREAM);
    }

    if(!connected()) {
        return returnError(HTTPC_ERROR_NOT_CONNECTED);
    }

    // get length of document (is -1 when Server sends no Content-Length header)
    int len = _size;
    int ret = 0;

    if(_transferEncoding == HTTPC_TE_IDENTITY) {
        ret = writeToStreamDataBlock(stream, len);

        // have we an error?
        if(ret < 0) {
            return returnError(ret);
        }
    } else if(_transferEncoding == HTTPC_TE_CHUNKED) {
        int size = 0;
        while(1) {
            if(!connected()) {
                return returnError(HTTPC_ERROR_CONNECTION_LOST);
            }
            String chunkHeader = _client->readStringUntil('\n');

            if(chunkHeader.length() <= 0) {
                return returnError(HTTPC_ERROR_READ_TIMEOUT);
            }

            chunkHeader.trim(); // remove \r

            // read size of chunk
            len = (uint32_t) strtol((const char *) chunkHeader.c_str(), NULL, 16);
            size += len;
            log_d(" read chunk len: %d", len);

            // data left?
            if(len > 0) {
                int r = writeToStreamDataBlock(stream, len);
                if(r < 0) {
                    // error in writeToStreamDataBlock
                    return returnError(r);
                }
                ret += r;
            } else {

                // if no length Header use global chunk size
                if(_size <= 0) {
                    _size = size;
                }

                // check if we have write all data out
                if(ret != _size) {
                    return returnError(HTTPC_ERROR_STREAM_WRITE);
                }
                break;
            }

            // read trailing \r\n at the end of the chunk
            char buf[2];
            auto trailing_seq_len = _client->readBytes((uint8_t*)buf, 2);
            if (trailing_seq_len != 2 || buf[0] != '\r' || buf[1] != '\n') {
                return returnError(HTTPC_ERROR_READ_TIMEOUT);
            }

            delay(0);
        }
    } else {
        return returnError(HTTPC_ERROR_ENCODING);
    }

//    end();
    disconnect(true);
    return ret;
}

/**
 * return all payload as String (may need lot of ram or trigger out of memory!)
 * @return String
 */
String HTTPClient::getString(void)
{
    // _size can be -1 when Server sends no Content-Length header
    if(_size > 0 || _size == -1) {
        StreamString sstring;
        // try to reserve needed memory (noop if _size == -1)
        if(sstring.reserve((_size + 1))) {
            writeToStream(&sstring);
            return sstring;            
        } else {
            log_d("not enough memory to reserve a string! need: %d", (_size + 1));
        }
    }

    return "";
}

/**
 * converts error code to String
 * @param error int
 * @return String
 */
String HTTPClient::errorToString(int error)
{
    switch(error) {
    case HTTPC_ERROR_CONNECTION_REFUSED:
        return F("connection refused");
    case HTTPC_ERROR_SEND_HEADER_FAILED:
        return F("send header failed");
    case HTTPC_ERROR_SEND_PAYLOAD_FAILED:
        return F("send payload failed");
    case HTTPC_ERROR_NOT_CONNECTED:
        return F("not connected");
    case HTTPC_ERROR_CONNECTION_LOST:
        return F("connection lost");
    case HTTPC_ERROR_NO_STREAM:
        return F("no stream");
    case HTTPC_ERROR_NO_HTTP_SERVER:
        return F("no HTTP server");
    case HTTPC_ERROR_TOO_LESS_RAM:
        return F("too less ram");
    case HTTPC_ERROR_ENCODING:
        return F("Transfer-Encoding not supported");
    case HTTPC_ERROR_STREAM_WRITE:
        return F("Stream write error");
    case HTTPC_ERROR_READ_TIMEOUT:
        return F("read Timeout");
    default:
        return String();
    }
}

/**
 * adds Header to the request
 * @param name
 * @param value
 * @param first
 */
void HTTPClient::addHeader(const String& name, const String& value, bool first, bool replace)
{
    // not allow set of Header handled by code
    if(!name.equalsIgnoreCase(F("Connection")) &&
       !name.equalsIgnoreCase(F("User-Agent")) &&
       !name.equalsIgnoreCase(F("Host")) &&
       !(name.equalsIgnoreCase(F("Authorization")) && _base64Authorization.length())){

        String headerLine = name;
        headerLine += ": ";

        if (replace) {
            int headerStart = _headers.indexOf(headerLine);
            if (headerStart != -1 && (headerStart == 0 || _headers[headerStart - 1] == '\n')) {
                int headerEnd = _headers.indexOf('\n', headerStart);
                _headers = _headers.substring(0, headerStart) + _headers.substring(headerEnd + 1);
            }
        }

        headerLine += value;
        headerLine += "\r\n";
        if(first) {
            _headers = headerLine + _headers;
        } else {
            _headers += headerLine;
        }
    }
}

void HTTPClient::collectHeaders(const char* headerKeys[], const size_t headerKeysCount)
{
    _headerKeysCount = headerKeysCount;
    if(_currentHeaders) {
        delete[] _currentHeaders;
    }
    _currentHeaders = new RequestArgument[_headerKeysCount];
    for(size_t i = 0; i < _headerKeysCount; i++) {
        _currentHeaders[i].key = headerKeys[i];
    }
}

String HTTPClient::header(const char* name)
{
    for(size_t i = 0; i < _headerKeysCount; ++i) {
        if(_currentHeaders[i].key == name) {
            return _currentHeaders[i].value;
        }
    }
    return String();
}

String HTTPClient::header(size_t i)
{
    if(i < _headerKeysCount) {
        return _currentHeaders[i].value;
    }
    return String();
}

String HTTPClient::headerName(size_t i)
{
    if(i < _headerKeysCount) {
        return _currentHeaders[i].key;
    }
    return String();
}

int HTTPClient::headers()
{
    return _headerKeysCount;
}

bool HTTPClient::hasHeader(const char* name)
{
    for(size_t i = 0; i < _headerKeysCount; ++i) {
        if((_currentHeaders[i].key == name) && (_currentHeaders[i].value.length() > 0)) {
            return true;
        }
    }
    return false;
}

/**
 * init TCP connection and handle ssl verify if needed
 * @return true if connection is ok
 */
bool HTTPClient::connect(void)
{
    if(connected()) {
        if(_reuse) {
            log_d("already connected, reusing connection");
        } else {
            log_d("already connected, try reuse!");
        }
        while(_client->available() > 0) {
            _client->read();
        }
        return true;
    }

#ifdef HTTPCLIENT_1_1_COMPATIBLE
     if(_transportTraits && !_client) {
        _tcpDeprecated = _transportTraits->create();
        if(!_tcpDeprecated) {
            log_e("failed to create client");
            return false;
        }
        _client = _tcpDeprecated.get();
     }
#endif

    if (!_client) {
        log_d("HTTPClient::begin was not called or returned error");
        return false;
    }
#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if (_tcpDeprecated && !_transportTraits->verify(*_client, _host.c_str())) {
        log_d("transport level verify failed");
        _client->stop();
        return false;
    }	
#endif
    if(!_client->connect(_host.c_str(), _port, _connectTimeout)) {
        log_d("failed connect to %s:%u", _host.c_str(), _port);
        return false;
    }

    // set Timeout for WiFiClient and for Stream::readBytesUntil() and Stream::readStringUntil()
    _client->setTimeout((_tcpTimeout + 500) / 1000);	

    log_d(" connected to %s:%u", _host.c_str(), _port);


/*
#ifdef ESP8266
    _client->setNoDelay(true);
#endif
 */
 return connected();
}

/**
 * sends HTTP request header
 * @param type (GET, POST, ...)
 * @return status
 */
bool HTTPClient::sendHeader(const char * type)
{
    if(!connected()) {
        return false;
    }

    String header = String(type) + " " + _uri + F(" HTTP/1.");

    if(_useHTTP10) {
        header += "0";
    } else {
        header += "1";
    }

    header += String(F("\r\nHost: ")) + _host;
    if (_port != 80 && _port != 443)
    {
        header += ':';
        header += String(_port);
    }
    header += String(F("\r\nUser-Agent: ")) + _userAgent +
              F("\r\nConnection: ");

    if(_reuse) {
        header += F("keep-alive");
    } else {
        header += F("close");
    }
    header += "\r\n";

    if(!_useHTTP10) {
        header += F("Accept-Encoding: identity;q=1,chunked;q=0.1,*;q=0\r\n");
    }

    if(_base64Authorization.length()) {
        _base64Authorization.replace("\n", "");
        header += F("Authorization: ");
        header += _authorizationType; 
        header += " ";
        header += _base64Authorization;
        header += "\r\n";
    }

    header += _headers + "\r\n";

    return (_client->write((const uint8_t *) header.c_str(), header.length()) == header.length());
}

/**
 * reads the response from the server
 * @return int http code
 */
int HTTPClient::handleHeaderResponse()
{

    if(!connected()) {
        return HTTPC_ERROR_NOT_CONNECTED;
    }

    _returnCode = 0;
    _size = -1;
    _canReuse = _reuse;

    String transferEncoding;

    _transferEncoding = HTTPC_TE_IDENTITY;
    unsigned long lastDataTime = millis();
    bool firstLine = true;
    String date;

    while(connected()) {
        size_t len = _client->available();
        if(len > 0) {
            String headerLine = _client->readStringUntil('\n');
            headerLine.trim(); // remove \r

            lastDataTime = millis();

            log_v("RX: '%s'", headerLine.c_str());

            if(firstLine) {
		        firstLine = false;
                if(_canReuse && headerLine.startsWith("HTTP/1.")) {
                    _canReuse = (headerLine[sizeof "HTTP/1." - 1] != '0');
                }
                int codePos = headerLine.indexOf(' ') + 1;
                _returnCode = headerLine.substring(codePos, headerLine.indexOf(' ', codePos)).toInt();
            } else if(headerLine.indexOf(':')) {
                String headerName = headerLine.substring(0, headerLine.indexOf(':'));
                String headerValue = headerLine.substring(headerLine.indexOf(':') + 1);
                headerValue.trim();

                if(headerName.equalsIgnoreCase("Date")) {
                    date = headerValue;
                }

                if(headerName.equalsIgnoreCase("Content-Length")) {
                    _size = headerValue.toInt();
                }

                if(_canReuse && headerName.equalsIgnoreCase("Connection")) {
                    if(headerValue.indexOf("close") >= 0 && headerValue.indexOf("keep-alive") < 0) {
                        _canReuse = false;
                    }
                }

                if(headerName.equalsIgnoreCase("Transfer-Encoding")) {
                    transferEncoding = headerValue;
                }

                if (headerName.equalsIgnoreCase("Location")) {
                    _location = headerValue;
                }

                if (headerName.equalsIgnoreCase("Set-Cookie")) {
                    setCookie(date, headerValue);
                }

                for (size_t i = 0; i < _headerKeysCount; i++) {
                    if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) {
                        // Uncomment the following lines if you need to add support for multiple headers with the same key:
                        // if (!_currentHeaders[i].value.isEmpty()) {
                        //     // Existing value, append this one with a comma
                        //     _currentHeaders[i].value += ',';
                        //     _currentHeaders[i].value += headerValue;
                        // } else {
                        _currentHeaders[i].value = headerValue;
                        // }
                        break; // We found a match, stop looking
                    }
                }

            }

            if(headerLine == "") {
                log_d("code: %d", _returnCode);

                if(_size > 0) {
                    log_d("size: %d", _size);
                }

                if(transferEncoding.length() > 0) {
                    log_d("Transfer-Encoding: %s", transferEncoding.c_str());
                    if(transferEncoding.equalsIgnoreCase("chunked")) {
                        _transferEncoding = HTTPC_TE_CHUNKED;
                    } else if(transferEncoding.equalsIgnoreCase("identity")) {
                        _transferEncoding = HTTPC_TE_IDENTITY;
                    } else {
                        return HTTPC_ERROR_ENCODING;
                    }
                } else {
                    _transferEncoding = HTTPC_TE_IDENTITY;
                }

                if(_returnCode) {
                    return _returnCode;
                } else {
                    log_d("Remote host is not an HTTP Server!");
                    return HTTPC_ERROR_NO_HTTP_SERVER;
                }
            }

        } else {
            if((millis() - lastDataTime) > _tcpTimeout) {
                return HTTPC_ERROR_READ_TIMEOUT;
            }
            delay(10);
        }
    }

    return HTTPC_ERROR_CONNECTION_LOST;
}

/**
 * write one Data Block to Stream
 * @param stream Stream *
 * @param size int
 * @return < 0 = error >= 0 = size written
 */
int HTTPClient::writeToStreamDataBlock(Stream * stream, int size)
{
    int buff_size = HTTP_TCP_BUFFER_SIZE;
    int len = size;
    int bytesWritten = 0;

    // if possible create smaller buffer then HTTP_TCP_BUFFER_SIZE
    if((len > 0) && (len < HTTP_TCP_BUFFER_SIZE)) {
        buff_size = len;
    }

    // create buffer for read
    uint8_t * buff = (uint8_t *) malloc(buff_size);

    if(buff) {
        // read all data from server
        while(connected() && (len > 0 || len == -1)) {

            // get available data size
            size_t sizeAvailable = _client->available();

            if(sizeAvailable) {

                int readBytes = sizeAvailable;

                // read only the asked bytes
                if(len > 0 && readBytes > len) {
                    readBytes = len;
                }

                // not read more the buffer can handle
                if(readBytes > buff_size) {
                    readBytes = buff_size;
                }
		    
        		// stop if no more reading    
        		if (readBytes == 0)
        			break;

                // read data
                int bytesRead = _client->readBytes(buff, readBytes);

                // write it to Stream
                int bytesWrite = stream->write(buff, bytesRead);
                bytesWritten += bytesWrite;

                // are all Bytes a writen to stream ?
                if(bytesWrite != bytesRead) {
                    log_d("short write asked for %d but got %d retry...", bytesRead, bytesWrite);

                    // check for write error
                    if(stream->getWriteError()) {
                        log_d("stream write error %d", stream->getWriteError());

                        //reset write error for retry
                        stream->clearWriteError();
                    }

                    // some time for the stream
                    delay(1);

                    int leftBytes = (readBytes - bytesWrite);

                    // retry to send the missed bytes
                    bytesWrite = stream->write((buff + bytesWrite), leftBytes);
                    bytesWritten += bytesWrite;

                    if(bytesWrite != leftBytes) {
                        // failed again
                        log_w("short write asked for %d but got %d failed.", leftBytes, bytesWrite);
                        free(buff);
                        return HTTPC_ERROR_STREAM_WRITE;
                    }
                }

                // check for write error
                if(stream->getWriteError()) {
                    log_w("stream write error %d", stream->getWriteError());
                    free(buff);
                    return HTTPC_ERROR_STREAM_WRITE;
                }

                // count bytes to read left
                if(len > 0) {
                    len -= readBytes;
                }

                delay(0);
            } else {
                delay(1);
            }
        }

        free(buff);

        log_d("connection closed or file end (written: %d).", bytesWritten);

        if((size > 0) && (size != bytesWritten)) {
            log_d("bytesWritten %d and size %d mismatch!.", bytesWritten, size);
            return HTTPC_ERROR_STREAM_WRITE;
        }

    } else {
        log_w("too less ram! need %d", HTTP_TCP_BUFFER_SIZE);
        return HTTPC_ERROR_TOO_LESS_RAM;
    }

    return bytesWritten;
}

/**
 * called to handle error return, may disconnect the connection if still exists
 * @param error
 * @return error
 */
int HTTPClient::returnError(int error)
{
    if(error < 0) {
        log_w("error(%d): %s", error, errorToString(error).c_str());
        if(connected()) {
            log_d("tcp stop");
            _client->stop();
        }
    }
    return error;
}

void HTTPClient::setFollowRedirects(followRedirects_t follow)
{
    _followRedirects = follow;
}

void HTTPClient::setRedirectLimit(uint16_t limit)
{
    _redirectLimit = limit;
}

/**
 * set the URL to a new value. Handy for following redirects.
 * @param url
 */
bool HTTPClient::setURL(const String& url)
{
    // if the new location is only a path then only update the URI
    if (url && url[0] == '/') {
        _uri = url;
        clear();
        return true;
    }

    if (!url.startsWith(_protocol + ':')) {
        log_d("new URL not the same protocol, expected '%s', URL: '%s'\n", _protocol.c_str(), url.c_str());
        return false;
    }

    // check if the port is specified
    int indexPort = url.indexOf(':', 6); // find the first ':' excluding the one from the protocol
    int indexURI = url.indexOf('/', 7); // find where the URI starts to make sure the ':' is not part of it
    if (indexPort == -1 || indexPort > indexURI) {
        // the port is not specified
        _port = (_protocol == "https" ? 443 : 80);
    }

    // disconnect but preserve _client. 
    // Also have to keep the connection otherwise it will free some of the memory used by _client 
    // and will blow up later when trying to do _client->available() or similar
    _canReuse = true;
    disconnect(true);
    return beginInternal(url, _protocol.c_str());
}

const String &HTTPClient::getLocation(void)
{
    return _location;
}

void HTTPClient::setCookieJar(CookieJar* cookieJar)
{
    _cookieJar = cookieJar;
}

void HTTPClient::resetCookieJar()
{
    _cookieJar = nullptr;
}

void HTTPClient::clearAllCookies()
{
    if (_cookieJar) _cookieJar->clear();
}

void HTTPClient::setCookie(String date, String headerValue)
{
    if (!_cookieJar)
    {
        return;
    }
    #define HTTP_TIME_PATTERN "%a, %d %b %Y %H:%M:%S"

    Cookie cookie;
    String value;
    int pos1, pos2;

    struct tm tm;
    strptime(date.c_str(), HTTP_TIME_PATTERN, &tm);
    cookie.date = mktime(&tm);

    pos1 = headerValue.indexOf('=');
    pos2 = headerValue.indexOf(';');

    if (pos1 >= 0 && pos2 > pos1){
        cookie.name = headerValue.substring(0, pos1);
        cookie.value = headerValue.substring(pos1 + 1, pos2);
    } else {
        return;     // invalid cookie header
    }

    // only Cookie Attributes are case insensitive from this point on
    headerValue.toLowerCase();

    // expires
    if (headerValue.indexOf("expires=") >= 0){
        pos1 = headerValue.indexOf("expires=") + strlen("expires=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            value = headerValue.substring(pos1, pos2);
        else
            value = headerValue.substring(pos1);

        strptime(value.c_str(), HTTP_TIME_PATTERN, &tm);
        cookie.expires.date = mktime(&tm);
        cookie.expires.valid = true;
    }

    // max-age
    if (headerValue.indexOf("max-age=") >= 0){
        pos1 = headerValue.indexOf("max-age=") + strlen("max-age=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            value = headerValue.substring(pos1, pos2);
        else
            value = headerValue.substring(pos1);

        cookie.max_age.duration = value.toInt();
        cookie.max_age.valid = true;
    }

    // domain
    if (headerValue.indexOf("domain=") >= 0){
        pos1 = headerValue.indexOf("domain=") + strlen("domain=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            value = headerValue.substring(pos1, pos2);
        else
            value = headerValue.substring(pos1);

        if (value.startsWith(".")) value.remove(0, 1);

        if (_host.indexOf(value) >= 0) {
            cookie.domain = value;
        } else {
            return;     // server tries to set a cookie on a different domain; ignore it
        }
    } else {
        pos1 = _host.lastIndexOf('.', _host.lastIndexOf('.') - 1);
        if (pos1 >= 0)
            cookie.domain = _host.substring(pos1 + 1);
        else
            cookie.domain = _host;
    }

    // path
    if (headerValue.indexOf("path=") >= 0){
        pos1 = headerValue.indexOf("path=") + strlen("path=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            cookie.path = headerValue.substring(pos1, pos2);
        else
            cookie.path = headerValue.substring(pos1);
    }

    // HttpOnly
    cookie.http_only = (headerValue.indexOf("httponly") >= 0);

    // secure
    cookie.secure = (headerValue.indexOf("secure") >= 0);

    // overwrite or delete cookie in/from cookie jar
    time_t now_local = time(NULL);
    time_t now_gmt = mktime(gmtime(&now_local));

    bool found = false;

    for (auto c = _cookieJar->begin(); c != _cookieJar->end(); ++c) {
        if (c->domain == cookie.domain && c->name == cookie.name) {
            // when evaluating, max-age takes precedence over expires if both are defined
            if ((cookie.max_age.valid && ((cookie.date + cookie.max_age.duration) < now_gmt)) || cookie.max_age.duration <= 0
            || (!cookie.max_age.valid && cookie.expires.valid && cookie.expires.date < now_gmt)) {
                _cookieJar->erase(c);
                c--;
            } else {
                *c = cookie;
            }
            found = true;
        }
    }

    // add cookie to jar
    if (!found && !(cookie.max_age.valid && cookie.max_age.duration <= 0)) 
        _cookieJar->push_back(cookie);

}

bool HTTPClient::generateCookieString(String *cookieString)
{
    time_t now_local = time(NULL);
    time_t now_gmt = mktime(gmtime(&now_local));

    *cookieString = "";
    bool found = false;

    if (!_cookieJar)
    {
        return false;
    }
 	for (auto c = _cookieJar->begin(); c != _cookieJar->end(); ++c) {
        if ((c->max_age.valid && ((c->date + c->max_age.duration) < now_gmt)) || (!c->max_age.valid && c->expires.valid && c->expires.date < now_gmt)) {
            _cookieJar->erase(c);
            c--;
        } else if (_host.indexOf(c->domain) >= 0 && (!c->secure || _secure) ) {
            if (*cookieString == "")
                *cookieString = c->name + "=" + c->value;
            else
                *cookieString += " ;" + c->name + "=" + c->value;
            found = true;
        }
    }
    
    return found;
}
