#include "propagate_ad.h"
#include "helpers.h"

#include <boost/current_function.hpp>
#include <pqxx/pqxx>

#include <sstream>

namespace maps {
namespace wiki {
namespace isocode {

namespace {

enum class AdMode {Parent, Subst};

std::string
adToChildAdQuery(size_t pAdLevel, AdMode mode)
{
    std::stringstream fields;
    std::stringstream tables;
    std::stringstream predicates;

    switch (mode) {
        case AdMode::Parent:
            fields << "ad" << pAdLevel << ".ad_id, ";
            break;
        case AdMode::Subst:
            fields << "ad_subst.ad_id, ";
            tables << "ad ad_subst, ";
            predicates << "ad_subst.g_ad_id = ad" << pAdLevel << ".ad_id AND "
                << "ad_subst.p_ad_id IS NULL AND ";
            break;
    }

    fields << "ad0.isocode ";

    tables << "ad ad0";
    for (size_t i = 1; i <= pAdLevel; ++i) {
        tables << ", ad ad" << i;
    }

    predicates << "ad0.level_kind=1 "
        << "AND ad0.g_ad_id IS NULL "
        << "AND ad0.p_ad_id IS NULL";
    for (size_t i = 1; i <= pAdLevel; ++i) {
        predicates << " AND ad" << i << ".p_ad_id = ad" << i-1 << ".ad_id ";
    }

    std::stringstream query;
    query << "INSERT INTO ad_isocode"
        << " SELECT " << fields.str()
        << " FROM " << tables.str()
        << " WHERE " << predicates.str();

    return query.str();
}

void
adToChildAd(pqxx::work& work)
{
    for (size_t parentLevel = 0; ; ++parentLevel) {
        std::string taskName = "adToChildAd_" + std::to_string(parentLevel);
        auto res = exec(work, adToChildAdQuery(parentLevel, AdMode::Parent), taskName);
        if (res.affected_rows() == 0) {
            break;
        }
        taskName = "adToChildAd_" + std::to_string(parentLevel) + "_subst";
        exec(work, adToChildAdQuery(parentLevel, AdMode::Subst), taskName);
    }
}

void
adToCenter(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode "
        "SELECT node_id, isocode "
        "FROM ad_center "
        "JOIN ad_isocode USING(ad_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
adToFace(pqxx::work& work)
{
    auto query =
        "INSERT INTO face_isocode "
        "("
            "SELECT face_id, isocode "
            "FROM ad_face "
            "JOIN ad_isocode USING(ad_id) "
            "UNION "
            "SELECT face_id, isocode "
            "FROM ad_face_patch "
            "JOIN ad_isocode USING(ad_id)"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

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

void
edgeToNodeFrom(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode "
            "SELECT DISTINCT f_node_id, isocode "
            "FROM edge "
            "JOIN edge_isocode USING(edge_id)";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

void
edgeToNodeTo(pqxx::work& work)
{
    auto query =
        "INSERT INTO node_isocode ("
            "SELECT t_node_id, isocode "
            "FROM edge "
            "JOIN edge_isocode USING(edge_id) "
            "EXCEPT "
            "SELECT node_id, isocode "
            "FROM node_isocode"
        ")";
    exec(work, query, BOOST_CURRENT_FUNCTION);
}

} // namespace

void
propagateAd(
    const std::string& connString,
    const std::string& schemaName)
{
    pqxx::connection conn(connString);

    auto execCommit = [&](const std::function<void(pqxx::work&)>& f)
    {
        pqxx::work work(conn);
        work.exec("SET search_path=" + schemaName + ",public");
        f(work);
        work.commit();
    };

    execCommit(adToChildAd);
    execCommit(adToFace);
    execCommit(faceToEdge);
    execCommit(edgeToNodeFrom);
    execCommit(edgeToNodeTo);
    execCommit(adToCenter);
}

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