#include "validate.h"

#include <yandex/maps/wiki/configs/editor/restrictions.h>
#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <yandex/maps/wiki/configs/editor/category_template.h>

#include <maps/libs/common/include/exception.h>

#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/distance.h>

namespace maps {
namespace wiki {
namespace importer {

namespace {

const std::map<OGRwkbGeometryType, std::string> GEOMETRY_TYPES_MAP = {
    {OGRwkbGeometryType::wkbPoint, "point"},
    {OGRwkbGeometryType::wkbLineString, "polyline"},
    {OGRwkbGeometryType::wkbPolygon, "polygon"},
};

size_t getNumPoints(const OGRGeometry* geometry)
{
    if (geometry->getGeometryType() == wkbLineString) {
        const auto* line = static_cast<const OGRLineString*>(geometry);
        return line->getNumPoints();
    } else if (geometry->getGeometryType() == wkbPolygon) {
        const auto* polygon = static_cast<const OGRPolygon*>(geometry);

        size_t numPoints = 0;
        numPoints += polygon->getExteriorRing()->getNumPoints();
        for (int i = 0; i < polygon->getNumInteriorRings(); ++i) {
            numPoints += polygon->getInteriorRing(i)->getNumPoints();
        }
        return numPoints;
    }
    return 0;
}

cfg::Size getRealSize(const OGRGeometry* geometry)
{
    REQUIRE(geometry->getGeometryType() == wkbPolygon,
        "Wrong geometry type " << OGRGeometryTypeToName(geometry->getGeometryType()));

    const auto* polygon = static_cast<const OGRPolygon*>(geometry);

    OGREnvelope envelope;
    polygon->getEnvelope(&envelope);
    REQUIRE(envelope.IsInit(), "Bad envelope");

    geolib3::Point2 lb(envelope.MinX, envelope.MinY);
    geolib3::Point2 lt(envelope.MinX, envelope.MaxY);
    geolib3::Point2 rb(envelope.MaxX, envelope.MinY);

    return cfg::Size(geolib3::geoDistance(lb, rb), geolib3::geoDistance(lb, lt));
}

template <class T>
T extractParam(const OGR_SRSNode& node, int childIdx)
{
    const auto* child = node.GetChild(childIdx);
    REQUIRE(child, "Missing SRS node child by index " << childIdx);

    const char* value = child->GetValue();
    REQUIRE(value, "Bad SRS node child value by index " << childIdx);
    return boost::lexical_cast<T>(value);
}

} // namespace

void validateFieldType(const cfg::AttrDefPtr& attrDef, OGRFieldType fieldType)
{
    switch (attrDef->valueType()) {
    case cfg::ValueType::Boolean:
    case cfg::ValueType::Bitmask:
        REQUIRE(fieldType == OFTInteger
            || fieldType == OFTString,
            "Unsupported GDAL field type '" << static_cast<int>(fieldType)
                << "' for attribute " << attrDef->id()
                << " with type " << static_cast<int>(attrDef->valueType()));
        break;
    case cfg::ValueType::Integer:
    case cfg::ValueType::Float:
        REQUIRE(fieldType == OFTInteger
            || fieldType == OFTReal
            || fieldType == OFTString,
            "Unsupported GDAL field type '" << static_cast<int>(fieldType)
                << "' for attribute " << attrDef->id()
                << " with type " << static_cast<int>(attrDef->valueType()));
        break;
    case cfg::ValueType::String:
    case cfg::ValueType::Enum:
    case cfg::ValueType::Json:
    case cfg::ValueType::Login:
        break;
    case cfg::ValueType::Table:
        throw maps::RuntimeError() << "Attribute " << attrDef->id()
            << " with type " << static_cast<int>(attrDef->valueType()) << " is not supported";
    }
}

void validateGeometry(const cfg::Category& category, const OGRGeometry* geometry)
{
    auto geometryType = geometry->getGeometryType();

    auto it = GEOMETRY_TYPES_MAP.find(geometryType);
    REQUIRE(it != GEOMETRY_TYPES_MAP.end(),
        "Invalid geometry type " << OGRGeometryTypeToName(geometryType));

    const auto& categoryTemplate = category.categoryTemplate();
    REQUIRE(categoryTemplate.isGeometryTypeValid(it->second),
        "Category " << category.id() << " does not support geometry type " << OGRGeometryTypeToName(geometryType));

    auto numPoints = getNumPoints(geometry);
    REQUIRE(numPoints < category.restrictions().maxVertexes(), "Too many points " << numPoints);

    OGREnvelope envelope;
    geometry->getEnvelope(&envelope);
    REQUIRE(envelope.IsInit(), "Envelope is not init");
    REQUIRE(envelope.MinX >= -180.0
        && envelope.MaxX <= 180.0
        && envelope.MinY >= -90.0
        && envelope.MaxY <= 90.0,
        "Envelope has bad size");

    if (!category.restrictions().gabarits()) {
        return;
    }

    const auto& gabarits = *category.restrictions().gabarits();
    if (gabarits.length() && geometryType == wkbLineString) {
        REQUIRE(getRealLength(geometry) * *gabarits.lengthAccuracy() < *gabarits.length(), "Line is too long");
    } else if (geometryType == wkbPolygon) {
        auto size = getRealSize(geometry);

        if (gabarits.maxSize()) {
            REQUIRE(size.less(*gabarits.maxSize()), "Polygon is too big");
        }
        if (gabarits.minSize()) {
            REQUIRE(size.greater(*gabarits.minSize()), "Polygon is too small");
        }
    }
}

void validateLayerSRS(OGRLayer& layer)
{
    const auto name = layer.GetName();
    const auto* spatialRef = layer.GetSpatialRef();

    REQUIRE(spatialRef, "Missing spatial reference in layer " << name);
    REQUIRE(spatialRef->IsGeographic(), "Projected SRS in layer " << name);

    auto* datum = spatialRef->GetAttrNode("DATUM");
    REQUIRE(datum, "Missing 'DATUM' parameter in layer " << name);

    auto datumName = extractParam<std::string>(*datum, 0);
    REQUIRE(datumName == SRS_DN_WGS84,
        "DATUM name is different from " << SRS_DN_WGS84 << ": " << datumName);

    auto* spheroid = datum->GetNode("SPHEROID");
    REQUIRE(spheroid, "Missing 'SPHEROID' parameter in layer " << name);

    auto wgsSemimajor = extractParam<double>(*spheroid, 1);
    auto wgsInvFlattering = extractParam<double>(*spheroid, 2);

    REQUIRE(wgsSemimajor == SRS_WGS84_SEMIMAJOR,
            "Wrong spheroid semi-major axis: " << wgsSemimajor);
    REQUIRE(wgsInvFlattering == SRS_WGS84_INVFLATTENING,
            "Wrong spheroid inverse flattering: " << wgsInvFlattering);
}

} // namespace importer
} // namespace wiki
} // namespace maps
