#include "propagate_geometry.h"
#include "helpers.h"

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

#include <yandex/maps/wiki/threadutils/threadpool.h>
#include <yandex/maps/wiki/threadutils/scheduler.h>

#include <boost/current_function.hpp>

#include <pqxx/pqxx>
#include <cstdint>

namespace maps {
namespace wiki {
namespace isocode {

namespace {

void
edgeToFace(pqxx::work& work)
{
    auto query =
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM face_edge "
            "JOIN edge_isocode USING(edge_id) "
            "EXCEPT "
            "SELECT face_id, isocode "
            "FROM face_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
geomToFt(pqxx::work& work)
{
    auto query =
        "INSERT INTO ft_isocode "
        "("
            "SELECT ft_id, isocode "
            "FROM ft_face "
            "JOIN face_isocode USING(face_id) "
            "UNION "
            "SELECT ft_id, isocode "
            "FROM ft_edge "
            "JOIN edge_isocode USING(edge_id) "
            "UNION "
            "SELECT ft_id, isocode "
            "FROM ft_center "
            "JOIN node_isocode USING(node_id) "
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
faceToBld(pqxx::work& work)
{
    auto query =
        "INSERT INTO bld_isocode "
        "SELECT DISTINCT bld_id, isocode "
        "FROM bld_face "
        "JOIN face_isocode USING(face_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToRd(pqxx::work& work)
{
    auto query =
        "INSERT INTO rd_isocode "
        "SELECT DISTINCT rd_id, rd_el_isocode.isocode "
        "FROM rd_rd_el "
        "JOIN rd_el_isocode USING(rd_el_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
adToAddr(pqxx::work& work)
{
    auto query =
        "INSERT INTO addr_isocode "
        "SELECT addr_id, ad_isocode.isocode "
        "FROM addr "
        "JOIN ad_isocode USING(ad_id) "
        "JOIN node_isocode USING(node_id) "
        "WHERE ad_isocode.isocode=node_isocode.isocode";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdToAddr(pqxx::work& work)
{
    auto query =
        "INSERT INTO addr_isocode "
        "SELECT addr_id, rd_isocode.isocode "
        "FROM addr "
        "JOIN rd_isocode USING(rd_id) "
        "JOIN node_isocode USING(node_id) "
        "WHERE rd_isocode.isocode=node_isocode.isocode";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToAddr(pqxx::work& work)
{
    auto query =
        "INSERT INTO addr_isocode "
        "SELECT addr_id, ft_isocode.isocode "
        "FROM addr "
        "JOIN ft_isocode USING(ft_id) "
        "JOIN node_isocode USING(node_id) "
        "WHERE ft_isocode.isocode=node_isocode.isocode";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
bldToModel3d(pqxx::work& work)
{
    auto query =
        "INSERT INTO model3d_isocode "
        "SELECT DISTINCT model3d_id, bld_isocode.isocode "
        "FROM bld "
        "JOIN bld_isocode USING(bld_id) "
        "WHERE model3d_id IS NOT NULL";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
bldToPBld(pqxx::work& work)
{
    auto query =
        "INSERT INTO bld_isocode "
        "("
            "SELECT p_bld_id, bld_isocode.isocode "
            "FROM bld "
            "JOIN bld_isocode USING(bld_id) "
            "WHERE p_bld_id IS NOT NULL "
            "EXCEPT "
            "SELECT bld_id, isocode "
            "FROM bld_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToCond(pqxx::work& work)
{
    auto query =
        "INSERT INTO cond_isocode "
        "SELECT DISTINCT cond_id, isocode "
        "FROM cond "
        "JOIN cond_rd_seq USING(cond_seq_id) "
        "JOIN rd_el_isocode USING(rd_el_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToFace(pqxx::work& work)
{
    auto query =
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM ft_face "
            "JOIN ft_isocode USING(ft_id) "
            "EXCEPT "
            "SELECT face_id, isocode "
            "FROM face_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
ftToCenter(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode "
        "("
            "SELECT node_id, isocode "
            "FROM ft_center "
            "JOIN ft_isocode USING(ft_id) "
            "EXCEPT "
            "SELECT node_id, isocode "
            "FROM node_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
bldToFace(pqxx::work& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_bld "
        "AS "
        "("
            "SELECT bld_id "
            "FROM bld_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_bld(bld_id);"
        ""
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM crossborder_bld "
            "JOIN bld_face USING(bld_id) "
            "JOIN bld_isocode USING(bld_id) "
            "EXCEPT "
            "SELECT face_id, isocode "
            "FROM face_isocode"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
condToRdEl(pqxx::work& work)
{
    auto query =
        "INSERT INTO rd_el_isocode "
        "("
            "SELECT rd_el_id, cond_isocode.isocode "
            "FROM cond_rd_seq "
            "JOIN cond USING(cond_seq_id) "
            "JOIN cond_isocode USING(cond_id) "
            "EXCEPT "
            "SELECT rd_el_id, isocode "
            "FROM rd_el_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
rdElToRdJc(pqxx::work& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_rd_el "
        "AS "
        "("
            "SELECT rd_el_id "
            "FROM rd_el_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_rd_el(rd_el_id);"
        ""
        "INSERT INTO rd_jc_isocode"
        "("
            "("
                "SELECT f_rd_jc_id, rd_el_isocode.isocode "
                "FROM crossborder_rd_el "
                "JOIN rd_el USING(rd_el_id) "
                "JOIN rd_el_isocode USING (rd_el_id) "
                "UNION "
                "SELECT t_rd_jc_id, rd_el_isocode.isocode "
                "FROM crossborder_rd_el "
                "JOIN rd_el USING(rd_el_id) "
                "JOIN rd_el_isocode USING (rd_el_id)"
            ") "
            "EXCEPT "
            "SELECT rd_jc_id, isocode "
            "FROM rd_jc_isocode"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
faceToEdge(pqxx::work& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_face "
        "AS "
        "("
            "SELECT face_id "
            "FROM face_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_face(face_id);"
        ""
        "INSERT INTO edge_isocode "
        "("
            "SELECT edge_id, isocode "
            "FROM crossborder_face "
            "JOIN face_isocode USING(face_id) "
            "JOIN face_edge USING(face_id) "
            "EXCEPT "
            "SELECT edge_id, isocode "
            "FROM edge_isocode"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
edgeToNode(pqxx::work& work)
{
    auto query =
        "CREATE TEMPORARY TABLE crossborder_edge "
        "AS "
        "("
            "SELECT edge_id "
            "FROM edge_isocode "
            "GROUP BY 1 "
            "HAVING count(isocode) >= 2"
        ");"
        "CREATE INDEX ON crossborder_edge(edge_id);"
        ""
        "INSERT INTO node_isocode"
        "("
            "("
                "SELECT f_node_id, isocode "
                "FROM crossborder_edge "
                "JOIN edge USING(edge_id) "
                "JOIN edge_isocode USING (edge_id) "
                "UNION "
                "SELECT t_node_id, isocode "
                "FROM crossborder_edge "
                "JOIN edge USING(edge_id) "
                "JOIN edge_isocode USING (edge_id) "
            ")"
            "EXCEPT "
            "SELECT node_id, isocode "
            "FROM node_isocode"
        ");";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

} // namespace


void
propagateGeometry(
    const std::string& connString,
    const std::string& schemaName,
    size_t threads,
    std::shared_ptr<std::atomic<bool>> fail)
{
    auto execCommit = [=](
        std::function<void(pqxx::work&)> f, const std::string& funcName)
    {
        safeRunner(
            [&] {
                pqxx::connection conn(connString);
                pqxx::work work(conn);
                work.exec("SET search_path=" + schemaName + ",public");
                f(work);
                work.commit();
            },
            "propagateGeometry " + funcName,
            fail);
    };
# define EXEC_COMMIT(f) execCommit(f, #f)

    ThreadPool pool(threads);
    auto executor = [&](Scheduler::Runner runner) { pool.push(runner); };

    Scheduler scheduler;
    auto edgeToFaceId = scheduler.addTask(
        [&] { EXEC_COMMIT(edgeToFace); }, executor, {});

    auto rdElToRdId = scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToRd); }, executor, {});

    auto geomToFtId = scheduler.addTask(
        [&] { EXEC_COMMIT(geomToFt); }, executor, {edgeToFaceId});

    auto faceToBldId = scheduler.addTask(
        [&] { EXEC_COMMIT(faceToBld); }, executor, {edgeToFaceId});

    /* auto bldToModel3dId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(bldToModel3d); }, executor, {faceToBldId});

    auto rdElToCondId = scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToCond); }, executor, {}); //{rdElToRdId}

    /* auto adToAddrId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(adToAddr); }, executor, {});

    /* auto rdToAddrId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(rdToAddr); }, executor, {rdElToRdId});

    /* auto ftToAddrId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(ftToAddr); }, executor, {geomToFtId});

    //auto ftToFaceId = scheduler.addTask(
    //    [&] { EXEC_COMMIT(ftToFace); }, executor, {geomToFtId});

    /* auto ftToCenterId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(ftToCenter); }, executor, {geomToFtId});

    auto bldToFaceId = scheduler.addTask(
        [&] { EXEC_COMMIT(bldToFace); }, executor, {faceToBldId});

    /* auto bldToPBldId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(bldToPBld); }, executor, {faceToBldId});

    auto condToRdElId = scheduler.addTask(
        [&] { EXEC_COMMIT(condToRdEl); }, executor, {rdElToCondId});

    /* auto rdElToRdJcId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(rdElToRdJc); }, executor, {condToRdElId});

    auto faceToEdgeId = scheduler.addTask(
        [&] { EXEC_COMMIT(faceToEdge); }, executor, {/*ftToFaceId, */bldToFaceId});

    /* auto edgeToNodeId = */ scheduler.addTask(
        [&] { EXEC_COMMIT(edgeToNode); }, executor, {faceToEdgeId});

    scheduler.executeAll();
    pool.shutdown();
}

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