/***************************************************************************
       SignalManager.cpp -  manager class for multi channel signals
			     -------------------
    begin                : Sun Oct 15 2000
    copyright            : (C) 2000 by Thomas Eschenbacher
    email                : Thomas.Eschenbacher@gmx.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "config.h"

#include <errno.h>
#include <math.h>

#include <new>

#include <QApplication>
#include <QByteArray>
#include <QCursor>
#include <QDate>
#include <QFile>
#include <QFileInfo>
#include <QMutableListIterator>
#include <QMutexLocker>
#include <QUrl>
#include <QVector>

#include <KAboutData>
#include <KLocalizedString>
#include <kxmlgui_version.h>

#include "libkwave/ClipBoard.h"
#include "libkwave/CodecManager.h"
#include "libkwave/Decoder.h"
#include "libkwave/Encoder.h"
#include "libkwave/FileProgress.h"
#include "libkwave/InsertMode.h"
#include "libkwave/LabelList.h"
#include "libkwave/MessageBox.h"
#include "libkwave/MultiTrackReader.h"
#include "libkwave/MultiTrackWriter.h"
#include "libkwave/Parser.h"
#include "libkwave/Sample.h"
#include "libkwave/Signal.h"
#include "libkwave/SignalManager.h"
#include "libkwave/String.h"
#include "libkwave/Track.h"
#include "libkwave/Utils.h"
#include "libkwave/Writer.h"
#include "libkwave/undo/UndoAction.h"
#include "libkwave/undo/UndoAddMetaDataAction.h"
#include "libkwave/undo/UndoDeleteAction.h"
#include "libkwave/undo/UndoDeleteMetaDataAction.h"
#include "libkwave/undo/UndoDeleteTrack.h"
#include "libkwave/undo/UndoInsertAction.h"
#include "libkwave/undo/UndoInsertTrack.h"
#include "libkwave/undo/UndoModifyAction.h"
#include "libkwave/undo/UndoModifyMetaDataAction.h"
#include "libkwave/undo/UndoSelection.h"
#include "libkwave/undo/UndoTransaction.h"
#include "libkwave/undo/UndoTransactionGuard.h"

#define CASE_COMMAND(x) } else if (parser.command() == _(x)) {

//***************************************************************************
Kwave::SignalManager::SignalManager(QWidget *parent)
    :QObject(),
    m_parent_widget(parent),
    m_closed(true),
    m_empty(true),
    m_modified(false),
    m_modified_enabled(true),
    m_signal(),
    m_selection(0,0),
    m_last_selection(0,0),
    m_last_track_selection(),
    m_last_length(0),
    m_playback_controller(*this),
    m_undo_enabled(false),
    m_undo_buffer(),
    m_redo_buffer(),
    m_undo_transaction(Q_NULLPTR),
    m_undo_transaction_level(0),
    m_undo_transaction_lock(QMutex::Recursive),
    m_meta_data()
{
    // connect to the track's signals
    Kwave::Signal *sig = &m_signal;
    connect(sig, SIGNAL(sigTrackInserted(uint,Kwave::Track*)),
            this, SLOT(slotTrackInserted(uint,Kwave::Track*)));
    connect(sig, SIGNAL(sigTrackDeleted(uint,Kwave::Track*)),
            this, SLOT(slotTrackDeleted(uint,Kwave::Track*)));
    connect(sig, SIGNAL(sigTrackSelectionChanged(bool)),
            this,SIGNAL(sigTrackSelectionChanged(bool)));
    connect(sig, SIGNAL(sigSamplesDeleted(unsigned int, sample_index_t,
            sample_index_t)),
            this, SLOT(slotSamplesDeleted(unsigned int, sample_index_t,
            sample_index_t)));
    connect(sig, SIGNAL(sigSamplesInserted(unsigned int, sample_index_t,
            sample_index_t)),
            this, SLOT(slotSamplesInserted(unsigned int, sample_index_t,
            sample_index_t)));
    connect(sig, SIGNAL(sigSamplesModified(unsigned int, sample_index_t,
            sample_index_t)),
            this, SLOT(slotSamplesModified(unsigned int, sample_index_t,
            sample_index_t)));
}

//***************************************************************************
Kwave::SignalManager::~SignalManager()
{
    close();
}

//***************************************************************************
int Kwave::SignalManager::loadFile(const QUrl &url)
{
    int res = 0;
    Kwave::FileProgress *dialog = Q_NULLPTR;

    // take over the new file name, so that we have a valid signal
    // name during loading
    QString filename = url.path();
    QFile src(filename);
    QFileInfo fi(src);
    {
	Kwave::FileInfo info(m_meta_data);
	info.set(Kwave::INF_FILENAME, fi.absoluteFilePath());
	m_meta_data.replace(Kwave::MetaDataList(info));
    }

    // work with a copy of meta data, to avoid flicker effects
    Kwave::MetaDataList meta_data(m_meta_data);

    // enter and stay in not modified state
    enableModifiedChange(true);
    setModified(false);
    enableModifiedChange(false);

    // disable undo (discards all undo/redo data)
    disableUndo();

    QString mimetype = Kwave::CodecManager::mimeTypeOf(url);
    qDebug("SignalManager::loadFile(%s) - [%s]",
           DBG(url.toDisplayString()), DBG(mimetype));
    Kwave::Decoder *decoder = Kwave::CodecManager::decoder(mimetype);
    while (decoder) {
	// be sure that the current signal is really closed
	m_signal.close();

	// open the source file
	if (!(res = decoder->open(m_parent_widget, src))) {
	    qWarning("unable to open source: '%s'", DBG(url.toDisplayString()));
	    res = -EIO;
	    break;
	}

	// get the initial meta data from the decoder
	meta_data = decoder->metaData();
	Kwave::FileInfo info(meta_data);

	// take the preliminary meta data, needed for estimated length
	m_meta_data = meta_data;

	// detect stream mode. if so, use one sample as display
	bool streaming = (!info.length());

	// we must change to open state to see the file while
	// it is loaded
	m_closed = false;
	m_empty = false;

	// create all tracks (empty)
	unsigned int track;
	const unsigned int tracks   = info.tracks();
	const sample_index_t length = info.length();
	Q_ASSERT(tracks);
	if (!tracks) break;

	for (track = 0; track < tracks; ++track) {
            Kwave::Track *t = m_signal.appendTrack(length, Q_NULLPTR);
	    Q_ASSERT(t);
	    if (!t || (t->length() != length)) {
		qWarning("SignalManager::loadFile: out of memory");
		res = -ENOMEM;
		break;
	    }
	}
	if (track < tracks) break;

	// create the multitrack writer as destination
	// if length was zero -> append mode / decode a stream ?
	Kwave::InsertMode mode = (streaming) ? Kwave::Append : Kwave::Overwrite;
	Kwave::MultiTrackWriter writers(*this, allTracks(), mode, 0,
	    (length) ? length-1 : 0);

	// try to calculate the resulting length, but if this is
	// not possible, we try to use the source length instead
	quint64 resulting_size = info.tracks() * info.length() *
	                              (info.bits() >> 3);
	bool use_src_size = (!resulting_size);
	if (use_src_size) resulting_size = src.size();

	// prepare and show the progress dialog
	dialog = new(std::nothrow) Kwave::FileProgress(m_parent_widget,
	    QUrl(filename), resulting_size,
	    info.length(), info.rate(), info.bits(), info.tracks());
	Q_ASSERT(dialog);

	if (dialog && use_src_size) {
	    // use source size for progress / stream mode
	    QObject::connect(decoder, SIGNAL(sourceProcessed(quint64)),
	                     dialog,  SLOT(setBytePosition(quint64)));
	    QObject::connect(&writers, SIGNAL(written(quint64)),
	                     dialog,   SLOT(setLength(quint64)));
	} else {
	    // use resulting size percentage for progress
	    QObject::connect(&writers, SIGNAL(progress(qreal)),
	                     dialog,   SLOT(setValue(qreal)));
	}
	QObject::connect(dialog,   SIGNAL(canceled()),
	                 &writers, SLOT(cancel()));

	// now decode
	res = 0;
	if (!decoder->decode(m_parent_widget, writers)) {
	    qWarning("decoding failed.");
	    res = -EIO;
	} else {
	    // read information back from the decoder, some settings
	    // might have become available during the decoding process
	    meta_data = decoder->metaData();
	    info = Kwave::FileInfo(meta_data);
	}

	decoder->close();

	// check for length info in stream mode
	if (!res && streaming) {
	    // source was opened in stream mode -> now we have the length
	    writers.flush();
	    sample_index_t new_length = writers.last();
	    if (new_length) new_length++;
	    info.setLength(new_length);
	} else {
	    info.setLength(this->length());
	    info.setTracks(tracks);
	}

	// enter the filename/mimetype and size into the file info
	info.set(Kwave::INF_FILENAME, fi.absoluteFilePath());
	info.set(Kwave::INF_FILESIZE, src.size());
	if (!info.contains(Kwave::INF_MIMETYPE))
	    info.set(Kwave::INF_MIMETYPE, mimetype);

	// remove the estimated length again, it is no longer needed
	info.set(Kwave::INF_ESTIMATED_LENGTH, QVariant());

	// take over the decoded and updated file info
	meta_data.replace(Kwave::MetaDataList(info));
	m_meta_data = meta_data;

	// update the length info in the progress dialog if needed
	if (dialog && use_src_size) {
	    dialog->setLength(
		quint64(info.length()) *
		quint64(info.tracks()));
	    dialog->setBytePosition(src.size());
	}

	break;
    }

    if (!decoder) {
	qWarning("unknown file type");
	res = -EINVAL;
    } else {
	delete decoder;
    }

    // process any queued events of the writers, like "sigSamplesInserted"
    qApp->processEvents(QEventLoop::ExcludeUserInputEvents);

    // remember the last length and selection
    m_last_length = length();
    rememberCurrentSelection();

    // from now on, undo is enabled
    enableUndo();

    // modified can change from now on
    enableModifiedChange(true);

    if (dialog) delete dialog;
    if (res) close();

    m_meta_data.dump();

    // we now have new meta data
    emit sigMetaDataChanged(m_meta_data);

    return res;
}

//***************************************************************************
int Kwave::SignalManager::save(const QUrl &url, bool selection)
{
    int res = 0;
    sample_index_t ofs  = 0;
    sample_index_t len  = length();
    unsigned int tracks = this->tracks();
    unsigned int bits   = this->bits();

    if (selection) {
	// zero-length -> nothing to do
	ofs = m_selection.offset();
	len = m_selection.length();
	tracks = selectedTracks().count();
    }

    if (!tracks || !len) {
	Kwave::MessageBox::error(m_parent_widget,
	    i18n("Signal is empty, nothing to save."));
	return 0;
    }

    QString mimetype_name;
    mimetype_name = Kwave::CodecManager::mimeTypeOf(url);
    qDebug("SignalManager::save(%s) - [%s] (%d bit, selection=%d)",
	DBG(url.toDisplayString()), DBG(mimetype_name), bits, selection);

    Kwave::Encoder *encoder = Kwave::CodecManager::encoder(mimetype_name);
    Kwave::FileInfo file_info(m_meta_data);
    if (encoder) {

	// maybe we now have a new mime type
	file_info.set(Kwave::INF_MIMETYPE, mimetype_name);

	// check if we lose information and ask the user if this would
	// be acceptable
	QList<Kwave::FileProperty> unsupported = encoder->unsupportedProperties(
	    file_info.properties().keys());
	if (!unsupported.isEmpty()) {
	    QString list_of_lost_properties = _("\n");
	    foreach (const Kwave::FileProperty &p, unsupported) {
		list_of_lost_properties +=
		    i18n(UTF8(file_info.name(p))) + _("\n");
	    }

	    // show a warning to the user and ask him if he wants to continue
	    if (Kwave::MessageBox::warningContinueCancel(m_parent_widget,
		i18n("Saving in this format will lose the following "
		     "additional file attribute(s):\n"
		     "%1\n"
		     "Do you still want to continue?",
		     list_of_lost_properties),
		QString(),
		QString(),
		QString(),
		_("accept_lose_attributes_on_export")
		) != KMessageBox::Continue)
	    {
		delete encoder;
		return -1;
	    }
	}

	// open the destination file
	QString filename = url.path();
	QFile dst(filename);

	Kwave::MultiTrackReader src(Kwave::SinglePassForward, *this,
	    (selection) ? selectedTracks() : allTracks(),
	    ofs, ofs + len - 1);

	// update the file information
	file_info.setLength(len);
	file_info.setRate(rate());
	file_info.setBits(bits);
	file_info.setTracks(tracks);

	if (!file_info.contains(Kwave::INF_SOFTWARE) &&
	    encoder->supportedProperties().contains(Kwave::INF_SOFTWARE))
	{
	    // add our Kwave Software tag
	    const KAboutData about_data = KAboutData::applicationData();
	    QString software = about_data.displayName() + _("-") +
	                       about_data.version() +
	                       i18n("(built with KDE Frameworks %1)",
	                            _(KXMLGUI_VERSION_STRING));
	    file_info.set(Kwave::INF_SOFTWARE, software);
	}

	if (!file_info.contains(Kwave::INF_CREATION_DATE) &&
	    encoder->supportedProperties().contains(Kwave::INF_CREATION_DATE))
	{
	    // add a date tag
	    QString date(QDate::currentDate().toString(_("yyyy-MM-dd")));
	    qDebug("adding date tag: '%s'", DBG(date));
	    file_info.set(Kwave::INF_CREATION_DATE, date);
	}

	// prepare and show the progress dialog
	Kwave::FileProgress *dialog = new(std::nothrow)
	    Kwave::FileProgress(m_parent_widget, QUrl(filename),
	        file_info.length() * file_info.tracks() *
	        (file_info.bits() >> 3),
	        file_info.length(), file_info.rate(), file_info.bits(),
	        file_info.tracks()
	    );
	Q_ASSERT(dialog);
	QObject::connect(&src,   SIGNAL(progress(qreal)),
	                 dialog, SLOT(setValue(qreal)),
	                 Qt::QueuedConnection);
	QObject::connect(dialog, SIGNAL(canceled()),
	                 &src,   SLOT(cancel()));

	// invoke the encoder...
	bool encoded = false;
	m_meta_data.replace(Kwave::MetaDataList(file_info));

	if (selection) {
	    // use a copy, don't touch the original !
	    Kwave::MetaDataList meta = m_meta_data;

	    // we have to adjust all position aware meta data
	    meta.cropByRange(ofs, ofs + len - 1);

	    // set the filename in the copy of the fileinfo, the original
	    // file which is currently open keeps it's name
	    Kwave::FileInfo info(meta);
	    info.set(Kwave::INF_FILENAME, filename);
	    meta.replace(Kwave::MetaDataList(info));

	    encoded = encoder->encode(m_parent_widget, src, dst, meta);
	} else {
	    // in case of a "save as" -> modify the current filename
	    file_info.set(Kwave::INF_FILENAME, filename);
	    m_meta_data.replace(Kwave::MetaDataList(file_info));
	    encoded = encoder->encode(m_parent_widget, src, dst, m_meta_data);
	}
	if (!encoded) {
	    Kwave::MessageBox::error(m_parent_widget,
	        i18n("An error occurred while saving the file."));
	    res = -1;
	}

	delete encoder;
        encoder = Q_NULLPTR;

	if (dialog) {
	    qApp->processEvents();
	    if (dialog->isCanceled()) {
		// user really pressed cancel !
		Kwave::MessageBox::error(m_parent_widget,
		    i18n("The file has been truncated and "
		          "might be corrupted."));
		res = -EINTR;
	    }
	    delete dialog;
            dialog = Q_NULLPTR;
	}
    } else {
	Kwave::MessageBox::error(m_parent_widget,
	    i18n("Sorry, the file type is not supported."));
	res = -EINVAL;
    }

    if (!res && !selection) {
	// saved without error -> no longer modified
	flushUndoBuffers();
	enableModifiedChange(true);
	setModified(false);
    }

    emit sigMetaDataChanged(m_meta_data);
    qDebug("SignalManager::save(): res=%d",res);
    return res;
}

//***************************************************************************
void Kwave::SignalManager::newSignal(sample_index_t samples, double rate,
                                     unsigned int bits, unsigned int tracks)
{
    // enter and stay in modified state
    enableModifiedChange(true);
    setModified(true);
    enableModifiedChange(false);

    // disable undo (discards all undo/redo data)
    disableUndo();

    m_meta_data.clear();
    Kwave::FileInfo file_info(m_meta_data);
    file_info.setRate(rate);
    file_info.setBits(bits);
    file_info.setTracks(tracks);
    m_meta_data.replace(Kwave::MetaDataList(file_info));

    // now the signal is considered not to be empty
    m_closed = false;
    m_empty = false;

    // add all empty tracks
    while (tracks) {
        m_signal.appendTrack(samples, Q_NULLPTR);
	tracks--;
    }

    // remember the last length
    m_last_length = samples;
    file_info.setLength(length());
    m_meta_data.replace(Kwave::MetaDataList(file_info));
    rememberCurrentSelection();

    // from now on, undo is enabled
    enableUndo();

    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
void Kwave::SignalManager::close()
{
    // stop the playback
    m_playback_controller.playbackStop();
    m_playback_controller.reset();

    // fix the modified flag to false
    enableModifiedChange(true);
    setModified(false);
    enableModifiedChange(false);

    // reset the last length of the signal
    m_last_length = 0;

    // disable undo and discard all undo buffers
    // undo will be re-enabled when a signal is loaded or created
    disableUndo();

    // for safety: flush all undo/redo buffers
    flushUndoBuffers();
    flushRedoBuffer();

    // reset the selection
    m_selection.clear();

    m_empty = true;
    while (tracks()) deleteTrack(tracks() - 1);
    m_signal.close();

    // clear all meta data
    m_meta_data.clear();

    m_closed = true;
    rememberCurrentSelection();

    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
QString Kwave::SignalManager::signalName()
{
    // if a file is loaded -> path of the URL if it has one
    QUrl url;
    url = QUrl(Kwave::FileInfo(m_meta_data).get(Kwave::INF_FILENAME).toString());
    if (url.isValid()) return url.path();

    // we have something, but no name yet
    if (!isClosed()) return QString(NEW_FILENAME);

    // otherwise: closed, nothing loaded
    return _("");
}

//***************************************************************************
const QVector<unsigned int> Kwave::SignalManager::selectedTracks()
{
    QVector<unsigned int> list;
    const unsigned int tracks = this->tracks();

    for (unsigned int track = 0; track < tracks; track++) {
	if (!m_signal.trackSelected(track)) continue;
	list.append(track);
    }

    return list;
}

//***************************************************************************
const QVector<unsigned int> Kwave::SignalManager::allTracks()
{
    return m_signal.allTracks();
}

//***************************************************************************
int Kwave::SignalManager::executeCommand(const QString &command)
{
    sample_index_t offset = m_selection.offset();
    sample_index_t length = m_selection.length();

    if (!command.length()) return -EINVAL;
    Kwave::Parser parser(command);

    if (false) {
    // --- undo / redo ---
    CASE_COMMAND("undo")
	undo();
    CASE_COMMAND("redo")
	redo();
    CASE_COMMAND("undo_all")
	while (m_undo_enabled && !m_undo_buffer.isEmpty())
	    undo();
    CASE_COMMAND("redo_all")
	while (!m_redo_buffer.isEmpty())
	    redo();

    // --- copy & paste + clipboard ---
    CASE_COMMAND("copy")
	if (length) {
	    Kwave::ClipBoard &clip = Kwave::ClipBoard::instance();
	    clip.copy(
		m_parent_widget,
		*this,
		selectedTracks(),
		offset, length
	    );
	    // remember the last selection
	    rememberCurrentSelection();
	}
    CASE_COMMAND("insert_at")
	Kwave::ClipBoard &clip = Kwave::ClipBoard::instance();
	if (clip.isEmpty()) return 0;
	if (!selectedTracks().size()) return 0;
	sample_index_t ofs = parser.toSampleIndex();

	Kwave::UndoTransactionGuard undo(*this,
	                                 i18n("Insert Clipboard at position"));

	selectRange(ofs, 0);
	clip.paste(m_parent_widget, *this, ofs, 0);

    CASE_COMMAND("paste")
	Kwave::ClipBoard &clip = Kwave::ClipBoard::instance();
	if (clip.isEmpty()) return 0;
	if (!selectedTracks().size()) return 0;

	Kwave::UndoTransactionGuard undo(*this, i18n("Paste"));
	clip.paste(m_parent_widget, *this, offset, length);
    CASE_COMMAND("cut")
	if (length) {
	    // remember the last selection
	    rememberCurrentSelection();

	    Kwave::ClipBoard &clip = Kwave::ClipBoard::instance();
	    clip.copy(
		m_parent_widget,
		*this,
		selectedTracks(),
		offset, length
	    );
	    Kwave::UndoTransactionGuard undo(*this, i18n("Cut"));
	    deleteRange(offset, length);
	    selectRange(m_selection.offset(), 0);
	}
    CASE_COMMAND("clipboard_flush")
	Kwave::ClipBoard::instance().clear();
    CASE_COMMAND("crop")
	if (length) {
	    Kwave::UndoTransactionGuard undo(*this, i18n("Crop"));
	    sample_index_t rest = this->length() - offset;
	    rest = (rest > length) ? (rest-length) : 0;
	    QVector<unsigned int> tracks = selectedTracks();
	    if (saveUndoDelete(tracks, offset+length, rest) &&
		saveUndoDelete(tracks, 0, offset))
	    {
		// remember the last selection
		rememberCurrentSelection();

		unsigned int count = tracks.count();
		while (count--) {
		    m_signal.deleteRange(count, offset+length, rest);
		    m_signal.deleteRange(count, 0, offset);
		}
		selectRange(0, length);
	    }
	}
    CASE_COMMAND("delete")
	Kwave::UndoTransactionGuard undo(*this, i18n("Delete"));
	deleteRange(offset, length);
	selectRange(m_selection.offset(), 0);

//    CASE_COMMAND("mixpaste")
//	if (globals.clipboard) {
//	    SignalManager *toinsert = globals.clipboard->getSignal();
//	    if (toinsert) {
//		unsigned int clipchan = toinsert->channels();
//		unsigned int sourcechan = 0;
//
//		/* ### check if the signal has to be re-sampled ### */
//
//		for (unsigned int i = 0; i < m_channels; i++) {
//		    Q_ASSERT(signal.at(i));
//		    if (signal.at(i)) {
//			signal.at(i)->mixPaste(
//			    toinsert->getSignal(sourcechan)
//			);
//		    }
//		    sourcechan++;
//		    sourcechan %= clipchan;
//		}
//	    }
//	}

    CASE_COMMAND("label:delete")
	int index = parser.toInt();
	deleteLabel(index, true);

    CASE_COMMAND("expandtolabel")
	Kwave::UndoTransactionGuard undo(*this,
	                                 i18n("Expand Selection to Label"));
	sample_index_t selection_left  = m_selection.first();
	sample_index_t selection_right = m_selection.last();
	Kwave::LabelList labels(m_meta_data);
	if (labels.isEmpty()) return false; // we need labels for this
	Kwave::Label label_left  = Kwave::Label();
	Kwave::Label label_right = Kwave::Label();

	// the last label <= selection start -> label_left
	// the first label >= selection end  -> label_right
	foreach (const Kwave::Label &label, labels) {
	    sample_index_t lp = label.pos();
	    if (lp <= selection_left)
		label_left = label;
	    if ((lp >= selection_right) && (label_right.isNull())) {
		label_right = label;
		break; // done
	    }
	}
	// default left label = start of file
	selection_left = (label_left.isNull()) ? 0 :
	    label_left.pos();
	// default right label = end of file
	selection_right = (label_right.isNull()) ?
	    (this->length() - 1) : label_right.pos();
	sample_index_t len = selection_right - selection_left + 1;
	selectRange(selection_left, len);

    CASE_COMMAND("selectnextlabels")
	Kwave::UndoTransactionGuard undo(*this, i18n("Select Next Labels"));
	sample_index_t selection_left;
	sample_index_t selection_right = m_selection.last();
	Kwave::Label label_left  = Kwave::Label();
	Kwave::Label label_right = Kwave::Label();
	Kwave::LabelList labels(m_meta_data);
	if (labels.isEmpty()) return false; // we need labels for this

	// special case: nothing selected -> select up to the first label
	if (selection_right == 0) {
	    label_right = labels.first();
	    selection_left = 0;
	} else {
	    // find the first label starting after the current selection
	    LabelListIterator it(labels);
	    while (it.hasNext()) {
		Kwave::Label label = it.next();
		if (label.pos() >= selection_right) {
		    // take it as selection start
		    label_left  = label;
		    // and it's next one as selection end (might be null)
		    label_right = it.hasNext() ? it.next() : Kwave::Label();
		    break;
		}
	    }
	    // default selection start = last label
	    if (label_left.isNull()) label_left = labels.last();
	    if (label_left.isNull()) return false; // no labels at all !?
	    selection_left = label_left.pos();
	}
	// default selection end = end of the file
	selection_right = (label_right.isNull()) ?
	    (this->length() - 1) : label_right.pos();
	sample_index_t len = (selection_right > selection_left) ?
	    (selection_right - selection_left + 1) : 1;
	selectRange(selection_left, len);

    CASE_COMMAND("selectprevlabels")
	Kwave::UndoTransactionGuard undo(*this, i18n("Select Previous Labels"));
	sample_index_t selection_left  = selection().first();
	sample_index_t selection_right = selection().last();
	Kwave::Label label_left  = Kwave::Label();
	Kwave::Label label_right = Kwave::Label();
	Kwave::LabelList labels(m_meta_data);
	if (labels.isEmpty()) return false; // we need labels for this

	// find the last label before the start of the selection
	foreach (const Kwave::Label &label, labels) {
	    if (label.pos() > selection_left)
		break; // done
	    label_left  = label_right;
	    label_right = label;
	}
	// default selection start = start of file
	selection_left = (label_left.isNull()) ? 0 :
	    label_left.pos();
	// default selection end = first label
	if (label_right.isNull()) label_right = labels.first();
	if (label_right.isNull()) return false; // no labels at all !?
	selection_right = label_right.pos();
	sample_index_t len = selection_right - selection_left + 1;
	selectRange(selection_left, len);

    // --- track related functions ---
    CASE_COMMAND("add_track")
	appendTrack();
    CASE_COMMAND("delete_track")
	Kwave::Parser p(command);
	unsigned int track = p.toUInt();
	if (track >= tracks()) return -EINVAL;
	deleteTrack(track);
    CASE_COMMAND("insert_track")
	Kwave::Parser p(command);
	unsigned int track = p.toUInt();
	insertTrack(track);

    // track selection
    CASE_COMMAND("select_track:all")
	Kwave::UndoTransactionGuard undo(*this, i18n("Select All Tracks"));
	foreach (unsigned int track, allTracks())
	    selectTrack(track, true);
    CASE_COMMAND("select_track:none")
	Kwave::UndoTransactionGuard undo(*this, i18n("Deselect all tracks"));
	foreach (unsigned int track, allTracks())
	    selectTrack(track, false);
    CASE_COMMAND("select_track:invert")
	Kwave::UndoTransactionGuard undo(*this, i18n("Invert Track Selection"));
	foreach (unsigned int track, allTracks())
	    selectTrack(track, !trackSelected(track));
    CASE_COMMAND("select_track:on")
	unsigned int track = parser.toUInt();
	if (track >= tracks()) return -EINVAL;
	Kwave::UndoTransactionGuard undo(*this, i18n("Select Track"));
	selectTrack(track, true);
    CASE_COMMAND("select_track:off")
	unsigned int track = parser.toUInt();
	if (track >= tracks()) return -EINVAL;
	Kwave::UndoTransactionGuard undo(*this, i18n("Deselect Track"));
	selectTrack(track, false);
    CASE_COMMAND("select_track:toggle")
	unsigned int track = parser.toUInt();
	if (track >= tracks()) return -EINVAL;
	Kwave::UndoTransactionGuard undo(*this, i18n("Toggle Track Selection"));
	selectTrack(track, !(trackSelected(track)));

    // playback control
    CASE_COMMAND("playback_start")
	m_playback_controller.playbackStart();

    CASE_COMMAND("fileinfo")
	QString property = parser.firstParam();
	QString value    = parser.nextParam();
	Kwave::FileInfo info(m_meta_data);
	bool found = false;
	foreach (Kwave::FileProperty p, info.allKnownProperties()) {
	    if (info.name(p) == property) {
		if (value.length())
		    info.set(p, QVariant(value)); // add/modify
		else
		    info.set(p, QVariant());      // delete
		found = true;
		break;
	    }
	}

	if (found) {
	    m_meta_data.replace(Kwave::MetaDataList(info));
	    // we now have new meta data
	    emit sigMetaDataChanged(m_meta_data);
	} else
	    return -EINVAL;
    CASE_COMMAND("dump_metadata")
	qDebug("DUMP OF META DATA => %s", DBG(parser.firstParam()));
	m_meta_data.dump();
    } else {
	return -ENOSYS;
    }

    return 0;
}

//***************************************************************************
void Kwave::SignalManager::appendTrack()
{
    Kwave::UndoTransactionGuard undo(*this, i18n("Append Track"));
    insertTrack(tracks());
}

//***************************************************************************
void Kwave::SignalManager::insertTrack(unsigned int index)
{
    Kwave::UndoTransactionGuard undo(*this, i18n("Insert Track"));

    const unsigned int count = tracks();
    Q_ASSERT(index <= count);
    if (index > count) index = count;

    // undo action for the track insert
    if (m_undo_enabled && !registerUndoAction(
	new(std::nothrow) Kwave::UndoInsertTrack(m_signal, index))) return;

    // if the signal is currently empty, use the last
    // known length instead of the current one
    sample_index_t len = (count) ? length() : m_last_length;

    if (index >= count) {
	// do an "append"
        m_signal.appendTrack(len, Q_NULLPTR);
    } else {
	// insert into the list
        m_signal.insertTrack(index, len, Q_NULLPTR);
    }

    // remember the last length
    m_last_length = length();
}

//***************************************************************************
void Kwave::SignalManager::deleteTrack(unsigned int index)
{
    Kwave::UndoTransactionGuard undo(*this, i18n("Delete Track"));

    const unsigned int count = tracks();
    Q_ASSERT(index <= count);
    if (index > count) return;

    if (m_undo_enabled) {
	// undo action for the track deletion
	if (!registerUndoAction(new(std::nothrow)
	    UndoDeleteTrack(m_signal, index)))
		return;
    }

    setModified(true);
    m_signal.deleteTrack(index);
}

//***************************************************************************
void Kwave::SignalManager::slotTrackInserted(unsigned int index,
                                             Kwave::Track *track)
{
    setModified(true);

    Kwave::FileInfo file_info(m_meta_data);
    file_info.setTracks(tracks());
    m_meta_data.replace(Kwave::MetaDataList(file_info));

    emit sigTrackInserted(index, track);
    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
void Kwave::SignalManager::slotTrackDeleted(unsigned int index,
                                             Kwave::Track *track)
{
    setModified(true);

    Kwave::FileInfo file_info(m_meta_data);
    file_info.setTracks(tracks());
    m_meta_data.replace(Kwave::MetaDataList(file_info));

    emit sigTrackDeleted(index, track);
    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
void Kwave::SignalManager::slotSamplesInserted(unsigned int track,
	sample_index_t offset, sample_index_t length)
{
    // remember the last known length
    m_last_length = m_signal.length();

    setModified(true);

    // only adjust the meta data once per operation
    QVector<unsigned int> tracks = selectedTracks();
    if (track == tracks.first()) {
	m_meta_data.shiftRight(offset, length);
    }

    emit sigSamplesInserted(track, offset, length);

    Kwave::FileInfo info(m_meta_data);
    info.setLength(m_last_length);
    m_meta_data.replace(Kwave::MetaDataList(info));
    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
void Kwave::SignalManager::slotSamplesDeleted(unsigned int track,
	sample_index_t offset, sample_index_t length)
{
    // remember the last known length
    m_last_length = m_signal.length();

    setModified(true);

    // only adjust the meta data once per operation
    QVector<unsigned int> tracks = selectedTracks();
    if (track == tracks.first()) {
	m_meta_data.shiftLeft(offset, length);
    }

    emit sigSamplesDeleted(track, offset, length);

    Kwave::FileInfo info(m_meta_data);
    info.setLength(m_last_length);
    m_meta_data.replace(Kwave::MetaDataList(info));
    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
void Kwave::SignalManager::slotSamplesModified(unsigned int track,
	sample_index_t offset, sample_index_t length)
{
    setModified(true);
    emit sigSamplesModified(track, offset, length);
}

//***************************************************************************
bool Kwave::SignalManager::deleteRange(sample_index_t offset,
				       sample_index_t length,
				       const QVector<unsigned int> &track_list)
{
    if (!length || track_list.isEmpty()) return true; // nothing to do

    // put the selected meta data into a undo action
    if (m_undo_enabled) {
	if (!registerUndoAction(new(std::nothrow) UndoDeleteMetaDataAction(
	    m_meta_data.copy(offset, length))))
	{
	    abortUndoTransaction();
	    return false;
	}
	m_meta_data.deleteRange(offset, length);

	// store undo data for all audio data (without meta data)
	if (!registerUndoAction(new(std::nothrow) UndoDeleteAction(
	    m_parent_widget, track_list, offset, length)))
	{
	    abortUndoTransaction();
	    return false;
	}
    } else {
	// delete without undo
	m_meta_data.deleteRange(offset, length);
    }

    // delete the ranges in all tracks
    // (this makes all metadata positions after the selected range invalid)
    foreach (unsigned int track, track_list) {
	m_signal.deleteRange(track, offset, length);
    }

    emit sigMetaDataChanged(m_meta_data);

    return true;
}

//***************************************************************************
bool Kwave::SignalManager::deleteRange(sample_index_t offset,
                                       sample_index_t length)
{
    return deleteRange(offset, length, selectedTracks());
}

//***************************************************************************
bool Kwave::SignalManager::insertSpace(sample_index_t offset,
                                       sample_index_t length,
				       const QVector<unsigned int> &track_list)
{
    if (!length) return true; // nothing to do
    Kwave::UndoTransactionGuard undo(*this, i18n("Insert Space"));

    unsigned int count = track_list.count();
    if (!count) return true; // nothing to do

    // first store undo data for all tracks
    if (m_undo_enabled) {
	if (!registerUndoAction(new(std::nothrow) Kwave::UndoInsertAction(
	    m_parent_widget, track_list, offset, length))) return false;
    }

    // then insert space into all tracks
    foreach (unsigned int track, track_list) {
	m_signal.insertSpace(track, offset, length);
    }

    return true;
}

//***************************************************************************
void Kwave::SignalManager::selectRange(sample_index_t offset,
                                       sample_index_t length)
{
    // first do some range checking
    sample_index_t len = this->length();

    if (length > len)  length = len;
    if (offset >= len) offset = (len) ? (len - 1) : 0;
    if ((offset + length) > len) length = len - offset;

    m_selection.select(offset, length);
}

//***************************************************************************
void Kwave::SignalManager::selectTracks(QVector<unsigned int> &track_list)
{
    unsigned int track;
    unsigned int n_tracks = tracks();
    for (track = 0; track < n_tracks; track++) {
	bool old_select = m_signal.trackSelected(track);
	bool new_select = track_list.contains(track);
	if (new_select != old_select) {
	    m_signal.selectTrack(track, new_select);
	}
    }
}

//***************************************************************************
void Kwave::SignalManager::selectTrack(unsigned int track, bool select)
{
    bool old_select = m_signal.trackSelected(track);
    if (select != old_select) {
	m_signal.selectTrack(track, select);
    }
}

//***************************************************************************
QList<Kwave::Stripe::List> Kwave::SignalManager::stripes(
    const QVector<unsigned int> &track_list,
    sample_index_t left, sample_index_t right)
{
    QList<Kwave::Stripe::List> stripes;

    foreach (unsigned int track, track_list) {
	Kwave::Stripe::List s = m_signal.stripes(track, left, right);
	if (s.isEmpty()) {
	    stripes.clear(); // something went wrong -> abort
	    break;
	}
	stripes.append(s);
    }

    return stripes;
}

//***************************************************************************
bool Kwave::SignalManager::mergeStripes(
    const QList<Kwave::Stripe::List> &stripes,
    const QVector<unsigned int> &track_list)
{
    Q_ASSERT(stripes.count() == track_list.count());
    if (stripes.count() != track_list.count())
	return false;

    QListIterator<Kwave::Stripe::List> it_s(stripes);
    QVector<unsigned int>::const_iterator it_t = track_list.constBegin();
    while (it_s.hasNext() && (it_t != track_list.constEnd())) {
	Kwave::Stripe::List s = it_s.next();
	unsigned int        t = *(it_t++);
	if (!m_signal.mergeStripes(s, t))
	    return false; // operation failed
    }

    return true;
}

//***************************************************************************
Kwave::PlaybackController &Kwave::SignalManager::playbackController()
{
    return m_playback_controller;
}

//***************************************************************************
void Kwave::SignalManager::startUndoTransaction(const QString &name)
{
    if (!m_undo_enabled) return; // undo is currently not enabled

    QMutexLocker lock(&m_undo_transaction_lock);

    // check for modified selection
    checkSelectionChange();

    // increase recursion level
    m_undo_transaction_level++;

    // start/create a new transaction if none existing
    if (!m_undo_transaction) {

	// if a new action starts, discard all redo actions !
	flushRedoBuffer();

	m_undo_transaction = new(std::nothrow) Kwave::UndoTransaction(name);
	Q_ASSERT(m_undo_transaction);
	if (!m_undo_transaction) return;

	// give all registered undo handlers a chance to register their own
	// undo actions
	if (!m_undo_manager.startUndoTransaction(m_undo_transaction)) {
	    delete m_undo_transaction;
            m_undo_transaction = Q_NULLPTR;
	}

	// if it is the start of the transaction, also create one
	// for the selection
	UndoAction *selection = new(std::nothrow) Kwave::UndoSelection(*this);
	Q_ASSERT(selection);
	if (selection && selection->store(*this)) {
	    m_undo_transaction->append(selection);
	} else {
	    // out of memory
	    delete selection;
	    delete m_undo_transaction;
            m_undo_transaction = Q_NULLPTR;
	}
    }
}

//***************************************************************************
void Kwave::SignalManager::closeUndoTransaction()
{
    QMutexLocker lock(&m_undo_transaction_lock);

    // decrease recursion level
    if (!m_undo_transaction_level) return; // undo was not enabled ?
    m_undo_transaction_level--;

    if (!m_undo_transaction_level) {
	// append the current transaction to the undo buffer if
	// not empty
	if (m_undo_transaction) {
	    if (!m_undo_transaction->isEmpty()) {
		// if the transaction has been aborted, undo all actions
		// that have currently been queued but do not
		// use the transaction any more, instead delete it.
		if (m_undo_transaction->isAborted()) {
		    qDebug("SignalManager::closeUndoTransaction(): aborted");
		    while (!m_undo_transaction->isEmpty()) {
			UndoAction *undo_action;
			UndoAction *redo_action;

			// unqueue the undo action
			undo_action = m_undo_transaction->takeLast();
			Q_ASSERT(undo_action);
			if (!undo_action) continue;

			// execute the undo operation
			redo_action = undo_action->undo(*this, false);

			// remove the old undo action if no longer used
			if (redo_action && (redo_action != undo_action))
			    delete redo_action;
			delete undo_action;
		    }
		    delete m_undo_transaction;
                    m_undo_transaction = Q_NULLPTR;
		} else {
		    m_undo_buffer.append(m_undo_transaction);
		}
	    } else {
		qDebug("SignalManager::closeUndoTransaction(): empty");
		delete m_undo_transaction;
                m_undo_transaction = Q_NULLPTR;
	    }
	}

	// dump, for debugging
// 	if (m_undo_transaction)
// 	    m_undo_transaction->dump("closed undo transaction: ");

	// declare the current transaction as "closed"
	rememberCurrentSelection();
        m_undo_transaction = Q_NULLPTR;
	emitUndoRedoInfo();
    }
}

//***************************************************************************
void Kwave::SignalManager::enableUndo()
{
    m_undo_enabled = true;
    emitUndoRedoInfo();
}

//***************************************************************************
void Kwave::SignalManager::disableUndo()
{
    if (m_undo_transaction_level)
	qWarning("SignalManager::disableUndo(): undo transaction level=%u",
	    m_undo_transaction_level);

    flushUndoBuffers();
    m_undo_enabled = false;
}

//***************************************************************************
void Kwave::SignalManager::flushUndoBuffers()
{
    QMutexLocker lock(&m_undo_transaction_lock);

    // if the signal was modified, it will stay in this state, it is
    // not possible to change to "non-modified" state through undo
    if ((!m_undo_buffer.isEmpty()) && (m_modified)) {
	enableModifiedChange(false);
    }

    // clear all buffers
    qDeleteAll(m_undo_buffer);
    qDeleteAll(m_redo_buffer);
    m_undo_buffer.clear();
    m_redo_buffer.clear();

    emitUndoRedoInfo();
}

//***************************************************************************
void Kwave::SignalManager::abortUndoTransaction()
{
    // abort the current transaction
    if (m_undo_transaction) m_undo_transaction->abort();
}

//***************************************************************************
void Kwave::SignalManager::flushRedoBuffer()
{
    qDeleteAll(m_redo_buffer);
    m_redo_buffer.clear();
    emitUndoRedoInfo();
}

//***************************************************************************
bool Kwave::SignalManager::continueWithoutUndo()
{
    // undo has not been enabled before?
    if (!m_undo_transaction) return true;

    // transaction has been aborted before
    if (m_undo_transaction->isAborted()) return false;

    // transaction is empty -> must have been flushed before, otherwise
    // it would contain at least a undo action for the selection
    // => user already has pressed "continue"
    if (m_undo_transaction->isEmpty()) return true;

    if (Kwave::MessageBox::warningContinueCancel(m_parent_widget,
	_("<html>") +
	i18n("Not enough memory for saving undo information.") +
	_("<br><br><b>") +
	i18n("Do you want to continue without the possibility to undo?") +
	_("</b><br><br><i>") +
	i18n("<b>Hint</b>: you can configure the amount of memory<br>"
	     "available for undo under '%1'/'%2'.",
	     i18n("Settings"),
	     i18n("Memory") +
	_("</i></html>"))) == KMessageBox::Continue)
    {
	// the signal was modified, it will stay in this state, it is
	// not possible to change to "non-modified" state through undo
	// from now on...
	setModified(true);
	enableModifiedChange(false);

	// flush the current undo transaction
	while (!m_undo_transaction->isEmpty()) {
	    Kwave::UndoAction *undo_action = m_undo_transaction->takeLast();
	    if (undo_action) delete undo_action;
	}

	// flush all undo/redo buffers
	flushUndoBuffers();
	return true;
    }

    // Set the undo transaction into "aborted" state. The final
    // closeUndoTransaction() will take care of the rest when
    // detecting that state and clean up...
    abortUndoTransaction();
    return false;
}

//***************************************************************************
bool Kwave::SignalManager::registerUndoAction(Kwave::UndoAction *action)
{
    QMutexLocker lock(&m_undo_transaction_lock);

    if (!action) return continueWithoutUndo();

    // if undo is not enabled, this will fail -> discard the action
    if (!m_undo_enabled) {
	delete action;
	return true;
    }

    // check if the undo action is too large
    qint64 limit_mb     = Kwave::undoLimit();
    qint64 needed_size  = action->undoSize();
    qint64 needed_mb    = needed_size  >> 20;
    if (needed_mb > limit_mb) {
	delete action;
	return continueWithoutUndo();
    }

    // undo has been aborted before ?
    if (!m_undo_transaction) return true;

    // transaction has been aborted before
    if (m_undo_transaction->isAborted()) {
	delete action;
	return true;
    }

    // make room...
    freeUndoMemory(needed_size);

    // now we might have enough place to append the undo action
    // and store all undo info
    if (!action->store(*this)) {
	delete action;
	return continueWithoutUndo();
    }

    // everything went ok, register internally
    m_undo_transaction->append(action);

    return true;
}

//***************************************************************************
bool Kwave::SignalManager::saveUndoDelete(QVector<unsigned int> &track_list,
                                          sample_index_t offset,
                                          sample_index_t length)
{
    if (!m_undo_enabled) return true;
    if (track_list.isEmpty()) return true;

    // create a undo action for deletion
    UndoDeleteAction *action = new(std::nothrow)
	UndoDeleteAction(m_parent_widget, track_list, offset, length);
    if (!registerUndoAction(action)) return false;

    return true;
}

//***************************************************************************
qint64 Kwave::SignalManager::usedUndoRedoMemory()
{
    qint64 size = 0;

    foreach (Kwave::UndoTransaction *undo, m_undo_buffer)
	if (undo) size += undo->undoSize();

    foreach (Kwave::UndoTransaction *redo, m_redo_buffer)
	if (redo) size += redo->undoSize();

    return size;
}

//***************************************************************************
void Kwave::SignalManager::freeUndoMemory(qint64 needed)
{
    qint64 size = usedUndoRedoMemory() + needed;
    qint64 undo_limit = Kwave::undoLimit() << 20;

    // remove old undo actions if not enough free memory
    while (!m_undo_buffer.isEmpty() && (size > undo_limit)) {
	Kwave::UndoTransaction *undo = m_undo_buffer.takeFirst();
	if (!undo) continue;
	qint64 s = undo->undoSize();
	size = (size >= s) ? (size - s) : 0;
	delete undo;

	// if the signal was modified, it will stay in this state, it is
	// not possible to change to "non-modified" state through undo
	if (m_modified) enableModifiedChange(false);
    }

    // remove old redo actions if still not enough memory
    while (!m_redo_buffer.isEmpty() && (size > undo_limit)) {
	Kwave::UndoTransaction *redo = m_redo_buffer.takeLast();
	if (!redo) continue;
	qint64 s = redo->undoSize();
	size = (size >= s) ? (size - s) : 0;
	delete redo;
    }
}

//***************************************************************************
void Kwave::SignalManager::emitUndoRedoInfo()
{
    QString undo_name = QString();
    QString redo_name = QString();

    if (m_undo_enabled) {
	Kwave::UndoTransaction *transaction;

	// get the description of the last undo action
	if (!m_undo_buffer.isEmpty()) {
	    transaction = m_undo_buffer.last();
	    if (transaction) undo_name = transaction->description();
	    if (!undo_name.length()) undo_name = i18n("Last Action");
	}

	// get the description of the last redo action
	if (!m_redo_buffer.isEmpty()) {
	    transaction = m_redo_buffer.first();
	    if (transaction) redo_name = transaction->description();
	    if (!redo_name.length()) redo_name = i18n("Last Action");
	}
    }

    // now emit the undo/redo transaction names
    emit sigUndoRedoInfo(undo_name, redo_name);
}

//***************************************************************************
void Kwave::SignalManager::undo()
{
    QMutexLocker lock(&m_undo_transaction_lock);

    // check for modified selection
    checkSelectionChange();

    // remember the last selection
    rememberCurrentSelection();

    // get the last undo transaction and abort if none present
    if (m_undo_buffer.isEmpty()) return;
    Kwave::UndoTransaction *undo_transaction = m_undo_buffer.takeLast();
    if (!undo_transaction) return;

    // dump, for debugging
//     undo_transaction->dump("before undo: ");

    // temporarily disable undo while undo itself is running
    bool old_undo_enabled = m_undo_enabled;
    m_undo_enabled = false;

    // get free memory for redo
    qint64 undo_limit = Kwave::undoLimit() << 20;
    qint64 redo_size = undo_transaction->redoSize();
    qint64 undo_size = undo_transaction->undoSize();
    Kwave::UndoTransaction *redo_transaction = Q_NULLPTR;
    if ((redo_size > undo_size) && (redo_size - undo_size > undo_limit)) {
	// not enough memory for redo
	qWarning("SignalManager::undo(): not enough memory for redo !");
    } else {
	// only free the memory if it will be used
	freeUndoMemory(redo_size);

	// create a new redo transaction
	QString name = undo_transaction->description();
	redo_transaction = new(std::nothrow) Kwave::UndoTransaction(name);
	Q_ASSERT(redo_transaction);
    }

    // if *one* redo fails, all following redoes will also fail or
    // produce inconsistent data -> remove all of them !
    if (!redo_transaction) {
	flushRedoBuffer();
	qDebug("SignalManager::undo(): redo buffer flushed!");
    }

    // set hourglass cursor
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    // execute all undo actions and store the resulting redo
    // actions into the redo transaction
    while (!undo_transaction->isEmpty()) {
	UndoAction *undo_action;
	UndoAction *redo_action;

	// unqueue the undo action
	undo_action = undo_transaction->takeLast();
	Q_ASSERT(undo_action);
	if (!undo_action) continue;

	// execute the undo operation
        redo_action = undo_action->undo(*this, (redo_transaction != Q_NULLPTR));

	// remove the old undo action if no longer used
	if (redo_action != undo_action) {
	    delete undo_action;
	}

	// queue the action into the redo transaction
	if (redo_action) {
	    if (redo_transaction) {
		redo_transaction->prepend(redo_action);
	    } else {
		// redo is not usable :-(
		delete redo_action;
	    }
	}
    }

    // now the undo_transaction should be empty -> get rid of it
    Q_ASSERT(undo_transaction->isEmpty());
    delete undo_transaction;

    if (redo_transaction && (redo_transaction->count() < 1)) {
	// if there is no redo action -> no redo possible
	qWarning("SignalManager::undo(): no redo possible");
	delete redo_transaction;
        redo_transaction = Q_NULLPTR;
    }

    // check whether the selection has changed, if yes: put a undo action
    // for this selection change at the end of the redo transaction
    if (redo_transaction) {
	bool range_modified = !(m_selection == m_last_selection);
	QVector<unsigned int> tracks = selectedTracks();
	bool tracks_modified = !(tracks == m_last_track_selection);
	if (range_modified || tracks_modified) {
	    UndoAction *redo_action = new(std::nothrow) Kwave::UndoSelection(
		*this,
		m_last_track_selection,
		m_last_selection.offset(),
		m_last_selection.length());
	    Q_ASSERT(redo_action);
	    if (redo_action) redo_transaction->append(redo_action);
	}
    }

    // find out if there is still an action in the undo buffer
    // that has to do with modification of the signal
    if (m_modified) {
	bool stay_modified = false;
	foreach (Kwave::UndoTransaction *transaction, m_undo_buffer) {
	    if (!transaction) continue;
	    if (transaction->containsModification()) {
		stay_modified = true;
		break;
	    }
	}
	if (!stay_modified) {
	    // try to return to non-modified mode (might be a nop if
	    // not enabled)
	    setModified(false);
	}
    }

    // save "redo" information if possible
    if (redo_transaction)
	m_redo_buffer.prepend(redo_transaction);

    // remember the last selection
    rememberCurrentSelection();

    // re-enable undo
    m_undo_enabled = old_undo_enabled;

    // finished / buffers have changed, emit new undo/redo info
    emitUndoRedoInfo();

    // maybe the meta data has changed
    emit sigMetaDataChanged(m_meta_data);

    // remove hourglass
    QApplication::restoreOverrideCursor();
}

//***************************************************************************
void Kwave::SignalManager::redo()
{
    QMutexLocker lock(&m_undo_transaction_lock);

    // get the last redo transaction and abort if none present
    if (m_redo_buffer.isEmpty()) return;
    Kwave::UndoTransaction *redo_transaction = m_redo_buffer.takeFirst();
    if (!redo_transaction) return;

    // check for modified selection
    checkSelectionChange();

    // temporarily disable undo while redo is running
    bool old_undo_enabled = m_undo_enabled;
    m_undo_enabled = false;

    // get free memory for undo
    qint64 undo_limit = Kwave::undoLimit() << 20;
    qint64 undo_size = redo_transaction->undoSize();
    qint64 redo_size = redo_transaction->redoSize();
    Kwave::UndoTransaction *undo_transaction = Q_NULLPTR;
    if ((undo_size > redo_size) && (undo_size - redo_size > undo_limit)) {
	// not enough memory for undo
	qWarning("SignalManager::redo(): not enough memory for undo !");
    } else {
	// only free the memory if it will be used
	freeUndoMemory(undo_size);

	// create a new undo transaction
	QString name = redo_transaction->description();
	undo_transaction = new(std::nothrow) Kwave::UndoTransaction(name);
	Q_ASSERT(undo_transaction);
    }

    // if *one* undo fails, all following undoes will also fail or
    // produce inconsistent data -> remove all of them !
    if (!undo_transaction) {
	qDeleteAll(m_undo_buffer);
	m_undo_buffer.clear();
    } else {
	m_undo_buffer.append(undo_transaction);
    }

    // set hourglass cursor
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    // execute all redo actions and store the resulting undo
    // actions into the undo transaction
    bool modified = false;
    while (!redo_transaction->isEmpty()) {
	UndoAction *undo_action;
	UndoAction *redo_action;

	// unqueue the undo action
	redo_action = redo_transaction->takeFirst();

	// execute the redo operation
	Q_ASSERT(redo_action);
	if (!redo_action) continue;
	modified |= redo_transaction->containsModification();
        undo_action = redo_action->undo(*this, (undo_transaction != Q_NULLPTR));

	// remove the old redo action if no longer used
	if (redo_action != undo_action) {
	    delete redo_action;
	}

	// queue the action into the undo transaction
	if (undo_action) {
	    if (undo_transaction) {
		undo_transaction->append(undo_action);
	    } else {
		// undo is not usable :-(
		delete undo_action;
	    }
	}
    }

    // now the redo_transaction should be empty -> get rid of it
    Q_ASSERT(redo_transaction->isEmpty());
    delete redo_transaction;

    if (undo_transaction && (undo_transaction->count() < 1)) {
	// if there is no undo action -> no undo possible
	qWarning("SignalManager::redo(): no undo possible");
	m_undo_buffer.removeAll(undo_transaction);
	delete undo_transaction;
    }

    // if the redo operation modified something,
    // we have to update the "modified" flag of the current signal
    if (modified) setModified(true);

    // remember the last selection
    rememberCurrentSelection();

    // re-enable undo
    m_undo_enabled = old_undo_enabled;

    // finished / buffers have changed, emit new undo/redo info
    emitUndoRedoInfo();

    // maybe the meta data has changed
    emit sigMetaDataChanged(m_meta_data);

    // remove hourglass
    QApplication::restoreOverrideCursor();
}

//***************************************************************************
void Kwave::SignalManager::setModified(bool mod)
{
    if (!m_modified_enabled) return;

    if (m_modified != mod) {
	m_modified = mod;
// 	qDebug("SignalManager::setModified(%d)",mod);
	emit sigModified();
    }
}

//***************************************************************************
void Kwave::SignalManager::enableModifiedChange(bool en)
{
    m_modified_enabled = en;
}

//***************************************************************************
void Kwave::SignalManager::setFileInfo(const Kwave::FileInfo &new_info,
                                       bool with_undo)
{
    if (m_undo_enabled && with_undo) {
	/* save data for undo */
	Kwave::UndoTransactionGuard undo_transaction(*this,
	                                             i18n("Modify File Info"));
	Kwave::FileInfo old_inf(m_meta_data);
	if (!registerUndoAction(
	    new(std::nothrow) Kwave::UndoModifyMetaDataAction(
		Kwave::MetaDataList(old_inf))))
	    return;
    }

    m_meta_data.replace(Kwave::MetaDataList(new_info));
    setModified(true);
    emitUndoRedoInfo();
    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
Kwave::Label Kwave::SignalManager::findLabel(sample_index_t pos)
{
    Kwave::LabelList labels(m_meta_data);
    foreach (const Kwave::Label &label, labels) {
	if (label.pos() == pos) return label; // found it
    }
    return Kwave::Label(); // nothing found
}

//***************************************************************************
int Kwave::SignalManager::labelIndex(const Kwave::Label &label) const
{
    int index = 0;
    Kwave::LabelList labels(m_meta_data);
    foreach (const Kwave::Label &l, labels) {
	if (l == label) return index; // found it
	index++;
    }
    return -1; // nothing found*/
}

//***************************************************************************
Kwave::Label Kwave::SignalManager::addLabel(sample_index_t pos,
                                            const QString &name)
{
    // if there already is a label at the given position, do nothing
    if (!findLabel(pos).isNull()) return Kwave::Label();

    // create a new label
    Kwave::Label label(pos, name);

    // register the undo action
    if (m_undo_enabled) {
	Kwave::UndoTransactionGuard undo(*this, i18n("Add Label"));
	if (!registerUndoAction(new(std::nothrow) UndoAddMetaDataAction(
	        Kwave::MetaDataList(label))))
	    return Kwave::Label();
    }

    // put the label into the list
    m_meta_data.add(label);

    // register this as a modification
    setModified(true);

    emit sigMetaDataChanged(m_meta_data);

    return label;
}

//***************************************************************************
void Kwave::SignalManager::deleteLabel(int index, bool with_undo)
{
    Kwave::LabelList labels(m_meta_data);
    int count = Kwave::toInt(labels.count());
    if (!count) return;

    if (index == -1) {
	// special handling for index == -1 -> delete all labels

	if (with_undo) startUndoTransaction(i18n("Delete All Labels"));

	for (index = count - 1; index >= 0; --index) {
	    Kwave::MetaData label(labels.at(index));
	    if (with_undo) {
		if (!registerUndoAction(new(std::nothrow)
		    UndoDeleteMetaDataAction(Kwave::MetaDataList(label))))
		    break;
	    }
	    m_meta_data.remove(label);
	}
    } else {
	// delete a single label
	if ((index < 0) || (index >= count)) return;

	Kwave::MetaData label(labels.at(index));

	// register the undo action
	if (with_undo) {
	    startUndoTransaction(i18n("Delete Label"));
	    if (!registerUndoAction(new(std::nothrow)
		UndoDeleteMetaDataAction(Kwave::MetaDataList(label)))) {
		abortUndoTransaction();
		return;
	    }
	}

	m_meta_data.remove(label);
    }

    if (with_undo) closeUndoTransaction();

    // register this as a modification
    setModified(true);

    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
bool Kwave::SignalManager::modifyLabel(int index, sample_index_t pos,
                                       const QString &name,
                                       bool with_undo)
{
    Kwave::LabelList labels(m_meta_data);
    if ((index < 0) || (index >= Kwave::toInt(labels.count())))
	return false;

    Kwave::Label label = labels.at(index);

    // check: if the label should be moved and there already is a label
    // at the new position -> fail
    if ((pos != label.pos()) && !findLabel(pos).isNull())
	return false;

    // add a undo action
    if (with_undo && m_undo_enabled) {
	Kwave::UndoModifyMetaDataAction *undo_modify =
	    new(std::nothrow) Kwave::UndoModifyMetaDataAction(
	            Kwave::MetaDataList(label));
	if (!registerUndoAction(undo_modify))
	    return false;
    }

    // now modify the label
    label.moveTo(pos);
    label.rename(name);
    m_meta_data.add(label);

    // register this as a modification
    setModified(true);

    emit sigMetaDataChanged(m_meta_data);
    return true;
}

//***************************************************************************
void Kwave::SignalManager::mergeMetaData(const Kwave::MetaDataList &meta_data)
{
    m_meta_data.add(meta_data);
    emit sigMetaDataChanged(m_meta_data);
}

//***************************************************************************
void Kwave::SignalManager::rememberCurrentSelection()
{
    m_last_selection       = m_selection;
    m_last_track_selection = selectedTracks();
}

//***************************************************************************
void Kwave::SignalManager::checkSelectionChange()
{
    if (m_undo_transaction_level) return;

    // detect sample selection change
    bool range_modified = !(m_selection == m_last_selection);

    // detect track selection change
    QVector<unsigned int> tracks = selectedTracks();
    bool tracks_modified = !(tracks == m_last_track_selection);

    if (range_modified || tracks_modified) {
	// selection has changed since last undo/redo operation
// 	qDebug("SignalManager::checkSelectionChange() => manually modified");
// 	qDebug("    before: [%8u...%8u]", m_last_selection.first(), m_last_selection.last());
// 	qDebug("    after : [%8u...%8u]", m_selection.first(),      m_selection.last());

	// temporarily activate the previous selection (last stored)
	Kwave::Selection new_selection(m_selection);
	m_selection = m_last_selection;
	selectTracks(m_last_track_selection);

	// save the last selection into a undo action
	if (tracks_modified && !range_modified)
	    Kwave::UndoTransactionGuard undo(*this,
	                                     i18n("Manual Track Selection"));
	else
	    Kwave::UndoTransactionGuard undo(*this,
	                                     i18n("Manual Selection"));

	// restore the current selection again
	m_selection = new_selection;
	selectTracks(tracks);
    }

}

//***************************************************************************
//***************************************************************************
