/*
 * This file is part of the KFTPGrabber project
 *
 * Copyright (C) 2003-2004 by the KFTPGrabber developers
 * Copyright (C) 2003-2004 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 <math.h>

#include "kftpqueue.h"
#include "kftpbookmarks.h"
#include "widgets/systemtray.h"
#include "kftpqueueprocessor.h"
#include "kftpsession.h"

#include "misc/config.h"
#include "misc/filter.h"

#include <kmessagebox.h>
#include <klocale.h>
#include <kio/renamedlg.h>
#include <kdiskfreesp.h>
#include <kfileitem.h>
#include <kopenwith.h>
#include <kstaticdeleter.h>
#include <kservice.h>
#include <kuserprofile.h>
#include <kstandarddirs.h>
#include <krun.h>
#include <kmdcodec.h>

#include <qapplication.h>
#include <qregexp.h>
#include <qobjectlist.h>
#include <qfile.h>

using namespace KFTPEngine;
using namespace KFTPCore::Filter;

namespace KFTPQueue {

OpenedFile::OpenedFile(TransferFile *transfer)
  : m_source(transfer->getSourceUrl()),
    m_dest(transfer->getDestUrl()),
    m_hash(QString::null)
{
  // Calculate the file's MD5 hash
  QFile file(m_dest.path());
  if (!file.open(IO_ReadOnly)) {
    return;
  }
  
  KMD5 context;
  if (context.update(file))
    m_hash = QString(context.hexDigest());
  file.close();
}

bool OpenedFile::hasChanged()
{
  // Compare the file's MD5 hash with stored value
  QFile file(m_dest.path());
  if (!file.open(IO_ReadOnly)) {
    return false;
  }
  
  QString tmp = QString::null;
  KMD5 context;
  if (context.update(file))
    tmp = QString(context.hexDigest());
  file.close();
  
  return tmp != m_hash;
}

UserDialogRequest::UserDialogRequest(TransferFile *transfer, filesize_t srcSize, time_t srcTime,
                  filesize_t dstSize, time_t dstTime)
  : m_transfer(transfer),
    m_srcSize(srcSize),
    m_srcTime(srcTime),
    m_dstSize(dstSize),
    m_dstTime(dstTime)
{
}

void UserDialogRequest::sendResponse(FileExistsWakeupEvent *event)
{
  m_transfer->wakeup(event);
  delete this;
}

Manager *Manager::m_self = 0;
static KStaticDeleter<Manager> staticManagerDeleter;

Manager *Manager::self()
{
  if (!m_self) {
    staticManagerDeleter.setObject(m_self, new Manager());
  }
  
  return m_self;
}

Manager::Manager()
  : m_topLevel(new QueueObject(this, QueueObject::Toplevel)),
    m_processingQueue(false),
    m_feDialogOpen(false),
    m_defaultFeAction(FE_DISABLE_ACT)
{
  m_topLevel->setId(0);
  
  m_lastQID = 1;
  m_curDownSpeed = 0;
  m_curUpSpeed = 0;

  m_emitUpdate = true;

  // Create the queue processor object
  m_queueProc = new KFTPQueueProcessor(this);

  connect(m_queueProc, SIGNAL(queueComplete()), this, SLOT(slotQueueProcessingComplete()));
  connect(m_queueProc, SIGNAL(queueAborted()), this, SLOT(slotQueueProcessingAborted()));

  // Create the queue converter object
  m_converter = new KFTPQueueConverter(this);
}

Manager::~Manager()
{
  if (m_self == this)
    staticManagerDeleter.setObject(m_self, 0, false);
}

void Manager::stopAllTransfers()
{
  if (isProcessing()) {
    abort();
  } else {
    QueueObject *i;
    QPtrList<QueueObject> sites = topLevelObject()->getChildrenList();
    
    for (i = sites.first(); i; i = sites.next()) {
      if (i->isRunning()) {
        i->abort();
      } else {
        QueueObject *t;
        QPtrList<QueueObject> list = i->getChildrenList();
        
        for (t = list.first(); t; t = list.next()) {
          if (t->isRunning())
            t->abort();
        }
      }
    }
  }
}

Transfer *Manager::findTransfer(long id)
{
  // First try the cache
  QueueObject *object = m_queueObjectCache[QString::number(id)];
  
  if (!object) {
    object = m_topLevel->findChildObject(id);
    m_queueObjectCache.insert(QString::number(id), object);
  }
  
  return static_cast<Transfer*>(object);
}

Site *Manager::findSite(KURL url, bool noCreate)
{
  // Reset path
  url.setPath("/");
  
  if (url.isLocalFile())
    return NULL;
  
  // Find the appropriate site and if one doesn't exist create a new one
  QueueObject *i;
  QPtrList<QueueObject> sites = topLevelObject()->getChildrenList();
  
  for (i = sites.first(); i; i = sites.next()) {
    if (i->getType() == QueueObject::Site) {
      Site *site = static_cast<Site*>(i);
      
      if (site->getUrl() == url)
        return site;
    }
  }
  
  // The site doesn't exist, let's create one
  if (!noCreate) {
    Site *site = new Site(topLevelObject(), url);
    site->setId(m_lastQID++);
    emit newSite(site);
    
    return site;
  }
  
  return 0;
}

void Manager::insertTransfer(Transfer *transfer)
{
  // Set id
  transfer->setId(m_lastQID++);
  
  // Reparent transfer
  filesize_t size = transfer->getSize();
  transfer->addSize(-size);
  
  if (transfer->hasParentObject())
    transfer->parentObject()->delChildObject(transfer);
  
  if (transfer->parent())
    transfer->parent()->removeChild(transfer);

  Site *site = 0;
  
  switch (transfer->getTransferType()) {
    case Download: site = findSite(transfer->getSourceUrl()); break;
    case Upload: site = findSite(transfer->getDestUrl()); break;
    case FXP: site = findSite(transfer->getSourceUrl()); break;
  }
  
  site->insertChild(transfer);
  site->addChildObject(transfer);
  transfer->addSize(size);

  emit newTransfer(transfer);
  
  if (m_emitUpdate)
    emit queueUpdate();
}

void Manager::insertTransfer(KURLDrag *drag)
{
  // Decode the drag
  KIO::MetaData p_meta;
  KURL::List p_urls;
  KURLDrag::decode(drag, p_urls, p_meta);

  // TODO make support for local drops - eg. from konqueror, where
  // we get no meta data, so we must get the file info ourselves and
  // reject remote urls (or show a dialog to ask the user if he
  // wants to connect to the remote site)

  // Now we should add transfers for all URLs
  Transfer *lastTransfer = 0L;
  KURL::List::iterator end(p_urls.end());

  for (KURL::List::iterator i(p_urls.begin()); i != end; ++i) {
    QString p_data = p_meta[(*i).htmlURL().local8Bit()];
    QChar type = p_data.at(0);
    filesize_t size = p_data.section(':', 1, 1).toULongLong();
    KURL sourceUrl = (*i);
    KURL destinationUrl = KURL(p_meta["DestURL"]);
    destinationUrl.addPath(sourceUrl.fileName());

    // Skip where both files are local
    if (sourceUrl.isLocalFile() && destinationUrl.isLocalFile())
      continue;

    lastTransfer = spawnTransfer(sourceUrl, destinationUrl, size, type == 'D', true, true, 0L, true);
  }

  // Execute the transfer if set in configuration
  if (!KFTPCore::Config::queueOnDND() && lastTransfer)
    static_cast<KFTPQueue::Site*>(lastTransfer->parentObject())->delayedExecute();
}

Transfer *Manager::spawnTransfer(KURL sourceUrl, KURL destinationUrl, filesize_t size, bool dir, bool ignoreSkip,
                                 bool insertToQueue, QObject *parent, bool noScan)
{
  const ActionChain *actionChain = Filters::self()->process(sourceUrl, size, dir);
  
  if (!ignoreSkip && (actionChain && actionChain->getAction(Action::Skip)))
    return 0;
  
  // Determine transfer type
  TransferType type;
  
  if (sourceUrl.isLocalFile())
    type = Upload;
  else if (destinationUrl.isLocalFile())
    type = Download;
  else
    type = FXP;

  // Should we lowercase the destination path ?
  if (actionChain && actionChain->getAction(Action::Lowercase))
    destinationUrl.setPath(destinationUrl.directory() + "/" + destinationUrl.fileName().lower());
  
  // Reset a possible preconfigured default action
  setDefaultFileExistsAction();

  if (!parent)
    parent = this;

  Transfer *transfer = 0L;

  if (dir)
    transfer = new TransferDir(parent);
  else {
    transfer = new TransferFile(parent);
    transfer->addSize(size);
  }

  transfer->setSourceUrl(sourceUrl);
  transfer->setDestUrl(destinationUrl);
  transfer->setTransferType(type);

  if (insertToQueue) {
    insertTransfer(transfer);
  } else {
    transfer->setId(m_lastQID++);
    emit newTransfer(transfer);
  }

  if (dir && !noScan) {
    // This is a directory, we should scan the directory and add all files/dirs found
    // as parent of current object
    KFTPSession::Session *session = KFTPSession::Manager::self()->spawnRemoteSession(KFTPSession::IgnoreSide, sourceUrl, 0, true);
    session->scanDirectory(transfer);
  }

  return transfer;
}

void Manager::removeTransfer(Transfer *transfer, bool abortSession)
{
  if (!transfer)
    return;
    
  transfer->abort();
  long id = transfer->getId();
  long sid = transfer->parentObject()->getId();
  
  // Remove transfer from cache
  m_queueObjectCache.remove(QString::number(id));

  // Should the site be removed as well ?
  QueueObject *site = 0;
  if (transfer->parentObject()->getType() == QueueObject::Site && transfer->parentObject()->getChildrenList().count() == 1)
    site = transfer->parentObject();

  // Signal destruction & delete transfer
  transfer->faceDestruction(abortSession);
  delete transfer;
  
  if (site) {
    delete site;
    emit siteRemoved(sid);
  }

  emit transferRemoved(id);
  
  if (m_emitUpdate)
    emit queueUpdate();
}

void Manager::revalidateTransfer(Transfer *transfer)
{
  QueueObject *i = transfer;
  
  while (i) {
    if (i->parentObject() == topLevelObject())
      break;
      
    i = i->parentObject();
  }
  
  // We have the site
  Site *curSite = static_cast<Site*>(i);
  Site *site = 0;
  
  switch (transfer->getTransferType()) {
    case Download: site = findSite(transfer->getSourceUrl()); break;
    case Upload: site = findSite(transfer->getDestUrl()); break;
    case FXP: site = findSite(transfer->getSourceUrl()); break;
  }
  
  // If the sites don't match, reparent transfer
  if (site != curSite) {
    transfer->parentObject()->delChildObject(transfer);
    transfer->parent()->removeChild(transfer);

    site->insertChild(transfer);
    site->addChildObject(transfer);
    
    emit transferRemoved(transfer->getId());
    emit newTransfer(transfer);
    
    if (curSite->getChildrenList().count() == 0) {
      emit siteRemoved(curSite->getId());
      curSite->deleteLater();
    }
  }
}

void Manager::removeFailedTransfer(FailedTransfer *transfer)
{
  // Remove the transfer and signal removal
  m_failedTransfers.remove(transfer);
  emit failedTransferRemoved(transfer->getTransfer()->getId());
  
  delete transfer;
}

void Manager::clearFailedTransferList()
{
  // Clear the failed transfers list
  FailedTransfer *transfer;
  QPtrListIterator<KFTPQueue::FailedTransfer> i(m_failedTransfers);
  
  while ((transfer = i.current()) != 0) {
    ++i;
    removeFailedTransfer(transfer);
  }
}

void Manager::moveTransferUp(QueueObject *object)
{
  object->parentObject()->moveChildUp(object);

  if (m_emitUpdate)
    emit queueUpdate();
}

void Manager::moveTransferDown(QueueObject *object)
{
  object->parentObject()->moveChildDown(object);

  if (m_emitUpdate)
    emit queueUpdate();
}

void Manager::moveTransferTop(QueueObject *object)
{
  object->parentObject()->moveChildTop(object);

  if (m_emitUpdate)
    emit queueUpdate();
}

void Manager::moveTransferBottom(QueueObject *object)
{
  object->parentObject()->moveChildBottom(object);

  if (m_emitUpdate)
    emit queueUpdate();
}

bool Manager::canBeMovedUp(QueueObject *object)
{
  return object ? object->parentObject()->canMoveChildUp(object) : false;
}

bool Manager::canBeMovedDown(QueueObject *object)
{
  return object ? object->parentObject()->canMoveChildDown(object) : false;
}

void Manager::doEmitUpdate()
{
  m_curDownSpeed = 0;
  m_curUpSpeed = 0;

  topLevelObject()->removeMarkedTransfers();
  
  // Get download/upload speeds
  QueueObject *i;
  QPtrList<QueueObject> sites = topLevelObject()->getChildrenList();
  
  for (i = sites.first(); i; i = sites.next()) {
    QueueObject *t;
    QPtrList<QueueObject> list = i->getChildrenList();
    
    for (t = list.first(); t; t = list.next()) {
      KFTPQueue::Transfer *tmp = static_cast<Transfer*>(t);
      
      switch (tmp->getTransferType()) {
        case Download: m_curDownSpeed += tmp->getSpeed(); break;
        case Upload: m_curUpSpeed += tmp->getSpeed(); break;
        case FXP: {
          m_curDownSpeed += tmp->getSpeed();
          m_curUpSpeed += tmp->getSpeed();
          break;
        }
      }
    }
  }

  // Emit global update to all GUI objects
  emit queueUpdate();
}

void Manager::start()
{
  if (m_processingQueue)
    return;
    
  m_processingQueue = true;
    
  // Now, go trough all queued files and execute them - try to do as little server connects
  // as possible
  m_queueProc->startProcessing();
}

void Manager::abort()
{
  m_processingQueue = false;
  
  // Stop further queue processing
  m_queueProc->stopProcessing();

  emit queueUpdate();
}

void Manager::slotQueueProcessingComplete()
{
  m_processingQueue = false;
  
  // Queue processing is now complete
  if (KFTPCore::Config::showBalloons())
    KFTPWidgets::SystemTray::self()->showBalloon(i18n("All queued transfers have been completed."));

  emit queueUpdate();
}

void Manager::slotQueueProcessingAborted()
{
  m_processingQueue = false;
}

void Manager::clearQueue()
{
  QueueObject *i;
  QPtrList<QueueObject> sites = topLevelObject()->getChildrenList();
  
  for (i = sites.first(); i; i = sites.next()) {
    QueueObject *t;
    QPtrList<QueueObject> list = i->getChildrenList();
    
    for (t = list.first(); t; t = list.next())
      removeTransfer(static_cast<Transfer*>(t));
  }
}

int Manager::getTransferPercentage()
{
  return 0;
}

int Manager::getNumRunning(bool onlyDirs)
{
  int running = 0;
  
  QueueObject *i;
  QPtrList<QueueObject> sites = topLevelObject()->getChildrenList();
  
  for (i = sites.first(); i; i = sites.next()) {
    QueueObject *t;
    QPtrList<QueueObject> list = i->getChildrenList();
    
    for (t = list.first(); t; t = list.next()) {
      if (t->isRunning() && (!onlyDirs || t->isDir()))
        running++;
    }
    
    if (i->isRunning())
      running++;
  }
  
  return running;
}

int Manager::getNumRunning(const KURL &remoteUrl)
{
  int running = 0;
  Site *site = findSite(remoteUrl, true);
  
  if (site) {
    QueueObject *i;
    QPtrList<QueueObject> transfers = site->getChildrenList();
    
    for (i = transfers.first(); i; i = transfers.next()) {
      if (i->isRunning())
        running++;
    }
  }
  
  return running;
}

KFTPEngine::FileExistsWakeupEvent *Manager::fileExistsAction(TransferFile *transfer,
                                                             QValueList<KFTPEngine::DirectoryEntry> stat)
{
  FileExistsWakeupEvent *event = new FileExistsWakeupEvent();
  FileExistsActions *fa = NULL;
  FEAction action;
  
  filesize_t srcSize = 0;
  time_t srcTime = 0;
  
  filesize_t dstSize = 0;
  time_t dstTime = 0;
  
  // Check if there is a default action set
  action = getDefaultFileExistsAction();
  
  if (action == FE_DISABLE_ACT) {
    switch (transfer->getTransferType()) {
      case KFTPQueue::Download: {
        KFileItem info(KFileItem::Unknown, KFileItem::Unknown, transfer->getDestUrl());
        dstSize = info.size();
        dstTime = info.time(KIO::UDS_MODIFICATION_TIME);
        
        srcSize = stat[0].size();
        srcTime = stat[0].time();
        
        fa = KFTPCore::Config::self()->dActions();
        break;
      }
      case KFTPQueue::Upload: {
        KFileItem info(KFileItem::Unknown, KFileItem::Unknown, transfer->getSourceUrl());
        srcSize = info.size();
        srcTime = info.time(KIO::UDS_MODIFICATION_TIME);
        
        dstSize = stat[0].size();
        dstTime = stat[0].time();
        
        fa = KFTPCore::Config::self()->uActions();
        break;
      }
      case KFTPQueue::FXP: {
        srcSize = stat[0].size();
        srcTime = stat[0].time();
        
        dstSize = stat[1].size();
        dstTime = stat[1].time();
        
        fa = KFTPCore::Config::self()->fActions();
        break;
      }
    }
    
    // Now that we have all data, get the action and do it
    action = fa->getActionForSituation(srcSize, srcTime, dstSize, dstTime);
  }
  
  switch (action) {
    default:
    case FE_SKIP_ACT: event->action = FileExistsWakeupEvent::Skip; break;
    case FE_OVERWRITE_ACT: event->action =  FileExistsWakeupEvent::Overwrite; break;
    case FE_RESUME_ACT: event->action =  FileExistsWakeupEvent::Resume; break;
    case FE_RENAME_ACT:
    case FE_USER_ACT: {
      appendUserDialogRequest(new UserDialogRequest(transfer, srcSize, srcTime, dstSize, dstTime));
      
      // Event shall be deferred
      delete event;
      event = 0;
    }
  }
  
  return event;
}

void Manager::appendUserDialogRequest(UserDialogRequest *request)
{
  m_userDialogRequests.append(request);
  
  if (m_userDialogRequests.count() == 1) {
    processUserDialogRequest();
  }
}

void Manager::processUserDialogRequest()
{
  UserDialogRequest *request = m_userDialogRequests.getFirst();
  if (!request)
    return;
  
  FEAction action = getDefaultFileExistsAction();
  FileExistsWakeupEvent *event = new FileExistsWakeupEvent();
  
  if (action == FE_DISABLE_ACT || action == FE_USER_ACT) {
    // A dialog really needs to be displayed
    TransferFile *transfer = request->getTransfer();
    
    QString newDestPath;
    KIO::RenameDlg_Result r = KIO::open_RenameDlg(
      i18n("File Exists"),
      transfer->getSourceUrl().prettyURL(),
      transfer->getDestUrl().prettyURL(),
      (KIO::RenameDlg_Mode) (KIO::M_OVERWRITE | KIO::M_RESUME | KIO::M_SKIP | KIO::M_MULTI),
      newDestPath,
      request->sourceSize(),
      request->destinationSize(),
      request->sourceTime(),
      request->destinationTime()
    );

    switch (r) {
      case KIO::R_RENAME: {
        KURL url = transfer->getDestUrl();
        url.setPath(newDestPath);
        transfer->setDestUrl(url);
        
        event->action = FileExistsWakeupEvent::Rename;
        event->newFileName = newDestPath;
        break;
      }
      case KIO::R_CANCEL: {
        // Abort queue processing
        abort();
        transfer->abort();
        
        // An event is not required, since we will not be recalling the process
        delete event;
        event = 0;
        break;
      }
      case KIO::R_AUTO_SKIP: setDefaultFileExistsAction(FE_SKIP_ACT);
      case KIO::R_SKIP: event->action = FileExistsWakeupEvent::Skip; break;
      case KIO::R_RESUME_ALL: setDefaultFileExistsAction(FE_RESUME_ACT);
      case KIO::R_RESUME: event->action = FileExistsWakeupEvent::Resume; break;
      case KIO::R_OVERWRITE_ALL: setDefaultFileExistsAction(FE_OVERWRITE_ACT);
      default: event->action = FileExistsWakeupEvent::Overwrite; break;
    }
  } else {
    switch (action) {
      default:
      case FE_SKIP_ACT: event->action = FileExistsWakeupEvent::Skip; break;
      case FE_OVERWRITE_ACT: event->action =  FileExistsWakeupEvent::Overwrite; break;
      case FE_RESUME_ACT: event->action =  FileExistsWakeupEvent::Resume; break;
    }
  }
  
  // Send a response to this request
  request->sendResponse(event);
  
  m_userDialogRequests.removeFirst();
  
  if (!m_userDialogRequests.isEmpty())
    processUserDialogRequest();
}

void Manager::openAfterTransfer(TransferFile *transfer)
{
  QString mimeType = KMimeType::findByURL(transfer->getDestUrl(), 0, true, true)->name();
  KService::Ptr offer = KServiceTypeProfile::preferredService(mimeType, "Application");
  
  if (!offer) {
    KOpenWithDlg dialog(KURL::List(transfer->getDestUrl()));
    
    if (dialog.exec() == QDialog::Accepted) {
      offer = dialog.service();
      
      if (!offer)
        offer = new KService("", dialog.text(), "");
    } else {
      return;
    }
  }
  
  QStringList params = KRun::processDesktopExec(*offer, KURL::List(transfer->getDestUrl()), false);
  KProcess *p = new KProcess(this);
  *p << params;
  
  connect(p, SIGNAL(processExited(KProcess*)), this, SLOT(slotEditProcessTerminated(KProcess*)));
  
  p->start();
  
  // Save the process
  m_editProcessList.insert(p->pid(), OpenedFile(transfer));
}

void Manager::slotEditProcessTerminated(KProcess *p)
{
  // A process has terminated, we should reupload
  OpenedFile file = m_editProcessList[p->pid()];
  
  // Only upload a file if it has been changed
  if (file.hasChanged()) {
    TransferFile *transfer = new TransferFile(KFTPQueue::Manager::self());
    transfer->setSourceUrl(file.destination());
    transfer->setDestUrl(file.source());
    transfer->setTransferType(KFTPQueue::Upload);
    transfer->addSize(KFileItem(KFileItem::Unknown, KFileItem::Unknown, file.destination()).size());
    insertTransfer(transfer);
    
    // Execute the transfer
    transfer->delayedExecute();
  }
  
  // Cleanup
  m_editProcessList.remove(p->pid());
  p->deleteLater();
}

}

#include "kftpqueue.moc"
