#include "pipeline.h"
#include "common.h"

#include <maps/wikimap/mapspro/services/mrc/libs/carsegm/include/carsegm.h>
#include <maps/wikimap/mapspro/services/mrc/libs/house_number_sign_detector/include/house_number_sign_detector.h>
#include <maps/wikimap/mapspro/services/mrc/libs/addr_pt_searcher/include/addr_pt_searcher.h>
#include <maps/wikimap/mapspro/services/mrc/libs/object/include/revision_loader.h>

#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pgpool3_helpers.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/revisionsgateway.h>

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/json/include/builder.h>

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

#include <mapreduce/yt/util/temp_table.h>

#include <util/charset/utf8.h>
#include <util/charset/wide.h>
#include <library/cpp/deprecated/atomic/atomic.h>
#include <util/thread/pool.h>
#include <util/thread/lfqueue.h>

#include <library/cpp/string_utils/base64/base64.h>

#include <opencv2/opencv.hpp>

#include <sstream>
#include <string>
#include <thread>

namespace maps::mrc::house_number_pipeline {

namespace {

constexpr size_t MiB = 1024 * 1024;
constexpr size_t GiB = 1024 * MiB;

NYT::TNode createOperationSpec(const TString& operationType, bool useGpu) {
    NYT::TNode operationSpec = NYT::TNode::CreateMap()
        ("title", "House number (test pipline)")
        ("scheduling_tag_filter", "!testing");

    const TString GPU_POOL_TREES = "gpu_geforce_1080ti";

    if (useGpu) {
        operationSpec
        ("pool_trees", NYT::TNode::CreateList().Add(GPU_POOL_TREES))
        ("scheduling_options_per_pool_tree", NYT::TNode::CreateMap()
            (GPU_POOL_TREES, NYT::TNode::CreateMap()("pool", "research_gpu"))
        )
        (operationType, NYT::TNode::CreateMap()
            ("memory_limit", 16 * GiB)
            ("gpu_limit", 1)
            ("layer_paths",  NYT::TNode::CreateList()
                            .Add("//porto_layers/delta/gpu/cuda/10.1")
                            .Add("//porto_layers/delta/gpu/driver/418.67")
                            .Add("//porto_layers/base/bionic/porto_layer_search_ubuntu_bionic_app_lastest.tar.gz")
            )
        );
    } else {
        operationSpec
        (operationType, NYT::TNode::CreateMap()
            ("cpu_limit", 4)
            ("memory_limit", 16 * GiB)
            ("memory_reserve_factor", 0.6)
        );
    }
    return operationSpec;
}

std::set<char16_t> getRecognizerSupportedSymbols() {
    const std::vector<char16_t> symbols = house_number_sign_detector::makeHouseNumberRecognizer().getSupportedSymbols();
    return{ symbols.begin(), symbols.end() };
}

bool compareNumbers(const TString& str1, const TString& str2, const std::set<char16_t>& supportedSymbols) {
    if (supportedSymbols.empty())
        return (str1 == str2);
    if (str1.empty())
        return str2.empty();
    if (str2.empty())
        return false;
    TUtf16String s1 = UTF8ToWide(str1);
    TUtf16String s2 = UTF8ToWide(str2);
    size_t idx1 = 0;
    size_t idx2 = 0;
    for (;idx1 < s1.size() && idx2 < s2.size();) {
        if (0 == supportedSymbols.count(s1[idx1])) {
            idx1++;
            continue;
        }
        if (0 == supportedSymbols.count(s2[idx2])) {
            idx2++;
            continue;
        }
        if (s1[idx1] != s2[idx2]) {
            return false;
        }
        idx1++;
        idx2++;
    }
    for (;idx1 < s1.size() && 0 == supportedSymbols.count(s1[idx1]); idx1++) {
        ;
    }
    for (;idx2 < s2.size() && 0 == supportedSymbols.count(s2[idx2]); idx2++) {
        ;
    }
    return (idx1 == s1.size() && idx2 == s2.size());
}

TString removeUnsupportedSymbols(const TString& str, const std::set<char16_t>& supportedSymbols) {
    if (supportedSymbols.empty()) {
        return str;
    }
    const TUtf16String strUtf16 = UTF8ToWide(str);
    TUtf16String result;
    for (size_t idx = 0; idx < strUtf16.size(); idx++) {
        if (0 == supportedSymbols.count(strUtf16[idx]))
            continue;
        result += strUtf16[idx];
    }
    return WideToUTF8(result);
}

cv::Mat decodeImage(const TString& encimageStr) {
    std::vector<std::uint8_t> encimage(Base64DecodeBufSize(encimageStr.length()));
    size_t encimageSize = Base64Decode(encimage.data(), encimageStr.begin(), encimageStr.end());
    encimage.resize(encimageSize);
    return cv::imdecode(encimage, cv::IMREAD_COLOR + cv::IMREAD_IGNORE_ORIENTATION);
}

cv::Rect NodeToRect(const NYT::TNode& node) {
    cv::Rect rc;
    const TVector<NYT::TNode>& rcNode = node.AsList();
    const int64_t x1 = rcNode[0].AsList()[0].AsInt64();
    const int64_t y1 = rcNode[0].AsList()[1].AsInt64();

    const int64_t x2 = rcNode[1].AsList()[0].AsInt64();
    const int64_t y2 = rcNode[1].AsList()[1].AsInt64();

    rc.x = std::min((int)x1, (int)x2);
    rc.y = std::min((int)y1, (int)y2);
    rc.width = (int)x2 - (int)x1;
    rc.height = (int)y2 - (int)y1;
    return rc;
}

NYT::TNode rectangleToTNode(const cv::Rect& rc) {
    return NYT::TNode::CreateList()
        .Add(NYT::TNode::CreateList().Add(rc.tl().x).Add(rc.tl().y))
        .Add(NYT::TNode::CreateList().Add(rc.br().x).Add(rc.br().y));
}

struct NamedRect {
    NamedRect() = default;
    NamedRect(const cv::Rect& _bbox, const TString& _name)
        : bbox(_bbox), name(_name) {}
    cv::Rect bbox;
    TString name;
};

std::vector<NamedRect> extractGroundTruthNamedRects(const NYT::TNode& node) {
    std::vector<NamedRect> result;
    const TVector<NYT::TNode>& objectList = node.AsList();
    for (const NYT::TNode& objectNode : objectList) {
        result.emplace_back(NodeToRect(objectNode[ITEM_NAME_BBOX]), objectNode[ITEM_NAME_VISIBLE_NUMBER].AsString());
    }
    return result;
}

std::vector<NamedRect> extractTestNamedRects(const NYT::TNode& node, double minConfidenceDetector, double minConfidenceRecognizer) {
    std::vector<NamedRect> result;
    const TVector<NYT::TNode>& objectList = node.AsList();
    for (const NYT::TNode& objectNode : objectList) {
        const double confidenceDetector = objectNode[ITEM_NAME_CONFIDENCE_DETECTOR].AsDouble();
        if (minConfidenceDetector >= confidenceDetector)
            continue;
        const double confidenceRecognizer = objectNode[ITEM_NAME_CONFIDENCE_RECOGNIZER].AsDouble();
        if (minConfidenceRecognizer >= confidenceRecognizer)
            continue;
        result.emplace_back(NodeToRect(objectNode[ITEM_NAME_BBOX]), objectNode[ITEM_NAME_NUM].AsString());
    }
    return result;
}

Statistic calculateDetectorRecognizerStatistic(
    const std::vector<NamedRect>& gtObjects,
    std::vector<NamedRect>&& tstObjects,
    double iouThreshold,
    bool useRecognizedNumber,
    const std::set<char16_t>& recognizerSupportedSymbols)
{
    size_t truePositives = 0;
    for (size_t gtIdx = 0; gtIdx < gtObjects.size(); gtIdx++) {
        const NamedRect& gtObject = gtObjects[gtIdx];
        double maxIoU = 0.;
        size_t maxIoUtstIdx = -1;
        for (size_t tstIdx = 0; tstIdx < tstObjects.size(); tstIdx++) {
            const NamedRect& tstObject = tstObjects[tstIdx];
            if (useRecognizedNumber && !compareNumbers(tstObject.name, gtObject.name, recognizerSupportedSymbols))
                continue;
            const int intersectionArea = (gtObject.bbox & tstObject.bbox).area();
            const int unionArea = gtObject.bbox.area() + tstObject.bbox.area() - intersectionArea;
            if (0 == unionArea)
                continue;
            const double iou = (double)intersectionArea / (double)unionArea;
            if (iou > maxIoU) {
                maxIoU = iou;
                maxIoUtstIdx = tstIdx;
            }
        }
        if ((0 <= maxIoUtstIdx) && (iouThreshold < maxIoU)) {
            truePositives++;
            tstObjects.erase(tstObjects.begin() + maxIoUtstIdx);
            if (tstObjects.empty())
                break;
        }
    }
    Statistic result;
    result.truePositives  = truePositives;
    result.falsePositives = tstObjects.size();
    result.falseNegatives = gtObjects.size() - truePositives;
    return result;
}

Statistic calculateDetectorRecognizerStatistic(
    const std::vector<std::pair<std::vector<NamedRect>, NYT::TNode>>& dataVector,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double iouThreshold,
    bool useRecognizedNumber,
    const std::set<char16_t>& recognizerSupportedSymbols)
{
    Statistic statistic;
    statistic.truePositives  = 0;
    statistic.falsePositives = 0;
    statistic.falseNegatives = 0;
    for (size_t i = 0; i < dataVector.size(); i++) {
        statistic +=
            calculateDetectorRecognizerStatistic(
                dataVector[i].first,
                extractTestNamedRects(dataVector[i].second, minConfidenceDetector, minConfidenceRecognizer),
                iouThreshold,
                useRecognizedNumber,
                recognizerSupportedSymbols);
    }
    return statistic;
}

Statistic calculateRecognizerStatistic(
    const std::vector<NamedRect>& gtObjects,
    std::vector<NamedRect>&& tstObjects,
    const std::set<char16_t>& recognizerSupportedSymbols)
{
    Statistic result;
    result.truePositives = 0;
    result.falsePositives = 0;
    for (size_t gtIdx = 0; gtIdx < gtObjects.size(); gtIdx++) {
        const NamedRect& gtObject = gtObjects[gtIdx];
        for (size_t tstIdx = 0; tstIdx < tstObjects.size(); tstIdx++) {
            const NamedRect& tstObject = tstObjects[tstIdx];
            if (gtObject.bbox == tstObject.bbox) {
                if (compareNumbers(tstObject.name, gtObject.name, recognizerSupportedSymbols)) {
                    result.truePositives++;
                } else {
                    result.falsePositives++;
                }
                tstObjects.erase(tstObjects.begin() + tstIdx);
                break;
            }
        }
    }
    result.falseNegatives = gtObjects.size() - result.truePositives - result.falsePositives;
    return result;
}

// Address Points
std::set<addr_pt_searcher::AddressPointCandidate> extractGroundTruthAddressPointCandidates(
    const NYT::TNode& node,
    const geolib3::Point2& mercatorPos,
    const std::set<char16_t>& recognizerSupportedSymbols)
{
    std::set<addr_pt_searcher::AddressPointCandidate> results;
    const TVector<NYT::TNode>& objectList = node.AsList();
    for (const NYT::TNode& objectNode : objectList) {
        const bool addrPtExists =
            objectNode.HasKey(ITEM_NAME_ADDR_NUMBER) &&
            objectNode.HasKey(ITEM_NAME_ADDR_PT_LON) &&
            objectNode.HasKey(ITEM_NAME_ADDR_PT_LAT);
        if (addrPtExists) {
            const geolib3::Point2 addrPtMercatorPos =
                geolib3::convertGeodeticToMercator(
                    geolib3::Point2(
                        objectNode[ITEM_NAME_ADDR_PT_LON].AsDouble(),
                        objectNode[ITEM_NAME_ADDR_PT_LAT].AsDouble()
                    )
                );
            results.insert({ addrPtMercatorPos, objectNode[ITEM_NAME_ADDR_NUMBER].AsString(), 1.0 });
        } else {
            results.insert(
            {
                mercatorPos,
                removeUnsupportedSymbols(objectNode[ITEM_NAME_VISIBLE_NUMBER].AsString(), recognizerSupportedSymbols),
                1.0
            }
            );
        }
    }
    return results;
}

std::set<addr_pt_searcher::AddressPointCandidate> extractTestAddressPointCandidates(
    const NYT::TNode& nodeAddrPts,
    const NYT::TNode& nodeHypotheses,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double minConfidenceAddrPoint)
{
    std::set<addr_pt_searcher::AddressPointCandidate> results;
    const TVector<NYT::TNode>& nodeListAddrPts = nodeAddrPts.AsList();
    for (const NYT::TNode& objectNode : nodeListAddrPts) {
        if (minConfidenceDetector < objectNode[ITEM_NAME_CONFIDENCE_DETECTOR].AsDouble() &&
            minConfidenceRecognizer < objectNode[ITEM_NAME_CONFIDENCE_RECOGNIZER].AsDouble() &&
            minConfidenceAddrPoint < objectNode[ITEM_NAME_CONFIDENCE].AsDouble()) {
            results.insert(
            {
                {
                    objectNode[ITEM_NAME_ADDR_PT_X].AsDouble(),
                    objectNode[ITEM_NAME_ADDR_PT_Y].AsDouble()
                },
                objectNode[ITEM_NAME_ADDR_NUMBER].AsString(),
                objectNode[ITEM_NAME_CONFIDENCE].AsDouble()
            });
        }
    }

    const TVector<NYT::TNode>& nodeListHypotheses = nodeHypotheses.AsList();
    for (const NYT::TNode& objectNode : nodeListHypotheses) {
        if (minConfidenceDetector < objectNode[ITEM_NAME_CONFIDENCE_DETECTOR].AsDouble() &&
            minConfidenceRecognizer < objectNode[ITEM_NAME_CONFIDENCE_RECOGNIZER].AsDouble() &&
            minConfidenceAddrPoint >= objectNode[ITEM_NAME_CONFIDENCE].AsDouble()) {
            results.insert(
            {
                {
                    objectNode[ITEM_NAME_ADDR_PT_X].AsDouble(),
                    objectNode[ITEM_NAME_ADDR_PT_Y].AsDouble()
                },
                objectNode[ITEM_NAME_ADDR_NUMBER].AsString(),
                objectNode[ITEM_NAME_CONFIDENCE].AsDouble()
            });
        }
    }
    return results;
}

Statistic calculateAddressPointStatistic(
    const std::set<addr_pt_searcher::AddressPointCandidate>& gtObjects,
    const std::set<addr_pt_searcher::AddressPointCandidate>& tstObjects)
{
    constexpr double EPSILON_METERS = 0.1;

    Statistic result;
    result.truePositives = 0;
    for (const addr_pt_searcher::AddressPointCandidate& gtObject : gtObjects) {
        const double EPSILON = geolib3::toMercatorUnits(EPSILON_METERS, gtObject.mercatorPos);
        for (const addr_pt_searcher::AddressPointCandidate& tstObject : tstObjects) {
            if (std::abs(gtObject.mercatorPos.x() - tstObject.mercatorPos.x()) < EPSILON &&
                std::abs(gtObject.mercatorPos.y() - tstObject.mercatorPos.y()) < EPSILON &&
                gtObject.number == tstObject.number)
            {
                result.truePositives++;
            }
        }
    }
    result.falsePositives = tstObjects.size() - result.truePositives;
    result.falseNegatives = gtObjects.size() - result.truePositives;
    return result;
}

// Hypotheses
std::set<TString> extractGroundTruthHypotheses(
    const NYT::TNode& node,
    bool removeObjectsAddressPointFromMap,
    const std::set<char16_t>& recognizerSupportedSymbols)
{
    std::set<TString> results;
    const TVector<NYT::TNode>& objectList = node.AsList();
    for (const NYT::TNode& objectNode : objectList) {
        const bool addrPtExists =
            objectNode.HasKey(ITEM_NAME_ADDR_NUMBER) &&
            objectNode.HasKey(ITEM_NAME_ADDR_PT_LON) &&
            objectNode.HasKey(ITEM_NAME_ADDR_PT_LAT);
        if (addrPtExists && !removeObjectsAddressPointFromMap)
            continue;
        results.insert(removeUnsupportedSymbols(objectNode[ITEM_NAME_VISIBLE_NUMBER].AsString(), recognizerSupportedSymbols));
    }
    return results;
}

std::set<TString> extractTestHypotheses(
    const NYT::TNode& node,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double minConfidenceAddrPoint)
{
    std::set<TString> results;
    const TVector<NYT::TNode>& objectList = node.AsList();
    for (const NYT::TNode& objectNode : objectList) {
        if (minConfidenceDetector < objectNode[ITEM_NAME_CONFIDENCE_DETECTOR].AsDouble() &&
            minConfidenceRecognizer < objectNode[ITEM_NAME_CONFIDENCE_RECOGNIZER].AsDouble() &&
            minConfidenceAddrPoint >= objectNode[ITEM_NAME_CONFIDENCE].AsDouble())
        {
            results.insert(objectNode[ITEM_NAME_ADDR_NUMBER].AsString());
        }
    }
    return results;
}

Statistic calculateHypothesesStatistic(
    const std::set<TString>& gtHypotheses,
    const std::set<TString>& tstHypotheses)
{
    Statistic result;
    result.truePositives = 0;
    for (const TString& gtObject : gtHypotheses) {
        for (const TString& tstObject : tstHypotheses) {
            if (gtObject == tstObject) {
                result.truePositives++;
            }
        }
    }
    result.falsePositives = tstHypotheses.size() - result.truePositives;
    result.falseNegatives = gtHypotheses.size() - result.truePositives;
    return result;
}

Statistic calculateHypothesesStatistic(
    const std::vector<std::pair<std::set<TString>, NYT::TNode>>& dataVector,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double minConfidenceAddrPoint)
{
    Statistic statistic;
    statistic.truePositives = 0;
    statistic.falsePositives = 0;
    statistic.falseNegatives = 0;
    for (size_t i = 0; i < dataVector.size(); i++) {
        statistic +=
            calculateHypothesesStatistic(
                dataVector[i].first,
                extractTestHypotheses(dataVector[i].second, minConfidenceDetector, minConfidenceRecognizer, minConfidenceAddrPoint)
            );
    }
    return statistic;
}

// Export detector results
void SaveRectToJson(const cv::Rect& rc, maps::json::ObjectBuilder& builder) {
    builder["bbox"] << [&](maps::json::ArrayBuilder builder) {
        builder << [&](maps::json::ArrayBuilder builder) {
                builder << rc.x;
                builder << rc.y;
            };
        builder << [&](maps::json::ArrayBuilder builder) {
                builder << rc.x + rc.width - 1;
                builder << rc.y + rc.height - 1;
            };
    };
}

void SaveNamedRectToJson(const NamedRect& nrc, maps::json::ArrayBuilder& builder) {
    builder << [&](maps::json::ObjectBuilder builder) {
        builder["type"] = nrc.name;
        SaveRectToJson(nrc.bbox, builder);
    };
}

void SaveNamedRectsToJson(const std::vector<NamedRect>& nrcs, maps::json::ObjectBuilder& builder) {
    builder["objects"] << [&](maps::json::ArrayBuilder builder) {
        for (const NamedRect& nrc: nrcs) {
            SaveNamedRectToJson(nrc, builder);
        };
    };
}

// YT Mappers
class TDetectorMapper
    : public NYT::IMapper<NYT::TTableReader<NYT::TNode>, NYT::TTableWriter<NYT::TNode>>  {
public:
    Y_SAVELOAD_JOB(filterCars_, recognizeNumbers_);

    TDetectorMapper() = default;

    TDetectorMapper(bool filterCars, bool recognizeNumbers)
        : filterCars_(filterCars)
        , recognizeNumbers_(recognizeNumbers)
    { }

    void Do(NYT::TTableReader<NYT::TNode>* reader, NYT::TTableWriter<NYT::TNode>* writer) override {
        INFO() << "Start detection ... ";
        for (; reader->IsValid(); reader->Next()) {
            const NYT::TNode &inpRow = reader->GetRow();
            cv::Mat image = decodeImage(inpRow[COLUMN_NAME_IMAGE].AsString());

            INFO() << "Feature Id: " << inpRow[COLUMN_NAME_FEATURE_ID].AsInt64();
            house_number_sign_detector::HouseNumberSigns houseNumbers =
                hnsDetector_.detect(image,
                                    recognizeNumbers_
                                    ? house_number_sign_detector::RecognizeNumber::Yes
                                    : house_number_sign_detector::RecognizeNumber::No);
            filterHouseNumberOnCars(image, houseNumbers);

            NYT::TNode houseNumberNode;
            houseNumberNode (COLUMN_NAME_FEATURE_ID, inpRow[COLUMN_NAME_FEATURE_ID].AsInt64())
                            (COLUMN_NAME_LAT, inpRow[COLUMN_NAME_LAT].AsDouble())
                            (COLUMN_NAME_LON, inpRow[COLUMN_NAME_LON].AsDouble())
                            (COLUMN_NAME_OBJECTS, inpRow[COLUMN_NAME_OBJECTS])
                            (COLUMN_NAME_DETECTED, houseNumbersToNode(houseNumbers));

            writer->AddRow(houseNumberNode);
        }
    }
private:
    bool filterCars_;
    bool recognizeNumbers_;
    house_number_sign_detector::FasterRCNNDetector hnsDetector_;
    carsegm::CarSegmentator carSegmentator_;

    NYT::TNode houseNumbersToNode(const house_number_sign_detector::HouseNumberSigns& houseNumbers) {
        NYT::TNode result = NYT::TNode::CreateList();
        for(size_t i = 0; i < houseNumbers.size(); i++) {
            const house_number_sign_detector::HouseNumberSign& houseNumber = houseNumbers[i];
            const std::string& number = recognizeNumbers_ ? houseNumber.number : "";
            NYT::TNode node;
            node(ITEM_NAME_BBOX, rectangleToTNode(houseNumber.box))
                (ITEM_NAME_NUM, NYT::TNode(number))
                (ITEM_NAME_CONFIDENCE_DETECTOR, houseNumber.confidenceDetector)
                (ITEM_NAME_CONFIDENCE_RECOGNIZER, houseNumber.confidenceRecognizer)
                (ITEM_NAME_CONFIDENCE, houseNumber.confidence);
            result.Add(node);
        }
        return result;
    }
    void filterHouseNumberOnCars(const cv::Mat& image, house_number_sign_detector::HouseNumberSigns& houseNumbers) {
        if (0 == houseNumbers.size() || !filterCars_)
            return;
        cv::Mat mask = carSegmentator_.segment(image);
        houseNumbers.erase(
            std::remove_if(houseNumbers.begin(), houseNumbers.end(),
                [&](const house_number_sign_detector::HouseNumberSign& houseNumber) {
                    return 2 * cv::countNonZero(mask(houseNumber.box)) > houseNumber.box.area();
                }
            ),
            houseNumbers.end()
        );
    }
};

REGISTER_MAPPER(TDetectorMapper);

class TRecognizerMapper
    : public NYT::IMapper<NYT::TTableReader<NYT::TNode>, NYT::TTableWriter<NYT::TNode>>  {
public:
    TRecognizerMapper() = default;

    void Do(NYT::TTableReader<NYT::TNode>* reader, NYT::TTableWriter<NYT::TNode>* writer) override {
        INFO() << "Start recognition ... ";
        number_recognizer::NumberRecognizer hnsRecognizer = house_number_sign_detector::makeHouseNumberRecognizer();

        for (; reader->IsValid(); reader->Next()) {
            const NYT::TNode &inpRow = reader->GetRow();
            cv::Mat image = decodeImage(inpRow[COLUMN_NAME_IMAGE].AsString());

            NYT::TNode objectsNode = NYT::TNode::CreateList();
            const TVector<NYT::TNode>& objectList = inpRow[COLUMN_NAME_OBJECTS].AsList();
            for (const NYT::TNode& objectNode : objectList) {
                const cv::Rect bbox = NodeToRect(objectNode[ITEM_NAME_BBOX]);
                std::pair<std::string, float> recognized = hnsRecognizer.recognize(image(bbox));
                NYT::TNode node;
                node(ITEM_NAME_BBOX, rectangleToTNode(bbox))
                    (ITEM_NAME_NUM, NYT::TNode(recognized.first))
                    (ITEM_NAME_CONFIDENCE_DETECTOR, 1.0)
                    (ITEM_NAME_CONFIDENCE_RECOGNIZER, recognized.second);
                objectsNode.Add(node);
            }

            NYT::TNode houseNumberNode;
            houseNumberNode (COLUMN_NAME_FEATURE_ID, inpRow[COLUMN_NAME_FEATURE_ID].AsInt64())
                            (COLUMN_NAME_LAT, inpRow[COLUMN_NAME_LAT].AsDouble())
                            (COLUMN_NAME_LON, inpRow[COLUMN_NAME_LON].AsDouble())
                            (COLUMN_NAME_OBJECTS, inpRow[COLUMN_NAME_OBJECTS])
                            (COLUMN_NAME_DETECTED, objectsNode);
            writer->AddRow(houseNumberNode);
        }
    }
};

REGISTER_MAPPER(TRecognizerMapper);

typedef TLockFreeQueue<NYT::TNode> NodeQueue;

class AddressPointsSearcherInQueue
    : public IObjectInQueue {

public:
    AddressPointsSearcherInQueue(
        NodeQueue* inpQueue,
        NodeQueue* outQueue,
        const maps::wiki::common::ExtendedXmlDoc& wikiConfig,
        maps::wiki::revision::DBID commitId,
        const std::set<char16_t>& recognizerSupportedSymbols,
        const TString& objectsColumnName,
        const TString& itemNumberName,
        bool removeObjectsAddressPointFromMap)
        : inpQueue_(inpQueue)
        , outQueue_(outQueue)
        , wikiConfig_(wikiConfig)
        , commitId_(commitId)
        , recognizerSupportedSymbols_(recognizerSupportedSymbols)
        , objectsColumnName_(objectsColumnName)
        , itemNumberName_(itemNumberName)
        , removeObjectsAddressPointFromMap_(removeObjectsAddressPointFromMap)
        , running_(true) {
        AtomicSet(waitData_, 1);
    }
    void Process(void* /*threadSpecificResource*/) override {
        maps::wiki::common::PoolHolder wiki(wikiConfig_, "core", "tasks");
        auto txn = wiki.pool().slaveTransaction();
        maps::wiki::revision::RevisionsGateway gateway(*txn);
        maps::wiki::revision::Snapshot snapshot = gateway.snapshot(commitId_);
        maps::mrc::object::LoaderHolder loader = maps::mrc::object::makeRevisionLoader(snapshot);
        addr_pt_searcher::AddressPointSearcher addrPtSearcher(*loader, recognizerSupportedSymbols_);
        for (;;) {
            NYT::TNode inpNode;
            if (inpQueue_->Dequeue(&inpNode)) {
                outQueue_->Enqueue(searchAddressPoints(addrPtSearcher, inpNode));
            }
            else if (!isWaitData())
                break;
            else
                std::this_thread::sleep_for(THREAD_WAIT_TIMEOUT);
        }
        running_ = false;
    }
    void DataEnded() {
        AtomicSet(waitData_, 0);
    }
    bool isWaitData() {
        return (0 != AtomicGet(waitData_));
    }
    bool isRunning() const {
        return running_;
    }
private:
    NodeQueue* inpQueue_;
    NodeQueue* outQueue_;
    TAtomic waitData_;
    const maps::wiki::common::ExtendedXmlDoc& wikiConfig_;
    maps::wiki::revision::DBID commitId_;
    std::set<char16_t> recognizerSupportedSymbols_;
    TString objectsColumnName_;
    TString itemNumberName_;
    bool removeObjectsAddressPointFromMap_;
    bool running_;

    void removeObjectAddressPointFromMap(const NYT::TNode& inpRow, std::vector<addr_pt_searcher::AddressPointCandidate>& candidates) {
        constexpr double EPSILON_METERS = 0.1;

        const TVector<NYT::TNode>& objectsList = inpRow[COLUMN_NAME_OBJECTS].AsList();
        for (const NYT::TNode& objectNode : objectsList) {
            const bool addrPtExists =
                objectNode.HasKey(ITEM_NAME_ADDR_NUMBER) &&
                objectNode.HasKey(ITEM_NAME_ADDR_PT_LON) &&
                objectNode.HasKey(ITEM_NAME_ADDR_PT_LAT);
            if (!addrPtExists)
                continue;

            const geolib3::Point2 mercatorPos =
                geolib3::convertGeodeticToMercator(
                    geolib3::Point2(
                        objectNode[ITEM_NAME_ADDR_PT_LON].AsDouble(),
                        objectNode[ITEM_NAME_ADDR_PT_LAT].AsDouble()
                    )
                );
            const TString number = objectNode[ITEM_NAME_ADDR_NUMBER].AsString();

            const double EPSILON = geolib3::toMercatorUnits(EPSILON_METERS, mercatorPos);

            for (int i = candidates.size() - 1; 0 <= i; i--) {
                const addr_pt_searcher::AddressPointCandidate& candidate = candidates[i];
                if (std::abs(mercatorPos.x() - candidate.mercatorPos.x()) < EPSILON &&
                    std::abs(mercatorPos.y() - candidate.mercatorPos.y()) < EPSILON &&
                    number == candidate.number)
                {
                    candidates.erase(candidates.begin() + i);
                }
            }
        }
    }

    NYT::TNode searchAddressPoints(addr_pt_searcher::AddressPointSearcher& addrPtSearcher, const NYT::TNode& inpRow)
    {
        const geolib3::Point2 mercatorPos =
            geolib3::convertGeodeticToMercator(
                geolib3::Point2(
                    inpRow[COLUMN_NAME_LON].AsDouble(),
                    inpRow[COLUMN_NAME_LAT].AsDouble()
                )
            );
        addrPtSearcher.resetPosition(mercatorPos);
        const TVector<NYT::TNode>& objectsList = inpRow[objectsColumnName_].AsList();
        NYT::TNode hypotheses = NYT::TNode::CreateList();
        NYT::TNode addrPointsNode = NYT::TNode::CreateList();
        for (const NYT::TNode& objectNode : objectsList) {
            const double confidenceDetector =
                objectNode.HasKey(ITEM_NAME_CONFIDENCE_DETECTOR)
                ? objectNode[ITEM_NAME_CONFIDENCE_DETECTOR].AsDouble()
                : 1.0;
            const double confidenceRecognizer =
                objectNode.HasKey(ITEM_NAME_CONFIDENCE_RECOGNIZER)
                ? objectNode[ITEM_NAME_CONFIDENCE_RECOGNIZER].AsDouble()
                : 1.0;
            // даже если мы запускаемся на "видимых номерах", но при этом хотим выкинуть
            // часть символов при поиске адресной точки, надо сразу передавать в функцию
            // "прореженный" стринг (каким бы он пришел от распознавателя), чтобы избежать
            // сайд эффектов внутри addrPtSearcher
            const TString number = removeUnsupportedSymbols(objectNode[itemNumberName_].AsString(), recognizerSupportedSymbols_);
            std::vector<addr_pt_searcher::AddressPointCandidate> candidates = addrPtSearcher.find(mercatorPos, number, -DBL_EPSILON);
            if (removeObjectsAddressPointFromMap_) {
                removeObjectAddressPointFromMap(inpRow, candidates);
            }
            for (size_t i = 0; i < candidates.size(); i++) {
                const addr_pt_searcher::AddressPointCandidate candidate = candidates[i];
                NYT::TNode node;
                node(ITEM_NAME_ADDR_PT_X, candidate.mercatorPos.x())
                    (ITEM_NAME_ADDR_PT_Y, candidate.mercatorPos.y())
                    (ITEM_NAME_ADDR_NUMBER, candidate.number)
                    (ITEM_NAME_CONFIDENCE_DETECTOR, confidenceDetector)
                    (ITEM_NAME_CONFIDENCE_RECOGNIZER, confidenceRecognizer)
                    (ITEM_NAME_CONFIDENCE, candidate.confidence);
                addrPointsNode.Add(node);
            }
            NYT::TNode node;
            node(ITEM_NAME_ADDR_PT_X, mercatorPos.x())
                (ITEM_NAME_ADDR_PT_Y, mercatorPos.y())
                (ITEM_NAME_ADDR_NUMBER, number)
                (ITEM_NAME_CONFIDENCE_DETECTOR, confidenceDetector)
                (ITEM_NAME_CONFIDENCE_RECOGNIZER, confidenceRecognizer);
            if (candidates.empty()) {
                node(ITEM_NAME_CONFIDENCE, -DBL_EPSILON);
            } else {
                node(ITEM_NAME_CONFIDENCE, candidates[0].confidence);
            }
            hypotheses.Add(node);
        }

        NYT::TNode outRow;
        outRow  (COLUMN_NAME_FEATURE_ID, inpRow[COLUMN_NAME_FEATURE_ID].AsInt64())
                (COLUMN_NAME_LAT, inpRow[COLUMN_NAME_LAT].AsDouble())
                (COLUMN_NAME_LON, inpRow[COLUMN_NAME_LON].AsDouble())
                (COLUMN_NAME_OBJECTS, inpRow[COLUMN_NAME_OBJECTS])
                (COLUMN_NAME_ADDR_POINTS, addrPointsNode)
                (COLUMN_NAME_HYPOTHESES, hypotheses);
        return outRow;
    }
};

} // namespace

void calculateGroundTruthStatistic(
    NYT::IClientPtr& client,
    const TString& inputYTPath)
{
    static const std::set<char16_t> LIKE_SLASH = {'/', '-', 67 /* C */, 75 /* 'K' */};

    std::set<char16_t> recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    recognizerSupportedSymbols.insert(LIKE_SLASH.begin(), LIKE_SLASH.end());

    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(inputYTPath.c_str());
    int processedItems = 0;
    size_t numbersWithAddressPointCnt = 0;
    size_t numbersWithoutAddressPointCnt = 0;
    size_t addressPointsCnt = 0;

    std::ofstream ofs("./hypotheses_feature_id.txt");
    for (; reader->IsValid(); reader->Next(), processedItems++) {
        const NYT::TNode& inpRow = reader->GetRow();
        const maps::geolib3::Point2 mercatorPos =
            maps::geolib3::convertGeodeticToMercator(
                maps::geolib3::Point2(
                    inpRow[COLUMN_NAME_LON].AsDouble(),
                    inpRow[COLUMN_NAME_LAT].AsDouble()
                )
            );
        std::set<maps::mrc::addr_pt_searcher::AddressPointCandidate> candidates;
        const TVector<NYT::TNode>& objectList = inpRow[COLUMN_NAME_OBJECTS].AsList();
        bool woAddrPt = false;
        for (const NYT::TNode& objectNode : objectList) {
            const bool addrPtExists =
                objectNode.HasKey(ITEM_NAME_ADDR_NUMBER) &&
                objectNode.HasKey(ITEM_NAME_ADDR_PT_LON) &&
                objectNode.HasKey(ITEM_NAME_ADDR_PT_LAT);
            if (addrPtExists) {
                const maps::geolib3::Point2 addrPtMercatorPos =
                    maps::geolib3::convertGeodeticToMercator(
                        maps::geolib3::Point2(
                            objectNode[ITEM_NAME_ADDR_PT_LON].AsDouble(),
                            objectNode[ITEM_NAME_ADDR_PT_LAT].AsDouble()
                        )
                    );
                numbersWithAddressPointCnt++;
                candidates.insert({ addrPtMercatorPos, objectNode[ITEM_NAME_ADDR_NUMBER].AsString(), 1.0});
            } else {
                woAddrPt = true;
                numbersWithoutAddressPointCnt++;
                candidates.insert({ mercatorPos, objectNode[ITEM_NAME_VISIBLE_NUMBER].AsString(), 1.0});
            }

        }
        addressPointsCnt += candidates.size();
        if (woAddrPt)
            ofs << inpRow[COLUMN_NAME_FEATURE_ID].AsInt64() << std::endl;
    }
    INFO() << "Input YT table: ";
    INFO() << " images:                         " << processedItems;
    INFO() << " numbers with address points:    " << numbersWithAddressPointCnt;
    INFO() << " numbers without address points: " << numbersWithoutAddressPointCnt;
    INFO() << " different address points:       " << addressPointsCnt;
}

void detectHouseNumbers(
    NYT::IClientPtr& client,
    const TString& inputYTTable,
    const TString& outputYTTable,
    bool filterCars,
    bool recognizeNumbers,
    bool useGpu)
{
    constexpr int JOB_COUNT = 10;
    INFO() << "Detect house numbers...";
    client->Map(
        NYT::TMapOperationSpec()
            .AddInput<NYT::TNode>(inputYTTable)
            .AddOutput<NYT::TNode>(outputYTTable)
            .JobCount(JOB_COUNT),
        new TDetectorMapper(filterCars, recognizeNumbers),
        NYT::TOperationOptions().Spec(createOperationSpec("mapper", useGpu))
    );
    client->Sort(
        NYT::TSortOperationSpec()
            .AddInput(outputYTTable)
            .Output(outputYTTable)
            .SortBy(COLUMN_NAME_FEATURE_ID)
    );
}

Statistic calculateDetectorRecognizerStatistic(
    NYT::IClientPtr& client,
    const TString& testYTTable,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double iouThreshold,
    bool useRecognizedNumber,
    bool skipSymbolsUnknownByRecognizer)
{
    INFO() << "Calculate detector statistic...";
    std::set<char16_t> recognizerSupportedSymbols;
    if (useRecognizedNumber && skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }
    if (!useRecognizedNumber) {
        minConfidenceRecognizer = -DBL_MAX;
    }

    Statistic statistic;
    statistic.truePositives = 0;
    statistic.falsePositives = 0;
    statistic.falseNegatives = 0;

    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(testYTTable);
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& inpRow = reader->GetRow();
        statistic +=
            calculateDetectorRecognizerStatistic(
                extractGroundTruthNamedRects(inpRow[COLUMN_NAME_OBJECTS]),
                extractTestNamedRects(inpRow[COLUMN_NAME_DETECTED], minConfidenceDetector, minConfidenceRecognizer),
                iouThreshold,
                useRecognizedNumber,
                recognizerSupportedSymbols);
    }
    return statistic;
}

void calculateDetectorRecognizerStatisticOnGrid(
    NYT::IClientPtr& client,
    const TString& testYTTable,
    double iouThreshold,
    bool useRecognizedNumber,
    bool skipSymbolsUnknownByRecognizer,
    const std::string& outputPath)
{
    INFO() << "Calculate detector statistic on the grid...";

    std::set<char16_t> recognizerSupportedSymbols;
    if (useRecognizedNumber && skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }

    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(testYTTable);
    // first = gtObjects
    // second = Node with test data
    std::vector<
        std::pair<
            std::vector<NamedRect>, NYT::TNode
        >
    > dataVector;
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& inpRow = reader->GetRow();
        std::pair<std::vector<NamedRect>, NYT::TNode> data;
        data.first = extractGroundTruthNamedRects(inpRow[COLUMN_NAME_OBJECTS]);
        data.second = inpRow[COLUMN_NAME_DETECTED];
        dataVector.emplace_back(data);
    }

    std::ofstream ofs(outputPath);
    REQUIRE(ofs.is_open(), "Unable to open output file: " << outputPath);
    int stepDetector = 10;
    for (int i = 0; i < 100; i+=stepDetector) {
        const double confidenceDetector = (double)i / 100.;
        if (useRecognizedNumber) {
            int stepRecognizer = 10;
            for (int j = 0; j < 100; j+=stepRecognizer) {
                const double confidenceRecognizer = (double)j / 100.;
                Statistic statistic = calculateDetectorRecognizerStatistic(dataVector, confidenceDetector, confidenceRecognizer, iouThreshold, useRecognizedNumber, recognizerSupportedSymbols);
                ofs << confidenceDetector << " "
                    << confidenceRecognizer << " "
                    << statistic.truePositives << " "
                    << statistic.falsePositives << " "
                    << statistic.falseNegatives << std::endl;
                if (j == 50) {
                    stepRecognizer = 2;
                }
            }
        } else {
            Statistic statistic = calculateDetectorRecognizerStatistic(dataVector, confidenceDetector, -DBL_MAX, iouThreshold, useRecognizedNumber, recognizerSupportedSymbols);
            ofs << confidenceDetector << " "
                << statistic.truePositives << " "
                << statistic.falsePositives << " "
                << statistic.falseNegatives << std::endl;
        }
        if (i == 50) {
            stepDetector = 2;
        }
    }
}

void recognizeHouseNumbers(
    NYT::IClientPtr& client,
    const TString& inputYTTable,
    const TString& outputYTTable,
    bool useGpu)
{
    constexpr int JOB_COUNT = 5;
    INFO() << "Recognize house numbers... ";
    client->Map(
        NYT::TMapOperationSpec()
            .AddInput<NYT::TNode>(inputYTTable)
            .AddOutput<NYT::TNode>(outputYTTable)
            .JobCount(JOB_COUNT),
        new TRecognizerMapper(),
        NYT::TOperationOptions().Spec(createOperationSpec("mapper", useGpu))
    );
    client->Sort(
        NYT::TSortOperationSpec()
            .AddInput(outputYTTable)
            .Output(outputYTTable)
            .SortBy(COLUMN_NAME_FEATURE_ID)
    );
}

Statistic calculateRecognizerStatistic(
    NYT::IClientPtr& client,
    const TString& testYTTable,
    double minConfidence,
    bool skipSymbolsUnknownByRecognizer)
{
    INFO() << "Calculate recognizer statistic... (confidence = " << minConfidence << ")";
    std::set<char16_t> recognizerSupportedSymbols;
    if (skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }

    Statistic statistic;
    statistic.truePositives = 0;
    statistic.falsePositives = 0;
    statistic.falseNegatives = 0;
    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(testYTTable);
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& inpRow = reader->GetRow();
        statistic +=
            calculateRecognizerStatistic(
                extractGroundTruthNamedRects(inpRow[COLUMN_NAME_OBJECTS]),
                extractTestNamedRects(inpRow[COLUMN_NAME_DETECTED], -DBL_MAX, minConfidence),
                recognizerSupportedSymbols);
    }
    return statistic;
}

void searchAddressPoints(
    const maps::wiki::common::ExtendedXmlDoc& wikiConfig,
    maps::wiki::revision::DBID commitId,
    NYT::IClientPtr& client,
    const TString& detectedYTTable,
    const TString& addressPointsYTTable,
    bool skipSymbolsUnknownByRecognizer,
    const TString& objectsColumnName,
    const TString& objectNumberItemName,
    bool removeObjectsAddressPointFromMap,
    int threadsCount)
{
    INFO() << "Search address points...";

    std::set<char16_t> recognizerSupportedSymbols;
    if (skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }

    TAutoPtr<IThreadPool> mtpQueue = CreateThreadPool(threadsCount);
    NodeQueue inpQueue;
    NodeQueue outQueue;
    std::vector<TAutoPtr<AddressPointsSearcherInQueue>> searchers;
    for (int i = 0; i < threadsCount; i++) {
        TAutoPtr<AddressPointsSearcherInQueue> searcher(new AddressPointsSearcherInQueue(
            &inpQueue,
            &outQueue,
            wikiConfig,
            commitId,
            recognizerSupportedSymbols,
            objectsColumnName,
            objectNumberItemName,
            removeObjectsAddressPointFromMap));
        mtpQueue->SafeAdd(searcher.Get());
        searchers.push_back(searcher);
    }

    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(detectedYTTable);
    for (; reader->IsValid(); reader->Next()) {
        inpQueue.Enqueue(reader->GetRow());
    }
    for (size_t i = 0; i < searchers.size(); i++) {
        searchers[i]->DataEnded();
    }
    for (;!inpQueue.IsEmpty();) {
        std::this_thread::sleep_for(THREAD_WAIT_TIMEOUT);
    }
    NYT::TTableWriterPtr<NYT::TNode> writer = client->CreateTableWriter<NYT::TNode>(addressPointsYTTable);
    bool wait = true;
    for (;wait;) {
        wait = false;
        for (size_t i = 0; i < searchers.size(); i++) {
            wait |= searchers[i]->isRunning();
        }
        for (;;) {
            NYT::TNode outNode;
            if (!outQueue.Dequeue(&outNode))
                break;
            writer->AddRow(outNode);
        }
        std::this_thread::sleep_for(THREAD_WAIT_TIMEOUT);
    }
    writer->Finish();

    client->Sort(
        NYT::TSortOperationSpec()
            .AddInput(addressPointsYTTable)
            .Output(addressPointsYTTable)
            .SortBy(COLUMN_NAME_FEATURE_ID)
    );
}

Statistic calculateAddressPointsStatistic(
    NYT::IClientPtr& client,
    const TString& testYTTable,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double minConfidenceAddrPoint,
    bool skipSymbolsUnknownByRecognizer)
{
    INFO() << "Calculate address points statistic ...";
    std::set<char16_t> recognizerSupportedSymbols;
    if (skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }

    Statistic statistic;
    statistic.truePositives = 0;
    statistic.falsePositives = 0;
    statistic.falseNegatives = 0;
    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(testYTTable);
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& inpRow = reader->GetRow();
        const geolib3::Point2 mercatorPos =
            geolib3::convertGeodeticToMercator(
                geolib3::Point2(
                    inpRow[COLUMN_NAME_LON].AsDouble(),
                    inpRow[COLUMN_NAME_LAT].AsDouble()
                )
            );
        statistic += calculateAddressPointStatistic(
            extractGroundTruthAddressPointCandidates(
                inpRow[COLUMN_NAME_OBJECTS],
                mercatorPos,
                recognizerSupportedSymbols),
            extractTestAddressPointCandidates(
                inpRow[COLUMN_NAME_ADDR_POINTS],
                inpRow[COLUMN_NAME_HYPOTHESES],
                minConfidenceDetector,
                minConfidenceRecognizer,
                minConfidenceAddrPoint));
    }
    return statistic;
}

Statistic calculateHypothesesStatistic(
    NYT::IClientPtr& client,
    const TString& testYTTable,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    double minConfidenceAddrPoint,
    bool skipSymbolsUnknownByRecognizer,
    bool removeObjectsAddressPointFromMap)
{
    INFO() << "Calculate hypotheses statistic ...";
    std::set<char16_t> recognizerSupportedSymbols;
    if (skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }

    Statistic statistic;
    statistic.truePositives = 0;
    statistic.falsePositives = 0;
    statistic.falseNegatives = 0;
    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(testYTTable);
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& inpRow = reader->GetRow();

        statistic += calculateHypothesesStatistic(
            extractGroundTruthHypotheses(
                inpRow[COLUMN_NAME_OBJECTS],
                removeObjectsAddressPointFromMap,
                recognizerSupportedSymbols),
            extractTestHypotheses(
                inpRow[COLUMN_NAME_HYPOTHESES],
                minConfidenceDetector,
                minConfidenceRecognizer,
                minConfidenceAddrPoint));
    }
    return statistic;
}

void calculateHypothesesStatisticOnGrid(
    NYT::IClientPtr& client,
    const TString& testYTTable,
    bool skipSymbolsUnknownByRecognizer,
    bool removeObjectsAddressPointFromMap,
    const std::string& outputPath)
{
    INFO() << "Calculate detector statistic on the grid...";
    std::set<char16_t> recognizerSupportedSymbols;
    if (skipSymbolsUnknownByRecognizer) {
        recognizerSupportedSymbols = getRecognizerSupportedSymbols();
    }

    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(testYTTable);
    // first = gtObjects
    // second = Node with test data
    std::vector<
        std::pair<
        std::set<TString>, NYT::TNode
        >
    > dataVector;
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& inpRow = reader->GetRow();
        std::pair<std::set<TString>, NYT::TNode> data;
        data.first = extractGroundTruthHypotheses(
                        inpRow[COLUMN_NAME_OBJECTS],
                        removeObjectsAddressPointFromMap,
                        recognizerSupportedSymbols);
        data.second = inpRow[COLUMN_NAME_HYPOTHESES];
        dataVector.emplace_back(data);
    }

    std::ofstream ofs(outputPath);
    REQUIRE(ofs.is_open(), "Unable to open output file: " << outputPath);
    int stepDetector = 10;
    for (int i = 0; i < 100; i += stepDetector) {
        const double confidenceDetector = (double)i / 100.;
        int stepRecognizer = 10;
        for (int j = 0; j < 100; j += stepRecognizer) {
            const double confidenceRecognizer = (double)j / 100.;
            int stepAddrPoint = 10;
            for (int k = 0; k < 100; k += stepAddrPoint) {
                const double confidenceAddrPoint = (double)k / 100.;
                Statistic statistic = calculateHypothesesStatistic(dataVector, confidenceDetector, confidenceRecognizer, confidenceAddrPoint);
                ofs << confidenceDetector << " "
                    << confidenceRecognizer << " "
                    << confidenceAddrPoint << " "
                    << statistic.truePositives << " "
                    << statistic.falsePositives << " "
                    << statistic.falseNegatives << std::endl;
                if (k == 50) {
                    stepAddrPoint = 2;
                }
            }
            if (j == 50) {
                stepRecognizer = 2;
            }
        }
        if (i == 50) {
            stepDetector = 2;
        }
    }
}

void exportDetectorResults(
    NYT::IClientPtr& client,
    const TString& detectorResultTable,
    double minConfidenceDetector,
    double minConfidenceRecognizer,
    const std::string& outputPath)
{
    std::ofstream ofs(outputPath);
    REQUIRE(ofs.is_open(), "Unable to open output file: " << outputPath);
    maps::json::Builder builder(ofs);
    NYT::TTableReaderPtr<NYT::TNode> reader = client->CreateTableReader<NYT::TNode>(detectorResultTable);
    builder << [&](maps::json::ObjectBuilder builder) {
        builder["results"] << [&](maps::json::ArrayBuilder builder) {
            for (; reader->IsValid(); reader->Next()) {
                const NYT::TNode& row = reader->GetRow();
                builder << [&](maps::json::ObjectBuilder builder) {
                    builder["feature_id"] << row[COLUMN_NAME_FEATURE_ID].AsInt64();
                    SaveNamedRectsToJson(extractTestNamedRects(row[COLUMN_NAME_DETECTED], minConfidenceDetector, minConfidenceRecognizer), builder);
                };
            };
        };
    };
}

} // namespace maps::mrc::house_number_pipeline
