/*
 * Copyright (c) 2013-2020 Meltytech, LLC
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "timelinecommands.h"
#include "mltcontroller.h"
#include "shotcut_mlt_properties.h"
#include "settings.h"
#include <Logger.h>
#include <QMetaObject>

namespace Timeline {

AppendCommand::AppendCommand(MultitrackModel &model, int trackIndex, const QString &xml, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_xml(xml)
    , m_undoHelper(m_model)
{
    setText(QObject::tr("Append to track"));
}


void AppendCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex;
    m_undoHelper.recordBeforeState();
    Mlt::Producer producer(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
    m_model.appendClip(m_trackIndex, producer);
    m_undoHelper.recordAfterState();
}

void AppendCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex;
    m_undoHelper.undoChanges();
}

InsertCommand::InsertCommand(MultitrackModel &model, int trackIndex,
    int position, const QString &xml, bool seek, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_position(position)
    , m_xml(xml)
    , m_undoHelper(m_model)
    , m_seek(seek)
    , m_rippleAllTracks(Settings.timelineRippleAllTracks())
{
    setText(QObject::tr("Insert into track"));
}

void InsertCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "position" << m_position;
    m_undoHelper.recordBeforeState();
    Mlt::Producer clip(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
    if (clip.type() == playlist_type) {
        Mlt::Playlist playlist(clip);
        int i = playlist.count();
        while (i--) {
            QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(i));
            clip = Mlt::Producer(info->producer);
            clip.set_in_and_out(info->frame_in, info->frame_out);
            m_model.insertClip(m_trackIndex, clip, m_position, m_rippleAllTracks, false);
        }
    } else {
        m_model.insertClip(m_trackIndex, clip, m_position, m_rippleAllTracks, m_seek);
    }
    m_undoHelper.recordAfterState();
}

void InsertCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "position" << m_position;
    m_undoHelper.undoChanges();
}

OverwriteCommand::OverwriteCommand(MultitrackModel &model, int trackIndex,
    int position, const QString &xml, bool seek, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_position(position)
    , m_xml(xml)
    , m_undoHelper(m_model)
    , m_seek(seek)
{
    setText(QObject::tr("Overwrite onto track"));
}

void OverwriteCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "position" << m_position;
    m_undoHelper.recordBeforeState();
    Mlt::Producer clip(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
    if (clip.type() == playlist_type) {
        Mlt::Playlist playlist(clip);
        int position = m_position;
        for (int i = 0; i < playlist.count(); i++) {
            QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(i));
            clip = Mlt::Producer(info->producer);
            clip.set_in_and_out(info->frame_in, info->frame_out);
            m_model.overwrite(m_trackIndex, clip, position, false);
            position += info->frame_count;
        }
    } else {
        m_model.overwrite(m_trackIndex, clip, m_position, m_seek);
    }
    m_undoHelper.recordAfterState();
}

void OverwriteCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "position" << m_position;
    m_undoHelper.undoChanges();
}

LiftCommand::LiftCommand(MultitrackModel &model, int trackIndex,
    int clipIndex, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_undoHelper(m_model)
{
    setText(QObject::tr("Lift from track"));
    m_undoHelper.setHints(UndoHelper::RestoreTracks);
}

void LiftCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
    m_undoHelper.recordBeforeState();
    m_model.liftClip(m_trackIndex, m_clipIndex);
    m_undoHelper.recordAfterState();
}

void LiftCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
    m_undoHelper.undoChanges();
}

RemoveCommand::RemoveCommand(MultitrackModel &model, int trackIndex,
    int clipIndex, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_undoHelper(m_model)
    , m_rippleAllTracks(Settings.timelineRippleAllTracks())
{
    setText(QObject::tr("Remove from track"));
    m_undoHelper.setHints(UndoHelper::RestoreTracks);
}

void RemoveCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
    m_undoHelper.recordBeforeState();
    m_model.removeClip(m_trackIndex, m_clipIndex, m_rippleAllTracks);
    m_undoHelper.recordAfterState();
}

void RemoveCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
    m_undoHelper.undoChanges();
}


NameTrackCommand::NameTrackCommand(MultitrackModel &model, int trackIndex,
    const QString &name, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_name(name)
    , m_oldName(model.data(m_model.index(trackIndex), MultitrackModel::NameRole).toString())
{
    setText(QObject::tr("Change track name"));
}

void NameTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "name" << m_name;
    m_model.setTrackName(m_trackIndex, m_name);
}

void NameTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "name" << m_name;
    m_model.setTrackName(m_trackIndex, m_oldName);
}

MergeCommand::MergeCommand(MultitrackModel& model, int trackIndex, int clipIndex, QUndoCommand * parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_undoHelper(m_model)
{
    setText(QObject::tr("Merge adjacent clips"));
}

void MergeCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipindex" << m_clipIndex;
    m_undoHelper.recordBeforeState();
    m_model.mergeClipWithNext(m_trackIndex, m_clipIndex, false);
    m_undoHelper.recordAfterState();
}

void MergeCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipindex" << m_clipIndex;
    m_undoHelper.undoChanges();
}


MuteTrackCommand::MuteTrackCommand(MultitrackModel &model, int trackIndex, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_oldValue(model.data(m_model.index(trackIndex), MultitrackModel::IsMuteRole).toBool())
{
    setText(QObject::tr("Toggle track mute"));
}

void MuteTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "mute" << !m_oldValue;
    m_model.setTrackMute(m_trackIndex, !m_oldValue);
}

void MuteTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "mute" << !m_oldValue;
    m_model.setTrackMute(m_trackIndex, m_oldValue);
}

HideTrackCommand::HideTrackCommand(MultitrackModel &model, int trackIndex, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_oldValue(model.data(m_model.index(trackIndex), MultitrackModel::IsHiddenRole).toBool())
{
    setText(QObject::tr("Toggle track hidden"));
}

void HideTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "hide" << !m_oldValue;
    m_model.setTrackHidden(m_trackIndex, !m_oldValue);
}

void HideTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "hide" << !m_oldValue;
    m_model.setTrackHidden(m_trackIndex, m_oldValue);
}

CompositeTrackCommand::CompositeTrackCommand(MultitrackModel &model, int trackIndex, bool value, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_value(value)
    , m_oldValue(model.data(m_model.index(trackIndex), MultitrackModel::IsCompositeRole).toBool())
{
    setText(QObject::tr("Change track compositing"));
}

void CompositeTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "composite" << m_value;
    m_model.setTrackComposite(m_trackIndex, m_value);
}

void CompositeTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "composite" << m_value;
    m_model.setTrackComposite(m_trackIndex, m_oldValue);
}

LockTrackCommand::LockTrackCommand(MultitrackModel &model, int trackIndex, bool value, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_value(value)
    , m_oldValue(model.data(m_model.index(trackIndex), MultitrackModel::IsLockedRole).toBool())
{
    setText(QObject::tr("Lock track"));
}

void LockTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "lock" << m_value;
    m_model.setTrackLock(m_trackIndex, m_value);
}

void LockTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "lock" << m_value;
    m_model.setTrackLock(m_trackIndex, m_oldValue);
}

MoveClipCommand::MoveClipCommand(MultitrackModel &model, int trackDelta, bool ripple, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackDelta(trackDelta)
    , m_ripple(ripple)
    , m_rippleAllTracks(Settings.timelineRippleAllTracks())
    , m_undoHelper(m_model)
    , m_redo(false)
    , m_start(-1)
{
    m_undoHelper.setHints(UndoHelper::RestoreTracks);
    m_undoHelper.recordBeforeState();
}

void MoveClipCommand::redo()
{
    LOG_DEBUG() << "track delta" << m_trackDelta;
    int trackIndex, clipIndex;
    QMultiMap<int, Mlt::Producer> newSelection;

    if (!m_redo) {
        if (m_selection.size() > 1)
            setText(QObject::tr("Move %1 timelime clips").arg(m_selection.size()));
        else
            setText(QObject::tr("Move timelime clip"));
    }
    if (m_ripple && !m_trackDelta && m_selection.size() == 1) {
        if (m_start == -1) {
            QScopedPointer<Mlt::ClipInfo> info(m_model.findClipByUuid(MLT.uuid(m_selection.first()), trackIndex, clipIndex));
            auto newStart = info->cut->get_int(kPlaylistStartProperty);
            auto mlt_index = m_model.trackList().at(trackIndex).mlt_index;
            QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(mlt_index));
            if (track) {
                Mlt::Playlist playlist(*track);
                auto targetIndex = playlist.get_clip_index_at(newStart);
                auto length = playlist.clip_length(clipIndex);
                auto targetIndexEnd = playlist.get_clip_index_at(newStart + length - 1);
                if (targetIndex >= clipIndex || // pushing clips on same track
                        // pulling clips on same track
                        ((playlist.is_blank_at(newStart) || targetIndex == clipIndex) &&
                         (playlist.is_blank_at(newStart + length - 1) || targetIndexEnd == clipIndex))) {
                    m_start = newStart;
                    m_trackIndex = trackIndex;
                    m_clipIndex = clipIndex;
                }
            }
        }
        if (m_start >= 0) {
            // Use old behavior to push or pull clips on the same track.
            m_model.moveClip(m_trackIndex, m_trackIndex, m_clipIndex, m_start, m_ripple, m_rippleAllTracks);
            m_undoHelper.recordAfterState();
            m_redo = true;
            return;
        }
    }
    // First, remove each clip.
    for (auto& clip : m_selection) {
        if (!m_redo) {
            // On the initial pass, remove clips while recording info about them.
            QScopedPointer<Mlt::ClipInfo> info(m_model.findClipByUuid(MLT.uuid(clip), trackIndex, clipIndex));
            if (info->producer && info->producer->is_valid()) {
                info->producer->set(kPlaylistIndexProperty, qBound(0, trackIndex + m_trackDelta, m_model.trackList().size() - 1));
                info->producer->pass_property(*info->cut, kPlaylistStartProperty);
                info->producer->set("_shotcut:trackIndex", trackIndex);
                info->producer->set("_shotcut:clipIndex", clipIndex);
                info->producer->set("_shotcut:in", info->frame_in);
                info->producer->set("_shotcut:out", info->frame_out);
                newSelection.insert(info->cut->get_int(kPlaylistStartProperty), info->producer);
            }
        } else {
            // On redo, use the recorded indices to remove them.
            trackIndex = clip.get_int("_shotcut:trackIndex");
            clipIndex = clip.get_int("_shotcut:clipIndex");
        }
        if (m_ripple)
            m_model.removeClip(trackIndex, clipIndex, m_rippleAllTracks);
        else
            m_model.liftClip(trackIndex, clipIndex);
    }
    if (!m_redo) {
        m_selection = newSelection;
    }
    // Next, add the save clips to their new locations.
    for (auto& clip : m_selection) {
        auto toTrack = clip.get_int(kPlaylistIndexProperty);
        auto start = clip.get_int(kPlaylistStartProperty);
        clip.set_in_and_out(clip.get_int("_shotcut:in"), clip.get_int("_shotcut:out"));
        if (start + clip.get_playtime() >= 0) {
//            LOG_DEBUG() << "adding clip" << clip.get_int("_shotcut:clipIndex") << "to track" << toTrack << "start" << start;
            if (m_ripple)
                m_model.insertClip(toTrack, clip, start, m_rippleAllTracks);
            else
                m_model.overwrite(toTrack, clip, start, false);
        }
    }

    if (!m_redo) {
        m_redo = true;
        m_undoHelper.recordAfterState();
    }
}

void MoveClipCommand::undo()
{
    LOG_DEBUG() << "track delta" << m_trackDelta;
    m_undoHelper.undoChanges();
}

TrimClipInCommand::TrimClipInCommand(MultitrackModel &model, int trackIndex, int clipIndex, int delta, bool ripple, bool redo, QUndoCommand* parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_delta(delta)
    , m_ripple(ripple)
    , m_rippleAllTracks(Settings.timelineRippleAllTracks())
    , m_redo(redo)
{
    setText(QObject::tr("Trim clip in point"));
}

void TrimClipInCommand::redo()
{
    if (m_redo) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
        m_undoHelper.reset(new UndoHelper(m_model));
        if (!m_ripple) m_undoHelper->setHints(UndoHelper::SkipXML);
        m_undoHelper->recordBeforeState();
        m_model.trimClipIn(m_trackIndex, m_clipIndex, m_delta, m_ripple, m_rippleAllTracks);
        m_undoHelper->recordAfterState();
    } else {
        Q_ASSERT(m_undoHelper);
        m_undoHelper->recordAfterState();
        m_redo = true;
    }
}

void TrimClipInCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
    Q_ASSERT(m_undoHelper);
    m_undoHelper->undoChanges();
}

bool TrimClipInCommand::mergeWith(const QUndoCommand *other)
{
    const TrimClipInCommand* that = static_cast<const TrimClipInCommand*>(other);
    LOG_DEBUG() << "this clipIndex" << m_clipIndex << "that clipIndex" << that->m_clipIndex;
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex)
        return false;
    Q_ASSERT(m_undoHelper);
    m_undoHelper->recordAfterState();
    m_delta += static_cast<const TrimClipInCommand*>(other)->m_delta;
    return true;
}

TrimClipOutCommand::TrimClipOutCommand(MultitrackModel &model, int trackIndex, int clipIndex, int delta, bool ripple, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_delta(delta)
    , m_ripple(ripple)
    , m_rippleAllTracks(Settings.timelineRippleAllTracks())
    , m_redo(redo)
{
    setText(QObject::tr("Trim clip out point"));
}

void TrimClipOutCommand::redo()
{
    if (m_redo) {
        m_undoHelper.reset(new UndoHelper(m_model));
        if (!m_ripple)
            m_undoHelper->setHints(UndoHelper::SkipXML);
        m_undoHelper->recordBeforeState();
        m_clipIndex = m_model.trimClipOut(m_trackIndex, m_clipIndex, m_delta, m_ripple, m_rippleAllTracks);
        m_undoHelper->recordAfterState();
    } else {
        Q_ASSERT(m_undoHelper);
        m_undoHelper->recordAfterState();
        m_redo = true;
    }
}

void TrimClipOutCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
    Q_ASSERT(m_undoHelper);
    m_undoHelper->undoChanges();
}

bool TrimClipOutCommand::mergeWith(const QUndoCommand *other)
{
    const TrimClipOutCommand* that = static_cast<const TrimClipOutCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex)
        return false;
    Q_ASSERT(m_undoHelper);
    m_undoHelper->recordAfterState();
    m_delta += static_cast<const TrimClipOutCommand*>(other)->m_delta;
    return true;
}

SplitCommand::SplitCommand(MultitrackModel &model, int trackIndex,
    int clipIndex, int position, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_position(position)
    , m_undoHelper(m_model)
{
    setText(QObject::tr("Split clip"));
}

void SplitCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    m_undoHelper.recordBeforeState();
    m_model.splitClip(m_trackIndex, m_clipIndex, m_position);
    m_undoHelper.recordAfterState();
}

void SplitCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    m_undoHelper.undoChanges();
}

FadeInCommand::FadeInCommand(MultitrackModel &model, int trackIndex, int clipIndex, int duration, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_duration(qMax(duration, 0))
{
    QModelIndex modelIndex = m_model.index(clipIndex, 0, m_model.index(trackIndex));
    m_previous = model.data(modelIndex, MultitrackModel::FadeInRole).toInt();
    setText(QObject::tr("Adjust fade in"));
}

void FadeInCommand::redo()
{
    m_model.fadeIn(m_trackIndex, m_clipIndex, m_duration);
}

void FadeInCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "duration" << m_duration;
    m_model.fadeIn(m_trackIndex, m_clipIndex, m_previous);
}

bool FadeInCommand::mergeWith(const QUndoCommand *other)
{
    const FadeInCommand* that = static_cast<const FadeInCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex
            || (!that->m_duration && m_duration != that->m_duration))
        return false;
    m_duration = static_cast<const FadeInCommand*>(other)->m_duration;
    return true;
}

FadeOutCommand::FadeOutCommand(MultitrackModel &model, int trackIndex, int clipIndex, int duration, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_duration(qMax(duration, 0))
{
    QModelIndex modelIndex = m_model.index(clipIndex, 0, m_model.index(trackIndex));
    m_previous = model.data(modelIndex, MultitrackModel::FadeOutRole).toInt();
    setText(QObject::tr("Adjust fade out"));
}

void FadeOutCommand::redo()
{
    m_model.fadeOut(m_trackIndex, m_clipIndex, m_duration);
}

void FadeOutCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "duration" << m_duration;
    m_model.fadeOut(m_trackIndex, m_clipIndex, m_previous);
}

bool FadeOutCommand::mergeWith(const QUndoCommand *other)
{
    const FadeOutCommand* that = static_cast<const FadeOutCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex
            || (!that->m_duration && m_duration != that->m_duration))
        return false;
    m_duration = static_cast<const FadeOutCommand*>(other)->m_duration;
    return true;
}

AddTransitionCommand::AddTransitionCommand(MultitrackModel &model, int trackIndex, int clipIndex, int position, bool ripple, QUndoCommand *parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_position(position)
    , m_transitionIndex(-1)
    , m_ripple(ripple)
    , m_undoHelper(model)
    , m_rippleAllTracks(Settings.timelineRippleAllTracks())
{
    setText(QObject::tr("Add transition"));
}

void AddTransitionCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    m_undoHelper.recordBeforeState();
    m_transitionIndex = m_model.addTransition(m_trackIndex, m_clipIndex, m_position, m_ripple, m_rippleAllTracks);
    LOG_DEBUG() << "m_transitionIndex" << m_transitionIndex;
    m_undoHelper.recordAfterState();
}

void AddTransitionCommand::undo()
{
    if (m_transitionIndex >= 0) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
        m_undoHelper.undoChanges();
    }
}

TrimTransitionInCommand::TrimTransitionInCommand(MultitrackModel &model, int trackIndex, int clipIndex, int delta, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_delta(delta)
    , m_notify(false)
    , m_redo(redo)
{
    setText(QObject::tr("Trim transition in point"));
}

void TrimTransitionInCommand::redo()
{
    if (m_redo) {
        m_model.trimTransitionIn(m_trackIndex, m_clipIndex, m_delta);
        if (m_notify && m_clipIndex >= 0)
            m_model.notifyClipIn(m_trackIndex, m_clipIndex);
    } else {
        m_redo = true;
    }
}

void TrimTransitionInCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
    if (m_clipIndex >= 0) {
        m_model.trimTransitionIn(m_trackIndex, m_clipIndex, -m_delta);
        m_model.notifyClipIn(m_trackIndex, m_clipIndex);
        m_notify = true;
    }
    else LOG_WARNING() << "invalid clip index" << m_clipIndex;
}

bool TrimTransitionInCommand::mergeWith(const QUndoCommand *other)
{
    const TrimTransitionInCommand* that = static_cast<const TrimTransitionInCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex)
        return false;
    m_delta += static_cast<const TrimTransitionInCommand*>(other)->m_delta;
    return true;
}

TrimTransitionOutCommand::TrimTransitionOutCommand(MultitrackModel &model, int trackIndex, int clipIndex, int delta, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_delta(delta)
    , m_notify(false)
    , m_redo(redo)
{
    setText(QObject::tr("Trim transition out point"));
}

void TrimTransitionOutCommand::redo()
{
    if (m_redo) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
        m_model.trimTransitionOut(m_trackIndex, m_clipIndex, m_delta);
        if (m_notify && m_clipIndex >= 0)
            m_model.notifyClipOut(m_trackIndex, m_clipIndex);
    } else {
        m_redo = true;
    }
}

void TrimTransitionOutCommand::undo()
{
    if (m_clipIndex >= 0) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
        m_model.trimTransitionOut(m_trackIndex, m_clipIndex, -m_delta);
        m_model.notifyClipOut(m_trackIndex, m_clipIndex);
        m_notify = true;
    }
    else LOG_WARNING() << "invalid clip index" << m_clipIndex;
}

bool TrimTransitionOutCommand::mergeWith(const QUndoCommand *other)
{
    const TrimTransitionOutCommand* that = static_cast<const TrimTransitionOutCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex)
        return false;
    m_delta += static_cast<const TrimTransitionOutCommand*>(other)->m_delta;
    return true;
}

AddTransitionByTrimInCommand::AddTransitionByTrimInCommand(MultitrackModel &model, int trackIndex, int clipIndex, int duration, int trimDelta, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_duration(duration)
    , m_trimDelta(trimDelta)
    , m_notify(false)
    , m_redo(redo)
{
    setText(QObject::tr("Add transition"));
}

void AddTransitionByTrimInCommand::redo()
{
    if (m_redo) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_trimDelta << "duration" << m_duration;
        if (m_trimDelta)
            m_model.trimClipIn(m_trackIndex, m_clipIndex + 1, m_trimDelta, false, false);
        m_model.addTransitionByTrimIn(m_trackIndex, m_clipIndex, m_duration);
        if (m_notify && m_clipIndex > 0)
            m_model.notifyClipOut(m_trackIndex, m_clipIndex - 1);
    } else {
        m_redo = true;
    }
}

void AddTransitionByTrimInCommand::undo()
{
    if (m_clipIndex > 0) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_trimDelta;
        m_model.removeTransitionByTrimIn(m_trackIndex, m_clipIndex, -m_trimDelta);
        m_notify = true;
    }
    else LOG_WARNING() << "invalid clip index" << m_clipIndex;
}

bool AddTransitionByTrimInCommand::mergeWith(const QUndoCommand *other)
{
    const AddTransitionByTrimInCommand* that = static_cast<const AddTransitionByTrimInCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex ||
        (that->m_clipIndex != m_clipIndex && m_clipIndex != that->m_clipIndex - 1))
        return false;
    return true;
}

RemoveTransitionByTrimInCommand::RemoveTransitionByTrimInCommand(MultitrackModel &model, int trackIndex, int clipIndex, int delta, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_delta(delta)
    , m_redo(redo)
{
    setText(QObject::tr("Remove transition"));
}

void RemoveTransitionByTrimInCommand::redo()
{
    if (m_redo) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
        QModelIndex modelIndex = m_model.makeIndex(m_trackIndex, m_clipIndex);
        int n = m_model.data(modelIndex, MultitrackModel::DurationRole).toInt();
        m_model.liftClip(m_trackIndex, m_clipIndex);
        m_model.trimClipIn(m_trackIndex, m_clipIndex + 1, -n, false, false);
        m_model.notifyClipIn(m_trackIndex, m_clipIndex + 1);
    } else {
        m_redo = true;
    }
}

void RemoveTransitionByTrimInCommand::undo()
{
    if (m_clipIndex > 0) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
        m_model.addTransitionByTrimOut(m_trackIndex, m_clipIndex - 1, m_delta);
        m_model.notifyClipIn(m_trackIndex, m_clipIndex + 1);
    }
    else LOG_WARNING() << "invalid clip index" << m_clipIndex;
}

RemoveTransitionByTrimOutCommand::RemoveTransitionByTrimOutCommand(MultitrackModel &model, int trackIndex, int clipIndex, int delta, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_delta(delta)
    , m_redo(redo)
{
    setText(QObject::tr("Remove transition"));
}

void RemoveTransitionByTrimOutCommand::redo()
{
    if (m_redo) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex;
        QModelIndex modelIndex = m_model.makeIndex(m_trackIndex, m_clipIndex);
        int n = m_model.data(modelIndex, MultitrackModel::DurationRole).toInt();
        m_model.liftClip(m_trackIndex, m_clipIndex);
        m_model.trimClipOut(m_trackIndex, m_clipIndex - 1, -n, false, false);
        m_model.notifyClipOut(m_trackIndex, m_clipIndex - 1);
    } else {
        m_redo = true;
    }
}

void RemoveTransitionByTrimOutCommand::undo()
{
    if (m_clipIndex > 0) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_delta;
        m_model.addTransitionByTrimIn(m_trackIndex, m_clipIndex, m_delta);
        m_model.notifyClipOut(m_trackIndex, m_clipIndex - 1);
    }
    else LOG_WARNING() << "invalid clip index" << m_clipIndex;
}

AddTransitionByTrimOutCommand::AddTransitionByTrimOutCommand(MultitrackModel &model, int trackIndex, int clipIndex, int duration, int trimDelta, bool redo, QUndoCommand *parent)
    : TrimCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_duration(duration)
    , m_trimDelta(trimDelta)
    , m_notify(false)
    , m_redo(redo)
{
    setText(QObject::tr("Add transition"));
}

void AddTransitionByTrimOutCommand::redo()
{
    if (m_redo) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_trimDelta << "duration" << m_duration;
        if (m_trimDelta)
            m_model.trimClipOut(m_trackIndex, m_clipIndex, m_trimDelta, false, false);
        m_model.addTransitionByTrimOut(m_trackIndex, m_clipIndex, m_duration);
        if (m_notify)
            m_model.notifyClipIn(m_trackIndex, m_clipIndex + 2);
    } else {
        m_redo = true;
    }
}

void AddTransitionByTrimOutCommand::undo()
{
    if (m_clipIndex + 2 < m_model.rowCount(m_model.index(m_trackIndex))) {
        LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "delta" << m_trimDelta;
        m_model.removeTransitionByTrimOut(m_trackIndex, m_clipIndex, -m_trimDelta);
        m_notify = true;
    }
    else LOG_WARNING() << "invalid clip index" << m_clipIndex;
}

bool AddTransitionByTrimOutCommand::mergeWith(const QUndoCommand *other)
{
    const AddTransitionByTrimOutCommand* that = static_cast<const AddTransitionByTrimOutCommand*>(other);
    if (that->id() != id() || that->m_trackIndex != m_trackIndex || that->m_clipIndex != m_clipIndex)
        return false;
    return true;
}

AddTrackCommand::AddTrackCommand(MultitrackModel& model, bool isVideo, QUndoCommand* parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_isVideo(isVideo)
{
    if (isVideo)
        setText(QObject::tr("Add video track"));
    else
        setText(QObject::tr("Add audio track"));
}

void AddTrackCommand::redo()
{
    LOG_DEBUG() << (m_isVideo? "video" : "audio");
    if (m_isVideo)
        m_trackIndex = m_model.addVideoTrack();
    else
        m_trackIndex = m_model.addAudioTrack();
}

void AddTrackCommand::undo()
{
    LOG_DEBUG() << (m_isVideo? "video" : "audio");
    m_model.removeTrack(m_trackIndex);
}

InsertTrackCommand::InsertTrackCommand(MultitrackModel& model, int trackIndex, QUndoCommand* parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_trackType(model.trackList().size() > 0 ? model.trackList().at(trackIndex).type : VideoTrackType)
{
    if (m_trackType == AudioTrackType)
        setText(QObject::tr("Insert audio track"));
    else if (m_trackType == VideoTrackType)
        setText(QObject::tr("Insert video track"));
}

void InsertTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "type" << (m_trackType == AudioTrackType? "audio" : "video");
    m_model.insertTrack(m_trackIndex, m_trackType);
}

void InsertTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "type" << (m_trackType == AudioTrackType? "audio" : "video");
    m_model.removeTrack(m_trackIndex);
}

RemoveTrackCommand::RemoveTrackCommand(MultitrackModel& model, int trackIndex, QUndoCommand* parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_trackType(model.trackList().at(trackIndex).type)
    , m_undoHelper(model)
{
    if (m_trackType == AudioTrackType)
        setText(QObject::tr("Remove audio track"));
    else if (m_trackType == VideoTrackType)
        setText(QObject::tr("Remove video track"));

    // Get the track as MLT playlist.
    int mlt_index = m_model.trackList().at(m_trackIndex).mlt_index;
    QScopedPointer<Mlt::Producer> producer(m_model.tractor()->multitrack()->track(mlt_index));
    if (producer && producer->is_valid()) {
        // Save track name.
        m_trackName = QString::fromUtf8(producer->get(kTrackNameProperty));
        // Save the track filters.
        if (producer->filter_count() > 0) {
            m_filtersProducer.reset(new Mlt::Producer(MLT.profile(), "color"));
            if (m_filtersProducer->is_valid())
                MLT.copyFilters(*producer, *m_filtersProducer);
        }
    }
}

void RemoveTrackCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "type" << (m_trackType == AudioTrackType? "audio" : "video");
    m_undoHelper.recordBeforeState();
    int mlt_index = m_model.trackList().at(m_trackIndex).mlt_index;
    QScopedPointer<Mlt::Producer> producer(m_model.tractor()->multitrack()->track(mlt_index));
    Mlt::Playlist playlist(*producer);
    playlist.clear();
    m_undoHelper.recordAfterState();
    m_model.removeTrack(m_trackIndex);
}

void RemoveTrackCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "type" << (m_trackType == AudioTrackType? "audio" : "video");
    m_model.insertTrack(m_trackIndex, m_trackType);
    m_model.setTrackName(m_trackIndex, m_trackName);

    // Restore track contents from UndoHelper.
    m_undoHelper.undoChanges();

    // Re-attach filters.
    int mlt_index = m_model.trackList().at(m_trackIndex).mlt_index;
    QScopedPointer<Mlt::Producer> producer(m_model.tractor()->multitrack()->track(mlt_index));
    Mlt::Playlist playlist(*producer);
    if (playlist.is_valid() && m_filtersProducer &&  m_filtersProducer->is_valid()) {
        MLT.copyFilters(*m_filtersProducer, playlist);
        QModelIndex modelIndex = m_model.index(m_trackIndex);
        emit m_model.dataChanged(modelIndex, modelIndex, QVector<int>() << MultitrackModel::IsFilteredRole);
    }
}

ChangeBlendModeCommand::ChangeBlendModeCommand(Mlt::Transition& transition, const QString& propertyName, const QString& mode, QUndoCommand* parent)
    : QUndoCommand(parent)
    , m_transition(transition)
    , m_propertyName(propertyName)
    , m_newMode(mode)
{
    setText(QObject::tr("Change track blend mode"));
    m_oldMode = m_transition.get(m_propertyName.toLatin1().constData());
}

void ChangeBlendModeCommand::redo()
{
    LOG_DEBUG() << "mode" << m_newMode;
    if (!m_newMode.isEmpty()) {
        m_transition.set("disable", 0);
        m_transition.set(m_propertyName.toLatin1().constData(), m_newMode.toUtf8().constData());
    } else {
        m_transition.set("disable", 1);
    }
    MLT.refreshConsumer();
    emit modeChanged(m_newMode);
}

void ChangeBlendModeCommand::undo()
{
    LOG_DEBUG() << "mode" << m_newMode;
    if (!m_oldMode.isEmpty()) {
        m_transition.set("disable", 0);
        m_transition.set(m_propertyName.toLatin1().constData(), m_oldMode.toUtf8().constData());
    } else {
        m_transition.set("disable", 1);
    }
    MLT.refreshConsumer();
    emit modeChanged(m_oldMode);
}

UpdateCommand::UpdateCommand(TimelineDock& timeline, int trackIndex, int clipIndex,
    int position, QUndoCommand* parent)
    : QUndoCommand(parent)
    , m_timeline(timeline)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_position(position)
    , m_isFirstRedo(true)
    , m_undoHelper(*timeline.model())
{
    setText(QObject::tr("Change clip properties"));
    m_undoHelper.recordBeforeState();
}

void UpdateCommand::setPosition(int trackIndex, int clipIndex, int position)
{
    if (trackIndex >= 0)
        m_trackIndex = trackIndex;
    if (clipIndex >= 0)
        m_clipIndex = clipIndex;
    if (position >= 0)
        m_position = position;
    m_undoHelper.recordBeforeState();
}

void UpdateCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    if (!m_isFirstRedo)
        m_undoHelper.recordBeforeState();
    Mlt::Producer clip(MLT.profile(), "xml-string", m_xmlAfter.toUtf8().constData());
    m_timeline.model()->liftClip(m_trackIndex, m_clipIndex);
    m_timeline.model()->overwrite(m_trackIndex, clip, m_position, false);
    m_undoHelper.recordAfterState();
}

void UpdateCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    m_undoHelper.undoChanges();
    m_timeline.emitSelectedFromSelection();
    m_isFirstRedo = false;
}

DetachAudioCommand::DetachAudioCommand(MultitrackModel& model, int trackIndex, int clipIndex, int position, const QString& xml, QUndoCommand* parent)
    : QUndoCommand(parent)
    , m_model(model)
    , m_trackIndex(trackIndex)
    , m_clipIndex(clipIndex)
    , m_position(position)
    , m_targetTrackIndex(-1)
    , m_xml(xml)
    , m_undoHelper(m_model)
    , m_trackAdded(false)
{
    setText(QObject::tr("Detach Audio"));
}

void DetachAudioCommand::redo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    Mlt::Producer clip(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
    if (clip.is_valid()) {
        // Save the original clip's audio_index.
        m_audioIndex = QString::fromLatin1(clip.get("audio_index"));

        // Disable audio on the original clip.
        int i = m_model.trackList().at(m_trackIndex).mlt_index;
        Mlt::Producer track(m_model.tractor()->track(i));
        if (track.is_valid()) {
            Mlt::Playlist playlist(track);
            Mlt::Producer originalClip(playlist.get_clip(m_clipIndex));
            if (originalClip.is_valid()) {
                originalClip.parent().set("audio_index", -1);
                QModelIndex modelIndex = m_model.makeIndex(m_trackIndex, m_clipIndex);
                emit m_model.dataChanged(modelIndex, modelIndex, QVector<int>() << MultitrackModel::AudioIndexRole);
            }
        }

        // Add an audio track if needed.
        int n = m_model.trackList().size();
        for (int i = 0; i < n; i++) {
            Mlt::Producer track(m_model.tractor()->track(m_model.trackList()[i].mlt_index));
            if (!track.is_valid())
                continue;
            int hide = track.get_int("hide");
            // hide: 1 = audio only track, 3 = muted audio-only track
            if (track.get(kAudioTrackProperty) || hide == 1 || hide == 3) {
                Mlt::Playlist playlist(track);
                int out = clip.get_playtime() - 1;
                // If the audio track is blank in the target region.
                if (playlist.is_blank_at(m_position) && playlist.is_blank_at(m_position + out)
                        && playlist.get_clip_index_at(m_position) == playlist.get_clip_index_at(m_position + out)) {
                    // Save the target track index.
                    m_targetTrackIndex = i;
                    break;
                }
            }
        }
        if (m_targetTrackIndex == -1) {
            // No target audio track
            m_targetTrackIndex = m_model.addAudioTrack();
            m_trackAdded = m_targetTrackIndex > -1;
        }

        if (m_targetTrackIndex > -1) {
            // Add the clip to the new audio track.
            m_undoHelper.recordBeforeState();
            m_model.overwrite(m_targetTrackIndex, clip, m_position, false);
            m_undoHelper.recordAfterState();
        }
    }
}

void DetachAudioCommand::undo()
{
    LOG_DEBUG() << "trackIndex" << m_trackIndex << "clipIndex" << m_clipIndex << "position" << m_position;
    if (m_trackAdded) {
        // Remove the new audio track.
        m_model.removeTrack(m_targetTrackIndex);
        m_targetTrackIndex = -1;
    } else {
        // Remove the clip from the audio track.
        m_undoHelper.undoChanges();
    }

    // Restore the audio stream index on the original clip.
    int i = m_model.trackList().at(m_trackIndex).mlt_index;
    Mlt::Producer track(m_model.tractor()->track(i));
    if (track.is_valid()) {
        Mlt::Playlist playlist(track);
        Mlt::Producer clip(playlist.get_clip(m_clipIndex));
        if (clip.is_valid()) {
            clip.parent().set("audio_index", m_audioIndex.toLatin1().constData());
            QModelIndex modelIndex = m_model.makeIndex(m_trackIndex, m_clipIndex);
            emit m_model.dataChanged(modelIndex, modelIndex, QVector<int>() << MultitrackModel::AudioIndexRole);
        }
    }
}

} // namespace

#include "moc_timelinecommands.cpp"
