/*     Copyright 2015-2019 Egor Yusov
 *  
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF ANY PROPRIETARY RIGHTS.
 *
 *  In no event and under no legal theory, whether in tort (including negligence), 
 *  contract, or otherwise, unless required by applicable law (such as deliberate 
 *  and grossly negligent acts) or agreed to in writing, shall any Contributor be
 *  liable for any damages, including any direct, indirect, special, incidental, 
 *  or consequential damages of any character arising as a result of this License or 
 *  out of the use or inability to use the software (including but not limited to damages 
 *  for loss of goodwill, work stoppage, computer failure or malfunction, or any and 
 *  all other commercial damages or losses), even if such Contributor has been advised 
 *  of the possibility of such damages.
 */

#pragma once

/// \file
/// Implementation of Diligent::RingBuffer class


#include <deque>
#include "../../../Primitives/interface/MemoryAllocator.h"
#include "../../../Platforms/Basic/interface/DebugUtilities.h"
#include "../../../Common/interface/Align.h"
#include "../../../Common/interface/STDAllocator.h"

namespace Diligent
{
    /// Implementation of a ring buffer. The class is not thread-safe.
    class RingBuffer
    {
    public:
        using OffsetType = size_t;
        struct FrameHeadAttribs
        {
            FrameHeadAttribs(Uint64 fv, OffsetType off, OffsetType sz)noexcept : 
                FenceValue(fv),
                Offset    (off),
                Size      (sz)
            {}

            // Fence value associated with the command list in which 
            // the allocation could have been referenced last time
            Uint64     FenceValue;
            OffsetType Offset;
            OffsetType Size;
        };
        static constexpr const OffsetType InvalidOffset = static_cast<OffsetType>(-1);

        RingBuffer(OffsetType MaxSize, IMemoryAllocator &Allocator)noexcept : 
            m_CompletedFrameHeads(STD_ALLOCATOR_RAW_MEM(FrameHeadAttribs, Allocator, "Allocator for deque<FrameHeadAttribs>")),
            m_MaxSize(MaxSize)
        {}

        RingBuffer(RingBuffer&& rhs)noexcept : 
            m_CompletedFrameHeads(std::move(rhs.m_CompletedFrameHeads)),
            m_Tail          (rhs.m_Tail),
            m_Head          (rhs.m_Head),
            m_MaxSize       (rhs.m_MaxSize),
            m_UsedSize      (rhs.m_UsedSize),
            m_CurrFrameSize (rhs.m_CurrFrameSize)
        {
            rhs.m_Tail          = 0;
            rhs.m_Head          = 0;
            rhs.m_MaxSize       = 0;
            rhs.m_UsedSize      = 0;
            rhs.m_CurrFrameSize = 0;
        }

        RingBuffer& operator = (RingBuffer&& rhs)noexcept
        {
            m_CompletedFrameHeads = std::move(rhs.m_CompletedFrameHeads);
            m_Tail          = rhs.m_Tail;
            m_Head          = rhs.m_Head;
            m_MaxSize       = rhs.m_MaxSize;
            m_UsedSize      = rhs.m_UsedSize;
            m_CurrFrameSize = rhs.m_CurrFrameSize;

            rhs.m_MaxSize       = 0;
            rhs.m_Tail          = 0;
            rhs.m_Head          = 0;
            rhs.m_UsedSize      = 0;
            rhs.m_CurrFrameSize = 0;

            return *this;
        }

        RingBuffer             (const RingBuffer&) = delete;
        RingBuffer& operator = (const RingBuffer&) = delete;

        ~RingBuffer()
        {
            VERIFY(m_UsedSize==0, "All space in the ring buffer must be released");
        }

        OffsetType Allocate(OffsetType Size, OffsetType Alignment)
        {
            VERIFY_EXPR(Size > 0);
            VERIFY(IsPowerOfTwo(Alignment), "Alignment (", Alignment, ") must be power of 2");
            Size = Align(Size, Alignment);

            if (m_UsedSize + Size > m_MaxSize)
            {
                return InvalidOffset;
            }

            auto AlignedHead = Align(m_Head, Alignment);
            if (m_Head >= m_Tail)
            {
                //                                         AlignedHead
                //                     Tail          Head  |            MaxSize
                //                     |                |  |            |
                //  [                  xxxxxxxxxxxxxxxxx...             ]
                //                                         
                //
                if (AlignedHead + Size <= m_MaxSize)
                {
                    auto Offset       = AlignedHead;
                    auto AdjustedSize = Size + (AlignedHead - m_Head);
                    m_Head           += AdjustedSize;
                    m_UsedSize       += AdjustedSize;
                    m_CurrFrameSize  += AdjustedSize;
                    return Offset;
                }
                else if (Size <= m_Tail)
                {
                    // Allocate from the beginning of the buffer
                    //
                    //                                                    
                    // Offset              Tail          Head               MaxSize
                    //  |                  |                |<---AddSize--->|
                    //  [                  xxxxxxxxxxxxxxxxx++++++++++++++++]
                    // 
                    OffsetType AddSize = (m_MaxSize - m_Head) + Size;
                    m_UsedSize      += AddSize;
                    m_CurrFrameSize += AddSize;
                    m_Head           = Size;
                    return 0;
                }
            }
            else if (AlignedHead + Size <= m_Tail )
            {
                //          AlignedHead
                //    Head  |              Tail             
                //       |  |              |             
                //  [xxxx...               xxxxxxxxxxxxxxxxxxxxxxxxxx]
                //
                auto Offset       = AlignedHead;
                auto AdjustedSize = Size + (AlignedHead - m_Head);
                m_Head           += AdjustedSize;
                m_UsedSize       += AdjustedSize;
                m_CurrFrameSize  += AdjustedSize;
                return Offset;
            }

            return InvalidOffset;
        }

        // FenceValue is the fence value associated with the command list in which the head
        // could have been referenced last time
        // See http://diligentgraphics.com/diligent-engine/architecture/d3d12/managing-resource-lifetimes/
        void FinishCurrentFrame(Uint64 FenceValue)
        {
#ifdef _DEBUG
            if (!m_CompletedFrameHeads.empty())
                VERIFY(FenceValue >= m_CompletedFrameHeads.back().FenceValue, "Current frame fence value (", FenceValue, ") is lower than the fence value of the previous frame (", m_CompletedFrameHeads.back().FenceValue, ")");
#endif
            // Ignore zero-size frames
            if (m_CurrFrameSize != 0)
            {
                m_CompletedFrameHeads.emplace_back(FenceValue, m_Head, m_CurrFrameSize);
                m_CurrFrameSize = 0;
            }
        }

        // CompletedFenceValue indicates GPU progress
        // See http://diligentgraphics.com/diligent-engine/architecture/d3d12/managing-resource-lifetimes/
        void ReleaseCompletedFrames(Uint64 CompletedFenceValue)
        {
            // We can release all heads whose associated fence value is less than or equal to CompletedFenceValue
            while(!m_CompletedFrameHeads.empty() && m_CompletedFrameHeads.front().FenceValue <= CompletedFenceValue)
            {
                const auto& OldestFrameHead = m_CompletedFrameHeads.front();
                VERIFY_EXPR(OldestFrameHead.Size <= m_UsedSize);
                m_UsedSize -= OldestFrameHead.Size;
                m_Tail      = OldestFrameHead.Offset;
                m_CompletedFrameHeads.pop_front();
            }

            if (IsEmpty())
            {
#ifdef _DEBUG
                VERIFY(m_CompletedFrameHeads.empty(), "Zero-size heads are not added to the list, and since the buffer is empty, there must be no heads in the list");
                for(const auto& head : m_CompletedFrameHeads)
                    VERIFY(head.Size == 0, "Non zero-size head found");
#endif
                m_CompletedFrameHeads.clear();

                //       t,h                 t,h           
                //  |     |     |   ====>     |           |
                m_Tail = m_Head = 0;
            }
        }

        OffsetType GetMaxSize() const { return m_MaxSize; }
        bool       IsFull()     const { return m_UsedSize==m_MaxSize; };
        bool       IsEmpty()    const { return m_UsedSize==0; };
        OffsetType GetUsedSize()const { return m_UsedSize; }

    private:
        std::deque< FrameHeadAttribs, STDAllocatorRawMem<FrameHeadAttribs> > m_CompletedFrameHeads;
        OffsetType m_Tail          = 0;
        OffsetType m_Head          = 0;
        OffsetType m_MaxSize       = 0;
        OffsetType m_UsedSize      = 0;
        OffsetType m_CurrFrameSize = 0;
    };
}
