/***************************************************************************
   SelectionTracker.cpp  -  tracker for selection changes
                             -------------------
    begin                : Tue Feb 25 2014
    copyright            : (C) 2014 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 "libkwave/Track.h"
#include "libkwave/undo/UndoAction.h"
#include "libkwave/undo/UndoManager.h"
#include "libkwave/undo/UndoTransaction.h"

#include "libgui/SelectionTracker.h"

//***************************************************************************
Kwave::SelectionTracker::SelectionTracker(Kwave::SignalManager *signal,
                                          sample_index_t offset,
                                          sample_index_t length,
                                          const QVector<unsigned int> *tracks)
    :m_signal(signal),
     m_offset(offset),
     m_length((length || !signal) ? length : signal->length()),
     m_tracks(),
     m_selection_only((length != 0)),
     m_lock()
{
    Q_ASSERT(signal);
    if (!signal) return;

    QObject::connect(
        signal, SIGNAL(sigTrackInserted(uint,Kwave::Track*)),
        this,   SLOT(slotTrackInserted(uint,Kwave::Track*)));
    connect(signal, SIGNAL(sigTrackDeleted(uint,Kwave::Track*)),
            this,   SLOT(slotTrackDeleted(uint,Kwave::Track*)));
    connect(
        signal,
        SIGNAL(sigSamplesDeleted(uint,sample_index_t,sample_index_t)),
        this,
        SLOT(slotSamplesDeleted(uint,sample_index_t,sample_index_t))
    );
    connect(
        signal,
        SIGNAL(sigSamplesInserted(uint,sample_index_t,sample_index_t)),
        this,
        SLOT(slotSamplesInserted(uint,sample_index_t,sample_index_t))
    );
    connect(
        signal,
        SIGNAL(sigSamplesModified(uint,sample_index_t,sample_index_t)),
        this,
        SLOT(slotSamplesModified(uint,sample_index_t,sample_index_t))
    );

    // register us at the undo manager
    Kwave::UndoManager &undo = signal->undoManager();
    undo.registerHandler(this);

    if (tracks && !tracks->isEmpty()) {
        // having a list of selected tracks
        foreach (unsigned int track, *tracks) {
            slotTrackInserted(track, nullptr);
            if (m_selection_only)
                m_tracks.append(m_signal->uuidOfTrack(track));
        }
    } else {
        // take over all tracks from the signal manager
        foreach (unsigned int track, m_signal->allTracks()) {
            slotTrackInserted(track, nullptr);
            if (m_selection_only)
                m_tracks.append(m_signal->uuidOfTrack(track));
        }
    }
}

//***************************************************************************
Kwave::SelectionTracker::~SelectionTracker()
{
    // unregister us from the undo manager (if it still exists)
    if (m_signal) {
        Kwave::UndoManager &undo = m_signal->undoManager();
        undo.unregisterHandler(this);
    }
}

//***************************************************************************
QList<QUuid> Kwave::SelectionTracker::allTracks()
{
    return m_tracks;
}

//***************************************************************************
bool Kwave::SelectionTracker::saveUndoData(Kwave::UndoTransaction &undo)
{
    // shortcut: we only have to do something when we are in
    //           "selection only" mode
    if (!m_selection_only)
        return true;

    Q_ASSERT(!m_signal.isNull());
    if (m_signal.isNull())
        return false; // should never happen

    Kwave::UndoAction *action =
        new(std::nothrow) Kwave::SelectionTracker::Undo(this);
    Q_ASSERT(action);
    if (!action) return false;

    if (action->store(*m_signal)) {
        undo.append(action);
    } else {
        // out of memory
        delete action;
        return false;
    }

    return true;
}

//***************************************************************************
void Kwave::SelectionTracker::slotTrackInserted(unsigned int index,
                                                Kwave::Track *track)
{
    QMutexLocker lock(&m_lock);

    if (m_selection_only)
        return; // in "selection only" mode we are not interested in this

    Q_ASSERT(track || !m_signal.isNull());
    if (!track && m_signal.isNull()) return;

    const QUuid &uuid = (track) ? track->uuid() : m_signal->uuidOfTrack(index);
    Q_ASSERT(!uuid.isNull());

    // track signal length changes when tracks were inserted
    sample_index_t new_len = m_signal->length();
    if (m_length != new_len) {
        m_length = new_len;
        emit sigLengthChanged(m_length);
    }

    // a new track has been inserted
    m_tracks.append(uuid);
    emit sigTrackInserted(uuid);
}

//***************************************************************************
void Kwave::SelectionTracker::slotTrackDeleted(unsigned int index,
                                               Kwave::Track *track)
{
    QMutexLocker lock(&m_lock);

    Q_UNUSED(index)

    Q_ASSERT(track);
    if (!track) return;

    const QUuid &uuid = track->uuid();
    Q_ASSERT(!uuid.isNull());
    if (!m_tracks.contains(uuid))
        return; // track not selected

    // track signal length changes when tracks were inserted
    if (!m_selection_only) {
        sample_index_t new_len = m_signal->length();
        if (m_length != new_len) {
            m_length = new_len;
            emit sigLengthChanged(m_length);
        }
    }

    // one of our selected tracks was deleted
    m_tracks.removeAll(uuid);
    emit sigTrackDeleted(uuid);
}

//***************************************************************************
void Kwave::SelectionTracker::slotSamplesInserted(unsigned int track,
                                                  sample_index_t offset,
                                                  sample_index_t length)
{
    QMutexLocker lock(&m_lock);

    Q_ASSERT(!m_signal.isNull());
    if (m_signal.isNull()) return;

    const QUuid uuid = m_signal->uuidOfTrack(track);
    if (!m_tracks.contains(uuid))
        return; // track not selected

    if (!length)
        return; // nothing to do

    // NOTE: adjust offsets/lengths only for the first selected track
    const bool is_first = (uuid == m_tracks.first());

    if (m_selection_only) {
        if (offset >= (m_offset + m_length))
            return; // right of us

        if (offset < m_offset) {
            // left of us, no overlap
            if (is_first) {
                m_offset += length;
                emit sigOffsetChanged(m_offset);
            }
            return;
        }
    }

    // in our range -> increase length and invalidate all
    // samples from offset to end of file
    if (is_first) {
        m_length += length;
        emit sigLengthChanged(m_length);
    }

    emit sigInvalidated(&uuid, offset, SAMPLE_INDEX_MAX);
}

//***************************************************************************
void Kwave::SelectionTracker::slotSamplesDeleted(unsigned int track,
                                                 sample_index_t offset,
                                                 sample_index_t length)
{
    QMutexLocker lock(&m_lock);

    Q_ASSERT(!m_signal.isNull());
    if (m_signal.isNull()) return;

    const QUuid uuid = m_signal->uuidOfTrack(track);
    if (!m_tracks.contains(uuid))
        return; // track not selected

    if (!length)
        return; // nothing to do

    if (offset >= (m_offset + m_length))
        return; // right of us

    // NOTE: adjust offsets/lengths only for the first selected track
    const bool is_first = (uuid == m_tracks.first());

    if ((offset + length - 1) < m_offset) {
        // left of us, no overlap
        if (is_first) {
            m_offset -= length;
            emit sigOffsetChanged(m_offset);
        }
        return;
    }

    // determine shift and number of deleted samples
    sample_index_t first_del = offset;
    sample_index_t last_del  = offset + length - 1;
    sample_index_t shift = (first_del < m_offset) ? (m_offset - first_del) : 0;
    sample_index_t left  = qMax(first_del, m_offset);
    sample_index_t right = qMin(last_del,  m_offset + m_length - 1);
    sample_index_t deleted_samples = right - left + 1;

    // adjust the start of the selection
    if (is_first && shift) {
        m_offset -= shift;
        left      = m_offset;
        emit sigOffsetChanged(m_offset);
    }

    // adjust the length of the selection
    if (is_first && deleted_samples) {
        m_length -= deleted_samples;
        emit sigLengthChanged(m_length);
    }

    // in our range -> invalidate all samples from offset to end of file
    emit sigInvalidated(&uuid, left, SAMPLE_INDEX_MAX);
}

//***************************************************************************
void Kwave::SelectionTracker::slotSamplesModified(unsigned int track,
                                                  sample_index_t offset,
                                                  sample_index_t length)
{
    QMutexLocker lock(&m_lock);

    if (!length) return; // nothing to do

    Q_ASSERT(!m_signal.isNull());
    if (m_signal.isNull()) return;

    const QUuid uuid = m_signal->uuidOfTrack(track);
    if (!m_tracks.contains(uuid))
        return; // track not selected

    if (offset >= (m_offset + m_length))
        return; // right out of our range -> out of interest

    if (offset + length < m_offset)
        return; // completely left from us -> out of interest

    // overlapping
    sample_index_t first_mod = offset;
    sample_index_t last_mod  = offset + length - 1;
    sample_index_t left  = qMax(first_mod, m_offset);
    sample_index_t right = qMin(last_mod,  m_offset + m_length - 1);

    emit sigInvalidated(&uuid, left, right);
}

//***************************************************************************
void Kwave::SelectionTracker::selectRange(QList<QUuid> tracks,
                                          sample_index_t offset,
                                          sample_index_t length)
{
    QMutexLocker lock(&m_lock);

    // remove deleted tracks
    foreach (const QUuid &uuid, m_tracks) {
        if (!tracks.contains(uuid)) {
            m_tracks.removeAll(uuid);
            emit sigTrackDeleted(uuid);
        }
    }

    // add new tracks
    foreach (const QUuid &uuid, tracks) {
        if (!m_tracks.contains(uuid)) {
            m_tracks.append(uuid);
            emit sigTrackInserted(uuid);
        }
    }

    // track signal length changes when tracks were inserted
    if (!m_selection_only) {
        sample_index_t new_len = m_signal->length();
        if (m_length != new_len) {
            m_length = new_len;
            emit sigLengthChanged(m_length);
        }

        return; // nothing more to do in this mode
    }

    if ((offset == m_offset) && (length == m_length))
        return; // no change, nothing to do

    if (!length)
        return; // makes no sense

    // remember the old settings
    const sample_index_t old_ofs = m_offset;
    const sample_index_t old_len = m_length;

    // take over the new selection
    if (m_length != length) {
        m_length = length;
        emit sigLengthChanged(m_length);
    }
    if (offset != old_ofs) {
        // offset has changed -> invalidate all
        m_offset = offset;
        emit sigOffsetChanged(m_offset);
        emit sigInvalidated(nullptr, m_offset, SAMPLE_INDEX_MAX);
    } else if (length > old_len) {
        // length has changed and increased -> invalidate new area
        emit sigInvalidated(nullptr, m_offset + old_len - 1,
                            SAMPLE_INDEX_MAX);
    } else if (length < old_len) {
        // length was reduced -> invalidate shrunken area at end
        emit sigInvalidated(nullptr, m_offset + length - 1,
                            SAMPLE_INDEX_MAX);
    }
}

//***************************************************************************
//***************************************************************************
Kwave::SelectionTracker::Undo::Undo(Kwave::SelectionTracker *selection)
    :Kwave::UndoAction(),
     m_tracker(selection),
     m_tracks(selection ? selection->allTracks() : QList<QUuid>()),
     m_offset(selection ? selection->offset() : 0),
     m_length(selection ? selection->length() : 0)
{
}

//***************************************************************************
Kwave::SelectionTracker::Undo::~Undo()
{
}

//***************************************************************************
QString Kwave::SelectionTracker::Undo::description()
{
    return QString();
}

//***************************************************************************
qint64 Kwave::SelectionTracker::Undo::undoSize()
{
    return sizeof(*this);
}

//***************************************************************************
qint64 Kwave::SelectionTracker::Undo::redoSize()
{
    return 0;
}

//***************************************************************************
bool Kwave::SelectionTracker::Undo::store(Kwave::SignalManager &manager)
{
    Q_UNUSED(manager) // data has already been stored in the constructor
    return true;
}

//***************************************************************************
Kwave::UndoAction *Kwave::SelectionTracker::Undo::undo(
    Kwave::SignalManager &manager, bool with_redo)
{
    Q_UNUSED(manager)

    if (!m_tracker.isNull()) {
        QList<QUuid>   tracks = m_tracks;
        sample_index_t ofs    = m_offset;
        sample_index_t len    = m_length;

        m_tracks = m_tracker->allTracks();
        m_offset = m_tracker->offset();
        m_length = m_tracker->length();

        m_tracker->selectRange(tracks, ofs, len);
    }

    return (with_redo) ? this : nullptr;
}

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

#include "moc_SelectionTracker.cpp"
