/*
 * This file is part of the KFTPGrabber project
 *
 * Copyright (C) 2003-2006 by the KFTPGrabber developers
 * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * file(s) with this exception, you may extend this exception to your
 * version of the file(s), but you are not obligated to do so.  If you
 * do not wish to do so, delete this exception statement from your
 * version.  If you delete this exception statement from all source
 * files in the program, then also delete it here.
 */
#include "socket.h"
#include "thread.h"
#include "connectionretry.h"
#include "speedlimiter.h"
#include "cache.h"

#include "misc/config.h"

#include <klocale.h>

namespace KFTPEngine {

Socket::Socket(Thread *thread, const QString &protocol)
 : m_remoteEncoding(new KRemoteEncoding()),
   m_cmdData(0),
   m_thread(thread),
   m_transferBytes(0),
   m_speedLastTime(0),
   m_speedLastBytes(0),
   m_protocol(protocol),
   m_currentCommand(Commands::CmdNone),
   m_errorReporting(true),
   m_shouldAbort(false)
{
  m_commandChain.setAutoDelete(true);
}

Socket::~Socket()
{
  delete m_remoteEncoding;
  
  if (m_connectionRetry)
    delete m_connectionRetry;
}

void Socket::initConfig()
{
  m_config.clear();
  
  // Fill in some default values
  setConfig("feat.epsv", 1);
  setConfig("feat.eprt", 1);
  setConfig("feat.pasv", 1);
  setConfig("feat.size", 1);
  setConfig("ssl.prot_mode", 2);
  setConfig("keepalive.enabled", 1);
  setConfig("keepalive.timeout", 60);
}

void Socket::emitError(ErrorCode code, const QString &param1)
{
  // Intercept connect and login errors and pass them on to the ConnectionRetry class (if enabled)
  if (getConfigInt("retry") && (code == ConnectFailed || code == LoginFailed)) {
    if (!m_connectionRetry)
      m_connectionRetry = new ConnectionRetry(this);
      
    m_connectionRetry->startRetry();
    return;
  }
  
  QValueList<EventParameter> params;
  params.append(EventParameter(code));
  params.append(EventParameter(param1));
  
  // Dispatch the event via socket thread
  m_thread->event(Event::EventError, params);
}

void Socket::emitEvent(Event::Type type, const QString &param1, const QString &param2)
{
  QValueList<EventParameter> params;
  params.append(EventParameter(param1));
  params.append(EventParameter(param2));
  
  // Dispatch the event via socket thread
  m_thread->event(type, params);
}

void Socket::emitEvent(Event::Type type, DirectoryListing param1)
{
  QValueList<EventParameter> params;
  params.append(EventParameter(param1));
  
  // Dispatch the event via socket thread
  m_thread->event(type, params);
}

void Socket::emitEvent(Event::Type type, filesize_t param1)
{
  QValueList<EventParameter> params;
  params.append(EventParameter(param1));
  
  // Dispatch the event via socket thread
  m_thread->event(type, params);
}

void Socket::emitEvent(Event::Type type, void *param1)
{
  QValueList<EventParameter> params;
  params.append(EventParameter(param1));
  
  // Dispatch the event via socket thread
  m_thread->event(type, params);
}

void Socket::changeEncoding(const QString &encoding)
{
  // Alter encoding and change socket config
  m_remoteEncoding->setEncoding(encoding.ascii());
  setConfig("encoding", encoding);
}

void Socket::protoDisconnect()
{
  resetCommandClass(UserAbort);
  
  emitEvent(Event::EventMessage, i18n("Disconnected."));
  emitEvent(Event::EventDisconnect);
}

void Socket::timeoutWait(bool start)
{
  if (start) {
    m_timeoutCounter.start();
  } else {
    m_timeoutCounter = QTime();
  }
}
    
void Socket::timeoutPing()
{
  m_timeoutCounter.restart();
}
    
void Socket::timeoutCheck()
{
  if (!isConnected())
    return;
  
  if (!m_timeoutCounter.isNull()) {
    Commands::Type command = getCurrentCommand();
    int timeout = 0;
    
    // Ignore timeouts for FXP transfers, since there is no way to do pings
    if (command == Commands::CmdFxp)
      return;
    
    if (command == Commands::CmdGet || command == Commands::CmdPut)
      timeout = KFTPCore::Config::dataTimeout();
    else
      timeout = KFTPCore::Config::controlTimeout();
    
    if (timeout > 0 && m_timeoutCounter.elapsed() > (timeout * 1000)) {
      timeoutWait(false);
      
      // We have a timeout, let's abort
      emitEvent(Event::EventMessage, i18n("Connection timed out."));
      protoDisconnect();
    }
  }
}

void Socket::keepaliveStart()
{
  m_keepaliveCounter.start();
}

void Socket::keepaliveCheck()
{
  // Ignore keepalive if the socket is busy
  if (isBusy() || !isConnected()) {
    m_keepaliveCounter.restart();
    return;
  }
  
  if (getConfigInt("keepalive.enabled") && m_keepaliveCounter.elapsed() > getConfigInt("keepalive.timeout") * 1000) {
    protoKeepAlive();
    
    // Reset the counter
    m_keepaliveCounter.restart();
  }
}

Commands::Type Socket::getCurrentCommand()
{
  if (m_commandChain.count() > 0) {
    QPtrList<Commands::Base>::iterator chainEnd = m_commandChain.end();
    for (QPtrList<Commands::Base>::iterator i = m_commandChain.begin(); i != chainEnd; i++) {
      if ((*i)->command() != Commands::CmdNone)
        return (*i)->command();
    }
  }
  
  return m_currentCommand;
}

Commands::Type Socket::getToplevelCommand()
{
  return m_currentCommand;
}

Commands::Type Socket::getPreviousCommand()
{
  if (!isChained())
    return Commands::CmdNone;
  
  if (m_commandChain.count() > 1) {
    Commands::Base *previous = m_commandChain.prev();
    m_commandChain.next();
    
    return previous->command();
  } else {
    return m_currentCommand;
  }
}

void Socket::resetCommandClass(ResetCode code)
{
  if (m_commandChain.count() > 0) {
    Commands::Base *current = m_commandChain.current();
    
    if (current->isProcessing()) {
      current->autoDestruct(code);
      return;
    } else {
      if (!current->isClean())
        current->cleanup();
          
      m_commandChain.remove();
    }
    
    if (code == Ok) {
      nextCommandAsync();
    } else {
      // Command has completed with an error code. We should abort the
      // complete chain.
      resetCommandClass(code);
    }
  } else {
    if (m_cmdData) {
      if (m_cmdData->isProcessing()) {
        m_cmdData->autoDestruct(code);
        return;
      } else {
        if (!m_cmdData->isClean())
          m_cmdData->cleanup();
          
        delete m_cmdData;
        m_cmdData = 0;
      }
    }
    
    if (code == Failed)
      emitError(OperationFailed);
    
    // Reset current command and emit a ready event
    if (getCurrentCommand() != Commands::CmdConnectRetry) {
      setCurrentCommand(Commands::CmdNone);
      emitEvent(Event::EventReady);
      emitEvent(Event::EventState, i18n("Idle."));
    }
    
    setErrorReporting(true);
    m_shouldAbort = false;
  }
}

void Socket::nextCommand()
{
  if (m_commandChain.count() > 0) {
    Commands::Base *current = m_commandChain.current();
    
    current->setProcessing(true);
    current->process();
    current->setProcessing(false);
    
    if (current->isDestructable())
      resetCommandClass(current->resetCode());
  } else if (m_cmdData) {
    m_cmdData->setProcessing(true);
    m_cmdData->process();
    m_cmdData->setProcessing(false);
    
    if (m_cmdData->isDestructable())
      resetCommandClass(m_cmdData->resetCode());
  }
}

void Socket::nextCommandAsync()
{
  m_thread->m_commandQueue.append(Commands::CmdNext);
}

void Socket::wakeup(WakeupEvent *event)
{
  if (m_commandChain.count() > 0) {
    Commands::Base *current = m_commandChain.current();
    
    if (current->isProcessing()) {
      qDebug("WARNING: Attempted to wakeup a processing socket!");
      return;
    }
    
    current->setProcessing(true);
    current->wakeup(event);
    current->setProcessing(false);
    
    if (current->isDestructable())
      resetCommandClass(current->resetCode());
  } else if (m_cmdData) {
    if (m_cmdData->isProcessing()) {
      qDebug("WARNING: Attempted to wakeup a processing socket!");
      return;
    }
    
    m_cmdData->setProcessing(true);
    m_cmdData->wakeup(event);
    m_cmdData->setProcessing(false);
    
    if (m_cmdData->isDestructable())
      resetCommandClass(m_cmdData->resetCode());
  }
}

filesize_t Socket::getTransferSpeed()
{
  time_t timeDelta = time(0) - m_speedLastTime;
  
  if (timeDelta == 0)
    return 0;
  
  if (m_speedLastBytes > m_transferBytes)
    m_speedLastBytes = 0;
  
  filesize_t speed = (m_transferBytes - m_speedLastBytes)/(time(0) - m_speedLastTime);
  
  m_speedLastBytes = m_transferBytes;
  m_speedLastTime = time(0);
  
  return speed;
}

void Socket::protoAbort()
{
  m_shouldAbort = true;
  
  if (m_connectionRetry && !m_cmdData)
    m_connectionRetry->abortRetry();
}

// *******************************************************************************************
// ******************************************* STAT ******************************************
// *******************************************************************************************

class FtpCommandStat : public Commands::Base {
public:
    enum State {
      None,
      WaitList
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandStat, Socket, CmdNone)
    
    KURL path;
    
    void process()
    {
      switch (currentState) {
        case None: {
          // Issue a list of the parent directory
          currentState = WaitList;
          socket()->setErrorReporting(false);
          socket()->protoList(path.directory());
          break;
        }
        case WaitList: {
          // Now just extract what we need
          QValueList<DirectoryEntry> list = socket()->getLastDirectoryListing().list();
          QValueList<DirectoryEntry>::iterator listEnd = list.end();
          for (QValueList<DirectoryEntry>::iterator i = list.begin(); i != listEnd; i++) {
            if ((*i).filename() == path.fileName()) {
              socket()->m_lastStatResponse = *i;
              socket()->resetCommandClass();
              return;
            }
          }
          
          // We found no such file
          socket()->m_lastStatResponse = DirectoryEntry();
          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void Socket::protoStat(const KURL &path)
{
  // Lookup the cache first and don't even try to list if cached
  DirectoryListing cached = Cache::self()->findCached(this, path.directory());
  if (cached.isValid()) {
    QValueList<DirectoryEntry> list = cached.list();
    QValueList<DirectoryEntry>::iterator listEnd = list.end();
    for (QValueList<DirectoryEntry>::iterator i = list.begin(); i != listEnd; i++) {
      if ((*i).filename() == path.fileName()) {
        m_lastStatResponse = *i;
        nextCommandAsync();
        return;
      }
    }
    
    // Cached is valid but file can't be found
    m_lastStatResponse = DirectoryEntry();
    nextCommandAsync();
    return;
  }
  
  // Not cached, let's do a real listing
  FtpCommandStat *stat = new FtpCommandStat(this);
  stat->path = path;
  addToCommandChain(stat);
  nextCommand();
}

// *******************************************************************************************
// ****************************************** SCAN *******************************************
// *******************************************************************************************

class FtpCommandScan : public Commands::Base {
public:
    enum State {
      None,
      SentList,
      ProcessList,
      ScannedDir
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandScan, Socket, CmdNone)
    
    QValueList<DirectoryEntry> currentList;
    QValueList<DirectoryEntry>::const_iterator currentEntry;
    
    QString currentDirectory;
    DirectoryTree *currentTree;
    
    void cleanup()
    {
      // We didn't emit the tree, so we should free it
      if (!socket()->isChained())
        delete currentTree;
    }
    
    void process()
    {
      // NOTE: The missing breaks are mising for a purpuse! Do not dare to add them ;)
      switch (currentState) {
        case None: {
          // We would like to disable error reporting
          socket()->setErrorReporting(false);
          
          // Issue a directory listing on the given URL
          currentState = SentList;
          socket()->protoList(currentDirectory);
          break;
        }
        case SentList: {
          currentList = socket()->getLastDirectoryListing().list();
          qHeapSort(currentList);
          
          currentEntry = currentList.begin();
          currentState = ProcessList;
          
          // Empty listing, we are done
          if (currentEntry == currentList.end()) {
            if (socket()->isChained()) {
              socket()->resetCommandClass();
            } else {
              // We are the toplevel scan command
              markClean();
              
              socket()->emitEvent(Event::EventScanComplete, currentTree);
              socket()->emitEvent(Event::EventMessage, i18n("Scan complete."));
              socket()->resetCommandClass();
            }
            
            return;
          }
        }
        case ProcessList: {
          if ((*currentEntry).isDirectory()) {
            // A directory entry
            DirectoryTree *tree = currentTree->addDirectory(*currentEntry);
            currentState = ScannedDir;
            
            FtpCommandScan *scan = new FtpCommandScan(socket());
            scan->currentDirectory = currentDirectory + "/" + (*currentEntry).filename();
            scan->currentTree = tree;
            socket()->addToCommandChain(scan);
            socket()->nextCommandAsync();
            return;
          } else {
            // A file entry
            currentTree->addFile(*currentEntry);
          }
        }
        case ScannedDir: {
          currentState = ProcessList;
          
          if (++currentEntry == currentList.end()) {
            // We are done
            if (socket()->isChained()) {
              socket()->resetCommandClass();
            } else {
              // We are the toplevel scan command
              markClean();
              
              socket()->emitEvent(Event::EventScanComplete, currentTree);
              socket()->emitEvent(Event::EventMessage, i18n("Scan complete."));
              socket()->resetCommandClass();
            }
          } else {
            socket()->nextCommandAsync();
          }
          break;
        }
      }
    }
};

void Socket::protoScan(const KURL &path)
{
  emitEvent(Event::EventMessage, i18n("Starting recursive directory scan..."));
  
  // We have to create a new command class manually, since we need to set the
  // currentTree parameter
  FtpCommandScan *scan = new FtpCommandScan(this);
  scan->currentDirectory = path.path();
  scan->currentTree = new DirectoryTree(DirectoryEntry());
  m_cmdData = scan;
  m_cmdData->process();
}

// *******************************************************************************************
// ***************************************** DELETE ******************************************
// *******************************************************************************************

class FtpCommandDelete : public Commands::Base {
public:
    enum State {
      None,
      VerifyDir,
      SimpleRemove,
      SentList,
      ProcessList,
      DeletedDir,
      DeletedFile
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandDelete, Socket, CmdDelete)
    
    QValueList<DirectoryEntry> currentList;
    QValueList<DirectoryEntry>::const_iterator currentEntry;
    
    KURL destinationPath;
    
    void process()
    {
      switch (currentState) {
        case None: {
          // We have to determine if the destination is a file or a directory
          // TODO use cached information
          if (socket()->isChained()) {
            // We know that it is a directory
            currentState = SentList;
            socket()->protoList(destinationPath);
          } else {
            currentState = VerifyDir;
            socket()->protoStat(destinationPath);
          }
          break;
        }
        case VerifyDir: {
          DirectoryEntry entry = socket()->getStatResponse();
          
          if (entry.filename().isEmpty()) {
            // The file doesn't exist, abort
            socket()->resetCommandClass(Failed);
          } else {
            if (entry.isDirectory()) {
              // It is a directory, remove recursively
              currentState = SentList;
              socket()->protoList(destinationPath);
            } else {
              // A single file, a simple remove
              currentState = SimpleRemove;
              socket()->setConfig("params.remove.directory", 0);
              socket()->protoRemove(destinationPath);
            }
          }          
          break;
        }
        case SimpleRemove: {
          if (!socket()->isChained())
            socket()->emitEvent(Event::EventReloadNeeded);
          socket()->resetCommandClass();
          break;
        }
        case SentList: {
          currentList = socket()->getLastDirectoryListing().list();
          currentEntry = currentList.begin();
          currentState = ProcessList;
          
          // Empty listing, we are done
          if (currentEntry == currentList.end()) {
            if (socket()->isChained())
              socket()->resetCommandClass();
            else {
              // We are the top level command class, remove the destination dir
              currentState = SimpleRemove;
              socket()->setConfig("params.remove.directory", 1);
              socket()->protoRemove(destinationPath);
            }
            
            return;
          }
        }
        case ProcessList: {
          KURL childPath = destinationPath;
          childPath.addPath((*currentEntry).filename());
          
          if ((*currentEntry).isDirectory()) {
            // A directory, chain another delete command
            currentState = DeletedDir;
            
            // Chain manually, since we need to set some parameters
            FtpCommandDelete *del = new FtpCommandDelete(socket());
            del->destinationPath = childPath;
            socket()->addToCommandChain(del);
            socket()->nextCommand();
          } else {
            // A file entry - remove
            currentState = DeletedFile;
            socket()->setConfig("params.remove.directory", 0);
            socket()->protoRemove(childPath);
          }
          break;
        }
        case DeletedDir: {
          // We have to remove the empty directory
          KURL childPath = destinationPath;
          childPath.addPath((*currentEntry).filename());
          
          currentState = DeletedFile;
          socket()->setConfig("params.remove.directory", 1);
          socket()->protoRemove(childPath);
          break;
        }
        case DeletedFile: {
          currentState = ProcessList;
          
          if (++currentEntry == currentList.end()) {
            if (socket()->isChained())
              socket()->resetCommandClass();
            else {
              // We are the top level command class, remove the destination dir
              currentState = SimpleRemove;
              socket()->setConfig("params.remove.directory", 1);
              socket()->protoRemove(destinationPath);
            }
          } else
            socket()->nextCommand();
          break;
        }
      }
    }
};

void Socket::protoDelete(const KURL &path)
{
  // We have to create a new command class manually to set some parameter
  FtpCommandDelete *del = new FtpCommandDelete(this);
  del->destinationPath = path;
  m_cmdData = del;
  m_cmdData->process();
}

// *******************************************************************************************
// ***************************************** CHMOD *******************************************
// *******************************************************************************************

class FtpCommandRecursiveChmod : public Commands::Base {
public:
    enum State {
      None,
      VerifyDir,
      SimpleChmod,
      SentList,
      ProcessList,
      ChmodedDir,
      ChmodedFile
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRecursiveChmod, Socket, CmdChmod)
    
    QValueList<DirectoryEntry> currentList;
    QValueList<DirectoryEntry>::const_iterator currentEntry;
    
    KURL destinationPath;
    int mode;
    
    void process()
    {
      switch (currentState) {
        case None: {
          // We have to determine if the destination is a file or a directory
          if (socket()->isChained()) {
            // We know that it is a directory
            currentState = SentList;
            socket()->protoList(destinationPath);
          } else {
            currentState = VerifyDir;
            socket()->protoStat(destinationPath);
          }
          break;
        }
        case VerifyDir: {
          DirectoryEntry entry = socket()->getStatResponse();
          
          if (entry.filename().isEmpty()) {
            // The file doesn't exist, abort
            socket()->resetCommandClass(Failed);
          } else {
            if (entry.isDirectory()) {
              // It is a directory, chmod recursively
              currentState = SentList;
              socket()->protoList(destinationPath);
            } else {
              // A single file, a simple chmod
              currentState = SimpleChmod;
              socket()->protoChmodSingle(destinationPath, mode);
            }
          }          
          break;
        }
        case SimpleChmod: {
          socket()->resetCommandClass();
          break;
        }
        case SentList: {
          currentList = socket()->getLastDirectoryListing().list();
          currentEntry = currentList.begin();
          currentState = ProcessList;
          
          // Empty listing, we are done
          if (currentEntry == currentList.end()) {
            if (socket()->isChained())
              socket()->resetCommandClass();
            else {
              // We are the top level command class, chmod the destination dir
              currentState = SimpleChmod;
              socket()->protoChmodSingle(destinationPath, mode);
            }
            
            return;
          }
        }
        case ProcessList: {
          KURL childPath = destinationPath;
          childPath.addPath((*currentEntry).filename());
          
          if ((*currentEntry).isDirectory()) {
            // A directory, chain another recursive chmod command
            currentState = ChmodedDir;
            
            // Chain manually, since we need to set some parameters
            FtpCommandRecursiveChmod *cm = new FtpCommandRecursiveChmod(socket());
            cm->destinationPath = childPath;
            cm->mode = mode;
            socket()->addToCommandChain(cm);
            socket()->nextCommand();
          } else {
            // A file entry - remove
            currentState = ChmodedFile;
            socket()->protoChmodSingle(childPath, mode);
          }
          break;
        }
        case ChmodedDir: {
          // We have to chmod the directory
          KURL childPath = destinationPath;
          childPath.addPath((*currentEntry).filename());
          
          currentState = ChmodedFile;
          socket()->protoChmodSingle(childPath, mode);
          break;
        }
        case ChmodedFile: {
          currentState = ProcessList;
          
          if (++currentEntry == currentList.end()) {
            if (socket()->isChained())
              socket()->resetCommandClass();
            else {
              // We are the top level command class, chmod the destination dir
              currentState = SimpleChmod;
              socket()->protoChmodSingle(destinationPath, mode);
            }
          } else
            socket()->nextCommand();
          break;
        }
      }
    }
};

void Socket::protoChmod(const KURL &path, int mode, bool recursive)
{
  if (recursive) {
    // We have to create a new command class manually to set some parameters
    FtpCommandRecursiveChmod *cm = new FtpCommandRecursiveChmod(this);
    cm->destinationPath = path;
    cm->mode = mode;
    m_cmdData = cm;
    m_cmdData->process();
  } else {
    // No recursive, just chmod a single file
    protoChmodSingle(path, mode);
  }
}

}
