#pragma once

#include <library/cpp/deprecated/atomic/atomic.h>

#include <util/system/types.h>
#include <util/system/compiler.h>
#include <util/generic/array_ref.h>

namespace NActors {

    class TSharedData {
    public:
        class IOwner {
        public:
            virtual ~IOwner() = default;

            virtual void Deallocate(char*) noexcept = 0;
        };

        struct TPrivateHeader {
            size_t AllocSize;
            size_t Pad;
        };

        static_assert(sizeof(TPrivateHeader) == 16, "TPrivateHeader has an unexpected size");

        struct THeader {
            TAtomic RefCount;
            IOwner* Owner;
        };

        static_assert(sizeof(THeader) == 16, "THeader has an unexpected size");

        enum : size_t {
            PrivateHeaderSize = sizeof(TPrivateHeader),
            HeaderSize = sizeof(THeader),
            OverheadSize = PrivateHeaderSize + HeaderSize,
            MaxDataSize = (std::numeric_limits<size_t>::max() - OverheadSize)
        };

    public:
        TSharedData() noexcept
            : Data_(nullptr)
            , Size_(0)
        { }

        ~TSharedData() noexcept {
            Release();
        }

        TSharedData(const TSharedData& other) noexcept
            : Data_(other.Data_)
            , Size_(other.Size_)
        {
            AddRef();
        }

        TSharedData(TSharedData&& other) noexcept
            : Data_(other.Data_)
            , Size_(other.Size_)
        {
            other.Data_ = nullptr;
            other.Size_ = 0;
        }

        TSharedData& operator=(const TSharedData& other) noexcept {
            if (this != &other) {
                Release();
                Data_ = other.Data_;
                Size_ = other.Size_;
                AddRef();
            }
            return *this;
        }

        TSharedData& operator=(TSharedData&& other) noexcept {
            if (this != &other) {
                Release();
                Data_ = other.Data_;
                Size_ = other.Size_;
                other.Data_ = nullptr;
                other.Size_ = 0;
            }
            return *this;
        }

        Y_FORCE_INLINE explicit operator bool() const { return Size_ > 0; }

        Y_FORCE_INLINE char* mutable_data() { Y_VERIFY_DEBUG(IsPrivate()); return Data_; }
        Y_FORCE_INLINE char* mutable_begin() { Y_VERIFY_DEBUG(IsPrivate()); return Data_; }
        Y_FORCE_INLINE char* mutable_end() { Y_VERIFY_DEBUG(IsPrivate()); return Data_ + Size_; }

        Y_FORCE_INLINE const char* data() const { return Data_; }
        Y_FORCE_INLINE const char* begin() const { return Data_; }
        Y_FORCE_INLINE const char* end() const { return Data_ + Size_; }

        Y_FORCE_INLINE size_t size() const { return Size_; }

        /**
         * Trims data to the specified size
         * Underlying data is not reallocated
         * Returns trimmed amount in bytes
         */
        size_t TrimBack(size_t size) noexcept {
            size_t trimmed = 0;
            if (Size_ > size) {
                trimmed = Size_ - size;
                if (!size) {
                    Release();
                    Data_ = nullptr;
                }
                Size_ = size;
            }
            return trimmed;
        }

        /**
         * Copies data to new allocated buffer if data is shared
         * New container loses original owner
         * Returns pointer to mutable buffer
         */
        char* Detach() {
            if (IsShared()) {
                *this = TSharedData::Copy(data(), size());
            }
            return Data_;
        }

        /**
         * Returns a view of underlying data starting with pos and up to len bytes
         */
        TStringBuf Slice(size_t pos = 0, size_t len = -1) const noexcept {
            pos = Min(pos, Size_);
            len = Min(len, Size_ - pos);
            return { Data_ + pos, len };
        }

        explicit operator TStringBuf() const noexcept {
            return Slice();
        }

        bool IsPrivate() const {
            return Data_ ? IsPrivate(Header()) : true;
        }

        bool IsShared() const {
            return !IsPrivate();
        }

        TString ToString() const {
            return TString(data(), size());
        }

        /**
         * Attach to pre-allocated data with a preceding THeader
         */
        static TSharedData AttachUnsafe(char* data, size_t size) noexcept {
            TSharedData result;
            result.Data_ = data;
            result.Size_ = size;
            return result;
        }

        /**
         * Make uninitialized buffer of the specified size
         */
        static TSharedData Uninitialized(size_t size) {
            return AttachUnsafe(Allocate(size), size);
        }

        /**
         * Make a copy of the specified data
         */
        static TSharedData Copy(const void* data, size_t size) {
            TSharedData result = Uninitialized(size);
            if (size) {
                ::memcpy(result.Data_, data, size);
            }
            return result;
        }

        /**
         * Make a copy of the specified data
         */
        static TSharedData Copy(TArrayRef<const char> data) {
            return Copy(data.data(), data.size());
        }

    private:
        Y_FORCE_INLINE THeader* Header() const noexcept {
            Y_VERIFY_DEBUG(Data_);
            return reinterpret_cast<THeader*>(Data_ - sizeof(THeader));
        }

        static bool IsPrivate(THeader* header) noexcept {
            return 1 == AtomicGet(header->RefCount);
        }

        void AddRef() noexcept {
            if (Data_) {
                AtomicIncrement(Header()->RefCount);
            }
        }

        void Release() noexcept {
            if (Data_) {
                auto* header = Header();
                if (IsPrivate(header) || 0 == AtomicDecrement(header->RefCount)) {
                    if (auto* owner = header->Owner) {
                        owner->Deallocate(Data_);
                    } else {
                        Deallocate(Data_);
                    }
                }
            }
        }

    private:
        static char* Allocate(size_t size);
        static void Deallocate(char* data) noexcept;

    private:
        char* Data_;
        size_t Size_;
    };

}