#ifndef FTPSERVER_H
#define FTPSERVER_H

#include "TCPServer.h"
#include "SDCardManager.h"
#include "definition.h"
#include "Dictionary.h"
#define DEBUG_FTPS
#define READ_BUFFER_SIZE 2500 //2500 is max to read sd card, more will crash

template <typename T>
class FTPServer : public TCPServer<T>
{
  public:
    enum FTPClientState {INIT, WAITING_FOR_COMMANDS};
    enum FTPClientDataTransfer {NONE, LIST_DF, NLST_DF, RETR_DF, STOR_DF};
    enum FileTransferStatus {OK, NOT_FOUND, NO_FILE_NAME};
    enum BinaryFlag {OFF = 0, ON};

    FTPServer(unsigned int port = 21, SDCardManager *sdCardManager = NULL, const char *login = NULL, const char *password = NULL, uint8_t maxClient = MAX_CLIENT, uint16_t clientCommandDataBufferSize = 255) : TCPServer<T>(port, maxClient, clientCommandDataBufferSize),
      _login(NULL),
      _password(NULL),
      _dataPort(1024),
      _dataServer(_dataPort),
      _sdCardManager(sdCardManager)
    {
      if (login != NULL)
      {
        if (strlen(login) > 0)
        {
          _login = (char *)malloc((sizeof(char) * strlen(login)) + 1);
          strcpy(_login, login);
        }
      }

      if (password != NULL)
      {
        if (strlen(password) > 0)
        {
          _password = (char *)malloc((sizeof(char) * strlen(password)) + 1);
          strcpy(_password, password);
        }
      }

      _dataServer.begin(_dataPort);
    }

    void setCustomDataPort(unsigned int port)
    {
      _dataPort = port;
    }

    virtual ~FTPServer()
    {
      free(_login); free(_password);
    }

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

    virtual void greetClient(T *client)
    {
      //The first time the client connects, we send the server's information
      client->_client.println("220 Welcome to the ESP8266SwissArmyBoard embedded FTP server.");
      client->_clientState = TCPClient::HANDLED;
    }

    virtual void processClientData(T *client)
    {
      if (client->_waitingForDataConnection)
      {
        //#ifdef DEBUG_FTPS
        //Serial.println("Listening for new data client");
        //#endif
        WiFiClient dataClient = _dataServer.available();

        if (dataClient)
        {
          if (dataClient.connected())
          {
            client->_waitingForDataConnection = false;
            client->setDataClient(dataClient);
            #ifdef DEBUG_FTPS
            Serial.println("Data client accepted successfully");
            #endif
          }
          else
            dataClient.stop();
        }
      }

      switch(client->_dataTransferPending)
      {
        case LIST_DF: //We list the files of the current directory
        //We check if the dataConnection is established:
        if (client->_dataClient.connected())
        {
          client->_client.println("150 File status okay;");
          if(sendFSTree(client))
          {
            client->_client.println("226 Closing data connection.");
          }
          else
          {
            client->_client.println("451 Requested action aborted.");
          }
          
          client->_dataClient.stop();
          client->_dataTransferPending = NONE;
        }
        //A timeout should be added
        /*else
        {
          client->_client.println("425 Can't open data connection.");
        }*/
          break;
        case RETR_DF:
          if (client->_dataClient.connected())
          {
            if(client->_fileSentBytes == 0)
              client->_client.println("150 File status okay;");
              
            FileTransferStatus fts;
            if(!sendFile(client,&fts))//File was sent or error occured
            {
              //we check the return code
              switch(fts)
              {
                case OK:
                  client->_client.println("226 Closing data connection.");
                  client->_dataClient.stop();
                  client->_dataTransferPending = NONE;
                  break;
                case NOT_FOUND:
                  client->_client.println("451 File not found.");
                  client->_dataClient.stop();
                  client->_dataTransferPending = NONE;
                  break;
              }
            }
          }
          //A timeout should be added
          /*else
          {
            client->_client.println("425 Can't open data connection.");
          }*/
          break;
         case STOR_DF :
          if (client->_dataClient.connected() || client->_dataClient.available())
          { 
            if(client->_dataClient.connected())
              client->_fileIsBeeingReceived = true;
              
            if(client->_dataClient.available())
            {
              #ifdef DEBUG_FTPS
              Serial.printf("receiving file %s\n", client->_currentFile);
              #endif
              FileTransferStatus fts = OK;
              
              if(!writeToSdCard(client, &fts)) //An error occured
              {
                if(fts == NO_FILE_NAME)
                {
                  client->_client.println("501 No file name given.");
                }
                else
                {
                  client->_client.println("451 Requested action aborted: local error in processing.");
                }
                client->_dataClient.stop();
                client->_fileIsBeeingReceived = false;
                client->_dataTransferPending = NONE;
              }
            }
          }
          else if(client->_fileIsBeeingReceived)
          {
            if(client->_fileRecvBytes == 0) //File was just created empty
            {
              if(_sdCardManager->exists(client->_currentFile))
               _sdCardManager->remove(client->_currentFile);
        
              File file2create = _sdCardManager->open(client->_currentFile, FILE_WRITE);

              file2create.close();
              #ifdef DEBUG_FTPS
              Serial.println("File just created");
              #endif
            }
            else
            {
              #ifdef DEBUG_FTPS
              Serial.println("Whole file received");
              #endif
            }
            
            client->_client.println("226 Closing data connection.");
            
            client->_fileIsBeeingReceived = false;
            client->_dataClient.stop();
            client->_dataTransferPending = NONE;
            client->_fileRecvBytes = 0;
          }
          break;
      }

      #ifdef DEBUG_FTPS
      if (client->_newDataAvailable)
      {
        Serial.print("Client --> "); Serial.print(client->_id); Serial.print(" : "); Serial.print((char *)client->_data); Serial.println("#");
      }
      #endif

      if (client->_dataSize > 0)
      {
        switch (client->_ftpClientState)
        {
          case INIT:
            client->setCurrentDirectory(FTP_DIR);
            client->_ftpClientState = WAITING_FOR_COMMANDS;
            break;
          case WAITING_FOR_COMMANDS:
            processCommands(client);
            break;
        }
      }
    }

  private:
    void processCommands(T *client)
    {
      if (!client->parseCommandAndParameters()) //Failed to retrieve command and parameters
      {
        //We can close the connection or do other things
        return;
      }

      #ifdef DEBUG_FTPS
      Serial.printf("Issued command : '%s'\n", client->_ftpCommand);
      Serial.println("Get params :");
      for (int i = 0; i < client->_cmdParameters->count(); i++)
      {
        Serial.print(client->_cmdParameters->getParameter(i)); Serial.print(" : "); Serial.printf("'%s'\n", client->_cmdParameters->getAt(i)->getString());
      }
      #endif

      if (strcmp(client->_ftpCommand, "USER") == 0)
      {
        //We check if we set a login and a password :
        if (_login != NULL && _password != NULL)
        {
          if (client->_cmdParameters->count() > 0)
          {
            client->_client.println("331 User name okay, need password.");
            client->setUsername(client->_cmdParameters->getAt(0)->getString());
          }
          else
            client->_client.println("530 Username required.");
        }
        else //The ftp access is open
        {
          client->_loggedIn = true;
          client->_client.println("230 User logged in, proceed.");
        }
      }
      else if (strcmp(client->_ftpCommand, "PASS") == 0)
      {
        //We now check if the username and password correspond
        if (client->_cmdParameters->count() > 0)
        {
          if (strcmp(_login, client->_username) == 0 && strcmp(_password, client->_cmdParameters->getAt(0)->getString()) == 0)
          {
            client->_loggedIn = true;
            client->_client.println("230 User logged in, proceed.");
          }
          else
          {
            client->_client.println("530 Wrong username or password !.");
          }
        }
        else
        {
          client->_client.println("530 Password required.");
        }
      }
      else if (strcmp(client->_ftpCommand, "PWD") == 0) //We set the default directory
      {
        client->_client.printf("257 \"%s\"\r\n", client->_currentDirectory);
      }
      else if (strcmp(client->_ftpCommand, "TYPE") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          switch (client->_cmdParameters->getAt(0)->getString()[0])
          {
            case 'I':
              client->_binaryFlag = ON;
              client->_client.println("200 Command okay.");
              break;
            case 'L':
              client->_binaryFlag = ON;
              client->_client.println("200 Command okay.");
              break;
            case 'A':
              client->_binaryFlag = OFF;
              client->_client.println("200 Command okay.");
              break;
            default:
              client->_client.println("504 Command not implemented for TYPE.");
          }
        }
        else
        {
          client->_client.println("504 Command not implemented for TYPE.");
        }
      }
      else if (strcmp(client->_ftpCommand, "PASV") == 0)
      {
        client->_client.printf("227 Entering Passive Mode (%u,%u,%u,%u,%d,%d).\r\n", WiFi.localIP()[0],  WiFi.localIP()[1],  WiFi.localIP()[2],  WiFi.localIP()[3], _dataPort / 256, _dataPort % 256);
        client->_waitingForDataConnection = true;
        #ifdef DEBUG_FTPS
        Serial.println("Opening data server for new data client");
        #endif
      }
      else if (strcmp(client->_ftpCommand, "LIST") == 0)
      {
        //We inform that a data transfer is pending
        client->_dataTransferPending = LIST_DF;
      }
      else if (strcmp(client->_ftpCommand, "CWD") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //Go back one level
          if (strcmp(client->_cmdParameters->getAt(0)->getString(), "..") == 0)
          {
            char *dirCopy = (char *)malloc((sizeof(char) * strlen(client->_currentDirectory)) + 1);
            strcpy(dirCopy, client->_currentDirectory);
            
            char *p = lastIndexOf(dirCopy, '/');
            if (dirCopy == p)
            {
              *(p + 1) = '\0';
            }
            else
              *(p) = '\0';

            #ifdef DEBUG_FTPS
            Serial.printf("Final dir : %s\n", dirCopy);
            #endif

            client->setCurrentDirectory(dirCopy);
            free(dirCopy);
          }
          else if (strcmp(client->_cmdParameters->getAt(0)->getString(), "/") == 0)
          {
            client->setCurrentDirectory("/");
          }
          else //If the client already nows the path, he will send it with a /
          {
            if(client->_cmdParameters->getAt(0)->getString()[0] == '/')//Then this is a name prefix
            {
              char *fullDirPath = constructFileNameWithPath("",client->_cmdParameters);
              client->setCurrentDirectory(fullDirPath);
              free(fullDirPath);
            }
            else
            {
              //Directories with spaces are now working:
              char *directoryFullpath = constructFileNameWithPath(client->_currentDirectory,client->_cmdParameters);
              
              #ifdef DEBUG_FTPS
              Serial.printf("Final dir : %s\n", directoryFullpath);
              #endif
              client->setCurrentDirectory(directoryFullpath);
              free(directoryFullpath);
            }
          }
          
          client->_client.println("250 Requested file action okay, completed.");
          #ifdef DEBUG_FTPS
          Serial.printf("CWD new dir : %s\n", client->_currentDirectory);
          #endif
        }
      }
      else if(strcmp(client->_ftpCommand, "RETR") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //We save the file path to be sent
          char *file2store(NULL);
          if(client->_cmdParameters->getAt(0)->getString()[0] == '/')
            file2store = constructFileNameWithPath("", client->_cmdParameters);
          else
            file2store = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);
          
          #ifdef DEBUG_FTPS
          Serial.printf("Final file path : %s\n",file2store);
          #endif
          client->setCurrentFile(file2store);
          free(file2store);
          client->_dataTransferPending = RETR_DF;
        }
      }
      else if(strcmp(client->_ftpCommand, "MKD") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          char *dirNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);
          
          if(dirNameWithPath != NULL)
          {
            #ifdef DEBUG_FTPS
            Serial.printf("Final dirName : #%s#\n",dirNameWithPath);
            #endif
            
            if(_sdCardManager->mkdir(dirNameWithPath))
            {
              client->_client.printf("257 \"%s\"\r\n", dirNameWithPath);
            }
            else
              client->_client.println("550 Failed to mkdir (no spaces allowed in dir name).");
              
            free(dirNameWithPath);
          }
          else
          {
            client->_client.println("550 Requested action not taken.");
          }
        }
        else
        {
          client->_client.println("550 Requested action not taken.");
        }
      }
      else if(strcmp(client->_ftpCommand, "RMD") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //We have the dir name, we need to append the current directory...
          char *dirNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);
          
          #ifdef DEBUG_FTPS
          Serial.printf("pathDirName to delete : #%s#\n",dirNameWithPath);
          #endif

          if(_sdCardManager->rmdir(dirNameWithPath))
          {
            client->_client.println("250 Requested file action okay.");
          }
          else
          {
            client->_client.println("550 Requested action not taken.");
          }
          
          free(dirNameWithPath);
        }
        else
        {
          client->_client.println("550 Requested action not taken.");
        }
      }
      else if(strcmp(client->_ftpCommand, "STOR") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //We save the file path to be sent
          char *fileNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);

          client->_client.println("150 File status okay; about to open data connection.");
          
          client->setCurrentFile(fileNameWithPath);
          
          #ifdef DEBUG_FTPS
          Serial.printf("File to store : #%s#\n", client->_currentFile);
          #endif
          free(fileNameWithPath);
          client->_dataTransferPending = STOR_DF;
          client->startTimeout();
        }
      }
      else if(strcmp(client->_ftpCommand, "SYST") == 0)
      {
        client->_client.println("215 UNIX Type: L8");
      }
      else if(strcmp(client->_ftpCommand, "DELE") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //We have the file name, we need to append the current directory...
          char *file2deleteNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);
          
          #ifdef DEBUG_FTPS
          Serial.printf("file to delete : #%s#\n",file2deleteNameWithPath);
          #endif

          if(_sdCardManager->remove(file2deleteNameWithPath))
          {
            client->_client.println("250 Requested file action okay.");
          }
          else
          {
            client->_client.println("550 Requested action not taken.");
          }
          
          free(file2deleteNameWithPath);
        }
        else
        {
          client->_client.println("550 Requested action not taken.");
        }
      }
      else if(strcmp(client->_ftpCommand, "RNFR") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //We have the file name, we need to append the current directory...
          char *fileNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);
          
          #ifdef DEBUG_FTPS
          Serial.printf("file to rename : #%s#\n",fileNameWithPath);
          #endif

          client->setCurrentFile(fileNameWithPath);
          free(fileNameWithPath);

          client->_client.println("350 Requested file action pending further information.");
        }
        else
        {
          client->_client.println("550 Requested action not taken.");
        }
      }
      else if(strcmp(client->_ftpCommand, "RNTO") == 0)
      {
        if (client->_cmdParameters->count() > 0)
        {
          //Here we rename the file
          char *file2RenameNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters);
          #ifdef DEBUG_FTPS
          Serial.printf("file to rename to : #%s#\n",file2RenameNameWithPath);
          Serial.printf("Old name : %s --> %s\n",client->_currentFile,file2RenameNameWithPath);
          #endif

          if(_sdCardManager->rename(client->_currentFile,file2RenameNameWithPath))
          {
            client->_client.println("250 Requested file action okay.");
          }else
            client->_client.println("550 Requested action not taken.");
          
          free(file2RenameNameWithPath);
        }
      }
      else
        client->_client.println("502 Command not implemented.");
    }

    //Here we write the received file to the sd card
    boolean writeToSdCard(T *client, FileTransferStatus *fts)
    {
      if (client->_currentFile != NULL)
      {
        if(_sdCardManager->exists(client->_currentFile) && client->_fileRecvBytes == 0)
          _sdCardManager->remove(client->_currentFile);
        
        File fileBeeingReceived = _sdCardManager->open(client->_currentFile, FILE_WRITE);

        if(fileBeeingReceived)
        {
          uint8_t recvBuffer[2048];

          /*fileBeeingReceived.seek(client->_fileRecvBytes);*/
          uint16_t size = client->_dataClient.read(recvBuffer, 2048);

          fileBeeingReceived.write(recvBuffer, size);

          client->_fileRecvBytes += size;
          fileBeeingReceived.close();
          #ifdef DEBUG_FTPS
          Serial.printf("Writting : %d bytes to file\n", size);
          #endif
        }
        else
        {
          return false;
        }
      }
      else
      {
        *fts = NO_FILE_NAME;
        return false;
      }
      return true;
    }
    
    //Here we send the fs tree to the ftp client
    boolean sendFSTree(T *client)
    {
      if (client->_currentDirectory != NULL)
      {
        #ifdef DEBUG_FTPS
        Serial.printf("Directory : %s\n",client->_currentDirectory);
        #endif
        File currentDirectory = _sdCardManager->open(client->_currentDirectory);
        if (currentDirectory)
        {
          currentDirectory.rewindDirectory();
          while (true) //Maybe be remove in the future to improve responsiveness
          {
            File fileOrDir = currentDirectory.openNextFile();

            if (!fileOrDir) //No more files in the directory
              break;

            #ifdef DEBUG_FTPS
            Serial.printf("Filename : %s\n", fileOrDir.name());
            #endif
            
            client->_dataClient.printf("%crwxrwxrwx 1 owner esp8266           %d Aug 26 16:31 %s\r\n", fileOrDir.isDirectory() ? 'd' : '-', fileOrDir.isDirectory() ? 0 : fileOrDir.size(), fileOrDir.name());

            fileOrDir.close();
          }

          currentDirectory.close();
        }
        else //Failed to open directory
        {
          #ifdef DEBUG_FTPS
          Serial.println("Failed to open directory");
          #endif
          return false;
        }

        return true;
      }

      return false;
    }

    //The binary flag needs to be taken into consideration
    boolean sendFile(T *client, FileTransferStatus *fts)
    {
      if (client->_currentFile != NULL)
      {
        uint8_t sendBuffer[READ_BUFFER_SIZE];
        File fileToSend = _sdCardManager->open(client->_currentFile);
        
        if (fileToSend)
        {
          *fts = OK;
          unsigned int readBytes(0);
          fileToSend.seek(client->_fileSentBytes);
          
          if(fileToSend.available())
          {
            readBytes = fileToSend.read(sendBuffer, READ_BUFFER_SIZE);
            client->_dataClient.write(sendBuffer, readBytes);

            client->_fileSentBytes += readBytes;

            #ifdef DEBUG_FTPS
            Serial.printf("File : bytes sent : %u\n",readBytes);
            #endif
          }
          else //The whole file has been sent
          {
            fileToSend.close();
            client->_fileSentBytes = 0;
            return false;
          }
          
          fileToSend.close();
        }
        else //Failed to open file maybe not found
        {
          #ifdef DEBUG_FTPS
          Serial.println("Failed to open file");
          #endif
          *fts = NOT_FOUND;
          return false;
        }
        
        return true;//Still things to send
      }
      
      *fts = NOT_FOUND;
      return false;
    }

    static char *_83FileNameFormat(char *filename)
    {
        char *buffer = (char *)malloc((sizeof(char) * strlen(filename)) + 1);
        strcpy(buffer, filename);
        char *delimiter = strchr(buffer, '.');
        if(delimiter != NULL)
        {
            *delimiter = '\0';
    
            if(strlen(buffer) > 8)
                buffer[8] = '\0';
    
            strcpy(filename, buffer);
    
            if(strlen(delimiter + 1) > 3)
                *(delimiter + 1 + 3) = '\0';
    
            strcat(filename, ".");
            strcat(filename, delimiter + 1);
        }
        else
        {
            if(strlen(buffer) > 8)
            {
                buffer[8] = '\0';
            }
            strcpy(filename, buffer);
        }
        free(buffer);
        return filename;
    }

    //This functions construct the full file path.
    //ie : if the current directory is : "/somedir/subdir" and the received file name is : "my file .txt"
    //then it will return : "/somedir/subdir/my file .txt"
    //Note that the file name is contained in a parameter list : "my" in the first element, then "file" in the second and finally ".txt" in the third.
	  //Return NULL if malloc fails
	  // DO NOT FORGET TO FREE THE ALLOCATED STRING AFTER USING IT....
    static char *constructFileNameWithPath(const char *dir, Dictionary<DictionaryHelper::StringEntity> *parameters)
    {
      uint16_t fileWithPathSize(strlen(dir) + 1) /*dir path plus filename*/, paramCount(parameters->count());
      char *fileNameWithPath(NULL);
	  
      for(int i(0); i < paramCount; i++)
      {
        fileWithPathSize += strlen(parameters->getAt(i)->getString()) + 1;
      }
      
      fileNameWithPath = (char *)malloc( sizeof(char) * fileWithPathSize );

      #ifdef DEBUG_FTPS
      Serial.printf("Allocated string size : %d\n",fileWithPathSize);
      #endif
	  
	    if(fileNameWithPath == NULL)//Malloc fails
		    return NULL;
      
      strcpy(fileNameWithPath, dir);
      if(strcmp(fileNameWithPath, "/") != 0 && strlen(dir) != 0)
        strcat(fileNameWithPath,"/");
      
      for(int i(0); i < paramCount; i++)
      {
        strcat(fileNameWithPath, parameters->getAt(i)->getString());

        if(i != paramCount-1)
          strcat(fileNameWithPath," ");
      }
	  
      return fileNameWithPath;
    }

    char *_login;
    char *_password;
    unsigned int _dataPort;

    WiFiServer _dataServer; //In passive mode, the FTP server opens two different ports (one for the commands and the other for the data stream)
    SDCardManager *_sdCardManager;
};

#endif //FTPSERVER_H
