#include <maps/libs/log8/include/log8.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/conversion.h>

#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/json/include/std.h>
#include <maps/libs/ymapsdf/include/rd.h>

namespace {

struct Edge {
    uint64_t edgeId;

    int speedLimitF = 0;
    int speedLimitT = 0;
    int speedLimitTruckF = 0;
    int speedLimitTruckT = 0;
    int oneWay = 3; //maps/libs/ymapsdf/include/rd.h enum class Direction ; Forward = fromNodeId -> toNodeId, Backward = toNodeId -> fromNodeId
    int fc = 0;
    double length = 0.;
};
using Edges = std::vector<Edge>;
using MapEdges = std::map<uint64_t, Edge>;

Edge loadEdge(const maps::json::Value& value) {
    Edge edge;
    edge.edgeId = value["id"].as<uint64_t>();
    edge.speedLimitF = value["speed_limit_f"].as<int>();
    edge.speedLimitT = value["speed_limit_t"].as<int>();
    edge.speedLimitTruckF = value["speed_limit_truck_f"].as<int>();
    edge.speedLimitTruckT = value["speed_limit_truck_t"].as<int>();
    if (value.hasField("oneway")) {
        edge.oneWay = value["oneway"].as<int>();
    } else {
        edge.oneWay = (int)maps::ymapsdf::rd::Direction::Both;
    }
    if (value.hasField("fc")) {
        edge.fc = value["fc"].as<int>();
    } else {
        edge.fc = 0;
    }
    maps::json::Value jsonPoints = value["points"];
    REQUIRE(jsonPoints.size() > 0, "Edge with empty array of points");

    maps::geolib3::Polyline2 pln;
    for (size_t i = 0; i < jsonPoints.size(); i++) {
        pln.add(maps::geolib3::Point2(jsonPoints[i][0].as<double>(), jsonPoints[i][1].as<double>()));
    }
    edge.length = maps::geolib3::toMeters(maps::geolib3::length(pln), pln.pointAt(0));

    return edge;
}

Edges loadEdges(const std::string& jsonPath) {
    Edges edges;
    maps::json::Value jsonFile = maps::json::Value::fromFile(jsonPath);
    maps::json::Value jsonEdges = jsonFile["rd_el"];
    for (size_t i = 0; i < jsonEdges.size(); i++) {
        edges.emplace_back(loadEdge(jsonEdges[i]));
    }
    return edges;
}

MapEdges loadMapEdges(const std::string& jsonPath) {
    MapEdges edges;
    maps::json::Value jsonFile = maps::json::Value::fromFile(jsonPath);
    maps::json::Value jsonEdges = jsonFile["rd_el"];
    for (size_t i = 0; i < jsonEdges.size(); i++) {
        const Edge edge = loadEdge(jsonEdges[i]);
        edges[edge.edgeId] = edge;
    }
    return edges;
}

struct Statistic
{
    size_t gtEdgesCount = 0;
    size_t gtEdgesDirectionCount = 0;
    size_t tstEdgesNotFound = 0;
    size_t validSpeedLimits = 0;

    double gtEdgesDirectionLength = 0.;
    double validEdgesLength = 0.;
    std::map<int, size_t> mapGTSpeedLimitCount;
};

void printStatistic(const Statistic& stat)
{
    if (stat.gtEdgesCount == 0)
        return;

    INFO() << "GT edges count:                   " << stat.gtEdgesCount;
    INFO() << "GT edges directions count:        " << stat.gtEdgesDirectionCount;
    INFO() << "TST edges not found:              " << stat.tstEdgesNotFound << " (" << 100.*stat.tstEdgesNotFound / stat.gtEdgesCount << "%)";
    INFO() << "----------------------------------";
    INFO() << "Ground truth by class count:";
    for (auto it = stat.mapGTSpeedLimitCount.begin(); it != stat.mapGTSpeedLimitCount.end(); it++) {
        INFO() << "  " << it->first << " kmh: " << it->second;
    }
    INFO() << "----------------------------------";
    INFO() << "Valid speed limit (by direction): " << stat.validSpeedLimits << " (" << 100.*stat.validSpeedLimits / stat.gtEdgesDirectionCount << "%)";
    if (stat.gtEdgesDirectionLength > 1E-6) {
        INFO() << "Valid speed limit length (by direction): " << stat.validEdgesLength << " (" << 100.*stat.validEdgesLength / stat.gtEdgesDirectionLength << "%)";
    }
}

void printStatistic(
    const Edges& gtEdges,
    const MapEdges& tstEdges,
    bool onlySummary)
{
    std::array<Statistic, 11> statOnFClass;
    Statistic statAll;
    for (size_t i = 0; i < gtEdges.size(); i++) {
        const Edge& edge = gtEdges[i];
        if (edge.fc >= 8)
            continue;
        Statistic& statFC = statOnFClass[edge.fc];

        statAll.gtEdgesCount++;
        statFC.gtEdgesCount++;
        auto it = tstEdges.find(edge.edgeId);
        if (it == tstEdges.end())
        {
            statAll.tstEdgesNotFound++;
            statFC.tstEdgesNotFound++;
            if ((edge.oneWay & (int)maps::ymapsdf::rd::Direction::Forward) &&
                (-1 != edge.speedLimitF))
            {
                statAll.gtEdgesDirectionCount++;
                statAll.gtEdgesDirectionLength += edge.length;
                statAll.mapGTSpeedLimitCount[edge.speedLimitF]++;
                statFC.gtEdgesDirectionCount++;
                statFC.mapGTSpeedLimitCount[edge.speedLimitF]++;

            }
            if ((edge.oneWay & (int)maps::ymapsdf::rd::Direction::Backward) &&
                (-1 != edge.speedLimitT))
            {
                statAll.gtEdgesDirectionCount++;
                statAll.gtEdgesDirectionLength += edge.length;
                statAll.mapGTSpeedLimitCount[edge.speedLimitT]++;
                statFC.gtEdgesDirectionCount++;
                statFC.mapGTSpeedLimitCount[edge.speedLimitT]++;
            }
            continue;
        }

        if ((edge.oneWay & (int)maps::ymapsdf::rd::Direction::Forward) &&
            (-1 != edge.speedLimitF))
        {
            statAll.gtEdgesDirectionCount++;
            statAll.gtEdgesDirectionLength += edge.length;
            statAll.mapGTSpeedLimitCount[edge.speedLimitF]++;
            statFC.gtEdgesDirectionCount++;
            statFC.mapGTSpeedLimitCount[edge.speedLimitF]++;
            if (edge.speedLimitF == it->second.speedLimitF) {
                statAll.validEdgesLength += edge.length;
                statAll.validSpeedLimits++;
                statFC.validSpeedLimits++;
            }
        }
        if ((edge.oneWay & (int)maps::ymapsdf::rd::Direction::Backward) &&
            (-1 != edge.speedLimitT))
        {
            statAll.gtEdgesDirectionCount++;
            statAll.gtEdgesDirectionLength += edge.length;
            statAll.mapGTSpeedLimitCount[edge.speedLimitT]++;
            statFC.gtEdgesDirectionCount++;
            statFC.mapGTSpeedLimitCount[edge.speedLimitT]++;
            if (edge.speedLimitT == it->second.speedLimitT) {
                statAll.validEdgesLength += edge.length;
                statAll.validSpeedLimits++;
                statFC.validSpeedLimits++;
            }
        }
    }
    if (!onlySummary) {
        INFO() << "Functional class undefined ";
        printStatistic(statOnFClass[0]);
        INFO() << "----------------------------------";
        INFO() << "";
        for (size_t i = 1; i < 11; i++) {
            INFO() << "Functional class: " << i;
            printStatistic(statOnFClass[i]);
            INFO() << "----------------------------------";
            INFO() << "";
        }
    }
    INFO() << "Full statistic";
    printStatistic(statAll);
    INFO() << "----------------------------------";
}

} //namespace

int main(int argc, const char** argv) try {
    maps::cmdline::Parser parser("Test");

    maps::cmdline::Option<std::string> gtJsonPath = parser.string("gt-json")
        .required()
        .help("Path to json file with ground truth data");

    maps::cmdline::Option<std::string> tstJsonPath = parser.string("tst-json")
        .required()
        .help("Path to json file with test data");

    maps::cmdline::Option<bool> onlySummary = parser.flag("only-summary")
        .help("Print only summary statistic");

    parser.parse(argc, const_cast<char**>(argv));
    Edges gtEdges = loadEdges(gtJsonPath);
    MapEdges tstEdges = loadMapEdges(tstJsonPath);

    printStatistic(gtEdges, tstEdges, onlySummary);

    return EXIT_SUCCESS;
}
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;
}
