#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/hex_wkb.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>

#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/sql_chemistry/include/batch_load.h>

#include <maps/wikimap/mapspro/services/mrc/libs/config/include/config.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>

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

#include <mapreduce/yt/interface/client.h>
#include <mapreduce/yt/util/ypath_join.h>
#include <util/charset/utf8.h>
#include <util/charset/wide.h>

#include <opencv2/opencv.hpp>

#include <string>
#include <fstream>
#include <iostream>

using namespace NYT;

namespace {

std::vector<std::string>
extractRegionCodes(const std::string& regionCodesParam)
{
    std::vector<std::string> regionCodes;
    std::stringstream ss(regionCodesParam);
    std::string code;
    while (std::getline(ss, code, ',')) {
        regionCodes.push_back(code);
    }
    return regionCodes;
}

std::set<wchar16>
extractSymbols(const std::string& symbolsParam)
{
    std::set<wchar16> symbols;
    std::stringstream ss(symbolsParam);
    std::string code;
    while (std::getline(ss, code, ',')) {
        symbols.insert((wchar16)std::stoi(code));
    }
    return symbols;
}

std::set<maps::mrc::db::TId> extractAddrIds(
    IClientPtr& client,
    const TString& ytFolderPath,
    const std::set<wchar16>& symbols
) {
    static const TString YT_TABLE_ADDR_NAME = "addr_nm";
    static const TString COL_NAME = "name";
    static const TString COL_ADDR_ID = "addr_id";

    INFO() << "Extract address ids";
    std::set<maps::mrc::db::TId> ids;
    TTableReaderPtr<TNode> reader = client->CreateTableReader<TNode>(JoinYPaths(ytFolderPath, YT_TABLE_ADDR_NAME));
    for (int processedItems = 0; reader->IsValid(); reader->Next(), processedItems++) {
        if ((processedItems + 1) % 1000000 == 0) {
            INFO() << "Processed " << (processedItems + 1) << " items";
        }
        const TNode& inpRow = reader->GetRow();
        if (0 == symbols.size()) {
            ids.insert(inpRow[COL_ADDR_ID].AsInt64());
        } else {
            const TUtf16String name = UTF8ToWide(inpRow[COL_NAME].AsString());
            for (size_t i = 0; i < name.size(); i++) {
                if (symbols.count(name[i]) > 0) {
                    ids.insert(inpRow[COL_ADDR_ID].AsInt64());
                    break;
                }
            }
        }
    };
    INFO() << "Extracted " << ids.size() << " address ids";
    return ids;
}

std::set<maps::mrc::db::TId> extractBldIds(
    IClientPtr& client,
    const TString& ytFolderPath,
    const std::set<maps::mrc::db::TId>& addrIds
) {
    static const TString YT_TABLE_BLD_ADDR = "bld_addr";
    static const TString COL_ADDR_ID = "addr_id";
    static const TString COL_BLD_ID  = "bld_id";

    INFO() << "Extract building ids";
    std::set<maps::mrc::db::TId> ids;
    TTableReaderPtr<TNode> reader = client->CreateTableReader<TNode>(JoinYPaths(ytFolderPath, YT_TABLE_BLD_ADDR));
    for (int processedItems = 0; reader->IsValid(); reader->Next(), processedItems++) {
        if ((processedItems + 1) % 1000000 == 0) {
            INFO() << "Processed " << (processedItems + 1) << " items";
        }
        const TNode& inpRow = reader->GetRow();
        if (addrIds.count(inpRow[COL_ADDR_ID].AsInt64()) > 0) {
            ids.insert(inpRow[COL_BLD_ID].AsInt64());
        }
    };
    INFO() << "Extracted " << ids.size() << " building ids";
    return ids;
}

std::vector<maps::geolib3::Polygon2> extractBldPolygons(
    IClientPtr& client,
    const TString& ytFolderPath,
    const std::set<maps::mrc::db::TId>& bldIds
) {
    static const TString YT_TABLE_BLD_GEOM = "bld_geom";
    static const TString COL_BLD_ID  = "bld_id";
    static const TString COL_SHAPE = "shape";

    INFO() << "Extract building shapes";
    std::vector<maps::geolib3::Polygon2> bldPolygons;
    TTableReaderPtr<TNode> reader = client->CreateTableReader<TNode>(JoinYPaths(ytFolderPath, YT_TABLE_BLD_GEOM));
    for (int processedItems = 0; reader->IsValid(); reader->Next(), processedItems++) {
        if ((processedItems + 1) % 1000000 == 0) {
            INFO() << "Processed " << (processedItems + 1) << " items";
        }
        const TNode& inpRow = reader->GetRow();
        if (bldIds.empty() || bldIds.count(inpRow[COL_BLD_ID].AsInt64()) > 0) {
            bldPolygons.emplace_back(
                convertGeodeticToMercator(
                    maps::wiki::autocart::hexWKBToPolygon(inpRow[COL_SHAPE].AsString())
                )
            );
        }
    };
    INFO() << "Extracted " << bldPolygons.size() << " building shapes";
    return bldPolygons;
}

std::vector<maps::geolib3::Polygon2> extractBldPolygonsInRegion(
    IClientPtr& client,
    const std::string& regionCode,
    const std::set<wchar16>& symbols
) {
    static const TString YT_YMAPSDF_FOLDER_PATH = "//home/maps/core/garden/stable/ymapsdf/latest/";

    INFO() << "Rework region code: " << regionCode;
    const TString ytRegionFolder = YT_YMAPSDF_FOLDER_PATH + regionCode +  "/";
    if (symbols.empty()) {
        return extractBldPolygons(client, ytRegionFolder, {});
    }

    std::set<maps::mrc::db::TId> addrIds =
        extractAddrIds(
            client,
            ytRegionFolder,
            symbols);
    std::set<maps::mrc::db::TId> bldIds =
        extractBldIds(
            client,
            ytRegionFolder,
            addrIds);
    return extractBldPolygons(client, ytRegionFolder, bldIds);
}

struct BuildingCluster {
    std::vector<maps::geolib3::Polygon2> buildings; // in mercator coordinates
    maps::geolib3::BoundingBox bbox; // in mercator coordinates
};

struct BBoxesNeighbors {
    bool operator()(const maps::geolib3::BoundingBox& a, const maps::geolib3::BoundingBox& b) {
        return maps::geolib3::spatialRelation(a, b, maps::geolib3::SpatialRelation::Intersects);
    }
};

std::vector<BuildingCluster> bldPolygonsClusterize(
    const std::vector<maps::geolib3::Polygon2>& bldPolygons
) {
    INFO() << "bldPolygonsClusterize polygons: " << bldPolygons.size();
    constexpr double CLUSTER_TOLERANCE_METERS = 50.;
    std::vector<maps::geolib3::BoundingBox> bboxes(bldPolygons.size());
    for (size_t i = 0; i < bldPolygons.size(); i++) {
        maps::geolib3::BoundingBox bbox = bldPolygons[i].boundingBox();
        bboxes[i] = resizeByValue(bbox, toMercatorUnits(CLUSTER_TOLERANCE_METERS, bbox.center()));
    }

    std::vector<int> labels;
    int lblsCnt = cv::partition(bboxes, labels, BBoxesNeighbors());
    INFO() << "Extract " << lblsCnt << " clusters";
    std::vector<BuildingCluster> clusters(lblsCnt);
    for (size_t i = 0; i < bldPolygons.size(); i++) {
        if (clusters[labels[i]].buildings.empty()) {
            clusters[labels[i]].bbox = bboxes[i];
        } else {
            clusters[labels[i]].bbox = maps::geolib3::expand(clusters[labels[i]].bbox, bboxes[i]);
        }
        clusters[labels[i]].buildings.push_back(bldPolygons[i]);
    }
    return clusters;
}

maps::mrc::db::Features extractFeatures(
    maps::pgpool3::Pool& pool,
    const BuildingCluster& cluster,
    int searchRadiusMeters,
    const maps::chrono::TimePoint& featureDate
) {
    maps::geolib3::StaticGeometrySearcher<maps::geolib3::Polygon2, int> searcher;
    for (size_t i = 0; i < cluster.buildings.size(); i++) {
        searcher.insert(&cluster.buildings[i], -1);
    }
    searcher.build();

    auto txn = pool.slaveTransaction();
    maps::mrc::db::Features features =
        maps::mrc::db::FeatureGateway(*txn).load(
            maps::mrc::db::table::Feature::isPublished &&
            maps::mrc::db::table::Feature::privacy == maps::mrc::db::FeaturePrivacy::Public &&
            maps::mrc::db::table::Feature::pos.intersects(cluster.bbox) &&
            (maps::mrc::db::table::Feature::date >= featureDate)
        );

    features.erase(
        std::remove_if(features.begin(), features.end(),
            [&](const maps::mrc::db::Feature& feature) {
                const maps::geolib3::Point2& pos = feature.mercatorPos();
                const double radius = toMercatorUnits(searchRadiusMeters, pos);
                const auto buildings =
                    searcher.find(
                        resizeByValue(
                            pos.boundingBox(),
                            radius
                        )
                    );
                for (auto itr = buildings.first; itr != buildings.second; itr++) {
                    if (maps::geolib3::distance(pos, itr->geometry()) < radius)
                        return false;
                }
                return true;
            }
        ),
        features.end()
    );
    return features;
}

maps::mrc::db::Features extractFeatures(
    maps::pgpool3::Pool& pool,
    const std::vector<maps::geolib3::Polygon2>& buildings,
    int searchRadiusMeters,
    const maps::chrono::TimePoint& featureDate
) {
    constexpr size_t batchSize = 50000;
    maps::geolib3::StaticGeometrySearcher<maps::geolib3::Polygon2, int> searcher;
    for (size_t i = 0; i < buildings.size(); i++) {
        searcher.insert(&buildings[i], -1);
    }
    searcher.build();

    INFO() << "Extract features";
    maps::mrc::db::Features features;
    auto txn = pool.slaveTransaction();
    maps::sql_chemistry::BatchLoad<maps::mrc::db::table::Feature>
        batch{
            batchSize,
            maps::mrc::db::table::Feature::isPublished &&
            maps::mrc::db::table::Feature::privacy == maps::mrc::db::FeaturePrivacy::Public &&
            (maps::mrc::db::table::Feature::date >= featureDate)};
    int processedItems = 0;
    while (batch.next(*txn)) {
        for (auto feature = batch.begin(); feature != batch.end(); feature++, processedItems++) {
            const maps::geolib3::Point2& pos = feature->mercatorPos();
            const double radius = toMercatorUnits(searchRadiusMeters, pos);
            const auto buildings =
                searcher.find(
                    resizeByValue(
                        pos.boundingBox(),
                        radius
                    )
                );
            for (auto itr = buildings.first; itr != buildings.second; itr++) {
                if (maps::geolib3::distance(pos, itr->geometry()) < radius) {
                    features.push_back(*feature);
                    break;
                }
            }
        }
        INFO() << "Processed " << processedItems << " items. Features collected: " << features.size();
    }
    return features;
}

void saveFeatures(
    std::ostream &outStream,
    const maps::mrc::db::Features& features,
    bool geodeticCoordinates
) {
    for (const maps::mrc::db::Feature& feature : features) {
        const maps::geolib3::Point2 pos =
            (geodeticCoordinates) ? maps::geolib3::mercator2GeoPoint(feature.mercatorPos()) : feature.mercatorPos();
        outStream << feature.id() << " "
                  << "http://storage-int.mds.yandex.net/get-maps_mrc/"
                  << feature.mdsGroupId() << "/"
                  << feature.mdsPath() << " "
                  << (int)feature.orientation() << " "
                  << pos.x() << " " << pos.y() << "\n";
    }
}

} // namespace

int main(int argc, const char** argv) try {
    static const TString YT_PROXY = "hahn";

    Initialize(argc, argv);
    maps::cmdline::Parser parser("Extract coordinate of address points, with name contains symbols from cmd");

    maps::cmdline::Option<std::string> regionCodesParam = parser.string("regions")
        .defaultValue("eu1")
        .help("Regions codes divided by commas (default: eu1)");

    maps::cmdline::Option<std::string> symbolsCodeParam = parser.string("symbols")
        .help("Symbols unicodes divided by commas");

    maps::cmdline::Option<int> searchRadiusParam = parser.num("radius")
        .defaultValue(20)
        .help("Maximal distance (in meters) to nearest building from photo");

    maps::cmdline::Option<std::string> featureDateParam = parser.string("feature-date-after")
        .defaultValue("2000-01-01 00:00:00.000000+03")
        .help("Extract feature with date after this parameter only");

    maps::cmdline::Option<bool> asGeodeticParam = parser.flag("geodetic-coord")
        .help("Save coordinates as geodetic");

    maps::cmdline::Option<std::string> mrcConfigPath = parser.string("mrc-config")
        .help("Path to mrc config");

    maps::cmdline::Option<std::string> secretVersion = parser.string("secret-version")
        .help("version for secrets from yav.yandex-team.ru");

    maps::cmdline::Option<std::string> outputFeaturesPath = parser.string("output")
        .help("Path to file for saving features description");

    parser.parse(argc, const_cast<char**>(argv));

    std::vector<std::string> regionCodes = extractRegionCodes(regionCodesParam);
    maps::chrono::TimePoint featureDate = maps::chrono::parseSqlDateTime(featureDateParam);

    const maps::mrc::common::Config mrcConfig =
        maps::mrc::common::templateConfigFromCmdPath(secretVersion, mrcConfigPath);

    maps::wiki::common::PoolHolder mrc(mrcConfig.makePoolHolder());
    maps::pgpool3::Pool& pool = mrc.pool();

    INFO() << "Connecting to yt::" << YT_PROXY;
    IClientPtr client = CreateClient(YT_PROXY);
    std::ofstream ofs(outputFeaturesPath);
    if (symbolsCodeParam.defined()) {
        std::set<wchar16> symbols = extractSymbols(symbolsCodeParam);
        for (const std::string& code : regionCodes) {
            std::vector<BuildingCluster> bldClusters = bldPolygonsClusterize(extractBldPolygonsInRegion(client, code, symbols));
            INFO() << "Clusters: " << bldClusters.size();
            for (const BuildingCluster& cluster : bldClusters) {
                maps::mrc::db::Features features = extractFeatures(pool, cluster, searchRadiusParam, featureDate);
                saveFeatures(ofs, features, asGeodeticParam);
            }
        }
    } else {
        std::vector<maps::geolib3::Polygon2> bldPolygons;
        for (const std::string& code : regionCodes) {
            std::vector<maps::geolib3::Polygon2> temp = extractBldPolygonsInRegion(client, code, {});
            INFO() << "Region " << code << " polygons: " << temp.size();
            bldPolygons.insert(bldPolygons.end(), temp.begin(), temp.end());
        }
        maps::mrc::db::Features features = extractFeatures(pool, bldPolygons, searchRadiusParam, featureDate);
        saveFeatures(ofs, features, asGeodeticParam);
    }
    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    FATAL() << "Tools failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Tools failed: " << e.what();
    return EXIT_FAILURE;
}
