#include "maps_reader.h"
#include "ymapsdf_reader.h"
#include "mapspro_reader.h"

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/conversion_geos.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/infra/yacare/include/parse.h>
#include <maps/libs/tile/include/utils.h>
#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>

#include <maps/wikimap/mapspro/services/autocart/libs/satellite/include/load_sat_image.h>
#include <maps/wikimap/mapspro/services/autocart/libs/satellite/include/load_sat_params.h>
#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/polygon_processing.h>

#include <geos/geom/Geometry.h>
#include <geos/geom/Polygon.h>
#include <geos/geom/LineString.h>

#include <opencv2/opencv.hpp>

#include <pqxx/pqxx>

#include <algorithm>
#include <fstream>
#include <string>
#include <vector>
#include <thread>
#include <chrono>
#include <list>
#include <memory>

namespace geolib3 = maps::geolib3;
namespace tile = maps::tile;
namespace semgen = maps::mrc::semgen;
namespace common = maps::wiki::common;
namespace autocart = maps::wiki::autocart;


namespace {

using BufferedRoad = geolib3::Polygon2;

const double ROAD_BUFFER_WIDTH_PIXELS = 4.;

template<class Object>
std::list<Object> convertToDisplay(const std::list<Object>& objects,
                                   size_t zoom,
                                   const geolib3::BoundingBox& mercBbox) {
    std::list<Object> displayObjects;

    auto leftBottom = tile::mercatorToDisplaySigned(mercBbox.lowerCorner(), zoom);
    auto rightTop = tile::mercatorToDisplaySigned(mercBbox.upperCorner(), zoom);
    tile::SignedDisplayCoord leftTop(leftBottom.x(), rightTop.y());

    for (const auto& object : objects) {
        std::vector<geolib3::Point2> displayPoints;
        for (size_t i = 0; i < object.pointsNumber(); i++) {
            auto point = tile::mercatorToDisplaySigned(object.pointAt(i), zoom);
            displayPoints.emplace_back(point.x() - leftTop.x(),
                                       point.y() - leftTop.y());
        }
        displayObjects.emplace_back(displayPoints);
    }
    return displayObjects;
}

std::list<semgen::Building>
convertToDisplay(const std::list<semgen::Building>& blds,
                 size_t zoom,
                 const geolib3::BoundingBox& mercBbox) {
    std::list<semgen::Building> displayBlds;

    auto leftBottom = tile::mercatorToDisplaySigned(mercBbox.lowerCorner(), zoom);
    auto rightTop = tile::mercatorToDisplaySigned(mercBbox.upperCorner(), zoom);
    tile::SignedDisplayCoord leftTop(leftBottom.x(), rightTop.y());
    for (const auto& bld : blds) {
        std::vector<geolib3::Point2> displayPoints;
        for (size_t i = 0; i < bld.pointsNumber(); i++) {
            auto point = tile::mercatorToDisplaySigned(bld.pointAt(i), zoom);
            displayPoints.emplace_back(point.x() - leftTop.x(),
                                       point.y() - leftTop.y());
        }
        displayBlds.emplace_back(geolib3::Polygon2(displayPoints), bld.height);
    }
    return displayBlds;
}

BufferedRoad buffer(const semgen::Road& road, double width)
{
    auto geosRoad = geolib3::internal::geolib2geosGeometry(road);
    std::unique_ptr<geos::geom::Geometry> bufferedGeom(
                geosRoad->buffer(width)
                );

    return geolib3::internal::geos2geolibGeometry(
                dynamic_cast<geos::geom::Polygon*>(bufferedGeom.get())
                );
}

template<class Object>
void writeObjects(std::ostream& out,
                  const cv::Mat& image,
                  const std::string& objType,
                  const std::list<Object>& objects) {
    INFO() << "  Saving " << objects.size() << " " << objType << " objects";
    geolib3::BoundingBox imageBbox(geolib3::Point2(0., 0.),
                                   geolib3::Point2(image.cols, image.rows));
    geolib3::Polygon2 imageBboxPolygon = imageBbox.polygon();
    for (const auto& object : objects) {
        geolib3::MultiPolygon2 intersectedPolygons
            = autocart::intersectPolygons(object, imageBboxPolygon);

        for (size_t i = 0; i < intersectedPolygons.polygonsNumber(); i++) {
            const geolib3::Polygon2 polygon = intersectedPolygons.polygonAt(i);
            out << objType << " " << polygon.pointsNumber();
            for (size_t j = 0; j < polygon.pointsNumber(); j++) {
                geolib3::Point2 point = polygon.pointAt(j);
                out << " " << point.x() << " " << point.y();
            }
            out << "\n";
        }
    }
}

semgen::Building shiftBld(const semgen::Building& bld,
                          double xShift, double yShift) {
    std::vector<geolib3::Point2> shiftedPoints;
    shiftedPoints.reserve(bld.pointsNumber());
    for (size_t i = 0; i < bld.pointsNumber(); i++) {
        geolib3::Point2 pt = bld.pointAt(i);
        shiftedPoints.emplace_back(pt.x() + xShift, pt.y() + yShift);
    }
    return semgen::Building(geolib3::Polygon2(shiftedPoints));
}

std::list<semgen::Building>
shiftBuildingsBySatellite(const std::list<semgen::Building>& blds,
                          const autocart::SatelliteParams& params) {
    if (params.angles.has_value()) {
        return blds;
    }
    std::list<semgen::Building> shiftedBlds;
    geolib3::Vector2 southDirection(0., 1.);
    geolib3::Vector2 satDirection = geolib3::rotate(southDirection,
                                                    params.angles->azimAngle);
    double heightCoeff = 1. / tan(params.angles->elev);
    for (const auto& bld : blds) {
        if (bld.height.has_value()) {
            double shiftInMeters = *bld.height * heightCoeff;
            double xShiftInMeters = shiftInMeters * satDirection.x();
            double yShiftInMeters = shiftInMeters * satDirection.y();

            double xShiftInPixels = xShiftInMeters / params.pixelSize.widthInMeters;
            double yShiftInPixels = yShiftInMeters / params.pixelSize.heightInMeters;
            shiftedBlds.push_back(shiftBld(bld, xShiftInPixels, yShiftInPixels));
        } else {
            shiftedBlds.push_back(bld);
        }
    }
    return shiftedBlds;
}


void loadObjects(const semgen::MapsLoader& loader,
                 std::list<semgen::Building>* blds,
                 std::list<semgen::Road>* roads,
                 std::list<semgen::Vegetation>* vegetations,
                 std::list<semgen::Hydro>* hydros) {
    *blds = loader.loadBuildings();
    *roads = loader.loadRoads();
    *vegetations = loader.loadVegetation();
    *hydros = loader.loadHydro();
}

} // namespace

int main(int argc, char** argv)
try {
    maps::cmdline::Parser parser;
    auto bboxStr = parser.string("bbox")\
            .required()\
            .help("region bbox in format lt_x,lt_y~rb_x,rb_y in geodetic coordinates");

    auto tileSourceUrl = parser.string("tile-source")\
            .required()\
            .help("url prefix for tile source");

    auto isBldShiftEnable = parser.flag("shift_bld")\
        .defaultValue(false)\
        .help("shift polygons of buildings using information about satellite");

    auto connStr = parser.string("connstr")\
            .defaultValue("")
            .help("PostgreSQL connection string to YMapsDF schema");

    auto config = parser.string("config")\
            .defaultValue("")
            .help("path to wiki services.xml config file");

    auto outputFilePrefix = parser.string("output")\
            .defaultValue("output")\
            .help("prefix for result files");

    auto zoom = parser.num("zoom").required();

    parser.parse(argc, argv);

    REQUIRE(connStr.empty() || config.empty(), "Only one data source must be specified");

    auto geodeticBbox = yacare::Parser<geolib3::BoundingBox>()(bboxStr);

    auto mercatorBbox = geolib3::convertGeodeticToMercator(geodeticBbox);

    INFO() << "Loading satellite image";
    auto image = autocart::loadSatImage(mercatorBbox, zoom, tileSourceUrl);
    auto imageFileName = outputFilePrefix + ".jpg";
    INFO() << "Saving image to " << imageFileName;
    cv::imwrite(imageFileName, image);

    INFO() << "Loading objects";
    std::list<semgen::Building> blds;
    std::list<semgen::Road> roads;
    std::list<semgen::Vegetation> vegetations;
    std::list<semgen::Hydro> hydros;
    if (!connStr.empty()) {
        INFO() << "Opening connection to " << connStr;
        pqxx::connection conn(connStr);
        pqxx::nontransaction txn(conn);
        semgen::YMapsDFLoader loader(txn, mercatorBbox);
        loadObjects(loader, &blds, &roads, &vegetations, &hydros);
    } else {
        std::unique_ptr<maps::wiki::common::ExtendedXmlDoc> configDocPtr;
        if (!config.empty()) {
            configDocPtr.reset(new common::ExtendedXmlDoc(config));
        } else {
            configDocPtr = common::loadDefaultConfig();
        }
        common::PoolHolder coreDbHolder(*configDocPtr, "core", "core");
        auto geomReadTxn = coreDbHolder.pool().slaveTransaction();
        semgen::MapsproLoader loader(*geomReadTxn, mercatorBbox);
        loadObjects(loader, &blds, &roads, &vegetations, &hydros);
    }

    INFO() << "Converting to display coordinates";
    std::list<semgen::Building> dispBlds = convertToDisplay(blds, zoom, mercatorBbox);
    std::list<semgen::Road> dispRoads = convertToDisplay(roads, zoom, mercatorBbox);
    std::list<semgen::Vegetation> dispVegetations = convertToDisplay(vegetations, zoom, mercatorBbox);
    std::list<semgen::Hydro> dispHydros = convertToDisplay(hydros, zoom, mercatorBbox);

    if (isBldShiftEnable) {
        INFO() << "Shifting buildings using information about satellite";
        autocart::SatelliteParams satParams
                = autocart::getSatelliteParams(image.size(), geodeticBbox);
        dispBlds = shiftBuildingsBySatellite(dispBlds, satParams);
    } else {
        INFO() << "Buidling shift is disabled";
    }

    INFO() << "Buffering roads";
    std::list<BufferedRoad> dispBufferedRoads;
    for (const auto& road : dispRoads) {
        dispBufferedRoads.push_back(buffer(road, ROAD_BUFFER_WIDTH_PIXELS));
    }

    auto objectsFileName = outputFilePrefix + ".txt";
    INFO() << "Saving objects to " << objectsFileName;
    std::ofstream objectsStream(objectsFileName);
    writeObjects(objectsStream, image, "bld", dispBlds);
    writeObjects(objectsStream, image, "road", dispBufferedRoads);
    writeObjects(objectsStream, image, "vegetation", dispVegetations);
    writeObjects(objectsStream, image, "hydro", dispHydros);

    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    INFO() << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    INFO() << e.what();
    return EXIT_FAILURE;
}
catch (...) {
    INFO() << "Caught unknown exception";
    return EXIT_FAILURE;
}
