// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#ifndef ASYNC_TASK_H_839147839170432143214321
#define ASYNC_TASK_H_839147839170432143214321

#include <zen/thread.h>
#include <zen/scope_guard.h>
#include <zen/stl_tools.h>
#include <wx/timer.h>


namespace zen
{
/* Run a task in an async thread, but process result in GUI event loop
   -------------------------------------------------------------------
    1. put AsyncGuiQueue instance inside a dialog:
        AsyncGuiQueue guiQueue;

    2. schedule async task and synchronous continuation:
        guiQueue.processAsync(evalAsync, evalOnGui);

    Alternative: wxWidgets' inter-thread communication (wxEvtHandler::QueueEvent) https://wiki.wxwidgets.org/Inter-Thread_and_Inter-Process_communication
            => don't bother, probably too many MT race conditions lurking around                          */

namespace impl
{
struct Task
{
    virtual ~Task() {}
    virtual bool resultReady   () const = 0;
    virtual void evaluateResult()       = 0;
};


template <class ResultType, class Fun>
class ConcreteTask : public Task
{
public:
    template <class Fun2>
    ConcreteTask(std::future<ResultType>&& asyncResult, Fun2&& evalOnGui) :
        asyncResult_(std::move(asyncResult)), evalOnGui_(std::forward<Fun2>(evalOnGui)) {}

    bool resultReady() const override { return isReady(asyncResult_); }

    void evaluateResult() override
    {
        if constexpr (std::is_same_v<ResultType, void>)
        {
            asyncResult_.get();
            evalOnGui_();
        }
        else
            evalOnGui_(asyncResult_.get());
    }

private:
    std::future<ResultType> asyncResult_;
    Fun evalOnGui_; //keep "evalOnGui" strictly separated from async thread: in particular do not copy in thread!
};


class AsyncTasks
{
public:
    AsyncTasks() {}

    template <class Fun, class Fun2>
    void add(Fun&& evalAsync, Fun2&& evalOnGui)
    {
        using ResultType = decltype(evalAsync());

        std::promise<ResultType> prom;
        tasks_.push_back(std::make_unique<ConcreteTask<ResultType, std::decay_t<Fun2>>>(prom.get_future(), std::forward<Fun2>(evalOnGui)));

        //uncaught exception? => let the app crash at throw location: requires InterruptibleThread, not std::thread!
        InterruptibleThread([prom = std::move(prom),
                     fun = std::forward<Fun>(evalAsync)] mutable
        {
            if constexpr (std::is_same_v<ResultType, void>)
            {
                fun(); //throw? => let it crash!
                prom.set_value();
            }
            else
                prom.set_value(fun()); //throw? => let it crash!
        }).detach();
    }
    //equivalent to "evalOnGui(evalAsync())"
    //  -> evalAsync: the usual thread-safety requirements apply!
    //  -> evalOnGui: no thread-safety concerns, but must only reference variables with greater-equal lifetime than the AsyncTask instance!

    void evalResults() //call from GUI thread repreatedly
    {
        if (!inRecursion_) //prevent implicit recursion, e.g. if we're called from an idle event and spawn another one within the callback below
        {
            inRecursion_ = true;
            ZEN_ON_SCOPE_EXIT(inRecursion_ = false);

            std::vector<std::unique_ptr<Task>> readyTasks; //Reentrancy; access to AsyncTasks::add is not protected! => evaluate outside of eraseIf()

            eraseIf(tasks_, [&](std::unique_ptr<Task>& task)
            {
                if (task->resultReady())
                {
                    readyTasks.push_back(std::move(task));
                    return true;
                }
                return false;
            });

            for (std::unique_ptr<Task>& task : readyTasks)
                task->evaluateResult();
        }
    }

    bool empty() const { return tasks_.empty(); }

private:
    AsyncTasks           (const AsyncTasks&) = delete;
    AsyncTasks& operator=(const AsyncTasks&) = delete;

    bool inRecursion_ = false;
    std::vector<std::unique_ptr<Task>> tasks_;
};
}


class AsyncGuiQueue : private wxEvtHandler
{
public:
    explicit AsyncGuiQueue(int pollingMs = 50) :
        pollingMs_(pollingMs) { timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { onTimerEvent(event); }); }

    template <class Fun, class Fun2>
    void processAsync(Fun&& evalAsync, Fun2&& evalOnGui)
    {
        asyncTasks_.add(std::forward<Fun >(evalAsync),
                        std::forward<Fun2>(evalOnGui));
        if (!timer_.IsRunning())
            timer_.Start(pollingMs_ /*unit: [ms]*/);
    }

private:
    void onTimerEvent(wxEvent& event) //schedule and run long-running tasks asynchronously
    {
        asyncTasks_.evalResults(); //process results on GUI queue
        if (asyncTasks_.empty())
            timer_.Stop();
    }

    const int pollingMs_;
    impl::AsyncTasks asyncTasks_;
    wxTimer timer_; //don't use wxWidgets' idle handling => repeated idle requests/consumption hogs 100% cpu!
};

}

#endif //ASYNC_TASK_H_839147839170432143214321
