#ifndef World_h
#define World_h

#include <iostream>
#include <limits>
#include <map>
#include <memory>
#include <string>
#include <vector>

#include "FileSys.h"
#include "Utils.h"
// #include "log.h"
#include "CoordSys.h"
#include "Save.h"
#include "Tile.h"

namespace jf {

template <typename inRetType, typename inIterType, typename inCalcFuncType, typename inAccFuncType>
inRetType ParallelAccumulate(const inIterType &first, const inIterType &last, const inRetType &init, const inCalcFuncType &calcFunc, const inAccFuncType &accFunc) {
    inIterType iter = first;
    inRetType ret = init;
    for (; iter != last; iter++) {
        auto d = *iter;
        ret = accFunc(ret, calcFunc(d));
    }
    return ret;
    return inRetType();
}

template <typename InNodeIdType, typename InNodeDataType, typename inTileType>
class World {
   public:
    typedef InNodeIdType linkIdType;
    typedef InNodeDataType linkType;
    typedef inTileType TileType;
    typedef typename inTileType::configType configType;

    World(Save *ins) : sv(ins) {}
    Save *GetSave() { return sv; }

    std::shared_ptr<inTileType> FindOrAddTile(const TileId &tid) {
        std::shared_ptr<inTileType> ret = nullptr;
        if (!Utils::MapValue(m, tid, ret)) {
            Rect2D r = TId2Rect(tid);
            ret = NewTile(tid, r);
        }
        return ret;
    }

    std::shared_ptr<inTileType> NewTile(const TileId &tid, const Rect2D &r) {
        std::shared_ptr<inTileType> t(new inTileType(r, tid));
        m.insert(std::make_pair(tid, t));
        // jf_log_info("[World::NewTile]: new Tile lonId: {0}: latId {1}", tid.lonId, tid.latId);
        return t;
    }

    std::shared_ptr<inTileType> NewTileDeserialize(std::string &s, const TileId &intid) {
        std::shared_ptr<inTileType> t(new inTileType(intid));
        t->Deserialize(s);
        m.insert(std::make_pair(intid, t));
        // jf_log_info("[World::NewTileDeserialize]: load Tile lonId: {0}: latId {1}", intid.lonId, intid.latId);
        return t;
    }

    void SerializeAll() {
        for (auto &p : m) {
            auto &tid = std::get<0>(p);
            auto &t = std::get<1>(p);
            std::string s;
            t->Serialize(s);
            sv->SaveTile<inTileType>(tid, s);
        }
    }

    void LoadAllTilesOnce() {
        std::set<TileId> tids;
        std::vector<std::string> fs;
        fs = FileSys::GetAllFiles(Save::GetSerializeDir<inTileType>());
        for (const std::string &f : fs) {
            TileId tid = String2TId(f);
            tids.insert(tid);
        }
        LoadTiles(tids);
    }

    std::map<TileId, std::shared_ptr<inTileType>> LoadTileInRect(const Rect2D &r) {
        std::set<TileId> tids = R2TIds(r);
        return LoadTiles(tids);
    }

    std::map<TileId, std::shared_ptr<inTileType>> LoadTiles(const std::set<TileId> &tids) {
        std::map<TileId, std::shared_ptr<inTileType>> ret;
        for (auto &tid : tids) {
            std::shared_ptr<inTileType> t = nullptr;
            if (!Utils::MapValue(m, tid, t)) {
                bool succ = LoadTile(tid, t);
                if (!succ) {
                    dontHaveTiles.insert(tid);
                    // jf_log_error("[World::LoadTileInRect]:cannot find Tile {0}, {1}", tid.lonId, tid.latId);
                    continue;
                }
            }
            ret.insert(std::make_pair(tid, t));
        }
        return ret;
    }

    std::set<InNodeIdType> GetNodeIdsInRect(const Rect2D &r) {
        Profiler::Start("World::GetNodeIdsInRect");
        std::set<TileId> ts = R2TIds(r);
        std::map<TileId, std::shared_ptr<inTileType>> m = LoadTiles(ts);
        std::vector<TileId> tsv = Utils::Map2Vector(m);

        std::set<linkIdType> ret = ParallelAccumulate<std::set<linkIdType>>(
            tsv.begin(), tsv.end(), {},
            [&m, &r](const TileId &t) {
                std::set<linkIdType> ns;
                m[t]->GetNodeIdsInRect(r, ns);
                return ns;
            },
            [](const std::set<linkIdType> &acc, const std::set<linkIdType> &now) { return jf::Utils::SetUnion(acc, now); });

        Profiler::End("World::GetNodeIdsInRect");
        return ret;
    }

    std::set<const InNodeDataType *> GetNodesInRect(const Rect2D &r) {
        Profiler::Start("World::GetNodesInRect");
        std::set<TileId> ts = R2TIds(r);
        std::map<TileId, std::shared_ptr<inTileType>> m = LoadTiles(ts);
        std::vector<TileId> tsv = Utils::Map2Vector(m);

        std::set<const linkType *> ret = ParallelAccumulate<std::set<const linkType *>>(
            tsv.begin(), tsv.end(), {},
            [&m, &r](const TileId &t) {
                std::set<const linkType *> ns;
                m[t]->GetNodesInRect(r, ns);
                return ns;
            },
            [](const std::set<const linkType *> &acc, const std::set<const linkType *> &now) { return jf::Utils::SetUnion(acc, now); });

        Profiler::End("World::GetNodesInRect");
        return ret;
    }

    bool LoadedTile(const TileId &tid, std::shared_ptr<inTileType> &ret) {
        // 已经加载
        if (Utils::MapValue(m, tid, ret)) {
            return true;
        } else {
            return false;
        }
    }

    void RemoveTile(const TileId &tid) {
        // jf_log_info("[World::RemoveTile] remove Tile {0} {1}", tid.lonId, tid.latId);
        m.erase(tid);
    }

    /**
     * @brief 根据nid得到NodeData
     *
     * @param nid nodeid
     * @return nodeData& node data
     */
    const InNodeDataType *GetNodeData(const InNodeIdType &nid) {
        std::shared_ptr<inTileType> t;
        for (TileId tid : nid.GetTileIds()) {
            t = Utils::MapValue(m, tid);
            if (!t) {
                if (!LoadTile(tid, t)) {
                    // jf_log_error("World::GetNodeData failed, tid: {0}, {1}", tid.lonId, tid.latId);
                    return nullptr;
                }
            }
        }
        if (t.get() == NULL) {
            // jf_log_error("World::GetNodeData failed");
            return nullptr;
        }
        return t->GetNodeData(nid);
    }

    Rect2D GetWorldRect() {
        Point minP(0, 0, 0), maxP(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), 0);
        for (auto &p : m) {
            inTileType *t = std::get<1>(p);
            Rect2D r = t->GetRect();
            minP.dLon = std::min(minP.dLon, r.lb.dLon);
            minP.dLat = std::min(minP.dLat, r.lb.dLat);
            maxP.dLon = std::max(maxP.dLon, r.rt.dLon);
            maxP.dLat = std::max(maxP.dLat, r.rt.dLat);
        }
        return Rect2D(minP, maxP);
    }

    void RemoveUnsedTiles(const Point &gps) {
        TileId gpsTid = P2TId(gps);
        std::vector<TileId> removeTids;
        for (auto &pair : m) {
            TileId tid = std::get<0>(pair);
            if (abs(tid.nLonId - gpsTid.nLonId) > 3 || abs(tid.nLatId - gpsTid.nLatId) > 3) {
                removeTids.push_back(tid);
            }
        }
        for (auto tid : removeTids) {
            RemoveTile(tid);
        }
    }

    const std::map<TileId, std::shared_ptr<inTileType>> &GetTiles() const { return m; }
    bool GetTile(TileId tid, std::shared_ptr<inTileType> &ret) { return Utils::MapValue(m, tid, ret); }

    bool GetNearestNodeId(const Point &ingps, const Rect2D &inr, InNodeIdType &out_id, double &out_d, bool consider_alt = false) {
        double min_d = std::numeric_limits<double>::max();
        InNodeIdType min_id;
        bool got = false;
        std::set<TileId> tids = R2TIds(inr);
        for (auto &tid : tids) {
            std::shared_ptr<inTileType> t;
            if (!LoadTile(tid, t)) {
                continue;
            }
            if (t->GetNearestNodeId(ingps, inr, out_id, out_d, consider_alt)) {
                if (min_d > out_d) {
                    min_d = out_d;
                    min_id = out_id;
                    got = true;
                }
            }
        }
        out_id = min_id;
        out_d = min_d;
        return got;
    }

    // 广度优先遍历
    template <typename iterNodeType, typename accResultType,
              typename visitFuncType>
    void BFS(const iterNodeType &from,        // 起始id
             accResultType &ret,              // 目标值
             const visitFuncType &visitFunc,  // 访问函数
             bool backward = false            // 按prev遍历，而非next
    ) {
        std::set<iterNodeType> nexts;
        if (visitFunc(false, true, from, ret, nexts)) {
            return;
        };
        std::set<iterNodeType> visited{from};
        while (nexts.size() != 0) {
            std::set<iterNodeType> current(nexts.begin(), nexts.end());
            nexts.clear();
            for (const iterNodeType &c : current) {
                if (visitFunc(jf::Utils::Contains(visited, c), false, c, ret, nexts)) {
                    visited.insert(c);
                    return;
                };
                visited.insert(c);
            }
        }
        return;
    }

   private:
    bool LoadTile(const TileId &tid, std::shared_ptr<inTileType> &ret) {
        if (Utils::SetContains(dontHaveTiles, tid)) {
            return false;
        }
        if (LoadedTile(tid, ret)) {
            return true;
        }
        std::string s;
        bool succ = sv->LoadTile<inTileType>(tid, s);
        if (succ) {
            // jf_log_info("[World::LoadTile] loaded Tile {0}, {1}", tid.lonId, tid.latId);
            ret = NewTileDeserialize(s, tid);
            return true;
        } else {
            dontHaveTiles.insert(tid);
            return false;
        }
    }

    std::map<TileId, std::shared_ptr<inTileType>> m;
    std::set<TileId> dontHaveTiles;
    Save *sv;
};
}  // namespace jf
#endif