#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_ASYNCNAVMESHUPDATER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_ASYNCNAVMESHUPDATER_H

#include "agentbounds.hpp"
#include "changetype.hpp"
#include "guardednavmeshcacheitem.hpp"
#include "navmeshcacheitem.hpp"
#include "navmeshdb.hpp"
#include "navmeshtilescache.hpp"
#include "offmeshconnectionsmanager.hpp"
#include "sharednavmeshcacheitem.hpp"
#include "stats.hpp"
#include "tilecachedrecastmeshmanager.hpp"
#include "tileposition.hpp"
#include "waitconditiontype.hpp"

#include <boost/geometry/geometries/point.hpp>
#include <boost/geometry/index/rtree.hpp>

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <iosfwd>
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <thread>
#include <tuple>

class dtNavMesh;

namespace Loading
{
    class Listener;
}

namespace DetourNavigator
{
    enum class JobState
    {
        Initial,
        WithDbResult,
    };

    struct Job
    {
        const std::size_t mId;
        const AgentBounds mAgentBounds;
        const std::weak_ptr<GuardedNavMeshCacheItem> mNavMeshCacheItem;
        const ESM::RefId mWorldspace;
        const TilePosition mChangedTile;
        std::chrono::steady_clock::time_point mProcessTime;
        ChangeType mChangeType;
        JobState mState = JobState::Initial;
        std::vector<std::byte> mInput;
        std::shared_ptr<RecastMesh> mRecastMesh;
        std::optional<TileData> mCachedTileData;
        std::unique_ptr<PreparedNavMeshData> mGeneratedNavMeshData;

        Job(const AgentBounds& agentBounds, std::weak_ptr<GuardedNavMeshCacheItem> navMeshCacheItem,
            ESM::RefId worldspace, const TilePosition& changedTile, ChangeType changeType,
            std::chrono::steady_clock::time_point processTime);
    };

    using JobIt = std::list<Job>::iterator;

    class SpatialJobQueue
    {
    public:
        std::size_t size() const { return mSize; }

        void clear();

        void push(JobIt job);

        std::optional<JobIt> pop(TilePosition playerTile);

        void update(TilePosition playerTile, int maxTiles, std::vector<JobIt>& removing);

    private:
        using IndexPoint = boost::geometry::model::point<int, 2, boost::geometry::cs::cartesian>;
        using UpdatingMap = std::map<TilePosition, std::deque<JobIt>>;
        using IndexValue = std::pair<IndexPoint, UpdatingMap::iterator>;

        std::size_t mSize = 0;
        UpdatingMap mValues;
        boost::geometry::index::rtree<IndexValue, boost::geometry::index::linear<4>> mIndex;
    };

    class JobQueue
    {
    public:
        JobQueueStats getStats() const
        {
            return JobQueueStats{
                .mRemoving = mRemoving.size(),
                .mUpdating = mUpdating.size(),
                .mDelayed = mDelayed.size(),
            };
        }

        bool hasJob(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const;

        void clear();

        void push(JobIt job, std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now());

        std::optional<JobIt> pop(
            TilePosition playerTile, std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now());

        void update(TilePosition playerTile, int maxTiles,
            std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now());

    private:
        std::vector<JobIt> mRemoving;
        SpatialJobQueue mUpdating;
        std::deque<JobIt> mDelayed;
    };

    enum class JobStatus
    {
        Done,
        Fail,
        MemoryCacheMiss,
    };

    std::ostream& operator<<(std::ostream& stream, JobStatus value);

    class DbJobQueue
    {
    public:
        void push(JobIt job);

        std::optional<JobIt> pop();

        void update(TilePosition playerTile);

        void stop();

        DbJobQueueStats getStats() const;

    private:
        mutable std::mutex mMutex;
        std::condition_variable mHasJob;
        SpatialJobQueue mReading;
        std::deque<JobIt> mWriting;
        TilePosition mPlayerTile;
        bool mShouldStop = false;
    };

    class AsyncNavMeshUpdater;

    class DbWorker
    {
    public:
        DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr<NavMeshDb>&& db, TileVersion version,
            const RecastSettings& recastSettings, bool writeToDb);

        ~DbWorker();

        DbWorkerStats getStats() const;

        void enqueueJob(JobIt job);

        void update(TilePosition playerTile) { mQueue.update(playerTile); }

        void stop();

    private:
        AsyncNavMeshUpdater& mUpdater;
        const RecastSettings& mRecastSettings;
        const std::unique_ptr<NavMeshDb> mDb;
        const TileVersion mVersion;
        bool mWriteToDb;
        TileId mNextTileId;
        ShapeId mNextShapeId;
        DbJobQueue mQueue;
        std::atomic_bool mShouldStop{ false };
        std::atomic_size_t mGetTileCount{ 0 };
        std::thread mThread;

        inline void run() noexcept;

        inline void processJob(JobIt job);

        inline void processReadingJob(JobIt job);

        inline void processWritingJob(JobIt job);
    };

    class AsyncNavMeshUpdater
    {
    public:
        AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager,
            OffMeshConnectionsManager& offMeshConnectionsManager, std::unique_ptr<NavMeshDb>&& db);
        ~AsyncNavMeshUpdater();

        void post(const AgentBounds& agentBounds, const SharedNavMeshCacheItem& navMeshCacheItem,
            const TilePosition& playerTile, ESM::RefId worldspace,
            const std::map<TilePosition, ChangeType>& changedTiles);

        void wait(WaitConditionType waitConditionType, Loading::Listener* listener);

        void stop();

        AsyncNavMeshUpdaterStats getStats() const;

        void enqueueJob(JobIt job);

        void removeJob(JobIt job);

    private:
        std::reference_wrapper<const Settings> mSettings;
        std::reference_wrapper<TileCachedRecastMeshManager> mRecastMeshManager;
        std::reference_wrapper<OffMeshConnectionsManager> mOffMeshConnectionsManager;
        std::atomic_bool mShouldStop;
        mutable std::mutex mMutex;
        std::condition_variable mHasJob;
        std::condition_variable mDone;
        std::condition_variable mProcessed;
        std::list<Job> mJobs;
        JobQueue mWaiting;
        std::set<std::tuple<AgentBounds, TilePosition>> mPushed;
        Misc::ScopeGuarded<TilePosition> mPlayerTile;
        NavMeshTilesCache mNavMeshTilesCache;
        Misc::ScopeGuarded<std::set<std::tuple<AgentBounds, TilePosition>>> mProcessingTiles;
        std::map<std::tuple<AgentBounds, TilePosition>, std::chrono::steady_clock::time_point> mLastUpdates;
        std::set<std::tuple<AgentBounds, TilePosition>> mPresentTiles;
        std::vector<std::thread> mThreads;
        std::unique_ptr<DbWorker> mDbWorker;
        std::atomic_size_t mDbGetTileHits{ 0 };

        void process() noexcept;

        JobStatus processJob(Job& job);

        inline JobStatus processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem);

        inline JobStatus processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem);

        inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job,
            const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh);

        JobIt getNextJob();

        void postThreadJob(JobIt job, std::deque<JobIt>& queue);

        void writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const;

        bool lockTile(std::size_t jobId, const AgentBounds& agentBounds, const TilePosition& changedTile);

        void unlockTile(std::size_t jobId, const AgentBounds& agentBounds, const TilePosition& changedTile);

        inline std::size_t getTotalJobs() const;

        void cleanupLastUpdates();

        inline void waitUntilJobsDoneForNotPresentTiles(Loading::Listener* listener);

        inline void waitUntilAllJobsDone();
    };
}

#endif