#include "gdal_helpers.h"
#include "config.h"

#include <maps/libs/codepage/include/codepage.h>
#include <maps/libs/concurrent/include/scoped_guard.h>
#include <maps/libs/introspection/include/comparison.h>
#include <maps/libs/geolib/include/variant.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>

#include <contrib/libs/gdal/ogr/ogrsf_frmts/ogrsf_frmts.h>
#include <map>

namespace maps::wiki::exporter {

using concurrent::ScopedGuard;

namespace {

const std::string CAT_PREFIX = "cat:";
const std::string OBJECTID_ATTR = "objectid";
const std::string GEOMETRY_ATTR = "geometry";
const std::string ATTRIBUTES_ATTR = "attributes";
const std::string SLAVE_ATTR = "slave";
const std::string MASTER_ATTR = "master";
const std::string ROLE_ATTR = "rel:role";
const std::string RELATIONS_ATTR = "relations";

constexpr int EPSG_ID_WGS84 = 4326;
constexpr int SHAPE_MAX_INT_WIDTH = 9;

struct Filter {
    std::string category;
    std::optional<std::pair<std::string, std::string>> attrValue;

    auto introspect()  const {
        return std::tie(category, attrValue);
    }
};

using introspection::operator<;


bool objectSatisfies(const json::Value& object, const Filter& filter) {
    const auto& attrs = object[ATTRIBUTES_ATTR];
    const std::string prefixedCategory = CAT_PREFIX + filter.category;
    bool categorySatisfies = attrs.hasField(prefixedCategory);
    if (!categorySatisfies || !filter.attrValue) {
        return categorySatisfies;
    }
    std::string fieldSuffix = ":" + filter.attrValue->first;
    for (const std::string& field : attrs.fields()) {
        if (field.size() >= fieldSuffix.size() &&
            std::equal(fieldSuffix.rbegin(), fieldSuffix.rend(), field.rbegin()) &&
            attrs[field].as<std::string>() == filter.attrValue->second
        ) {
            return true;
        }
    }
    return false;
}

std::string getLayerName(const Filter& filter) {
    if (!filter.attrValue) {
        return filter.category;
    }
    return filter.category + "_" + filter.attrValue->first + "_" + filter.attrValue->second;
}


std::vector<Filter> makeFilters(
    const std::string& category,
    const CategoriesFiltersMap& categoriesFilters)
{
    if (categoriesFilters.count(category) == 0 || categoriesFilters.at(category).empty()) {
        return {{category, std::nullopt}};
    }
    std::vector<Filter> result;
    result.reserve(categoriesFilters.at(category).size());
    for (auto&& filter : categoriesFilters.at(category)) {
        result.emplace_back(Filter{category, {filter}});
    }
    return result;
}


OGRFieldType fieldTypeFromString(const std::string& fieldType)
{
    static const std::map<std::string, OGRFieldType> fieldTypeMap = {
        {"OFTInteger", OGRFieldType::OFTInteger},
        {"OFTIntegerList", OGRFieldType::OFTIntegerList},
        {"OFTReal", OGRFieldType::OFTReal},
        {"OFTRealList", OGRFieldType::OFTRealList},
        {"OFTString", OGRFieldType::OFTString},
        {"OFTStringList", OGRFieldType::OFTStringList},
        {"OFTWideString", OGRFieldType::OFTWideString},
        {"OFTWideStringList", OGRFieldType::OFTWideStringList},
        {"OFTBinary", OGRFieldType::OFTBinary},
        {"OFTDate", OGRFieldType::OFTDate},
        {"OFTTime", OGRFieldType::OFTTime},
        {"OFTDateTime", OGRFieldType::OFTDateTime}
    };

    auto itr = fieldTypeMap.find(fieldType);
    REQUIRE(itr != fieldTypeMap.end(), "Invalid field type: " << fieldType);
    return itr->second;
}


OGRwkbGeometryType geometryTypeFromString(const std::string& geomType)
{
    static const std::map<std::string, OGRwkbGeometryType> geomTypeMap = {
        {"wkbUnknown", OGRwkbGeometryType::wkbUnknown},
        {"wkbPoint", OGRwkbGeometryType::wkbPoint},
        {"wkbLineString", OGRwkbGeometryType::wkbLineString},
        {"wkbPolygon", OGRwkbGeometryType::wkbPolygon},
        {"wkbMultiPoint", OGRwkbGeometryType::wkbMultiPoint},
        {"wkbMultiLineString", OGRwkbGeometryType::wkbMultiLineString},
        {"wkbMultiPolygon", OGRwkbGeometryType::wkbMultiPolygon},
        {"wkbGeometryCollection", OGRwkbGeometryType::wkbGeometryCollection},
        {"wkbNone", OGRwkbGeometryType::wkbNone},
        {"wkbLinearRing", OGRwkbGeometryType::wkbLinearRing},
        {"wkbPoint25D", OGRwkbGeometryType::wkbPoint25D},
        {"wkbLineString25D", OGRwkbGeometryType::wkbLineString25D},
        {"wkbPolygon25D", OGRwkbGeometryType::wkbPolygon25D},
        {"wkbMultiPoint25D", OGRwkbGeometryType::wkbMultiPoint25D},
        {"wkbMultiLineString25D", OGRwkbGeometryType::wkbMultiLineString25D},
        {"wkbMultiPolygon25D", OGRwkbGeometryType::wkbMultiPolygon25D},
        {"wkbGeometryCollection25D", OGRwkbGeometryType::wkbGeometryCollection25D}
    };

    auto itr = geomTypeMap.find(geomType);
    REQUIRE(itr != geomTypeMap.end(), "Invalid geometry type: " << geomType);
    return itr->second;
}


OGRLayer* createOgrLayer(
    GDALDataset* dataSource,
    const std::string name,
    OGRSpatialReference* sref,
    OGRwkbGeometryType geomType,
    const CategoryAttrNameToInfo& attributes)
{
    OGRLayer* layer = dataSource->CreateLayer(name.c_str(), sref, geomType);

    for (const auto& [attrName, attrValue]: attributes) {
        if (attrName == GEOMETRY_ATTR)
            continue;

        const auto attrType = fieldTypeFromString(attrValue.type);
        OGRFieldDefn ogrField(attrName.c_str(), attrType);

        if (attrType == OGRFieldType::OFTInteger) {
            ogrField.SetWidth(SHAPE_MAX_INT_WIDTH);
        }
        REQUIRE(layer->CreateField(&ogrField) == OGRERR_NONE,
                "Can't create field " << attrName << " in layer " << name);
    }
    return layer;
}


void setOgrFeatureGeometry(OGRFeature* feature, const json::Value& object)
{
    // Convert through wkb, since gdal geometry can't be created from geoJson
    auto geometryVariant = geolib3::readGeojson<geolib3::SimpleGeometryVariant>(object[GEOMETRY_ATTR]);
    auto wkb = geolib3::WKB::toString(geometryVariant);

    OGRGeometry* geometry;
    auto res = OGRGeometryFactory::createFromWkb((unsigned char*)wkb.c_str(),
            nullptr, &geometry, wkb.size());
    REQUIRE(res == OGRERR_NONE, "Failed to create geometry from wkb: " << wkb);

    REQUIRE(feature->SetGeometry(geometry) == OGRERR_NONE,
        "Failed to set geometry for feature " << feature->GetDefnRef()->GetName());
}

struct Relation
{
    RelatioType relationType;
    std::string role;
    std::string relativeId;
};

std::vector<Relation>
parseRelations(const maps::json::Value& object)
{
    std::vector<Relation> relations;
    if (!object.hasField(RELATIONS_ATTR)) {
        return relations;
    }
    const auto relationsArray = object[RELATIONS_ATTR];
    for (const auto& relationObj : relationsArray) {
        Relation relation {
            .relationType = relationObj.hasField(SLAVE_ATTR)
                ? RelatioType::ToSlave
                : RelatioType::ToMaster,
            .role = relationObj[ATTRIBUTES_ATTR][ROLE_ATTR].as<std::string>(),
            .relativeId = relationObj.hasField(SLAVE_ATTR)
                ? relationObj[SLAVE_ATTR].as<std::string>()
                : relationObj[MASTER_ATTR].as<std::string>()
        };
        relations.emplace_back(std::move(relation));
    }
    return relations;
}

std::string findRelativeId(
    const std::vector<Relation>& relations,
    const RelativeIdSource& relativeIdSource)
{
    std::string relativeId;
    for (const auto& relation : relations) {
        if (relation.relationType != relativeIdSource.relationType ||
            relation.role != relativeIdSource.role) {
            continue;
        }
        if (!relativeId.empty()) {
            throw maps::RuntimeError() << "Role relation not unique '" << relation.role << "'";
        }
        relativeId = relation.relativeId;
    }
    return relativeId;
}

void json2OgrFeature(
    const std::string& category,
    const std::string& objectId,
    const maps::json::Value& object,
    OGRLayer* layer,
    const CategoriesMap& categories)
{
    auto* layerDefn = layer->GetLayerDefn();
    auto* feature = OGRFeature::CreateFeature(layerDefn);
    ScopedGuard fGuard([&]{ OGRFeature::DestroyFeature(feature); });

    const auto attrs = object[ATTRIBUTES_ATTR];
    const auto categoryIt = categories.find(category);
    REQUIRE(categoryIt != categories.end(),
        "Category '" << category << "' not configured.");

    const auto& categoryInfo = categoryIt->second;

    for (int fieldNum = 0; fieldNum < layerDefn->GetFieldCount(); ++fieldNum) {
        OGRFieldDefn* fieldDefn = layerDefn->GetFieldDefn(fieldNum);
        const auto fieldName = fieldDefn->GetNameRef();
        const auto attrIt = categoryInfo.find(fieldName);
        REQUIRE(attrIt != categoryInfo.end(),
                "Category '" << category
                << "' attr '" << fieldName << "' not configured.");
        const auto& attrInfo = attrIt->second;
        const auto& optDefVal = attrInfo.defVal;

        const auto attrName = attrInfo.sourceAttrName
            ? *attrInfo.sourceAttrName
            : (category + ":" + fieldName);

        std::string relativeId;
        if (attrInfo.relativeIdSource) {
            relativeId = findRelativeId(parseRelations(object), *attrInfo.relativeIdSource);
        }

        std::string featureValue;
        if (fieldName == OBJECTID_ATTR) {
            featureValue = objectId;
        } else if (attrs.hasField(attrName)) {
            featureValue = attrs[attrName].as<std::string>();
        } else if (attrInfo.relativeIdSource && !relativeId.empty()) {
            featureValue = relativeId;
        } else if (optDefVal) {
            featureValue = *optDefVal;
        } else {
            throw maps::RuntimeError() << "attr '" << attrName << "' not found";
        }


        if (fieldDefn->GetType() == OGRFieldType::OFTString) {
            std::u32string u32value = codepage::convertToUtf32(codepage::u8string_view(featureValue));
            codepage::cp1251string cp1251value;
            codepage::convert(u32value, cp1251value);
            featureValue = std::move(cp1251value.value());
        }
        feature->SetField(fieldName, featureValue.c_str());
    }

    setOgrFeatureGeometry(feature, object);

    REQUIRE(layer->CreateFeature(feature) == OGRERR_NONE,
        "Failed to create feature " << feature->GetDefnRef()->GetName());
}

} // namespace


class SpatialReferenceHolder {
public:
    SpatialReferenceHolder(int epsgCode)
    {
        sRef_.importFromEPSG(epsgCode);
    }

    OGRSpatialReference& get()
    {
        return sRef_;
    }
private:
    OGRSpatialReference sRef_;
};


void json2Shape(
    const std::string& inputFile,
    const std::string& outputDir,
    const CategoriesMap& categoriesMap,
    const CategoriesFiltersMap& categoriesFilters)
try {
    OGRRegisterAll();
    auto driver = GetGDALDriverManager()->GetDriverByName("ESRI Shapefile");

    auto dataSource = driver->Create(outputDir.c_str(), 0, 0, 0, GDT_Unknown, nullptr);
    REQUIRE(dataSource, "Can't open output data source");
    ScopedGuard dsGuard([&]{ delete dataSource; });

    static SpatialReferenceHolder sRef(EPSG_ID_WGS84);

    std::map<Filter, OGRLayer*> categoryToLayer;
    for (auto& item: categoriesMap) {
        const auto& catName = item.first;
        const auto& catData = item.second;
        auto geometryAttr = catData.find(GEOMETRY_ATTR);
        REQUIRE(geometryAttr != catData.end(),
            "Missing geometry attribute in category " << catName);
        auto geomType = geometryTypeFromString(geometryAttr->second.type);

        for (const Filter& filter : makeFilters(catName, categoriesFilters)) {
            categoryToLayer.emplace(filter, createOgrLayer(
                dataSource, getLayerName(filter), &sRef.get(), geomType, catData));
        }
    }

    auto jsonData = json::Value::fromFile(inputFile);
    auto objects = jsonData["objects"];

    std::ostringstream errCollector;
    for (auto key: objects.fields()) {
        const auto& object = objects[key];
        for (const auto& [filter, layerPtr]: categoryToLayer) {
            if (objectSatisfies(object, filter)) {
                try {
                    json2OgrFeature(filter.category, key, object, layerPtr, categoriesMap);
                } catch (const std::exception& e) {
                    errCollector
                        << "Object '" << key << "' has error: " << e.what() << "\n";
                }
            }
        }
    }
    auto errLog = errCollector.str();
    REQUIRE(errLog.empty(), "Broken json:\n" << errLog);
} catch (const std::exception& e) {
    throw Json2ShapeError() << "json2shape error: " << e.what();
}


void convertFromShape(
    const std::string& inputDir,
    const std::string& outputDir,
    const std::string& gdalDriverName)
try {
    OGRRegisterAll();
    auto driver = GetGDALDriverManager()->GetDriverByName("ESRI Shapefile");
    auto geojsonDriver = GetGDALDriverManager()->GetDriverByName(gdalDriverName.c_str());

    auto dataSource = driver->Create(inputDir.c_str(), 0, 0, 0, GDT_Unknown, nullptr);
    REQUIRE(dataSource, "Can't open input data source");
    ScopedGuard indsGuard([&]{ delete dataSource; });

    for (const auto& shapeLayer: dataSource->GetLayers()) {
        fs::path outFile = (fs::path(outputDir) / shapeLayer->GetName());
        outFile.replace_extension("geojson");
        auto curDataSource = geojsonDriver->Create(outFile.c_str(), 0, 0, 0, GDT_Unknown, nullptr);
        REQUIRE(curDataSource, "Can't open output data source: " << outFile.string());
        ScopedGuard dsGuard([&]{ delete curDataSource; });
        OGRLayer* curLayer = curDataSource->CreateLayer(
            shapeLayer->GetName(),
            shapeLayer->GetSpatialRef(),
            shapeLayer->GetGeomType());

        auto* shapeLayerDefn = shapeLayer->GetLayerDefn();
        for (int i = 0; i < shapeLayerDefn->GetFieldCount(); ++i) {
            const auto fieldDefn = shapeLayerDefn->GetFieldDefn(i);
            curLayer->CreateField(fieldDefn);
        }

        for (const auto& inFeature : *shapeLayer) {
            auto* outFeature = inFeature->Clone();
            ScopedGuard fGuard([&]{ OGRFeature::DestroyFeature(outFeature); });

            REQUIRE(curLayer->CreateFeature(outFeature) == OGRERR_NONE,
                "Failed to create feature " << inFeature->GetDefnRef()->GetName());
        }
    }
} catch (const std::exception& e) {
    throw ConvertFromShapeError() <<
        "convert from shape to " << gdalDriverName << " error: " << e.what();
}

} // namespace maps::wiki::exporter
