#include "generalization.h"
#include <maps/wikimap/mapspro/services/editor/src/common.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/object.h>

#include <maps/libs/tile/include/coord.h>
#include <maps/libs/tile/include/utils.h>

namespace maps::wiki {

Generalization::Generalization(const maps::xml3::Node& node)
    : id_(node.attr<std::string>("id"))
    , zoomInterval_(node)
{
    const maps::xml3::Node polygon = node.node("polygon", true);
    if (!polygon.isNull()) {
        std::string geomType = Geom::geomTypeNamePolygon;
        geomGeneralizations_.insert(geomType, new GeomGeneralization(polygon));
    }
    const maps::xml3::Node point = node.node("point", true);
    if (!point.isNull()) {
        std::string geomType = Geom::geomTypeNamePoint;
        geomGeneralizations_.insert(geomType, new GeomGeneralization(point));
    }
    const maps::xml3::Node polyline = node.node("polyline", true);
    if (!polyline.isNull()) {
        std::string geomType = Geom::geomTypeNameLine;
        geomGeneralizations_.insert(geomType, new GeomGeneralization(polyline));
    }
}

ZoomInterval
Generalization::objectVisibility(const GeoObject* object, Transaction& work) const
{
    if (object->geom().isNull()) {
        return zoomInterval_;
    }
    GeomGeneralizations::const_iterator it =
    geomGeneralizations_.find(object->geom().geometryTypeName());
    if (it == geomGeneralizations_.end()) {
        return zoomInterval_;
    } else {
        return it->second->geomVisibilityInterval(object, work);
    }
}

GeomGeneralization::GeomGeneralization(const maps::xml3::Node& node)
    : method_(GeneralizationMethod::create(node))
    , zoomInterval_(node)
{}

ZoomInterval
GeomGeneralization::geomVisibilityInterval(
    const GeoObject* object, Transaction& work) const
{
    return method_.get()
        ? method_->visibilityInterval(object, work, zoomInterval_)
        : zoomInterval_ ;
}


GeneralizationMethod::GeneralizationMethod(const maps::xml3::Node& )
{}

GeneralizationMethod::~GeneralizationMethod()
{}

GeneralizationMethodPtr
GeneralizationMethod::create(const maps::xml3::Node& node)
{
    if (!node.node("gabarits", true).isNull()) {
        return GeneralizationMethodPtr(new GabaritsGeneralization(node));
    }

    if (!node.node("collision", true).isNull()) {
        return GeneralizationMethodPtr(new CollisionGeneralization(node));
    }

    if (!node.node("functional", true).isNull()) {
        return GeneralizationMethodPtr(new FunctionalGeneralization(node));
    }

    if (!node.node("attributes", true).isNull()) {
        return GeneralizationMethodPtr(new AttributesGeneralization(node));
    }
    return GeneralizationMethodPtr();
}

// --------- GabaritsGeneralization -----------------

GabaritsGeneralization::GabaritsGeneralization(const maps::xml3::Node& node)
    : GeneralizationMethod(node)
    , window_(node.node("gabarits/window").attr<double>("width"),
        node.node("gabarits/window").attr<double>("height"))
    , minSize_(node.node("gabarits/min-size").attr<double>("width"),
        node.node("gabarits/min-size").attr<double>("height"))
    , minFraction_(node.node("gabarits/min-fraction").value<double>())
{}

/**
 * Details:
 * https://wiki/JandeksKarty/development/fordevelopers/servants/wikimap/generalisation
 */
ZoomInterval
GabaritsGeneralization::visibilityInterval(
    const GeoObject* object, Transaction& , const ZoomInterval& zoomRange) const
{
    double rMax = tile::zoomToResolution(tile::MAX_ZOOM);
    double rMin = tile::zoomToResolution(0);
    rMax *= rMax;
    rMin *= rMin;
    double area = object->geom()->getArea();
    auto zmin =
        std::max(tile::MAX_ZOOM -
                 ::log(area / (minSize_.width() * minSize_.height() * rMax))
                 / (2.0 * ::log(2.0)) +0.5, static_cast<double>(zoomRange.zmin()));
    auto zmax =
        std::min(::log((window_.width() * window_.height() * rMin)
                       /(area * minFraction_) )
                 / (2.0 * ::log(2.0)), static_cast<double>(zoomRange.zmax()));
    zmax = std::max (zmin, zmax);
    return ZoomInterval(zmin, zmax);
}

// ---------- CollisionGeneralization ------------

CollisionGeneralization::CollisionGeneralization(const maps::xml3::Node& node)
    : GeneralizationMethod(node)
    , radius_(node.node("collision").attr<double>("radius"))
{ }

TZoom CollisionGeneralization::goodZoom(
    double closestNeighbourDstMeters, const ZoomInterval& zoomRange) const
{
    return zoomRange.zmin() + 1 +
        std::log((2 * radius_)
                 /(closestNeighbourDstMeters / tile::zoomToResolution(zoomRange.zmin())) )
        / std::log(2.0);
}


ZoomInterval
CollisionGeneralization::visibilityInterval(
    const GeoObject* object, Transaction& work, const ZoomInterval& zoomRange) const
{
    const double minimumDistance = 2.0 * radius_ * tile::zoomToResolution(zoomRange.zmin());
    const auto geomStr = "ST_GeomFromWKB('" + work.esc_raw(object->geom().wkb()) + "', 3395)";

    std::stringstream query;
    //! retreive potentialy colliding objects
    query << "SELECT ST_Distance(the_geom, " << geomStr << ") as dst, zmin, zmax FROM "
        << object->tablename()
        << " WHERE ST_DWithin(the_geom, " << geomStr << ", " << minimumDistance << ")"
        << " AND id<>" << object->id();

    auto r = work.exec(query.str());

    if (!r.empty()) {
        typedef std::map<TZoom, double> ZoomDistance;
        //At each zoom store objects distances
        ZoomDistance distancesByZooms;
        for (size_t i = 0; i < r.size(); ++i) {
            TZoom rZMin = r[i]["zmin"].as<TZoom>();
            TZoom rZMax = r[i]["zmax"].as<TZoom>();
            double rDistance = r[i]["dst"].as<double>();
            for (TZoom z = rZMin; z < rZMax; ++z) {
                std::pair<ZoomDistance::iterator, bool> itPair =
                    distancesByZooms.insert(std::make_pair(z, rDistance));
                itPair.first->second = std::min(itPair.first->second, rDistance);
            }
        }

        //Now find zLevel at which no objects collide with current
        //that means - no distance converted to pixels at corresponding zoom
        //is less then radius_
        for (TZoom zLevel = zoomRange.zmin(); zLevel < zoomRange.zmax(); ++zLevel) {
            ZoomDistance::const_iterator it = distancesByZooms.find(zLevel);
            if (it->second / tile::zoomToResolution(zLevel) > radius_) {
                return ZoomInterval(zLevel, zoomRange.zmax());
            }
        }
        return ZoomInterval(zoomRange.zmax(), zoomRange.zmax());
    }
    return zoomRange;
}

// ------------------ FunctionalGeneralization ---------------
FunctionalGeneralization::FunctionalGeneralization(const maps::xml3::Node& node)
    : GeneralizationMethod(node)
    , attributeId_(node.node("functional").attr<std::string>("attribute-id"))
{
    maps::xml3::Nodes nodes = node.nodes("functional/item");
    for (size_t i = 0; i < nodes.size(); i++) {
        items_.insert(std::make_pair(
            nodes[i].attr<std::string>("value"), ZoomInterval(nodes[i])));
    }
}

ZoomInterval
FunctionalGeneralization::visibilityInterval(
        const GeoObject* object,
        Transaction& ,
        const ZoomInterval& zoomRange) const
{
    std::string attrValue = object->attributes().value(attributeId_);
    if (attrValue.empty()) {
        attrValue = object->serviceAttrValue(attributeId_);
    }
    Items::const_iterator item = items_.find(attrValue);
    if (item == items_.end() ){
        return zoomRange;
    }
    return item->second;
}

// ------------------ AttributeGeneralization ---------------
AttributesGeneralization::AttributesGeneralization(const maps::xml3::Node& node)
    : GeneralizationMethod(node)
{
    maps::xml3::Nodes nodes = node.nodes("attributes/attribute");
    for (size_t i = 0; i < nodes.size(); i++) {
        attrToRange_.insert(std::make_pair(
            nodes[i].attr<std::string>("id"), ZoomInterval(nodes[i])));
    }
}

ZoomInterval
AttributesGeneralization::visibilityInterval(
        const GeoObject* object,
        Transaction& ,
        const ZoomInterval& zoomRange) const
{
    for (const auto& attrRangePair : attrToRange_) {
        const auto& attrId = attrRangePair.first;
        std::string attrValue = object->attributes().value(attrId);
        if (attrValue.empty()) {
            attrValue = object->serviceAttrValue(attrId);
        }
        if (!attrValue.empty()) {
            return attrRangePair.second;
        }
    }
    return zoomRange;
}

} // namespace maps::wiki
