#include "edges_processing_common.h"

#include "../utils/unique_list.h"
#include "../utils/sync.h"

#include <yandex/maps/wiki/threadutils/executor.h>
#include <yandex/maps/wiki/common/batch.h>

#include <maps/libs/geolib/include/closest_point.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/geolib/include/dynamic_geometry_searcher.h>

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

#include <queue>
#include <set>
#include <mutex>

namespace maps {
namespace wiki {
namespace topology_fixer {

namespace gl = maps::geolib3;

namespace {

const double MIN_SEARCH_BOX_SIZE = 10.0;

const size_t START_GRID_SIZE = 64;

typedef std::set<DBIdType> OrderedIdSet;

bool
diffEmpty(const OrderedIdSet& ids1, const OrderedIdSet& ids2)
{
    for (auto id : ids1) {
        if (!ids2.count(id)) {
            return false;
        }
    }
    return true;
}

std::list<OrderedIdSet>
splitEdgeIdsByGrid(
    const TopologyData& data,
    size_t gridSize,
    double maxInteractingEdgesDistance)
{
    std::unique_ptr<BBoxSearcher> searcherPtr;
    if (data.ftGroup()->topologyType() == TopologyType::Contour) {
        searcherPtr = std::unique_ptr<BBoxSearcher>(new StaticEdgeSearcher(data));
    } else {
        searcherPtr = std::unique_ptr<BBoxSearcher>(new EdgeMasterSearcher(data));
    }
    const auto& searcher = *searcherPtr;

    const auto& bboxOpt = searcher.bbox();
    if (!bboxOpt) {
        return {};
    }
    auto bbox = *bboxOpt;
    const double boxWidth = std::max(bbox.width() / gridSize, MIN_SEARCH_BOX_SIZE);
    const double boxHeight = std::max(bbox.height() / gridSize, MIN_SEARCH_BOX_SIZE);

    std::list<OrderedIdSet> result;
    for (double minX = bbox.minX(); minX < bbox.maxX(); minX += boxWidth) {
        for (double minY = bbox.minY(); minY < bbox.maxY(); minY += boxHeight) {
            gl::BoundingBox b{{minX, minY}, {minX + boxWidth, minY + boxHeight}};

            OrderedIdSet edgeIds;

            if (data.ftGroup()->topologyType() == TopologyType::Contour) {
                auto unorderedEdgeIds = searcher.idsByBBox(
                    gl::resizeByValue(b, -maxInteractingEdgesDistance));
                edgeIds.insert(unorderedEdgeIds.begin(), unorderedEdgeIds.end());
            } else {
                ASSERT(data.ftGroup()->topologyType() == TopologyType::Linear);

                auto unorderedMasterIds = searcher.idsByBBox(
                    gl::resizeByValue(b, -maxInteractingEdgesDistance));
                OrderedIdSet masterIds(unorderedMasterIds.begin(), unorderedMasterIds.end());
                for (auto masterId : masterIds) {
                    for (auto edgeId : data.master(masterId).edgeIds()) {
                        const auto& unorderedEdgeMasterIds = data.edge(edgeId).masterIds();
                        OrderedIdSet edgeMasterIds(
                            unorderedEdgeMasterIds.begin(), unorderedEdgeMasterIds.end());
                        if (diffEmpty(edgeMasterIds, masterIds)) {
                            edgeIds.insert(edgeId);
                        }
                    }
                }
            }

            if (!edgeIds.empty()) {
                result.push_back(std::move(edgeIds));
            }
        }
    }
    return result;
}

} // namespace

EdgesProcessor::EdgesBatch::EdgesBatch(
        utils::TopologyDataProxy& data,
        std::shared_ptr<utils::EdgesFixer> fixer,
        EdgeIdsList edgeIds,
        DynamicEdgeSearcher searcher,
        double maxInteractingEdgesDistance)
    : data_(data)
    , fixer_(std::move(fixer))
    , edgeIds_(std::move(edgeIds))
    , searcher_(std::move(searcher))
    , maxInteractingEdgesDistance_(maxInteractingEdgesDistance)
{
    edgeIds_.sort(); //Unsorted list causes tests to fail. Why?
}

void
EdgesProcessor::EdgesBatch::doWork()
{
    if (edgeIds_.empty()) {
        return;
    }

    ProfileTimer pt;

    INFO() << "Starting edges batch, edges to process: " << edgeIds_.size();

    size_t counter = 0;

    IdSet processedEdgeIds;

    for (auto edgeId : edgeIds_) {
        counter++;
        if (counter % 1000 == 0) {
            INFO() << "EdgesBatch::doWork counter=" << counter
                << " edge count="<< edgeIds_.size();
        }

        if (processedEdgeIds.count(edgeId)) {
            continue;
        }
        IdSet interestEdgeIds = searcher_.edgeIdsByBBox(
            gl::resizeByValue(
                data_.edge(edgeId).boundingBox(),
                2.0 * maxInteractingEdgesDistance_));
        std::list<EdgeId> otherEdgeIds;
        for (auto otherEdgeId : interestEdgeIds) {
            if (otherEdgeId != edgeId && !processedEdgeIds.count(otherEdgeId)) {
                otherEdgeIds.push_back(otherEdgeId);
            }
        }
        if (otherEdgeIds.empty()) {
            processedEdgeIds.insert(edgeId);
            continue;
        }
        // overlap
        EdgeIdsSet otherEdgeIdsSet(otherEdgeIds.begin(), otherEdgeIds.end());
        std::pair<EdgeIdsSet, EdgeIdsSet> splitEdgeIds =
            processEdgeInteractions(edgeId, std::move(otherEdgeIds));
        // current edge parts are fully processed
        processedEdgeIds.insert(splitEdgeIds.first.begin(), splitEdgeIds.first.end());
        processedEdgeIds.insert(edgeId);
        // other edges must be marked as processed or added to list and index
        for (auto otherEdgePartId : splitEdgeIds.second) {
            bool isNewEdge = !otherEdgeIdsSet.count(otherEdgePartId);
            if (isNewEdge) {
                searcher_.insert(
                    data_.edge(otherEdgePartId).linestring(),
                    data_.edge(otherEdgePartId).boundingBox(),
                    otherEdgePartId);
                edgeIds_.push_back(otherEdgePartId);
            }
        }
        for (auto origOtherEdgeId : otherEdgeIdsSet) {
            bool isDeletedEdge = !splitEdgeIds.second.count(origOtherEdgeId) &&
                !splitEdgeIds.first.count(origOtherEdgeId);
            if (isDeletedEdge) {
                processedEdgeIds.insert(origOtherEdgeId);
            }
        }
    }

    INFO() << "Finish edges batch, edges to process: " << edgeIds_.size()
        << " finished in " << pt.getElapsedTime();
}

namespace {

typedef utils::UniqueList<EdgeId> EdgesUniqueList;

struct EdgeQueueElement {
    EdgeId thisEdgeId;
    boost::optional<EdgeId> otherStartId;
};


/**
 * Replaces edgeId with newEdgeIds in idsList preserving order.
 * Returns id of first unchecked element in idsList if exists.
 */
EdgesUniqueList::Iterator
replaceEdge(EdgesUniqueList& edges, EdgeId edgeId, const EdgeIdsSet& newEdgeIds)
{
    auto listIt = edges.find(edgeId);
    REQUIRE(listIt != edges.end(), "Edge " << edgeId << " not in list");
    return edges.replace(edgeId, EdgeIdsList(newEdgeIds.begin(), newEdgeIds.end()));
}


/**
 * Returns iterator to other edges list to start checking overlaps from it.
 */
EdgeIdsList::iterator
startOtherEdgeIdsIterator(
    const EdgeQueueElement& queueElement,
    EdgesUniqueList& otherEdges)
{
    if (queueElement.otherStartId) {
        auto it = otherEdges.find(*queueElement.otherStartId);
        if (it != otherEdges.end()) {
            return it;
        }
    }

    return otherEdges.begin();
}

} // namespace

std::pair<EdgeIdsSet, EdgeIdsSet>
EdgesProcessor::EdgesBatch::processEdgeInteractions(
    EdgeId edgeId, EdgeIdsList&& otherEdgeIds)
{
    std::deque<EdgeQueueElement> edgesQueue;
    auto edges = EdgesUniqueList(EdgeIdsList{edgeId});
    auto otherEdges = EdgesUniqueList(std::move(otherEdgeIds));
    edgesQueue.push_back({*edges.begin(), boost::none});
    while (!edgesQueue.empty()) {
        auto queueElement = edgesQueue.front();
        edgesQueue.pop_front();
        auto otherEdgesIt = startOtherEdgeIdsIterator(queueElement, otherEdges);
        for ( ; otherEdgesIt != otherEdges.end(); ) {
            const auto edgeId = queueElement.thisEdgeId;
            const auto otherEdgeId = *otherEdgesIt;
            if (edgeId == otherEdgeId) {
                ++otherEdgesIt;
                continue;
            }
            //INFO() << "Processing " << edgeId << ", " << otherEdgeId;
            auto splitResult = fixer_->fixEdges(edgeId, otherEdgeId);
            logSplitResult(fixer_->name(), edgeId, otherEdgeId, splitResult);

            const auto& edgesChunks = splitResult.edgeIds1;
            auto otherEdgesChunks = splitResult.edgeIds2;
            for (auto edgeChunkId : edgesChunks) {
                otherEdgesChunks.erase(edgeChunkId);
            }
            replaceEdge(edges, edgeId, edgesChunks);
            auto nextOtherEdgesIt = replaceEdge(otherEdges, otherEdgeId, otherEdgesChunks);
            if (edgesChunks.size() == 1 && *edgesChunks.begin() == edgeId) {
                // no changes
                otherEdgesIt = nextOtherEdgesIt;
            } else {
                // otherEdgesIt shows to last checked element in splitResult.second, and is not end
                boost::optional<EdgeId> lastCheckedId;
                if (nextOtherEdgesIt != otherEdges.end()) {
                    lastCheckedId = *nextOtherEdgesIt;
                    for (auto newEdgeId : edgesChunks) {
                        edgesQueue.push_front({newEdgeId, lastCheckedId});
                    }
                }
                break; // stop processing this edge
            }
        }
    }
    return {
        {edges.begin(), edges.end()},
        {otherEdges.begin(), otherEdges.end()}};
}

void
EdgesProcessor::EdgesBatch::logSplitResult(
    const std::string& fixerName,
    EdgeId edgeId, EdgeId otherEdgeId,
    const utils::EdgesFixResult& splitResult) const
{
    bool edgeHasNoChanges = splitResult.edgeIds1.size() == 1 &&
        *splitResult.edgeIds1.begin() == edgeId;
    bool otherEdgeHasNoChanges = splitResult.edgeIds2.size() == 1 &&
        *splitResult.edgeIds2.begin() == otherEdgeId;
    if (edgeHasNoChanges && otherEdgeHasNoChanges) {
        return;
    }
    std::ostringstream log;
    log << fixerName << ": fixing edges: [" << edgeId
        << ", " << otherEdgeId << "]";
    if (!edgeHasNoChanges) {
        log << ", edge replaced by [" << toString(splitResult.edgeIds1) << "]";
    }
    if (!otherEdgeHasNoChanges) {
        log << ", affected edge replaced by ["
            << toString(splitResult.edgeIds2) << "]";
    }
    INFO() << log.str();
}

namespace {

const size_t MAX_EDGES_SEARCH_BATCH_SIZE = 100;

EdgeIdsList
findInteractedEdgeIds(
    const TopologyData& data,
    const EdgeIdsList& edgeIds,
    const std::vector<EdgeId>& potentiallyInterestEdgeIds,
    double maxInteractingEdgesDistance,
    ThreadPool& pool)
{
    ProfileTimer pt;

    DynamicEdgeSearcher allEdgesSearcher(
        data,
        EdgeIdsSet(
            potentiallyInterestEdgeIds.begin(),
            potentiallyInterestEdgeIds.end()));

    EdgeIdsList resultEdgeIds;
    std::mutex mutex;

    const size_t batchSize = std::max(
        MAX_EDGES_SEARCH_BATCH_SIZE,
        edgeIds.size() / pool.threadsCount());

    executeInThreads<EdgeIdsList>(
        pool,
        edgeIds,
        batchSize,
        [&](const EdgeIdsList& batch) {
            EdgeIdsSet interactedEdgeIds;
            for (auto edgeId : batch) {
                IdSet interestEdgeIds = allEdgesSearcher.edgeIdsByBBox(
                    gl::resizeByValue(
                        data.edge(edgeId).boundingBox(),
                        4.0 * maxInteractingEdgesDistance));
                interactedEdgeIds.insert(interestEdgeIds.begin(), interestEdgeIds.end());
            }
            EdgeIdsList edgesList(interactedEdgeIds.begin(), interactedEdgeIds.end());

            std::lock_guard<std::mutex> guard(mutex);
            resultEdgeIds.splice(resultEdgeIds.end(), edgesList);
        });

    INFO() << "interactedEdgeIds finished in " << pt.getElapsedTime()
        << " edge count " << edgeIds.size() << " interacted count " << resultEdgeIds.size();

    return resultEdgeIds;
}

struct EdgesBatchData {
    EdgeIdsList edgeIds;
    DynamicEdgeSearcher searcher;
};

std::list<EdgesBatchData>
computeBatches(
    const TopologyData& data,
    const std::list<OrderedIdSet>& originalBatches,
    const OrderedIdSet& processedEdgeIds,
    double maxInteractingEdgesDistance,
    ThreadPool& pool)
{
    std::list<EdgesBatchData> result;

    for (const auto& originalEdgeIds : originalBatches) {
        EdgeIdsList edgeIds;

        std::set_difference(
            originalEdgeIds.begin(), originalEdgeIds.end(),
            processedEdgeIds.begin(), processedEdgeIds.end(),
            std::back_inserter(edgeIds));

        if (edgeIds.empty()) {
            continue;
        }

        auto interactedEdgeIds = findInteractedEdgeIds(
                data,
                edgeIds,
                std::vector<EdgeId>(originalEdgeIds.begin(), originalEdgeIds.end()),
                maxInteractingEdgesDistance,
                pool);
        if (interactedEdgeIds.empty()) {
            continue;
        }

        result.push_back({
            std::move(edgeIds),
            DynamicEdgeSearcher(
                data,
                EdgeIdsSet(interactedEdgeIds.begin(), interactedEdgeIds.end()))
        });
    }

    return result;
}

} // namespace

void EdgesProcessor::operator()(TopologyData& data, FaceLocker& locker, ThreadPool& pool) const
{
    if (data.edgeIds().empty()) {
        return;
    }

    OrderedIdSet processedEdgeIds;
    size_t gridSize = START_GRID_SIZE;
    while (gridSize > 1) {
        INFO() << "GRID SIZE: " << gridSize;

        auto edgeBatchesList = splitEdgeIdsByGrid(
            data,
            gridSize, maxInteractingEdgesDistance_);

        if (edgeBatchesList.empty()) {
            gridSize /= 2;
            continue;
        }

        std::list<EdgesBatchData> batches = computeBatches(
            data,
            edgeBatchesList,
            processedEdgeIds,
            maxInteractingEdgesDistance_,
            pool);

        utils::ThreadSafeTopologyDataProxy dataProxy(data);

        Executor executor;
        for (auto&& batch : batches) {
            executor.addTask(EdgesBatch(
                dataProxy,
                createFixer(dataProxy, locker),
                std::move(batch.edgeIds),
                std::move(batch.searcher),
                maxInteractingEdgesDistance_));
        }
        executor.executeAllInThreads(pool);

        edgeBatchesList = splitEdgeIdsByGrid(
            data,
            gridSize, maxInteractingEdgesDistance_);

        for (const auto& edgeIds : edgeBatchesList) {
            processedEdgeIds.insert(edgeIds.begin(), edgeIds.end());
        }
        gridSize /= 2;
    }
    processLastEdgesBatch(data, locker, pool, processedEdgeIds);
}

void
EdgesProcessor::processLastEdgesBatch(
    TopologyData& data,
    FaceLocker& locker,
    ThreadPool& pool,
    const EdgeIdsSet& processedEdgeIds) const
{
    INFO() << "GRID SIZE: 1";

    EdgeIdsList remainedEdgeIds;
    auto allEdgeIds = data.edgeIds();
    std::remove_copy_if(
        allEdgeIds.begin(), allEdgeIds.end(),
        std::back_inserter(remainedEdgeIds),
        [&](EdgeId edgeId) {
            return processedEdgeIds.count(edgeId) > 0;
        });

    auto indexEdgeIds = findInteractedEdgeIds(
        data,
        remainedEdgeIds,
        allEdgeIds,
        maxInteractingEdgesDistance_,
        pool);

    utils::TrivialTopologyDataProxy dataProxy(data);
    EdgesBatch(
        dataProxy,
        createFixer(dataProxy, locker),
        std::move(remainedEdgeIds),
        DynamicEdgeSearcher(data, EdgeIdsSet(indexEdgeIds.begin(), indexEdgeIds.end())),
        maxInteractingEdgesDistance_)
    ();
}

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