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

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/wiki/threadutils/threadpool.h>

#include <boost/format.hpp>
#include <boost/iterator/iterator_facade.hpp>

#include <vector>
#include <string>
#include <thread>
#include <mutex>

namespace geolib = maps::geolib3;

namespace maps {
namespace wiki {
namespace topology_fixer {

namespace {

const std::string JOIN_FT = " INNER JOIN ft ON " + TABLE_FT + "." + FIELD_FT_ID + " = " + TABLE_FACE_MASTER_ALIAS + "." + FIELD_FT_ID;

std::string
groupQuery(const TopologyGroup& group)
{
    return group.id() == TopologyGroupId::Ad
        ? " "
        : JOIN_FT + " WHERE " + group.sqlFtTypeFilter(FIELD_FT_TYPE_ID);
};

const std::map<TableName, const std::string> masterTables = {
    {TableName::AD_FACE, "ad"},
    {TableName::FT_FACE, "ft"},
};


const std::string JOIN__FM__FACE_EDGE = " INNER JOIN " + TABLE_FACE_EDGE +
    " ON " + TABLE_FACE_EDGE + "." + FIELD_FACE_ID + " = " + TABLE_FACE_MASTER_ALIAS + "." + FIELD_FACE_ID;

const std::string JOIN__FT__FT_EDGE = " INNER JOIN " + TABLE_FT_EDGE +
    " ON " + TABLE_FT_EDGE + "." + FIELD_FT_ID + " = " + TABLE_FT + "." + FIELD_FT_ID;

const std::string JOIN__FACE_EDGE__EDGE = " INNER JOIN " + TABLE_EDGE +
    " ON " + TABLE_FACE_EDGE + "." + FIELD_EDGE_ID + " = " + TABLE_EDGE + "." + FIELD_EDGE_ID;

const std::string JOIN__FT_EDGE__EDGE = " INNER JOIN " + TABLE_EDGE +
    " ON " + TABLE_FT_EDGE + "." + FIELD_EDGE_ID + " = " + TABLE_EDGE + "." + FIELD_EDGE_ID;

const std::string JOIN__EDGE__NODE = " INNER JOIN " + TABLE_NODE +
    " ON (" + TABLE_EDGE + "." + FIELD_F_NODE_ID + " = " + TABLE_NODE + "." + FIELD_NODE_ID +
        " OR " + TABLE_EDGE + "." + FIELD_T_NODE_ID + " = " + TABLE_NODE + "." + FIELD_NODE_ID + ") ";

const std::string FROM_FM = " FROM %1%_face as " + TABLE_FACE_MASTER_ALIAS;

const std::string CUSTOM_QUERY = " %2% ";

const std::string CUSTOM_FIRST_QUERY = "%1%";


const std::string SELECT_NODES_PREFIX =
    " SELECT DISTINCT " +
        FIELD_NODE_ID +  " , " +
        " ST_AsBinary(" + TABLE_NODE + "." + FIELD_SHAPE + ") as " + FIELD_SHAPE;

const std::string SELECT_POLYGONAL_NODES(
    SELECT_NODES_PREFIX +
        FROM_FM + JOIN__FM__FACE_EDGE + JOIN__FACE_EDGE__EDGE + JOIN__EDGE__NODE + CUSTOM_QUERY
    );

const std::string SELECT_LINEAL_NODES(
    SELECT_NODES_PREFIX +
        " FROM " + TABLE_FT + JOIN__FT__FT_EDGE + JOIN__FT_EDGE__EDGE + JOIN__EDGE__NODE
        + " WHERE " + CUSTOM_FIRST_QUERY
    );


const std::string SELECT_EDGES_PREFIX =
    " SELECT DISTINCT " +
        TABLE_EDGE + "." + FIELD_EDGE_ID + " , " +
        " ST_AsBinary(" + FIELD_SHAPE + ") as " + FIELD_SHAPE + ", " +
        FIELD_F_NODE_ID + " , " +
        FIELD_T_NODE_ID + " , " +
        FIELD_F_ZLEV + " , " +
        FIELD_T_ZLEV;

const std::string SELECT_POLYGONAL_EDGES(
    SELECT_EDGES_PREFIX +
        FROM_FM + JOIN__FM__FACE_EDGE + JOIN__FACE_EDGE__EDGE + CUSTOM_QUERY);

const std::string SELECT_LINEAL_EDGES(
    SELECT_EDGES_PREFIX +
        " FROM " + TABLE_FT + JOIN__FT__FT_EDGE + JOIN__FT_EDGE__EDGE
        + " WHERE " + CUSTOM_FIRST_QUERY);


const std::string SELECT_FACE_EDGES(
    " SELECT DISTINCT " +
        TABLE_FACE_EDGE + "." + FIELD_FACE_ID + " , " +
        FIELD_EDGE_ID +
    FROM_FM + JOIN__FM__FACE_EDGE + CUSTOM_QUERY
    );

const std::string SELECT_MASTER_FACES(
    " SELECT " +
        TABLE_FACE_MASTER_ALIAS + ".%1%_id as " + FIELD_MASTER_ID + ", " +
        TABLE_FACE_MASTER_ALIAS + "." + FIELD_FACE_ID + " , " +
        FIELD_IS_INTERIOR +
        FROM_FM + CUSTOM_QUERY
    );


const std::string SELECT_FT_EDGES(
    " SELECT " +
        TABLE_FT + "." + FIELD_FT_ID + ", " +
        TABLE_FT_EDGE + "." + FIELD_EDGE_ID +
        " FROM " + TABLE_FT + JOIN__FT__FT_EDGE + " WHERE " + CUSTOM_FIRST_QUERY
    );


const std::string SELECT_NAMED_FT_IDS(
    " SELECT " + TABLE_FT + "." + FIELD_FT_ID
        + " FROM " + TABLE_FT + ", " + TABLE_FT_NM
        + " WHERE " + TABLE_FT + "." + FIELD_FT_ID + " = " + TABLE_FT_NM + "." + FIELD_FT_ID
        + " AND " + CUSTOM_FIRST_QUERY
        + " GROUP BY " + TABLE_FT + "." + FIELD_FT_ID
        + " HAVING COUNT(*) > 0");

const std::string SELECT_ADDR_ASSOCIATED_FT_IDS(
    " SELECT " + TABLE_FT + "." + FIELD_FT_ID
        + " FROM " + TABLE_FT + ", " + TABLE_ADDR
        + " WHERE " + TABLE_FT + "." + FIELD_FT_ID + " = " + TABLE_ADDR + "." + FIELD_FT_ID
        + " AND " + CUSTOM_FIRST_QUERY
        + " GROUP BY " + TABLE_FT + "." + FIELD_FT_ID
        + " HAVING COUNT(*) > 0");


const std::string SELECT_FT_CENTERS(
    "SELECT "
        + TABLE_FT + "." + FIELD_FT_ID + " AS " + FIELD_FT_ID
            + ", " + TABLE_FT_CENTER + "." + FIELD_NODE_ID + " AS " + FIELD_NODE_ID
        + " FROM " + TABLE_FT + ", " + TABLE_FT_CENTER
        + " WHERE " + TABLE_FT + "." + FIELD_FT_ID + " = " + TABLE_FT_CENTER + "." + FIELD_FT_ID
        + " AND " + CUSTOM_FIRST_QUERY);

const std::string SELECT_FT_ADS(
    "SELECT "
        + TABLE_FT + "." + FIELD_FT_ID + " AS " + FIELD_FT_ID
            + ", " + TABLE_FT_AD + "." + FIELD_AD_ID + " AS " + FIELD_AD_ID
        + " FROM " + TABLE_FT + ", " + TABLE_FT_AD
        + " WHERE " + TABLE_FT + "." + FIELD_FT_ID + " = " + TABLE_FT_AD + "." + FIELD_FT_ID
        + " AND " + CUSTOM_FIRST_QUERY);


const std::string SELECT_FT_ATTRIBUTES(
    "SELECT "
        + FIELD_FT_ID + ", " + FIELD_P_FT_ID + ", " + FIELD_FT_TYPE_ID
        + ", " + FIELD_DISP_CLASS + ", " + FIELD_SEARCH_CLASS
        + ", " + FIELD_ISOCODE + ", " + FIELD_SUBCODE
        + " FROM " + TABLE_FT
        + " WHERE " + CUSTOM_FIRST_QUERY);


inline std::string selectContourNodesQuery(
    TableName faceMaster, const TopologyGroup& group)
{
    return (boost::format(SELECT_POLYGONAL_NODES)
        % masterTables.at(faceMaster)
        % groupQuery(group)).str();
}

inline std::string selectLinearNodesQuery(const TopologyGroup& group)
{
    auto str = (boost::format(SELECT_LINEAL_NODES)
        % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str();
    INFO() << "Linear nodes query " << str;
    return str;
}


inline std::string selectContourEdgesQuery(
    TableName faceMaster, const TopologyGroup& group)
{
    return (boost::format(SELECT_POLYGONAL_EDGES)
        % masterTables.at(faceMaster)
        % groupQuery(group)).str();
}

inline std::string selectLinearEdgesQuery(const TopologyGroup& group)
{
    auto str = (boost::format(SELECT_LINEAL_EDGES)
        % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str();
    INFO() << "Linear edges query " << str;
    return str;
}

inline std::string selectFaceEdgesQuery(TableName faceMaster, const TopologyGroup& group) {
    return (boost::format(SELECT_FACE_EDGES)
        % masterTables.at(faceMaster)
        % groupQuery(group)).str();
}

inline std::string selectMasterEdgesQuery(const TopologyGroup& group) {
    auto str = (boost::format(SELECT_FT_EDGES)
        % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str();
    INFO() << "Select ft edges query " << str;
    return str;
}

inline std::string selectMasterFacesQuery(TableName faceMaster, const TopologyGroup& group) {
    return (boost::format(SELECT_MASTER_FACES)
        % masterTables.at(faceMaster)
        % groupQuery(group)).str();
}

std::unordered_map<MasterId, FtAttributes>
loadFtData(const TopologyGroup& group, pqxx::work& work)
{
    auto query = (boost::format(SELECT_FT_ATTRIBUTES)
        % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str();
    pqxx::result result = work.exec(query);
    INFO() << "Query " << query;
    std::unordered_map<MasterId, FtAttributes> ftData;
    for (const pqxx::row& row : result) {
        //make_optional prevents "maybe-uninitialized" warning
        auto parentId = boost::make_optional(false, MasterId());
        auto searchClass = boost::make_optional(false, SearchClassId());
        boost::optional<std::string> isocode;
        boost::optional<std::string> subcode;
        if (!row.at(FIELD_P_FT_ID).is_null()) {
            parentId = row.at(FIELD_P_FT_ID).as<MasterId>();
        }
        if (!row.at(FIELD_SEARCH_CLASS).is_null()) {
            searchClass = row.at(FIELD_SEARCH_CLASS).as<SearchClassId>();
        }
        if (!row.at(FIELD_ISOCODE).is_null()) {
            isocode = row.at(FIELD_ISOCODE).as<std::string>();
        }
        if (!row.at(FIELD_SUBCODE).is_null()) {
            subcode = row.at(FIELD_SUBCODE).as<std::string>();
        }
        ftData.insert({row.at(FIELD_FT_ID).as<MasterId>(),
            {row.at(FIELD_FT_TYPE_ID).as<FtTypeId>(),
             row.at(FIELD_DISP_CLASS).as<DispClassId>(),
             searchClass, parentId, isocode, subcode}});
    }
    return ftData;
}

std::unordered_set<MasterId>
ftIdsByQuery(const std::string& query, pqxx::work& work)
{
    pqxx::result result = work.exec(query);
    std::unordered_set<MasterId> ids;
    for (const pqxx::row& row : result) {
        ids.insert(row.at(FIELD_FT_ID).as<MasterId>());
    }
    return ids;
}

std::unordered_set<MasterId>
namedFtIds(const TopologyGroup& group, pqxx::work& work)
{
    return ftIdsByQuery(
        (boost::format(SELECT_NAMED_FT_IDS) % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str(),
        work);
}

std::unordered_set<MasterId>
addrAssociatedFtIds(const TopologyGroup& group, pqxx::work& work)
{
    return ftIdsByQuery(
        (boost::format(SELECT_ADDR_ASSOCIATED_FT_IDS)
            % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str(),
        work);
}


std::unordered_map<MasterId, NodeId>
ftCenters(const TopologyGroup& group, pqxx::work& work)
{
    auto query = (boost::format(SELECT_FT_CENTERS)
        % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str();
    pqxx::result result = work.exec(query);
    std::unordered_map<MasterId, NodeId> centers;
    for (const pqxx::row& row : result) {
        centers.insert({
            row.at(FIELD_FT_ID).as<MasterId>(),
            row.at(FIELD_NODE_ID).as<NodeId>()});
    }
    return centers;
}

std::unordered_map<MasterId, MasterId>
ftAds(const TopologyGroup& group, pqxx::work& work)
{
    auto query = (boost::format(SELECT_FT_ADS)
        % group.sqlFtTypeFilter(FIELD_FT_TYPE_ID)).str();
    pqxx::result result = work.exec(query);
    std::unordered_map<MasterId, MasterId> ads;
    for (const pqxx::row& row : result) {
        ads.insert({
            row.at(FIELD_FT_ID).as<MasterId>(),
            row.at(FIELD_AD_ID).as<MasterId>()});
    }
    return ads;
}


template <class Data>
class DataRange {
public:
    void append(std::list<Data>& newData)
    {
        std::lock_guard<std::mutex> guard(mutex_);
        data_.splice(data_.end(), newData);
    }

    const std::list<Data>& operator * () { return data_; }

private:
    std::mutex mutex_;
    std::list<Data> data_;
};

template <class Data>
class DataLoader {
public:
    typedef std::function<Data(const pqxx::row&)> ConstructData;

    DataLoader(
            const std::string& query,
            const std::string& connStr,
            const std::string& schemaName,
            ConstructData constructData,
            DataRange<Data>& data)
        : query_(query)
        , connStr_(connStr)
        , schemaName_(schemaName)
        , constructData_(constructData)
        , data_(data)
    {}

    void operator () ()
    {
        pqxx::connection conn(connStr_);
        pqxx::work work(conn);
        work.exec("SET search_path=" + schemaName_ + ",public");
        auto res = work.exec(query_);
        std::list<Data> loadedData;
        for (const auto& row : res) {
            loadedData.push_back(constructData_(row));
        }
        data_.append(loadedData);
    }

private:
    std::string query_;
    std::string connStr_;
    std::string schemaName_;
    ConstructData constructData_;
    DataRange<Data>& data_;
};

class NodeDataFromRow {
public:
    explicit NodeDataFromRow(SRID srid) : srid_(srid) {}

    NodeData operator () (const pqxx::row& row) const
    {
        return {
            row[FIELD_NODE_ID].as<DBIdType>(),
            geomFromWkb<geolib3::Point2>(
                pqxx::binarystring(row[FIELD_SHAPE]).str(), srid_)
        };
    }

private:
    SRID srid_;
};

class EdgeDataFromRow {
public:
    explicit EdgeDataFromRow(SRID srid) : srid_(srid) {}

    EdgeData operator () (const pqxx::row& row) const
    {
        return {
            row[FIELD_EDGE_ID].as<DBIdType>(),
            geomFromWkb<geolib3::Polyline2>(pqxx::binarystring(row[FIELD_SHAPE]).str(), srid_),
            row[FIELD_F_NODE_ID].as<DBIdType>(),
            row[FIELD_T_NODE_ID].as<DBIdType>(),
            row[FIELD_F_ZLEV].as<ZLevelType>(),
            row[FIELD_T_ZLEV].as<ZLevelType>()
        };
    }

private:
    SRID srid_;
};

typename DataLoader<FaceEdgeData>::ConstructData faceEdgeDataFromRow =
    [] (const pqxx::row& row) -> FaceEdgeData
    {
        return
            {row[FIELD_FACE_ID].as<DBIdType>(), row[FIELD_EDGE_ID].as<DBIdType>()};
    };

typename DataLoader<EdgeMasterData>::ConstructData edgeMasterDataFromRow =
    [] (const pqxx::row& row) -> EdgeMasterData
    {
        return
            {row[FIELD_FT_ID].as<DBIdType>(), row[FIELD_EDGE_ID].as<DBIdType>()};
    };

typename DataLoader<FaceMasterData>::ConstructData faceMasterDataFromRow =
    [] (const pqxx::row& row) -> FaceMasterData
    {
        return {
            row[FIELD_MASTER_ID].as<DBIdType>(),
            row[FIELD_FACE_ID].as<DBIdType>(),
            row[FIELD_IS_INTERIOR].as<bool>() ? FaceRelationType::Interior : FaceRelationType::Exterior
        };
    };

} // namespace


TopologyData TopologyLoader::loadData(const TopologyGroupPtr& group)
{
    INFO() << "SRID: " << srid_;

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

    DataRange<NodeData> nodesRange;
    DataRange<EdgeData> edgesRange;
    DataRange<FaceEdgeData> faceEdgesRange;
    DataRange<EdgeMasterData> masterEdgesRange;
    DataRange<FaceMasterData> masterFacesRange;

    const auto topologyType = group->topologyType();
    size_t poolSize = topologyType == TopologyType::Linear ? 3 : 4;
    ThreadPool pool(poolSize);
    if (topologyType == TopologyType::Linear) {
        pool.push(DataLoader<NodeData>(selectLinearNodesQuery(*group),
            connStr_, schemaName_, NodeDataFromRow(srid_), nodesRange));
        pool.push(DataLoader<EdgeData>(selectLinearEdgesQuery(*group),
            connStr_, schemaName_, EdgeDataFromRow(srid_), edgesRange));
        pool.push(DataLoader<EdgeMasterData>(
            selectMasterEdgesQuery(*group), connStr_, schemaName_, edgeMasterDataFromRow, masterEdgesRange));
    }
    if (topologyType == TopologyType::Contour) {
        pool.push(DataLoader<NodeData>(selectContourNodesQuery(master, *group),
            connStr_, schemaName_, NodeDataFromRow(srid_), nodesRange));
        pool.push(DataLoader<EdgeData>(selectContourEdgesQuery(master, *group),
            connStr_, schemaName_, EdgeDataFromRow(srid_), edgesRange));
        pool.push(DataLoader<FaceEdgeData>(
            selectFaceEdgesQuery(master, *group), connStr_, schemaName_, faceEdgeDataFromRow, faceEdgesRange));
        pool.push(DataLoader<FaceMasterData>(
            selectMasterFacesQuery(master, *group), connStr_, schemaName_, faceMasterDataFromRow, masterFacesRange));
    }
    pool.shutdown();

    TopologyData data(
        group,
        *nodesRange, *edgesRange, *faceEdgesRange, *masterEdgesRange, *masterFacesRange,
        gen_, srid_);
    if (group->id() != TopologyGroupId::Ad) {
        pqxx::connection conn(connStr_);
        pqxx::work work(conn);
        work.exec("SET search_path=" + schemaName_ + ",public");
        loadFeatureAttributes(data, work);
    }
    return data;
}

void
TopologyLoader::loadFeatureAttributes(TopologyData& data, pqxx::work& work)
{
    const TopologyGroup& group = *data.ftGroup();
    REQUIRE(group.id() != TopologyGroupId::Ad, "No feature attributes for group ad");
    auto ftData = loadFtData(group, work);
    for (auto namedFtId : namedFtIds(group, work)) {
        ftData.at(namedFtId).hasName_ = true;
    }
    for (auto associatedFtId : addrAssociatedFtIds(group, work)) {
        ftData.at(associatedFtId).isAddrAssociatedWith_ = true;
    }
    for (auto ftAdId : ftAds(group, work)) {
        ftData.at(ftAdId.first).adId_ = ftAdId.second;
    }
    for (auto ftCenterId : ftCenters(group, work)) {
        ftData.at(ftCenterId.first).centerId_ = ftCenterId.second;
    }
    for (const auto& d : ftData) {
        if (d.second.parentId()) {
            ftData.at(*d.second.parentId()).hasChildren_ = true;
        }
    }
    for (const auto& d : ftData) {
        data.setFtAttributes(d.first, d.second);
    }
}
} // namespace topology_fixer
} // namespace wiki
} // namespace maps
