#pragma once

#include "BaseObj.h"
#include "Types.h"
#include "Util.h"

#include <initializer_list>
#include <utility>

namespace ehs
{
    /// An array with extra memory pre-allocated for fast pushes.
    /// @tparam T Array data type to use.
    /// @tparam N Number data type to use.
    /// @note If extra memory is set to five then each time that memory is filled it will add five extra.
	template<typename T, typename N = UInt_64>
	class Vector : public BaseObj
	{
	protected:
		N rawSize;
		N size;
		N stride;
		T* data;

	public:
        /// Frees any data created on the heap.
        ~Vector() override
		{
            delete[] data;
		}

        /// Default members initialization.
		Vector()
			: rawSize(0), size(0), stride(5), data(nullptr)
		{
			AddType("Vector");
		}

		/// Initializes members for pre-allocated memory to write to later.
		/// @param [in] size The size of memory to pre-allocate.
		/// @param [in] stride The stride size of memory to pre-allocate.
		Vector(const N size, const N stride)
			: rawSize(size + stride), size(size), stride(stride), data(new T[rawSize])
		{
			AddType("Vector");
		}

        /// Initializes this vector with an initializer list object.
        /// @param [in] list The given initializer list.
        /// @param [in] stride The extra amount of memory to allocate.
        Vector(std::initializer_list<T> list, const N stride = 5)
			: rawSize(0), size(list.size()), stride(stride), data(nullptr)
        {
			AddType("Vector");

			if (stride)
			{
				rawSize = list.size() / stride * stride;
				if (list.size() % stride)
					rawSize += stride;
			}
			else
			{
				rawSize = list.size();
			}

			data = new T[rawSize];

            N i = 0;
            for (auto v = list.begin(); v != list.end(); ++v)
                data[i++] = std::move(*v);
        }

        /// Initializes members with given C-style array.
        /// @param [in] data The C-style array.
        /// @param [in] size The size of the given C-style array.
        /// @param [in] stride The size of the extra memory allocated.
		Vector(const T* data, const N size, const N stride)
			: rawSize(0), size(size), stride(stride), data(nullptr)
		{
			AddType("Vector");

			if (stride)
			{
				rawSize = size / stride * stride;
				if (size % stride)
					rawSize += stride;
			}
			else
			{
				rawSize = size;
			}

			data = new T[rawSize];

			for (N i = 0; i < size; ++i)
				this->data[i] = data[i];
		}

        /// Copies all members from the given vector object.
        /// @param [in] vec The vector object to copy from.
		Vector(const Vector& vec)
			: BaseObj(vec), rawSize(vec.rawSize), size(vec.size), stride(vec.stride), data(new T[rawSize])
		{
			for (N i = 0; i < size; ++i)
				data[i] = vec.data[i];
		}

        Vector(Vector&& vec) noexcept
            : BaseObj((BaseObj&&)vec), rawSize(vec.rawSize), size(vec.size), stride(vec.stride), data(vec.data)
        {
            vec.rawSize = 0;
            vec.size = 0;
            vec.stride = 0;
            vec.data = nullptr;
        }

        /// Copies all members from the given vector object.
        /// @param [in] vec The vector object to copy from.
        /// @returns The vector that has been assigned to.
		Vector& operator=(const Vector& vec)
		{
            if (this == &vec)
                return *this;

			BaseObj::operator=(vec);

			rawSize = vec.rawSize;
			size = vec.size;
			stride = vec.stride;

            delete[] data;
			data = new T[rawSize];

			for (N i = 0; i < size; ++i)
				data[i] = vec.data[i];

			return *this;
		}

        Vector& operator=(Vector&& vec) noexcept
        {
            if (this == &vec)
                return *this;

			BaseObj::operator=((BaseObj&&)vec);

            rawSize = vec.rawSize;
            size = vec.size;
            stride = vec.stride;
			delete[] data;
            data = vec.data;

            vec.rawSize = 0;
            vec.size = 0;
            vec.stride = 0;
            vec.data = nullptr;

            return *this;
        }

		bool operator==(const Vector& in) const
        {
        	if (size != in.size)
        		return false;

        	return Util::Compare(data, in.data, size);
        }

		bool operator!=(const Vector& in) const
        {
        	if (size != in.size)
        		return true;

        	return !Util::Compare(data, in.data, size);
        }

        /// Adds a given initializer list at the end of the vector.
        /// @param [in] value The given initializer list to push to the end of the vector.
        Vector& operator+=(std::initializer_list<T> value)
        {
			if (size + value.size() >= rawSize)
			{
				if (stride)
				{
					rawSize = (size + value.size()) / stride * stride;
					if ((size + value.size()) % stride)
						rawSize += stride;
				}
				else
				{
					rawSize = size + value.size();
				}

				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(std::move(data[i]));

				delete[] data;
				data = result;
			}

			for (auto v = value.begin(); v != value.end(); ++v)
				data[size++] = std::move(*v);

			return *this;
        }

        /// Adds a given value at the end of the vector.
        /// @param [in] value The given value to push to the end of the vector.
        Vector& operator+=(const T value)
        {
			if (size + 1 >= rawSize)
			{
				if (stride)
					rawSize = size + stride;
				else
					rawSize = size + 1;

				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}

			data[size++] = std::move(value);

			return *this;
        }

        /// Retrieves the raw C-style array from casting an array object.
		operator T* () const
		{
			return data;
		}

        /// Retrieves the size of the vector object including the extra memory allocated.
        /// @returns The raw size.
		N RawSize() const
		{
			return rawSize;
		}

        /// Retrieves the size of the array not including the extra memory allocated.
        /// @returns The size.
		N Size() const
		{
			return size;
		}

        /// Retrieves the size of the extra memory allocated.
        /// @returns The extra size.
		N Stride() const
		{
			return stride;
		}

		/// Retrieves the index at the end of the array.
		/// @returns The index.
		N End() const
		{
			return size ? size - 1 : size;
		}

		/// Copies a vector object with offsets.
		/// @param [in] dstOffset The offset index to copy the given vector object to.
		/// @param [in] src The given vector object.
		/// @param [in] srcOffset The offset index from the given vector object to copy from.
		void Copy(const N dstOffset, Vector<T, N> src, const N srcOffset = 0)
		{
			for (N i = 0; i < src.Size() - srcOffset; ++i)
				data[i + dstOffset] = std::move(src[i + srcOffset]);
		}

        /// Copies a C-style array with offsets.
        /// @param [in] dstOffset The offset index to copy the given C-style array to.
        /// @param [in] src The given C-style array.
        /// @param [in] size The size from the given C-style array to copy.
		void Copy(const N dstOffset, const T* src, const N inSize)
		{
			if (dstOffset + inSize > size)
				return;

			for (N i = 0; i < inSize; ++i)
				data[i + dstOffset] = src[i];
		}

        /// Swaps two values in the vector.
        /// @param [in] a The first index to swap with.
        /// @param [in] b The second index to swap with.
		void Swap(N a, N b)
		{
			T tmp = std::move(data[a]);

			data[a] = std::move(data[b]);
			data[b] = std::move(tmp);
		}

        /// Inserts a value at a specified index that is available.
        /// @param [in] index The index to insert the value at.
        /// @param [in] value The given value to insert.
        void Insert(const N index, const T value)
        {
			N newSize = 0;
			if (index > size - 1)
				newSize = size + ((index + 1) - size);
			else
				newSize = size + 1;

			if (newSize >= rawSize)
			{
				if (stride)
					rawSize += newSize + stride;
				else
					rawSize = newSize;

				T* result = new T[rawSize];

				for (N i = 0; i < index; ++i)
					result[i] = std::move(data[i]);

				result[index] = std::move(value);

				for (N i = index; i < size; ++i)
					result[i + 1] = std::move(data[i]);

				delete[] data;
				data = result;
			}
			else
			{
				for (N i = index; i < size; ++i)
					data[i + 1] = std::move(data[i]);

				data[index] = std::move(value);
			}

			size = newSize;
        }

        /// Removes a value at a specified index.
        /// @param [in] index The index to remove the value at.
        /// @returns The removed data.
        T Remove(const N index)
        {
			T popped = {};

            if (!size || index >= size)
                return popped;

			popped = std::move(data[index]);

			if (!stride)
			{
				rawSize = size - 1;
				T* result = new T[rawSize];

				for (N i = 0; i < index; ++i)
					result[i] = std::move(data[i]);

				for (N i = index + 1; i < size; ++i)
					result[i - 1] = std::move(data[i]);

				delete[] data;
				data = result;
			}
			else if (rawSize - stride && size - 1 <= rawSize - stride)
			{
				rawSize -= stride;
				T* result = new T[rawSize];

				for (N i = 0; i < index; ++i)
					result[i] = std::move(data[i]);

				for (N i = index + 1; i < size; ++i)
					result[i - 1] = std::move(data[i]);

				delete[] data;
				data = result;
			}
			else
			{
				for (N i = index + 1; i < size; ++i)
					data[i - 1] = std::move(data[i]);
			}

			--size;

			return popped;
        }

        /// Adds a given C-style array at the end of the vector.
        /// @param [in] value The given C-style array to push to the end of the vector.
        /// @param [in] size The size of the given C-style array.
		void Push(const T* const value, const N size)
		{
			if (this->size + size >= rawSize)
			{
				if (stride)
				{
					rawSize = (this->size + size()) / stride * stride;
					if ((this->size + size) % stride)
						rawSize += stride;
				}
				else
				{
					rawSize = this->size + size;
				}

				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}

			for (N i = 0; i < size; ++i)
				data[this->size + i] = value[i];

			this->size += size;
		}

        /// Adds a given vector object at the end of the vector.
        /// @param [in] value The given vector object to push to the end of the vector.
		void Push(Vector<T> value)
		{
			if (size + value.size >= rawSize)
			{
				if (stride)
				{
					rawSize = (size + value.size) / stride * stride;
					if ((size + value.size) % stride)
						rawSize += stride;
				}
				else
				{
					rawSize = size + value.size;
				}

				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}

			for (N i = 0; i < value.size; ++i)
				data[size + i] = std::move(value.data[i]);

			size += value.size;
		}

        /// Adds a given initializer at the end of the vector.
        /// @param [in] value The given initializer list to push to the end of the vector.
        void Push(std::initializer_list<T> value)
        {
			if (size + value.size() >= rawSize)
			{
				if (stride)
				{
					rawSize = (size + value.size()) / stride * stride;
					if ((size + value.size()) % stride)
						rawSize += stride;
				}
				else
				{
					rawSize = size + value.size();
				}

				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}

            for (auto v = value.begin(); v != value.end(); ++v)
                data[size++] = std::move(*v);
        }

        /// Adds a given value at the end of the vector.
        /// @param [in] value The given value to push to the end of the vector.
		void Push(T value)
		{
			if (size + 1 >= rawSize)
			{
				if (stride)
					rawSize += stride;
				else
					rawSize = size + 1;

				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}

			data[size++] = std::move(value);
		}

        /// Much like the stack it pops a value at the end of the vector.
        /// @returns The removed value.
		T Pop()
		{
			T popped = {};

			if (!size)
				return popped;

			popped = std::move(data[--size]);

			if (!stride)
			{
				rawSize = size;
				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}
			else if (rawSize - stride && size < rawSize - stride)
			{
				rawSize -= stride;
				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

				delete[] data;
				data = result;
			}

			return popped;
		}

		/// Will swap the value at the given index with the value at the end of the vector and pops it.
		/// @param [in] index The index of the value to swap with.
		/// @returns The removed value.
		T Pop(const N index)
		{
			if (!size)
				return {};

			N lastIndex = size - 1;

			if (index < lastIndex)
				Swap(index, lastIndex);

			return Pop();
		}

        /// Resizes the vector while keeping its alignment.
        /// @param [in] newSize The size to change to.
		void Resize(const N newSize)
		{
			if (newSize == size)
				return;

			if (stride)
			{
				rawSize = newSize / stride * stride;
				if (newSize % stride)
					rawSize += stride;
			}
			else
			{
				rawSize = newSize;
			}

			T* result = new T[rawSize];

			for (N i = 0; i < size && i < newSize; ++i)
				result[i] = std::move(data[i]);

            delete[] data;
			data = result;
			size = newSize;
		}

        /// Removes any extra allocated memory.
		void ExactSize()
		{
			if (!stride)
				return;

			stride = 0;

			if (size)
			{
				rawSize = size;
				T* result = new T[rawSize];

				for (N i = 0; i < size; ++i)
					result[i] = std::move(data[i]);

                delete[] data;
				data = result;
			}
			else
			{
				rawSize = 0;
				delete[] data;
				data = nullptr;
			}
		}

		/// Releases the resources of the vector.
		void Clear()
		{
			if (!size)
				return;

			rawSize = stride;
			size = 0;

			delete[] data;

			if (rawSize)
				data = new T[rawSize];
			else
				data = nullptr;
		}
	};
}