#include <maps/wikimap/mapspro/libs/tfrecord_writer/include/tfrecord_writer.h>

#include <yandex/maps/mrc/traffic_signs/signs.h>

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

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

#include <util/system/fs.h>
#include <util/system/datetime.h>
#include <util/random/random.h>
#include <util/generic/map.h>
#include <util/generic/hash.h>
#include <util/generic/hash_set.h>
#include <library/cpp/string_utils/base64/base64.h>

#include <random>
#include <string>
#include <list>
#include <fstream>

using namespace NYT;
namespace tfw = maps::wiki::tfrecord_writer;
namespace ts = maps::mrc::traffic_signs;

class TFilterSizeMapper
    : public IMapper<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    Y_SAVELOAD_JOB(MinSize_);

    TFilterSizeMapper() = default;

    TFilterSizeMapper(int minsize)
        : MinSize_(minsize)
    { }

    void Do(TReader* reader, TWriter* writer) override
    {
        const TString unknownType = "unknown";
        for (; reader->IsValid(); reader->Next()) {
            const auto& inpRow = reader->GetRow();
            TNode outObjectRow;
            outObjectRow["feature_id"] = (int64_t)reader->GetRowIndex();
            bool hasValidObjects = false;
            const auto& objects = inpRow["objects"].AsList();
            for (const auto& object: objects) {
                if (object["type"] == unknownType)
                    continue;
                const auto& bbox = object["bbox"].AsList();
                const int64_t x1 = bbox[0].AsList()[0].AsInt64();
                const int64_t y1 = bbox[0].AsList()[1].AsInt64();

                const int64_t x2 = bbox[1].AsList()[0].AsInt64();
                const int64_t y2 = bbox[1].AsList()[1].AsInt64();
                if (abs(x2 - x1) < MinSize_ && abs(y2 - y1) < MinSize_)
                    continue;
                hasValidObjects = true;
                outObjectRow["type"] = object["type"];
                outObjectRow["bbox"] = object["bbox"];
                writer->AddRow(outObjectRow, 1);
            }
            if (hasValidObjects) {
                TNode outImageRow;
                outImageRow["feature_id"] = (int64_t)reader->GetRowIndex();
                outImageRow["image"] = inpRow["image"];
                writer->AddRow(outImageRow, 0);
            }
        }
    }
private:
    int MinSize_ = 0;
};
REGISTER_MAPPER(TFilterSizeMapper);

class TJoinSpeedLimitsMapper
    : public IMapper<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    void Do(TReader* reader, TWriter* writer) override
    {
        const TString prohibitoryMaxSpeed = "prohibitory_max_speed";
        for (; reader->IsValid(); reader->Next()) {
            const auto& inpRow = reader->GetRow();
            TNode outRow = inpRow;
            TString type = inpRow["type"].AsString();
            if (0 == type.find(prohibitoryMaxSpeed)) {
                outRow["type"] = prohibitoryMaxSpeed;
            }
            writer->AddRow(outRow);
        }
    }
};
REGISTER_MAPPER(TJoinSpeedLimitsMapper);

class TJoinMetaClassMapper
    : public IMapper<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    Y_SAVELOAD_JOB(ClassToMetaClass_);

    TJoinMetaClassMapper() = default;

    TJoinMetaClassMapper(const TMap<TString, TString> &classToMetaClass)
        : ClassToMetaClass_(classToMetaClass)
    { }

    void Do(TReader* reader, TWriter* writer) override
    {
        const TString unknownType = "unknown";
        for (; reader->IsValid(); reader->Next()) {
            const auto& inpRow = reader->GetRow();
            if (!ClassToMetaClass_.contains(inpRow["type"].AsString()))
                continue;
            TNode outRow = inpRow;
            outRow["type"] = ClassToMetaClass_[inpRow["type"].AsString()];
            writer->AddRow(outRow);
        }
    }
private:
    TMap<TString, TString> ClassToMetaClass_;
};
REGISTER_MAPPER(TJoinMetaClassMapper);

class TCountClassesReduce
    : public IReducer<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    void Do(TReader* reader, TWriter* writer) override
    {
        const auto& row = reader->GetRow();
        const auto type = row["type"];

        ui32 count = 0;
        for (; reader->IsValid(); reader->Next()) {
            ++count;
        }

        TNode result;
        result["type"] = type;
        result["count"] = count;
        writer->AddRow(result);
    }
};
REGISTER_REDUCER(TCountClassesReduce);

class TFilterValidClassesMapper
    : public IMapper<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    Y_SAVELOAD_JOB(ValidClasses_);

    TFilterValidClassesMapper() = default;

    TFilterValidClassesMapper(const TMap<TString, int> &validclasses)
        : ValidClasses_(validclasses)
    { }

    void Do(TReader* reader, TWriter* writer) override
    {
        for (; reader->IsValid(); reader->Next()) {
            const auto& inpRow = reader->GetRow();
            if (!ValidClasses_.contains(inpRow["type"].AsString()))
                continue;
            writer->AddRow(inpRow);
        }
    }
private:
    // valid classes
    TMap<TString, int> ValidClasses_;
};
REGISTER_MAPPER(TFilterValidClassesMapper);

class TSplitMapper
    : public IMapper<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    Y_SAVELOAD_JOB(FirstPart_);

    TSplitMapper() = default;

    TSplitMapper(uint64_t firstPart)
        : FirstPart_(firstPart)
    { }

    void Do(TReader* reader, TWriter* writer) override
    {
        for (; reader->IsValid() && reader->GetRowIndex() < FirstPart_;
               reader->Next()) {
            writer->AddRow(reader->GetRow(), 0);
        }
        for (; reader->IsValid(); reader->Next()) {
            writer->AddRow(reader->GetRow(), 1);
        }
    }
private:
    uint64_t FirstPart_;
};
REGISTER_MAPPER(TSplitMapper);

class TFeatureIDReduce
    : public IReducer<TTableReader<TNode>, TTableWriter<TNode>>
{
public:
    Y_SAVELOAD_JOB(RandomSeed_);

    TFeatureIDReduce() = default;

    TFeatureIDReduce(unsigned int randomseed)
        : RandomSeed_(randomseed)
    { }
    void Do(TReader* reader, TWriter* writer) override {
        constexpr size_t RANDOM_DATA_SIZE = 2000;
        TNode outRow;
        bool hasSigns = false;
        for (; reader->IsValid(); reader->Next()) {
            const auto& curRow = reader->GetRow();

            auto tableIndex = reader->GetTableIndex();
            if (tableIndex == 0) {
                TString randomData     = curRow["image"].AsString().substr(0, RANDOM_DATA_SIZE);
                outRow["feature_id"]   = curRow["feature_id"];
                outRow["image"]        = curRow["image"];
                outRow["random_value"] = TNode((unsigned int)ComputeHash(randomData) ^ RandomSeed_);

                outRow["objects"]    = TNode::CreateList();
            } else if (tableIndex == 1) {
                outRow["objects"].Add(TNode()
                                      ("type", curRow["type"])
                                      ("bbox", curRow["bbox"]));
                hasSigns = true;
            } else {
                Y_FAIL();
            }
        }

        if (hasSigns) {
            writer->AddRow(outRow);
        }
    }
private:
    unsigned int RandomSeed_;
};
REGISTER_REDUCER(TFeatureIDReduce)

void FilterByMinSize(
    IClientPtr client,
    const TString &inputTable,
    int minSize,
    const TString &imagesTable,
    const TString &objectsTable)
{
    INFO() << "Filter dataset by min bbox size";
    client->Map(
        TMapOperationSpec()
            .AddInput<TNode>(inputTable)
            .AddOutput<TNode>(imagesTable)
            .AddOutput<TNode>(objectsTable),
        new TFilterSizeMapper(minSize));
}

void JoinSpeedLimits(
    IClientPtr client,
    const TString &inputTable,
    const TString &outputTable)
{
    INFO() << "Join all speed limits classes in one";
    client->Map(
        TMapOperationSpec()
            .AddInput<TNode>(inputTable)
            .AddOutput<TNode>(outputTable),
        new TJoinSpeedLimitsMapper());
}


THashSet<TString> loadFilterClassTable(
    IClientPtr client,
    const TString &filterClassTableName)
{
    THashSet<TString> filter;
    auto reader = client->CreateTableReader<TNode>(filterClassTableName);
    for (; reader->IsValid(); reader->Next()) {
        const auto& inpRow = reader->GetRow();
        filter.insert(inpRow["name"].AsString());
    }
    return filter;
}

TMap<TString, TString> loadRenameClassTable(
    IClientPtr client,
    const TString &renameClassTableName)
{
    TMap<TString, TString> renameMap;
    auto reader = client->CreateTableReader<TNode>(renameClassTableName);
    for (; reader->IsValid(); reader->Next()) {
        const auto& inpRow = reader->GetRow();
        renameMap[inpRow["class"].AsString()] = inpRow["meta"].AsString();
    }
    return renameMap;
}

void JoinMetaClasses(
    IClientPtr client,
    const TString &inputOutputTable,
    const TMap<TString, TString> &classToMetaClass)
{
    INFO() << "Join classes to meta classes";
    client->Map(
        TMapOperationSpec()
            .AddInput<TNode>(inputOutputTable)
            .AddOutput<TNode>(inputOutputTable),
        new TJoinMetaClassMapper(classToMetaClass));
}

THashSet<TString> ValidClasses(
    IClientPtr client,
    const TString &inputTable,
    uint64_t minClassesInstance)
{
    const TTempTable minszFilteredSorted(client);
    const TTempTable minszFilteredClassesCnt(client);

    INFO() << "Count instance of classes";

    client->Sort(
        TSortOperationSpec()
            .AddInput(inputTable)
            .Output(minszFilteredSorted.Name())
            .SortBy({"type"}));

    client->Reduce(
        TReduceOperationSpec()
            .ReduceBy({"type"})
            .AddInput<TNode>(minszFilteredSorted.Name())
            .AddOutput<TNode>(minszFilteredClassesCnt.Name()),
        new TCountClassesReduce);

    THashSet<TString> classesName;
    auto reader = client->CreateTableReader<TNode>(minszFilteredClassesCnt.Name());
    for (; reader->IsValid(); reader->Next()) {
        const auto& inpRow = reader->GetRow();
        if (minClassesInstance <= inpRow["count"].AsUint64()) {
            classesName.insert(inpRow["type"].AsString());
        }
    }
    return classesName;
}

void extendClassesListByHorzFlip(THashSet<TString> &classesName) {
    for (const auto& cl : classesName) {
        const ts::TrafficSign typeFlipped =
            ts::flipTrafficSignHorz(ts::stringToTrafficSign(cl));
        if (ts::TrafficSign::Unknown == typeFlipped)
            continue;
        classesName.insert(TString(ts::toString(typeFlipped)));
    }
}

TMap<TString, int> makeClassToIDMap(const THashSet<TString> &classesName, const THashSet<TString> &filter) {
    TMap<TString, int> mapClassesName;
    int lastClassID = 1;
    for (const auto& cl : classesName) {
        if (filter.empty() || filter.contains(cl)) {
            mapClassesName[cl] = lastClassID;
            lastClassID++;
        }
    }
    return mapClassesName;
}

void FilterByValidClasses(
    IClientPtr client,
    const TString &inputTable,
    const TMap<TString, int> &mapClassesName,
    const TString &outputTable)
{
    INFO() << "Filter dataset by classes with enought instances";
    client->Map(
        TMapOperationSpec()
        .AddInput<TNode>(inputTable)
        .AddOutput<TNode>(outputTable),
        new TFilterValidClassesMapper(mapClassesName));
}

void SplitTable(
    IClientPtr client,
    const TString &filteredTable,
    const TString &partFirstTable,
    const TString &partSecondTable,
    double firstPart)
{
    const TTempTable filteredRandomSorted(client);
    INFO() << "Split dataset to train and test parts";

    client->Sort(
        TSortOperationSpec()
            .AddInput(filteredTable)
            .Output(filteredRandomSorted.Name())
            .SortBy({"random_value"}));

    uint64_t count = (uint64_t)(firstPart * client->Get(filteredRandomSorted.Name() + "/@row_count").AsInt64());

    client->Map(
        TMapOperationSpec()
        .AddInput<TNode>(filteredRandomSorted.Name())
        .AddOutput<TNode>(partFirstTable)
        .AddOutput<TNode>(partSecondTable),
        new TSplitMapper(count));
}

void JoinValidSignsWithImages(
    IClientPtr client,
    const TString &inputTable,
    const TString &filteredTable,
    const TString &resultTable)
{
    const TTempTable sorted(client);
    const TTempTable filteredSorted(client);

    INFO() << "Join valid signs objects with image dataset";

    client->Sort(
        TSortOperationSpec()
            .AddInput(inputTable)
            .Output(sorted.Name())
            .SortBy({"feature_id"}));

    client->Sort(
        TSortOperationSpec()
            .AddInput(filteredTable)
            .Output(filteredSorted.Name())
            .SortBy({"feature_id"}));

    client->Reduce(
        TReduceOperationSpec()
            .ReduceBy({"feature_id"})
            .AddInput<TNode>(sorted.Name())
            .AddInput<TNode>(filteredSorted.Name())
            .AddOutput<TNode>(resultTable),
        new TFeatureIDReduce);
}

void SaveLabelMap(const TMap<TString, int> &mapClassesName,
                  const TString &labelMapPath) {
    std::vector<TString> classesNames(mapClassesName.size());
    for (const auto &item : mapClassesName) {
        classesNames[item.second - 1] = item.first;
    }

    std::ofstream ofs(labelMapPath.begin());
    for (size_t i = 0; i < classesNames.size(); i++) {
        ofs << "item {" << std::endl;
        ofs << "  id: " << (i + 1) << std::endl;
        ofs << "  name: '" << classesNames[i] << "'" << std::endl;
        ofs << "}" << std::endl << std::endl;
    }
}

cv::Rect NodeToRect(const TNode& node) {
    cv::Rect rc;
    const auto& 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  = abs((int)x2 - (int)x1) + 1;
    rc.height = abs((int)y2 - (int)y1) + 1;
    return rc;
}

tfw::FasterRCNNObjects NodeToObjects(const TNode& node,
                                     const TMap<TString, int> &mapClassesName) {
    tfw::FasterRCNNObjects objects;
    const auto& objectList = node.AsList();
    for (const auto& objectNode : objectList) {
        tfw::FasterRCNNObject object;
        const TString type = objectNode["type"].AsString();
        object.text = type;
        object.label = mapClassesName.at(type);
        object.bbox = NodeToRect(objectNode["bbox"]);
        objects.emplace_back(object);
    }
    return objects;
}

tfw::FasterRCNNObjects FlipHorzObjectsClasses(const tfw::FasterRCNNObjects &objects, const TMap<TString, int> &mapClassesName, const cv::Size &imgSize) {
    tfw::FasterRCNNObjects objsFlipped;
    for (const auto &obj : objects) {
        tfw::FasterRCNNObject objFlipped;
        const ts::TrafficSign typeFlipped =
            ts::flipTrafficSignHorz(ts::stringToTrafficSign(obj.text));
        if (ts::TrafficSign::Unknown == typeFlipped)
            continue;
        TString flipped = TString(ts::toString(typeFlipped));
        if (!mapClassesName.contains(flipped))
            continue;
        objFlipped.text = flipped;
        objFlipped.label = mapClassesName.at(flipped);
        objFlipped.bbox = obj.bbox;
        objFlipped.bbox.x = imgSize.width - 1 - (objFlipped.bbox.x + objFlipped.bbox.width - 1);
        objsFlipped.emplace_back(objFlipped);
    }
    return objsFlipped;
}

void BlurFlipHorzObjectsOfUnknownClasses(const tfw::FasterRCNNObjects &objects, const TMap<TString, int> &mapClassesName, cv::Mat &image) {
    for (const auto &obj : objects) {
        const ts::TrafficSign typeFlipped =
            ts::flipTrafficSignHorz(ts::stringToTrafficSign(obj.text));
        if (ts::TrafficSign::Unknown != typeFlipped) {
            TString flipped = TString(ts::toString(typeFlipped));
            if (mapClassesName.contains(flipped))
                continue;
        }
        cv::Rect bbox = obj.bbox;
        bbox.x = image.cols - 1 - (bbox.x + bbox.width - 1);
        if (bbox.x < 0) {
            bbox.width += bbox.x;
            bbox.x = 0;
        }
        cv::blur(image(bbox), image(bbox), bbox.size() / 2);
    }
}


void MakeTFRecord(IClientPtr client,
                  const TString &inputTable,
                  const TMap<TString, int> &mapClassesName,
                  const TString &tfRecordPath,
                  bool horzFlip = false,
                  bool blurSigns = false) {
    REQUIRE(!blurSigns || horzFlip, "Signs blur can be enabled only on fliped images");


    TFileOutput tfrecordFile(tfRecordPath);
    tfw::TFRecordWriter<tfw::FasterRCNNObject> tfrecordWriter(&tfrecordFile);

    auto reader = client->CreateTableReader<TNode>(inputTable);
    for (int processedItems = 0; reader->IsValid(); reader->Next(), processedItems++) {
        const auto& inpRow = reader->GetRow();

        std::ostringstream stream;
        stream << inpRow["feature_id"].AsInt64();
        const TString feature_id = stream.str().c_str();
        tfw::FasterRCNNObjects objects = NodeToObjects(inpRow["objects"], mapClassesName);

        TString encimageStr = inpRow["image"].AsString();
        std::vector<std::uint8_t> encimage(Base64DecodeBufSize(encimageStr.length()));
        size_t encimageSize = Base64Decode(encimage.data(), encimageStr.begin(), encimageStr.end());
        encimage.resize(encimageSize);

        tfrecordWriter.AddRecord(encimage, objects, feature_id);
        if (horzFlip) {
            cv::Mat image = cv::imdecode(encimage, 1);
            tfw::FasterRCNNObjects objectsFlipped = FlipHorzObjectsClasses(objects, mapClassesName, image.size());
            if (!objectsFlipped.empty()) {
                cv::flip(image, image, 1);
                if (blurSigns)
                    BlurFlipHorzObjectsOfUnknownClasses(objects, mapClassesName, image);
                tfrecordWriter.AddRecord(image, objectsFlipped, feature_id + "_flipped");
            }
        }

        if ((processedItems + 1) % 1000 == 0) {
            INFO() << "Processed " << (processedItems + 1) << " items";
        }
    }

    INFO() << "Images: " << tfrecordWriter.GetRecordsCount();
    INFO() << "Objects: " << tfrecordWriter.GetObjectsCount();
}

TString temporaryString(const TString& prefix) {
    return prefix + ToString(MicroSeconds()) + "-" + ToString(RandomNumber<ui64>());
}

int main(int argc, const char** argv) try {
    Initialize(argc, argv);

    maps::cmdline::Parser parser("Download traffic signs dataset from YT table to tfrecord");

    auto intputTableName = parser.string("input")
        .required()
        .help("Input YT table name");

    auto intputTestTableName = parser.string("input-test")
        .help("Input YT table name with test data");

    auto outputTrainPath = parser.string("outtrain")
        .required()
        .help("Path to output file for train data");

    auto outputTestPath = parser.string("outtest")
        .required()
        .help("Path to output file for test data");

    auto outputLabelMapPath = parser.string("labelmap")
        .required()
        .help("Path to output label_map file with classes");

    auto minSize = parser.num("minsize")
        .defaultValue(0)
        .help("Minimal value of size (width or height) of signs (default: 0)");

    auto minAmount = parser.num("minamount")
        .defaultValue(0)
        .help("Minimal amount of insances of class (default: 0)");

    auto trainPart = parser.real("trainpart")
        .defaultValue(.9)
        .help("Part of dataset for building train.tfrecord (from 0. to 1.0) other part will use for test.tfrecord  (default: 0.9)");

    auto joinSpeedLimits = parser.flag("join-speed-limits")
        .defaultValue(false)
        .help("Join all speed limits classes in one");

    auto augmHorzFlip = parser.flag("horzflip")
        .defaultValue(false)
        .help("Augment dataset by horizontal flip of image");

    auto blurFlipedSign = parser.flag("blurfliped")
        .defaultValue(false)
        .help("Blur signs, which brings to unknown class after horizontal fliped (work only with horzflip flag)");

    auto filterClassTableName = parser.string("filter_class_table")
        .defaultValue("")
        .help("Input YT table with classes names, which we only add to tfrecord \n(this is additional filter, if class doesn't have enought instance it will not add to results tfrecords)");

    auto renameClassTableName = parser.string("rename_class_table")
        .defaultValue("")
        .help("Input YT table with pair: class - meta. Classes of object will be join into the meta classes, which we "
              "add to tfrecord \n(this is additional filter, if metaclass doesn't have enought instance it will not add to results tfrecords)");

    auto randomSeed = parser.num("seed")
        .defaultValue(42)
        .help("Seed of random generator to split train and test records (default: 42)");

    parser.parse(argc, const_cast<char**>(argv));

    REQUIRE(filterClassTableName.empty() || renameClassTableName.empty(),
            "You can use either filter_class_table param or rename_class_table param not both");

    REQUIRE(!augmHorzFlip || renameClassTableName.empty(),
            "Horizontal flip is not allowed if you want to use metaclasses");

    if (intputTestTableName.defined() && trainPart < 1.0) {
        INFO() << "All data from " << intputTableName.c_str() << " table will send to train part, because --input-test defined";
    }

    INFO() << "Connecting to yt::hahn";
    IClientPtr client = CreateClient("hahn");

    const TTempTable minszFilteredImgs(client);
    const TTempTable minszFilteredObjs(client);
    FilterByMinSize(client, intputTableName.c_str(), minSize, minszFilteredImgs.Name(), minszFilteredObjs.Name());
    if (joinSpeedLimits) {
        JoinSpeedLimits(client, minszFilteredObjs.Name(), minszFilteredObjs.Name());
    }

    if (!renameClassTableName.empty()) {
        TMap<TString, TString> classesToMetaClasses = loadRenameClassTable(client, renameClassTableName.c_str());
        JoinMetaClasses(client, minszFilteredObjs.Name(), classesToMetaClasses);
    }
    THashSet<TString> validClasses = ValidClasses(client, minszFilteredObjs.Name(), minAmount);
    REQUIRE(!validClasses.empty(), "there are not classes with required properties in table");
    if (augmHorzFlip) {
        extendClassesListByHorzFlip(validClasses);
    }
    THashSet<TString> filterClasses;
    if (!filterClassTableName.empty()) {
        filterClasses = loadFilterClassTable(client, filterClassTableName.c_str());
    }
    TMap<TString, int> classes = makeClassToIDMap(validClasses, filterClasses);

    const TTempTable resultTable(client);
    const TTempTable minszValidClassesFiltered(client);
    FilterByValidClasses(client, minszFilteredObjs.Name(), classes, minszValidClassesFiltered.Name());
    JoinValidSignsWithImages(client, minszFilteredImgs.Name(), minszValidClassesFiltered.Name(), resultTable.Name());

    uint64_t count = (uint64_t)client->Get(resultTable.Name() + "/@row_count").AsInt64();
    if ((uint64_t)(trainPart * count) < count && !intputTestTableName.defined()) {
        const TTempTable resultTrainTable(client);
        const TTempTable resultTestTable(client);
        SplitTable(client, resultTable.Name(), resultTrainTable.Name(), resultTestTable.Name(), trainPart);
        INFO() << "Make result tfrecord for training";
        MakeTFRecord(client, resultTrainTable.Name(), classes, outputTrainPath.c_str(), augmHorzFlip, blurFlipedSign);
        INFO() << "Make result tfrecord for tests";
        MakeTFRecord(client, resultTestTable.Name(),  classes, outputTestPath.c_str());
    } else {
        INFO() << "Make result tfrecord for training";
        MakeTFRecord(client, resultTable.Name(), classes, outputTrainPath.c_str(), augmHorzFlip, blurFlipedSign);
        if (intputTestTableName.defined()) {
            INFO() << "Prepare test data from table: " << intputTestTableName.c_str();
            const TTempTable minszTestFilteredImgs(client);
            const TTempTable minszTestFilteredObjs(client);
            // use minSize = 0 because we want test on all sizes objects for not penalize detector if it detect small objects
            FilterByMinSize(client, intputTestTableName.c_str(), 0, minszTestFilteredImgs.Name(), minszTestFilteredObjs.Name());
            if (joinSpeedLimits) {
                JoinSpeedLimits(client, minszTestFilteredObjs.Name(), minszTestFilteredObjs.Name());
            }
            if (!renameClassTableName.empty()) {
                TMap<TString, TString> classesToMetaClasses = loadRenameClassTable(client, renameClassTableName.c_str());
                JoinMetaClasses(client, minszTestFilteredObjs.Name(), classesToMetaClasses);
            }
            const TTempTable minszTestValidClassesFiltered(client);
            FilterByValidClasses(client, minszTestFilteredObjs.Name(), classes, minszTestValidClassesFiltered.Name());
            const TTempTable resultTestTable(client);
            JoinValidSignsWithImages(client, minszTestFilteredImgs.Name(), minszTestValidClassesFiltered.Name(), resultTestTable.Name());
            INFO() << "Make result tfrecord for tests from table: " << intputTestTableName.c_str();
            MakeTFRecord(client, resultTestTable.Name(), classes, outputTestPath.c_str());
        } else {
            INFO() << "Tfrecord for tests will be empty, because trainPart parameter";
        }
    }
    SaveLabelMap(classes, outputLabelMapPath.c_str());
    return 0;
}
catch (const maps::Exception& e) {
    FATAL() << "Worker failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
