#include "geometry_processor.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/coverage5/coverage.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/wiki/threadutils/threadpool.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <boost/none.hpp>

#include <pqxx/pqxx>
#include <cstdint>
#include <unordered_set>
#include <vector>


namespace maps {
namespace wiki {
namespace isocode {

namespace {

typedef int64_t ID;
typedef std::unordered_set<std::string> Isocodes;

const std::string UNKNOWN_ISOCODE_001 = "001";
const size_t IDS_BATCH_SIZE = 10000;

} // namespace


class GeometryProcessor::Impl
{
public:
    Impl(
        const std::string& coverageFile,
        const std::string& connString,
        const std::string& schemaName)
            : coverage_(coverageFile)
            , layer_(coverage_[coverage_.layersNames().front()])
            , connString_(connString)
            , schemaName_(schemaName)
    {
        checkRegions();
    }

    void processRdEl(size_t threads) const
    {
        createIsocodes<geolib3::Polyline2>("rd_el", "rd_el_id", threads);
    }

    void processRdJc(size_t threads) const
    {
        createIsocodes<geolib3::Point2>("rd_jc", "rd_jc_id", threads);
    }

    void processEdge(size_t threads) const
    {
        createIsocodes<geolib3::Polyline2>("edge", "edge_id", threads);
    }

    void processNode(size_t threads) const
    {
        createIsocodes<geolib3::Point2>("node", "node_id", threads);
    }

private:
    template <typename GeomType>
    void createIsocodes(
        const std::string& tableName,
        const std::string& idName,
        size_t threads) const
    {
        ASSERT(threads > 1);
        INFO() << tableName << " start, threads: " << threads;

        ThreadPool pool(threads - 1);

        createIsocodes<GeomType>(tableName, idName, pool);
        INFO() << tableName << " waiting";
        pool.shutdown();

        INFO() << tableName << " processed";
    }

    template <typename GeomType>
    void createIsocodes(
        const std::string& tableName,
        const std::string& idName,
        ThreadPool& pool) const
    {
        pqxx::connection conn(connString_);
        pqxx::work work(conn);

        std::unordered_set<ID> existedIds;
        INFO() << tableName << " loading existing data";
        for (const auto& row : work.exec(
                "SELECT " + idName +
                " FROM " + schemaName_ + "." + tableName + "_isocode")) {
            existedIds.insert(row[0].as<ID>());
        }
        INFO() << tableName << " existing isocodes, size: "  << existedIds.size();

        size_t count = 0;
        ID lastId = 0;
        for (;;) {
            std::vector<ID> ids;
            std::ostringstream query;
            query << "SELECT " << idName << " FROM " << schemaName_ << "." << tableName;
            query << " WHERE " << idName << " > " << lastId;
            query << " ORDER BY 1 LIMIT " << IDS_BATCH_SIZE;
            auto rows = work.exec(query.str());
            if (rows.empty()) {
                break;
            }
            ids.reserve(rows.size());
            for (const auto& row : rows) {
                lastId = row[0].as<ID>();
                if (!existedIds.count(lastId)) {
                    ids.push_back(lastId);
                }
            }
            count += ids.size();
            pool.push([=](){ batch<GeomType>(tableName, idName, ids); });

            if (rows.size() < IDS_BATCH_SIZE) {
                break;
            }
        }
        INFO() << tableName << " pushed, size: "  << count;
    }

    template <typename GeomType>
    void batch(
        const std::string& tableName,
        const std::string& idName,
        const std::vector<ID>& ids) const
    {
        if (ids.empty()) {
            return;
        }
        auto batchInfo = "batch " + tableName +
            " [" + std::to_string(ids[0]) + "," +
                   std::to_string(ids.back()) + "]";
        DEBUG() << batchInfo << " loading ids: " << ids.size();

        pqxx::connection conn(connString_);
        pqxx::work work(conn);

        auto rows = work.exec(
            "SELECT " + idName + ", ST_AsBinary(shape)"
            " FROM " + schemaName_ + "." + tableName +
            " WHERE " + idName + " IN (" + common::join(ids, ',') + ")");
        ASSERT(rows.size() == ids.size());

        DEBUG() << batchInfo << " calculating";

        std::list<std::string> values;
        for (const auto& row : rows) {
            auto id = row[0].as<std::string>();
            auto wkb = pqxx::binarystring(row[1]).str();

            for (const auto& isocode : calcIsocodes<GeomType>(id, wkb)) {
                values.push_back(id + ",'" + isocode + "'");
            }
        }
        if (!values.empty()) {
            DEBUG() << batchInfo << " writing";
            work.exec(
                "INSERT INTO " + schemaName_ + "." + tableName + "_isocode"
                " VALUES (" + common::join(values, "),(") + ")");
            work.commit();
        }
        DEBUG() << batchInfo << " done";
    }

    void checkRegions() const
    {
        pqxx::connection conn(connString_);
        pqxx::work work(conn);

        Isocodes isocodes;

        auto query =
            "SELECT isocode FROM " + schemaName_ + ".ad "
            " WHERE level_kind=1 AND g_ad_id IS NULL AND isocode IS NOT NULL";
        for (const auto& row : work.exec(query)) {
            isocodes.insert(row[0].as<std::string>());
        }
        INFO() << "ad isocodes: " << isocodes.size();
        auto regions = layer_.regions(boost::none);
        INFO() << "layer regions: " << regions.size();
        for (const auto& region : regions) {
            auto regionId = region.id();
            ASSERT(regionId);
            std::string isocode = region.metaData();
            REQUIRE(isocodes.count(isocode),
                "unknown isocode for layer region: " << *regionId << " : " << isocode);
        }
    }

    void addIsocodes(
        const std::string& id,
        const geolib3::Point2& point,
        Isocodes& isocodes) const
    {
        auto regions = layer_.regions(point, boost::none);
        if (regions.empty()) {
            if (isocodes.insert(UNKNOWN_ISOCODE_001).second) {
                DEBUG() << "no regions for " << id;
            }
            return;
        }
        for (const auto& region : regions) {
            isocodes.insert(region.metaData());
        }
    }

    template <typename GeomType>
    Isocodes calcIsocodes(
        const std::string& id,
        const std::string& wkb) const;

private:
    const coverage5::Coverage coverage_;
    const coverage5::Layer& layer_;
    const std::string connString_;
    const std::string schemaName_;
    const std::string coverageFile_;
};

template <>
Isocodes
GeometryProcessor::Impl::calcIsocodes<geolib3::Point2>(const std::string& id, const std::string& wkb) const
{
    std::istringstream rawStream(wkb);
    auto geom = geolib3::WKB::read<geolib3::Point2>(rawStream);

    std::unordered_set<std::string> isocodes;
    addIsocodes(id, geom, isocodes);
    return isocodes;
}

template <>
Isocodes
GeometryProcessor::Impl::calcIsocodes<geolib3::Polyline2>(const std::string& id, const std::string& wkb) const
{
    std::istringstream rawStream(wkb);
    auto geom = geolib3::WKB::read<geolib3::Polyline2>(rawStream);

    std::unordered_set<std::string> isocodes;
    for (const auto& point : geom.points()) {
        addIsocodes(id, point, isocodes);
    }
    return isocodes;
}


NONCOPYABLE_PIMPL_DEFINITIONS(GeometryProcessor);

GeometryProcessor::GeometryProcessor(
        const std::string& coverageFile,
        const std::string& connString,
        const std::string& schemaName)
    : impl_(new Impl(coverageFile, connString, schemaName))
{}

void
GeometryProcessor::processRdEl(size_t threads) const
{
    impl_->processRdEl(threads);
}

void
GeometryProcessor::processRdJc(size_t threads) const
{
    impl_->processRdJc(threads);
}

void
GeometryProcessor::processEdge(size_t threads) const
{
    impl_->processEdge(threads);
}

void
GeometryProcessor::processNode(size_t threads) const
{
    impl_->processNode(threads);
}

} // namespace isocode
} // namespace wiki
} // namespace maps
