#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <SD.h>
#include "TCPServer.h"
#include "Dictionary.h"
#include "HttpConstants.h"
#include "utilities.h"
//#define DEBUG_WEBS
#define READ_WRITE_BUFFER_SIZE 5000

template <typename T>
class WEBServer : public TCPServer<T>, public HttpConstants
{
	public:
		enum HttpParserStatus 
    {
      PARSE_HTTP_VERB,
      PARSE_HTTP_RESOURCE,
      PARSE_HTTP_VERSION,
      PARSE_HTTP_RESOURCE_QUERY,
      PARSE_HTTP_POST_DATA, 
      PARSE_HTTP_HEADER_PARAMS,
      PARSE_HTTP_COOKIES
    };
    enum WEBClientState {ACCEPTED, PARSING, QUERY_PARSED, RESPONSE_SENT, DONE};

    struct HttpCookie
    {
      DictionaryHelper::StringEntity value;
    };
    
    struct HttpRequestData
    {
      HttpRequestMethod HRM;
      HttpVersion HV;
      HttpMIMEType HMT;
      size_t contentLength;
      
      Dictionary<HttpCookie> cookies;
      Dictionary<DictionaryHelper::StringEntity> getParams;
      Dictionary<DictionaryHelper::StringEntity> postParams;
      char *postParamsDataPointer; //Used in the postParams algorithm
      
      char *httpResource;
      uint16_t maxResourceBuffer;
      char *httpBody;
      uint16_t maxBodyBuffer;
    };
    
    WEBServer(uint16_t port = 80, SDClass *sdClass = NULL, uint8_t maxClient = MAX_CLIENT, uint16_t clientDataBufferSize = 256) : TCPServer<T>(port, maxClient, clientDataBufferSize), _sdClass(sdClass) {}

    virtual ~WEBServer()
    {
      free(_WWWDir);
    }

    using ApiRoutineCallback = boolean (*)(WEBServer<T>*, HttpRequestData&, WiFiClient*, void*);

    boolean addApiRoutine(const char *uri, ApiRoutineCallback apiRoutine, void *pData, HttpRequestMethod HRM = UNDEFINED)
    {
      return _apiDictionary.add(uri, new ApiRoutine({apiRoutine, pData, HRM}));
    }

    void clearApiRoutine(void) { _apiDictionary.clear(); };
    
    boolean removeApiRoutine(const char *uri)
    {
      return _apiDictionary.remove(uri);
    }

    unsigned int apiRoutineCount(void) const
    {
      return _apiDictionary.count();
    }

    boolean addHttpHeader(const char *name, const char *value)
    {
      return _httpHeadersDictionary.add(name, DictionaryHelper::StringEntity(value));
    }

    void clearHttpHeaders(void)
    {
      _httpHeadersDictionary.clear();
    }

    boolean removeHttpHeader(const char *name)
    {
      return _httpHeadersDictionary.remove(name);
    }

    unsigned int httpHeadersCount(void) const
    {
      return _httpHeadersDictionary.count();
    }

    /**
     * The addCookie method adds cookies to the cookie dictionary which will be used when calling the sendHTTPResponse method.
     * Once the cookies are sent, the dictionary will be emptied.
     **/
    boolean addCookies(const char *cookieName, const char *cookieValue = nullptr, int32_t maxAge = -1, const char *cookiePath = nullptr, const char *cookieDomain = nullptr, boolean httpOnly = false, boolean sameSite = false)
    {
      return _setCookieDictionary.add(cookieName, new ServerHttpCookie({cookieValue, cookieDomain, cookiePath, sameSite, httpOnly, maxAge}));
    }

    void clearCookies(void)
    {
      _setCookieDictionary.clear();
    }

    boolean removeCookie(const char *cookieName)
    {
      return _setCookieDictionary.remove(cookieName);
    }

    void sendHTTPResponse(WiFiClient *client, const char *contentType, const size_t contentLength = 0, HttpVersion version = HttpVersion::HTTP_1_1, HTTP_CODE HTTPCode = HTTP_CODE::HTTP_CODE_OK)
    {
      if(!client) return;
      (void)HTTPCode;

      client->printf("%s 200 OK\r\nContent-Type: %s", httpVersionToString(version), contentType);

      if(contentLength)
      {
        client->printf("\r\nContent-Length: %d", contentLength);
      }

      //We here send user defined HTTP headers :)
      for(unsigned int i(0); i < _httpHeadersDictionary.count(); i++)
      {
        client->printf("\r\n%s: %s", _httpHeadersDictionary.getParameter(i), _httpHeadersDictionary.getAt(i) ? _httpHeadersDictionary.getAt(i)->getString() : "");
      }

      //We here send the user defined cookies :)
      for(unsigned int i(0); i < _setCookieDictionary.count(); i++)
      {
        ServerHttpCookie *pCookie = _setCookieDictionary.getAt(i);

        if(pCookie)
        {
          client->printf_P(PSTR("\r\nSet-Cookie: %s=%s"), _setCookieDictionary.getParameter(i), pCookie->value.getString());
          if(pCookie->maxAge != -1)
            client->printf_P(PSTR("; Max-Age=%d"), pCookie->maxAge);
          if(pCookie->sameSite)
            client->printf_P(PSTR("; SameSite=Strict"));
          if(pCookie->domain.getString()[0] != '\0')
            client->printf_P(PSTR("; Domain=%s"), pCookie->domain.getString());
          if(pCookie->path.getString()[0] != '\0')
            client->printf_P(PSTR("; Path=%s"), pCookie->path.getString());
          if(pCookie->httpOnly)
            client->printf_P(PSTR("; HttpOnly"));
        }
       
      }

      //We do not forget to clear the cookie dictionary after
      clearCookies();
        
      client->print("\r\n\r\n");
    }

    void setWWWDir(const char *WWWDir)
    {
      if(WWWDir)
      {
        free(_WWWDir);
        _WWWDir = (char *)malloc((strlen(WWWDir) * sizeof(char)) + 1);
        strcpy(_WWWDir, WWWDir);
      }
      else
      {
        free(_WWWDir);
        _WWWDir = nullptr;
      }
    }

    const char * getWWWDir(void) const
    {
      return _WWWDir;
    }

    void setSDClass(SDClass *sdClass)
    {
      _sdClass = sdClass;
    }

	protected:
	private:
    virtual T* createNewClient(WiFiClient wc)
    {
      return new T(wc, TCPServer<T>::freeClientId(), TCPServer<T>::_clientDataBufferSize);
    }

    /**
     * The fillDataBuffer method from the TCPServer base class was overloaded in order to read from socket
     * until the \r\n\r\n body delimiter.
     * The remaining data should NOT BE READ if the MIME TYPE is not APPLICATION_X_WWW_FORM_URLENCODED
     * so that we can consume the full request body later.
     */
    #if 0
    virtual void fillDataBuffer(T *client)
    {
      uint16_t freeSpace = (client->_dataBufferSize-1/*for \0*/ - client->_dataSize);
      uint32_t bytesAvailable(client->_client.available());

      void *bodyDelimiter(memmem((client->_client).peekBuffer(), (client->_client).peekAvailable(), "\r\n\r\n", 4));

      if(bodyDelimiter && client->_httpRequestData.HMT != HttpMIMEType::APPLICATION_X_WWW_FORM_URLENCODED)
      {
        bytesAvailable = (const char *)bodyDelimiter - (client->_client).peekBuffer();
      }

      if(freeSpace > 0)
      {
        int amountToBeRead = bytesAvailable < freeSpace ? bytesAvailable : freeSpace, read(0);

        read = (client->_client).read(client->_data + client->_dataSize, amountToBeRead);
        client->_dataSize += read;
        client->_data[client->_dataSize] = '\0';
        client->_newDataAvailable = true;
      }
    }
    #endif
    
    virtual void greetClient(T *client)
    {
      (void)client;
    }
    
		virtual void processClientData(T *client)
    {
      if(client->_dataSize > 0)
      {
        switch(client->_WEBClientState)
        {
          case ACCEPTED:
            #ifdef DEBUG_WEBS
            Serial.println("WEBServer : ACCEPTED");
            #endif
            client->_WEBClientState = WEBClientState::PARSING;
            break;
          case PARSING:
            queryParser(client);
            break;
          case QUERY_PARSED:
            #ifdef DEBUG_WEBS
            Serial.println("WEBServer : QUERY_PARSED");
            #endif
            sendDataToClient(client);
            break;
          case RESPONSE_SENT:
            #ifdef DEBUG_WEBS
            Serial.println("WEBServer : RESPONSE_SENT");
            #endif
            client->_WEBClientState = WEBClientState::DONE;
            break;
          case DONE:
            #ifdef DEBUG_WEBS
            Serial.println("WEBServer : DONE");
            #endif
            client->_clientState = TCPClient::ClientState::DISCARDED;
            break;
        }
      }
    }

    void queryParser(T *client)
    {
        switch(client->_httpParserState)
        {
          case HttpParserStatus::PARSE_HTTP_VERB:
          {
            #ifdef DEBUG_WEBS
            Serial.println((char *)client->_data);
            #endif
            
            char *pVerb(strchr((char *)client->_data, ' '));
            
            if(pVerb != NULL)
            {
              *pVerb = '\0';
              client->_httpRequestData.HRM = getHttpVerbEnumValue((char *)client->_data);
              client->freeDataBuffer((pVerb - (char *)client->_data) +1);

              if(client->_httpRequestData.HRM == HttpRequestMethod::UNDEFINED) //Error 400
              {
                sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
                client->_clientState = TCPClient::ClientState::DISCARDED;
                break;
              }
              
              #ifdef DEBUG_WEBS
              Serial.print("Verb : ");Serial.println(client->_httpRequestData.HRM);
              Serial.println((char *)client->_data);
              #endif
              
              client->_httpParserState = HttpParserStatus::PARSE_HTTP_RESOURCE;
            }
            else
            {
              sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
              client->_clientState = TCPClient::ClientState::DISCARDED;
            }
          } 
            break;
          case HttpParserStatus::PARSE_HTTP_RESOURCE:
          {
            char *pRsrc(strchr((char *)client->_data, ' ')), *pRsrcQuery(strchr((char *)client->_data, '?'));
            //!\ the ? should be present before ' ' if ' ' is found !!!!
            if(pRsrc && pRsrcQuery)
              if(pRsrcQuery > pRsrc)pRsrcQuery = nullptr;
            //The case where we have the resource complete or not complete with query parameters like : GET /some/path/resource.rsrc?param1=one&param2 HTTP/1.1
            if(pRsrc || pRsrcQuery)
            {
              uint16_t rawLengthOfResource(0);
              if(pRsrcQuery )
              {
                *pRsrcQuery = '\0'; // The ? is the end of the resource string
                rawLengthOfResource = pRsrcQuery - (char *)client->_data;
                #ifdef DEBUG_WEBS
                Serial.printf("Resource w/ query\n");
                #endif
              }
              else
              {
                *pRsrc = '\0';
                rawLengthOfResource = pRsrc - (char *)client->_data;
                #ifdef DEBUG_WEBS
                Serial.printf("Resource w/o query\nRaw length : %u\n",rawLengthOfResource);
                #endif
              }

              if(rawLengthOfResource >= client->_httpRequestData.maxResourceBuffer) //Then we cannot handle the full resource and there is no point of truncating it
              //better tell the client about it ...
              {
                #ifdef DEBUG_WEBS
                Serial.printf("Resource too long\nResource raw length is : %u (\\0 included)\nMax length is : %u (\\0 included)\nclient->_data : #%s#\n", rawLengthOfResource + 1, client->_httpRequestData.maxResourceBuffer, (char *)client->_data);
                #endif
                sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, PSTR("Resource too long"));
                client->_clientState = TCPClient::ClientState::DISCARDED;
                break;
              }

              client->_httpRequestData.httpResource = (char *) malloc(sizeof(char) * (rawLengthOfResource + 1)); // +1 for the \0
              if(client->_httpRequestData.httpResource != nullptr)
              {
                strncpy(client->_httpRequestData.httpResource, (char *)client->_data, rawLengthOfResource);
                client->_httpRequestData.httpResource[rawLengthOfResource] = '\0';
              }
              else //Error 500
              {
                sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for resources");
                client->_clientState = TCPClient::ClientState::DISCARDED;
                break;
              }

              client->freeDataBuffer(rawLengthOfResource + 1); //+1 to go past the \0, only the query parameters left in the buffer '?' excluded

              #ifdef DEBUG_WEBS
              Serial.printf("Resource length : %u\nRsrc : %s\nclient->_data : #%s#\n",
              rawLengthOfResource,
              client->_httpRequestData.httpResource,
              (char *)client->_data);
              #endif

              if(pRsrcQuery)
                client->_httpParserState = HttpParserStatus::PARSE_HTTP_RESOURCE_QUERY;
              else
                client->_httpParserState = HttpParserStatus::PARSE_HTTP_VERSION;
            }
            else //The URL is too long to fit in the buffer, we dont know it's length nor we now if it has query parameters.
                 //TODO : Maybe the query is incomplete and the client will send more data, case to handle.
            {
              #ifdef DEBUG_WEBS
              Serial.printf("Could not find ' ' or '?' delimiter\nclient->_data : #%s#\n",
              (char *)client->_data);
              #endif
              sendInfoResponse(HTTP_CODE::HTTP_CODE_URI_TOO_LONG, client, PSTR("Resource too long"));
              client->_clientState = TCPClient::ClientState::DISCARDED;
              break;
            }
          }
            break;
          case HttpParserStatus::PARSE_HTTP_VERSION:
          {
            char *pEndline = strstr((char *)client->_data, "\r\n");
            
            if(pEndline == NULL) pEndline = strchr((char *)client->_data, '\n');
            
            char *pVers = strstr((char *)client->_data, "HTTP/");
            
            if(pEndline != NULL && pVers!= NULL)
            {
              *pEndline = '\0';
              client->_httpRequestData.HV = getHttpVersionEnumValue(pVers+5);
              
              #ifdef DEBUG_WEBS
              Serial.print("Vers : ");Serial.println(pVers+5);
              Serial.print("Vers : ");Serial.println(client->_httpRequestData.HV);
              #endif
              
              client->freeDataBuffer((pEndline - (char *)client->_data)+2);

              #ifdef DEBUG_WEBS
              Serial.println((char *)client->_data);
              #endif
              
              client->_httpParserState = HttpParserStatus::PARSE_HTTP_HEADER_PARAMS;
            }
          }
            break;

          //index.htm?var1=1&var2=2...
          //----------^^^^^^^^^^^^^^
          case  HttpParserStatus::PARSE_HTTP_RESOURCE_QUERY:
            //If we are here, it means we are sure that there is at least one parameter
            if(!httpRsrcParamParser(client))
            {
              #ifdef DEBUG_WEBS
              Serial.println("Get params :");
              for(unsigned int i = 0; i < client->_httpRequestData.getParams.count(); i++)
              {
                Serial.printf("%s : %s\n", client->_httpRequestData.getParams.getParameter(i), client->_httpRequestData.getParams.getAt(i)->getString());
              }
              Serial.printf("client->_data : #%s#\n", client->_data);
              #endif
              client->_httpParserState = HttpParserStatus::PARSE_HTTP_VERSION;
            }
            break;
          //Here we parse the different header params until we arrive to \r\n\r\n
          //We also know that header params are of the form : Param1: value1\r\nParam2: value2\r\n etc
          //So we look for the delimitor which is ":"
          case HttpParserStatus::PARSE_HTTP_HEADER_PARAMS: 
          {
            char *pDelimiter(strchr((char *)client->_data, ':'));
            char *endHeaderDelimiter(strstr((char *)client->_data, "\r\n"));

            //If we found the delimiter, this means there is at least one parameter
            if(pDelimiter)
            {
              *pDelimiter = '\0';
              httpHeaderParamParser(client);
            }
            //There is maybe not headers so we go to the http body part
            else if(endHeaderDelimiter)
            {
              if(client->_httpRequestData.contentLength) //If a body is expected
                client->_httpParserState = HttpParserStatus::PARSE_HTTP_POST_DATA;
              else //Else we are done
                client->_WEBClientState = WEBClientState::QUERY_PARSED;

              client->freeDataBuffer((endHeaderDelimiter - (char *)client->_data) +1); //client->_data must not be empty so we keep the last \n...
              break;
            }
            else
            {
              #ifdef DEBUG_WEBS
              Serial.println("Error, should have found \\r\\n\\r\\n");
              #endif

              sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
              client->_clientState = TCPClient::ClientState::DISCARDED;
            }
          }
            break;
          case HttpParserStatus::PARSE_HTTP_COOKIES:
            if(!httpCookiesParser(client))
            {
              #ifdef DEBUG_WEBS
              Serial.println("Cookies :");
              for(unsigned int i = 0; i < client->_httpRequestData.cookies.count(); i++)
              {
                Serial.printf("#%s# : #%s#\n", client->_httpRequestData.cookies.getParameter(i), client->_httpRequestData.cookies.getAt(i)->value.getString());
              }
              Serial.printf("phc client->_data : #%s#\n", client->_data);
              #endif
              //Once we are done parsing the cookies, we go back to the HTTP HEADER PARAMS PARSER as there may be still some more after the cookies ?
              client->_httpParserState = HttpParserStatus::PARSE_HTTP_HEADER_PARAMS;
            }
            break;
          case  HttpParserStatus::PARSE_HTTP_POST_DATA:

              switch(client->_httpRequestData.HMT)
              {
                case APPLICATION_X_WWW_FORM_URLENCODED:
                  #ifdef DEBUG_WEBS
                  Serial.printf("Post data : APPLICATION_X_WWW_FORM_URLENCODED\nPost data : #%s#\n", client->_data);
                  #endif
                  
                  //we parse it !
                  if(!httpPostParamParser(client))
                  {
                    //Parsing done!
                    #ifdef DEBUG_WEBS
                    Serial.println("Post params :");
                    for(unsigned int i = 0; i < client->_httpRequestData.postParams.count(); i++)
                    {
                      Serial.printf("#%s# : #%s#\n", client->_httpRequestData.postParams.getParameter(i), client->_httpRequestData.postParams.getAt(i)->getString());
                    }
                    #endif
                    client->_WEBClientState = WEBClientState::QUERY_PARSED;
                  }
                  
                  break;
                default :
                  client->_WEBClientState = WEBClientState::QUERY_PARSED;
              }
              
            break;
          default :
            sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "WEB server error");
            client->_clientState = TCPClient::ClientState::DISCARDED;
            break;
        }
    }

    /*
     * This function parses the header parameters in order to find particular parameters.
     * For example we look for the "Content-Type" header or for the "Range: bytes=" header
     */
    void httpHeaderParamParser(T *client)
    {
      char *pHeaderParam((char *)client->_data);
      char *pHeaderValue((char *)client->_data + strlen((char *)client->_data) + 2); //+2 is to discard the ": "
      char *pHeaderEndOfValue(strstr((char *)pHeaderValue, "\r\n"));

      if(pHeaderEndOfValue)
      {
        *pHeaderEndOfValue = '\0';

        #ifdef DEBUG_WEBS
        Serial.printf("Header param->value : #%s# -> #%s#\n", pHeaderParam, pHeaderValue);
        #endif

        char *searchParam(strstr(pHeaderParam, "t-Type"));
        char *searchValue(strstr(pHeaderValue, "ion/x-www-for"));

        if(searchParam && searchValue)
        {
          client->_httpRequestData.HMT = APPLICATION_X_WWW_FORM_URLENCODED;
          client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
          #ifdef DEBUG_WEBS
          Serial.println("Content-Type is APPLICATION_X_WWW_FORM_URLENCODED");
          #endif
          return;
        }

        if((searchParam = strstr(pHeaderParam, "ion")) && (searchValue = strstr(pHeaderValue, "keep-al")))
        {
          client->_keepAlive = true;
          client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
          #ifdef DEBUG_WEBS
          Serial.println("Connection: keep-alive");
          #endif
          return;
        }

        if((searchParam = strstr(pHeaderParam, "ent-Len")))
        {
          char *check(nullptr);
          client->_httpRequestData.contentLength = strtoul(pHeaderValue, &check, 10);

          if(*check != '\0') //Failed to parse the content length !
          {
            client->_httpRequestData.contentLength = 0;
            #ifdef DEBUG_WEBS
            Serial.println("Failed to parse Content-Length");
            #endif
          }
          #ifdef DEBUG_WEBS
          else
          {
            Serial.printf("Content-Length: %u\n",  client->_httpRequestData.contentLength);
          }
          #endif

          client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
          return;
        }

        //Range part for file downloads and media playback
        if((searchParam = strstr(pHeaderParam, "nge")) && (searchValue = strstr(pHeaderValue, "ytes=")))
        {
          //We need to parse the range values
          if(fillRangeByteStruct(client, pHeaderValue))
          {
            #ifdef DEBUG_WEBS
            Serial.printf("Range (bytes) data : start -> %u ; end -> %u\n", client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
            #endif
          }
          #ifdef DEBUG_WEBS
          else
          {
            Serial.printf("Range (bytes) data parse error : start -> %u ; end -> %u\n", client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
          }
          #endif

          client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
          return;
        }

        //Here we check if there are some cookies to be parsed
        if((searchParam = strstr(pHeaderParam, "okie")))
        {
          client->_httpParserState = HttpParserStatus::PARSE_HTTP_COOKIES;
          client->freeDataBuffer(pHeaderValue - (char *)client->_data);
          return;
        }

        //We still need to remove
        client->freeDataBuffer((pHeaderEndOfValue - (char *)client->_data) + 2);
      }
      //Error, we did not find the \r\n, maybe the parameter value is too long, error needs to be handled !
      else
      {
        #ifdef DEBUG_WEBS
        Serial.println("Error : header value \\r\\n not found!");
        #endif

        sendInfoResponse(HTTP_CODE::HTTP_CODE_BAD_REQUEST, client, PSTR("The server could not understand the request due to invalid syntax"));
        client->_clientState = TCPClient::ClientState::DISCARDED;
      }
    }

    /*
     * This function fills the client's _rangeData struct with proper values
     */
    bool fillRangeByteStruct(T *client, char *rangeBytesData)
    {
      if(!rangeBytesData)return false;

      char *rangeStart = strchr(rangeBytesData, '='), *delimiter = strchr(rangeBytesData, '-'), *check(nullptr);

      if(!rangeStart)return false;

      rangeStart++; //We move one char forward

      //We parse the 1st part of the range byte
      if(!delimiter) // If only one part (ill-formed)
      {
        client->_rangeData._rangeStart = strtoull(rangeStart, &check, 10);
        if(*check != '\0')return false;
        return true;
      }
      else
      {
        *delimiter = '\0';
        client->_rangeData._rangeStart = strtoull(rangeStart, &check, 10);
        if(*check != '\0')return false;
      }

      rangeStart = delimiter+1;

      //We parse the 2nd part of the range byte
      client->_rangeData._rangeEnd = strtoull(rangeStart, &check, 10);
      if(*check != '\0')return false;

      client->_rangeData._rangeRequest = true;
      
      return true;
    }

    /*
     * This function parses resources query parameters
     */
    boolean httpRsrcParamParser(T *client)
    {
      char *end(strchr((char *)client->_data, ' '));

      //If we find the end we mark it, this is needed for subsequent strchr
      if(end)*end = '\0';

      char *key(strchr((char *)client->_data, '=')), *value(strchr((char *)client->_data, '&'));

      if(key == nullptr && value == nullptr) //Only the key is present
      {
        client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(NULL));
        client->freeDataBuffer(strlen((char *)client->_data) + 1);
        #ifdef DEBUG_WEBS
        Serial.printf("client->_data : #%s#\n", client->_data);
        #endif
        return false;
      }
      else if(key != nullptr && value != nullptr)
      {
        if(key < value)*key = '\0';
        *value = '\0';
        
        client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(key > value ? NULL : key + 1));
        client->freeDataBuffer((value - (char *)client->_data) + 1);
        
        #ifdef DEBUG_WEBS
        Serial.printf("client->_data : #%s#\n", client->_data);
        #endif
      }
      else if(key != nullptr && value == nullptr) //Only one key/value pair present
      {
        *key = '\0';
        
        client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(key+1));
        client->freeDataBuffer((key - (char *)client->_data) + strlen(key+1) + 2);
        #ifdef DEBUG_WEBS
        Serial.printf("client->_data : #%s#\n", client->_data);
        #endif
        return false;
      }
      else if(key == nullptr && value != nullptr)
      {
        *value = '\0';

        client->_httpRequestData.getParams.add((char *)client->_data, new DictionaryHelper::StringEntity(NULL));
        client->freeDataBuffer((value - (char *)client->_data) + 1);
        #ifdef DEBUG_WEBS
        Serial.printf("client->_data : #%s#\n", client->_data);
        #endif
      }
      return true;
    }

    boolean httpPostParamParser(T* client)
    {
      if(client->_httpRequestData.postParamsDataPointer == NULL)
      {
          if(strlen((char *)client->_data + 1) != client->_httpRequestData.contentLength)
            return true;
          client->_httpRequestData.postParamsDataPointer = (char *)client->_data + 1;//We save the starting position of the string to parse and we ignore the \n
      }
      
      char *key = strchr(client->_httpRequestData.postParamsDataPointer, '=');
      char *value = strchr(client->_httpRequestData.postParamsDataPointer, '&');

      if(key == NULL && value == NULL) //Nothing to parse or done
      {
        return false;
      }
      else if(key != NULL && value == NULL) //Only one key is present
      {
        *key = '\0';
        client->_httpRequestData.postParams.add(client->_httpRequestData.postParamsDataPointer, new DictionaryHelper::StringEntity(key+1));
        return false;
      }
      else if(key != NULL && value != NULL)
      {
        *key = '\0';
        *value = '\0';
        client->_httpRequestData.postParams.add(client->_httpRequestData.postParamsDataPointer, new DictionaryHelper::StringEntity(key+1));
        memmove(client->_httpRequestData.postParamsDataPointer, value +1, strlen(value+1) + 1);
      }
      else if(key == NULL && value != NULL)//Should never happen
        return false;
        
      return true;
    }

    boolean httpCookiesParser(T *client)
    {
      //He we have the the Cookie field value like : TestCookie=TestValue; TestCookie2=; TestCookie3=TestValue3
      //It is what we need to parse.
      char *endOfKey(strchr((char *)client->_data, '='));
      char *endOfValue(strchr((char *)client->_data, ';'));
      char *failSafe(strstr((char *)client->_data, "\r\n"));
      boolean notFinished(true);

      //In the case where we have cookies followed by post data, there was an issue were we mistaken the = from the cookie separator with the = of the post data separator.
      //Here just in case of the last cookie value finishing with a ';' 
      if(failSafe)
        return false;

      //There is at least one key/value pair
      if(endOfKey)
      {
        *endOfKey = '\0';
        endOfKey++;
      
        if(endOfValue)
        {
          *endOfValue = '\0';
        }
        else
        {
          endOfValue = endOfKey + strlen(endOfKey) + 1;
          notFinished = false;
        }
        #ifdef DEBUG_WEBS
        Serial.printf("Key -> Value : #%s# Value : #%s#\n", ((char)*client->_data == ' ') ? (char*)client->_data + 1 : (char*)client->_data , endOfKey);
        #endif
        client->_httpRequestData.cookies.add(((char)*client->_data == ' ') ? (char*)client->_data + 1 : (char*)client->_data, new HttpCookie({endOfKey}));
        //We dont forget to free the parsed data
        client->freeDataBuffer((endOfValue + 1 - (char *)client->_data));
        return notFinished;
      }
      return false;
    }

    void sendDataToClient(T *client)
    {
      if(!sendPageToClientFromApiDictio(client)) //Then we check if it is not a file that is requested 
      {
        if(!sendPageToClientFromSdCard(client)) //If this function returns false, we close the connection with the client. An error occured (An error message has already been sent) or the whole file has been sent.
        {
          client->_WEBClientState = WEBClientState::RESPONSE_SENT;
        }
      }
      else //If we found the api endpoint, we can close the connection with the client after the data has been sent.
        client->_WEBClientState = WEBClientState::RESPONSE_SENT;
    }

    boolean sendPageToClientFromApiDictio(T *client)
    {
      if(_apiDictionary.count() == 0 || client->_httpRequestData.httpResource == NULL)
        return false;
        
      ApiRoutine *ref = _apiDictionary(client->_httpRequestData.httpResource);
      
      if(ref == NULL)
        return false;

      // We consume the body's \r\n\r\n
      uint8_t discard[4];
      client->_client.read(discard, 4);
    
      if(ref->HRM == UNDEFINED)
      {
        return (*(ref->apiRoutine))(this, client->_httpRequestData, &(client->_client), ref->pData);
      }
      else if(ref->HRM == client->_httpRequestData.HRM)
      {
        return (*(ref->apiRoutine))(this, client->_httpRequestData, &(client->_client), ref->pData);
      }
      else
        return false;
    }

    boolean sendPageToClientFromSdCard(T *client)
    {
      if(_sdClass != NULL)
      {
        File pageToSend;
        char *filePath(NULL), *header(NULL);
        size_t readBytesFromFS(0), sentBytesFromSocket(0);
        //We check what kind of http verb it is
        switch(client->_httpRequestData.HRM)
        {
          case GET:
            filePath = getFilePathByHttpResource(_WWWDir, client->_httpRequestData.httpResource);
            if(filePath == NULL)
            {
              sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the filePath");
              return false;
            }
            
            #ifdef DEBUG_WEBS
            Serial.printf("File path : #%s#\n",filePath);
            #endif
            
            pageToSend = _sdClass->open(filePath);
            free(filePath);filePath = NULL;

            //If we couldn't open the file
            if(!pageToSend)
            {
              char *response(NULL);
              
              response = (char *) malloc(sizeof(char) * (36 + strlen(client->_httpRequestData.httpResource) + 1));
              if(response == NULL)
              {
                sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the response");
                return false;
              }
              
              sprintf(response, "Resource : %s not found on this server", client->_httpRequestData.httpResource);
              sendInfoResponse(HTTP_CODE::HTTP_CODE_NOT_FOUND, client, response);
              
              free(response);response = NULL;
              return false;
            }
    
            #ifdef DEBUG_WEBS
            Serial.print("FILE SIZE : ");
            Serial.println(pageToSend.size());
            Serial.print("FILE NAME : ");
            Serial.println(pageToSend.name());
            #endif

            if(pageToSend.isDirectory())
            {
              //If the ressource wasn't terminated by a '/' then we issue a 301 Moved Permanently, else all's good
              if(client->_httpRequestData.httpResource[strlen(client->_httpRequestData.httpResource)-1] != '/')
              {
                uint16_t locationLength(strlen(client->_httpRequestData.httpResource) + 7/*http://*/ + 16/*ip addr + :*/ + 5/*port*/ + 2);
                char *location = (char *)malloc(locationLength * sizeof(char));
                char *message = (char *)malloc((27 + locationLength + 1) * sizeof(char));
                if(!location || !message)
                {
                  sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the location or message");
                  pageToSend.close();
                  return false;
                }
                sprintf(location, "http://%s:%u%s/",client->_client.localIP().toString().c_str(), TCPServer<T>::getPort(), client->_httpRequestData.httpResource);
                sprintf(message, "The document has moved to %s.", location);
                sendInfoResponse(HTTP_CODE::HTTP_CODE_MOVED_PERMANENTLY, client, message, location);

                free(location);free(message);
              }
              else
                sendDirectoryListing(client, pageToSend); //Sends the content of the directory like what apache does by default.
              
              pageToSend.close();
              return false;
            }

            if(client->_fileSentBytes == 0)
            {
              size_t pageToSendSize(pageToSend.size());
              char *fileName = (char *) malloc(sizeof(char) * strlen(pageToSend.name()) + 1); 
              
              if(fileName == NULL)
              {
                sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the response");
                pageToSend.close();
                return false;
              }
              strcpy(fileName, pageToSend.name());

              if(client->_rangeData._rangeRequest)
              {
                client->_fileSentBytes = client->_rangeData._rangeStart;
                client->_rangeData._rangeEnd = !client->_rangeData._rangeEnd ? pageToSendSize - 1 : client->_rangeData._rangeEnd;
                pageToSend.seek(client->_fileSentBytes);
              }
              else
                client->_rangeData._rangeEnd = pageToSendSize - 1; //Needed to carry on sending when not a partial file

              header = getHTTPHeader(getMIMETypeByExtension(strlwr(getFileExtension(fileName))), pageToSendSize, client->_rangeData._rangeRequest, client->_rangeData._rangeStart, client->_rangeData._rangeEnd);
              
              #ifdef DEBUG_WEBS
              Serial.print("FILE EXTENSION : ");
              Serial.println(getFileExtension(fileName));
              #endif
              
              free(fileName);
              
              if(header == NULL)
              {
                sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Failed to allocate memory for the header");
                pageToSend.close();
                return false;
              }
              
              client->_client.print(header);

              free(header);header = NULL;
            }
            else
            {
              pageToSend.seek(client->_fileSentBytes);
            }
            
            if(pageToSend.available() && client->_fileSentBytes < client->_rangeData._rangeEnd + 1) // File is done sending once the whole file was sent or the partial part is done sending
            {
              size_t mallocAcceptedSize(0);
              /*
                The original issue was that a buffer of 2048 bytes was allocated on the stack causing a hard to track down stack overflow.
                Possible solutions I cam up with : 
                1) Create a statically allocated buffer which would live in the BSS (Block Started by a Symbol) RAM segment.
                  Advantage : buffer is allocated once and ready to be used immediately.
                  Drawback : takes space in RAM (lost space) even if the buffer isn't used.
                2) Create a dynamically allocated buffer using malloc and friends which would live in the heap RAM segment - SOLUTION I IMPLEMENTED
                  Advantage : buffer is taking RAM only when needed and can be freed afterwards.
                  Drawback : Allocating and deallocating heap memory a lot is costly in MCU time, leads to RAM fragmentation and could potentially fail...
              */
              uint8_t *sendBuffer = (uint8_t*)mallocWithFallback(READ_WRITE_BUFFER_SIZE * sizeof(uint8_t), &mallocAcceptedSize);

              if(!sendBuffer)
              {
                pageToSend.close();
                return false; //Not clean but what else can I do. Should never happen anyway.
              }
              
              readBytesFromFS = pageToSend.read(sendBuffer,mallocAcceptedSize);
              sentBytesFromSocket = client->_client.write(sendBuffer, readBytesFromFS);

              free(sendBuffer);

              #ifdef DEBUG_WEBS
              Serial.printf("Bytes read from FS : %u, Bytes sent : %u, got allocated buffer size : %u - free stack : %u\n", readBytesFromFS, sentBytesFromSocket, mallocAcceptedSize, ESP.getFreeContStack());
              #endif

              client->_fileSentBytes += sentBytesFromSocket; //We save the number of bytes sent so that we can reopen the file to this position later on.
            }
            else 
            {
              pageToSend.close();
              return false;
            }
            
            pageToSend.close();
            break;
          default: //If not supported
            sendInfoResponse(HTTP_CODE::HTTP_CODE_METHOD_NOT_ALLOWED, client, "The method used is not allowed");
            return false;
            break;
        }
      }
      else
      {
        sendInfoResponse(HTTP_CODE::HTTP_CODE_INTERNAL_SERVER_ERROR, client, "Api endpoint does not exist");
        return false;
      }
      
      return true;
    }

    void sendDirectoryListing(T *client, File& pageToSend)
    {
      sendHTTPResponse(&client->_client, HttpConstants::httpMIMETypeToString(HttpConstants::TEXT_HTML));
      client->_client.printf_P(PSTR( "<!DOCTYPE HTML>\r\n\
                                      <html>\r\n\
                                        <head>\r\n\
                                          <title>Index of %s</title>\r\n\
                                        </head>\r\n\
                                        <body>\r\n\
                                          <h1>Index of %s</h1>\r\n\
                                          <table>\r\n\
                                            <tr><th>Type</th><th>Name</th><th>Created</th><th>Last modified</th><th>Size</th></tr>\r\n\
                                            <tr><th colspan=\"5\"><hr></th></tr>\r\n")
                                            , client->_httpRequestData.httpResource, client->_httpRequestData.httpResource);
      
       if(strlen(client->_httpRequestData.httpResource) > 1) //Then we are not at the root of the WEBServer's directory.
       {
         char *rsrcCopy = strdup(client->_httpRequestData.httpResource); 
         if(rsrcCopy)
         {
            char *lastSlash(strrchr(rsrcCopy, '/'));
            if(lastSlash)*lastSlash = '\0';
            lastSlash = strrchr(rsrcCopy, '/');
            if(lastSlash)*lastSlash = '\0';

            client->_client.printf_P(PSTR("<tr><td>[DIR]</td><td><a href=\"%s/\">Parent Directory</a></td><td> - </td><td align=\"right\"> - </td><td> </td></tr>\r\n"), rsrcCopy);
            free(rsrcCopy);
         }
       }

      File nextFile;
      for(;;)
      {
        if(!(nextFile = pageToSend.openNextFile()))break;
        
        char zero_prepended[8][3] = {"","","","","","","",""};
        time_t rawCreationTime(nextFile.getCreationTime()), rawLastModifiedTime(nextFile.getLastWrite());
        tm creationTime(*localtime(&rawCreationTime)), lastModifiedTime(*localtime(&rawLastModifiedTime));

        client->_client.printf_P(PSTR("<tr><td>%s</td><td><a href=\"%s\">%s</a></td><td align=\"right\">%d-%s-%s %s:%s</td><td align=\"right\">%d-%s-%s %s:%s</td><td align=\"right\">%.1fK</td></tr>\r\n"),
        nextFile.isDirectory() ? "[DIR]":"[FILE]",
        nextFile.name(),
        nextFile.name(),
        creationTime.tm_year + 1900, dateTimeFormater(zero_prepended[0], creationTime.tm_mon + 1, '0'), dateTimeFormater(zero_prepended[1], creationTime.tm_mday, '0'), dateTimeFormater(zero_prepended[2], creationTime.tm_hour, '0'), dateTimeFormater(zero_prepended[3], creationTime.tm_min, '0'),
        lastModifiedTime.tm_year + 1900, dateTimeFormater(zero_prepended[4], lastModifiedTime.tm_mon + 1, '0'), dateTimeFormater(zero_prepended[5], lastModifiedTime.tm_mday, '0'), dateTimeFormater(zero_prepended[6], lastModifiedTime.tm_hour, '0'), dateTimeFormater(zero_prepended[7], lastModifiedTime.tm_min, '0'),
        nextFile.size() / 1024.0
        );

        #ifdef DEBUG_WEBS
        Serial.printf("File name : %s\nFile size : %u\nFree stack : %u\n", nextFile.name(), nextFile.size(), ESP.getFreeContStack());
        #endif
        nextFile.close();
        delay(5);
      }

      client->_client.printf_P(PSTR(       "<tr><th colspan=\"5\"><hr></th></tr>\r\n\
                                        </table>\r\n\
                                        <address>SAB WEBServer, Version %s at %s Port %u</address>\r\n\
                                      </body>\r\n\
                                    </html>"), "1.0.0", client->_client.localIP().toString().c_str(), TCPServer<T>::getPort());
    }
    
    /*Static helper methods*/

    static void sendInfoResponse(HTTP_CODE http_code, T *client, const char *message, const char *location = nullptr)
    {
      char codeLiteral[100];
      switch(http_code)
      {
        case HTTP_CODE_MOVED_PERMANENTLY:
          strcpy_P(codeLiteral,PSTR("Moved Permanently"));
          break;
        case HTTP_CODE_BAD_REQUEST:
          strcpy_P(codeLiteral,PSTR("Bad Request"));
          break;
        case HTTP_CODE_FORBIDDEN:
          strcpy_P(codeLiteral,PSTR("Forbidden"));
          break;
         case HTTP_CODE_NOT_FOUND:
          strcpy_P(codeLiteral,PSTR("Not Found"));
          break;
        case HTTP_CODE_METHOD_NOT_ALLOWED:
          strcpy_P(codeLiteral,PSTR("Method Not Allowed"));
          break;
        case HTTP_CODE_URI_TOO_LONG:
          strcpy_P(codeLiteral,PSTR("URI Too Long"));
          break;
        case HTTP_CODE_INTERNAL_SERVER_ERROR:
          strcpy_P(codeLiteral,PSTR("Internal Server Error"));
          break;
        default:
          strcpy_P(codeLiteral,PSTR("Error Not Defined"));
          break;
      }
      client->_client.printf_P(PSTR("HTTP/1.1 %d %s\r\n"), http_code, codeLiteral);


      if(http_code == HTTP_CODE_MOVED_PERMANENTLY)
        client->_client.printf_P(PSTR("Location: %s\r\n"), location);

      client->_client.printf_P(PSTR("Content-Type: text/html\r\nContent-Length: %d\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n<h1>Error %d</h1><p>%s</p>\r\n</html>"), strlen(message) + 56 + (http_code != 0 ? 3:1), http_code , message);
    }
    
		static HttpRequestMethod getHttpVerbEnumValue(const char *parseBuffer)
    {
      if(parseBuffer == NULL)return HttpRequestMethod::UNDEFINED;
      
      //UNDEFINED, GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
      if(strcmp(parseBuffer,"GET") == 0){return HttpRequestMethod::GET;}
      else if(strcmp(parseBuffer,"POST") == 0){return HttpRequestMethod::POST;}
      else if(strcmp(parseBuffer,"HEAD") == 0){return HttpRequestMethod::HEAD;}
      else if(strcmp(parseBuffer,"PUT") == 0){return HttpRequestMethod::PUT;}
      else if(strcmp(parseBuffer,"DELETE") == 0){return HttpRequestMethod::DELETE;}
      else if(strcmp(parseBuffer,"CONNECT") == 0){return HttpRequestMethod::CONNECT;}
      else if(strcmp(parseBuffer,"TRACE") == 0){return HttpRequestMethod::TRACE;}
      else if(strcmp(parseBuffer,"PATCH") == 0){return HttpRequestMethod::PATCH;}
      else if(strcmp(parseBuffer,"OPTIONS") == 0){return HttpRequestMethod::OPTIONS;}
      else
        return HttpRequestMethod::UNDEFINED;
    }

    static HttpMIMEType getMIMETypeByExtension(const char *extension)
    {
      if(extension == NULL)return UNKNOWN_MIME;
      
      //TEXT_PLAIN, TEXT_CSS, TEXT_HTML, TEXT_JAVASCRIPT
      if(strcmp(extension, "web") == 0) return TEXT_HTML;
      else if(strcmp(extension, "htm") == 0) return TEXT_HTML;
      else if(strcmp(extension, "css") == 0) return TEXT_CSS;
      else if(strcmp(extension, "js") == 0) return TEXT_JAVASCRIPT;
      else if(strcmp(extension, "png") == 0) return IMAGE_PNG;
      else if(strcmp(extension, "jpg") == 0) return IMAGE_JPEG;
      else if(strcmp(extension, "ico") == 0) return IMAGE_X_ICO;
      else if(strcmp(extension, "mp3") == 0) return AUDIO_MPEG;
      else if(strcmp(extension, "txt") == 0) return TEXT_PLAIN;
      else return UNKNOWN_MIME;
    }
    
    static char *getHTTPHeader(HttpMIMEType httpMIMEType, const size_t contentLength, bool acceptRanges = false, size_t rangeStart = 0, size_t rangeEnd = 0)
    {
      size_t headerToAllocSize(/*strlen("HTTP/1.1 200 OK\r\nContent-Type: \r\nContent-Length: \r\nCache-Control: max-age=31536000\r\n\r\n")*/86 + 255/*Longest MIME-TYPE that RFC allows*/ + 10 /*Max unsigned long footprint*/ + 1 /*\0 character*/);
      if(acceptRanges)headerToAllocSize += (22 + 25 + 10*3 + 13); //"Accept-Ranges: bytes\r\n" is 22 characters + "Content-Range: bytes %u-%u/%u\r\n" + 3*Max unsigned long footprint + space for 206 Partial Content

      char *header = (char *) malloc(sizeof(char) * headerToAllocSize);

      if(!header)return NULL;

      if(!acceptRanges)
        sprintf(header,"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %u\r\nCache-Control: max-age=31536000\r\n\r\n", httpMIMETypeToString(httpMIMEType), contentLength);
      else
        sprintf(header,"HTTP/1.1 206 Partial Content\r\nContent-Type: %s\r\nAccept-Ranges: bytes\r\nContent-Length: %u\r\nContent-Range: bytes %u-%u/%u\r\nCache-Control: max-age=31536000\r\n\r\n", httpMIMETypeToString(httpMIMEType), rangeEnd-rangeStart+1,rangeStart ,rangeEnd, contentLength);

      return header;
    }
    
    static char *getFileExtension(char *name)
    {
      char *p(strrchr(name, '.'));
      return p != NULL ? p+1 : NULL;
    }
    
    static char *getFilePathByHttpResource(const char *WWWDir, char *res)
    {
      uint16_t buffSize = (WWWDir ? strlen(WWWDir) : 0 /*default is / */)  + (strcmp(res, "/") == 0 ? 10:strlen(res)) + 1;//10 for /index.htm +1 for \0
      char *filePath = (char*) malloc( sizeof(char) * buffSize); 
      
      if(filePath == NULL)
        return NULL;
    
      WWWDir ? strcpy(filePath, WWWDir) : strcpy(filePath, "");
      strcat(filePath, (strcmp(res, "/") == 0) ? "/index.htm":res);
    
      #ifdef DEBUG_FILEPATH
      Serial.println(res);
      Serial.print("Reserved space : ");Serial.println(buffSize);
      Serial.print("Actual size : ");Serial.println(strlen(filePath));
      Serial.println(filePath);
      #endif
      
      return filePath;
    }
    
  	static HttpVersion getHttpVersionEnumValue(const char *parseBuffer)
    {
       //HTTP_0_9, HTTP_1_1, HTTP_1_0, HTTP_2_0
       if(strcmp(parseBuffer,"1.1") == 0){return HttpVersion::HTTP_1_1;}
       else if(strcmp(parseBuffer,"2.0") == 0){return HttpVersion::HTTP_2_0;}
       else if(strcmp(parseBuffer,"1.0") == 0){return HttpVersion::HTTP_1_0;}
       else if(strcmp(parseBuffer,"0.9") == 0){return HttpVersion::HTTP_0_9;}
       else
         return HttpVersion::UNKNOWN;
    }
   
    struct ApiRoutine
    {
      ApiRoutineCallback apiRoutine;
      void *pData;
      HttpRequestMethod HRM;
    };

    //Server side http cookie handling
    struct ServerHttpCookie : public HttpCookie
    {
      DictionaryHelper::StringEntity domain;
      DictionaryHelper::StringEntity path;
      int32_t sameSite : 1, httpOnly : 1, maxAge : 30;
      //Need to add the expires field as well. Thinking about the best way of doing it.
    };
    
    Dictionary<ApiRoutine> _apiDictionary;
    Dictionary<DictionaryHelper::StringEntity> _httpHeadersDictionary;
    Dictionary<ServerHttpCookie> _setCookieDictionary;
    SDClass *_sdClass;
    char *_WWWDir = nullptr; //Website root folder
};

#endif //WEBSERVER_H
