#pragma once

#include "geo_object.h"

#include "globals.h"

#include <saas/rtyserver/components/l2/l2_parser.h>
#include <saas/rtyserver/components/l2/l2_disk_manager.h>
#include <saas/rtyserver/components/l2/l2_core.h>

#include <saas/rtyserver/factors/function.h>
#include <library/cpp/geo/geo.h>
#include <library/cpp/geolocation/calcer.h>
#include <util/ysaveload.h>
#include <util/generic/cast.h>
#include <util/stream/mem.h>

namespace NRTYServer {
    using NRTYGeo::TRawPoint;

    //
    // TMinGeoSearcherCore: a base class for TMinGeoCore
    //
    class TMinGeoSearcherCore: public IL2ComponentCore {
    private:
        THashMap<TString, ui8> LayerNames;

    public:
        Y_FORCE_INLINE const THashMap<TString, ui8> GetLayerNames() const {
            return LayerNames;
        }

        ui8 GetLayerId(const TString& layerName, ui8 def) const {
            auto p = LayerNames.find(layerName);
            return Y_UNLIKELY(p == LayerNames.end()) ? def : p->second;
        }

        virtual void Init(const TRTYServerConfig& config) override;
    };

    //
    // TGeoTransientEntity: a TRTYFullArchiveLightEntity (a blob) plus some "transient" (non-serializable) extensions.
    // The extensions exist only for Builder or Merger, and they may be "restored" from the application-level Record.
    //
    class TGeoTransientEntity: public TRTYFullArchiveLightEntity {
    public:
        using TBuilderInvData = NRTYGeo::TInvRecord;

        TGeoTransientEntity(IParsedEntity::TConstructParams& params);

    public:
        TMaybe<TBuilderInvData> BuilderExtension;
    };

    //
    // TMinGeoParser: the class that knows what is stored in the DocEntity, and provides conversion routines
    //
    class TMinGeoParser: public TL2ComponentParserBase {
    public:
        using TRecord = NRTYGeo::TRecord;
        using TObject = NRTYGeo::TGeoObject;

    public:
        using TL2ComponentParserBase::TL2ComponentParserBase;

        virtual void Parse(TParsingContext& context) const {
            // The "Parse" name is somewhat misleading: this routine is called on Index, and is
            // not called when searcher or merger fetches the data.
            const TMessage::TDocument& document = context.Document;

            static constexpr char CoordsProp[] = "coords";

            bool hasData = false;
            TRecord record;

            if (document.HasGeoData()) {
                const auto& geo = document.GetGeoData();
                for (size_t l = 0; l < geo.LayersSize(); ++l) {
                    const auto& layer = geo.GetLayers(l);
                    const TString& layerName = layer.GetLayer();
                    const ui8 layerId = GetCore().GetLayerId(layerName, Max<ui8>());
                    if (Y_UNLIKELY(layerId == Max<ui8>())) {
                        DEBUG_LOG << "Unknown geo layer " << layerName << " in incoming message" << Endl;
                        continue;
                    }
                    Y_ENSURE(layer.GeoDocSize() == 1, "no GeoObject in a layer");

                    FromProto(layer.GetGeoDoc(0), layerId, record);
                    hasData = true;
                }
            } else {
                // coordinates may be given as a text string in CoordsProp (legacy option)
                for (const auto& property : document.GetDocumentProperties()) {
                    if (property.GetName() == CoordsProp) {
                        FromText(property.GetValue(), record);
                        hasData = true;
                        break;
                    }
                }
            }

            if (hasData) {
                TBufferOutput data;
                Save(data, record);

                TGeoTransientEntity* entity = GetComponentEntity(context); // may be null
                if (entity) {
                    WriteRawData(entity, context, TBlob::FromBuffer(data.Buffer()));

                    ui64 keyPrefix = document.HasKeyPrefix() ? document.GetKeyPrefix() : 0ull;
                    MakeBuilderInvData(*entity, record, keyPrefix);
                }
            }
        }

    protected:
        TGeoTransientEntity* GetComponentEntity(const TParsingContext& context) const {
            return context.Result.GetComponentEntity<TGeoTransientEntity>(ComponentName);
        };

        static void MakeBuilderInvData(TGeoTransientEntity& entity, const TRecord& record, ui64 keyPrefix) {
            entity.BuilderExtension = MakeBuilderInvData(record, keyPrefix);
        }

        TMinGeoSearcherCore& GetCore() const {
            return *CheckedCast<TMinGeoSearcherCore*>(TL2ComponentParserBase::GetCore());
        }

    public:
        static TGeoTransientEntity::TBuilderInvData MakeBuilderInvData(const TRecord& record, ui64 keyPrefix);

        static void Save(IOutputStream& s, const TRecord& record);

        static void Load(IInputStream& s, TRecord& record);

        static void Load(const TBlob& b, TRecord& record);

        static bool Load(IInputStream& s, ui8 layer, TObject& target);

        static bool Load(const TBlob& b, ui8 layer, TObject& target);

        static void FromProto(const TMessage::TGeoObject& proto, ui8 layerId, TRecord& rec);

        static void FromText(const TString& text, TRecord& rec);

        static bool TryLoad(const TBlob& serializedRec, TRecord& record) {
            Y_ASSERT(serializedRec.IsNull() || !serializedRec.Empty());
            if (serializedRec.IsNull() || serializedRec.Empty())
                return false;
            TMinGeoParser::Load(serializedRec, record); // throws
            return true;
        }

        static bool TryLoad(const TBlob& serializedRec, ui8 layer, TObject& object) {
            Y_ASSERT(serializedRec.IsNull() || !serializedRec.Empty());
            if (serializedRec.IsNull() || serializedRec.Empty())
                return false;
            return TMinGeoParser::Load(serializedRec, layer, object); // throws
        }

    public:
        static constexpr float InfiniteDistance = 1.0e9f; // 1e9 is better than Max<float> as value for factor
    };

    class TMinGeoIndexData;

    class TMinGeoSearcher: public TL2ComponentDiskManagerBase, public ISearchFilter {
    private:
        THolder<TMinGeoIndexData> IndexData;
        const TFsPath IndexDir;

    public:
        TMinGeoSearcher(const TString& componentName, const TString& indexDir, const TL2DocStorageParams&, IL2ComponentCore::TPtr);

        ~TMinGeoSearcher();

    private:
        bool GetGeoObject(ui32 docId, i64 layerId, TMinGeoParser::TObject& object) const {
            Y_ENSURE(layerId >= Min<ui8>() && layerId < Max<ui8>());
            ui8 layer = static_cast<ui8>(layerId);
            TBlob serializedRec = FetchRawL2Data(docId);
            return TMinGeoParser::TryLoad(serializedRec, layer, object);
        }

    protected:
        template <typename TAggregator>
        Y_FORCE_INLINE float AggregateImpl(ui32 docId, i64 layerId, float dummy, TAggregator aggregator) const {
            TMinGeoParser::TObject object;
            if (Y_LIKELY(GetGeoObject(docId, layerId, object))) {
                return aggregator(object, dummy);
            } else {
                return dummy;
            }
        }

    public:
        // IIndexComponentManager
        // ----------------------
        virtual bool DoOpen() override;

        virtual bool DoClose() override;

        // ExportedFunctions
        // -----------------
        template <typename TFunc, typename TComp>
        static float PointAggregate(const TMinGeoParser::TObject& object, float dummy, TFunc func, TComp comp) {
            float value = dummy;
            if (Y_LIKELY(object.Kind != NRTYGeo::EGeoObjectType::MultiRect)) {
                if (object.GeoPoints.empty()) {
                    return value;
                }
                value = func(object.GeoPoints[0]);
                for (size_t i = 1; i < object.GeoPoints.size(); ++i) {
                    float curValue = func(object.GeoPoints[i]);
                    if (comp(value, curValue)) {
                        value = curValue;
                    }
                }
            } else {
                if (object.GeoPoints.size() < 2) {
                    return value;
                }
                {
                    const TRawPoint& lower = object.GeoPoints[0];
                    const TRawPoint& upper = object.GeoPoints[1];
                    TRawPoint center((lower.first + upper.first) / 2, (lower.second + upper.second) / 2);
                    value = func(center);
                }
                for (size_t i = 2, end = object.GeoPoints.size() / 2 * 2; i < end; i += 2) {
                    const TRawPoint& lower = object.GeoPoints[i];
                    const TRawPoint& upper = object.GeoPoints[i + 1];
                    TRawPoint center((lower.first + upper.first) / 2, (lower.second + upper.second) / 2);
                    float curValue = func(center);
                    if (comp(value, curValue)) {
                        value = curValue;
                    }
                }
            }
            return value;
        }

        template <typename TFunc>
        float MinDistanceToTmpl(ui32 docId, i64 layerId, TFunc func) const {
            return AggregateImpl(docId, layerId, TMinGeoParser::InfiniteDistance, [&](const TMinGeoParser::TObject& object, float dummy) {
                return PointAggregate(object, dummy, func, [](float minValue, float value) { return minValue > value; });
            });
        };


        //FIXME(SAAS-5726): the 'min_distance_to' function is deprecated, remove it
        float MinDistanceTo(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 2); // this was checked before (@see TFactorCalcerFunc::Validate())
            TRawPoint q(args[0], args[1]);

            // This function simply uses std::hypotf
            return MinDistanceToTmpl(docId, layerId, [q](const TRawPoint& point) {
                return std::hypotf(point.first - q.first, point.second - q.second);
            });
        };

        static float NormalizeLongitudeForQuery(float q, float p) {
            constexpr float HALF_WORLD = NGeo::WORLD_WIDTH / 2;
            Y_ASSERT(fabs(q - p) <= 3 * NGeo::WORLD_WIDTH);
            while (p < q - HALF_WORLD)
                p += NGeo::WORLD_WIDTH;
            while (p >= q + HALF_WORLD)
                p -= NGeo::WORLD_WIDTH;
            Y_ASSERT(fabs(q - p) <= HALF_WORLD);
            return p;
        }

        float MinGeoDistanceTo(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 2); // this was checked before (@see TFactorCalcerFunc::Validate())
            TRawPoint q(args[0], args[1]);

            // This function utilizes NGeo::TGeoPoint::Distance(), which is fast and has no asimutal bias.
            // The result is calculated using an Equirectangular projection, in which lat0:=avg(lat1, lat2) is taken as the "standard" parallel.
            // To covert the result to meters: metricDistance = deg2rad(geoPointDistance) * WGS84::R
            return MinDistanceToTmpl(docId, layerId, [q](const TRawPoint& p) {
                constexpr float HALF_WORLD = NGeo::WORLD_WIDTH / 2;
                float pLon = p.first;
                if (Y_UNLIKELY(fabs(q.first - pLon) > HALF_WORLD))
                    pLon = NormalizeLongitudeForQuery(q.first, pLon);
                return NGeo::TGeoPoint(/*lon=*/pLon, /*lat=*/p.second).Distance(NGeo::TGeoPoint(q.first, q.second));
            });
        }

        float MinSphericalDistanceTo(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 2); // this was checked before (@see TFactorCalcerFunc::Validate())
            TRawPoint q(args[0], args[1]);

            // This function uses the precise formula from spherical geometry
            return MinDistanceToTmpl(docId, layerId, [q](const TRawPoint& p) {
                using namespace NGeolocationFeatures; //FIXME: move CalcDistance from library/cpp/geolocation to library/geo
                return CalcDistance(/*lat1=*/p.second, /*lon1=*/p.first, /*lat2=*/q.second, /*lon2=*/q.first); // precise distance in Km
            });
        }

        float HitTheArea(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 4); // this was checked before (@see TFactorCalcerFunc::Validate())
            NGeo::TGeoWindow qw(NGeo::TGeoPoint(args[0], args[1]), NGeo::TGeoPoint(args[2], args[3]));

            return AggregateImpl(docId, layerId, 0, [&](const TMinGeoParser::TObject& object, float dummy) {
                return PointAggregate(object,
                                      dummy,
                                      [qw](const TRawPoint& p) { return qw.Contains(NGeo::TGeoPoint(p.first, p.second)); },
                                      [](float minValue, float value) { return minValue < value; });
            });
        }

        template <typename TFunc, typename TComp, typename TPost>
        static float RectangleAggregate(const TMinGeoParser::TObject& object, float dummy, TFunc func, TComp comp, TPost post) {
            float value = dummy;
            NGeo::TGeoWindow valueWindow;
            if (Y_LIKELY(object.Kind == NRTYGeo::EGeoObjectType::MultiRect)) {
                if (object.GeoPoints.size() < 2) {
                    return value;
                }
                {
                    const TRawPoint& lower = object.GeoPoints[0];
                    const TRawPoint& upper = object.GeoPoints[1];
                    valueWindow = NGeo::TGeoWindow(NGeo::TGeoPoint(lower.first, lower.second), NGeo::TGeoPoint(upper.first, upper.second));
                    value = func(valueWindow);
                }
                for (size_t i = 2, end = object.GeoPoints.size() / 2 * 2; i < end; i += 2) {
                    const TRawPoint& lower = object.GeoPoints[i];
                    const TRawPoint& upper = object.GeoPoints[i + 1];
                    NGeo::TGeoWindow window(NGeo::TGeoPoint(lower.first, lower.second), NGeo::TGeoPoint(upper.first, upper.second));
                    float curValue = func(window);
                    if (comp(value, curValue)) {
                        value = curValue;
                        valueWindow = std::move(window);
                    }
                }
                value = post(value, valueWindow);
            }
            return value;
        }

        template <typename TFunc, typename TComp, typename TPost>
        float RectangleAggregateToImpl(ui32 docId, i64 layerId, float dummy, TFunc func, TComp comp, TPost post) const {
            return AggregateImpl(docId, layerId, dummy, [&](const TMinGeoParser::TObject& object, float dummy) {
                return RectangleAggregate(object, dummy, func, comp, post);
            });
        }

        float MaxIntersectionTo(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 4); // this was checked before (@see TFactorCalcerFunc::Validate())
            NGeo::TGeoWindow qw(NGeo::TGeoPoint(args[0], args[1]), NGeo::TGeoPoint(args[2], args[3]));

            return RectangleAggregateToImpl(docId, layerId, 0,
                [qw](const NGeo::TGeoWindow& window) {
                    if (auto&& intersection = NGeo::Intersection(window, qw)) {
                        return intersection->Area();
                    }
                    return 0.;
                },
                [](float value, float curValue) {
                    return value < curValue;
                },
                [](float value, const NGeo::TGeoWindow&) {
                    return value;
                }
            );
        }

        float MaxRelativeIntersectionTo(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 4); // this was checked before (@see TFactorCalcerFunc::Validate())
            NGeo::TGeoWindow qw(NGeo::TGeoPoint(args[0], args[1]), NGeo::TGeoPoint(args[2], args[3]));

            return RectangleAggregateToImpl(docId, layerId, 0,
                [qw](const NGeo::TGeoWindow& window) {
                    if (auto&& intersection = NGeo::Intersection(window, qw)) {
                        return intersection->Area();
                    }
                    return 0.;
                },
                [](float value, float curValue) {
                    return value < curValue;
                },
                [](float value, const NGeo::TGeoWindow& window) {
                    return value / window.Area();
                }
            );
        }

        float MinRectangleGeoDistanceTo(ui32 docId, i64 layerId, TArrayRef<const float> args, const TRTYFunctionCtx&) const {
            Y_ASSERT(args.size() == 4); // this was checked before (@see TFactorCalcerFunc::Validate())
            NGeo::TGeoWindow qw(NGeo::TGeoPoint(args[0], args[1]), NGeo::TGeoPoint(args[2], args[3]));

            return RectangleAggregateToImpl(docId, layerId, TMinGeoParser::InfiniteDistance,
                [qw](const NGeo::TGeoWindow& window) {
                    return qw.Distance(window);
                },
                [](float value, float curValue) {
                    return value > curValue;
                },
                [](float value, const NGeo::TGeoWindow&) {
                    return value;
                }
            );
        }

        virtual void GetExportedFunctions(NRTYFeatures::TImportedFunctionsBuilder& exports) const override {
            using namespace NRTYFeatures;
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::MinDistanceTo, this, "min_distance_to", 2);
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::MinGeoDistanceTo, this, "min_geo_dist_to", 2);
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::MinSphericalDistanceTo, this, "min_sph_dist_to", 2);
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::HitTheArea, this, "hit_the_area", 4);
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::MaxIntersectionTo, this, "max_intersection_to", 4);
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::MaxRelativeIntersectionTo, this, "max_relintersection_to", 4);
            exports.Add<TFactorCalcerLayeredFunc>(&TMinGeoSearcher::MinRectangleGeoDistanceTo, this, "min_rectangle_geo_dist_to", 4);
        }

        // ISearchFilter
        // -------------

        // @brief creates TMinGeoSearchContext
        virtual TComponentSearcher::TPtr CreateSpecialSearcher() const override;
    };

    class TMinGeoBuilder;
    class TMinGeoInvNormalizer;

    class TMinGeoCore: public TMinGeoSearcherCore {
    public:
        using TDiskManager = TMinGeoSearcher;
        using TParser = TMinGeoParser;
        using TBuilder = TMinGeoBuilder;
        using TDocEntity = TGeoTransientEntity;
        using TNormalizer = TMinGeoInvNormalizer;

    public:
        virtual TStringBuf GetName() const override {
            return MinGeoComponentName;
        }
    };
}
