#include <maps/wikimap/mapspro/services/mrc/libs/addr_pt_searcher/include/addr_pt_searcher.h>

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

#include <util/charset/utf8.h>
#include <util/charset/wide.h>

#include <locale>

namespace maps::mrc::addr_pt_searcher  {

namespace {
constexpr double SEARCH_RADIUS_METERS = 35.;
static const std::set<char16_t> LIKE_SLASH = {'/', '-', 67 /* C */, 75 /* 'K' */};

bool isSymbolLikeSlash(wchar16 ch) {
    return (LIKE_SLASH.count(ch) > 0);
}

double calculateConfidence(
    const geolib3::Point2& featureMercatorPos,
    const TUtf16String& featureNumber,
    const geolib3::Polygon2& candidateMercatorPolygon,
    const TUtf16String& candidateNumber) {

    constexpr double STR_CONFIDENCE_COEF = 0.5;
    constexpr double PT_CONFIDENCE_COEF = 1.0 - STR_CONFIDENCE_COEF;
    constexpr double DISTANCE_DIV_PERIMETER_MAX = 1.5;

    const double strDistance = stringDistance(featureNumber, candidateNumber);
    const double strConfidence = (strDistance > 1.0) ? 0.0 : (1.0 - strDistance);

    const double ptDistance =
        geolib3::toMeters(geolib3::distance(featureMercatorPos, candidateMercatorPolygon), featureMercatorPos) /
        geolib3::toMeters(candidateMercatorPolygon.perimeter(), featureMercatorPos) /
        DISTANCE_DIV_PERIMETER_MAX;
    const double ptConfidence = (ptDistance > 1.0) ? 0.0 : (1.0 - ptDistance);

    return STR_CONFIDENCE_COEF * strConfidence + PT_CONFIDENCE_COEF * ptConfidence;
}

} // namespace

TUtf16String normalizeSymbolsInString(
    const TUtf16String& str,
    const std::set<char16_t>& validSymbols)
{
    static const std::map<char16_t, char16_t> symbolNormalizeMap = {
        { 1040,   65 }, // А
        { 1042,   66 }, // В
        { 1045,   69 }, // Е
        { 1047,   51 }, // З
        { 1050,   75 }, // К
        { 1052,   77 }, // М
        { 1053,   72 }, // Н
        { 1054,   79 }, // О
        { 1056,   80 }, // Р
        { 1057,   67 }, // С
        { 1058,   84 }, // Т
        { 1059,   89 }, // У
        { 1061,   88 }, // Х
    };

    std::locale loc("en_US.utf8");
    TUtf16String result;
    for (size_t chIdx = 0; chIdx < str.size(); chIdx++) {
        char16_t ch = std::toupper((wchar_t)str[chIdx], loc);
        auto it = symbolNormalizeMap.find(ch);
        if (it != symbolNormalizeMap.end()) {
           ch = it->second;
        }
        if (validSymbols.empty() || validSymbols.count(ch) > 0)
        {
            result += ch;
        }
    }
    return result;
}


double stringDistance(const TUtf16String& featureNumber, const TUtf16String& candidateNumber) {
    constexpr double PENALTY_ONE_SLASH = 0.2;
    constexpr double PENALTY_COEF_1 = 1.0;
    constexpr double PENALTY_COEF_2_DIGIT = 0.7;
    constexpr double PENALTY_COEF_2_NOT_DIGIT = 0.2;
    constexpr double PENALTY_MAX_ERROR = 2.0;

    if (0 == featureNumber.size() || 0 == candidateNumber.size())
        return PENALTY_MAX_ERROR;

    const size_t len1 = featureNumber.size();
    const size_t len2 = candidateNumber.size();
    size_t idx1 = 0;
    size_t idx2 = 0;
    double penalty = 0.;
    for (;idx1 < len1 && idx2 < len2;) {
        if (featureNumber[idx1] == candidateNumber[idx2]) {
            idx1++;
            idx2++;
            continue;
        }
        const bool likeSlash1 = isSymbolLikeSlash(featureNumber[idx1]);
        const bool likeSlash2 = isSymbolLikeSlash(candidateNumber[idx2]);
        if (!likeSlash1 && !likeSlash2)
            break;
        if (likeSlash1) {
            idx1++;
        } else {
            penalty += PENALTY_ONE_SLASH;
        }
        if (likeSlash2) {
            idx2++;
        } else {
            penalty += PENALTY_ONE_SLASH;
        }
    }
    if ((idx1 < len1) && (idx2 < len2))
        return PENALTY_MAX_ERROR;

    for (;idx1 < len1 && isSymbolLikeSlash(featureNumber[idx1]); idx1++) {
        penalty += PENALTY_ONE_SLASH;
    }
    penalty += (len1 - idx1) * PENALTY_COEF_1;

    if (idx2 < len2 && isSymbolLikeSlash(candidateNumber[idx2])) {
        penalty += PENALTY_ONE_SLASH;
        idx2++;
    } else {
        for (;idx2 < len2 && std::iswdigit((wchar_t)candidateNumber[idx2]); idx2++) {
            penalty += PENALTY_COEF_2_DIGIT;
        }
    }
    penalty += (len2 - idx2) * PENALTY_COEF_2_NOT_DIGIT;
    return penalty;
}

AddressPointSearcher::BuildingSearcher::BuildingSearcher(
    const maps::mrc::object::Buildings& buildings)
{
    for (const maps::mrc::object::Building& building : buildings) {
            this->insert(building.geom());
    }
    searcher_.build();
}

const AddressPointSearcher::BuildingSearcher::InternalSearcher::SearchResult AddressPointSearcher::BuildingSearcher::find(const geolib3::Point2& mercatorPos) const
{
    constexpr double BUILDING_SEARCH_RADIUS_METERS = 75.;
    const double mercatorDiameter = 2. * geolib3::toMercatorUnits(BUILDING_SEARCH_RADIUS_METERS, mercatorPos);
    geolib3::BoundingBox searchBox(mercatorPos, mercatorDiameter, mercatorDiameter);
    return searcher_.find(searchBox);
}
const AddressPointSearcher::BuildingSearcher::InternalSearcher::SearchResult AddressPointSearcher::BuildingSearcher::find(const maps::geolib3::BoundingBox& searchBox) const
{
    return searcher_.find(searchBox);
}
void AddressPointSearcher::BuildingSearcher::insert(const maps::geolib3::Polygon2& pgn)
{
    polygons_.push_back(pgn);
    searcher_.insert(&polygons_.back(), -1);
}


AddressPointSearcher::GeometrySearcher::GeometrySearcher(
    const object::AddressPointWithNames& addrPointsWithName,
    const maps::mrc::object::Buildings& buildings,
    const geolib3::BoundingBox& bbox)
    : searchBbox_(bbox)
{
    // median perimiter of building on our dataset is 55 meters
    //   55 / 4 = 13.75 meters - side of square building around address point
    //   13.75 / 2 = 6.875 meters - radius
    constexpr double POINT_WITHOUT_BUILDING_RADIUS_METERS = 6.875;

    BuildingSearcher buildingSearcher(buildings);
    for (const maps::mrc::object::AddressPointWithName& addrPointWithName : addrPointsWithName) {
        bool buildingFound = false;
        const maps::geolib3::Point2& addrPoint = addrPointWithName.geom();
        auto buildingsForAddrPoint = buildingSearcher.find(addrPoint.boundingBox());
        for (auto itr = buildingsForAddrPoint.first; itr != buildingsForAddrPoint.second; itr++) {
            if (!maps::geolib3::spatialRelation(addrPoint, itr->geometry(), maps::geolib3::SpatialRelation::Within))
                continue;
            this->insert(itr->geometry(), addrPoint, addrPointWithName.name());
            buildingFound = true;
            break;
        }
        if (!buildingFound &&
            maps::geolib3::spatialRelation(addrPoint, searchBbox_, maps::geolib3::SpatialRelation::Within))
        {
            const double radius = maps::geolib3::toMercatorUnits(POINT_WITHOUT_BUILDING_RADIUS_METERS, addrPoint);
            maps::geolib3::Polygon2 pointPolygon =
                maps::geolib3::Polygon2(
                    maps::geolib3::LinearRing2(
                        {
                            {addrPoint.x() - radius, addrPoint.y() - radius},
                            {addrPoint.x() - radius, addrPoint.y() + radius},
                            {addrPoint.x() + radius, addrPoint.y() + radius},
                            {addrPoint.x() + radius, addrPoint.y() - radius},
                        },
                        false),
                    {}, false);
            this->insert(pointPolygon, addrPoint, addrPointWithName.name());
        }
    }
    searcher_.build();
}

const AddressPointSearcher::GeometrySearcher::InternalSearcher::SearchResult
AddressPointSearcher::GeometrySearcher::find(const geolib3::Point2& mercatorPos) const
{
    // -0.1 потому что мы хотим проверять попадет ли поисковый BBox внутрь того прямоугольника
    // которым мы инициализировали геометрический поиск. А если мы инициализируем поиск вокруг
    // одной фотографии, и затем вокруг нее же начинаем искать, то прямоугольники должны совпадать
    // с точностью до вычислений с плавающей точкой. Но даже если совпадает, то SpatialRelation::Within
    // возвращает false. При этом изменение на 10 см. не должно сказаться на результатах
    const double mercatorDiameter = 2. * geolib3::toMercatorUnits(SEARCH_RADIUS_METERS - 0.1, mercatorPos);
    geolib3::BoundingBox searchBox(mercatorPos, mercatorDiameter, mercatorDiameter);
    REQUIRE(maps::geolib3::spatialRelation(searchBox, searchBbox_, maps::geolib3::SpatialRelation::Within),
        "Unable to search by coordinates outside the search region");
    return searcher_.find(searchBox);
}

void AddressPointSearcher::GeometrySearcher::insert(
    const maps::geolib3::Polygon2& pgn,
    const geolib3::Point2& addrPointMercatorPos,
    const std::string& name)
{
    polygons_.push_back(pgn);
    searcher_.insert(&polygons_.back(), {addrPointMercatorPos, name});
}

AddressPointSearcher::AddressPointSearcher(object::Loader& loader, std::set<char16_t> recognizerSupportedSymbols)
    : loader_(loader)
    , recognizerSupportedSymbols_(std::move(recognizerSupportedSymbols))
{
    if (!recognizerSupportedSymbols_.empty()) {
        recognizerSupportedSymbols_.insert(LIKE_SLASH.begin(), LIKE_SLASH.end());
    }
}

void AddressPointSearcher::resetPosition(const geolib3::BoundingBox& mercatorBbox) {
    const double mercatorDiameter = 2. * geolib3::toMercatorUnits(SEARCH_RADIUS_METERS, mercatorBbox.center());
    resetGeometrySearcher({mercatorBbox.center(), mercatorBbox.width() + mercatorDiameter, mercatorBbox.height() + mercatorDiameter});
}

void AddressPointSearcher::resetPosition(const geolib3::Point2& mercatorPos)
{
    const double mercatorDiameter = 2. * geolib3::toMercatorUnits(SEARCH_RADIUS_METERS, mercatorPos);
    resetGeometrySearcher({mercatorPos, mercatorDiameter, mercatorDiameter});
}

std::vector<AddressPointCandidate> AddressPointSearcher::find(const geolib3::Point2& mercatorPos, const TString& number, double minConfidence) const
{
    const TUtf16String numberNormalized = normalizeSymbolsInString(UTF8ToWide(number), recognizerSupportedSymbols_);
    if (numberNormalized.empty())
        return {};
    std::vector<AddressPointCandidate> result;
    auto searchResult = geometrySearcher_->find(mercatorPos);
    for (auto itr = searchResult.first; itr != searchResult.second; itr++) {
        const auto& namedPt = itr->value();
        const TUtf16String candidateNumberNormalized =
            normalizeSymbolsInString(UTF8ToWide(TString(namedPt.name.c_str())), recognizerSupportedSymbols_);
        const double confidence =
            calculateConfidence(mercatorPos, numberNormalized, itr->geometry(), candidateNumberNormalized);
        if (confidence > minConfidence) {
            result.emplace_back(namedPt.mercatorPos, namedPt.name.c_str(), confidence);
        }
    }
    std::sort(
        result.begin(), result.end(),
        [] (const AddressPointCandidate& a, const AddressPointCandidate& b) {
            return a.confidence > b.confidence;
        });
    return result;
}

bool AddressPointSearcher::hasCandidate(const geolib3::Point2& mercatorPos, const TString& number, double minConfidence) const
{
    const TUtf16String numberNormalized = normalizeSymbolsInString(UTF8ToWide(number), recognizerSupportedSymbols_);
    if (numberNormalized.empty())
        return false;
    auto searchResult = geometrySearcher_->find(mercatorPos);
    for (auto itr = searchResult.first; itr != searchResult.second; itr++) {
        const auto& namedPt = itr->value();
        const TUtf16String candidateNumberNormalized =
            normalizeSymbolsInString(UTF8ToWide(TString(namedPt.name.c_str())), recognizerSupportedSymbols_);
        const double confidence =
            calculateConfidence(mercatorPos, numberNormalized, itr->geometry(), candidateNumberNormalized);
        if (confidence > minConfidence) {
            return true;
        }
    }
    return false;
}

void AddressPointSearcher::resetGeometrySearcher(const geolib3::BoundingBox& searchBbox) {
    maps::mrc::object::Buildings buildings = loader_.loadBuildings(searchBbox);
    geolib3::BoundingBox buildingsBBox = searchBbox;
    if (0 < buildings.size()) {
        for (size_t i = 0; i < buildings.size(); i++) {
            const maps::mrc::object::Building& building = buildings[i];
            buildingsBBox = maps::geolib3::expand(buildingsBBox, building.geom());
        }
    }

    geometrySearcher_ = std::make_unique<GeometrySearcher>(
        loader_.loadAddressPointWithNames(buildingsBBox),
        buildings,
        searchBbox);
}

} // maps::mrc::addr_pt_searcher

