#include "topology_uploader.h"
#include "topology_data.h"
#include "db_strings.h"
#include "common.h"
#include "utils/geom_helpers.h"

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

#include <boost/lexical_cast.hpp>
#include <boost/format.hpp>

#include <vector>
#include <set>
#include <unordered_map>
#include <map>
#include <string>
#include <initializer_list>

#define SQLTYPE_BIGINT "bigint", pqxx::prepare::treat_direct
#define SQLTYPE_INTEGER "integer", pqxx::prepare::treat_direct
#define SQLTYPE_SMALLINT "smallint", pqxx::prepare::treat_direct
#define SQLTYPE_BOOLEAN "boolean",pqxx::prepare::treat_string
#define SQLTYPE_GEOMETRY "bytea", pqxx::prepare::treat_binary

namespace maps {
namespace wiki {
namespace topology_fixer {

namespace {

const std::map<TableName, std::string> tableNames = {
    {TableName::FT_FACE, TABLE_FT_FACE},
    {TableName::AD_FACE, TABLE_AD_FACE},
    {TableName::FT_EDGE, TABLE_FT_EDGE},
    {TableName::FACE_EDGE, TABLE_FACE_EDGE},
    {TableName::EDGE, TABLE_EDGE},
    {TableName::NODE, TABLE_NODE},
    {TableName::FACE, TABLE_FACE},
};

typedef std::vector<std::string> Strings;

const std::map<TableName, std::vector<std::string> > tableFields = {
    {TableName::FT_FACE, Strings({FIELD_FT_ID, FIELD_FACE_ID, FIELD_IS_INTERIOR})},
    {TableName::AD_FACE, Strings({FIELD_AD_ID, FIELD_FACE_ID, FIELD_IS_INTERIOR})},
    {TableName::FT_EDGE, Strings({FIELD_FT_ID, FIELD_EDGE_ID})},
    {TableName::FACE_EDGE, Strings({FIELD_FACE_ID, FIELD_EDGE_ID})},
    {TableName::EDGE, Strings({
        FIELD_EDGE_ID,
        FIELD_F_NODE_ID,
        FIELD_T_NODE_ID,
        FIELD_F_ZLEV,
        FIELD_T_ZLEV,
        FIELD_SHAPE
    })},
    {TableName::NODE, Strings({FIELD_NODE_ID, FIELD_SHAPE})},
    {TableName::FACE, Strings({FIELD_FACE_ID})},
};

std::string insertQueryHeader(TableName table) {
    std::string result = "INSERT INTO " + tableNames.at(table) + "(" + tableFields.at(table).front();
    for (uint i = 1; i < tableFields.at(table).size(); i++) {
        result += ", " + tableFields.at(table)[i];
    }
    result += ") VALUES ";
    return result;
}

template<typename Value>
inline std::string listString(const Value& value) {
    return boost::lexical_cast<std::string>(value);
}

template<typename First, typename ... Rest>
inline std::string listString(const First& first, const Rest& ... rest) {
    return boost::lexical_cast<std::string>(first) + ", " + listString(rest ...);
}

template<typename ... T>
inline std::string valuesString(const T& ... values) {
    return "(" + listString(values ...) + ")";
}

const size_t INSERT_ROW_COUNT = 1000;

class MultiInserter {
public:
    MultiInserter(TableName table, pqxx::transaction_base& work)
        : table_(table)
        , rowsCounter_(0)
        , work_(work)
    {
        query_ << insertQueryHeader(table_);
    }

    MultiInserter& operator << (const std::string& valueStr) {
        if (rowsCounter_++) {
            query_ << ", ";
        }

        query_ << valueStr;

        if (rowsCounter_ == INSERT_ROW_COUNT) {
            rowsCounter_ = 0;
            work_.exec(query_.str());
            query_.str("");
            query_ << insertQueryHeader(table_);
        }

        return *this;
    }

    void complete() {
        if (rowsCounter_) {
            work_.exec(query_.str());
        }
    }
private:
    TableName table_;
    size_t rowsCounter_;
    std::stringstream query_;
    pqxx::transaction_base& work_;
};

} // namespace


void TopologyUploader::saveFaceMasterRelations(const TopologyData& data)
{
    INFO() << "Uploading face-master relations";
    pqxx::work work(conn_);

    auto master = data.ftGroup()->id() == TopologyGroupId::Ad
        ? TableName::AD_FACE
        : TableName::FT_FACE;

    MultiInserter inserter(master, work);

    for (const auto& master: data.masters()) {
        for (const auto& faceRel: master.second.faceRels()) {
            inserter << valuesString(
                master.first,
                faceRel.first,
                faceRel.second == FaceRelationType::Interior ? "'t'" : "'f'");
        }
    }
    inserter.complete();
    work.commit();
    INFO() << "Uploading face-master relations: [Done]";
}

void TopologyUploader::saveEdgeMasterRelations(const TopologyData& data)
{
    ASSERT(data.ftGroup()->id() != TopologyGroupId::Ad);
    INFO() << "Uploading ft-edge relations";
    pqxx::work work(conn_);

    MultiInserter inserter(TableName::FT_EDGE, work);

    for (const auto& master: data.masters()) {
        for (auto edgeId : master.second.edgeIds()) {
            inserter << valuesString(master.first, edgeId);
        }
    }
    inserter.complete();
    work.commit();
    INFO() << "Uploading ft-edge relations: [Done]";
}

void TopologyUploader::saveFaces(const TopologyData& data)
{
    INFO() << "Uploading faces and face-edge relations";
    pqxx::work work(conn_);

    MultiInserter faceInserter(TableName::FACE, work);

    for (const auto& face: data.faces()) {
        faceInserter << valuesString(face.first);
    }
    faceInserter.complete();

    MultiInserter faceEdgeInserter(TableName::FACE_EDGE, work);

    size_t numRows = 0;
    for (const auto& face: data.faces()) {
        for (auto edgeId : face.second.edgeIds()) {
            faceEdgeInserter << valuesString(face.first, edgeId);
            ++numRows;
        }
    }
    faceEdgeInserter.complete();
    work.commit();
    INFO() << "Uploaded " << numRows << " face-edge relations";
    INFO() << "Uploading faces and face-edge relations: [Done]";
}

void TopologyUploader::saveNodes(const TopologyData& data)
{
    INFO() << "Uploading nodes";
    pqxx::work work(conn_);

    MultiInserter inserter(TableName::NODE, work);

    const SRID srid = data.srid();
    for (const auto& node: data.nodes()) {
        inserter << valuesString(
            node.first,
            "ST_GeomFromWKB('" + work.esc_raw(geomToWkb(node.second.point(), srid)) + "', 4326)");
    }
    inserter.complete();
    work.commit();
    INFO() << "Uploading nodes: [Done]";
}

void TopologyUploader::saveEdges(const TopologyData& data)
{
    INFO() << "Uploading edges";
    pqxx::work work(conn_);

    MultiInserter inserter(TableName::EDGE, work);

    const SRID srid = data.srid();
    for (const auto& edge: data.edges()) {

        auto attrs = edge.second.getAttrs();

        std::stringstream values;
        values << "(" << edge.first;
        for (size_t i = 1; i < tableFields.at(TableName::EDGE).size() - 1; i++) {
            values << ", " << attrs.at(tableFields.at(TableName::EDGE)[i]);
        }
        values << ", ST_GeomFromWKB" << valuesString(
            "'" + work.esc_raw(geomToWkb(edge.second.linestring(), srid)) + "'",
            4326) << ")";
        inserter << values.str();
    }

    inserter.complete();
    work.commit();
    INFO() << "Uploading edges:[Done]";
}

void TopologyUploader::uploadData(const TopologyData& data)
{
    if (data.ftGroup()->id() != TopologyGroupId::Ad &&
        data.ftGroup()->topologyType() == TopologyType::Contour)
    {
        saveFeatures(data);
        deleteFeatures(data);
    }
    saveNodes(data);
    saveEdges(data);
    if (data.ftGroup()->topologyType() == TopologyType::Contour) {
        saveFaces(data);
        saveFaceMasterRelations(data);
    }
    if (data.ftGroup()->topologyType() == TopologyType::Linear) {
        saveEdgeMasterRelations(data);
    }
}

namespace {

const size_t BATCH_SIZE = 1000;

void
execBatchQuery(pqxx::work& work, const std::string& queryPrefix,
    const std::list<DBIdType>& ids)
{
    if (!ids.empty()) {
        auto query = queryPrefix +
            (ids.size() == 1
                ? " = " + std::to_string(ids.front())
                : " IN (" + toString(ids) + ")");
        work.exec(query);
    }
}

void
deleteFtAdRelations(const TopologyData::DeletedFeaturesData& ftsData,
    pqxx::work& work)
{
    const auto queryPrefix = "DELETE FROM " + TABLE_FT_AD
        + " WHERE " + FIELD_FT_ID;
    std::list<DBIdType> ids;
    for (const auto& ftData : ftsData) {
        if (ftData.second.adId()) {
            ids.push_back(ftData.first);
        }
        if (ids.size() == BATCH_SIZE) {
            execBatchQuery(work, queryPrefix, ids);
            ids.clear();
        }
    }
    execBatchQuery(work, queryPrefix, ids);
}

void
deleteFtGeomRelations(const TopologyData::DeletedFeaturesData& ftsData,
    pqxx::work& work)
{
    const auto queryPrefix = "DELETE FROM " + TABLE_FT_GEOM
        + " WHERE " + FIELD_FT_ID;
    std::list<DBIdType> ids;
    for (const auto& ftData : ftsData) {
        ids.push_back(ftData.first);
        if (ids.size() == BATCH_SIZE) {
            execBatchQuery(work, queryPrefix, ids);
            ids.clear();
        }
    }
    execBatchQuery(work, queryPrefix, ids);
}

void
deleteFtCenterRelations(const TopologyData::DeletedFeaturesData& ftsData,
    pqxx::work& work)
{
    const auto ftCenterQueryPrefix = "DELETE FROM " + TABLE_FT_CENTER
        + " WHERE " + FIELD_FT_ID;
//    const auto nodeQueryPrefix = "DELETE FROM " + TABLE_NODE
//        + " WHERE " + FIELD_NODE_ID;
    std::list<DBIdType> ids;
//    std::list<DBIdType> nodeIds;
    for (const auto& ftData : ftsData) {
        if (ftData.second.centerId()) {
            ids.push_back(ftData.first);
//            nodeIds.push_back(*ftData.second.centerId());
        }
        if (ids.size() == BATCH_SIZE) {
            execBatchQuery(work, ftCenterQueryPrefix, ids);
            ids.clear();
        }
//        if (nodeIds.size() == BATCH_SIZE) {
//            execBatchQuery(work, nodeQueryPrefix, nodeIds);
//            nodeIds.clear();
//        }
    }
    execBatchQuery(work, ftCenterQueryPrefix, ids);
//    execBatchQuery(work, nodeQueryPrefix, nodeIds);
}

void
deleteFts(const TopologyData::DeletedFeaturesData& ftsData,
    pqxx::work& work)
{
    const auto queryPrefix = "DELETE FROM " + TABLE_FT
        + " WHERE " + FIELD_FT_ID;
    std::list<DBIdType> ids;
    for (const auto& ftData : ftsData) {
        ids.push_back(ftData.first);
        if (ids.size() == BATCH_SIZE) {
            execBatchQuery(work, queryPrefix, ids);
            ids.clear();
        }
    }
    execBatchQuery(work, queryPrefix, ids);
}

} // namespace

void TopologyUploader::deleteFeatures(const TopologyData& data)
{
    if (data.ftGroup()->id() == TopologyGroupId::Ad ||
        data.ftGroup()->topologyType() != TopologyType::Contour)
    {
        return;
    }

    const auto& deletedFtsData = data.deletedFeaturesData();
    pqxx::work work(conn_);
    deleteFtCenterRelations(deletedFtsData, work);
    deleteFtGeomRelations(deletedFtsData, work);
    deleteFtAdRelations(deletedFtsData, work);
    deleteFts(deletedFtsData, work);
    work.commit();
}


void TopologyUploader::saveFeatures(const TopologyData& data)
{
    if (data.ftGroup()->id() == TopologyGroupId::Ad ||
        data.ftGroup()->topologyType() != TopologyType::Contour)
    {
        return;
    }

    if (schemaName_ == origSchemaName_) {
        return;
    }

    pqxx::work work(conn_);
    auto ftsQuery = "SELECT " + FIELD_FT_ID +
        " FROM " + TABLE_FT + " WHERE " + data.ftGroup()->sqlFtTypeFilter(FIELD_FT_TYPE_ID);
    OrderedIdSet outputSchemaMasters;
    for (const auto& row : work.exec(ftsQuery)) {
        outputSchemaMasters.insert(row[0].as<DBIdType>());
    }

    OrderedIdSet currentMasters;
    const auto& mastersRange = data.masters();
    std::transform(
        mastersRange.begin(), mastersRange.end(),
        std::inserter(currentMasters, currentMasters.end()),
        [] (const std::pair<DBIdType, Master>& master) -> DBIdType
        {
            return master.first;
        });

    OrderedIdSet existingMasterIds;
    std::set_difference(
        currentMasters.begin(), currentMasters.end(),
        outputSchemaMasters.begin(), outputSchemaMasters.end(),
        std::inserter(existingMasterIds, existingMasterIds.end()));

    INFO() << existingMasterIds.size() << " masters absent in " << schemaName_ << " schema";

    if (existingMasterIds.empty()) {
        return;
    }

    boost::format queryPrefixFormat(
        "INSERT INTO " + schemaName_ + ".%1% " +
        " SELECT * FROM " + origSchemaName_ + ".%1% " +
        " WHERE " + FIELD_FT_ID);

    pqxx::connection conn(connStr_);
    pqxx::work ftTxn(conn);

    for (const auto& table : {TABLE_FT, TABLE_FT_AD, TABLE_FT_GEOM, TABLE_FT_CENTER}) {
        auto queryPrefix = (queryPrefixFormat % table).str();
        std::list<DBIdType> ids;
        for (auto id : existingMasterIds) {
            ids.push_back(id);
            if (ids.size() == BATCH_SIZE) {
                std::list<DBIdType> nids;
                nids.swap(ids);
                execBatchQuery(ftTxn, queryPrefix, nids);
            }
        }
        execBatchQuery(ftTxn, queryPrefix, ids);
    }

    ftTxn.commit();
}

} // namespace topology_fixer
} // namespace wiki
} // namespace maps
