/*
 *  SPDX-FileCopyrightText: 2015 Jouni Pentikäinen <joupent@gmail.com>
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "kis_onion_skin_compositor.h"

#include "kis_paint_device.h"
#include "kis_painter.h"
#include "KoColor.h"
#include "KoColorSpace.h"
#include "KoCompositeOpRegistry.h"
#include "KoColorSpaceConstants.h"
#include "kis_image_config.h"
#include "kis_raster_keyframe_channel.h"

Q_GLOBAL_STATIC(KisOnionSkinCompositor, s_instance)

struct KisOnionSkinCompositor::Private
{
    int numberOfSkins = 0;
    int tintFactor = 0;
    QColor backwardTintColor;
    QColor forwardTintColor;
    QVector<int> backwardOpacities;
    QVector<int> forwardOpacities;
    int configSeqNo = 0;
    QSet<int> colorLabelFilter;

    int skinOpacity(int offset)
    {
        const QVector<int> &bo = backwardOpacities;
        const QVector<int> &fo = forwardOpacities;

        return offset > 0 ? fo[qAbs(offset) - 1] : bo[qAbs(offset) - 1];
    }

    KisPaintDeviceSP setUpTintDevice(const QColor &tintColor, const KoColorSpace *colorSpace)
    {
        KisPaintDeviceSP tintDevice = new KisPaintDevice(colorSpace);
        KoColor color = KoColor(tintColor, colorSpace);
        tintDevice->setDefaultPixel(color);
        return tintDevice;
    }


    KisRasterKeyframeSP getNextFrameToComposite(KisKeyframeChannel *channel, int &outFrame, bool backwards) // TODO: Double-check this function... outFrame might be weird?
    {
        while (!channel->keyframeAt(outFrame).isNull()) {
            outFrame = backwards ? channel->previousKeyframeTime(outFrame) : channel->nextKeyframeTime(outFrame);
            if (colorLabelFilter.isEmpty()) {
                return channel->keyframeAt<KisRasterKeyframe>(outFrame);
            } else if (channel->keyframeAt<KisRasterKeyframe>(outFrame)) {
                if (colorLabelFilter.contains(channel->keyframeAt(outFrame)->colorLabel())) {
                    return channel->keyframeAt<KisRasterKeyframe>(outFrame);
                }
            }
        }
        return channel->keyframeAt<KisRasterKeyframe>(outFrame);
    }

    void tryCompositeFrame(KisRasterKeyframeSP keyframe, KisPainter &gcFrame, KisPainter &gcDest, KisPaintDeviceSP tintSource, int opacity, const QRect &rect)
    {
        if (keyframe.isNull() || opacity == OPACITY_TRANSPARENT_U8) return;

        keyframe->writeFrameToDevice(gcFrame.device());

        gcFrame.bitBlt(rect.topLeft(), tintSource, rect);

        gcDest.setOpacityU8(opacity);
        gcDest.bitBlt(rect.topLeft(), gcFrame.device(), rect);
    }

    void refreshConfig()
    {
        KisImageConfig config(true);

        numberOfSkins = config.numberOfOnionSkins();
        tintFactor = config.onionSkinTintFactor();
        backwardTintColor = config.onionSkinTintColorBackward();
        forwardTintColor = config.onionSkinTintColorForward();

        backwardOpacities.resize(numberOfSkins);
        forwardOpacities.resize(numberOfSkins);

        const int mainState = (int) config.onionSkinState(0);
        const qreal scaleFactor = mainState * config.onionSkinOpacity(0) / 255.0;

        for (int i = 0; i < numberOfSkins; i++) {
            int backwardState = (int) config.onionSkinState(-(i + 1));
            int forwardState = (int) config.onionSkinState(i + 1);

            backwardOpacities[i] = scaleFactor * backwardState * config.onionSkinOpacity(-(i + 1));
            forwardOpacities[i] = scaleFactor * forwardState * config.onionSkinOpacity(i + 1);
        }

        configSeqNo++;
    }

    QRect updateExtentOnFrameChange(KisRasterKeyframeChannel *channel,
                                    int prevActiveTime, int prevIgnoredTime,
                                    int nowActiveTime, int nowIgnoredTime);

};

KisOnionSkinCompositor *KisOnionSkinCompositor::instance()
{
    return s_instance;
}

KisOnionSkinCompositor::KisOnionSkinCompositor()
    : m_d(new Private)
{
    m_d->refreshConfig();
}

KisOnionSkinCompositor::~KisOnionSkinCompositor()
{}

int KisOnionSkinCompositor::configSeqNo() const
{
    return m_d->configSeqNo;
}

void KisOnionSkinCompositor::setColorLabelFilter(QSet<int> colors)
{
    m_d->colorLabelFilter = colors;
}

QSet<int> KisOnionSkinCompositor::colorLabelFilter()
{
    return m_d->colorLabelFilter;
}

void KisOnionSkinCompositor::composite(const KisPaintDeviceSP sourceDevice, KisPaintDeviceSP targetDevice, const QRect& rect)
{
    KisRasterKeyframeChannel *keyframes = sourceDevice->keyframeChannel();

    KisPaintDeviceSP frameDevice = new KisPaintDevice(sourceDevice->colorSpace());
    KisPainter gcFrame(frameDevice);
    QBitArray channelFlags = sourceDevice->colorSpace()->channelFlags(true, false);
    gcFrame.setChannelFlags(channelFlags);
    gcFrame.setOpacityU8(m_d->tintFactor);

    KisPaintDeviceSP backwardTintDevice = m_d->setUpTintDevice(m_d->backwardTintColor, sourceDevice->colorSpace());
    KisPaintDeviceSP forwardTintDevice = m_d->setUpTintDevice(m_d->forwardTintColor, sourceDevice->colorSpace());

    KisPainter gcDest(targetDevice);
    gcDest.setCompositeOpId(sourceDevice->colorSpace()->compositeOp(COMPOSITE_BEHIND));

    int keyframeTimeBck;
    int keyframeTimeFwd;

    int time = sourceDevice->defaultBounds()->currentTime();

    if (!keyframes) { // it happens when you try to show onion skins on non-animated layer with opacity keyframes
        return;
    }

    keyframeTimeBck = keyframeTimeFwd = keyframes->activeKeyframeTime(time);

    for (int offset = 1; offset <= m_d->numberOfSkins; offset++) {
        KisRasterKeyframeSP backKeyframe = m_d->getNextFrameToComposite(keyframes, keyframeTimeBck, true);
        KisRasterKeyframeSP forwardKeyframe = m_d->getNextFrameToComposite(keyframes, keyframeTimeFwd, false);

        if (!backKeyframe.isNull()) {
            m_d->tryCompositeFrame(backKeyframe, gcFrame, gcDest, backwardTintDevice, m_d->skinOpacity(-offset), rect);
        }

        if (!forwardKeyframe.isNull()) {
            m_d->tryCompositeFrame(forwardKeyframe, gcFrame, gcDest, forwardTintDevice, m_d->skinOpacity(offset), rect);
        }
    }

}

QRect KisOnionSkinCompositor::calculateFullExtent(const KisPaintDeviceSP device)
{
    QRect rect;

    KisRasterKeyframeChannel *channel = device->keyframeChannel();
    if (!channel) return rect;

    int currentKeyTime = channel->firstKeyframeTime();

    while (channel->keyframeAt(currentKeyTime)) {
        rect |= channel->frameExtents(channel->keyframeAt(currentKeyTime));
        currentKeyTime = channel->nextKeyframeTime(currentKeyTime);
    }

    return rect;
}

QRect KisOnionSkinCompositor::calculateExtent(const KisPaintDeviceSP device, int time)
{
    QRect rect;
    int keyframeTimeBack;
    int keyframeTimeFwd;

    KisRasterKeyframeChannel *channel = device->keyframeChannel(); //TODO: take in channel instead of device...?

    if (!channel) { // it happens when you try to show onion skins on non-animated layer with opacity keyframes
        return rect;
    }

    keyframeTimeBack = keyframeTimeFwd = time;

    for (int offset = 1; offset <= m_d->numberOfSkins; offset++) {
        if (channel->keyframeAt(keyframeTimeBack)) {
            keyframeTimeBack = channel->previousKeyframeTime(keyframeTimeBack);

            if (channel->keyframeAt(keyframeTimeBack)) {
                rect |= channel->frameExtents(channel->keyframeAt(keyframeTimeBack));
            }
        }

        if (channel->keyframeAt(keyframeTimeFwd)) {
            keyframeTimeFwd = channel->nextKeyframeTime(keyframeTimeFwd);

            if (channel->keyframeAt(keyframeTimeFwd)) {
                rect |= channel->frameExtents(channel->keyframeAt(keyframeTimeFwd));
            }
        }
    }

    return rect;
}

QRect KisOnionSkinCompositor::calculateExtent(const KisPaintDeviceSP device)
{
    KisRasterKeyframeChannel *channel = device->keyframeChannel(); //TODO: take in channel instead of device...?

    if (!channel) { // it happens when you try to show onion skins on non-animated layer with opacity keyframes
        return QRect();
    }

    return calculateExtent(device, channel->activeKeyframeTime());
}


QRect KisOnionSkinCompositor::Private::updateExtentOnFrameChange(KisRasterKeyframeChannel *channel,
                                                                 int prevActiveTime, int prevIgnoredTime,
                                                                 int nowActiveTime, int nowIgnoredTime)
{
    QRect rect;

    std::vector<int> skinsBefore;
    std::vector<int> skinsAfter;

    auto fetchSkins = [this] (KisRasterKeyframeChannel *channel, const int activeTime, int ignoredFrame) {
        std::vector<int> skinsLeft;
        std::vector<int> skinsRight;

        int keyframeTimeBck = activeTime;
        int keyframeTimeFwd = activeTime;

        auto addNextFrame = [channel, this, ignoredFrame] (int offset, int &startTime, std::vector<int> &skins, bool backwards) {
            KisRasterKeyframeSP keyframe;
            do {
                keyframe = getNextFrameToComposite(channel, startTime, backwards);
            } while (keyframe && startTime == ignoredFrame);

            if (keyframe && skinOpacity(-offset) != OPACITY_TRANSPARENT_U8) {
                skins.push_back(startTime);
            }
        };

        for (int offset = 1; offset <= numberOfSkins; offset++) {
            addNextFrame(offset, keyframeTimeBck, skinsLeft, true);
            addNextFrame(offset, keyframeTimeFwd, skinsRight, false);
        }

        std::reverse(skinsLeft.begin(), skinsLeft.end());
        skinsLeft.reserve(skinsLeft.size() + skinsRight.size());
        std::copy(skinsRight.begin(), skinsRight.end(), std::back_inserter(skinsLeft));

        return skinsLeft;
    };

    skinsBefore = fetchSkins(channel, prevActiveTime, prevIgnoredTime);
    skinsAfter = fetchSkins(channel, nowActiveTime, nowIgnoredTime);

    std::vector<int> changedSkins;

    std::set_symmetric_difference(skinsBefore.begin(), skinsBefore.end(),
                                  skinsAfter.begin(), skinsAfter.end(),
                                  std::back_inserter(changedSkins));

//    ENTER_FUNCTION() << ppVar(skinsBefore);
//    ENTER_FUNCTION() << ppVar(skinsAfter);
//    ENTER_FUNCTION() << ppVar(changedSkins);

    for (auto it = changedSkins.begin(); it != changedSkins.end(); ++it) {
        KIS_SAFE_ASSERT_RECOVER(channel->keyframeAt(*it)) { continue; }
        rect |= channel->frameExtents(channel->keyframeAt(*it));
    }

    return rect;
}

QRect KisOnionSkinCompositor::updateExtentOnAddition(const KisPaintDeviceSP device, int addedTime)
{
    QRect rect;

    KisRasterKeyframeChannel *channel = device->keyframeChannel(); //TODO: take in channel instead of device...?

    if (!channel) { // it happens when you try to show onion skins on non-animated layer with opacity keyframes
        return rect;
    }

    int currentActiveTime = channel->activeKeyframeTime();
    int prevActiveTime = -1;

    if (currentActiveTime == addedTime) {
        prevActiveTime = channel->previousKeyframeTime(currentActiveTime);
    } else {
        prevActiveTime = currentActiveTime;
    }

    rect = m_d->updateExtentOnFrameChange(channel,
                                          prevActiveTime, addedTime,
                                          currentActiveTime, -1);

    return rect;
}

void KisOnionSkinCompositor::configChanged()
{
    m_d->refreshConfig();
    Q_EMIT sigOnionSkinChanged();
}
