#include <yandex/maps/wiki/validator/area_of_interest.h>

#include "common/utils.h"
#include "loader/db_gateway.h"

#include <yandex/maps/wiki/common/batch.h>
#include <yandex/maps/wiki/geom_tools/polygonal/builder.h>
#include <yandex/maps/wiki/revision/common.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/coverage5/builder.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/common/include/unique_ptr.h>

#include <geos/geom/Polygon.h>
#include <geos/operation/buffer/BufferOp.h>

#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>

#include <functional>
#include <iterator>
#include <set>
#include <sstream>
#include <utility>

namespace rev = maps::wiki::revision;
namespace rf = maps::wiki::revision::filters;
namespace fs = boost::filesystem;

namespace maps {
namespace wiki {
namespace validator {

namespace {

// In meters
const double MIN_DISTANCE_TO_BUFFER = 10;
const double MAX_DISTANCE_TO_BUFFER = 100000;

const int BUFFER_QUADRANT_SEGMENTS = 4;

const size_t OBJECTS_BATCH_COUNT = 500;
const double GEOM_TOLERANCE = 0.001;

const std::string CAT_AD = "ad";
const std::string CAT_AD_NEUTRAL = "ad_neutral";
const std::string CAT_REGION = "region";

const std::set<std::string> CONTOUR_CAT_IDS = {
    CAT_AD,
    CAT_AD_NEUTRAL
};

const std::string CAT_FC_POSTFIX = "_fc";

const std::string ATTR_CAT_PREFIX = "cat";
const std::string ATTR_IS_INTERIOR_POSTFIX = "is_interior";
const std::string ATTR_LEVEL_KIND = "level_kind";
const std::string ATTR_LEVEL_KIND_COUNTRY = "1";

const std::string COVERAGE_LAYER_VERSION = "1";

struct RegionElements
{
    std::vector<DBID> adIds;
    std::vector<DBID> adNeutralIds;
};

bool isInteriorFace(const rev::Attributes& faceAttrs)
{
    for (const auto& attr : faceAttrs) {
        if (boost::algorithm::ends_with(attr.first, ATTR_IS_INTERIOR_POSTFIX)) {
            return true;
        }
    }
    return false;
}

geolib3::LinearRing2 buildFaceGeom(
    rev::Snapshot& snapshot,
    const rev::ObjectRevision& faceRev)
{
    auto lineRelRevs = snapshot.loadSlaveRelations(faceRev.id().objectId());

    geolib3::PolylinesVector lineGeoms;
    lineGeoms.reserve(lineRelRevs.size());

    common::applyBatchOp<rev::Revisions>(
        lineRelRevs,
        OBJECTS_BATCH_COUNT,
        [&](const rev::Revisions& batch) {
            std::vector<DBID> lineOids;
            for (const auto& lineRelRev : batch) {
                lineOids.push_back(lineRelRev.data().relationData->slaveObjectId());
            }

            // FC object can have only EL objects as childs - no need to check category
            auto lineRevFilter =
                rf::ObjRevAttr::objectId().in(lineOids) &&
                rf::ObjRevAttr::isNotRelation() &&
                rf::ObjRevAttr::isNotDeleted() &&
                rf::Geom::defined();

            for (const auto& lineRev : snapshot.objectRevisionsByFilter(lineRevFilter)) {
                lineGeoms.push_back(geolib3::WKB::read<geolib3::Polyline2>(*lineRev.data().geometry));
            }
        });

    return geom_tools::LinearRingBuilder::build(lineGeoms, GEOM_TOLERANCE);
}

PolygonVector buildContourGeom(
    rev::Snapshot& snapshot,
    rev::DBID contourId,
    const std::string& catId)
{
    auto faceRelRevs = snapshot.loadSlaveRelations(contourId);

    geom_tools::GeolibLinearRingVector extFaceGeoms;
    geom_tools::GeolibLinearRingVector intFaceGeoms;
    extFaceGeoms.reserve(faceRelRevs.size());
    intFaceGeoms.reserve(faceRelRevs.size());

    common::applyBatchOp<rev::Revisions>(
        faceRelRevs,
        OBJECTS_BATCH_COUNT,
        [&](const rev::Revisions& batch) {
            std::vector<DBID> faceOids;
            for (const auto& faceRelRev : batch) {
                faceOids.push_back(faceRelRev.data().relationData->slaveObjectId());
            }

            auto faceRevFilter =
                rf::Attr(ATTR_CAT_PREFIX + ":" + catId + CAT_FC_POSTFIX).defined() &&
                rf::ObjRevAttr::objectId().in(faceOids) &&
                rf::ObjRevAttr::isNotRelation() &&
                rf::ObjRevAttr::isNotDeleted() &&
                !rf::Geom::defined();

            for (const auto& faceRev : snapshot.objectRevisionsByFilter(faceRevFilter)) {
                auto faceGeom = buildFaceGeom(snapshot, faceRev);
                if (isInteriorFace(*faceRev.data().attributes)) {
                    intFaceGeoms.push_back(std::move(faceGeom));
                } else {
                    extFaceGeoms.push_back(std::move(faceGeom));
                }
            }
        });
    return geom_tools::PolygonBuilder::build(
        extFaceGeoms, intFaceGeoms, geom_tools::ValidateResult::Yes);
}

typedef std::function<bool(const geolib3::Polygon2&)> GeomFilter;

DBID2PolygonVector buildExcludedCountriesGeom(
    rev::Snapshot& snapshot,
    const std::vector<DBID>& oidsToSkip,
    GeomFilter geomFilter)
{
    REQUIRE(!oidsToSkip.empty(), "Empty vector with oids to skip - must contain all countries from region");
    auto countryRevFilter =
        rf::Attr(ATTR_CAT_PREFIX + ":" + CAT_AD).defined() &&
        rf::Attr(CAT_AD + ":" + ATTR_LEVEL_KIND) == ATTR_LEVEL_KIND_COUNTRY &&
        !rf::ObjRevAttr::objectId().in(oidsToSkip) &&
        rf::ObjRevAttr::isNotRelation() &&
        rf::ObjRevAttr::isNotDeleted() &&
        !rf::Geom::defined();

    DBID2PolygonVector result;
    INFO() << "Countries geometry loading started";
    auto countryRevs = snapshot.objectRevisionsByFilter(countryRevFilter);
    INFO() << "Countries geometry loading completed";
    for (const auto& countryRev : countryRevs) {
        auto countryOid = countryRev.id().objectId();
        INFO() << "Processing country: oid=" << countryOid;

        auto countryPolygons = buildContourGeom(snapshot, countryOid, CAT_AD);
        for (const auto& countryPolygon : countryPolygons) {
            if (geomFilter(countryPolygon)) {
                INFO() << "Geometry filter passed: oid=" << countryOid;
                auto& polygonGroup = result[countryOid];
                polygonGroup.push_back(std::move(countryPolygon));
            }
        }
    }
    INFO() << "Countries geometry processing completed";
    return result;
}

RegionElements getRegionElements(rev::Snapshot& snapshot, rev::DBID regionId)
{
    auto relRevs = snapshot.loadSlaveRelations(regionId);

    RegionElements result;
    common::applyBatchOp<rev::Revisions>(
        relRevs,
        OBJECTS_BATCH_COUNT,
        [&](const rev::Revisions& batch) {
            std::vector<DBID> oids;
            for (const auto& relRev : batch) {
                oids.push_back(relRev.data().relationData->slaveObjectId());
            }

            auto revFilter =
                rf::ObjRevAttr::objectId().in(oids) &&
                rf::ObjRevAttr::isNotRelation() &&
                rf::ObjRevAttr::isNotDeleted() &&
                !rf::Geom::defined();

            for (const auto& rev : snapshot.objectRevisionsByFilter(revFilter)) {
                auto catId = extractCategoryId(*rev.data().attributes);
                auto oid = rev.id().objectId();
                if (catId == CAT_AD) {
                    result.adIds.push_back(oid);
                } else if (catId == CAT_AD_NEUTRAL) {
                    result.adNeutralIds.push_back(oid);
                }
            }
        });
    return result;
}

geolib3::Polygon2 bufferedPolygon(const geolib3::Polygon2& geom, double buffer)
{
    if (buffer < MIN_DISTANCE_TO_BUFFER) {
        return geom;
    }
    auto geosGeomPtr = geolib3::internal::geolib2geosGeometry(geom);
    return geolib3::Polygon2(
        ::maps::common::dynamic_unique_cast<geos::geom::Polygon>(
            geosGeomPtr->buffer(buffer, BUFFER_QUADRANT_SEGMENTS,
                geos::operation::buffer::BufferOp::CAP_SQUARE)),
        geolib3::Validate::Yes);
}

std::vector<geolib3::BoundingBox> makeDividedBoundingBoxes(
    const std::vector<const DBID2PolygonVector*>& polygonGroups)
{
    std::vector<geolib3::BoundingBox> bboxes;
    for (const auto* polygonGroupMap : polygonGroups) {
        for (const auto& [_, polygons] : *polygonGroupMap) {
            for (const auto& polygon : polygons) {
                bboxes.push_back(polygon.boundingBox());
            }
        }
    }

    return unionIntersectedBoundingBoxes(std::move(bboxes));
}

} // namespace

AreaOfInterest::AreaOfInterest()
    : buffer_(0.0)
    , boundingBox_(boost::none)
{
}

AreaOfInterest::~AreaOfInterest()
{
    fs::remove_all(coverageDir_);
}

void
AreaOfInterest::setBuffer(double buffer)
{
    REQUIRE(buffer >= 0.0 && buffer < MAX_DISTANCE_TO_BUFFER,
        "Invalid buffer distance: " << buffer);
    buffer_ = buffer;
}

void
AreaOfInterest::addByIds(
    pgpool3::Pool& pgPool,
    DBID branchId,
    DBID commitId,
    const std::vector<DBID>& oids)
{
    DBGateway gateway(pgPool, branchId, commitId);
    auto txn = gateway.getTransaction();
    auto snapshot = gateway.snapshot(*txn);

    auto revs = snapshot.objectRevisions(oids);
    for (const auto& pair : revs) {
        auto oid = pair.first;
        const auto& rev = pair.second;

        auto catId = extractCategoryId(*rev.data().attributes);
        INFO() << "Area of interest category id: " << catId;

        if (catId == CAT_REGION) {
            addRegionById(snapshot, oid);
            continue;
        }
        if (!rev.data().geometry) {
            REQUIRE(CONTOUR_CAT_IDS.count(catId),
                "Must be contour object: oid=" << oid);
            addContourObjectById(snapshot, oid, catId, Importance::Primary);
            continue;
        }

        try {
            includedPrimaryPolygonGroups_[rev.id().objectId()].emplace_back(
                geolib3::WKB::read<geolib3::Polygon2>(*rev.data().geometry));
        } catch (const RuntimeError& e) {
            throw RuntimeError() << "Must have polygon geometry: oid=" << oid;
        }
    }
}

void
AreaOfInterest::addContourObjectById(
    rev::Snapshot& snapshot,
    rev::DBID contourId,
    const std::string& catId,
    Importance importance)
{
    auto contourPolygons = buildContourGeom(snapshot, contourId, catId);
    auto& polygonGroup = importance == Importance::Primary
        ? includedPrimaryPolygonGroups_[contourId]
        : includedSecondaryPolygonGroups_[contourId];
    polygonGroup.insert(
        std::end(polygonGroup),
        std::make_move_iterator(contourPolygons.begin()),
        std::make_move_iterator(contourPolygons.end()));
}

void
AreaOfInterest::addRegionById(
    rev::Snapshot& snapshot,
    rev::DBID regionId)
{
    auto elements = getRegionElements(snapshot, regionId);
    for (auto adId : elements.adIds) {
        INFO() << "Region oid=" << regionId << " contains ad oid=" << adId;
        addContourObjectById(snapshot, adId, CAT_AD, Importance::Primary);
    }
    for (auto adNeutralId : elements.adNeutralIds) {
        INFO() << "Region oid=" << regionId << " contains ad_neutral oid=" << adNeutralId;
        addContourObjectById(snapshot, adNeutralId, CAT_AD_NEUTRAL, Importance::Secondary);
    }

    auto geomFilterByPolygonGroups = [](
        const geolib3::Polygon2& polygon,
        const DBID2PolygonVector& polygonGroups) {
            const auto& bbox = polygon.boundingBox();
            for (const auto& pair : polygonGroups) {
                const auto& polygons = pair.second;
                for (const auto& polygonInGroup : polygons) {
                    if (geolib3::intersects(bbox, polygonInGroup.boundingBox())) {
                        return true;
                    }
                }
            }
            return false;
        };

    auto geomFilterByRegion = [&, this](const geolib3::Polygon2& polygon) {
        return geomFilterByPolygonGroups(polygon, includedPrimaryPolygonGroups_)
            || geomFilterByPolygonGroups(polygon, includedSecondaryPolygonGroups_);
    };

    excludedPolygonGroups_ = buildExcludedCountriesGeom(
        snapshot, elements.adIds, geomFilterByRegion);
    INFO() << "Excluded countries count: " << excludedPolygonGroups_.size();
}

void
AreaOfInterest::build()
{
    auto calcBufferedPolygons = [](const DBID2PolygonVector& polygonGroups, double buffer) {
        DBID2PolygonVector result;
        for (const auto& [groupId, polygons] : polygonGroups) {
            for (const auto& polygon : polygons) {
                result[groupId].push_back(bufferedPolygon(polygon, buffer));
            }
        }
        return result;
    };

    auto extendBoundingBox = [](
        const geolib3::BoundingBox& boundingBox,
        const DBID2PolygonVector& polygonGroups) {
            auto result = boundingBox;
            for (const auto& [_, polygons] : polygonGroups) {
                for (const auto& polygon : polygons) {
                    result = geolib3::expand(result, polygon);
                }
            }
            return result;
    };

    includedPrimaryBufferedPolygonGroups_ = calcBufferedPolygons(includedPrimaryPolygonGroups_, buffer_);
    excludedBufferedPolygonGroups_ = calcBufferedPolygons(excludedPolygonGroups_, -buffer_);
    includedSecondaryBufferedPolygonGroups_ = calcBufferedPolygons(includedSecondaryPolygonGroups_, buffer_);

    auto bboxString = [] (const geolib3::BoundingBox& bbox) {
        return
            std::to_string(bbox.minX()) + " " + std::to_string(bbox.minY()) + " : " +
            std::to_string(bbox.maxX()) + " " + std::to_string(bbox.maxY());
    };

    auto commonBoundingBox = [&](
        const DBID2PolygonVector& polygonGroups1, const DBID2PolygonVector& polygonGroups2)
    {
        auto boundingBox = polygonGroups1.begin()->second.front().boundingBox();
        boundingBox = extendBoundingBox(boundingBox, polygonGroups1);
        boundingBox = extendBoundingBox(boundingBox, polygonGroups2);
        return boundingBox;
    };

    if (!includedPrimaryPolygonGroups_.empty()) {
        boundingBox_ = commonBoundingBox(
            includedPrimaryPolygonGroups_, includedSecondaryPolygonGroups_);
        INFO() << "BoundingBox: " << bboxString(*boundingBox_);
    }
    if (!includedPrimaryBufferedPolygonGroups_.empty()) {
        auto bufferedBoundingBox = commonBoundingBox(
            includedPrimaryBufferedPolygonGroups_,
            includedSecondaryBufferedPolygonGroups_);
        INFO() << "BufferedBoundingBox: " << bboxString(bufferedBoundingBox);

        bufferedBoundingBoxes_ = makeDividedBoundingBoxes(
            {&includedPrimaryBufferedPolygonGroups_,
             &includedSecondaryBufferedPolygonGroups_});
        for (const auto& bbox : bufferedBoundingBoxes_) {
            INFO() << "BufferedBoundingBox part: " << bboxString(bbox);
        }
    }

    fs::create_directories(fs::path(coverageDir_));
    buildLayer(LAYER_INCLUDED_PRIMARY_ORIGINAL, includedPrimaryPolygonGroups_);
    buildLayer(LAYER_INCLUDED_PRIMARY_BUFFERED, includedPrimaryBufferedPolygonGroups_);
    buildLayer(LAYER_EXCLUDED_ORIGINAL, excludedPolygonGroups_);
    buildLayer(LAYER_EXCLUDED_BUFFERED, excludedBufferedPolygonGroups_);
    buildLayer(LAYER_INCLUDED_SECONDARY_ORIGINAL, includedSecondaryPolygonGroups_);
    buildLayer(LAYER_INCLUDED_SECONDARY_BUFFERED, includedSecondaryBufferedPolygonGroups_);

    coverage_ = std::make_unique<coverage5::Coverage>(coverageDir_,
        coverage5::SpatialRefSystem::Mercator);
}

void
AreaOfInterest::buildLayer(
    const std::string& layerName,
    const DBID2PolygonVector& polygonGroups) const
{
    fs::path fileName(coverageDir_);
    fileName /= layerName + ".mms." + COVERAGE_LAYER_VERSION;
    INFO() << "Building coverage file: " << fileName.string();

    auto builder = coverage5::dataLayerBuilder(
        fileName.string(),
        layerName,
        COVERAGE_LAYER_VERSION,
        boost::none,
        coverage5::DEFAULT_MIN_BOX_SIZE,
        coverage5::DEFAULT_MAX_VERTICES,
        geolib3::EPS,
        false,
        coverage5::SpatialRefSystem::Mercator);

    for (const auto& pair : polygonGroups) {
        const auto& groupId = pair.first;
        const auto& polygons = pair.second;

        INFO() << "Layer '" << layerName << "' : adding polygon group " << groupId;
        builder->addRegion(boost::none, boost::none, boost::none,
            std::to_string(groupId), {}, geolib3::MultiPolygon2(polygons));
    }

    builder->build();
    INFO() << "Coverage file: " << fileName.string() << " builded";
}

} // namespace validator
} // namespace wiki
} // namespace maps
