#pragma once

#include <util/generic/fwd.h>
#include <util/generic/strbuf.h>
#include <util/generic/string.h>
#include <util/generic/yexception.h>
#include <util/generic/typetraits.h>
#include <util/generic/algorithm.h>
#include <util/stream/output.h>
#include <util/stream/input.h>
#include <util/system/compiler.h>

#ifndef __NVCC__
    // cuda is compiled in C++14 mode at the time
    #include <optional>
    #include <variant>
#endif

template <typename T>
class TSerializeTypeTraits {
public:
    /*
     *  pointer types cannot be serialized as POD-type
     */
    enum {
        IsSerializablePod = TTypeTraits<T>::IsPod && !std::is_pointer<T>::value
    };
};

struct TSerializeException: public yexception {
};

struct TLoadEOF: public TSerializeException {
};

template <class T>
static inline void Save(IOutputStream* out, const T& t);

template <class T>
static inline void SaveArray(IOutputStream* out, const T* t, size_t len);

template <class T>
static inline void Load(IInputStream* in, T& t);

template <class T>
static inline void LoadArray(IInputStream* in, T* t, size_t len);

template <class T, class TStorage>
static inline void Load(IInputStream* in, T& t, TStorage& pool);

template <class T, class TStorage>
static inline void LoadArray(IInputStream* in, T* t, size_t len, TStorage& pool);

template <class T>
static inline void SavePodType(IOutputStream* rh, const T& t) {
    rh->Write(&t, sizeof(T));
}

namespace NPrivate {
    [[noreturn]] void ThrowLoadEOFException(size_t typeSize, size_t realSize, TStringBuf structName);
    [[noreturn]] void ThrowUnexpectedVariantTagException(ui8 tagIndex);
}

template <class T>
static inline void LoadPodType(IInputStream* rh, T& t) {
    const size_t res = rh->Load(&t, sizeof(T));

    if (Y_UNLIKELY(res != sizeof(T))) {
        ::NPrivate::ThrowLoadEOFException(sizeof(T), res, TStringBuf("pod type"));
    }
}

template <class T>
static inline void SavePodArray(IOutputStream* rh, const T* arr, size_t count) {
    rh->Write(arr, sizeof(T) * count);
}

template <class T>
static inline void LoadPodArray(IInputStream* rh, T* arr, size_t count) {
    const size_t len = sizeof(T) * count;
    const size_t res = rh->Load(arr, len);

    if (Y_UNLIKELY(res != len)) {
        ::NPrivate::ThrowLoadEOFException(len, res, TStringBuf("pod array"));
    }
}

template <class It>
static inline void SaveIterRange(IOutputStream* rh, It b, It e) {
    while (b != e) {
        ::Save(rh, *b++);
    }
}

template <class It>
static inline void LoadIterRange(IInputStream* rh, It b, It e) {
    while (b != e) {
        ::Load(rh, *b++);
    }
}

template <class It, class TStorage>
static inline void LoadIterRange(IInputStream* rh, It b, It e, TStorage& pool) {
    while (b != e) {
        ::Load(rh, *b++, pool);
    }
}

template <class T, bool isPod>
struct TSerializerTakingIntoAccountThePodType {
    static inline void Save(IOutputStream* out, const T& t) {
        ::SavePodType(out, t);
    }

    static inline void Load(IInputStream* in, T& t) {
        ::LoadPodType(in, t);
    }

    template <class TStorage>
    static inline void Load(IInputStream* in, T& t, TStorage& /*pool*/) {
        ::LoadPodType(in, t);
    }

    static inline void SaveArray(IOutputStream* out, const T* t, size_t len) {
        ::SavePodArray(out, t, len);
    }

    static inline void LoadArray(IInputStream* in, T* t, size_t len) {
        ::LoadPodArray(in, t, len);
    }
};

namespace NHasSaveLoad {
    Y_HAS_MEMBER(SaveLoad);
}

template <class T, class = void>
struct TSerializerMethodSelector;

template <class T>
struct TSerializerMethodSelector<T, std::enable_if_t<NHasSaveLoad::THasSaveLoad<T>::value>> {
    static inline void Save(IOutputStream* out, const T& t) {
        //assume Save clause do not change t
        (const_cast<T&>(t)).SaveLoad(out);
    }

    static inline void Load(IInputStream* in, T& t) {
        t.SaveLoad(in);
    }

    template <class TStorage>
    static inline void Load(IInputStream* in, T& t, TStorage& pool) {
        t.SaveLoad(in, pool);
    }
};

template <class T>
struct TSerializerMethodSelector<T, std::enable_if_t<!NHasSaveLoad::THasSaveLoad<T>::value>> {
    static inline void Save(IOutputStream* out, const T& t) {
        t.Save(out);
    }

    static inline void Load(IInputStream* in, T& t) {
        t.Load(in);
    }

    template <class TStorage>
    static inline void Load(IInputStream* in, T& t, TStorage& pool) {
        t.Load(in, pool);
    }
};

template <class T>
struct TSerializerTakingIntoAccountThePodType<T, false>: public TSerializerMethodSelector<T> {
    static inline void SaveArray(IOutputStream* out, const T* t, size_t len) {
        ::SaveIterRange(out, t, t + len);
    }

    static inline void LoadArray(IInputStream* in, T* t, size_t len) {
        ::LoadIterRange(in, t, t + len);
    }

    template <class TStorage>
    static inline void LoadArray(IInputStream* in, T* t, size_t len, TStorage& pool) {
        ::LoadIterRange(in, t, t + len, pool);
    }
};

template <class It, bool isPtr>
struct TRangeSerialize {
    static inline void Save(IOutputStream* rh, It b, It e) {
        SaveArray(rh, b, e - b);
    }

    static inline void Load(IInputStream* rh, It b, It e) {
        LoadArray(rh, b, e - b);
    }

    template <class TStorage>
    static inline void Load(IInputStream* rh, It b, It e, TStorage& pool) {
        LoadArray(rh, b, e - b, pool);
    }
};

template <class It>
struct TRangeSerialize<It, false> {
    static inline void Save(IOutputStream* rh, It b, It e) {
        SaveIterRange(rh, b, e);
    }

    static inline void Load(IInputStream* rh, It b, It e) {
        LoadIterRange(rh, b, e);
    }

    template <class TStorage>
    static inline void Load(IInputStream* rh, It b, It e, TStorage& pool) {
        LoadIterRange(rh, b, e, pool);
    }
};

template <class It>
static inline void SaveRange(IOutputStream* rh, It b, It e) {
    TRangeSerialize<It, std::is_pointer<It>::value>::Save(rh, b, e);
}

template <class It>
static inline void LoadRange(IInputStream* rh, It b, It e) {
    TRangeSerialize<It, std::is_pointer<It>::value>::Load(rh, b, e);
}

template <class It, class TStorage>
static inline void LoadRange(IInputStream* rh, It b, It e, TStorage& pool) {
    TRangeSerialize<It, std::is_pointer<It>::value>::Load(rh, b, e, pool);
}

template <class T>
class TSerializer: public TSerializerTakingIntoAccountThePodType<T, TSerializeTypeTraits<T>::IsSerializablePod> {
};

template <class T>
class TArraySerializer: public TSerializerTakingIntoAccountThePodType<T, TSerializeTypeTraits<T>::IsSerializablePod> {
};

template <class T>
static inline void Save(IOutputStream* out, const T& t) {
    TSerializer<T>::Save(out, t);
}

template <class T>
static inline void SaveArray(IOutputStream* out, const T* t, size_t len) {
    TArraySerializer<T>::SaveArray(out, t, len);
}

template <class T>
static inline void Load(IInputStream* in, T& t) {
    TSerializer<T>::Load(in, t);
}

template <class T>
static inline void LoadArray(IInputStream* in, T* t, size_t len) {
    TArraySerializer<T>::LoadArray(in, t, len);
}

template <class T, class TStorage>
static inline void Load(IInputStream* in, T& t, TStorage& pool) {
    TSerializer<T>::Load(in, t, pool);
}

template <class T, class TStorage>
static inline void LoadArray(IInputStream* in, T* t, size_t len, TStorage& pool) {
    TArraySerializer<T>::LoadArray(in, t, len, pool);
}

static inline void SaveSize(IOutputStream* rh, size_t len) {
    if ((ui64)len < 0xffffffff) {
        ::Save(rh, (ui32)len);
    } else {
        ::Save(rh, (ui32)0xffffffff);
        ::Save(rh, (ui64)len);
    }
}

static inline size_t LoadSize(IInputStream* rh) {
    ui32 oldVerSize;
    ui64 newVerSize;
    ::Load(rh, oldVerSize);
    if (oldVerSize != 0xffffffff) {
        return oldVerSize;
    } else {
        ::Load(rh, newVerSize);
        return newVerSize;
    }
}

template <class C>
static inline void LoadSizeAndResize(IInputStream* rh, C& c) {
    c.resize(LoadSize(rh));
}

template <class TStorage>
static inline char* AllocateFromPool(TStorage& pool, size_t len) {
    return static_cast<char*>(pool.Allocate(len));
}

template <>
class TSerializer<const char*> {
public:
    static inline void Save(IOutputStream* rh, const char* s) {
        size_t length = strlen(s);
        ::SaveSize(rh, length);
        ::SavePodArray(rh, s, length);
    }

    template <class Char, class TStorage>
    static inline void Load(IInputStream* rh, Char*& s, TStorage& pool) {
        const size_t len = LoadSize(rh);

        char* res = AllocateFromPool(pool, len + 1);
        ::LoadPodArray(rh, res, len);
        res[len] = 0;
        s = res;
    }
};

template <class TVec>
class TVectorSerializer {
    using TIter = typename TVec::iterator;

public:
    static inline void Save(IOutputStream* rh, const TVec& v) {
        ::SaveSize(rh, v.size());
        ::SaveRange(rh, v.begin(), v.end());
    }

    static inline void Load(IInputStream* rh, TVec& v) {
        ::LoadSizeAndResize(rh, v);
        TIter b = v.begin();
        TIter e = (TIter)v.end();
        ::LoadRange(rh, b, e);
    }

    template <class TStorage>
    static inline void Load(IInputStream* rh, TVec& v, TStorage& pool) {
        ::LoadSizeAndResize(rh, v);
        TIter b = v.begin();
        TIter e = (TIter)v.end();
        ::LoadRange(rh, b, e, pool);
    }
};

template <class T, class A>
class TSerializer<TVector<T, A>>: public TVectorSerializer<TVector<T, A>> {
};

template <class T, class A>
class TSerializer<std::vector<T, A>>: public TVectorSerializer<std::vector<T, A>> {
};

template <class T, class A>
class TSerializer<TList<T, A>>: public TVectorSerializer<TList<T, A>> {
};

template <class T, class A>
class TSerializer<std::list<T, A>>: public TVectorSerializer<std::list<T, A>> {
};

template <>
class TSerializer<TString>: public TVectorSerializer<TString> {
};

template <>
class TSerializer<TUtf16String>: public TVectorSerializer<TUtf16String> {
};

template <class TChar>
class TSerializer<std::basic_string<TChar>>: public TVectorSerializer<std::basic_string<TChar>> {
};

template <class T, class A>
class TSerializer<TDeque<T, A>>: public TVectorSerializer<TDeque<T, A>> {
};

template <class T, class A>
class TSerializer<std::deque<T, A>>: public TVectorSerializer<std::deque<T, A>> {
};

template <class TArray>
class TStdArraySerializer {
public:
    static inline void Save(IOutputStream* rh, const TArray& a) {
        ::SaveArray(rh, a.data(), a.size());
    }

    static inline void Load(IInputStream* rh, TArray& a) {
        ::LoadArray(rh, a.data(), a.size());
    }
};

template <class T, size_t N>
class TSerializer<std::array<T, N>>: public TStdArraySerializer<std::array<T, N>> {
};

template <class A, class B>
class TSerializer<std::pair<A, B>> {
    using TPair = std::pair<A, B>;

public:
    static inline void Save(IOutputStream* rh, const TPair& p) {
        ::Save(rh, p.first);
        ::Save(rh, p.second);
    }

    static inline void Load(IInputStream* rh, TPair& p) {
        ::Load(rh, p.first);
        ::Load(rh, p.second);
    }

    template <class TStorage>
    static inline void Load(IInputStream* rh, TPair& p, TStorage& pool) {
        ::Load(rh, p.first, pool);
        ::Load(rh, p.second, pool);
    }
};

template <class T>
struct TTupleSerializer {
    template <class F, class Tuple, size_t... Indices>
    static inline void ReverseUseless(F&& f, Tuple&& t, std::index_sequence<Indices...>) {
        ApplyToMany(
            std::forward<F>(f),
            // We need to do this trick because we don't want to break backward compatibility.
            // Tuples are being packed in reverse order.
            std::get<std::tuple_size<T>::value - Indices - 1>(std::forward<Tuple>(t))...);
    }

    static inline void Save(IOutputStream* stream, const T& t) {
        ReverseUseless([&](const auto& v) { ::Save(stream, v); }, t,
                       std::make_index_sequence<std::tuple_size<T>::value>{});
    }

    static inline void Load(IInputStream* stream, T& t) {
        ReverseUseless([&](auto& v) { ::Load(stream, v); }, t,
                       std::make_index_sequence<std::tuple_size<T>::value>{});
    }
};

template <typename... TArgs>
struct TSerializer<std::tuple<TArgs...>>: TTupleSerializer<std::tuple<TArgs...>> {
};

template <>
class TSerializer<TBuffer> {
public:
    static void Save(IOutputStream* rh, const TBuffer& buf);
    static void Load(IInputStream* rh, TBuffer& buf);
};

template <class TSetOrMap, class TValue>
class TSetSerializerInserterBase {
public:
    inline TSetSerializerInserterBase(TSetOrMap& s)
        : S_(s)
    {
        S_.clear();
    }

    inline void Insert(const TValue& v) {
        S_.insert(v);
    }

protected:
    TSetOrMap& S_;
};

template <class TSetOrMap, class TValue, bool sorted>
class TSetSerializerInserter: public TSetSerializerInserterBase<TSetOrMap, TValue> {
    using TBase = TSetSerializerInserterBase<TSetOrMap, TValue>;

public:
    inline TSetSerializerInserter(TSetOrMap& s, size_t cnt)
        : TBase(s)
    {
        Y_UNUSED(cnt);
    }
};

template <class TSetType, class TValue>
class TSetSerializerInserter<TSetType, TValue, true>: public TSetSerializerInserterBase<TSetType, TValue> {
    using TBase = TSetSerializerInserterBase<TSetType, TValue>;

public:
    inline TSetSerializerInserter(TSetType& s, size_t cnt)
        : TBase(s)
    {
        Y_UNUSED(cnt);
        P_ = this->S_.begin();
    }

    inline void Insert(const TValue& v) {
        P_ = this->S_.insert(P_, v);
    }

private:
    typename TSetType::iterator P_;
};

template <class T1, class T2, class T3, class T4, class T5, class TValue>
class TSetSerializerInserter<THashMap<T1, T2, T3, T4, T5>, TValue, false>: public TSetSerializerInserterBase<THashMap<T1, T2, T3, T4, T5>, TValue> {
    using TMapType = THashMap<T1, T2, T3, T4, T5>;
    using TBase = TSetSerializerInserterBase<TMapType, TValue>;

public:
    inline TSetSerializerInserter(TMapType& m, size_t cnt)
        : TBase(m)
    {
        m.reserve(cnt);
    }
};

template <class T1, class T2, class T3, class T4, class T5, class TValue>
class TSetSerializerInserter<THashMultiMap<T1, T2, T3, T4, T5>, TValue, false>: public TSetSerializerInserterBase<THashMultiMap<T1, T2, T3, T4, T5>, TValue> {
    using TMapType = THashMultiMap<T1, T2, T3, T4, T5>;
    using TBase = TSetSerializerInserterBase<TMapType, TValue>;

public:
    inline TSetSerializerInserter(TMapType& m, size_t cnt)
        : TBase(m)
    {
        m.reserve(cnt);
    }
};

template <class T1, class T2, class T3, class T4, class TValue>
class TSetSerializerInserter<THashSet<T1, T2, T3, T4>, TValue, false>: public TSetSerializerInserterBase<THashSet<T1, T2, T3, T4>, TValue> {
    using TSetType = THashSet<T1, T2, T3, T4>;
    using TBase = TSetSerializerInserterBase<TSetType, TValue>;

public:
    inline TSetSerializerInserter(TSetType& s, size_t cnt)
        : TBase(s)
    {
        s.reserve(cnt);
    }
};

template <class TSetType, class TValue, bool sorted>
class TSetSerializerBase {
public:
    static inline void Save(IOutputStream* rh, const TSetType& s) {
        ::SaveSize(rh, s.size());
        ::SaveRange(rh, s.begin(), s.end());
    }

    static inline void Load(IInputStream* rh, TSetType& s) {
        const size_t cnt = ::LoadSize(rh);
        TSetSerializerInserter<TSetType, TValue, sorted> ins(s, cnt);

        TValue v;
        for (size_t i = 0; i != cnt; ++i) {
            ::Load(rh, v);
            ins.Insert(v);
        }
    }

    template <class TStorage>
    static inline void Load(IInputStream* rh, TSetType& s, TStorage& pool) {
        const size_t cnt = ::LoadSize(rh);
        TSetSerializerInserter<TSetType, TValue, sorted> ins(s, cnt);

        TValue v;
        for (size_t i = 0; i != cnt; ++i) {
            ::Load(rh, v, pool);
            ins.Insert(v);
        }
    }
};

template <class TMapType, bool sorted = false>
struct TMapSerializer: public TSetSerializerBase<TMapType, std::pair<typename TMapType::key_type, typename TMapType::mapped_type>, sorted> {
};

template <class TSetType, bool sorted = false>
struct TSetSerializer: public TSetSerializerBase<TSetType, typename TSetType::value_type, sorted> {
};

template <class T1, class T2, class T3, class T4>
class TSerializer<TMap<T1, T2, T3, T4>>: public TMapSerializer<TMap<T1, T2, T3, T4>, true> {
};

template <class K, class T, class C, class A>
class TSerializer<std::map<K, T, C, A>>: public TMapSerializer<std::map<K, T, C, A>, true> {
};

template <class T1, class T2, class T3, class T4>
class TSerializer<TMultiMap<T1, T2, T3, T4>>: public TMapSerializer<TMultiMap<T1, T2, T3, T4>, true> {
};

template <class K, class T, class C, class A>
class TSerializer<std::multimap<K, T, C, A>>: public TMapSerializer<std::multimap<K, T, C, A>, true> {
};

template <class T1, class T2, class T3, class T4, class T5>
class TSerializer<THashMap<T1, T2, T3, T4, T5>>: public TMapSerializer<THashMap<T1, T2, T3, T4, T5>, false> {
};

template <class T1, class T2, class T3, class T4, class T5>
class TSerializer<THashMultiMap<T1, T2, T3, T4, T5>>: public TMapSerializer<THashMultiMap<T1, T2, T3, T4, T5>, false> {
};

template <class K, class C, class A>
class TSerializer<TSet<K, C, A>>: public TSetSerializer<TSet<K, C, A>, true> {
};

template <class K, class C, class A>
class TSerializer<std::set<K, C, A>>: public TSetSerializer<std::set<K, C, A>, true> {
};

template <class T1, class T2, class T3, class T4>
class TSerializer<THashSet<T1, T2, T3, T4>>: public TSetSerializer<THashSet<T1, T2, T3, T4>, false> {
};

template <class T1, class T2>
class TSerializer<TQueue<T1, T2>> {
public:
    static inline void Save(IOutputStream* rh, const TQueue<T1, T2>& v) {
        ::Save(rh, v.Container());
    }
    static inline void Load(IInputStream* in, TQueue<T1, T2>& t) {
        ::Load(in, t.Container());
    }
};

template <class T1, class T2, class T3>
class TSerializer<TPriorityQueue<T1, T2, T3>> {
public:
    static inline void Save(IOutputStream* rh, const TPriorityQueue<T1, T2, T3>& v) {
        ::Save(rh, v.Container());
    }
    static inline void Load(IInputStream* in, TPriorityQueue<T1, T2, T3>& t) {
        ::Load(in, t.Container());
    }
};

#ifndef __NVCC__

template <typename T>
struct TSerializer<std::optional<T>> {
    static inline void Save(IOutputStream* os, const std::optional<T>& v) {
        ::Save(os, v.has_value());
        if (v.has_value()) {
            ::Save(os, *v);
        }
    }

    static inline void Load(IInputStream* is, std::optional<T>& v) {
        v.reset();

        bool hasValue;
        ::Load(is, hasValue);

        if (hasValue) {
            ::Load(is, v.emplace());
        }
    }
};

namespace NPrivate {
    template <class Variant, class T, size_t I>
    void LoadVariantAlternative(IInputStream* is, Variant& v) {
        T loaded;
        ::Load(is, loaded);
        v.template emplace<I>(std::move(loaded));
    }
}

template <typename... Args>
struct TSerializer<std::variant<Args...>> {
    using TVar = std::variant<Args...>;

    static_assert(sizeof...(Args) < 256, "We use ui8 to store tag");

    static void Save(IOutputStream* os, const TVar& v) {
        ::Save<ui8>(os, v.index());
        std::visit([os](const auto& data) {
            ::Save(os, data);
        }, v);
    }

    static void Load(IInputStream* is, TVar& v) {
        ui8 index;
        ::Load(is, index);
        if (Y_UNLIKELY(index >= sizeof...(Args))) {
            ::NPrivate::ThrowUnexpectedVariantTagException(index);
        }
        LoadImpl(is, v, index, std::index_sequence_for<Args...>{});
    }

private:
    template <size_t... Is>
    static void LoadImpl(IInputStream* is, TVar& v, ui8 index, std::index_sequence<Is...>) {
        using TLoader = void (*)(IInputStream*, TVar & v);
        constexpr TLoader loaders[] = {::NPrivate::LoadVariantAlternative<TVar, Args, Is>...};
        loaders[index](is, v);
    }
};

#endif

template <class T>
static inline void SaveLoad(IOutputStream* out, const T& t) {
    Save(out, t);
}

template <class T>
static inline void SaveLoad(IInputStream* in, T& t) {
    Load(in, t);
}

template <class S, class... Ts>
static inline void SaveMany(S* s, const Ts&... t) {
    ApplyToMany([&](const auto& v) { Save(s, v); }, t...);
}

template <class S, class... Ts>
static inline void LoadMany(S* s, Ts&... t) {
    ApplyToMany([&](auto& v) { Load(s, v); }, t...);
}

#define Y_SAVELOAD_DEFINE(...)                 \
    inline void Save(IOutputStream* s) const { \
        ::SaveMany(s, __VA_ARGS__);            \
    }                                          \
                                               \
    inline void Load(IInputStream* s) {        \
        ::LoadMany(s, __VA_ARGS__);            \
    }                                          \
    Y_SEMICOLON_GUARD

#define Y_SAVELOAD_DEFINE_OVERRIDE(...)          \
    void Save(IOutputStream* s) const override { \
        ::SaveMany(s, __VA_ARGS__);              \
    }                                            \
                                                 \
    void Load(IInputStream* s) override {        \
        ::LoadMany(s, __VA_ARGS__);              \
    }                                            \
    Y_SEMICOLON_GUARD

template <class T>
struct TNonVirtualSaver {
    const T* Data;
    void Save(IOutputStream* out) const {
        Data->T::Save(out);
    }
};

template <typename S, typename T, typename... R>
inline void LoadMany(S* s, TNonVirtualSaver<T> t, R&... r) {
    const_cast<T*>(t.Data)->T::Load(s);
    ::LoadMany(s, r...);
}