#include "mingeo_core.h"
#include "mingeo_search.h"

#include <saas/api/geo.h>
#include <saas/rtyserver/config/config.h>
#include <saas/rtyserver/config/searcher_config.h>
#include <library/cpp/string_utils/scan/scan.h>
#include <util/string/cast.h>
#include <util/ysaveload.h>

namespace NRTYServer {
    void TMinGeoSearcherCore::Init(const TRTYServerConfig& config) {
        if (config.GetSearcherConfig().Factors) {
            const auto& confList = config.GetSearcherConfig().Factors->GetGeoLayers();
            for (const NRTYFactors::TSimpleFactorDescription& layer: confList) {
                CHECK_WITH_LOG(layer.IndexGlobal < Max<ui8>());
                LayerNames[layer.Name] = layer.IndexGlobal;
            }
        }
    }

    void TMinGeoParser::Save(IOutputStream& s, const TRecord& record) {
        ::Save<ui8>(&s, 1u); // version
        TVector<size_t> order(record.Layers.size());
        std::iota(order.begin(), order.end(), 0);
        SortBy(order, [&record](const size_t idx) {
            return record.Layers[idx].LayerId;
        });
        for (size_t idx: order) {
            const NRTYGeo::TGeoLayer& layer = record.Layers[idx];
            Y_ASSERT(layer.LayerId != Max<ui8>());
            ::Save<ui8>(&s, layer.LayerId); // layerid
            ::Save<ui8>(&s, (ui8)layer.Object.Kind);
            ::Save(&s, layer.Object.GeoPoints);
        }
        ::Save<ui8>(&s, Max<ui8>()); //  endmark
    }

    void TMinGeoParser::Load(IInputStream& s, TRecord& record) {
        ui8 ver, tag;
        TRY
            ::Load<ui8>(&s, ver);
            if (ver == 0) {
                record.Layers.emplace_back();
                NRTYGeo::TGeoLayer& layer = record.Layers.back();
                layer.LayerId = 0;
                layer.Object.Kind = NRTYGeo::EGeoObjectType::MultiPoint;
                ::Load(&s, layer.Object.GeoPoints);
            } else {
                Y_ENSURE(ver <= 1, "Unsupported version of MinGeo data");
                for (;;) {
                    ::Load<ui8>(&s, tag);
                    if (tag == Max<ui8>())
                        break;

                    record.Layers.emplace_back();
                    NRTYGeo::TGeoLayer& layer = record.Layers.back();
                    layer.LayerId = tag;
                    ::Load<ui8>(&s, tag); // kind
                    layer.Object.Kind = (NRTYGeo::EGeoObjectType)tag;
                    ::Load(&s, layer.Object.GeoPoints);
                }
            }
        CATCH_AND_RETHROW("MinGeoParser::Load()");
    }

    bool TMinGeoParser::Load(IInputStream& s, ui8 layer, TObject& target) {
        ui8 ver, tag;
        TRY
            ::Load<ui8>(&s, ver);
            if (ver == 0) {
                if (layer != 0)
                    return false;
                target.Kind = NRTYGeo::EGeoObjectType::MultiPoint;
                ::Load(&s, target.GeoPoints);
                return true;
            } else {
                Y_ENSURE(ver <= 1, "Unsupported version of MinGeo data");
                for (;;) {
                    ::Load<ui8>(&s, tag);
                    if (tag == Max<ui8>())
                        return false;

                    if (layer != tag) {
                        //Skip array
                        ::Load<ui8>(&s, tag); // kind
                        size_t sz = LoadSize(&s) * (sizeof(TRawPoint::first) + sizeof(TRawPoint::second));
                        size_t nSkipped = s.Skip(sz);
                        if (nSkipped < sz) {
                            ythrow yexception() << "unexpected end of stream";
                        }
                        continue;
                    }

                    ::Load<ui8>(&s, tag); // kind
                    target.Kind = (NRTYGeo::EGeoObjectType)tag;
                    ::Load(&s, target.GeoPoints);
                    return true;
                }
            }
        CATCH_AND_RETHROW("MinGeoParser::Load()");
    }

    void TMinGeoParser::Load(const TBlob& b, TRecord& record) {
        TMemoryInput inp(b.Data(), b.Size());
        Load(inp, record);
    }

    bool TMinGeoParser::Load(const TBlob& b, ui8 layer, TObject& target) {
        TMemoryInput inp(b.Data(), b.Size());
        return Load(inp, layer, target);
    }

    void TMinGeoParser::FromText(const TString& s, TRecord& record) {
        record.Layers.emplace_back();
        auto& layer = record.Layers.back();
        layer.LayerId = 0;

        auto parsePoint = [&layer](TStringBuf lon, TStringBuf lat) {
            layer.Object.GeoPoints.emplace_back(FromString<float>(lon), FromString<float>(lat));
        };
        ScanKeyValue<false,';',':'>(s, parsePoint);
    }

    namespace {
        inline NRTYGeo::EGeoObjectType GetInvHitType(NSaas::EGeoObjectKind objectKind) {
            using NRTYGeo::EGeoObjectType;
            using NSaas::EGeoObjectKind;
            switch (objectKind) {
                case EGeoObjectKind::PointSet: return EGeoObjectType::MultiPoint;
                case EGeoObjectKind::RectSet: return EGeoObjectType::MultiRect;
                default: return EGeoObjectType::Void;
            }
        }
    }

    void TMinGeoParser::FromProto(const TMessage::TGeoObject& proto, ui8 layerId, TRecord& record) {
        NSaas::EGeoObjectKind kind;
        TRY
            kind = FromString<NSaas::EGeoObjectKind>(proto.GetType());
        CATCH_AND_RETHROW("TMinGeoParser::FromProto");

        record.Layers.emplace_back();
        auto& layer = record.Layers.back();
        layer.LayerId = layerId;
        layer.Object.Kind = GetInvHitType(kind);

        const size_t coordsEnd = proto.DataSize() / 2 * 2;
        Y_ENSURE(coordsEnd == proto.DataSize(), "incorrect DataSize in TGeoObject");
        for (size_t i = 0; i < coordsEnd; i += 2) {
            layer.Object.GeoPoints.emplace_back(proto.GetData(i), proto.GetData(i + 1));
        }
    }

    TGeoTransientEntity::TGeoTransientEntity(IParsedEntity::TConstructParams& params)
        : TRTYFullArchiveLightEntity(params) { }

    TGeoTransientEntity::TBuilderInvData TMinGeoParser::MakeBuilderInvData(const TRecord& layeredRecord, ui64 keyPrefix) {
        constexpr ui8 indexedLayerId = 0; //TODO(SAAS-5949): include all layers in TBuilderInvData

        auto p = std::find_if(layeredRecord.Layers.begin(), layeredRecord.Layers.end(), [](const NRTYGeo::TGeoLayer& l) -> bool {
            return l.LayerId == indexedLayerId;
        });

        if (p == layeredRecord.Layers.end())
            return TGeoTransientEntity::TBuilderInvData();

        const TObject& record = p->Object;

        if (record.GeoPoints.empty()) {
            return TGeoTransientEntity::TBuilderInvData();
        }

        if (record.Kind == NRTYGeo::EGeoObjectType::MultiPoint) {
            TGeoTransientEntity::TBuilderInvData result;
            result.KeyPrefix = keyPrefix;
            for (auto i = record.GeoPoints.begin(); i != record.GeoPoints.end(); i++) {
                NGeo::TGeoPoint point(i->first, i->second);
                result.Keys.push_back({point, point});
            }
            return result;
        } else if (record.Kind == NRTYGeo::EGeoObjectType::MultiRect) {
            TGeoTransientEntity::TBuilderInvData result;
            result.KeyPrefix = keyPrefix;
            auto i = record.GeoPoints.begin();
            auto iEnd = record.GeoPoints.end();
            if (record.GeoPoints.size() % 2 != 0) {
                iEnd = std::prev(iEnd);
            }

            for (; i != iEnd;) {
                const auto& lower = *i++;
                const auto& upper = *i++;
                NGeo::TGeoWindow w(NGeo::TGeoPoint(lower.first, lower.second), NGeo::TGeoPoint(upper.first, upper.second));
                result.Keys.push_back({w.GetLowerLeftCorner(), w.GetUpperRightCorner()});
            }
            return result;
        } else if (record.Kind == NRTYGeo::EGeoObjectType::SingleCompactObject) {
            Y_ASSERT(0); // This branch is not used now

            // Transform the "mesh" into a window
            std::pair<float, float> lower;
            std::pair<float, float> upper;

            auto i = record.GeoPoints.begin();
            lower = *i;
            upper = *i;
            for (++i; i != record.GeoPoints.end(); ++i) {
                if (lower.first > i->first)
                    lower.first = i->first;
                if (upper.first < i->first)
                    upper.first = i->first;
                if (lower.second > i->second)
                    lower.second = i->second;
                if (upper.second > i->second)
                    upper.second = i->second;
            }

            return TGeoTransientEntity::TBuilderInvData{
                {{NGeo::TGeoPoint(lower.first, lower.second), NGeo::TGeoPoint(upper.first, upper.second)}}, 
                keyPrefix
            };
        } else {
            Y_ASSERT(record.Kind == NRTYGeo::EGeoObjectType::Void);
            return TGeoTransientEntity::TBuilderInvData{{}, keyPrefix};
        }
    }

    //
    // TMinGeoSearcher
    //
    TMinGeoSearcher::TMinGeoSearcher(const TString& componentName, const TString& indexDir, const TL2DocStorageParams& l2params, IL2ComponentCore::TPtr core)
        : TL2ComponentDiskManagerBase(componentName, l2params, core)
        , IndexDir(indexDir)
    {
    }

    TMinGeoSearcher::~TMinGeoSearcher() = default;

    bool TMinGeoSearcher::DoOpen() {
        IndexData = MakeHolder<TMinGeoIndexData>(IndexDir, static_cast<IL2RawAccessor&>(*this));
        try {
            IndexData->Open();
            return true;
        } catch (...) {
            // we need to produce a "not opened" manager here for TFinalIndexNormalizer, which will Fix() our index then
            ERROR_LOG << "Failed to open IndexData in TMinGeoSearcher: " << CurrentExceptionMessage() << Endl;
            return false;
        }
    }

    bool TMinGeoSearcher::DoClose() {
        IndexData.Destroy();
        return true;
    }

    TComponentSearcher::TPtr TMinGeoSearcher::CreateSpecialSearcher() const {
        CHECK_WITH_LOG(IndexData); // should be opened
        return MakeIntrusive<TMinGeoSearchContext>(*IndexData);
    }
}
