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

#include <maps/libs/common/include/exception.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/point.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/geolib/include/spatial_relation.h>

#include <maps/libs/json/include/value.h>

#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/extended_xml_doc.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pgpool3_helpers.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/object/include/revision_loader.h>
#include <maps/wikimap/mapspro/services/mrc/libs/config/include/config.h>

#include <opencv2/opencv.hpp>

#include <codecvt>

namespace {
constexpr double SEARCH_RADIUS_METERS = 50.;

maps::geolib3::BoundingBox calculateBbox(const maps::mrc::object::Buildings& buildings) {
    REQUIRE(0 < buildings.size(), "Unable to calculate bbox, there are no buildings");

    maps::geolib3::BoundingBox bbox = buildings[0].geom().boundingBox();
    for (size_t i = 1; i < buildings.size(); i++) {
        const maps::mrc::object::Building& building = buildings[i];
        bbox = maps::geolib3::expand(bbox, building.geom());
    }
    return bbox;
}

maps::geolib3::BoundingBox calculateMercatorSearchBox(double lon, double lat) {
    const maps::geolib3::Point2 pt = maps::geolib3::convertGeodeticToMercator({lon, lat});
    const double mercatorRadius = maps::geolib3::toMercatorUnits(SEARCH_RADIUS_METERS, pt);
    return maps::geolib3::BoundingBox(pt, 2. * mercatorRadius, 2. * mercatorRadius);
}

maps::mrc::object::Buildings loadBuildings(maps::mrc::object::Loader& loader, double lon, double lat) {
    return loader.loadBuildings(calculateMercatorSearchBox(lon, lat));
}

void convertToOutputFormat(
    const maps::json::Value& inputJson,
    const std::string& outputJsonPath,
    maps::wiki::revision::DBID commitId
) {

    std::ofstream ofs(outputJsonPath);
    if (!ofs.is_open())
        ERROR() << "Unable to open output file: " << outputJsonPath;
    maps::json::Builder builder(ofs);
    builder << [&](maps::json::ObjectBuilder builder) {
        builder["commit_id"] << commitId;
        builder["hypotheses"] << [&](maps::json::ArrayBuilder builder) {
            int processedItems = 0;
            for(const maps::json::Value& hyp : inputJson["hypotheses"]) {
                if ((processedItems + 1) % 100 == 0) {
                    INFO() << "Processed " << (processedItems + 1) << " items";
                }
                processedItems++;

                builder << [&](maps::json::ObjectBuilder builder) {
                    builder["feature_id"] << hyp["feature_id"];
                    builder["image"] << hyp["image"];
                    builder["lat"] << hyp["lat"];
                    builder["lon"] << hyp["lon"];
                    builder["objects"] << [&](maps::json::ArrayBuilder builder) {
                        for(const maps::json::Value& object : hyp["objects"]) {
                            builder << [&](maps::json::ObjectBuilder builder) {
                                builder["bbox"] << object["bbox"];
                                builder["visible_number"] << object["visible_number"];
                                //TODO
                            };
                        };
                    };
                };
            };
        };
    };
}

void dumpMapsData(
    const maps::json::Value& inputJson,
    const std::string& outputJsonMapPath,
    maps::mrc::object::Loader& loader,
    maps::wiki::revision::DBID commitId
) {
    std::ofstream ofs(outputJsonMapPath);
    if (!ofs.is_open())
        ERROR() << "Unable to open output file: " << outputJsonMapPath;
    maps::json::Builder builderMap(ofs);
    builderMap << [&](maps::json::ObjectBuilder builder) {
        builder["commit_id"] << commitId;
        builder["data"] << [&](maps::json::ArrayBuilder builder) {
            int processedItems = 0;
            for(const maps::json::Value& hyp : inputJson["hypotheses"]) {
                if ((processedItems + 1) % 100 == 0) {
                    INFO() << "Processed " << (processedItems + 1) << " items";
                }
                processedItems++;

                const double lon = hyp["lon"].as<double>();
                const double lat = hyp["lat"].as<double>();
                const maps::mrc::object::Buildings buildings = loadBuildings(loader, lon, lat);
                const maps::geolib3::BoundingBox bbox = (0 < buildings.size()) ? calculateBbox(buildings) : calculateMercatorSearchBox(lon, lat);

                maps::mrc::object::AddressPointWithNames addrPointWithNames = loader.loadAddressPointWithNames(bbox);
                const maps::geolib3::Point2 featurePt = maps::geolib3::convertGeodeticToMercator({lon, lat});
                std::sort(addrPointWithNames.begin(), addrPointWithNames.end(),
                    [&](const maps::mrc::object::AddressPointWithName& l, const maps::mrc::object::AddressPointWithName& r) {
                        const maps::geolib3::Point2& lpt = l.geom();
                        const maps::geolib3::Point2& rpt = r.geom();
                        return ((lpt.x() - featurePt.x()) * (lpt.x() - featurePt.x()) + (lpt.y() - featurePt.y()) * (lpt.y() - featurePt.y())) <
                               ((rpt.x() - featurePt.x()) * (rpt.x() - featurePt.x()) + (rpt.y() - featurePt.y()) * (rpt.y() - featurePt.y()));
                    }
                );

                builder << [&](maps::json::ObjectBuilder builder) {
                    builder["feature_id"] << hyp["feature_id"];
                    builder["lat"] << hyp["lat"];
                    builder["lon"] << hyp["lon"];
                    builder["buildings"] << [&](maps::json::ArrayBuilder builder) {
                        for (size_t i = 0; i < buildings.size(); i++) {
                            const maps::mrc::object::Building& building = buildings[i];
                            maps::geolib3::Polygon2 pgn2 = maps::geolib3::convertMercatorToGeodetic(building.geom());
                            maps::geolib3::LinearRing2 lr2 = pgn2.exteriorRing();
                            if (0 == lr2.pointsNumber())
                                continue;
                            builder << [&](maps::json::ObjectBuilder builder) {
                                builder["polygon"] << [&](maps::json::ArrayBuilder builder) {
                                    for (size_t j = 0; j < lr2.pointsNumber(); j++) {
                                        const maps::geolib3::Point2& pt = lr2.pointAt(j);
                                        builder << [&](maps::json::ArrayBuilder builder) {
                                            builder << pt.x();
                                            builder << pt.y();
                                        };
                                    };
                                };
                            };
                        };
                    };
                    builder["addr_pts"] << [&](maps::json::ArrayBuilder builder) {
                        for (size_t i = 0; i < addrPointWithNames.size(); i++) {
                            const maps::mrc::object::AddressPointWithName& addrPointWithName = addrPointWithNames[i];
                            maps::geolib3::Point2 pt = maps::geolib3::convertMercatorToGeodetic(addrPointWithName.geom());
                            builder << [&](maps::json::ObjectBuilder builder) {
                                builder["lat"] << pt.y();
                                builder["lon"] << pt.x();
                                builder["name"] << addrPointWithName.name();
                            };
                        };
                    };
                };
            };
        };
    };


}

} //namespace

int main(int argc, const char** argv) try {
    maps::cmdline::Parser parser("Prepare address points information from json");

    maps::cmdline::Option<std::string> inputJsonPath = parser.string("input")
        .required()
        .help("Path to input json");

    maps::cmdline::Option<std::string> outputJsonPath = parser.string("output")
        .help("Path to output json");

    maps::cmdline::Option<std::string> outputJsonMapPath = parser.string("maps-output")
        .help("Path to output json for buildings and addresses points");

    maps::cmdline::Option<std::string> wikiConfigPath = parser.string("wiki-config")
        .required()
        .help("Path to services config for wikimap");

    maps::cmdline::Option<int> headCommitID = parser.num("commit-id")
        .help("Snapshot commit ID ");

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

    maps::wiki::common::ExtendedXmlDoc wikiConfig(wikiConfigPath);
    maps::wiki::common::PoolHolder wiki(wikiConfig, "core", "tasks");

    auto txn = wiki.pool().slaveTransaction();
    maps::wiki::revision::RevisionsGateway gateway(*txn);

    const maps::wiki::revision::DBID commitId =
        headCommitID.defined() ? headCommitID : gateway.headCommitId();
    INFO() << "Snapshot commit id " << commitId;

    maps::wiki::revision::Snapshot snapshot = gateway.snapshot(commitId);
    maps::mrc::object::LoaderHolder loader = maps::mrc::object::makeRevisionLoader(snapshot);

    maps::json::Value fileJson = maps::json::Value::fromFile(inputJsonPath);
    if (outputJsonPath.defined())
        convertToOutputFormat(fileJson, outputJsonPath, commitId);
    if (outputJsonMapPath.defined())
        dumpMapsData(fileJson, outputJsonMapPath, *loader, commitId);
    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    FATAL() << "Worker failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
