#include "snapshot_impl.h"
#include "helpers.h"
#include "magic_strings.h"
#include "object_impl.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/retry_duration.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <yandex/maps/wiki/revision/filters.h>

#include <pqxx/pqxx>
#include <sstream>

namespace mwr = maps::wiki::revision;
namespace rf = maps::wiki::revision::filters;

namespace maps {
namespace wiki {
namespace diffalert {
namespace {

const size_t COORD_PRECISION = 12;

namespace objects {

const std::string ALL = "objects_g";
const std::string POINT = "objects_p_view";
const std::string LINESTRING = "objects_l_view";
const std::string POLYGON = "objects_a_view";

} // namsepace objects

std::string envelopeViewClause(Envelope envelope)
{
    fixEnvelope(envelope);

    std::ostringstream clauseStream;
    clauseStream.precision(COORD_PRECISION);
    clauseStream << "the_geom && ST_SetSRID(ST_MakeBox2D("
        << "ST_Point(" << envelope.getMinX() << ',' << envelope.getMinY() << "),"
        << "ST_Point(" << envelope.getMaxX() << ',' << envelope.getMaxY() << ")),"
        << "3395)";
    return clauseStream.str();
}

std::string categoryIdsViewClause(
        const pqxx::transaction_base& txn,
        const std::vector<std::string>& categoryIds)
{
    if (categoryIds.empty()) {
        return "TRUE";
    }

    return "domain_attrs ?| ARRAY[" +
        common::join(
            categoryIds,
            [&](const std::string& catId)
                { return txn.quote(CATEGORY_ATTR_PREFIX + catId); }, ',') +
        ']';
}

const std::string& tableByGeomType(GeometryType geomType)
{
    switch (geomType)
    {
        case GeometryType::All:
            return objects::ALL;
        case GeometryType::Point:
            return objects::POINT;
        case GeometryType::LineString:
            return objects::LINESTRING;
        case GeometryType::Polygon:
            return objects::POLYGON;
    }
    ASSERT(!"unexpected geometry type");
}

rf::GeomFilterExpr::Operation geomOperationByGeomType(GeometryType geomType)
{
    switch (geomType)
    {
        case GeometryType::All:
            return rf::GeomFilterExpr::Operation::Intersects;
        case GeometryType::Point:
            return rf::GeomFilterExpr::Operation::IntersectsPoints;
        case GeometryType::LineString:
            return rf::GeomFilterExpr::Operation::IntersectsLinestrings;
        case GeometryType::Polygon:
            return rf::GeomFilterExpr::Operation::IntersectsPolygons;
    }
    ASSERT(!"unexpected geometry type");
}

} // namespace

LongtaskSnapshot::Impl::Impl(
        RevSnapshotFactory revSnapshotFct,
        ViewTxnFactory viewTxnFct,
        const GeomIndexPtr& bldWith3Dmodel,
        const EditorConfig& config)
    : revSnapshotFct_(std::move(revSnapshotFct))
    , viewTxnFct_(std::move(viewTxnFct))
    , bldWith3Dmodel_(bldWith3Dmodel)
    , config_(config)
{
    ASSERT(bldWith3Dmodel_);
}

std::vector<Snapshot::ObjectPtr> LongtaskSnapshot::Impl::primitivesByEnvelopeFromRevision(
    Envelope envelope,
    GeometryType geomType,
    const std::vector<std::string>& categoryIds)
{
    fixEnvelope(envelope);

    rf::ProxyFilterExpr filter = rf::GeomFilterExpr(
        geomOperationByGeomType(geomType),
        envelope.getMinX(),
        envelope.getMinY(),
        envelope.getMaxX(),
        envelope.getMaxY());

    if (!categoryIds.empty()) {
        std::vector<std::string> canonicalCategoryIds;
        canonicalCategoryIds.reserve(categoryIds.size());
        for (const auto& categoryId : categoryIds) {
            canonicalCategoryIds.push_back(CATEGORY_ATTR_PREFIX + categoryId);
        }
        filter &= rf::Attr::definedAny(canonicalCategoryIds);
    }

    filter &= rf::ObjRevAttr::isNotDeleted();
    filter &= rf::ObjRevAttr::isNotRelation();

    auto revisions = common::retryDuration([&] {
        auto snapshot = revSnapshotFct_.get();
        return snapshot->objectRevisionsByFilter(filter);
    });

    std::vector<Snapshot::ObjectPtr> objects;
    for (const auto& rev : revisions) {
        objects.emplace_back(new LongtaskObject(
            LongtaskObject::Impl(rev, revSnapshotFct_, config_)));
    }
    return objects;
}

std::vector<Snapshot::ObjectPtr> LongtaskSnapshot::Impl::primitivesByEnvelopeFromView(
    Envelope envelope,
    GeometryType geomType,
    const std::vector<std::string>& categoryIds)
{
    auto rows = common::retryDuration([&] {
        auto txn = viewTxnFct_.get();

        std::string query =
            "SELECT id, hstore_to_json(domain_attrs), ST_AsBinary(the_geom) FROM ";
        query += tableByGeomType(geomType);
        query += " WHERE ";
        query += envelopeViewClause(std::move(envelope));
        if (!categoryIds.empty()) {
            query += " AND ";
            query += categoryIdsViewClause(*txn, categoryIds);
        }
        return txn->exec(query);
    });

    std::vector<Snapshot::ObjectPtr> objects;
    for (const auto& row : rows) {
        auto oid = row[0].as<TId>();
        auto attrs = json::Value::fromString(
            row[1].as<std::string>()).as<mwr::Attributes>();
        auto geom = Geom(pqxx::binarystring(row[2]).str());
        objects.emplace_back(new LongtaskObject(
            LongtaskObject::Impl(
                oid,
                std::move(attrs),
                std::move(geom),
                revSnapshotFct_,
                config_)));
    }

    return objects;
}

} // namespace diffalert
} // namespace wiki
} // namespace maps
