#pragma once

#include "histogram_collector.h"
#include "metric_value_type.h"
#include "summary_collector.h"
#include "log_histogram_snapshot.h"

#include <util/datetime/base.h>
#include <util/generic/algorithm.h>
#include <util/generic/vector.h>
#include <util/generic/cast.h>
#include <util/generic/ymath.h>

namespace NMonitoring {
    namespace NPrivate {
        template <typename T>
        T FromFloatSafe(double d) {
            static_assert(std::is_integral<T>::value, "this function only converts floats to integers");
            Y_ENSURE(::IsValidFloat(d) && d >= Min<T>() && d <= MaxFloor<T>(), "Cannot convert " << d << " to an integer value");
            return static_cast<T>(d);
        }

        inline auto POINT_KEY_FN = [](auto& p) {
            return p.GetTime();
        };
    } // namespace NPrivate

    template <typename T, typename Enable = void>
    struct TValueType;

    template <>
    struct TValueType<double> {
        static constexpr auto Type = EMetricValueType::DOUBLE;
    };

    template <>
    struct TValueType<i64> {
        static constexpr auto Type = EMetricValueType::INT64;
    };

    template <>
    struct TValueType<ui64> {
        static constexpr auto Type = EMetricValueType::UINT64;
    };

    template <>
    struct TValueType<TLogHistogramSnapshot*> {
        static constexpr auto Type = EMetricValueType::LOGHISTOGRAM;
    };

    template <typename T>
    struct TValueType<T*, typename std::enable_if_t<std::is_base_of<IHistogramSnapshot, T>::value>> {
        static constexpr auto Type = EMetricValueType::HISTOGRAM;
    };

    template <typename T>
    struct TValueType<T*, typename std::enable_if_t<std::is_base_of<ISummaryDoubleSnapshot, T>::value>> {
        static constexpr auto Type = EMetricValueType::SUMMARY;
    };

    ///////////////////////////////////////////////////////////////////////////
    // TMetricValue
    ///////////////////////////////////////////////////////////////////////////
    // TMetricValue represents a generic value. It does not contain type
    // information about a value. This is done to minimize object footprint.
    // To read an actual value from the object the type must be checked
    // first or provided to AsXxxx(type) member-functions.
    // This class does not hold an ownership of an IHistogramSnapshot or
    // SummarySnapshot, so this must be done somewhere outside.
    class TMetricValue {
    public:
        TMetricValue() noexcept {
            Value_.Uint64 = 0;
        }

        explicit TMetricValue(double value) noexcept {
            Value_.Double = value;
        }

        explicit TMetricValue(i64 value) noexcept {
            Value_.Int64 = value;
        }

        explicit TMetricValue(ui64 value) noexcept {
            Value_.Uint64 = value;
        }

        explicit TMetricValue(IHistogramSnapshot* histogram) noexcept {
            Value_.Histogram = histogram;
        }

        explicit TMetricValue(ISummaryDoubleSnapshot* summary) noexcept {
            Value_.Summary = summary;
        }

        explicit TMetricValue(TLogHistogramSnapshot* logHist) noexcept {
            Value_.LogHistogram = logHist;
        }

        double AsDouble() const noexcept {
            return Value_.Double;
        }

        // will cast value into double, current value type is determined by
        // the given type argument
        double AsDouble(EMetricValueType type) const {
            switch (type) {
                case EMetricValueType::DOUBLE:
                    return Value_.Double;
                case EMetricValueType::INT64:
                    return static_cast<double>(Value_.Int64);
                case EMetricValueType::UINT64:
                    return static_cast<double>(Value_.Uint64);
                case EMetricValueType::HISTOGRAM:
                    ythrow yexception() << "histogram cannot be casted to Double";
                case EMetricValueType::SUMMARY:
                    ythrow yexception() << "summary cannot be casted to Double";
                case EMetricValueType::LOGHISTOGRAM:
                    ythrow yexception() << "loghistogram cannot be casted to Double";
                case EMetricValueType::UNKNOWN:
                    ythrow yexception() << "unknown value type";
            }
            Y_FAIL(); // for GCC
        }

        ui64 AsUint64() const noexcept {
            return Value_.Uint64;
        }

        // will cast value into uint64, current value's type is determined by
        // the given type argument
        ui64 AsUint64(EMetricValueType type) const {
            switch (type) {
                case EMetricValueType::DOUBLE:
                    return NPrivate::FromFloatSafe<ui64>(Value_.Double);
                case EMetricValueType::INT64:
                    return SafeIntegerCast<ui64>(Value_.Int64);
                case EMetricValueType::UINT64:
                    return Value_.Uint64;
                case EMetricValueType::HISTOGRAM:
                    ythrow yexception() << "histogram cannot be casted to Uint64";
                case EMetricValueType::SUMMARY:
                    ythrow yexception() << "summary cannot be casted to Uint64";
                case EMetricValueType::LOGHISTOGRAM:
                    ythrow yexception() << "loghistogram cannot be casted to Uint64";
                case EMetricValueType::UNKNOWN:
                    ythrow yexception() << "unknown value type";
            }
            Y_FAIL(); // for GCC
        }

        i64 AsInt64() const noexcept {
            return Value_.Int64;
        }

        // will cast value into int64, current value's type is determined by
        // the given type argument
        i64 AsInt64(EMetricValueType type) const {
            switch (type) {
                case EMetricValueType::DOUBLE:
                    return NPrivate::FromFloatSafe<i64>(Value_.Double);
                case EMetricValueType::INT64:
                    return Value_.Int64;
                case EMetricValueType::UINT64:
                    return SafeIntegerCast<i64>(Value_.Uint64);
                case EMetricValueType::HISTOGRAM:
                    ythrow yexception() << "histogram cannot be casted to Int64";
                case EMetricValueType::SUMMARY:
                    ythrow yexception() << "summary cannot be casted to Int64";
                case EMetricValueType::LOGHISTOGRAM:
                    ythrow yexception() << "loghistogram cannot be casted to Int64";
                case EMetricValueType::UNKNOWN:
                    ythrow yexception() << "unknown value type";
            }
            Y_FAIL(); // for GCC
        }

        IHistogramSnapshot* AsHistogram() const noexcept {
            return Value_.Histogram;
        }

        IHistogramSnapshot* AsHistogram(EMetricValueType type) const {
            if (type != EMetricValueType::HISTOGRAM) {
                ythrow yexception() << type << " cannot be casted to Histogram";
            }

            return Value_.Histogram;
        }

        ISummaryDoubleSnapshot* AsSummaryDouble() const noexcept {
            return Value_.Summary;
        }

        ISummaryDoubleSnapshot* AsSummaryDouble(EMetricValueType type) const {
            if (type != EMetricValueType::SUMMARY) {
                ythrow yexception() << type << " cannot be casted to SummaryDouble";
            }

            return Value_.Summary;
        }

        TLogHistogramSnapshot* AsLogHistogram() const noexcept {
            return Value_.LogHistogram;
        }

        TLogHistogramSnapshot* AsLogHistogram(EMetricValueType type) const {
            if (type != EMetricValueType::LOGHISTOGRAM) {
                ythrow yexception() << type << " cannot be casted to LogHistogram";
            }

            return Value_.LogHistogram;
        }

    protected:
        union {
            double Double;
            i64 Int64;
            ui64 Uint64;
            IHistogramSnapshot* Histogram;
            ISummaryDoubleSnapshot* Summary;
            TLogHistogramSnapshot* LogHistogram;
        } Value_;
    };

    ///////////////////////////////////////////////////////////////////////////
    // TMetricValueWithType
    ///////////////////////////////////////////////////////////////////////////
    // Same as TMetricValue, but this type holds an ownership of
    // snapshots and contains value type information.
    class TMetricValueWithType: private TMetricValue, public TMoveOnly {
    public:
        using TBase = TMetricValue;

        template <typename T>
        explicit TMetricValueWithType(T value)
            : TBase(value)
            , ValueType_{TValueType<T>::Type}
        {
            Ref();
        }

        TMetricValueWithType(TMetricValueWithType&& other)
            : TBase(std::move(other))
            , ValueType_{other.ValueType_}
        {
            Ref();
            other.Clear();
        }

        TMetricValueWithType& operator=(TMetricValueWithType&& other) {
            TBase::operator=(other);
            ValueType_ = other.ValueType_;

            Ref();
            other.Clear();

            return *this;
        }

        ~TMetricValueWithType() {
            UnRef();
        }

        void Clear() {
            UnRef();
            ValueType_ = EMetricValueType::UNKNOWN;
        }

        EMetricValueType GetType() const noexcept {
            return ValueType_;
        }

        double AsDouble() const {
            return TBase::AsDouble(ValueType_);
        }

        ui64 AsUint64() const {
            return TBase::AsUint64(ValueType_);
        }

        i64 AsInt64() const {
            return TBase::AsInt64(ValueType_);
        }

        IHistogramSnapshot* AsHistogram() const {
            return TBase::AsHistogram(ValueType_);
        }

        ISummaryDoubleSnapshot* AsSummaryDouble() const {
            return TBase::AsSummaryDouble(ValueType_);
        }

        TLogHistogramSnapshot* AsLogHistogram() const {
            return TBase::AsLogHistogram(ValueType_);
        }

    private:
        void Ref() {
            if (ValueType_ == EMetricValueType::SUMMARY) {
                TBase::AsSummaryDouble()->Ref();
            } else if (ValueType_ == EMetricValueType::HISTOGRAM) {
                TBase::AsHistogram()->Ref();
            } else if (ValueType_ == EMetricValueType::LOGHISTOGRAM) {
                TBase::AsLogHistogram()->Ref();
            }
        }

        void UnRef() {
            if (ValueType_ == EMetricValueType::SUMMARY) {
                TBase::AsSummaryDouble()->UnRef();
            } else if (ValueType_ == EMetricValueType::HISTOGRAM) {
                TBase::AsHistogram()->UnRef();
            } else if (ValueType_ == EMetricValueType::LOGHISTOGRAM) {
                TBase::AsLogHistogram()->UnRef();
            }
        }

    private:
        EMetricValueType ValueType_ = EMetricValueType::UNKNOWN;
    };

    static_assert(sizeof(TMetricValue) == sizeof(ui64),
                  "expected size of TMetricValue is one machine word");

    ///////////////////////////////////////////////////////////////////////////
    // TMetricTimeSeries
    ///////////////////////////////////////////////////////////////////////////
    class TMetricTimeSeries: private TMoveOnly {
    public:
        class TPoint {
        public:
            TPoint()
                : Time_(TInstant::Zero())
            {
            }

            template <typename T>
            TPoint(TInstant time, T value)
                : Time_(time)
                , Value_(value)
            {
            }

            TInstant GetTime() const noexcept {
                return Time_;
            }

            TMetricValue GetValue() const noexcept {
                return Value_;
            }

            void ClearValue() {
                Value_ = {};
            }

        private:
            TInstant Time_;
            TMetricValue Value_;
        };

    public:
        TMetricTimeSeries() = default;

        TMetricTimeSeries(TMetricTimeSeries&& rhs) noexcept
            : ValueType_(rhs.ValueType_)
            , Points_(std::move(rhs.Points_))
        {
            rhs.ValueType_ = EMetricValueType::UNKNOWN;
        }

        TMetricTimeSeries& operator=(TMetricTimeSeries&& rhs) noexcept {
            Clear();

            ValueType_ = rhs.ValueType_;
            rhs.ValueType_ = EMetricValueType::UNKNOWN;

            Points_ = std::move(rhs.Points_);
            return *this;
        }

        ~TMetricTimeSeries() {
            Clear();
        }

        template <typename T>
        void Add(TInstant time, T value) {
            Add(TPoint(time, value), TValueType<T>::Type);
        }

        void Add(TPoint point, EMetricValueType valueType) {
            if (Empty()) {
                ValueType_ = valueType;
            } else {
                CheckTypes(ValueType_, valueType);
            }
            Points_.push_back(point);

            if (ValueType_ == EMetricValueType::SUMMARY) {
                TPoint& p = Points_.back();
                p.GetValue().AsSummaryDouble()->Ref();
            } else if (ValueType_ == EMetricValueType::HISTOGRAM) {
                TPoint& p = Points_.back();
                p.GetValue().AsHistogram()->Ref();
            } else if (ValueType_ == EMetricValueType::LOGHISTOGRAM) {
                TPoint& p = Points_.back();
                p.GetValue().AsLogHistogram()->Ref();
            }
        }

        void CopyFrom(const TMetricTimeSeries& other) {
            if (Empty()) {
                ValueType_ = other.ValueType_;
            } else {
                CheckTypes(GetValueType(), other.GetValueType());
            }

            size_t prevSize = Points_.size();
            Copy(std::begin(other.Points_), std::end(other.Points_),
                 std::back_inserter(Points_));

            if (ValueType_ == EMetricValueType::HISTOGRAM) {
                for (size_t i = prevSize; i < Points_.size(); i++) {
                    TPoint& point = Points_[i];
                    point.GetValue().AsHistogram()->Ref();
                }
            } else if (ValueType_ == EMetricValueType::SUMMARY) {
                for (size_t i = prevSize; i < Points_.size(); ++i) {
                    TPoint& point = Points_[i];
                    point.GetValue().AsSummaryDouble()->Ref();
                }
            } else if (ValueType_ == EMetricValueType::LOGHISTOGRAM) {
                for (size_t i = prevSize; i < Points_.size(); ++i) {
                    TPoint& point = Points_[i];
                    point.GetValue().AsLogHistogram()->Ref();
                }
            }
        }

        template <typename TConsumer>
        void ForEach(TConsumer c) const {
            for (const auto& point : Points_) {
                c(point.GetTime(), ValueType_, point.GetValue());
            }
        }

        bool Empty() const noexcept {
            return Points_.empty();
        }

        size_t Size() const noexcept {
            return Points_.size();
        }

        size_t Capacity() const noexcept {
            return Points_.capacity();
        }

        const TPoint& operator[](size_t index) const noexcept {
            return Points_[index];
        }

        void SortByTs();

        void Clear() noexcept;

        EMetricValueType GetValueType() const noexcept {
            return ValueType_;
        }

    private:
        static void CheckTypes(EMetricValueType t1, EMetricValueType t2) {
            Y_ENSURE(t1 == t2,
                     "Series type mismatch: expected " << t1 <<
                     ", but got " << t2);
        }

    private:
        EMetricValueType ValueType_ = EMetricValueType::UNKNOWN;
        TVector<TPoint> Points_;
    };

    template <EMetricValueType valueType, typename TPoint>
    static inline void SnapshotUnRef(TPoint& point) {
        if constexpr (valueType == EMetricValueType::HISTOGRAM) {
            if (auto* hist = point.GetValue().AsHistogram()) {
                hist->UnRef();
            }
        } else if constexpr (valueType == EMetricValueType::SUMMARY) {
            if (auto* summary = point.GetValue().AsSummaryDouble()) {
                summary->UnRef();
            }
        } else if constexpr (valueType == EMetricValueType::LOGHISTOGRAM) {
            if (auto* logHist = point.GetValue().AsLogHistogram()) {
                logHist->UnRef();
            }
        }
    }

    template <EMetricValueType valueType, typename TPoint>
    static void EraseDuplicates(TVector<TPoint>& points) {
        // we have to manually clean reference to a snapshot from point
        // while removing duplicates
        auto result = points.rbegin();
        for (auto it = result + 1; it != points.rend(); ++it) {
            if (result->GetTime() != it->GetTime() && ++result != it) {
                SnapshotUnRef<valueType>(*result);
                *result = *it;    // (2) copy
                it->ClearValue(); // (3) clean pointer in the source
            }
        }

        // erase tail points
        for (auto it = result + 1; it != points.rend(); ++it) {
            SnapshotUnRef<valueType>(*it);
        }
        points.erase(points.begin(), (result + 1).base());
    }

    template <typename TPoint>
    void SortPointsByTs(EMetricValueType valueType, TVector<TPoint>& points) {
        if (points.size() < 2) {
            return;
        }

        if (valueType != EMetricValueType::HISTOGRAM && valueType != EMetricValueType::SUMMARY
            && valueType != EMetricValueType::LOGHISTOGRAM) {
            // Stable sort + saving only the last point inside a group of duplicates
            StableSortBy(points, NPrivate::POINT_KEY_FN);
            auto it = UniqueBy(points.rbegin(), points.rend(), NPrivate::POINT_KEY_FN);
            points.erase(points.begin(), it.base());
        } else {
            StableSortBy(points, NPrivate::POINT_KEY_FN);
            if (valueType == EMetricValueType::HISTOGRAM) {
                EraseDuplicates<EMetricValueType::HISTOGRAM>(points);
            } else if (valueType == EMetricValueType::LOGHISTOGRAM) {
                EraseDuplicates<EMetricValueType::LOGHISTOGRAM>(points);
            } else {
                EraseDuplicates<EMetricValueType::SUMMARY>(points);
            }
        }
    }
}
