/*
 *  SPDX-FileCopyrightText: 2010 Dmitry Kazakov <dimula73@gmail.com>
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 */

#ifndef __KIS_UPDATE_SCHEDULER_H
#define __KIS_UPDATE_SCHEDULER_H

#include <QObject>
#include "kritaimage_export.h"
#include "kis_types.h"

#include "kis_image_interfaces.h"
#include "kis_stroke_strategy_factory.h"
#include "kis_strokes_queue_undo_result.h"
#include "KisLodPreferences.h"
#include "KisProjectionUpdateFlags.h"

class QRect;
class KoProgressProxy;
class KisProjectionUpdateListener;
class KisSpontaneousJob;
class KisPostExecutionUndoAdapter;


class KRITAIMAGE_EXPORT KisUpdateScheduler : public QObject, public KisStrokesFacade
{
    Q_OBJECT

public:
    KisUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener, QObject *parent = 0);
    ~KisUpdateScheduler() override;

    /**
     * Set the number of threads used by the scheduler
     */
    void setThreadsLimit(int value);

    /**
     * Return the number of threads available to the scheduler
     */
    int threadsLimit() const;

    /**
     * Sets the proxy that is going to be notified about the progress
     * of processing of the queues. If you want to switch the proxy
     * on runtime, you should do it under the lock held.
     *
     * \see lock(), unlock()
     */
    void setProgressProxy(KoProgressProxy *progressProxy);

    /**
     * Blocks processing of the queues.
     * The function will wait until all the executing jobs
     * are finished.
     * NOTE: you may add new jobs while the block held, but they
     * will be delayed until unlock() is called.
     *
     * \see unlock()
     */
    void immediateLockForReadOnly();

    /**
     * Unblocks the process and calls processQueues()
     *
     * \see processQueues()
     */
    void unlock(bool resetLodLevels = true);

    /**
     * Waits until all the running jobs are finished.
     *
     * If some other thread adds jobs in parallel, then you may
     * wait forever. If you don't want it, consider lock() instead.
     *
     * \see lock()
     */
    void waitForDone();

    /**
     * Waits until the queues become empty, then blocks the processing.
     * To unblock processing you should use unlock().
     *
     * If some other thread adds jobs in parallel, then you may
     * wait forever. If you don't want it, consider lock() instead.
     *
     * \see unlock(), lock()
     */
    void barrierLock();


    /**
     * Works like barrier lock, but returns false immediately if barrierLock
     * can't be acquired.
     *
     * \see barrierLock()
     */
    bool tryBarrierLock();

    /**
     * Tells if there are no strokes or updates are running at the
     * moment. Internally calls to tryBarrierLock(), so it is not O(1).
     */
    bool isIdle();

    /**
     * Blocks all the updates from execution. It doesn't affect
     * strokes execution in any way. This type of lock is supposed
     * to be held by the strokes themselves when they need a short
     * access to some parts of the projection of the image.
     *
     * From all the other places you should use usual lock()/unlock()
     * methods
     *
     * \see lock(), unlock()
     */
    void blockUpdates();

    /**
     * Unblocks updates from execution previously locked by blockUpdates()
     *
     * \see blockUpdates()
     */
    void unblockUpdates();

    void updateProjection(KisNodeSP node, const QVector<QRect> &rects, const QRect &cropRect, KisProjectionUpdateFlags flags);
    void fullRefreshAsync(KisNodeSP root, const QVector<QRect>& rects, const QRect &cropRect, KisProjectionUpdateFlags flags);

    // simplified override for testing purposes
    void updateProjection(KisNodeSP node, const QRect &rc, const QRect &cropRect);

    // simplified override for testing purposes
    void fullRefreshAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect);

    void addSpontaneousJob(KisSpontaneousJob *spontaneousJob);

    bool hasUpdatesRunning() const;

    KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override;
    void addJob(KisStrokeId id, KisStrokeJobData *data) override;
    void endStroke(KisStrokeId id) override;
    bool cancelStroke(KisStrokeId id) override;

    /**
     * Sets the desired level of detail preferences on which the strokes should
     * work. Please note that this configuration will be applied starting from
     * the next stroke. Please also note that this value is not guaranteed to
     * coincide with the one returned by currentLevelOfDetail()
     */
    void setLodPreferences(const KisLodPreferences &value);

    /**
     * Return current lod preferences loaded in the strokes queue
     */
    KisLodPreferences lodPreferences() const;

    /**
     * Explicitly start regeneration of LoD planes of all the devices
     * in the image. This call should be performed when the user is idle,
     * just to make the quality of image updates better.
     */
    void explicitRegenerateLevelOfDetail();

    /**
     * Install a factory of a stroke strategy, that will be started
     * every time when the scheduler needs to synchronize LOD caches
     * of all the paint devices of the image.
     */
    void setLod0ToNStrokeStrategyFactory(const KisLodSyncStrokeStrategyFactory &factory);

    /**
     * Install a factory of a stroke strategies, that will be started
     * every time when the scheduler needs to postpone/resume all the updates
     * of the *LOD0* strokes.
     */
    void setSuspendResumeUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyPairFactory &factory);

    /**
     * Sets a callback that is called by the scheduler in the
     * beginning of every stroke that declared the necessity
     * of the redo state clearance.
     */
    void setPurgeRedoStateCallback(const std::function<void()> &callback);

    /**
     * Sets a callback that is called bu the scheduler when it
     * thinks that Lod0 tiles **in the GUI** should be refetched
     * by the GUI. Such situation happens in cases when a Lod0
     * stroke has been cancelled, after its LodN buddy has managed
     * to do some manipulations on the canvas. Without this call
     * the GUI will still contain LodN tiles active.
     */
    void setPostSyncLod0GUIPlaneRequestForResumeCallback(const std::function<void()> &callback);

    KisPostExecutionUndoAdapter* lodNPostExecutionUndoAdapter() const;


    /**
     * tryCancelCurrentStrokeAsync() checks whether there is a
     * *running* stroke (which is being executed at this very moment)
     * which is not still open by the owner (endStroke() or
     * cancelStroke() have already been called) and cancels it.
     *
     * \return true if some stroke has been found and cancelled
     *
     * \note This method is *not* part of KisStrokesFacade! It is too
     *       low level for KisImage.  In KisImage it is combined with
     *       more high level requestStrokeCancellation().
     */
    bool tryCancelCurrentStrokeAsync();

    UndoResult tryUndoLastStrokeAsync();

    bool wrapAroundModeSupported() const;
    int currentLevelOfDetail() const;

    void continueUpdate(const QRect &rect);
    void doSomeUsefulWork();
    void spareThreadAppeared();

protected:
    // Trivial constructor for testing support
    KisUpdateScheduler();
    void connectSignals();
    void processQueues();

protected Q_SLOTS:
    /**
     * Called when it is necessary to reread configuration
     */
    void updateSettings();

private:
    friend class UpdatesBlockTester;
    bool haveUpdatesRunning();
    void tryProcessUpdatesQueue();
    void wakeUpWaitingThreads();

    void progressUpdate();

protected:
    struct Private;
    Private * const m_d;
};


class KisTestableSimpleUpdateQueue;
class KisUpdaterContext;

class KRITAIMAGE_EXPORT KisTestableUpdateScheduler : public KisUpdateScheduler
{
public:
    KisTestableUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener,
                               qint32 threadCount);

    KisUpdaterContext* updaterContext();
    using KisUpdateScheduler::processQueues;
};

#endif /* __KIS_UPDATE_SCHEDULER_H */

