#pragma once

#include "common.h"
#include "topology_group.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>

#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/point.h>

#include <boost/range/iterator_range.hpp>
#include <boost/iterator/iterator_facade.hpp>
#include <boost/utility.hpp>

#include <cstdint>
#include <string>
#include <list>
#include <sstream>
#include <memory>
#include <unordered_map>
#include <unordered_set>

namespace maps {
namespace wiki {
namespace topology_fixer {

class TopologyLoader;

namespace utils {

class EdgesOverlapsFixer;
class TopologyDataProxy;

} // namespace utils

class Node;
class Edge;
class Face;
class Master;

typedef int ZLevelType;

enum class FaceRelationType { Interior, Exterior };

typedef std::unordered_map<DBIdType, Node> Nodes;
typedef std::unordered_map<DBIdType, Edge> Edges;
typedef std::unordered_map<DBIdType, Face> Faces;
typedef std::unordered_map<DBIdType, Master> Masters;
typedef std::unordered_map<DBIdType, FaceRelationType> FaceRelations;

typedef std::unordered_map<std::string, std::string> AttributesMap;

struct NodeData
{
    DBIdType id;
    geolib3::Point2 point;
};

class Node {
public:
    DBIdType id() const { return id_; }
    const geolib3::Point2& point() const { return point_; }

    const IdSet& edgeIds() const { return edgeIds_; }

    bool isIsolated() const { return edgeIds_.empty(); }

private:
    Node(DBIdType id, const geolib3::Point2& point)
        : id_(id)
        , point_(point)
    {}

    friend class TopologyData;

    DBIdType id_;
    geolib3::Point2 point_;

    IdSet edgeIds_;
};

struct EdgeData
{
    DBIdType id;
    geolib3::Polyline2 linestring;
    DBIdType fnodeId;
    DBIdType tnodeId;
    ZLevelType fZlev;
    ZLevelType tZlev;
};

class Edge {
public:
    DBIdType id() const { return id_; }

    const geolib3::Polyline2& linestring() const { return linestring_; }
    const geolib3::BoundingBox& boundingBox() const { return boundingBox_; }

    DBIdType fnodeId() const { return fnodeId_; }
    DBIdType tnodeId() const { return tnodeId_; }
    ZLevelType fZlev() const { return fZlev_; }
    ZLevelType tZlev() const { return tZlev_; }

    AttributesMap getAttrs() const;

    const IdSet& faceIds() const { return faceIds_; }
    const IdSet& masterIds() const { return masterIds_; }

    bool isLinear() const { return faceIds_.empty() && !masterIds_.empty(); }
    bool isContour() const { return !faceIds_.empty() && masterIds_.empty(); }
    bool isIsolated() const { return faceIds_.empty() && masterIds_.empty(); }

private:
    Edge(
        DBIdType id,
        geolib3::Polyline2 linestring,
        DBIdType fnodeId,
        DBIdType tnodeId,
        ZLevelType fZlev,
        ZLevelType tZlev);

    void setNewGeometry(geolib3::Polyline2 geom);

    friend class TopologyData;

    DBIdType id_;
    DBIdType fnodeId_;
    DBIdType tnodeId_;
    ZLevelType fZlev_;
    DBIdType tZlev_;

    IdSet faceIds_;
    IdSet masterIds_;

    geolib3::Polyline2 linestring_;
    geolib3::BoundingBox boundingBox_;
};

struct FaceEdgeData
{
    DBIdType faceId;
    DBIdType edgeId;
};

struct EdgeMasterData
{
    DBIdType masterId;
    DBIdType edgeId;
};

class Face {
public:
    DBIdType id() const { return id_; }

    const IdSet& edgeIds() const { return edgeIds_; }
    const IdSet& masterIds() const { return masterIds_; }

private:
    explicit Face(DBIdType id) : id_(id) {}

    friend class TopologyData;

    DBIdType id_;
    IdSet edgeIds_;
    IdSet masterIds_;
};

struct FaceMasterData
{
    DBIdType masterId;
    DBIdType faceId;
    FaceRelationType relationType;
};

class FtAttributes {
public:
    FtAttributes(
        FtTypeId ftTypeId,
        DispClassId dispClassId,
        boost::optional<SearchClassId> searchClassId,
        boost::optional<MasterId> parentId,
        const boost::optional<std::string>& isocode,
        const boost::optional<std::string>& subcode);

    FtTypeId ftTypeId() const { return ftTypeId_; }
    DispClassId dispClassId() const { return dispClassId_; }
    boost::optional<SearchClassId> searchClassId() const { return searchClassId_; }
    bool hasName() const { return hasName_; }
    bool isAddrAssociatedWith() const { return isAddrAssociatedWith_; }
    boost::optional<NodeId> centerId() const { return centerId_; }
    boost::optional<MasterId> parentId() const { return parentId_; }
    boost::optional<MasterId> adId() const { return adId_; }
    const boost::optional<std::string>& isocode() const { return isocode_; }
    const boost::optional<std::string>& subcode() const { return subcode_; }

    bool canBeDeleted() const;
    bool canBeMergedWith(const FtAttributes& toLeaveId) const;

private:
    friend class TopologyLoader;

    FtTypeId ftTypeId_;
    DispClassId dispClassId_;
    boost::optional<SearchClassId> searchClassId_;
    bool hasName_;
    bool isAddrAssociatedWith_;
    bool hasChildren_;
    boost::optional<NodeId> centerId_;
    boost::optional<MasterId> parentId_;
    boost::optional<MasterId> adId_;
    boost::optional<std::string> isocode_;
    boost::optional<std::string> subcode_;
};

class Master {
public:
    DBIdType id() const { return id_; }

    boost::iterator_range<FaceRelations::const_iterator> faceRels() const;
    boost::iterator_range<FaceRelations::iterator> faceRels();

    IdSet faceIds() const;
    IdSet faceIds(FaceRelationType relationType) const;

    FaceRelationType relationType(DBIdType faceId) const;

    const IdSet& edgeIds() const { return edgeIds_; }

    const boost::optional<FtAttributes>& ftAttributes() const { return ftAttributes_; }

private:
    explicit Master(DBIdType id) : id_(id) {}
    Master(DBIdType id, const FtAttributes& ftAttrs)
        : id_(id)
        , ftAttributes_(ftAttrs)
    {}

    friend class TopologyData;

    DBIdType id_;
    FaceRelations faceRels_;
    IdSet edgeIds_;
    boost::optional<FtAttributes> ftAttributes_;
};

enum class DeletionMode { Restrict, Cascade };

class TopologyData {
public:
    template <
        class NodeInputIteratorRange,
        class EdgeInputIteratorRange,
        class FaceEdgeInputIteratorRange,
        class MasterEdgeInputIteratorRange,
        class MasterFaceInputIteratorRange>
    TopologyData(
        TopologyGroupPtr ftGroup,
        const NodeInputIteratorRange& nodes,
        const EdgeInputIteratorRange& edges,
        const FaceEdgeInputIteratorRange& faceEdges,
        const MasterEdgeInputIteratorRange& masterEdges,
        const MasterFaceInputIteratorRange& masterFaces,
        std::shared_ptr<IdGenerator> gen,
        SRID srid);

    TopologyData(const TopologyData&) = delete;
    TopologyData& operator = (const TopologyData&) = delete;

    std::unique_ptr<TopologyData> clone() const;

    TopologyData(TopologyData&& oth)
        : nodes_(std::move(oth.nodes_))
        , edges_(std::move(oth.edges_))
        , faces_(std::move(oth.faces_))
        , masters_(std::move(oth.masters_))
        , idGen_(oth.idGen_)
        , srid_(oth.srid_)
        , ftGroup_(oth.ftGroup_)
        , deletedFtAttributes_(std::move(oth.deletedFtAttributes_))
    {}

    TopologyData& operator = (TopologyData&&) = delete;

    boost::iterator_range<Nodes::iterator> nodes();
    boost::iterator_range<Edges::iterator> edges();
    boost::iterator_range<Faces::iterator> faces();
    boost::iterator_range<Masters::iterator> masters();
    boost::iterator_range<Nodes::const_iterator> nodes() const;
    boost::iterator_range<Edges::const_iterator> edges() const;
    boost::iterator_range<Faces::const_iterator> faces() const;
    boost::iterator_range<Masters::const_iterator> masters() const;

    std::vector<DBIdType> nodeIds() const;
    std::vector<DBIdType> edgeIds() const;
    std::vector<DBIdType> faceIds() const;

    std::vector<geolib3::Point2> faceVertices(DBIdType faceId) const;

    bool isNodeLinear(NodeId nodeId) const;
    bool isNodeContour(NodeId nodeId) const;

    void setNodePos(NodeId nodeId, const geolib3::Point2& pos)
    {
        Node& node = this->node(nodeId);
        for (auto edgeId : node.edgeIds()) {
            const Edge& edge = this->edge(edgeId);
            const auto& points = edge.linestring().points();
            if (edge.fnodeId() == nodeId) {
                REQUIRE(pos == points.front(),
                    "Edge " << edgeId << " start point is not equal to node " << nodeId << " new pos");
            }
            if (edge.tnodeId() == nodeId) {
                REQUIRE(pos == points.back(),
                    "Edge " << edgeId << " end point is not equal to node " << nodeId << " new pos");
            }
        }
        node.point_ = pos;
    }
    void setEdgeGeometry(EdgeId edgeId, geolib3::Polyline2 geom)
    {
        Edge& edge = this->edge(edgeId);
        const Node& fnode = this->node(edge.fnodeId());
        const Node& tnode = this->node(edge.tnodeId());
        REQUIRE(geom.points().front() == fnode.point(),
            "Edge " << edgeId << " new start point is not equal to f_node " << fnode.id() << " pos");
        REQUIRE(geom.points().back() == tnode.point(),
            "Edge " << edgeId << " new end point is not equal to t_node " << tnode.id() << " pos");
        edge.setNewGeometry(std::move(geom));
    }

    void setFNode(DBIdType edgeId, DBIdType nodeId);
    void setTNode(DBIdType edgeId, DBIdType nodeId);

    void addFaceEdge(DBIdType faceId, DBIdType edgeId);
    void addMasterFaceRel(DBIdType masterId, DBIdType faceId, FaceRelationType isInterior);
    void addMasterEdgeRel(DBIdType masterId, DBIdType edgeId);

    void setFtAttributes(MasterId ftId, const FtAttributes& attributes);

    bool canRemoveNode(const Node& node) const;

    void removeNode(DBIdType id);
    void removeEdge(DBIdType id, DeletionMode mode);
    void removeFace(DBIdType id, DeletionMode mode);
    /**
     * For ft's: saves deleted feature data in special map to
     *   clear database tables during result upload.
     */
    void removeMaster(DBIdType id, DeletionMode mode);

    /**
     * Does not remove face if it became empty after relation deletion.
     */
    void removeFaceEdgeRel(DBIdType faceId, DBIdType edgeId);
    /**
     * Does not remove master if it became empty after relation deletion.
     */
    void removeMasterFaceRel(DBIdType masterId, DBIdType faceId);
    /**
     * Does not remove master if it became empty after relation deletion.
     */
    void removeMasterEdgeRel(DBIdType masterId, DBIdType edgeId);

    bool nodeExists(NodeId nodeId) const { return nodes_.count(nodeId); }

    DBIdType addNode(const geolib3::Point2& point);
    void addExistingNode(DBIdType id, const geolib3::Point2& point);

    DBIdType addEdge(
        geolib3::Polyline2 linestring,
        DBIdType fnodeId,
        DBIdType tnodeId,
        ZLevelType fZlev,
        ZLevelType tZlev);

    void addExistingEdge(
        DBIdType id,
        geolib3::Polyline2 linestring,
        DBIdType fnodeId,
        DBIdType tnodeId,
        ZLevelType fZlev,
        ZLevelType tZlev);

    DBIdType addFace();
    void addExistingFace(DBIdType id);

    DBIdType addMaster();
    DBIdType addMaster(const FtAttributes& ftAttrs);
    void addExistingMaster(DBIdType id);
    void addExistingMaster(DBIdType id, const FtAttributes& ftAttrs);

    //! Removes second node
    void uniteNodes(DBIdType toLeaveId, DBIdType toRemoveId,
        double maxRemovableEdgeLength);

    void copyFaceRelations(const Edge& source, DBIdType target);

    void replaceEdge(DBIdType edgeId, const IdSet& newEdgeIds);

    Node& node(DBIdType id);
    Edge& edge(DBIdType id);
    Face& face(DBIdType id);
    Master& master(DBIdType id);

    const Node& node(DBIdType id) const;
    const Edge& edge(DBIdType id) const;
    const Face& face(DBIdType id) const;
    const Master& master(DBIdType id) const;

    const TopologyGroupPtr& ftGroup() const { return ftGroup_; }
    std::shared_ptr<IdGenerator> idGenerator() const { return idGen_; }
    SRID srid() const { return srid_; }

    typedef std::unordered_map<MasterId, FtAttributes> DeletedFeaturesData;
    const DeletedFeaturesData& deletedFeaturesData() const
    {
        return deletedFtAttributes_;
    }

private:
    void addMaster(DBIdType masterId);

    friend class utils::EdgesOverlapsFixer;
    friend class utils::TopologyDataProxy;

    DBIdType addNodeInternal(
        DBIdType id,
        const geolib3::Point2& point);

    /**
     * The only place where No option is used is in TopologyData constructor
     *   to let JunctionsMismatchFixer correct node misplacement.
     */
    enum class CheckMisplacedNodes { Yes, No };

    DBIdType addEdgeInternal(
        DBIdType id,
        geolib3::Polyline2 linestring,
        DBIdType fnodeId,
        DBIdType tnodeId,
        ZLevelType fZlev,
        ZLevelType tZlev,
        CheckMisplacedNodes checkMisplacedNodes = CheckMisplacedNodes::Yes);

    DBIdType getId();

    Nodes nodes_;
    Edges edges_;
    Faces faces_;
    Masters masters_;

    std::shared_ptr<IdGenerator> idGen_;
    const SRID srid_;

    const TopologyGroupPtr ftGroup_;

    DeletedFeaturesData deletedFtAttributes_;
};

template <
    class NodeInputIteratorRange,
    class EdgeInputIteratorRange,
    class FaceEdgeInputIteratorRange,
    class MasterEdgeInputIteratorRange,
    class MasterFaceInputIteratorRange>
TopologyData::TopologyData(
        TopologyGroupPtr ftGroup,
        const NodeInputIteratorRange& nodes,
        const EdgeInputIteratorRange& edges,
        const FaceEdgeInputIteratorRange& faceEdges,
        const MasterEdgeInputIteratorRange& masterEdges,
        const MasterFaceInputIteratorRange& masterFaces,
        std::shared_ptr<IdGenerator> gen,
        SRID srid)
    : idGen_(std::move(gen))
    , srid_(srid)
    , ftGroup_(ftGroup)
{
    for (const auto& data : nodes) {
        nodes_.insert({data.id, Node(data.id, data.point)});
    }

    for (const auto& data : edges) {
        addEdgeInternal(data.id, data.linestring,
            data.fnodeId, data.tnodeId,
            data.fZlev, data.tZlev,
            CheckMisplacedNodes::No);
    }

    for (const auto& data : faceEdges) {
        faces_.insert({data.faceId, Face(data.faceId)});
        addFaceEdge(data.faceId, data.edgeId);
    }

    for (const auto& data : masterEdges) {
        DBIdType masterId = data.masterId;
        DBIdType edgeId = data.edgeId;
        try {
            edge(edgeId); // check exists
        }
        catch (const MissingObjectException&) {
            ERROR() << "Master to edge relation not added: edge " << edgeId << " not found";
            continue;
        }

        masters_.insert({masterId, Master(masterId)});
        addMasterEdgeRel(masterId, edgeId);
    }
    for (const auto& data : masterFaces) {
        DBIdType masterId = data.masterId;
        DBIdType faceId = data.faceId;
        try {
            face(faceId); // check exists
        }
        catch (const MissingObjectException&) {
            ERROR() << "Master to face relation not added: face " << faceId << " not found";
            continue;
        }

        masters_.insert({masterId, Master(masterId)});
        addMasterFaceRel(masterId, faceId, data.relationType);
    }
}

} // namespace topology_fixer
} // namespace wiki
} // namespace maps
