/* This file is part of the KDE project
   Copyright (C) 2000 Simon Hausmann <hausmann@kde.org>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "konq_undo.h"

#undef Always

#include <tdeio/uiserver_stub.h>
#include "konq_operations.h"

#include <assert.h>

#include <dcopclient.h>
#include <dcopref.h>

#include <tdeapplication.h>
#include <kdatastream.h>
#include <kdebug.h>
#include <tdelocale.h>
#include <tdeglobalsettings.h>
#include <tdeconfig.h>
#include <kipc.h>

#include <tdeio/job.h>
#include <kdirnotify_stub.h>

inline const char *dcopTypeName( const KonqCommand & ) { return "KonqCommand"; }
inline const char *dcopTypeName( const KonqCommand::Stack & ) { return "KonqCommand::Stack"; }

/**
 * checklist:
 * copy dir -> overwrite -> works
 * move dir -> overwrite -> works
 * copy dir -> rename -> works
 * move dir -> rename -> works
 *
 * copy dir -> works
 * move dir -> works
 *
 * copy files -> works
 * move files -> works (TODO: optimize (change FileCopyJob to use the renamed arg for copyingDone)
 *
 * copy files -> overwrite -> works
 * move files -> overwrite -> works
 *
 * copy files -> rename -> works
 * move files -> rename -> works
 */

class KonqUndoJob : public TDEIO::Job
{
public:
    KonqUndoJob() : TDEIO::Job( true ) { KonqUndoManager::incRef(); };
    virtual ~KonqUndoJob() { KonqUndoManager::decRef(); }

    virtual void kill( bool q) { KonqUndoManager::self()->stopUndo( true ); TDEIO::Job::kill( q ); }
};

class KonqCommandRecorder::KonqCommandRecorderPrivate
{
public:
  KonqCommandRecorderPrivate()
  {
  }
  ~KonqCommandRecorderPrivate()
  {
  }

  KonqCommand m_cmd;
};

KonqCommandRecorder::KonqCommandRecorder( KonqCommand::Type op, const KURL::List &src, const KURL &dst, TDEIO::Job *job )
  : TQObject( job, "konqcmdrecorder" )
{
  d = new KonqCommandRecorderPrivate;
  d->m_cmd.m_type = op;
  d->m_cmd.m_valid = true;
  d->m_cmd.m_src = src;
  d->m_cmd.m_dst = dst;
  connect( job, TQT_SIGNAL( result( TDEIO::Job * ) ),
           this, TQT_SLOT( slotResult( TDEIO::Job * ) ) );

  if ( op != KonqCommand::MKDIR ) {
      connect( job, TQT_SIGNAL( copyingDone( TDEIO::Job *, const KURL &, const KURL &, bool, bool ) ),
               this, TQT_SLOT( slotCopyingDone( TDEIO::Job *, const KURL &, const KURL &, bool, bool ) ) );
      connect( job, TQT_SIGNAL( copyingLinkDone( TDEIO::Job *, const KURL &, const TQString &, const KURL & ) ),
               this, TQT_SLOT( slotCopyingLinkDone( TDEIO::Job *, const KURL &, const TQString &, const KURL & ) ) );
  }

  KonqUndoManager::incRef();
}

KonqCommandRecorder::~KonqCommandRecorder()
{
  KonqUndoManager::decRef();
  delete d;
}

void KonqCommandRecorder::slotResult( TDEIO::Job *job )
{
  if ( job->error() )
    return;

  KonqUndoManager::self()->addCommand( d->m_cmd );
}

void KonqCommandRecorder::slotCopyingDone( TDEIO::Job *job, const KURL &from, const KURL &to, bool directory, bool renamed )
{
  KonqBasicOperation op;
  op.m_valid = true;
  op.m_directory = directory;
  op.m_renamed = renamed;
  op.m_src = from;
  op.m_dst = to;
  op.m_link = false;

  if ( d->m_cmd.m_type == KonqCommand::TRASH )
  {
      Q_ASSERT( from.isLocalFile() );
      Q_ASSERT( to.protocol() == "trash" );
      TQMap<TQString, TQString> metaData = job->metaData();
      TQMap<TQString, TQString>::ConstIterator it = metaData.find( "trashURL-" + from.path() );
      if ( it != metaData.end() ) {
          // Update URL
          op.m_dst = it.data();
      }
  }

  d->m_cmd.m_opStack.prepend( op );
}

void KonqCommandRecorder::slotCopyingLinkDone( TDEIO::Job *, const KURL &from, const TQString &target, const KURL &to )
{
  KonqBasicOperation op;
  op.m_valid = true;
  op.m_directory = false;
  op.m_renamed = false;
  op.m_src = from;
  op.m_target = target;
  op.m_dst = to;
  op.m_link = true;
  d->m_cmd.m_opStack.prepend( op );
}

KonqUndoManager *KonqUndoManager::s_self = 0;
unsigned long KonqUndoManager::s_refCnt = 0;

class KonqUndoManager::KonqUndoManagerPrivate
{
public:
  KonqUndoManagerPrivate()
  {
      m_uiserver = new UIServer_stub( "tdeio_uiserver", "UIServer" );
      m_undoJob = 0;
  }
  ~KonqUndoManagerPrivate()
  {
      delete m_uiserver;
  }

  bool m_syncronized;

  KonqCommand::Stack m_commands;

  KonqCommand m_current;
  TDEIO::Job *m_currentJob;
  UndoState m_undoState;
  TQValueStack<KURL> m_dirStack;
  TQValueStack<KURL> m_dirCleanupStack;
  TQValueStack<KURL> m_fileCleanupStack;
  TQValueList<KURL> m_dirsToUpdate;

  bool m_lock;

  UIServer_stub *m_uiserver;
  int m_uiserverJobId;

  KonqUndoJob *m_undoJob;
};

KonqUndoManager::KonqUndoManager()
: DCOPObject( "KonqUndoManager" )
{
  if ( !kapp->dcopClient()->isAttached() )
      kapp->dcopClient()->attach();

  d = new KonqUndoManagerPrivate;
  d->m_syncronized = initializeFromKDesky();
  d->m_lock = false;
  d->m_currentJob = 0;
}

KonqUndoManager::~KonqUndoManager()
{
  delete d;
}

void KonqUndoManager::incRef()
{
  s_refCnt++;
}

void KonqUndoManager::decRef()
{
  s_refCnt--;
  if ( s_refCnt == 0 && s_self )
  {
    delete s_self;
    s_self = 0;
  }
}

KonqUndoManager *KonqUndoManager::self()
{
  if ( !s_self )
  {
    if ( s_refCnt == 0 )
      s_refCnt++; // someone forgot to call incRef
    s_self = new KonqUndoManager;
  }
  return s_self;
}

void KonqUndoManager::addCommand( const KonqCommand &cmd )
{
  broadcastPush( cmd );
}

bool KonqUndoManager::undoAvailable() const
{
  return ( d->m_commands.count() > 0 ) && !d->m_lock;
}

TQString KonqUndoManager::undoText() const
{
  if ( d->m_commands.count() == 0 )
    return i18n( "Und&o" );

  KonqCommand::Type t = d->m_commands.top().m_type;
  if ( t == KonqCommand::COPY )
    return i18n( "Und&o: Copy" );
  else if ( t == KonqCommand::LINK )
    return i18n( "Und&o: Link" );
  else if ( t == KonqCommand::MOVE )
    return i18n( "Und&o: Move" );
  else if ( t == KonqCommand::TRASH )
    return i18n( "Und&o: Trash" );
  else if ( t == KonqCommand::MKDIR )
    return i18n( "Und&o: Create Folder" );
  else
    assert( false );
  /* NOTREACHED */
  return TQString::null;
}

void KonqUndoManager::undo()
{
  KonqCommand cmd = d->m_commands.top();
  assert( cmd.m_valid );

  d->m_current = cmd;

  TQValueList<KonqBasicOperation>& opStack = d->m_current.m_opStack;

  // Let's first ask for confirmation if we need to delete any file (#99898)
  KURL::List fileCleanupStack;
  TQValueList<KonqBasicOperation>::Iterator it = opStack.begin();
  for ( ; it != opStack.end() ; ++it ) {
      if ( !(*it).m_directory && !(*it).m_link && d->m_current.m_type == KonqCommand::COPY ) {
          fileCleanupStack.append( (*it).m_dst );
      }
  }
  if ( !fileCleanupStack.isEmpty() ) {
      // Because undo can happen with an accidental Ctrl-Z, we want to always confirm.
      if ( !KonqOperations::askDeleteConfirmation( fileCleanupStack, KonqOperations::DEL,
                                                   KonqOperations::FORCE_CONFIRMATION,
                                                   0 /* TODO parent */ ) )
          return;
  }

  d->m_dirCleanupStack.clear();
  d->m_dirStack.clear();
  d->m_dirsToUpdate.clear();

  d->m_undoState = MOVINGFILES;

  broadcastPop();
  broadcastLock();

  it = opStack.begin();
  TQValueList<KonqBasicOperation>::Iterator end = opStack.end();
  while ( it != end )
  {
    if ( (*it).m_directory && !(*it).m_renamed )
    {
      d->m_dirStack.push( (*it).m_src );
      d->m_dirCleanupStack.prepend( (*it).m_dst );
      it = d->m_current.m_opStack.remove( it );
      d->m_undoState = MAKINGDIRS;
      kdDebug(1203) << "KonqUndoManager::undo MAKINGDIRS" << endl;
    }
    else if ( (*it).m_link )
    {
      if ( !d->m_fileCleanupStack.contains( (*it).m_dst ) )
        d->m_fileCleanupStack.prepend( (*it).m_dst );

      if ( d->m_current.m_type != KonqCommand::MOVE )
        it = d->m_current.m_opStack.remove( it );
      else
        ++it;
    }
    else
      ++it;
  }

  /* this shouldn't be necessary at all:
   * 1) the source list may contain files, we don't want to
   *    create those as... directories
   * 2) all directories that need creation should already be in the
   *    directory stack
  if ( d->m_undoState == MAKINGDIRS )
  {
    KURL::List::ConstIterator it = d->m_current.m_src.begin();
    KURL::List::ConstIterator end = d->m_current.m_src.end();
    for (; it != end; ++it )
      if ( !d->m_dirStack.contains( *it) )
        d->m_dirStack.push( *it );
  }
  */

  if ( d->m_current.m_type != KonqCommand::MOVE )
    d->m_dirStack.clear();

  d->m_undoJob = new KonqUndoJob;
  d->m_uiserverJobId = d->m_undoJob->progressId();
  undoStep();
}

void KonqUndoManager::stopUndo( bool step )
{
    d->m_current.m_opStack.clear();
    d->m_dirCleanupStack.clear();
    d->m_fileCleanupStack.clear();
    d->m_undoState = REMOVINGDIRS;
    d->m_undoJob = 0;

    if ( d->m_currentJob )
        d->m_currentJob->kill( true );

    d->m_currentJob = 0;

    if ( step )
        undoStep();
}

void KonqUndoManager::slotResult( TDEIO::Job *job )
{
  d->m_uiserver->jobFinished( d->m_uiserverJobId );
  if ( job->error() )
  {
    job->showErrorDialog( 0L );
    d->m_currentJob = 0;
    stopUndo( false );
    if ( d->m_undoJob )
    {
        delete d->m_undoJob;
        d->m_undoJob = 0;
    }
  }

  undoStep();
}


void KonqUndoManager::addDirToUpdate( const KURL& url )
{
  if ( d->m_dirsToUpdate.find( url ) == d->m_dirsToUpdate.end() )
    d->m_dirsToUpdate.prepend( url );
}

void KonqUndoManager::undoStep()
{
  d->m_currentJob = 0;

  if ( d->m_undoState == MAKINGDIRS )
      undoMakingDirectories();

  if ( d->m_undoState == MOVINGFILES )
      undoMovingFiles();

  if ( d->m_undoState == REMOVINGFILES )
      undoRemovingFiles();

  if ( d->m_undoState == REMOVINGDIRS )
      undoRemovingDirectories();

  if ( d->m_currentJob )
    connect( d->m_currentJob, TQT_SIGNAL( result( TDEIO::Job * ) ),
             this, TQT_SLOT( slotResult( TDEIO::Job * ) ) );
}

void KonqUndoManager::undoMakingDirectories()
{
    if ( !d->m_dirStack.isEmpty() ) {
      KURL dir = d->m_dirStack.pop();
      kdDebug(1203) << "KonqUndoManager::undoStep creatingDir " << dir.prettyURL() << endl;
      d->m_currentJob = TDEIO::mkdir( dir );
      d->m_uiserver->creatingDir( d->m_uiserverJobId, dir );
    }
    else
      d->m_undoState = MOVINGFILES;
}

void KonqUndoManager::undoMovingFiles()
{
    if ( !d->m_current.m_opStack.isEmpty() )
    {
      KonqBasicOperation op = d->m_current.m_opStack.pop();

      assert( op.m_valid );
      if ( op.m_directory )
      {
        if ( op.m_renamed )
        {
          kdDebug(1203) << "KonqUndoManager::undoStep rename " << op.m_dst.prettyURL() << " " << op.m_src.prettyURL() << endl;
          d->m_currentJob = TDEIO::rename( op.m_dst, op.m_src, false );
          d->m_uiserver->moving( d->m_uiserverJobId, op.m_dst, op.m_src );
        }
        else
          assert( 0 ); // this should not happen!
      }
      else if ( op.m_link )
      {
        kdDebug(1203) << "KonqUndoManager::undoStep symlink " << op.m_target << " " << op.m_src.prettyURL() << endl;
        d->m_currentJob = TDEIO::symlink( op.m_target, op.m_src, true, false );
      }
      else if ( d->m_current.m_type == KonqCommand::COPY )
      {
        kdDebug(1203) << "KonqUndoManager::undoStep file_delete " << op.m_dst.prettyURL() << endl;
        d->m_currentJob = TDEIO::file_delete( op.m_dst );
        d->m_uiserver->deleting( d->m_uiserverJobId, op.m_dst );
      }
      else if ( d->m_current.m_type == KonqCommand::MOVE
                || d->m_current.m_type == KonqCommand::TRASH )
      {
        kdDebug(1203) << "KonqUndoManager::undoStep file_move " << op.m_dst.prettyURL() << " " << op.m_src.prettyURL() << endl;
        d->m_currentJob = TDEIO::file_move( op.m_dst, op.m_src, -1, true );
        d->m_uiserver->moving( d->m_uiserverJobId, op.m_dst, op.m_src );
      }

      // The above KIO jobs are lowlevel, they don't trigger KDirNotify notification
      // So we need to do it ourselves (but schedule it to the end of the undo, to compress them)
      KURL url( op.m_dst );
      url.setPath( url.directory() );
      addDirToUpdate( url );

      url = op.m_src;
      url.setPath( url.directory() );
      addDirToUpdate( url );
    }
    else
      d->m_undoState = REMOVINGFILES;
}

void KonqUndoManager::undoRemovingFiles()
{
    kdDebug(1203) << "KonqUndoManager::undoStep REMOVINGFILES" << endl;
    if ( !d->m_fileCleanupStack.isEmpty() )
    {
      KURL file = d->m_fileCleanupStack.pop();
      kdDebug(1203) << "KonqUndoManager::undoStep file_delete " << file.prettyURL() << endl;
      d->m_currentJob = TDEIO::file_delete( file );
      d->m_uiserver->deleting( d->m_uiserverJobId, file );

      KURL url( file );
      url.setPath( url.directory() );
      addDirToUpdate( url );
    }
    else
    {
      d->m_undoState = REMOVINGDIRS;

      if ( d->m_dirCleanupStack.isEmpty() && d->m_current.m_type == KonqCommand::MKDIR )
        d->m_dirCleanupStack << d->m_current.m_dst;
    }
}

void KonqUndoManager::undoRemovingDirectories()
{
    if ( !d->m_dirCleanupStack.isEmpty() )
    {
      KURL dir = d->m_dirCleanupStack.pop();
      kdDebug(1203) << "KonqUndoManager::undoStep rmdir " << dir.prettyURL() << endl;
      d->m_currentJob = TDEIO::rmdir( dir );
      d->m_uiserver->deleting( d->m_uiserverJobId, dir );
      addDirToUpdate( dir );
    }
    else
    {
      d->m_current.m_valid = false;
      d->m_currentJob = 0;
      if ( d->m_undoJob )
      {
          kdDebug(1203) << "KonqUndoManager::undoStep deleting undojob" << endl;
          d->m_uiserver->jobFinished( d->m_uiserverJobId );
          delete d->m_undoJob;
          d->m_undoJob = 0;
      }
      KDirNotify_stub allDirNotify( "*", "KDirNotify*" );
      TQValueList<KURL>::ConstIterator it = d->m_dirsToUpdate.begin();
      for( ; it != d->m_dirsToUpdate.end(); ++it ) {
          kdDebug() << "Notifying FilesAdded for " << *it << endl;
          allDirNotify.FilesAdded( *it );
      }
      broadcastUnlock();
    }
}

void KonqUndoManager::push( const KonqCommand &cmd )
{
  d->m_commands.push( cmd );
  emit undoAvailable( true );
  emit undoTextChanged( undoText() );
}

void KonqUndoManager::pop()
{
  d->m_commands.pop();
  emit undoAvailable( undoAvailable() );
  emit undoTextChanged( undoText() );
}

void KonqUndoManager::lock()
{
//  assert( !d->m_lock );
  d->m_lock = true;
  emit undoAvailable( undoAvailable() );
}

void KonqUndoManager::unlock()
{
//  assert( d->m_lock );
  d->m_lock = false;
  emit undoAvailable( undoAvailable() );
}

KonqCommand::Stack KonqUndoManager::get() const
{
  return d->m_commands;
}

void KonqUndoManager::broadcastPush( const KonqCommand &cmd )
{
  if ( !d->m_syncronized )
  {
    push( cmd );
    return;
  }

  DCOPRef( "kdesktop", "KonqUndoManager" ).send( "push", cmd );
  DCOPRef( "konqueror*", "KonqUndoManager" ).send( "push", cmd );
}

void KonqUndoManager::broadcastPop()
{
  if ( !d->m_syncronized )
  {
    pop();
    return;
  }
  DCOPRef( "kdesktop", "KonqUndoManager" ).send( "pop" );
  DCOPRef( "konqueror*", "KonqUndoManager" ).send( "pop" );
}

void KonqUndoManager::broadcastLock()
{
//  assert( !d->m_lock );

  if ( !d->m_syncronized )
  {
    lock();
    return;
  }
  DCOPRef( "kdesktop", "KonqUndoManager" ).send( "lock" );
  DCOPRef( "konqueror*", "KonqUndoManager" ).send( "lock" );
}

void KonqUndoManager::broadcastUnlock()
{
//  assert( d->m_lock );

  if ( !d->m_syncronized )
  {
    unlock();
    return;
  }
  DCOPRef( "kdesktop", "KonqUndoManager" ).send( "unlock" );
  DCOPRef( "konqueror*", "KonqUndoManager" ).send( "unlock" );
}

bool KonqUndoManager::initializeFromKDesky()
{
  // ### workaround for dcop problem and upcoming 2.1 release:
  // in case of huge io operations the amount of data sent over
  // dcop (containing undo information broadcasted for global undo
  // to all konqueror instances) can easily exceed the 64kb limit
  // of dcop. In order not to run into trouble we disable global
  // undo for now! (Simon)
  // ### FIXME: post 2.1
  return false;

  DCOPClient *client = kapp->dcopClient();

  if ( client->appId() == "kdesktop" ) // we are master :)
    return true;

  if ( !client->isApplicationRegistered( "kdesktop" ) )
    return false;

  d->m_commands = DCOPRef( "kdesktop", "KonqUndoManager" ).call( "get" );
  return true;
}

TQDataStream &operator<<( TQDataStream &stream, const KonqBasicOperation &op )
{
    stream << op.m_valid << op.m_directory << op.m_renamed << op.m_link
           << op.m_src << op.m_dst << op.m_target;
  return stream;
}
TQDataStream &operator>>( TQDataStream &stream, KonqBasicOperation &op )
{
  stream >> op.m_valid >> op.m_directory >> op.m_renamed >> op.m_link
         >> op.m_src >> op.m_dst >> op.m_target;
  return stream;
}

TQDataStream &operator<<( TQDataStream &stream, const KonqCommand &cmd )
{
  stream << cmd.m_valid << (TQ_INT8)cmd.m_type << cmd.m_opStack << cmd.m_src << cmd.m_dst;
  return stream;
}

TQDataStream &operator>>( TQDataStream &stream, KonqCommand &cmd )
{
  TQ_INT8 type;
  stream >> cmd.m_valid >> type >> cmd.m_opStack >> cmd.m_src >> cmd.m_dst;
  cmd.m_type = static_cast<KonqCommand::Type>( type );
  return stream;
}

#include "konq_undo.moc"