#pragma once

#include "../common.h"
#include "../topology_data.h"
#include "segments/segments_graph.h"
#include "segments/segments_processing_common.h"
#include "segments/segments_overlapper.h"
#include "topology_data_proxy.h"
#include "../utils/sync.h"

#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/segment.h>
#include <maps/libs/geolib/include/polygon.h>

#include <boost/optional.hpp>

#include <list>
#include <unordered_map>
#include <thread>

namespace maps {
namespace wiki {
namespace topology_fixer {
namespace utils {

struct EdgesFixResult {
    EdgeIdsSet edgeIds1;
    EdgeIdsSet edgeIds2;
};

class EdgesFixer {
public:
    EdgesFixer(
            TopologyDataProxy& data,
            FaceLocker& locker,
            double maxInteractingEdgesDistance)
        : data_(data)
        , locker_(locker)
        , maxInteractingEdgesDistance_(maxInteractingEdgesDistance)
    {}

    virtual ~EdgesFixer() = default;

    virtual const std::string& name() const = 0;

    EdgesFixResult fixEdges(EdgeId edgeId1, EdgeId edgeId2)
    {
        if (!edgesShouldBeFixed(edgeId1, edgeId2) ||
            !edgesFixingAllowed(edgeId1, edgeId2))
        {
            return {{edgeId1}, {edgeId2}};
        }
        std::unique_ptr<ProcessingData> pData = initProcessingData(edgeId1, edgeId2);
        try {
            auto splitResult = pData->processEdges();
            if (!splitResult.hasChanges) {
                return {{edgeId1}, {edgeId2}};
            }
            pData->edgesData.at(edgeId1) = splitResult.segmentIds1;
            pData->edgesData.at(edgeId2) = splitResult.segmentIds2;
            if (!pData->newNodesValid()) {
                WARN() << "Edges " << edgeId1 << ", " << edgeId2
                    << " overlapping can not be processed because new nodes are invalid";
                return {{edgeId1}, {edgeId2}};
            }

            pData->createNewNodes();
            pData->createNewEdges();
            if (!pData->restoreTopologyInCopy()) {
                WARN() << "Edges " << edgeId1 << ", " << edgeId2
                    << " can not be processed because of topology invalidation";
                return {{edgeId1}, {edgeId2}};
            }
        } catch (const maps::Exception& e) {
            WARN() << "Edges " << edgeId1 << ", " << edgeId2
                << " can not be processed because of error: " << e;
            return {{edgeId1}, {edgeId2}};
        } catch (const std::exception& e) {
            WARN() << "Edges " << edgeId1 << ", " << edgeId2
                   << " can not be processed because of error: " << e.what();
            return {{edgeId1}, {edgeId2}};
        }

        pData->fixEdgesInOriginalData();

        return {pData->originalToSplittedEdgesMap.at(edgeId1),
                pData->originalToSplittedEdgesMap.at(edgeId2)};
    }

protected:

    class ProcessingData {
    public:
        ProcessingData(EdgeId edgeId1, EdgeId edgeId2, TopologyDataProxy& data, FaceLocker& locker)
            : edgeId1(edgeId1)
            , edgeId2(edgeId2)
            , fullData(data)
            , locker(locker)
            , dataCopy(copyByEdges(data, locker, {edgeId1, edgeId2}))
            , originalDataCopy(dataCopy->clone())
        {
            addEdge(edgeId1);
            addEdge(edgeId2);
        }

        virtual ~ProcessingData() = default;

        void addEdge(EdgeId edgeId);

        virtual SegmentsProcessingResult processEdges() = 0;

        virtual bool newNodesValid() const = 0;

        /**
         * Default implementation creates new nodes where degree != 2.
         */
        virtual void createNewNodes();

        virtual bool restoreTopologyInCopy() = 0;

        void createNewEdges();

        boost::optional<EdgeId>
        findMatchInExistingEdges(const SegmentIdsList& segIds) const;

        void
        fixEdgesInOriginalData();

        typedef std::unordered_map<NodeId, PointId> NodeIdToPointIdMap;
        typedef std::unordered_map<EdgeId, SegmentIdsList> EdgeSegmentsMap;
        typedef std::unordered_map<EdgeId, EdgeIdsSet> EdgeIdsMap;

        EdgeId edgeId1;
        EdgeId edgeId2;
        TopologyDataProxy& fullData;
        FaceLocker& locker;
        SegmentsGraph segmentsGraph;
        std::unique_ptr<TopologyData> dataCopy;
        std::unique_ptr<TopologyData> originalDataCopy;
        NodeIdToPointIdMap nodeIdToPointIdMap;
        EdgeSegmentsMap edgesData;
        EdgeIdsMap originalToSplittedEdgesMap;
        boost::optional<ZLevelType> zLev;
    };

    virtual std::unique_ptr<ProcessingData> initProcessingData(
        EdgeId edgeId1, EdgeId edgeId2) = 0;

    /**
     * Default implementation checks whether all edge nodes have one zlevel value.
     */
    virtual bool edgesFixingAllowed(EdgeId edgeId1, EdgeId edgeId2) const;

    /**
     * Default implementation checks whether edges bounding boxes overlap
     */
    bool edgesShouldBeFixed(EdgeId edgeId1, EdgeId edgeId2);

    TopologyDataProxy& data_;
    FaceLocker& locker_;
    double maxInteractingEdgesDistance_;
};

typedef std::unordered_map<NodeId, PointId> NodeIdToPointIdMap;

SegmentIdsList
addEdgeToSegmentsGraph(const TopologyData& data, EdgeId edgeId,
    SegmentsGraph& graph, NodeIdToPointIdMap& nodeIdsMap);

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