#include <yandex/maps/wiki/outsource/config.h>

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/xml/include/xml.h>

#include <boost/lexical_cast.hpp>
#include <geos/geom/LineString.h>

namespace maps {
namespace wiki {
namespace outsource {

namespace da = diffalert;
namespace gl = geolib3;

namespace {

const std::string ATTR_FC = "fc";

const std::string ATTR_TASK_TYPE = "outsource_region:task_type";
const std::string ATTR_FC_SET = "outsource_region:fc_set";
const std::string ATTR_COMPLEXITY_RATE = "outsource_region:complexity_rate";
const std::string ATTR_COMPANY_ID = "outsource_region:company_id";
const std::string ATTR_OUTSOURCER_LOGIN = "outsource_region:outsourcer_login";
const std::string ATTR_QUALITY = "outsource_region:quality";

constexpr uint64_t RD_EL_FC_DEFAULT_VALUE = 7;
constexpr double DEFAULT_COMPLEXITY_RATE = 1.0;

typedef da::AoiDiffLoader Loader;

bool elementMatchesTaskAttributes(const da::OptionalObject& object, const FixedTaskAttributes& taskAttrs)
{
    if (!taskAttrs.fcSet) {
        return true;
    }
    if (object->categoryId() != "rd_el") {
        return true;
    }

    auto fc = object->attr(ATTR_FC).as<uint64_t>(RD_EL_FC_DEFAULT_VALUE);
    return taskAttrs.fcSet & (1 << (fc - 1));
}

double geoLengthKilometers(const da::Geom& geom)
{
    const auto* lineString = dynamic_cast<const geos::geom::LineString*>(geom.geosGeometryPtr());
    ASSERT(lineString);
    const auto* coordinates = lineString->getCoordinatesRO();
    double length = 0.0;
    for (size_t i = 1; i < coordinates->size(); ++i) {
        const auto& prevCoord = (*coordinates)[i - 1];
        const auto& coord = (*coordinates)[i];
        length += gl::geoDistance(
                gl::mercator2GeoPoint(gl::Point2(prevCoord.x, prevCoord.y)),
                gl::mercator2GeoPoint(gl::Point2(coord.x, coord.y)));
    }
    return length * 0.001;
}

double calcNewElementsLength(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& taskAttrs,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    double result = 0.0;
    for (const auto& diff : diffs) {
        if (!diff.oldObject() && diff.newObject() &&
            elementMatchesTaskAttributes(diff.newObject(), taskAttrs)) {
                result += geoLengthKilometers(diff.newObject()->geom());
        }
    }
    return result;
}

double calcChangedElementsLength(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& taskAttrs,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    double result = 0.0;
    for (const auto& diff : diffs) {
        if (diff.newObject() &&
            elementMatchesTaskAttributes(diff.newObject(), taskAttrs)) {
                result += geoLengthKilometers(diff.newObject()->geom());
        }
    }
    return result;
}

double calcElementsWithChangedGeomLength(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& taskAttrs,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    double result = 0.0;
    for (const auto& diff : diffs) {
        if (diff.geomChanged() && diff.newObject() &&
            elementMatchesTaskAttributes(diff.newObject(), taskAttrs)) {
                result += geoLengthKilometers(diff.newObject()->geom());
        }
    }
    return result;
}

double calcElementsWithChangedAttrsLength(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& taskAttrs,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    double result = 0.0;
    for (const auto& diff : diffs) {
        if (diff.attrsChanged() && diff.newObject() &&
            elementMatchesTaskAttributes(diff.newObject(), taskAttrs)) {
                result += geoLengthKilometers(diff.newObject()->geom());
        }
    }
    return result;
}

double countNewObjects(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& /*taskAttrs*/,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    return std::count_if(
            diffs.begin(), diffs.end(),
            [](const da::AoiDiffContext& diff) { return !diff.oldObject() && diff.newObject(); });
}

double countNewOrEditedObjects(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& /*taskAttrs*/,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    return std::count_if(
            diffs.begin(), diffs.end(),
            [](const da::AoiDiffContext& diff) { return diff.newObject(); });
}

double countObjectsWithChangedGeom(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& /*taskAttrs*/,
        const Loader& loader)
{
    auto diffs = loader.loadDiffContexts(categoryIds);
    return std::count_if(
            diffs.begin(), diffs.end(),
            [](const da::AoiDiffContext& diff) { return diff.geomChanged() && diff.newObject(); });
}

double countObjectsWithChangedParts(
        const std::set<std::string>& categoryIds,
        const FixedTaskAttributes& /*taskAttrs*/,
        const Loader& loader)
{
    auto counter = [&](const da::AoiDiffContext& diff)
    {
        if (!diff.newObject()) {
            return false;
        }

        const auto& geomPartRoles = loader.config().geomPartRoles(diff.categoryId());

        for (const auto& rel : diff.relationsAdded()) {
            if (rel.masterId == diff.objectId() && geomPartRoles.count(rel.role)) {
                return true;
            }
        }
        for (const auto& rel : diff.relationsDeleted()) {
            if (rel.masterId == diff.objectId() && geomPartRoles.count(rel.role)) {
                return true;
            }
        }

        return false;
    };

    auto diffs = loader.loadDiffContexts(categoryIds);
    return std::count_if(diffs.begin(), diffs.end(), counter);
}

struct CalcInfo
{
    template<typename Func>
    CalcInfo(
            const std::set<std::string>& categoryIds,
            Func calcWork_,
            Func calcCorrections_)
        : calcWork([=](const FixedTaskAttributes& taskAttrs, const Loader& loader){
            return calcWork_(categoryIds, taskAttrs, loader);
        })
        , calcCorrections([=](const FixedTaskAttributes& taskAttrs, const Loader& loader){
            return calcCorrections_(categoryIds, taskAttrs, loader);
        })
    {}

    template<typename Func>
    CalcInfo(const std::set<std::string>& categoryIds, Func calc)
        : CalcInfo(categoryIds, calc, calc)
    {}

    CalcDiff calcWork;
    CalcDiff calcCorrections;
};

const std::set<std::string> POI_CATEGORIES = {
    "poi_medicine",
    "poi_edu",
    "poi_finance",
    "poi_shopping",
    "poi_government",
    "poi_religion",
    "poi_food",
    "poi_auto",
    "poi_sport",
    "poi_leisure",
    "poi_urban",
    "poi_service",
    "poi_other",
};

const std::map<std::string, CalcInfo> TASK_TYPE_TO_CALC_INFO {
    { "rd_el_laying",
      CalcInfo({"rd_el"}, calcElementsWithChangedGeomLength)
    },
    { "rd_el_drawing",
      CalcInfo({"rd_el"}, calcNewElementsLength, calcElementsWithChangedGeomLength)
    },
    { "rd_el_drawing_and_redrawing",
      CalcInfo({"rd_el"}, calcElementsWithChangedGeomLength)
    },
    { "bld_drawing",
      CalcInfo({"bld"}, countNewOrEditedObjects)
    },
    { "rd_adding_parts",
      CalcInfo({"rd"}, countObjectsWithChangedParts)
    },
    { "addr_moving_to_base_layer",
      CalcInfo({"addr"}, countObjectsWithChangedGeom)
    },
    { "addr_moving_to_bld",
      CalcInfo({"addr"}, countObjectsWithChangedGeom)
    },
    { "rd_el_attrs",
      CalcInfo({"rd_el"}, calcElementsWithChangedAttrsLength)
    },
    { "rd_el_bicycle_attrs",
      CalcInfo({"rd_el"}, calcElementsWithChangedAttrsLength)
    },
    { "cond_drawing",
      CalcInfo({"cond"}, countNewOrEditedObjects)
    },
    { "parking_lot_linear_drawing",
      CalcInfo({"urban_roadnet_parking_lot_linear"}, calcChangedElementsLength)
    },
    { "hydro_laying",
      CalcInfo({"hydro_fc_el", "hydro_ln_el"}, calcElementsWithChangedGeomLength)
    },
    { "hydro_drawing",
      CalcInfo({"hydro_fc_el", "hydro_ln_el"}, calcNewElementsLength, calcElementsWithChangedGeomLength)
    },
    { "vegetation_laying",
      CalcInfo({"vegetation_el"}, calcElementsWithChangedGeomLength)
    },
    { "vegetation_drawing",
      CalcInfo({"vegetation_el"}, calcNewElementsLength, calcElementsWithChangedGeomLength)
    },
    { "islands_laying",
      CalcInfo({"relief_el"}, calcElementsWithChangedGeomLength)
    },
    { "islands_drawing",
      CalcInfo({"relief_el"}, calcNewElementsLength, calcElementsWithChangedGeomLength)
    },
    { "urban_roadnet_geom_editing",
      CalcInfo({"urban_roadnet_areal"}, countObjectsWithChangedGeom)
    },
    { "poi_moving_to_base_layer",
      CalcInfo(POI_CATEGORIES, countObjectsWithChangedGeom)
    },
    { "poi_moving_to_bld",
      CalcInfo(POI_CATEGORIES, countObjectsWithChangedGeom)
    },
    { "ad_drawing",
      CalcInfo({"ad_el"}, calcElementsWithChangedGeomLength, calcElementsWithChangedGeomLength)
    },
    { "addr_drawing_from_pano",
      CalcInfo({"addr"}, countNewObjects, countNewOrEditedObjects)
    },
    { "traffic_light_from_pano",
      CalcInfo({"cond_traffic_light"}, countNewOrEditedObjects)
    },
};

template<typename T>
T getAttrValue(
    const revision::ObjectRevision& rev,
    const std::string& attrName,
    const T& defaultValue)
{
    const auto& attrs = *rev.data().attributes;
    auto attrIt = attrs.find(attrName);

    return attrIt == attrs.end()
        ? defaultValue
        : boost::lexical_cast<T>(attrIt->second);
}

const std::string& getAttrValue(
    const revision::ObjectRevision& rev,
    const std::string& attrName)
{
    const auto& attrs = *rev.data().attributes;
    auto attrIt = attrs.find(attrName);
    REQUIRE(attrIt != attrs.end(),
            "revision id: " << rev.id() << " without " << attrName << " attribute");
    return attrIt->second;
}

} // namespace

FixedTaskAttributes::FixedTaskAttributes(const revision::ObjectRevision& rev)
{
    REQUIRE(rev.data().attributes, "revision id: " << rev.id() << " without attributes");

    taskType = getAttrValue(rev, ATTR_TASK_TYPE);
    fcSet = getAttrValue<int>(rev, ATTR_FC_SET, MAX_FC_SET);
    companyId = getAttrValue<std::string>(rev, ATTR_COMPANY_ID, "");
}

VaryingTaskAttributes::VaryingTaskAttributes(const revision::ObjectRevision& rev)
{
    REQUIRE(rev.data().attributes, "revision id: " << rev.id() << " without attributes");

    complexityRate = getAttrValue<double>(rev, ATTR_COMPLEXITY_RATE, DEFAULT_COMPLEXITY_RATE);
    outsourcerLogin = getAttrValue<std::string>(rev, ATTR_OUTSOURCER_LOGIN, "");
    quality = getAttrValue<std::string>(rev, ATTR_QUALITY, "");
}

Config::Config(const std::string& content)
{
    xml3::Doc doc(content, xml3::Doc::Source::String);

    auto companyNodes = doc.nodes("/config/companies/company");
    for (size_t i = 0; i < companyNodes.size(); ++i) {
        auto id = companyNodes[i].attr<std::string>("id");
        auto name = companyNodes[i].attr<std::string>("name");
        auto managerLogin = companyNodes[i].attr<std::string>("manager-login");
        auto taxIncluded = companyNodes[i].attr<bool>("tax-included");
        auto taxRate = companyNodes[i].attr<double>("tax-rate");
        auto rateId = companyNodes[i].attr<std::string>("rate-id");

        companiesById_.emplace(id,
            CompanyInfo{id, name, managerLogin, taxIncluded, taxRate, rateId});
    }

    auto ratesNodes = doc.nodes("/config/rates/task-types");
    for (size_t rI = 0; rI < ratesNodes.size(); ++rI) {
        const auto rateId = ratesNodes[rI].attr<std::string>("rate-id");
        auto taskTypeNodes = ratesNodes[rI].nodes("task-type");
        for (size_t i = 0; i < taskTypeNodes.size(); ++i) {
            auto id = taskTypeNodes[i].attr<std::string>("id");
            auto rate = taskTypeNodes[i].attr<double>("rate");
            auto labelRu = taskTypeNodes[i].attr<std::string>("label-ru");

            auto calcInfoIt = TASK_TYPE_TO_CALC_INFO.find(id);
            REQUIRE(calcInfoIt != TASK_TYPE_TO_CALC_INFO.end(),
                    "could not find calc info for task type: " << id);
            const auto& calcInfo = calcInfoIt->second;
            rateIdTaskTypesById_[rateId].emplace(
                    id,
                    TaskTypeInfo{id, calcInfo.calcWork, calcInfo.calcCorrections, rate, labelRu});
        }
    }
}

Config::CompanyInfo Config::companyInfo(const std::string& id) const
{
    auto it = companiesById_.find(id);
    return
        it != companiesById_.end()
        ? it->second
        : CompanyInfo{"", "", "", false, 1.0, "base"};
}

const Config::TaskTypeInfo& Config::taskTypeInfo(const std::string& id, const std::string& companyId) const
{
    const auto company = companyInfo(companyId);
    auto rateIt = rateIdTaskTypesById_.find(company.rateId);
    REQUIRE(rateIt != rateIdTaskTypesById_.end(), "Company " << companyId << " rate " << company.rateId << " not found.");
    const auto& taskTypesById = rateIt->second;
    auto it = taskTypesById.find(id);
    REQUIRE(it != taskTypesById.end(), "task type '" << id << "' not found");
    return it->second;
}

} // namespace outsource
} // namespace wiki
} // namespace maps
