#include "HttpClient.h"
#include <IPAddress.h>

//#define DEBUG_HTTP_CLIENT

/*
 * End of HttpClientHelper
 */

HttpClient::HttpClient(const char *address, uint16_t port, uint32_t timeout) : WiFiClient(), _pAddress(address), _port(port)
{
  setTimeout(timeout);
}

HttpClient::HttpClient(const char *address, const char *resource, uint16_t port, uint32_t timeout) : HttpClient(address, port, timeout)
{
  if(resource != NULL)
  {
    _resource = (char *)malloc(strlen(resource) * sizeof(char) + 1);
    strcpy(_resource, resource);
  }
}

HttpClient::HttpClient(const HttpClient &object) : WiFiClient()
{
  if(object._resource != NULL)
  {
    _resource = (char *)malloc(strlen(object._resource) * sizeof(char) + 1);
    strcpy(_resource, object._resource);
  }
  else
    _resource = NULL;
    
  _pAddress = object._pAddress;
  _connectionStatus = object._connectionStatus;
  _keepAlive = object._keepAlive;
  _maxRetries = object._maxRetries;
  _retries = object._retries;
  _port = object._port;
  _httpCode = object._httpCode;
  _httpCodeParsed = object._httpCodeParsed;
}

HttpClient::~HttpClient()
{
  if(connected())
    stop();
  if(_resource != NULL)
    free(_resource);
}

boolean HttpClient::connectByHostOrIp()
{
  IPAddress ipAddress;
  #ifdef DEBUG_HTTP_CLIENT
  Serial.printf("About to connect\n");
  #endif

  //If we constructed the HttpClient with a NULL server address, we don't go further.
  if(!_pAddress)
  {
    #ifdef DEBUG_HTTP_CLIENT
    Serial.printf("IP Address is NULL !\n");
    #endif
    _connectionStatus = FAILED;
    return false;
  }

  if(ipAddress.fromString(_pAddress))
  {
    _isIp = true;
    _connectionStatus = connect(ipAddress, _port) == 1 ? SUCCESSFUL : FAILED;
    #ifdef DEBUG_HTTP_CLIENT
    Serial.printf("Correct ip address. Connection status : %d\n", _connectionStatus);
    #endif
  }
  else
  {
    _isIp = false;
    _connectionStatus = connect(_pAddress, _port) == 1 ? SUCCESSFUL : FAILED;
    #ifdef DEBUG_HTTP_CLIENT
    Serial.printf("Probably a hostname. Connection status : %d\n", _connectionStatus);
    #endif
  }

  return _connectionStatus == SUCCESSFUL;
}

HttpClient::HttpQueryStatus HttpClient::sendHttpQuery(const char *resource, HttpRequestMethod method, Dictionary<DictionaryHelper::StringEntity> *getData, Dictionary<DictionaryHelper::StringEntity> *postData, Dictionary<DictionaryHelper::StringEntity> *headerData)
{
  if(resource != NULL) //We overwrite the resource if it has been already defined
  {
    if(_resource != NULL)
      free(_resource);

    _resource = (char *) malloc(strlen(resource) * sizeof(char) + 1);
    strcpy(_resource, resource);
  }
  
  //We check the result
 return sendHttpQuery(method, getData, postData, headerData);
}

HttpClient::HttpQueryStatus HttpClient::sendHttpQuery(HttpRequestMethod method, Dictionary<DictionaryHelper::StringEntity> *getData, Dictionary<DictionaryHelper::StringEntity> *postData, Dictionary<DictionaryHelper::StringEntity> *headerData)
{
  //We reset this two flags
  _httpCode = HTTP_CODE::UNDEFINED_CODE;
  _httpCodeParsed = false;

  #ifdef DEBUG_HTTP_CLIENT
  if(_keepAlive)
    Serial.printf("Link status : %u\n", status());
  #endif

  //If we did not want to keep the connection alive and it is still open, then we first close it.
  if(!_keepAlive && connected())stop();

  if(!connected() || _connectionStatus == FAILED)
  {
    if(_retries == _maxRetries) return HttpQueryStatus::ERR_CONN_MAX_RETRY;
    
    if(_connectionStatus == FAILED)
    {
      stop();
      if(_maxRetries != -1)_retries++;
    }
    
    #ifdef DEBUG_HTTP_CLIENT
    if(_keepAlive)
      Serial.printf("Link broken, we try to reconnect : addr : %s port %u\nretries : %u\n", _pAddress ? _pAddress : "NULL", _port, _retries);
    else
      Serial.printf("We start a new connection : %s port %u\nretries : %u\n", _pAddress ? _pAddress : "NULL", _port, _retries);
    #endif
    
    if(!connectByHostOrIp())
    {
       #ifdef DEBUG_HTTP_CLIENT
       Serial.printf("Failed to reconnect\n");
       #endif
       stop();
       return HttpQueryStatus::ERR_CONN;
    }
    else
      _retries = 0;
  }

  if(connected())
  {
    #ifdef DEBUG_HTTP_CLIENT
    Serial.printf("Server is listening, socket status : %u\n", status());
    #endif
    
    switch(method)
    {
      case HttpRequestMethod::GET:
        //No Content-Length in this case
        printf("GET %s", _resource == NULL ? "/" : _resource);
        //Here we send the parameters
        sendUriWithGetParams(getData);
        sendHeader(HttpMIMEType::UNKNOWN_MIME, 0, headerData);
        flush(); //We force the send of the request to prevent any timeout on reception !
        break;
      case HttpRequestMethod::POST:
        //It is necessary to compute the content length
        printf("POST %s", _resource == NULL ? "/" : _resource);
        //Here we send the parameters
        sendUriWithGetParams(getData);
        sendHeader(HttpMIMEType::APPLICATION_X_WWW_FORM_URLENCODED, computeBodyLength(postData), headerData);
        sendPostData(postData);
        flush(); //We force the send of the request to prevent any timeout on reception !
        break;
      default:
        #ifdef DEBUG_HTTP_CLIENT
        Serial.printf("Http verb unspecified\n");
        #endif
        if(!_keepAlive)stop();
        return HttpQueryStatus::UNKNOWN_HTTP_METHOD;
        break;
    }
  }
  
  return HttpQueryStatus::SUCCESS;
}

HttpClient::HTTP_CODE HttpClient::isReplyAvailable(uint16_t timeout)
{
  uint32_t ts(millis());
  char buffer[100];
  char *bodyDelimiter(NULL);
  char *newLineDelimiter(NULL);

  if(!_httpCodeParsed)_httpCodeParsed = true;
  else
  {
    return _httpCode;
  }
  #ifdef DEBUG_HTTP_CLIENT
  Serial.println("Before timeout");
  #endif

  //This is the loop where we parse the data
  while((available() || !bodyDelimiter) && millis() - ts < timeout)
  {
    int bytesCount(available());
    if(bytesCount)
    {
      //If we do not have the HTTP response code yet, we parse it
      if(_httpCode == HTTP_CODE::UNDEFINED_CODE)
      {
        uint16_t safeSize = bytesCount > 99 ? 99 : bytesCount;
        buffer[peekBytes((uint8_t*)buffer,safeSize)] = '\0';
        //We look for the end of the first line ie : HTTP/1.1 200 OK\r\n
        char *pNewLine = strstr(buffer, "\r\n");
        //If we found the new line, we can retrieve the code
        if(pNewLine)
        {
          #ifdef DEBUG_HTTP_CLIENT
          Serial.println("New line found");
          #endif
          //We extract the code
          char *code = strchr((char *)buffer, ' '), *endP;
          if(code)
          {
            endP = strchr(code + 1, ' ');
            if(endP != NULL)
            {
              *endP = '\0';
      
              #ifdef DEBUG_HTTP_CLIENT
              Serial.printf("Http code : %s\n", code + 1);
              #endif
              
      
              _httpCode = (HTTP_CODE)strtoul(code+1, NULL, 10);
              //We can now discard the first line
              #ifdef DEBUG_HTTP_CLIENT
              Serial.printf("First line length : %u\n",(pNewLine - buffer) + 2);
              #endif
              read((uint8_t *)buffer, (pNewLine - buffer) + 2);
              #ifdef DEBUG_HTTP_CLIENT
              bytesCount = available();
              safeSize = bytesCount > 99 ? 99 : bytesCount;
              buffer[peekBytes((uint8_t*)buffer,safeSize)] = '\0';
              Serial.printf("Next chunk is (size %u) : %s\n",safeSize,buffer);
              #endif
            }
          }
          else
          {
            #ifdef DEBUG_HTTP_CLIENT
            Serial.println("Code delimiter NOT found, leaving");
            #endif
            break;
          }
        }
        else if(millis() - ts >= timeout)//Time out is over, received data is probably junk.
        {
          //we leave the loop;
          #ifdef DEBUG_HTTP_CLIENT
          Serial.println("New line NOT found, leaving");
          #endif
          break;
        }
      }
      else//We found the HTTP code, now we discard all the header data
      {
        int safeSize = bytesCount > 99 ? 99 : bytesCount;
        buffer[peekBytes((uint8_t*)buffer,safeSize)] = '\0';
        #ifdef DEBUG_HTTP_CLIENT
        Serial.printf("Peeked for (size %u) : %s\n",safeSize,buffer);
        #endif
          
        bodyDelimiter = strstr(buffer, "\r\n\r\n");
        newLineDelimiter = strstr(buffer, "\r\n");
        if(!bodyDelimiter)
        {
          if(newLineDelimiter)
          {
            read((uint8_t *)buffer, (newLineDelimiter - buffer) + 2);
          }
          else
          {
            read((uint8_t *)buffer, strlen(buffer));
          }
        }
        else
        {
          #ifdef DEBUG_HTTP_CLIENT
          Serial.println("We found the body delimiter");
          #endif
          read((uint8_t *)buffer, (bodyDelimiter - buffer) + 4);
          #ifdef DEBUG_HTTP_CLIENT
          safeSize = available() > 99 ? 99 : available();
          buffer[peekBytes((uint8_t*)buffer,safeSize)] = '\0';
          Serial.printf("Body chunk is : %s\n", buffer);
          #endif
          break;
        }
      }
      ts = millis();
    }
    yield();
  }

  #ifdef DEBUG_HTTP_CLIENT
  Serial.printf("\nAfter timeout or all data is received, HTTP_CODE is : %d\n", _httpCode);
  #endif
  
  return _httpCode;
}

uint16_t HttpClient::readHttpBody(uint8_t *buffer, uint32_t size)
{
  uint32_t bytesAvailable(available());
  uint8_t safeSize(0);

  if(bytesAvailable)
  {
    safeSize = bytesAvailable > size - 1 ? size - 1 : bytesAvailable;

    read(buffer, safeSize);
  }

  buffer[safeSize] = '\0';
  
  return safeSize;
}

void HttpClient::sendUriWithGetParams(Dictionary<DictionaryHelper::StringEntity> *data)
{
  if(data == NULL)return;

  uint8_t count(data->count());
  
  if(count == 0)return;
  print("?");
  
  for(int i(0); i < count; i++)
  {
    char str[2] = "";        
    if(data->getAt(i) != NULL)
    {
      if(strlen(data->getAt(i)->getString()) > 0)
        strcpy(str, "=");
    } 
    printf("%s%s%s", data->getParameter(i), str, data->getAt(i) != NULL ? data->getAt(i)->getString() : "");
          
    if(i < count - 1)
      print("&");
  }
}

void HttpClient::sendPostData(Dictionary<DictionaryHelper::StringEntity> *data)
{
  if(data == NULL)return;

  uint8_t count(data->count());
  
  if(count == 0)return;

  for(int i(0); i < count; i++)
  {
    printf("%s=%s", data->getParameter(i), data->getAt(i) != NULL ? data->getAt(i)->getString() : "");
          
    if(i < count - 1)
      print("&");
  }
}

void HttpClient::keepAlive(boolean enabled)
{
  _keepAlive = enabled;
}

void HttpClient::sendHeader(HttpMIMEType contentType, uint64_t contentLength, Dictionary<DictionaryHelper::StringEntity> *headerData, HttpVersion httpVersion)
{
  //Host could be an IP address or a host name
  if(_isIp)
    printf(" %s\r\nHost: %u.%u.%u.%u:%u\r\nConnection: %s\r\n", httpVersionToString(httpVersion), remoteIP()[0], remoteIP()[1], remoteIP()[2], remoteIP()[3], remotePort(), _keepAlive ? "keep-alive" : "close");
  else
    printf(" %s\r\nHost: %s\r\nConnection: %s\r\n", httpVersionToString(httpVersion), _pAddress, _keepAlive ? "keep-alive" : "close");
    
  if(contentLength > 0)
  {
    printf("Content-Length: %llu\r\n", contentLength);
    printf("Content-Type: %s\r\n", httpMIMETypeToString(contentType));
  }

  //We send the headerData if not NULL
  if(headerData != NULL)
  {
    uint8_t count(headerData->count());
    if(count > 0)
    {
      for(uint8_t i(0); i < count; i++)
      {
        printf("%s: %s\r\n", headerData->getParameter(i), headerData->getAt(i) != NULL ? headerData->getAt(i)->getString() : "");
      }
    }
  }
  
  printf("\r\n");
}

uint64_t HttpClient::computeBodyLength(Dictionary<DictionaryHelper::StringEntity> *data)
{
  uint64_t length(0);

  if(data == NULL)return length;

  uint8_t count(data->count());
  
  if(count == 0)return length;

  for(int i(0); i < count; i++)
  {
    length += data->getAt(i) != NULL ? strlen(data->getAt(i)->getString()) + 1 : 1;
    length += strlen(data->getParameter(i));
      
    if(i < count - 1)
      length++;
  }
  
  return length;
}

void HttpClient::setMaxRetries(int16_t retries)
{
  _maxRetries = retries < 0 ? -1 : retries;
}

int16_t HttpClient::getMaxRetries(void) const
{
  return _maxRetries;
}

uint16_t HttpClient::retriesCount(void) const
{
  return _retries;
}

void HttpClient::resetRetriesCount(void)
{
  _retries = 0;
}
