/*===================== begin_copyright_notice ==================================

Copyright (c) 2017 Intel Corporation

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

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.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


======================= end_copyright_notice ==================================*/

#pragma once

#include "cif/builtins/memory/buffer/buffer.h"

#include "cif/export/pimpl_base.h"

#include "cif/macros/enable.h"

#include "cif/helpers/memory.h"

namespace CIF {

namespace Builtins {

CIF_DECLARE_INTERFACE_PIMPL(Buffer) : CIF::PimplBase {
    CIF_PIMPL_DECLARE_CONSTRUCTOR(Version_t version, CIF::ICIF *parentInterface) {
    }

    CIF_PIMPL_DECLARE_CONSTRUCTOR(Version_t version) {
    }

    CIF_PIMPL_DECLARE_CONSTRUCTOR() {
    }

    CIF_PIMPL_DECLARE_DESTRUCTOR(){
        Deallocate();
    }

  /// Copies given element to the end of the buffer
  /// Note : If (packed == false), then this function will automatically align current underlying buffer
  ///        pointer to alignof(ElementT)
  template<typename ElementT>
  bool PushBackRawCopy(const ElementT &newEl, bool packed = true){
      static_assert(std::is_pod<ElementT>::value, "Supporting only POD types");
      if(packed == false){
          size_t alignment = alignof(ElementT);
          bool success = AlignUp(alignment);
          if(success == false){
              return false;
          }
      }

      return PushBackRawBytes(&newEl, sizeof(ElementT));
  }

    /// Sets custom allocator and deallocator functions to be used by this buffer interface
    /// Note : will reallocate using new allocators only when additional space will be needed
    /// Note : reallocator is optional and can be nullptr
    void SetAllocator(AllocatorT allocator, DeallocatorT deallocator, ReallocatorT reallocator){
        if(allocator == nullptr){
            allocator = EmptyAllocator;
        }

        if((this->memory) && (this->tempDeallocator == nullptr)){
            this->tempDeallocator = this->deallocator;
        }

        this->allocator = allocator;
        this->deallocator = deallocator;
        this->reallocator = reallocator;
    }

    /// Sets underlying buffer storage and its deallocator
    /// Note : will destroy current underlying buffer (if any)
    void SetUnderlyingStorage(void *memory, size_t size, DeallocatorT deallocator){
        Deallocate();

        this->memory = memory;
        this->size = size;
        this->capacity = size;
        this->tempDeallocator = deallocator;
    }

    /// Sets underlying constant buffer storage and its deallocator
    /// Note : will destroy current underlying buffer (if any)
    /// Note : will allocate new memory if this const memory will be accessed in non-const manners
    void SetUnderlyingStorage(const void *memory, size_t size){
        Deallocate();

        // casting-away constness, but marking as const
        this->memory = const_cast<void*>(memory);
        this->isConst = true;

        this->size = size;
        this->capacity = size;
        this->tempDeallocator = nullptr;
    }

    /// Detaches and returns current allocation leaving this buffer's underlying buffer empty
    void *DetachAllocation(){
        void *mem = memory;
        this->memory = nullptr;
        this->size = 0;
        this->capacity = 0;
        this->isConst = false;
        this->tempDeallocator = nullptr;
        return mem;
    }

    /// Returns const raw access to underlying buffer
    const void *GetMemoryRaw() const{
        return memory;
    }

    /// Returns writeable access to underlyng buffer
    /// Note : for constant buffer this will cause reallocation (copy!) to writeable memory
    void *GetMemoryRawWriteable(){
        if(this->isConst){
            if(false == Reallocate(this->size, this->capacity)){
                return nullptr;
            }
        }
        return memory;
    }

    /// Returns size in bytes of underlying buffer
    size_t GetSizeRaw() const{
        return size;
    }

    /// Returns capacity in bytes of underlying buffer
    size_t GetCapacityRaw() const{
        return capacity;
    }

    /// Resizes (may reallocate) underlying buffer's size
    bool Resize(size_t newSize) {
        return this->Reallocate(newSize, std::max(this->capacity, newSize));
    }

    /// Resizes (may reallocate) the underlying buffer's capacity
    bool Reserve(size_t newCapacity){
        return this->Reallocate(this->size, newCapacity);
    }

    /// Sets the size to 0
    void Clear(){
        this->size = 0;
    }

    /// Sets the size to 0 and deallocates underlying buffer
    void Deallocate(){
        if(isConst){
            this->memory = nullptr;
            this->size = 0;
            this->capacity = 0;
            this->isConst = false;
            this->tempDeallocator = nullptr;
        }else if(tempDeallocator){
            tempDeallocator(this->memory);
            this->memory = nullptr;
            this->size = 0;
            this->capacity = 0;
            this->tempDeallocator = nullptr;
        }else{
            if(deallocator != nullptr){
                deallocator(this->memory);
            }
            this->memory = nullptr;
            this->size = 0;
            this->capacity = 0;
        }
    }

    /// Aligns up current size to meet required alignment
    bool AlignUp(uint32_t alignment){
        size_t misalligned = alignment - (this->size % alignment);
        size_t newSize = this->size + misalligned;
        return Reallocate(newSize, newSize);
    }

    /// Pushes new raw element to buffer
    bool PushBackRawBytes(const void *newData, size_t size){
        if(size == 0){
            // nothing to do
            return true;
        }

        size_t oldSize = this->size;
        if(false == Reallocate(this->size + size, this->size + size)){
            return false;
        }

        CIF::SafeCopy(CIF::OffsetedPtr(GetMemoryRawWriteable(), oldSize), this->capacity - oldSize, newData, size);
        return true;
    }

    /// Can be true if SetUnderlyingStorage(const void *, size_t) was used, please refer to SetUnderlyingStorage
    bool IsConst() const{
        return isConst;
    }

protected:
    static void * CIF_CALLING_CONV EmptyAllocator(size_t size){
        return nullptr;
    }

    static void * CIF_CALLING_CONV DefaultAllocator(size_t size){
        return new(std::nothrow) char[size];
    }

    static void CIF_CALLING_CONV DefaultDeallocator(void * memory){
        if(memory != nullptr){
            delete []reinterpret_cast<char*>(memory);
        }
    }

    bool Reallocate(size_t newSize, size_t newCapacity){
        if(isConst){
            void * newMem = this->allocator(newCapacity);
            if(newMem == nullptr){
                return false;
            }

            size_t bytesToCopy = std::min(newSize, this->size);
            CIF::SafeCopy(newMem, newCapacity, this->memory, bytesToCopy);
            this->memory = newMem;
            this->isConst = false;
            this->tempDeallocator = nullptr;
            this->capacity = newCapacity;
            this->size = newSize;
            return true;
        }

        if(tempDeallocator != nullptr){
            if(newCapacity <= this->capacity){
                this->size = newSize;
                return true;
            }

            void * newMem = this->allocator(newCapacity);
            if(newMem == nullptr){
                return false;
            }
            size_t bytesToCopy = std::min(newSize, this->size);
            CIF::SafeCopy(newMem, newCapacity, this->memory, bytesToCopy);
            void *oldMemory = this->memory;
            this->memory = newMem;
            this->capacity = newCapacity;
            this->size = newSize;
            this->tempDeallocator(oldMemory);
            this->tempDeallocator = nullptr;
            return true;
        }

        if(newCapacity <= this->capacity){
            this->size = newSize;
            return true;
        }

        if(reallocator != nullptr){
            void *newMem = reallocator(this->memory, this->capacity, newCapacity);
            if(newMem == nullptr){
                return false;
            }

            this->memory = newMem;
            this->capacity = newCapacity;
            this->size = newSize;
            return true;
        }

        void *newMem = allocator(newCapacity);
        if(newMem == nullptr){
            return false;
        }

        size_t bytesToCopy = std::min(newSize, this->size);
        CIF::SafeCopy(newMem, newCapacity, this->memory, bytesToCopy);
        if(deallocator != nullptr){
            deallocator(this->memory);
        }
        this->memory = newMem;
        this->capacity = newCapacity;
        this->size = newSize;

        return true;
    }

    void *memory = nullptr;
    size_t size = 0;
    size_t capacity = 0;
    bool isConst = false;
    DeallocatorT tempDeallocator = nullptr;
    AllocatorT allocator = DefaultAllocator;
    DeallocatorT deallocator = DefaultDeallocator;
    ReallocatorT reallocator = nullptr;
};

CIF_DEFINE_INTERFACE_TO_PIMPL_FORWARDING_CTOR_DTOR(Buffer);

}

}

#include "cif/macros/disable.h"
