#include "ymapsdf_reader.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/conversion.h>

#include <pqxx/binarystring>
#include <pqxx/result>

#include <optional>
#include <sstream>

namespace maps::mrc::semgen {

namespace {

std::string makeBBoxFilter(const std::string& geomField,
                           const geolib3::BoundingBox& bbox)
{
    geolib3::BoundingBox geodeticBbox = geolib3::convertMercatorToGeodetic(bbox);
    std::stringstream str;
    str << "ST_Intersects(" << geomField << ", ST_SetSRID(ST_MakeBox2D(ST_MakePoint("
            << geodeticBbox.minX() << ", " << geodeticBbox.minY() << "), "
            << "ST_MakePoint(" << geodeticBbox.maxX() << ", " << geodeticBbox.maxY() << ")), 4326))";
    return str.str();
}

template<class Geometry>
std::list<Geometry>
loadGeometriesFromTableByBbox(pqxx::transaction_base& txn,
                              const std::string& objCategory,
                              const std::string& fromExpr,
                              const std::string& geomColumnName,
                              const geolib3::BoundingBox& bbox,
                              const std::string& filterExpr = std::string())
{
    INFO() << "  Loading category: " << objCategory;
    std::stringstream sstream;
    sstream << "SELECT ST_AsBinary(" << geomColumnName << ") shape\n"
        << "FROM " << fromExpr << "\n"
        << "WHERE " << makeBBoxFilter("shape", bbox);
    if (!filterExpr.empty()) {
        sstream << " and " << filterExpr;
    }
    auto query = sstream.str();

    std::list<Geometry> result;
    for (const auto& row : txn.exec(query)) {
        auto geomField = row["shape"];
        REQUIRE(!geomField.is_null(), "empty geom field");
        std::string geomWkb = pqxx::binarystring(geomField).str();
        std::stringstream geomWkbStream(geomWkb);
        Geometry geom = geolib3::WKB::read<Geometry>(geomWkbStream);
        result.push_back(geolib3::convertGeodeticToMercator(geom));
    }
    INFO() << "  Loaded " << result.size() << " objects";
    return result;
}


} // namespace

std::list<Building>
YMapsDFLoader::loadBuildings() const
{
    INFO() << "  Loading category: bld";
    std::stringstream sstream;
    sstream << "SELECT ST_AsBinary(shape) shape, height\n"
        << "FROM bld JOIN bld_geom USING (bld_id)\n"
        << "WHERE " << makeBBoxFilter("shape", bbox_);
    auto query = sstream.str();

    std::list<Building> result;
    for (const auto& row : txn_.exec(query)) {
        auto geomField = row["shape"];
        REQUIRE(!geomField.is_null(), "empty geom field");
        std::string geomWkb = pqxx::binarystring(geomField).str();
        std::stringstream geomWkbStream(geomWkb);
        geolib3::Polygon2 footprint = geolib3::WKB::read<geolib3::Polygon2>(geomWkbStream);

        auto heightField = row["height"];
        std::optional<double> height;
        if (!heightField.is_null()) {
            height = heightField.as<double>();
        }
        result.emplace_back(geolib3::convertGeodeticToMercator(footprint),
                            height);
    }
    INFO() << "  Loaded " << result.size() << " objects";
    return result;
}

std::list<Road>
YMapsDFLoader::loadRoads() const
{
    return loadGeometriesFromTableByBbox<Road>(txn_, "road", "rd_el", "shape", bbox_);
}

std::list<Vegetation>
YMapsDFLoader::loadVegetation() const
{
    return loadGeometriesFromTableByBbox<Vegetation>(
        txn_, "vegetation", "ft JOIN ft_geom USING (ft_id)", "shape", bbox_,
        "ft_type_id IN (401, 402) AND ST_GeometryType(shape) = 'ST_Polygon'"
    );
}

std::list<Hydro>
YMapsDFLoader::loadHydro() const
{
    return loadGeometriesFromTableByBbox<Hydro>(
        txn_, "hydro", "ft JOIN ft_geom USING (ft_id)", "shape", bbox_,
        "ft_type_id / 100 = 5 AND ST_GeometryType(shape) = 'ST_Polygon'"
    );
}

} // namespace maps::mrc::semgen
