#include "math_util.h"

#include <util/generic/yexception.h>

namespace NSaas {
    const std::vector<ui64> TCoarser::Scales = {250000, 50000, 10000, 2500, 500, 100, 25}; // microseconds

    std::pair<TCoarser::TInterval, ui64> TCoarser::OuterCoarse(const TCoarser::TInterval ci, const std::vector<ui64>& scales, const ui64 endLevel) {
        Y_ASSERT(ci.Max >= ci.Min);

        TInterval result = TInterval::Empty();

        ui64 level = 0;
        if (level < endLevel) {
            ui64 scale = scales[level];
            result.Min = GetCoarsePoint(ci.Min, scale);
            result.Max = GetCoarsePoint(ci.Max, scale) + scale;
            Y_ASSERT(result.Max - result.Min >= scale);
            Y_ASSERT(result.Min <= ci.Min && ci.Max <= result.Max);
        }

        for (; level < endLevel; ++level) {
            ui64 scale = scales[level];
            TInterval v = GetCoarseInterval(ci.Min, scale);
            if (v.Max < ci.Max)
                v.Max += scale; //allows 2*scale intervals
            if (v.Max < ci.Max)
                break; // use prev.approximation
            result = v;
        }

        Y_ASSERT(!result.IsEmpty() || ci.IsEmpty());
        Y_ASSERT(result.IsEmpty() || result.Min <= ci.Min && ci.Max <= result.Max);

        const ui64 nextLevel = level; // may still be 0, if there is no result yielded from the cycle above
        return std::pair<TInterval, ui64>(result, nextLevel);
    }

    TCoarser::TInterval TCoarser::SmartCoarse(const TCoarser::TInterval ci, const std::vector<ui64>& scales, const ui64 endLevel) {
        std::vector<TInterval> candidates;

        std::pair<TInterval, ui64> outerCoarse = OuterCoarse(ci, scales, endLevel);
        candidates.push_back(outerCoarse.first);

        ui64 level = outerCoarse.second;
        if (level < endLevel) {
            // generate 6 candidates that are one level finer than the outerCoarse scale
            const ui64 scale = scales[level];
            candidates.push_back(GetCoarseInterval(ci.Min, scale));
            candidates.push_back(TInterval{candidates.back().Min, candidates.back().Max + scale});
            candidates.push_back(TInterval{candidates.back().Min + scale, candidates.back().Max});
            if (ci.Max > ci.Min) {
                candidates.push_back(GetCoarseInterval(ci.Max - 1, scale));
                if (candidates.back().Min >= scale) {
                    candidates.push_back(TInterval{candidates.back().Min - scale, candidates.back().Max});
                    candidates.push_back(TInterval{candidates.back().Min, candidates.back().Max - scale});
                } else {
                    candidates.push_back(TInterval{0, scale});
                }
            }
        }

        TInterval result = TInterval::Empty();
        float metric = -1.0f;
        constexpr float minIntersectRatio = 2.0f / 3;
        for (const TInterval& v : candidates) {
            const TInterval i = ci.Intersect(v);
            if (!i.Length())
                continue;

            const float len = i.Length();
            if (len <= minIntersectRatio * ci.Length())
                continue;

            const float m = len * len / v.Length() / ci.Length();
            if (m > metric) {
                result = v;
                metric = m;
            }
        }

        if (result.IsEmpty()) {
            result = candidates.front(); // the result from OuterCoarse
        }

        Y_ASSERT(metric > 0 || ci.IsEmpty());
        return result;
    }

    namespace {
        inline ui64 GetEndLevel(const std::vector<ui64>& scales, ui64 scale) {
            ui64 endLevel = 0;
            for (; endLevel < scales.size(); ++endLevel) {
                ui64 curScale = scales[endLevel];
                if (curScale * 2 < scale)
                    break;
            }
            return endLevel;
        }
    }

    std::pair<i64, i64> TCoarser::GetScaleDivision(i64 sAvg, i64 sDelta, ui64 maxPrec) {
        Y_ASSERT(sDelta >= 0);
        const ui64 delta = (ui64)sDelta;
        const ui64 avg = sAvg < 0 ? -sAvg : sAvg;
        const TInterval ci = {Max(avg, delta) - delta, avg + delta};
        const ui64 endLevel = GetEndLevel(Scales, maxPrec);
        const TInterval coarse = SmartCoarse(ci, Scales, endLevel);

        std::pair<i64, i64> division;
        division.first = coarse.Min;
        division.second = coarse.Max - coarse.Min;

        if (division.first < (i64)maxPrec && division.second > (i64)maxPrec * 2) {
            // fix cases like 0(5000) among 1000(1000) results
            division.first = (i64)maxPrec;
        }
        if (division.second < (i64)maxPrec) {
            // fix cases like x(500) when maxPrec == 1000
            division.second *= 2;
        }

        if (sAvg < 0) {
            division.first = -division.first;
            division.second = -division.second;
        }
        return division;
    };
}
