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

Copyright (C) 2020-2021 Intel Corporation

SPDX-License-Identifier: MIT

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

//===----------------------------------------------------------------------===//
///
/// This is a simplified array type container that is suitable for use in an
/// interface with the UMD.  We want to be able to mix and match debug/release
/// dlls with each other.  Each dll statically links the CRT.  While it would
/// be nice to use STL containers on the interface:
///
/// 1) Their layouts can differ between debug and release builds so, for
///    example, a std::vector place in an output structure by IGC in debug
///    could be interpreted differently by a release UMD and cause corruption.
///
/// 2) memory allocated in IGC must be freed in IGC (same with UMD).  Having
///    destructors in the containers could cause accidental deletion across
///    domains if we're not careful.
///
/// In this Array class, we force deletion on explicitly via the destroy()
/// method.  We also clearly don't have any #ifdef _DEBUG (or the like) that
/// would cause the memory layout to be different.
///
//===----------------------------------------------------------------------===//

#pragma once

#include <iterator>
#include <type_traits>
#include <assert.h>

namespace Interface {

template<typename T>
class Array
{
    // We should only be placing objects in here that don't do anything
    // interesting in the destructor to discourage accidental deletions.
    static_assert(std::is_trivially_destructible_v<T>,
        "Array contained type must be trivially destructible!");
private:
    // Standard iterator style interface
    class iterator
    {
    public:
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        using pointer = T*;
        using reference = T&;
        using iterator_category = std::random_access_iterator_tag;
    private:
        pointer Ptr;
        iterator(pointer Ptr) : Ptr(Ptr) {}
    public:
        reference operator*() const { return *Ptr; }
        pointer operator->() const { return Ptr; }
        bool operator==(const iterator& RHS) const { return Ptr == RHS.Ptr; }
        bool operator!=(const iterator& RHS) const { return !(*this == RHS); }
        iterator operator++(int)
        {
            iterator V = *this;
            ++*this;
            return V;
        }
        iterator& operator++()
        {
            Ptr++;
            return *this;
        }

        friend Array;
    };

    // const version
    class const_iterator
    {
    public:
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        using pointer = const T*;
        using reference  = const T&;
        using iterator_category = std::random_access_iterator_tag;
    private:
        pointer Ptr;
        const_iterator(pointer Ptr) : Ptr(Ptr) {}
    public:
        reference operator*() const { return *Ptr; }
        pointer operator->() const { return Ptr; }
        bool operator==(const const_iterator& RHS) const { return Ptr == RHS.Ptr; }
        bool operator!=(const const_iterator& RHS) const { return !(*this == RHS); }
        const_iterator operator++(int)
        {
            const_iterator V = *this;
            ++*this;
            return V;
        }
        const_iterator& operator++()
        {
            Ptr++;
            return *this;
        }

        friend Array;
    };

public:
    Array() = default;

    Array(uint32_t NumElts)
    {
        if (NumElts == 0)
            return;

        Start = allocate(NumElts);
        End = Start + NumElts;

        for (uint32_t i = 0; i < NumElts; i++)
            Start[i] = typename iterator::value_type();
    }

    Array& operator=(const Array&) = delete;
    // no copying or copy assignment.  It would only make sense to have copy
    // assignment if we could delete the previous contents which may be unsafe.
    Array(const Array&) = delete;

    Array(Array&& RHS)
    {
        Start = RHS.Start;
        End = RHS.End;

        RHS.Start = nullptr;
        RHS.End = nullptr;
    }

    // move it around instead.
    Array& operator=(Array&& RHS) noexcept
    {
        Start = RHS.Start;
        End = RHS.End;

        RHS.Start = nullptr;
        RHS.End = nullptr;

        return *this;
    }

    // Convenince constructor to build your data up with the usual containers
    // prior to filling this up.
    template <typename InputIterator>
    Array(InputIterator first, InputIterator last)
    {
        size_t NumElts = std::distance(first, last);

        Start = allocate(NumElts);
        End = Start + NumElts;

        std::copy(first, last, this->begin());
    }

    size_t size() const {
        return End - Start;
    }

    // Only way to delete, explicitly.  Destructors are trivial so we don't
    // need to explicitly call them here.
    void destroy()
    {
        for (auto& v : *this)
        {
            v.destroy();
        }

        ::operator delete(Start);
    }

    iterator begin() { return iterator(Start); }
    iterator end() { return iterator(End); }

    const_iterator begin() const { return const_iterator(Start); }
    const_iterator end() const { return const_iterator(End); }

    const typename iterator::reference operator[](size_t i) const {
        assert((i < size()) && "out of bounds!");
        return Start[i];
    }
    typename iterator::reference operator[](size_t i) {
        assert((i < size()) && "out of bounds!");
        return Start[i];
    }

    // Useful for when we slightly over allocate to avoid having to do two
    // trips through a collection: one to the find the size and the other to
    // write the data.  Just shrink the size down after the fact.
    void trimSize(uint32_t NewSize) {
        if (NewSize > size())
            return;

        End = Start + NewSize;
    }

    bool empty() const {
        return Start == End;
    }
private:
    typename iterator::pointer Start = nullptr;
    typename iterator::pointer End = nullptr;

    inline typename iterator::pointer allocate(size_t NumElts)
    {
        return static_cast<typename iterator::pointer>(
            ::operator new (NumElts * sizeof(typename iterator::value_type)));
    }
};

} // Interface
