/*
 *
 *    soniK digital audio editor
 *    Copyright (C) 2004-2006  Robert Walker <rob@tenfoot.org.uk>
 *
 *    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 WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  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 Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "actionmanager.h"

using Sonik::Action;
using Sonik::ActionManager;

#undef DEBUG_ACTIONS

namespace
{
  static const size_t kMaxActions = 20;

  class CompoundAction : public Action
  {
  public:
    CompoundAction(ActionManager& actionManager, const QString& name)
      : Action(name), mActionManager(actionManager) { }
    virtual ~CompoundAction();

    Sonik::ActionSequence* actions() { return &mActions; }

    virtual void apply();

    QString debugPrint(int indent);

  private:
    Sonik::ActionManager&  mActionManager;
    Sonik::ActionSequence  mActions;
  };


  void clearActionSequence(Sonik::ActionSequence& seq)
  {
    Sonik::ActionSequence::const_iterator it = seq.begin();
    Sonik::ActionSequence::const_iterator e  = seq.end();
    for ( ; it != e; ++it)
      delete (*it);
    seq.clear();
  }

#ifdef DEBUG_ACTIONS
  void dumpActions(const Sonik::ActionSequence& undo,
                   const Sonik::ActionSequence& redo)
  {
    // debug printing
    QString dbg;
    Sonik::ActionSequence::const_iterator it;

    dbg += "Undo: \n";
    for (it = undo.begin(); it != undo.end(); ++it)
      dbg += (*it)->debugPrint(2) + "\n";

    dbg += "Redo: \n";
    for (it = redo.begin(); it != redo.end(); ++it)
      dbg += (*it)->debugPrint(2) + "\n";

    kdDebug(60606) << "ActionManager:: " << dbg << "\n";
  }
#else
  void dumpActions(const Sonik::ActionSequence& /*undo*/,
                   const Sonik::ActionSequence& /*redo*/)
  {
  }
#endif
}


Action::Action(const QString& name)
  : mName(name)
{
}

Action::~Action ()
{
}

QString Action::debugPrint(int indent)
{
  QString str = "";
  while (indent--)
    str += " ";
  return str;
}

CompoundAction::~CompoundAction()
{
  clearActionSequence(mActions);
}

void CompoundAction::apply()
{
  mActionManager.beginCompoundAction(mName);

  while (!mActions.empty())
  {
    Action *action = mActions.front();
    mActions.pop_front();

    action->apply();

    delete action;
  }

  mActionManager.endCompoundAction();
}

QString CompoundAction::debugPrint(int indent)
{
  QString ind = Action::debugPrint(indent);
  QString str = ind + "Compound action: " + name() + "\n";

  Sonik::ActionSequence::const_iterator it = mActions.begin();
  Sonik::ActionSequence::const_iterator e  = mActions.end();
  for ( ; it != e; ++it)
    str += (*it)->debugPrint(indent + 2) + "\n";

  return str;
}

ActionManager::ActionManager()
  : mState(DISABLED)
{
  mActionSequences.push_front(&mUndoActions);
}

ActionManager::~ActionManager()
{
  clear();
}

void ActionManager::recordAction(Action *action)
{
  if (mState == DISABLED || mState == ABORTING)
  {
    delete action;
    return;
  }

  mActionSequences.front()->push_front(action);

  if (mState == NORMAL && mActionSequences.size() == 1)
  {
    // free older actions
    // (undo only: redo stack is implicity limited by size of undo stack)
    while (mUndoActions.size() > kMaxActions)
    {
      delete mUndoActions.back();
      mUndoActions.pop_back();
    }

    clearActionSequence(mRedoActions);

    emit undoChanged(action->name());
    emit redoChanged(QString::null);
  }
}

void ActionManager::undo()
{
  if (mUndoActions.empty())
    return;

  Action *action = mUndoActions.front();
  mUndoActions.pop_front();

  mActionSequences.push_front(&mRedoActions);
  mState = UNDOING;
  action->apply();
  mState = NORMAL;
  mActionSequences.pop_front();

  delete action;

  if (mUndoActions.empty())
    emit undoChanged(QString::null);
  else
    emit undoChanged(mUndoActions.front()->name());

  if (mRedoActions.empty())
    emit redoChanged(QString::null);
  else
    emit redoChanged(mRedoActions.front()->name());

  dumpActions(mUndoActions, mRedoActions);
}

void ActionManager::redo()
{
  if (mRedoActions.empty())
    return;

  Action *action = mRedoActions.front();
  mRedoActions.pop_front();

  mState = REDOING;
  action->apply();
  mState = NORMAL;

  delete action;

  if (mUndoActions.empty())
    emit undoChanged(QString::null);
  else
    emit undoChanged(mUndoActions.front()->name());

  if (mRedoActions.empty())
    emit redoChanged(QString::null);
  else
    emit redoChanged(mRedoActions.front()->name());

  dumpActions(mUndoActions, mRedoActions);
}

void ActionManager::abort()
{
  if (mUndoActions.empty())
    return;

  Action *action = mUndoActions.front();
  mUndoActions.pop_front();

  mState = ABORTING;
  action->apply();
  mState = NORMAL;

  delete action;

  if (mUndoActions.empty())
    emit undoChanged(QString::null);
  else
    emit undoChanged(mUndoActions.front()->name());

  dumpActions(mUndoActions, mRedoActions);
}

void ActionManager::setEnabled(bool enabled)
{
  // Don't change state when undoing/redoing (shouldn't happen)
  if (mState == DISABLED && enabled)
    mState = NORMAL;
  else if (mState == NORMAL && !enabled)
    mState = DISABLED;
}

void ActionManager::beginCompoundAction(const QString& name)
{
  CompoundAction *action = new CompoundAction(*this, name);
  recordAction(action);
  mActionSequences.push_front(action->actions());
}

void ActionManager::endCompoundAction()
{
  mActionSequences.pop_front();
}

void ActionManager::abortCompoundAction()
{
  endCompoundAction();
  abort();
}

void ActionManager::clear()
{
  clearActionSequence(mUndoActions);
  clearActionSequence(mRedoActions);

  emit undoChanged(QString::null);
  emit redoChanged(QString::null);
}

