/*
 * Copyright (c) 2018-2026 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 "keyframesmodel.h"

#include "Logger.h"
#include "mltcontroller.h"
#include "qmltypes/qmlapplication.h"
#include "qmltypes/qmlfilter.h"
#include "qmltypes/qmlmetadata.h"

#include <QRegularExpression>
#include <QTimer>

static const quintptr NO_PARENT_ID = quintptr(-1);

/*!
    \qmltype KeyframesModel
    \inqmlmodule org.shotcut.qml
    \brief A two-level item model describing a filter's keyframeable parameters and their keyframes.

    \c KeyframesModel is available as the \c parameters context property in the Keyframes panel.
    The top level contains one row per keyframeable parameter; each parameter row contains
    one child row per keyframe.

    \section2 InterpolationType enum

    Passed to \l setInterpolation() and \l addKeyframe():

    \table
    \header \li Value \li Name
    \row \li 0 \li DiscreteInterpolation
    \row \li 1 \li LinearInterpolation
    \row \li 2 \li SmoothLooseInterpolation
    \row \li 3 \li SmoothNaturalInterpolation
    \row \li 4 \li SmoothTightInterpolation
    \row \li 5–7 \li EaseInSinusoidal / EaseOutSinusoidal / EaseInOutSinusoidal
    \row \li 8–10 \li EaseIn/Out/InOutQuadratic
    \row \li 11–13 \li EaseIn/Out/InOutCubic
    \row \li 14–16 \li EaseIn/Out/InOutQuartic
    \row \li 17–19 \li EaseIn/Out/InOutQuintic
    \row \li 20–22 \li EaseIn/Out/InOutExponential
    \row \li 23–25 \li EaseIn/Out/InOutCircular
    \row \li 26–28 \li EaseIn/Out/InOutBack
    \row \li 29–31 \li EaseIn/Out/InOutElastic
    \row \li 32–34 \li EaseIn/Out/InOutBounce
    \endtable
*/

/*!
    \qmlsignal KeyframesModel::loaded()
    \brief Emitted after the model has been populated for a new filter.
*/

/*!
    \qmlsignal KeyframesModel::keyframeAdded(string parameter, int position)
    \brief Emitted when a keyframe is added. \a parameter is the MLT property name;
    \a position is the frame number.
*/

KeyframesModel::KeyframesModel(QObject *parent)
    : QAbstractItemModel(parent)
{}

KeyframesModel::~KeyframesModel() {}

int KeyframesModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        // keyframes
        if (parent.row() < m_keyframeCounts.count())
            return m_keyframeCounts[parent.row()];
        return 0;
    }
    // parameters
    return m_propertyNames.count();
}

int KeyframesModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 1;
}

QVariant KeyframesModel::data(const QModelIndex &index, int role) const
{
    if (!m_metadata || !index.isValid())
        return QVariant();
    if (index.parent().isValid()) {
        //        LOG_DEBUG() << "keyframe" << index.internalId() << index.row() << role;
        // keyframes
        if (m_filter && index.internalId() < quintptr(m_propertyNames.count())) {
            QString name = m_propertyNames[index.internalId()];
            Mlt::Animation animation = m_filter->getAnimation(name);
            if (animation.is_valid()) {
                int position = const_cast<Mlt::Animation &>(animation).key_get_frame(index.row());
                if (position >= 0) {
                    switch (role) {
                    case Qt::DisplayRole:
                    case NameRole: {
                        QString type = tr("Linear");
                        switch (const_cast<Mlt::Animation &>(animation).key_get_type(index.row())) {
                        case mlt_keyframe_discrete:
                            type = tr("Hold");
                            break;
                        case mlt_keyframe_smooth_loose:
                        case mlt_keyframe_smooth_natural:
                        case mlt_keyframe_smooth_tight:
                            type = tr("Smooth");
                            break;
                        case mlt_keyframe_sinusoidal_in:
                            type = tr("Ease In Sinusoidal");
                            break;
                        case mlt_keyframe_sinusoidal_out:
                            type = tr("Ease Out Sinusoidal");
                            break;
                        case mlt_keyframe_sinusoidal_in_out:
                            type = tr("Ease In/Out Sinusoidal");
                            break;
                        case mlt_keyframe_quadratic_in:
                            type = tr("Ease In Quadratic");
                            break;
                        case mlt_keyframe_quadratic_out:
                            type = tr("Ease Out Quadratic");
                            break;
                        case mlt_keyframe_quadratic_in_out:
                            type = tr("Ease In/Out Quadratic");
                            break;
                        case mlt_keyframe_cubic_in:
                            type = tr("Ease In Cubic");
                            break;
                        case mlt_keyframe_cubic_out:
                            type = tr("Ease Out Cubic");
                            break;
                        case mlt_keyframe_cubic_in_out:
                            type = tr("Ease In/Out Cubic");
                            break;
                        case mlt_keyframe_quartic_in:
                            type = tr("Ease In Quartic");
                            break;
                        case mlt_keyframe_quartic_out:
                            type = tr("Ease Out Quartic");
                            break;
                        case mlt_keyframe_quartic_in_out:
                            type = tr("Ease In/Out Quartic");
                            break;
                        case mlt_keyframe_quintic_in:
                            type = tr("Ease In Quintic");
                            break;
                        case mlt_keyframe_quintic_out:
                            type = tr("Ease Out Quintic");
                            break;
                        case mlt_keyframe_quintic_in_out:
                            type = tr("Ease In/Out Quintic");
                            break;
                        case mlt_keyframe_exponential_in:
                            type = tr("Ease In Exponential");
                            break;
                        case mlt_keyframe_exponential_out:
                            type = tr("Ease Out Exponential");
                            break;
                        case mlt_keyframe_exponential_in_out:
                            type = tr("Ease In/Out Exponential");
                            break;
                        case mlt_keyframe_circular_in:
                            type = tr("Ease In Circular");
                            break;
                        case mlt_keyframe_circular_out:
                            type = tr("Ease Out Circular");
                            break;
                        case mlt_keyframe_circular_in_out:
                            type = tr("Ease In/Out Circular");
                            break;
                        case mlt_keyframe_back_in:
                            type = tr("Ease In Back");
                            break;
                        case mlt_keyframe_back_out:
                            type = tr("Ease Out Back");
                            break;
                        case mlt_keyframe_back_in_out:
                            type = tr("Ease In/Out Back");
                            break;
                        case mlt_keyframe_elastic_in:
                            type = tr("Ease In Elastic");
                            break;
                        case mlt_keyframe_elastic_out:
                            type = tr("Ease Out Elastic");
                            break;
                        case mlt_keyframe_elastic_in_out:
                            type = tr("Ease In/Out Elastic");
                            break;
                        case mlt_keyframe_bounce_in:
                            type = tr("Ease In Bounce");
                            break;
                        case mlt_keyframe_bounce_out:
                            type = tr("Ease Out Bounce");
                            break;
                        case mlt_keyframe_bounce_in_out:
                            type = tr("Ease In/Out Bounce");
                            break;
                        default:
                            break;
                        }
                        double value = m_filter->getDouble(name, position);
                        QString units = m_metadata->keyframes()
                                            ->parameter(m_metadataIndex[index.internalId()])
                                            ->units();
                        return QStringLiteral("%1 - %2\n%3%4")
                            .arg(QmlApplication::singleton().timeFromFrames(position))
                            .arg(type)
                            .arg(value)
                            .arg(units);
                    }
                    case FrameNumberRole:
                        return position;
                    case KeyframeTypeRole:
                        if (index.row() >= animation.key_count() - 1) {
                            return DiscreteInterpolation;
                        }
                        return const_cast<Mlt::Animation &>(animation).key_get_type(index.row());
                    case PrevKeyframeTypeRole:
                        if (index.row() <= 0) {
                            return DiscreteInterpolation;
                        }
                        return const_cast<Mlt::Animation &>(animation).key_get_type(index.row() - 1);
                    case NumericValueRole:
                        return m_filter->getDouble(name, position);
                    case MinimumFrameRole: {
                        int result = 0;
                        if (animation.previous_key(position - 1, result)) {
                            // first Keyframe
                            result = 0;
                        } else {
                            result += 1;
                        }

                        //                        LOG_DEBUG() << "keyframeIndex" << index.row() << "minimumFrame" << result;
                        return result;
                    }
                    case MaximumFrameRole: {
                        int result = 0;
                        if (animation.next_key(position + 1, result)) {
                            // Last Keyframe
                            result = m_filter->producer().get_out();
                        } else {
                            result -= 1;
                        }
                        //                        LOG_DEBUG() << "keyframeIndex" << index.row() << "maximumFrame" << result;
                        return result;
                    }
                    default:
                        break;
                    }
                }
            }
        }
    } else if (index.row() < m_metadata->keyframes()->parameterCount()) {
        //        LOG_DEBUG() << "parameter" << index.row() << role;
        // parameters
        switch (role) {
        case Qt::DisplayRole:
        case NameRole:
            return m_metadata->keyframes()->parameter(m_metadataIndex[index.row()])->name();
        case PropertyNameRole:
            return m_metadata->keyframes()->parameter(m_metadataIndex[index.row()])->property();
        case IsCurveRole:
            return m_metadata->keyframes()->parameter(m_metadataIndex[index.row()])->isCurve();
        case MinimumValueRole: {
            QmlKeyframesParameter *param = m_metadata->keyframes()->parameter(
                m_metadataIndex[index.row()]);
            if (param->rangeType() == QmlKeyframesParameter::MinMax) {
                return m_metadata->keyframes()->parameter(m_metadataIndex[index.row()])->minimum();
            } else if (param->rangeType() == QmlKeyframesParameter::ClipLength) {
                return 0.0;
            }
            return 0.0;
        }
        case MaximumValueRole: {
            QmlKeyframesParameter *param = m_metadata->keyframes()->parameter(
                m_metadataIndex[index.row()]);
            if (param->rangeType() == QmlKeyframesParameter::MinMax) {
                return m_metadata->keyframes()->parameter(m_metadataIndex[index.row()])->maximum();
            } else if (param->rangeType() == QmlKeyframesParameter::ClipLength) {
                int length = m_filter->producer().get_length() - m_filter->in();
                return (double) length / MLT.profile().fps();
            }
            return 0.0;
        }
        case LowestValueRole: {
            QmlKeyframesParameter *param = m_metadata->keyframes()->parameter(
                m_metadataIndex[index.row()]);
            Mlt::Animation animation = m_filter->getAnimation(param->property());
            double min = std::numeric_limits<double>::max();
            if (animation.is_valid()) {
                for (int i = 0; i < animation.key_count(); i++) {
                    int frame = animation.key_get_frame(i);
                    if (frame >= 0) {
                        double value = m_filter->getDouble(param->property(), frame);
                        if (value < min)
                            min = value;
                    }
                }
            }
            if (min == std::numeric_limits<double>::max())
                min = 0;
            return min;
        }
        case HighestValueRole: {
            QmlKeyframesParameter *param = m_metadata->keyframes()->parameter(
                m_metadataIndex[index.row()]);
            Mlt::Animation animation = m_filter->getAnimation(param->property());
            double max = std::numeric_limits<double>::lowest();
            if (animation.is_valid()) {
                for (int i = 0; i < animation.key_count(); i++) {
                    int frame = animation.key_get_frame(i);
                    if (frame >= 0) {
                        double value = m_filter->getDouble(param->property(), frame);
                        if (value > max)
                            max = value;
                    }
                }
            }
            if (max == std::numeric_limits<double>::lowest())
                max = 0;
            return max;
        }
        default:
            break;
        }
    }
    return QVariant();
}

QModelIndex KeyframesModel::index(int row, int column, const QModelIndex &parent) const
{
    if (column > 0)
        return QModelIndex();
    QModelIndex result;
    if (parent.isValid()) {
        // keyframes
        result = createIndex(row, column, parent.row());
    } else if (row < m_propertyNames.count()) {
        result = createIndex(row, column, NO_PARENT_ID);
    }
    return result;
}

QModelIndex KeyframesModel::parent(const QModelIndex &index) const
{
    if (!index.isValid() || index.internalId() == NO_PARENT_ID)
        return QModelIndex();
    else
        return createIndex(index.internalId(), 0, NO_PARENT_ID);
}

QHash<int, QByteArray> KeyframesModel::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[NameRole] = "name";
    roles[PropertyNameRole] = "property";
    roles[IsCurveRole] = "isCurve";
    roles[MinimumValueRole] = "minimum";
    roles[MaximumValueRole] = "maximum";
    roles[LowestValueRole] = "lowest";
    roles[HighestValueRole] = "highest";
    roles[FrameNumberRole] = "frame";
    roles[KeyframeTypeRole] = "interpolation";
    roles[PrevKeyframeTypeRole] = "prevInterpolation";
    roles[NumericValueRole] = "value";
    roles[MinimumFrameRole] = "minimumFrame";
    roles[MaximumFrameRole] = "maximumFrame";
    return roles;
}

void KeyframesModel::load(QmlFilter *filter, QmlMetadata *meta)
{
    beginResetModel();
    m_propertyNames.clear();
    m_keyframeCounts.clear();
    m_metadataIndex.clear();
    m_filter = filter;
    m_metadata = meta;
    if (m_filter && m_metadata && m_filter->animateIn() <= 0 && m_filter->animateOut() <= 0)
        for (int i = 0; i < m_metadata->keyframes()->parameterCount(); i++) {
            if (m_filter->keyframeCount(m_metadata->keyframes()->parameter(i)->property()) > 0) {
                m_propertyNames << m_metadata->keyframes()->parameter(i)->property();
                m_keyframeCounts << keyframeCount(m_propertyNames.count() - 1);
                m_metadataIndex << i;
                //        LOG_DEBUG() << m_propertyNames.last() << m_filter->get(m_propertyNames.last()) << keyframeCount(i);
            }
        }
    endResetModel();
    emit loaded();
}

/*!
    \qmlmethod bool KeyframesModel::remove(int parameterIndex, int keyframeIndex)
    \brief Removes the keyframe at \a keyframeIndex on parameter \a parameterIndex.
    Returns \c true on success.
*/

bool KeyframesModel::remove(int parameterIndex, int keyframeIndex)
{
    bool error = true;
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        Mlt::Animation animation = m_filter->getAnimation(name);
        // Do not allow the user to delete the last keyframe.
        // Keyframes should be disabled in the filter panel instead
        if (animation.is_valid() && animation.key_count() > 1) {
            int frame_num = animation.key_get_frame(keyframeIndex);
            error = animation.remove(frame_num);
            if (!error) {
                m_filter->startUndoRemoveKeyframeCommand();
                animation.interpolate();
                m_filter->updateUndoCommand(name);
                beginRemoveRows(index(parameterIndex), keyframeIndex, keyframeIndex);
                m_keyframeCounts[parameterIndex] -= 1;
                endRemoveRows();
                for (auto &key : gangedProperties(parameterIndex)) {
                    Mlt::Animation animation = m_filter->getAnimation(key);
                    if (animation.is_valid() && !animation.remove(frame_num)) {
                        animation.interpolate();
                        m_filter->updateUndoCommand(key);
                    }
                }
                mlt_event_data eventData = mlt_event_data_from_string(name.toUtf8().constData());
                mlt_events_fire(m_filter->service().get_properties(), "property-changed", eventData);
                QModelIndex modelIndex;
                if (keyframeIndex > 0) {
                    modelIndex = index(keyframeIndex - 1, 0, index(parameterIndex));
                    emit dataChanged(modelIndex, modelIndex, QVector<int>() << MaximumFrameRole);
                }
                if (keyframeIndex < keyframeCount(parameterIndex)) {
                    modelIndex = index(keyframeIndex, 0, index(parameterIndex));
                    emit dataChanged(modelIndex, modelIndex, QVector<int>() << MinimumFrameRole);
                }
                emit dataChanged(index(parameterIndex),
                                 index(parameterIndex),
                                 QVector<int>() << LowestValueRole << HighestValueRole);
                emit m_filter->changed(name.toUtf8().constData());
                m_filter->endUndoCommand();
            }
        }
    }
    return error;
}

int KeyframesModel::previousKeyframePosition(int parameterIndex, int currentPosition)
{
    int result = -1;
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        Mlt::Animation animation = m_filter->getAnimation(name);
        if (animation.is_valid()) {
            currentPosition -= m_filter->in();
            bool error = animation.previous_key(animation.is_key(currentPosition)
                                                    ? currentPosition - 1
                                                    : currentPosition,
                                                result);
            if (!error)
                result += m_filter->in();
        }
    }
    return result;
}

int KeyframesModel::nextKeyframePosition(int parameterIndex, int currentPosition)
{
    int result = -1;
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        Mlt::Animation animation = m_filter->getAnimation(name);
        if (animation.is_valid()) {
            currentPosition -= m_filter->in();
            bool error = animation.next_key(animation.is_key(currentPosition) ? currentPosition + 1
                                                                              : currentPosition,
                                            result);
            if (!error)
                result += m_filter->in();
        }
    }
    return result;
}

/*!
    \qmlmethod int KeyframesModel::keyframeIndex(int parameterIndex, int currentPosition)
    \brief Returns the keyframe index at time \a currentPosition for the given \a parameterIndex,
    or \c -1 if no keyframe exists at that position.
*/

int KeyframesModel::keyframeIndex(int parameterIndex, int currentPosition)
{
    int result = -1;
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        Mlt::Animation animation = m_filter->getAnimation(name);
        if (animation.is_valid()) {
            for (int i = 0; i < animation.key_count() && result == -1; i++) {
                int frame = animation.key_get_frame(i);
                if (frame == currentPosition)
                    result = i;
                else if (frame > currentPosition)
                    break;
            }
        }
    }
    return result;
}

/*!
    \qmlmethod int KeyframesModel::parameterIndex(string propertyName)
    \brief Returns the row index of the parameter identified by \a propertyName, or \c -1.
*/

int KeyframesModel::parameterIndex(const QString &propertyName) const
{
    return m_propertyNames.indexOf(propertyName);
}

/*!
    \qmlmethod bool KeyframesModel::setInterpolation(int parameterIndex, int keyframeIndex, int type)
    \brief Sets the interpolation \a type for the keyframe at \a keyframeIndex on \a parameterIndex.
    \a type is a value from the \c InterpolationType enum.
    Returns \c true on success.
*/

bool KeyframesModel::setInterpolation(int parameterIndex, int keyframeIndex, InterpolationType type)
{
    bool error = true;
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        Mlt::Animation animation = m_filter->getAnimation(name);
        if (animation.is_valid()) {
            if (!animation.key_set_type(keyframeIndex, mlt_keyframe_type(type))) {
                //                LOG_DEBUG() << "keyframe index" << keyframeIndex << "keyframe type" << type;
                m_filter->startUndoModifyKeyframeCommand(parameterIndex, keyframeIndex);
                m_filter->updateUndoCommand(name);
                for (auto &key : gangedProperties(parameterIndex)) {
                    Mlt::Animation animation = m_filter->getAnimation(key);
                    if (animation.is_valid()) {
                        animation.key_set_type(keyframeIndex, mlt_keyframe_type(type));
                        m_filter->updateUndoCommand(key);
                    }
                }
                mlt_event_data eventData = mlt_event_data_from_string(name.toUtf8().constData());
                mlt_events_fire(m_filter->service().get_properties(), "property-changed", eventData);
                QModelIndex modelIndex = index(keyframeIndex, 0, index(parameterIndex));
                emit dataChanged(modelIndex,
                                 modelIndex,
                                 QVector<int>() << KeyframeTypeRole << NameRole);
                QModelIndex nextModelIndex = index(keyframeIndex + 1, 0, index(parameterIndex));
                emit dataChanged(nextModelIndex,
                                 nextModelIndex,
                                 QVector<int>() << PrevKeyframeTypeRole);
                error = false;
                emit m_filter->changed(name.toUtf8().constData());
                emit m_filter->propertyChanged(name.toUtf8().constData());
                m_filter->endUndoCommand();
            }
        }
    }
    if (error)
        LOG_ERROR() << "failed to set keyframe"
                    << "at parameter index" << parameterIndex << "keyframeIndex" << keyframeIndex
                    << "to type" << type;
    return error;
}

/*!
    \qmlmethod void KeyframesModel::setKeyframePosition(int parameterIndex, int keyframeIndex, int position)
    \brief Moves the keyframe at \a keyframeIndex on \a parameterIndex to \a position (in frames).
*/

void KeyframesModel::setKeyframePosition(int parameterIndex, int keyframeIndex, int position)
{
    if (!m_filter) {
        LOG_ERROR() << "Invalid Filter" << parameterIndex;
        return;
    }

    if (parameterIndex >= m_propertyNames.count()) {
        LOG_ERROR() << "Invalid parameter index" << parameterIndex;
        return;
    }

    QString name = m_propertyNames[parameterIndex];
    Mlt::Animation animation = m_filter->getAnimation(name);
    if (!animation.is_valid()) {
        LOG_ERROR() << "Invalid animation" << parameterIndex;
        return;
    }

    if (keyframeIndex >= animation.key_count()) {
        LOG_ERROR() << "Invalid key index" << parameterIndex << keyframeIndex;
        return;
    }

    if (position < 0) {
        LOG_ERROR() << "Invalid key position" << parameterIndex << keyframeIndex << position;
        return;
    }

    int prevPosition = animation.key_get_frame(keyframeIndex);
    if (position == prevPosition) {
        LOG_ERROR() << "Position did not change" << parameterIndex << keyframeIndex << position;
        return;
    }

    if (animation.key_set_frame(keyframeIndex, position)) {
        LOG_ERROR() << "Failed to set position" << parameterIndex << keyframeIndex << position;
        return;
    }

    m_filter->startUndoModifyKeyframeCommand(parameterIndex, keyframeIndex);
    m_filter->updateUndoCommand(name);
    for (auto &key : gangedProperties(parameterIndex)) {
        Mlt::Animation animation = m_filter->getAnimation(key);
        if (animation.is_valid()) {
            animation.key_set_frame(keyframeIndex, position);
            m_filter->updateUndoCommand(key);
        }
    }
    mlt_event_data eventData = mlt_event_data_from_string(name.toUtf8().constData());
    mlt_events_fire(m_filter->service().get_properties(), "property-changed", eventData);
    QModelIndex modelIndex = index(keyframeIndex, 0, index(parameterIndex));
    emit dataChanged(modelIndex, modelIndex, QVector<int>() << FrameNumberRole << NameRole);
    updateNeighborsMinMax(parameterIndex, keyframeIndex);
    emit m_filter->changed(name.toUtf8().constData());
    emit m_filter->propertyChanged(name.toUtf8().constData());
    m_filter->endUndoCommand();
}

/*!
    \qmlmethod void KeyframesModel::addKeyframe(int parameterIndex, double value, int position, int type)
    \brief Adds a keyframe at time \a position for \a parameterIndex with numeric \a value and
    interpolation \a type.
*/

/*!
    \qmlmethod void KeyframesModel::addKeyframe(int parameterIndex, int position)
    \brief Adds a keyframe at time \a position for \a parameterIndex using the current filter value.
*/

void KeyframesModel::addKeyframe(int parameterIndex,
                                 double value,
                                 int position,
                                 KeyframesModel::InterpolationType type)
{
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        m_filter->startUndoAddKeyframeCommand();
        m_filter->set(name, value, position, mlt_keyframe_type(type));
        m_filter->updateUndoCommand(name);
        for (auto &key : gangedProperties(parameterIndex)) {
            m_filter->set(key, value, position, mlt_keyframe_type(type));
            m_filter->updateUndoCommand(key);
        }
        m_filter->endUndoCommand();
    }
}

void KeyframesModel::addKeyframe(int parameterIndex, int position)
{
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        auto parameter = m_metadata->keyframes()->parameter(m_metadataIndex[parameterIndex]);
        if (parameter->isRectangle()) {
            m_filter->startUndoAddKeyframeCommand();
            auto value = m_filter->getRect(name, position);
            Mlt::Animation anim = m_filter->getAnimation(name);
            if (anim.is_valid() && !anim.is_key(position)) {
                mlt_keyframe_type keyframeType = m_filter->getKeyframeType(anim,
                                                                           position,
                                                                           mlt_keyframe_type(-1));
                m_filter->blockSignals(true);
                m_filter->set(name, value, position, keyframeType);
                m_filter->blockSignals(false);
                emit keyframeAdded(name, position);
            }
            m_filter->endUndoCommand();
        } else if (parameter->isColor()) {
            m_filter->startUndoAddKeyframeCommand();
            // Color values
            auto value = m_filter->getColor(name, position);
            Mlt::Animation anim = m_filter->getAnimation(name);
            if (anim.is_valid() && !anim.is_key(position)) {
                mlt_keyframe_type keyframeType = m_filter->getKeyframeType(anim,
                                                                           position,
                                                                           mlt_keyframe_type(-1));
                m_filter->blockSignals(true);
                m_filter->set(name, value, position, keyframeType);
                for (auto &key : parameter->gangedProperties()) {
                    value = m_filter->get(key, position);
                    m_filter->set(key, value, position, keyframeType);
                }
                m_filter->blockSignals(false);
            }
            emit keyframeAdded(name, position);
            m_filter->endUndoCommand();
        } else {
            m_filter->startUndoAddKeyframeCommand();
            // Get the value from the existing position.
            double value = m_filter->getDouble(name, position);
            Mlt::Animation anim = m_filter->getAnimation(name);
            if (anim.is_valid() && !anim.is_key(position)) {
                mlt_keyframe_type keyframeType = m_filter->getKeyframeType(anim,
                                                                           position,
                                                                           mlt_keyframe_type(-1));
                // Simply adding a keyframe does not change the current value of
                // the filter parameter. So, no need to trigger a bunch of signals
                // and refresh the consumer. Besides, refreshing the consumer is
                // some how causing the player to seek ahead by one frame inadvertently
                // such that changing the parameter value causes the addition of a
                // keyframe just after this one. MLT.refreshConsumer() with
                // frame-dropping enabled may have dropped video of the most recent
                // frame from the producer, but Shotcut does not know about it
                // because it did not receive a "consumer-frame-show" event for it.
                m_filter->blockSignals(true);
                m_filter->set(name, value, position, keyframeType);
                for (auto &key : parameter->gangedProperties()) {
                    value = m_filter->getDouble(key, position);
                    m_filter->set(key, value, position, keyframeType);
                }
                for (auto &key : parameter->gangedRectProperties()) {
                    const auto rect = m_filter->getRect(key, position);
                    m_filter->set(key, rect, position, keyframeType);
                }
                m_filter->blockSignals(false);
                emit keyframeAdded(name, position);
                m_filter->endUndoCommand();
            }
        }
        onFilterChanged(name);
    }
}

/*!
    \qmlmethod void KeyframesModel::setKeyframeValue(int parameterIndex, int keyframeIndex, double value)
    \brief Sets the numeric \a value of an existing keyframe on parameter \a parameterIndex
    at \a keyframeIndex.
*/

void KeyframesModel::setKeyframeValue(int parameterIndex, int keyframeIndex, double value)
{
    if (!m_filter) {
        LOG_ERROR() << "Invalid Filter" << parameterIndex;
        return;
    }

    if (parameterIndex >= m_propertyNames.count()) {
        LOG_ERROR() << "Invalid parameter index" << parameterIndex;
        return;
    }

    QString name = m_propertyNames[parameterIndex];
    Mlt::Animation animation = m_filter->getAnimation(name);
    if (!animation.is_valid()) {
        LOG_ERROR() << "Invalid animation" << parameterIndex;
        return;
    }

    if (keyframeIndex >= animation.key_count()) {
        LOG_ERROR() << "Invalid key index" << parameterIndex << keyframeIndex;
        return;
    }

    int position = animation.key_get_frame(keyframeIndex);
    if (position < 0) {
        LOG_ERROR() << "Invalid position" << parameterIndex << keyframeIndex;
        return;
    }
    m_filter->startUndoModifyKeyframeCommand(parameterIndex, keyframeIndex);
    mlt_keyframe_type type = animation.key_get_type(keyframeIndex);
    m_filter->service().anim_set(name.toUtf8().constData(),
                                 value,
                                 position,
                                 m_filter->duration(),
                                 type);
    m_filter->updateUndoCommand(name);
    for (auto &key : gangedProperties(parameterIndex)) {
        m_filter->service().anim_set(key.toUtf8().constData(),
                                     value,
                                     position,
                                     m_filter->duration(),
                                     type);
        m_filter->updateUndoCommand(key);
    }
    emit m_filter->changed(name.toUtf8().constData());
    emit m_filter->propertyChanged(name.toUtf8().constData());
    QModelIndex modelIndex = index(keyframeIndex, 0, index(parameterIndex));
    emit dataChanged(modelIndex, modelIndex, QVector<int>() << NumericValueRole << NameRole);
    emit dataChanged(index(parameterIndex),
                     index(parameterIndex),
                     QVector<int>() << LowestValueRole << HighestValueRole);
    m_filter->endUndoCommand();
}

/*!
    \qmlmethod void KeyframesModel::setKeyframeValuePosition(int parameterIndex, int keyframeIndex, double value, int position)
    \brief Sets both the \a value and time \a position of keyframe \a keyframeIndex on
    parameter \a parameterIndex in one operation.
*/

void KeyframesModel::setKeyframeValuePosition(int parameterIndex,
                                              int keyframeIndex,
                                              double value,
                                              int position)
{
    if (!m_filter) {
        LOG_ERROR() << "Invalid Filter" << parameterIndex;
        return;
    }

    if (parameterIndex >= m_propertyNames.count()) {
        LOG_ERROR() << "Invalid parameter index" << parameterIndex;
        return;
    }

    QString name = m_propertyNames[parameterIndex];
    Mlt::Animation animation = m_filter->getAnimation(name);
    if (!animation.is_valid()) {
        LOG_ERROR() << "Invalid animation" << parameterIndex;
        return;
    }

    if (keyframeIndex >= animation.key_count()) {
        LOG_ERROR() << "Invalid key index" << parameterIndex << keyframeIndex;
        return;
    }

    if (position < 0) {
        LOG_ERROR() << "Invalid key position" << parameterIndex << keyframeIndex << position;
        return;
    }
    m_filter->startUndoModifyKeyframeCommand(parameterIndex, keyframeIndex);
    QVector<int> roles;
    int prevPosition = animation.key_get_frame(keyframeIndex);
    if (position != prevPosition) {
        if (animation.key_set_frame(keyframeIndex, position)) {
            LOG_ERROR() << "Failed to set position" << parameterIndex << keyframeIndex << position;
            return;
        }
        for (auto &key : gangedProperties(parameterIndex)) {
            Mlt::Animation animation = m_filter->getAnimation(key);
            if (animation.is_valid()) {
                animation.key_set_frame(keyframeIndex, position);
            }
        }
        roles << FrameNumberRole;
        updateNeighborsMinMax(parameterIndex, keyframeIndex);
    }

    mlt_keyframe_type type = animation.key_get_type(keyframeIndex);
    m_filter->service().anim_set(name.toUtf8().constData(),
                                 value,
                                 position,
                                 m_filter->duration(),
                                 type);
    m_filter->updateUndoCommand(name);
    for (auto &key : gangedProperties(parameterIndex)) {
        m_filter->service().anim_set(key.toUtf8().constData(),
                                     value,
                                     position,
                                     m_filter->duration(),
                                     type);
        m_filter->updateUndoCommand(key);
    }
    emit m_filter->changed(name.toUtf8().constData());
    emit m_filter->propertyChanged(name.toUtf8().constData());
    roles << NumericValueRole << NameRole;
    QModelIndex modelIndex = index(keyframeIndex, 0, index(parameterIndex));
    emit dataChanged(modelIndex, modelIndex, roles);
    emit dataChanged(index(parameterIndex),
                     index(parameterIndex),
                     QVector<int>() << LowestValueRole << HighestValueRole);
    m_filter->endUndoCommand();
}

/*!
    \qmlmethod bool KeyframesModel::isKeyframe(int parameterIndex, int position)
    \brief Returns \c true if a keyframe exists at time \a position for \a parameterIndex.
*/

bool KeyframesModel::isKeyframe(int parameterIndex, int position)
{
    if (m_filter && parameterIndex < m_propertyNames.count()) {
        QString name = m_propertyNames[parameterIndex];
        Mlt::Animation anim = m_filter->getAnimation(name);
        return anim.is_valid() && anim.is_key(position);
    }
    return false;
}

/*!
    \qmlmethod bool KeyframesModel::advancedKeyframesInUse()
    \brief Returns \c true if the filter is currently using advanced (per-keyframe) interpolation.
*/

bool KeyframesModel::advancedKeyframesInUse()
{
    if (m_filter && m_metadata && m_filter->animateIn() <= 0 && m_filter->animateOut() <= 0)
        for (int i = 0; i < m_metadata->keyframes()->parameterCount(); i++) {
            if (m_filter->keyframeCount(m_metadata->keyframes()->parameter(i)->property()) > 0) {
                return true;
            }
        }
    return false;
}

/*!
    \qmlmethod void KeyframesModel::removeAdvancedKeyframes()
    \brief Converts all advanced keyframes to simple (linear) keyframes.
*/

void KeyframesModel::removeAdvancedKeyframes()
{
    if (m_filter && m_metadata && m_filter->animateIn() <= 0 && m_filter->animateOut() <= 0) {
        for (int i = 0; i < m_metadata->keyframes()->parameterCount(); i++) {
            QString name = m_metadata->keyframes()->parameter(i)->property();
            if (m_filter->keyframeCount(name) > 0) {
                m_filter->set(name, m_filter->get(name, 0));
                m_filter->updateUndoCommand(name);
                for (auto &key : m_metadata->keyframes()->parameter(i)->gangedProperties()) {
                    m_filter->set(key, m_filter->get(key, 0));
                    m_filter->updateUndoCommand(key);
                }
            }
        }
        reload();
    }
}

/*!
    \qmlmethod bool KeyframesModel::simpleKeyframesInUse()
    \brief Returns \c true if the filter is currently using simple (two-keyframe) animation.
*/

bool KeyframesModel::simpleKeyframesInUse()
{
    return m_filter && m_metadata && (m_filter->animateIn() > 0 || m_filter->animateOut() > 0);
}

/*!
    \qmlmethod void KeyframesModel::removeSimpleKeyframes()
    \brief Removes the simple keyframe animation, returning the filter to a static state.
*/

void KeyframesModel::removeSimpleKeyframes()
{
    if (simpleKeyframesInUse()) {
        for (int i = 0; i < m_metadata->keyframes()->parameterCount(); i++) {
            QString name = m_metadata->keyframes()->parameter(i)->property();
            auto parameter = m_metadata->keyframes()->parameter(i);
            if (parameter->gangedProperties().count() > 0) {
                // Do not attempt to detect all matching keyframes for ganged properties.
                // Always convert to advanced.
                continue;
            }
            bool clearKeyframes = true;
            // Find out if all keyframe values are the same. If they are all the same,
            // then clear keyframes and set the parameter to a single value.
            if (parameter->isRectangle()) {
                auto firstValue = m_filter->getRect(name, 0);
                Mlt::Animation anim = m_filter->getAnimation(name);
                if (anim.is_valid()) {
                    for (int k = 1; k < anim.key_count(); k++) {
                        auto value = m_filter->getRect(name, anim.key_get_frame(k));
                        if (value != firstValue) {
                            clearKeyframes = false;
                            break;
                        }
                    }
                }
                if (clearKeyframes) {
                    m_filter->set(name, firstValue);
                    m_filter->updateUndoCommand(name);
                }
            } else if (parameter->isColor()) {
                auto firstValue = m_filter->getColor(name, 0);
                Mlt::Animation anim = m_filter->getAnimation(name);
                if (anim.is_valid()) {
                    for (int k = 1; k < anim.key_count(); k++) {
                        auto value = m_filter->getColor(name, anim.key_get_frame(k));
                        if (value != firstValue) {
                            clearKeyframes = false;
                            break;
                        }
                    }
                }
                if (clearKeyframes) {
                    m_filter->set(name, firstValue);
                    m_filter->updateUndoCommand(name);
                    m_filter->blockSignals(true);
                    for (auto &key : parameter->gangedProperties()) {
                        m_filter->set(key, m_filter->getColor(key, 0));
                        m_filter->updateUndoCommand(key);
                    }
                    m_filter->blockSignals(false);
                }
            } else {
                double firstValue = m_filter->getDouble(name, 0);
                Mlt::Animation anim = m_filter->getAnimation(name);
                if (anim.is_valid()) {
                    for (int k = 1; k < anim.key_count(); k++) {
                        auto value = m_filter->getDouble(name, anim.key_get_frame(k));
                        if (value != firstValue) {
                            clearKeyframes = false;
                            break;
                        }
                    }
                }
                if (clearKeyframes) {
                    m_filter->set(name, firstValue);
                    m_filter->updateUndoCommand(name);
                    m_filter->blockSignals(true);
                    for (auto &key : parameter->gangedProperties()) {
                        m_filter->set(key, m_filter->getDouble(key, 0));
                        m_filter->updateUndoCommand(key);
                    }
                    m_filter->blockSignals(false);
                }
            }
        }
        m_filter->clearAnimateInOut();
    }
}

/*!
    \qmlmethod void KeyframesModel::reload()
    \brief Reloads the model from the current filter, refreshing all parameter and keyframe rows.
*/

void KeyframesModel::reload()
{
    beginResetModel();
    m_propertyNames.clear();
    m_keyframeCounts.clear();
    m_metadataIndex.clear();
    if (m_filter && m_metadata && m_filter->animateIn() <= 0 && m_filter->animateOut() <= 0)
        for (int i = 0; i < m_metadata->keyframes()->parameterCount(); i++) {
            if (m_filter->keyframeCount(m_metadata->keyframes()->parameter(i)->property()) > 0) {
                m_propertyNames << m_metadata->keyframes()->parameter(i)->property();
                m_keyframeCounts << keyframeCount(m_propertyNames.count() - 1);
                m_metadataIndex << i;
            }
        }
    endResetModel();
}

void KeyframesModel::onFilterChanged(const QString &property)
{
    bool isKeyframeProperty = false;
    for (int p = 0; p < m_metadata->keyframes()->parameterCount() && isKeyframeProperty == false;
         p++) {
        if (m_metadata->keyframes()->parameter(p)->property() == property) {
            isKeyframeProperty = true;
            break;
        }
    }
    if (!isKeyframeProperty) {
        // Does not affect this model.
        return;
    }

    int i = m_propertyNames.indexOf(property);
    if (i < 0) {
        // First keyframe added. Reset model to add this parameter.
        reload();
        return;
    }

    int prevCount = m_keyframeCounts[i];
    m_keyframeCounts[i] = keyframeCount(i);
    if (m_keyframeCounts[i] == 0) {
        // All keyframes removed. Reset model to remove this parameter.
        reload();
    } else if (prevCount != m_keyframeCounts[i]) {
        // Keyframe count changed. Remove all old and insert all new.
        if (prevCount > 0) {
            beginRemoveRows(index(i), 0, prevCount - 1);
            endRemoveRows();
        }
        beginInsertRows(index(i), 0, m_keyframeCounts[i] - 1);
        endInsertRows();
        emit dataChanged(index(i), index(i), QVector<int>() << LowestValueRole << HighestValueRole);
    } else {
        // Keyframe count is unchanged. A value must have changed.
        emit dataChanged(index(i), index(i), QVector<int>() << LowestValueRole << HighestValueRole);
        emit dataChanged(index(0, 0, index(i)),
                         index(m_keyframeCounts[i] - 1, 0, index(i)),
                         QVector<int>() << NumericValueRole << NameRole);
    }
}

void KeyframesModel::onFilterInChanged(int /*delta*/)
{
    QTimer::singleShot(0, this, SLOT(reload()));
}

void KeyframesModel::trimFilterIn(int in)
{
    Mlt::Service &service = m_filter->service();
    if (service.is_valid() && service.type() == mlt_service_filter_type) {
        Mlt::Filter filter = service;
        MLT.adjustFilter(&filter,
                         filter.get_in(),
                         filter.get_out(),
                         in - filter.get_in(),
                         0,
                         in - filter.get_in());
        m_filter->updateUndoCommand("in");
    }
}

void KeyframesModel::trimFilterOut(int out)
{
    Mlt::Service &service = m_filter->service();
    if (service.is_valid() && service.type() == mlt_service_filter_type) {
        Mlt::Filter filter = service;
        MLT.adjustFilter(&filter, filter.get_in(), filter.get_out(), 0, filter.get_out() - out, 0);
        m_filter->updateUndoCommand("out");
    }
}

int KeyframesModel::keyframeCount(int index) const
{
    if (index < m_propertyNames.count())
        return qMax(const_cast<QmlFilter *>(m_filter)->keyframeCount(m_propertyNames[index]), 0);
    else
        return 0;
}

void KeyframesModel::updateNeighborsMinMax(int parameterIndex, int keyframeIndex)
{
    QModelIndex modelIndex;
    if (keyframeIndex > 0) {
        modelIndex = index(keyframeIndex - 1, 0, index(parameterIndex));
        emit dataChanged(modelIndex, modelIndex, QVector<int>() << MaximumFrameRole);
    }
    if (keyframeIndex < keyframeCount(parameterIndex) - 1) {
        modelIndex = index(keyframeIndex + 1, 0, index(parameterIndex));
        emit dataChanged(modelIndex, modelIndex, QVector<int>() << MinimumFrameRole);
    }
}

QStringList KeyframesModel::gangedProperties(int parameterIndex) const
{
    return m_metadata->keyframes()->parameter(m_metadataIndex[parameterIndex])->gangedProperties();
}
