#pragma once

#ifndef WIKIMAP_EDITOR_PEDESTRIAN_LOAD_OBJECTS_TPL
#error "Direct inclusion of load_objects-impl.h is not allowed, "
    "please include load_objects.h instead"
#endif

#include <maps/wikimap/mapspro/services/editor/src/exception.h>
#include <maps/wikimap/mapspro/services/editor/src/views/objects_query.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>

#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/wiki/revision/common.h>

#include <geos/geom/Polygon.h>
#include <geos/geom/Point.h>

namespace maps::wiki::pedestrian {

namespace impl {

const std::string LIVING_BLD_FT_TYPE = "101";
const std::string MIN_BLD_HEIGHT = "8";
const std::string MIN_BLD_AREA_SQ_METERS = "100";

const size_t BUILDINGS_LIMIT = 10 * 1000;
const size_t ENTRANCES_LIMIT = 40 * 1000;
const size_t ADDRESSES_LIMIT = 20 * 1000;


template<typename T>
typename std::enable_if<std::is_base_of_v<PointNameObject, T>, T>::type
asTypedObject(const views::ViewObject& viewObject)
{
    auto pointGeosGeom = dynamic_cast<const geos::geom::Point*>(
        viewObject.geom().geosGeometryPtr()
    );
    ASSERT(pointGeosGeom != nullptr);

    std::optional<std::string> entranceName;
    if (!viewObject.screenLabel().empty()) {
        entranceName = viewObject.screenLabel();
    }

    return T(
        viewObject.id(),
        geolib3::Point2(pointGeosGeom->getX(), pointGeosGeom->getY()),
        std::move(entranceName)
    );
}

template<typename T>
typename std::enable_if<std::is_same_v<Building, T>, T>::type
asTypedObject(const views::ViewObject& viewObject)
{
    std::unique_ptr<geos::geom::Geometry> objectGeom(
        viewObject.geom().geosGeometryPtr()->clone()
    );
    ASSERT(dynamic_cast<geos::geom::Polygon*>(objectGeom.get()));

    return Building (
        viewObject.id(),
        geolib3::Polygon2(
            std::unique_ptr<geos::geom::Polygon>(
                dynamic_cast<geos::geom::Polygon*>(objectGeom.release())
            ),
            geolib3::Validate::Yes
        )
    );
}

template<typename T>
std::vector<T> asTypedObjects(const std::vector<views::ViewObject>& viewObjects)
{
    std::vector<T> typedObjects;

    std::transform(
        viewObjects.begin(), viewObjects.end(),
        std::back_inserter(typedObjects),
        asTypedObject<T>
    );

    return typedObjects;
}

static
std::vector<views::ViewObject> loadFilteredObjects(
    pqxx::transaction_base& viewTxn,
    const geolib3::Polygon2& polygonMerc,
    std::vector<std::unique_ptr<views::Condition>> filterConditions,
    int limit)
{
    viewTxn.exec("SET search_path=vrevisions_trunk,public");

    views::ObjectsQuery objectsQuery;

    objectsQuery.addCondition(
        views::CoveredByGeometryCondition(
            viewTxn,
            common::Geom(geolib3::WKB::toString(polygonMerc))
        )
    );

    for (auto&& condition : filterConditions) {
        objectsQuery.addCondition(std::move(condition));
    }

    try {
        return objectsQuery.exec(viewTxn, revision::TRUNK_BRANCH_ID, limit);
    } catch (const wiki::LogicException&) {
        throw ObjectsLimitException();
    }
}

template<typename BldLinkedObject>
std::vector<std::unique_ptr<views::Condition>>
buildingsFilterConditions(pqxx::transaction_base& viewTxn)
{
    std::vector<std::unique_ptr<views::Condition>> conditions;

    conditions.push_back(
        std::make_unique<views::CategoriesCondition>(
            viewTxn,
            StringSet{CATEGORY_BLD}
        )
    );

    std::vector<std::string> sqlConditions;
    sqlConditions.push_back(
        "domain_attrs @>hstore ('bld:ft_type_id', '" + LIVING_BLD_FT_TYPE + "')"
    );

    if constexpr(std::is_same_v<BldLinkedObject, Entrance>) {
        sqlConditions.push_back(
            "(domain_attrs->'bld:height')::int > " + MIN_BLD_HEIGHT
        );
        sqlConditions.push_back(
            "ST_Area((ST_Transform(the_geom, 4326))::geography) > " + MIN_BLD_AREA_SQ_METERS
        );
    }

    for (auto& sqlCondition : sqlConditions) {
        conditions.push_back(
            std::make_unique<views::GenericSqlCondition>(std::move(sqlCondition))
        );
    }

    return conditions;
}

template<typename BldLinkedObject>
std::string bldLinkedObjectCategory()
{
    if constexpr(std::is_same_v<BldLinkedObject, Entrance>) {
        return CATEGORY_ENTRANCE;
    } else if constexpr(std::is_same_v<BldLinkedObject, Address>) {
        return CATEGORY_ADDR;
    }
}

template<typename BldLinkedObject>
int bldLinkedObjectsLoadLimit()
{
    if constexpr(std::is_same_v<BldLinkedObject, Entrance>) {
        return ENTRANCES_LIMIT;
    } else if constexpr(std::is_same_v<BldLinkedObject, Address>) {
        return ADDRESSES_LIMIT;
    }
}

template<typename T>
std::unique_ptr<views::Condition>
bldLinkedObjectsCategoryFilter(pqxx::transaction_base& viewTxn)
{
    return std::make_unique<views::CategoriesCondition>(
        viewTxn,
        StringSet{bldLinkedObjectCategory<T>()}
    );
}

} // impl


template<typename BldLinkedObject>
std::vector<Building> loadBuildings(
    const geolib3::Polygon2& polygonMerc,
    pqxx::transaction_base& viewTxn)
{
    auto filteredViewObjects = impl::loadFilteredObjects(
        viewTxn,
        polygonMerc,
        impl::buildingsFilterConditions<BldLinkedObject>(viewTxn),
        impl::BUILDINGS_LIMIT
    );

    return impl::asTypedObjects<Building>(filteredViewObjects);
}


template<typename BldLinkedObject>
std::vector<BldLinkedObject> loadBldLinkedObjects(
    const geolib3::Polygon2& polygonMerc,
    pqxx::transaction_base& viewTxn)
{
    std::vector<std::unique_ptr<views::Condition>> filterConditions;
    filterConditions.push_back(
        impl::bldLinkedObjectsCategoryFilter<BldLinkedObject>(viewTxn)
    );

    auto filteredViewObjects = impl::loadFilteredObjects(
        viewTxn,
        polygonMerc,
        std::move(filterConditions),
        impl::bldLinkedObjectsLoadLimit<BldLinkedObject>()
    );

    return impl::asTypedObjects<BldLinkedObject>(filteredViewObjects);
}

} // namespace maps::wiki::pedestrian
