#include "misc.h"
#include "registry.h"
#include "calc.h"
#include "generic.h"
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/category_traits.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/point_object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/areal_object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/complex_object.h>

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/common/natural_sort.h>
#include <yandex/maps/wiki/common/string_utils.h>

namespace maps {
namespace wiki {
namespace srv_attrs {

namespace {

void
addScreenLabelToMasterDepends(ServiceAttribute& attrReg, const std::string& categoryId)
{
    // Go trough all categories and find masters for given one
    // We can't use .masterRoles since it doesn't contain
    // all masters due to not unique role used as key
    for (const auto& idCat : cfg()->editor()->categories()) {
        const auto& masterCat = idCat.second;
        for (const auto& slaveRole : masterCat.slavesRoles()) {
            if (slaveRole.categoryId() != categoryId) {
                continue;
            }
            const auto& masterCatId = masterCat.id();
            const auto& masterNameAttr = nameAttrId(masterCatId);
            if (!masterNameAttr.empty()) {
                attrReg.depends(
                    {
                        {Aspect::Type::Attribute, masterNameAttr},
                        {{RelationType::Master, slaveRole.roleId(), masterCatId}}
                    });
            }
            const auto& masterFtTypeAttr = ftTypeAttrDefForCategory(masterCatId);
            if (masterFtTypeAttr) {
                attrReg.depends(
                {
                    {Aspect::Type::Attribute, masterFtTypeAttr->id()},
                    {{RelationType::Master, slaveRole.roleId(), masterCatId}}
                });
            }
        }
    }
}

} // namespace

class ADDR : public ServiceAttributesRegistry::Registrar
{
public:
    ADDR(ServiceAttributesRegistry& registry):ServiceAttributesRegistry::Registrar(registry, CATEGORY_ADDR)
    {
        auto& attrReg = registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<PointObject>(screenLabel));
        addScreenLabelToMasterDepends(attrReg, CATEGORY_ADDR);
        registerAttr(SRV_RENDER_LABEL, CallbackWrapper<PointObject>(renderLabel));
        registerAttr(SRV_HAS_ZIPCODE,
                     CallbackWrapper<PointObject>(hasZipcode));
        registerAttr(ADDR_IS_RELATED_TO_ROAD_WITHOUT_GEOMETRY,
                     CallbackWrapper<PointObject>(isRelatedToRoadWithoutGeometry))
            .depends(
            {
                {Aspect::Type::Relations, STR_TO_ATTR_OWNER},
                {{RelationType::Master, ROLE_ASSOCIATED_WITH, CATEGORY_RD}}
            })
            .setIsAffectedByCallback(isAffectedByRoadWithoutGeometry);
    }

private:
    static bool isAffectedByRoadWithoutGeometry(const GeoObject* obj)
    {
        REQUIRE(obj->categoryId() == CATEGORY_RD,
            "Unexpected object " << obj->id() << " of category '" << obj->categoryId() << "'");

        auto diff = obj->slaveRelations().diff(ROLE_PART);
        if (diff.empty()) {
            return false; //Nothing changed
        }
        if (!diff.deleted.empty()) {
            return false; //Road had previously some rd_els that get deleted. Suppose that we can't delete all rd_els
        }
        auto range = obj->slaveRelations().range(ROLE_PART, {diff.added.size(), RelativesLimit::Type::All});
        if (!range) {
            return false; //Road has more rd_els than added count
        }
        return true;
    }

    static std::string isRelatedToRoadWithoutGeometry(const PointObject* addr, ObjectsCache&)
    {
        for (const auto& rel : addr->masterRelations().range(ROLE_ASSOCIATED_WITH)) {
            if (rel.categoryId() != CATEGORY_RD) {
                continue;
            }
            auto range = rel.relative()->slaveRelations().range(ROLE_PART, {1, RelativesLimit::Type::All});
            if (range && range->empty()) {
                return SRV_ATTR_TRUE;
            }
        }
        return SRV_ATTR_FALSE;
    }

    static std::string hasZipcode(const PointObject* addr, ObjectsCache&)
    {
        return addr->masterRelations().range(ROLE_ADDR_ASSOCIATED_WITH_ZIPCODE).empty() ?
            SRV_ATTR_FALSE : SRV_ATTR_TRUE;
    }

    static std::string renderLabel(const PointObject* pt, ObjectsCache& cache)
    {
        return objectNameByType(pt->id(),
                NAME_TYPE_RENDER_LABEL,
                cache);
    }

    static std::string screenLabel(const PointObject* pt, ObjectsCache& cache)
    {
        std::string officialName = objectNameByType(pt->id(), NAME_TYPE_OFFICIAL, cache);
        std::string masterName;
        StringSet addressRoles{ROLE_ADDR_ASSOCIATED_WITH, ROLE_ASSOCIATED_WITH}; // as opposite to zipcode role (see ymapsdf.ft_addr.role)

        for (const auto& master : pt->masterRelations().range(addressRoles)) {
            masterName = objectNameByType(master.id(), NAME_TYPE_OFFICIAL, cache);
            break;
        }
        if (!masterName.empty() && !officialName.empty()) {
            officialName = masterName + ", " + officialName;
        }
        return officialName;
    }
};

class ADDR_UNITY : public ServiceAttributesRegistry::Registrar
{
public:
    ADDR_UNITY(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_ADDR_UNITY)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<PointObject>(screenLabel));
        registerSuggestCallback(SuggestCallbackWrapper<PointObject>(suggestTexts));
    }

private:
    static std::string screenLabel(const PointObject* addrUnity, ObjectsCache&)
    {
        return addrUnity->attributes().value(ATTR_ADDR_UNITY_NAME);
    }
    static SuggestTexts suggestTexts(const PointObject* addrUnity, ObjectsCache&)
    {
        return
            {{
                NM_LANG_RU,
                addrUnity->attributes().value(ATTR_ADDR_UNITY_NAME)
            }};
    }
};

class ARRIVAL_POINT : public ServiceAttributesRegistry::Registrar
{
public:
    ARRIVAL_POINT(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_ARRIVAL_POINT)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<PointObject>(screenLabel));
    }

private:
    static std::string screenLabel(const PointObject* pt, ObjectsCache& cache)
    {
        auto result = objectNameByType(pt->id(), NAME_TYPE_OFFICIAL, cache);
        if (result.empty()) {
            result = pt->category().label();
        }
        if (!pt->attributes().value(ATTR_ARRIVAL_POINT_IS_MAJOR).empty()) {
            result += " (" + pt->attributes().find(ATTR_ARRIVAL_POINT_IS_MAJOR)->def().label() + ")";
        }
        return result;
    }
};

namespace {
const std::string DIGITS = "0123456789";
}

class ENTRANCE : public ServiceAttributesRegistry::Registrar
{
public:
    ENTRANCE(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_ENTRANCE)
    {
        registerAttr(SRV_HAS_FLAT_RANGES,
                     CallbackWrapper<PointObject>(hasFlatRanges));
        registerAttr(SRV_HOTSPOT_LABEL,
                     CallbackWrapper<PointObject>(hotSpotLabel));
        registerAttr(SRV_RENDER_LABEL,
                     CallbackWrapper<GeoObject>(GenericNamedObject::renderLabel));
        registerAttr(SRV_SCREEN_LABEL,
                     CallbackWrapper<PointObject>(screenLabel));
    }

private:

    static std::string hasFlatRanges(const PointObject* entrance, ObjectsCache&)
    {
        return entrance->slaveRelations().range(ROLE_FLAT_RANGE_ASSIGNED).empty() ?
            SRV_ATTR_FALSE : SRV_ATTR_TRUE;
    }

    static std::string entranceTypeName(const PointObject* entrance)
    {
        const auto typeValue = entrance->attributes().value(ATTR_POI_ENTRANCE_TYPE);
        if (typeValue.empty()) {
            return {};
        }
        return valueLabel(
            ATTR_POI_ENTRANCE_TYPE,
            typeValue);
    }

    static std::string screenLabel(const PointObject* entrance, ObjectsCache& cache)
    {
        const auto name = GenericNamedObject::screenLabel(entrance, cache);
        const auto typeName = entranceTypeName(entrance);
        if (name.empty()) {
            return typeName;
        }
        return typeName.empty()
            ? name
            : name + " (" + typeName + ")";
    }

    static std::string hotSpotLabel(const PointObject* entrance, ObjectsCache& cache)
    {
        auto result = GenericNamedObject::hotspotLabel(entrance, cache);
        const auto flatAssignedRange = entrance->slaveRelations().range(
            ROLE_FLAT_RANGE_ASSIGNED);
        std::set<std::string, common::natural_sort> hotSpotLabelParts;
        for (const auto relInfo : flatAssignedRange) {
            const auto flatRange = relInfo.relative();
            const auto flats = flatRange->attributes().value(
                ATTR_FLAT_RANGE_FLATS);
            if (!flats.empty()) {
                hotSpotLabelParts.insert(flats);
                continue;
            }
            const auto flatFirst = flatRange->attributes().value(
                ATTR_FLAT_RANGE_FLAT_FIRST);
            const auto flatLast = flatRange->attributes().value(
                ATTR_FLAT_RANGE_FLAT_LAST);

            hotSpotLabelParts.insert(
                flatFirst == flatLast
                ? flatFirst
                : flatFirst + '-' + flatLast);
        }
        std::vector<std::string> parts;
        parts.reserve(hotSpotLabelParts.size() + 1);
        const auto typeName = entranceTypeName(entrance);
        if (result.empty()) {
            result = typeName;
        } else if (!typeName.empty()) {
            parts.push_back(typeName);
        }
        for (const auto& part : hotSpotLabelParts) {
            parts.push_back(part);
        }
        if (!parts.empty()) {
            result += " (" + common::join(parts, ", ") + ')';
        }
        return result;
    }
};

class AOI : public ServiceAttributesRegistry::Registrar
{
public:
    AOI(ServiceAttributesRegistry& registry):ServiceAttributesRegistry::Registrar(registry, CATEGORY_AOI)
    {
        registerSuggestCallback(aoiName);
    }

private:
    static SuggestTexts aoiName(const GeoObject* aoi, ObjectsCache&)
    {
        return {{NM_LANG_RU, aoi->attributes().value("aoi:name")}};
    }
};

class OUTSOURCE_REGION : public ServiceAttributesRegistry::Registrar
{
public:
    OUTSOURCE_REGION(ServiceAttributesRegistry& registry):ServiceAttributesRegistry::Registrar(registry, CATEGORY_OUTSOURCE_REGION)
    {
        registerAttr(SRV_RENDER_LABEL, CallbackWrapper<ArealObject>(renderLabel));
        registerSuggestCallback(regionName);
    }

private:
    static std::string renderLabel(const ArealObject* region, ObjectsCache&)
    {
        return region->attributes().value("outsource_region:name");
    }
    static SuggestTexts regionName(const GeoObject* region, ObjectsCache&)
    {
        return {{NM_LANG_RU, region->attributes().value("outsource_region:name")}};
    }
};

class MRC_PEDESTRIAN_REGION : public ServiceAttributesRegistry::Registrar
{
public:
    MRC_PEDESTRIAN_REGION(ServiceAttributesRegistry& registry):ServiceAttributesRegistry::Registrar(registry, CATEGORY_MRC_PEDESTRIAN_REGION)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ArealObject>(screenLabel));
        registerAttr(SRV_HOTSPOT_LABEL, CallbackWrapper<ArealObject>(hotSpotLabel));
    }

private:
    static std::string screenLabel(const GeoObject* region, ObjectsCache& cache)
    {
        return label(region, cache);
    }

    static std::string hotSpotLabel(const GeoObject* region, ObjectsCache& cache)
    {
        return label(region, cache);
    }

    static std::string label(const GeoObject* region, ObjectsCache& cache)
    {
        std::string masterAdLabelPart;
        const auto masterAds = region->masterRelations().range(
            ROLE_MRC_PEDESTRIAN_REGION_ASSOCIATED_WITH);
        if (masterAds.size() == 1) {
            masterAdLabelPart = ", " +
                objectNameByType(masterAds.begin()->id(), NAME_TYPE_OFFICIAL, cache);
        }

        return
            region->attributes().value(ATTR_MRC_PEDESTRIAN_REGION_NAME) +
            " (" + std::to_string(region->id()) + masterAdLabelPart + ")";
    }
};

namespace {

std::string
ftTypeText(const GeoObject* obj)
{
    const auto& ftTypeDef = ftTypeAttrDefForCategory(obj->categoryId());
    if (ftTypeDef) {
        const auto& ftTypeValue = obj->attributes().value(ftTypeDef->id());
        if (!ftTypeValue.empty()) {
            return valueLabel(ftTypeDef->id(), ftTypeValue);
        }
    }
    return s_emptyString;
}

std::string
arealScreenLabelWithFTType(const ArealObject* areal, ObjectsCache& cache)
{
    StringVec labels;
    for (const auto& masterRel : areal->masterRelations().range()) {
        auto name = objectNameByType(masterRel.id(), NAME_TYPE_OFFICIAL, cache);
        const auto& masterFtTypeLabel = ftTypeText(masterRel.relative());
        if (!masterFtTypeLabel.empty()) {
            name = name.empty()
                ? masterFtTypeLabel
                : name + " (" + masterFtTypeLabel + ")";
        }
        if (!name.empty()) {
            labels.push_back(name);
        } else {
            labels.push_back(masterRel.relative()->category().label());
        }
    }
    const auto& ftTypeLabel = ftTypeText(areal);
    if (!ftTypeLabel.empty()) {
        labels.push_back(ftTypeLabel);
    }
    return common::join(labels, '\n');
}

} // namepsace

class BLD : public ServiceAttributesRegistry::Registrar
{
public:
    BLD(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_BLD)
    {
        registerAttr(SRV_HAS_MODEL3D, CallbackWrapper<ArealObject>(hasModel3d))
            .depends(
            {
                {Aspect::Type::Relations, STR_TO_ATTR_OWNER},
                {{RelationType::Master, ROLE_ASSOCIATED_WITH, "model3d"}}
            });
        auto& attrReg = registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ArealObject>(arealScreenLabelWithFTType));
        addScreenLabelToMasterDepends(attrReg, CATEGORY_BLD);
    }

private:
    static std::string hasModel3d(const ArealObject* bld, ObjectsCache& /*cache*/)
    {
        return bld->masterRelations().range(ROLE_ASSOCIATED_WITH).empty()
            ? SRV_ATTR_FALSE
            : SRV_ATTR_TRUE;
    }

};

class URBAN_AREAL : public ServiceAttributesRegistry::Registrar
{
public:
    URBAN_AREAL(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_URBAN_AREAL)
    {
        auto& attrReg = registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<ArealObject>(arealScreenLabelWithFTType));
        addScreenLabelToMasterDepends(attrReg, CATEGORY_URBAN_AREAL);

        registerAttr(SRV_PRIMARY_POINT_CAT, CallbackWrapper<ArealObject>(primaryPointCategory));
    }

    static std::string primaryPointCategory(const ArealObject* areal, ObjectsCache&)
    {
        const auto& urbanArealAssignedRelations = areal->masterRelations().range(ROLE_URBAN_AREAL_ASSIGNED);
        if (urbanArealAssignedRelations.empty()){
            return SRV_ATTR_EMPTY;
        }
        return urbanArealAssignedRelations.begin() -> categoryId();
    }
};


class MERGE_REGION : public ServiceAttributesRegistry::Registrar
{
public:
    MERGE_REGION(ServiceAttributesRegistry& registry):ServiceAttributesRegistry::Registrar(registry, CATEGORY_MERGE_REGION)
    {
        registerSuggestCallback(regionName);
    }

private:
    static SuggestTexts regionName(const GeoObject* region, ObjectsCache&)
    {
        return {{NM_LANG_RU, region->attributes().value("merge_region:name")}};
    }
};

class IMAGE_OVERLAY : public ServiceAttributesRegistry::Registrar
{
public:
    IMAGE_OVERLAY(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_IMAGE_OVERLAY)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<PointObject>(screenLabel));
    }

private:
    static std::string screenLabel(const PointObject* imageOverlay, ObjectsCache&)
    {
        return imageOverlay->attributes().value("image_overlay:name");
    }
};

class IMAGE_OVERLAY_PUBLIC : public ServiceAttributesRegistry::Registrar
{
public:
    IMAGE_OVERLAY_PUBLIC(ServiceAttributesRegistry& registry)
        : ServiceAttributesRegistry::Registrar(registry, CATEGORY_IMAGE_OVERLAY_PUBLIC)
    {
        registerAttr(SRV_SCREEN_LABEL, CallbackWrapper<PointObject>(screenLabel));
    }

private:
    static std::string screenLabel(const PointObject* imageOverlay, ObjectsCache&)
    {
        return imageOverlay->attributes().value("image_overlay:name");
    }
};


MiscServiceAttributes::MiscServiceAttributes(ServiceAttributesRegistry &registry)
{
    ADDR addr(registry);
    ADDR_UNITY addrUnity(registry);
    AOI aoi(registry);
    ARRIVAL_POINT arrivalPoint(registry);
    BLD bld(registry);
    ENTRANCE entrance(registry);
    IMAGE_OVERLAY imageOverlay(registry);
    IMAGE_OVERLAY_PUBLIC imageOverlayPublic(registry);
    MERGE_REGION mergeRegion(registry);
    URBAN_AREAL urbanAreal(registry);
    OUTSOURCE_REGION outsourceRegion(registry);
    MRC_PEDESTRIAN_REGION mrcPedestrianRegion(registry);
}

} // namepspace srv_attrs
} // namespace wiki
} // namespace maps
