#pragma once

#include <utility>
#include <cstddef>
#include <stdexcept>
#include <iostream>

template <typename T>
class ArrayList {
    private:
        std::size_t m_length;
        T* m_data;

    public:
        ArrayList() : m_length(0),  m_data(static_cast<T*>(::operator new(0))) {}

        explicit ArrayList(std::size_t capacity) : m_length(0), m_data(static_cast<T*>(::operator new(sizeof(T) * capacity))) {}

        ArrayList(const ArrayList& rhs)
        : m_length(rhs.size())
        , m_data(static_cast<T*>(::operator new(sizeof(T) * m_length))) {
            for (std::size_t i = 0; i < rhs.size(); i++) {
                m_data[i] = rhs[i];
            }
        }

        ~ArrayList() {
            destruct_current();
            ::operator delete(m_data); 
        }
        
        ArrayList& operator=(const ArrayList& rhs) {
            if (&rhs == this) 
                return *this;

            destruct_current();
            ::operator delete(m_data);
            m_length = rhs.size();
            m_data = static_cast<T*>(::operator new(sizeof(T) * m_length));

            for (std::size_t i = 0; i < m_length; i++) {
                m_data[i] = rhs[i];
            }
          
            return *this;
        }

        T& operator[](std::size_t index) {
            if (index < 0 || index >= m_length) {
                throw std::out_of_range("index out of range");
            }

            return m_data[index];
        }

        const T& operator[](std::size_t index) const {
            if (index < 0 || index >= m_length) {
                throw std::out_of_range("index out of range");
            }

            return m_data[index];
        }

        T* items() const {
            return m_data;
        }

        std::size_t size() const {
            return m_length;
        }

        void insert(std::size_t index, const T& item) {
            if (index < 0 || index > m_length) {
                throw std::out_of_range("index is out of range");
            }

            T* temp = static_cast<T*>(::operator new(sizeof(T) * (m_length + 1)));

            for (std::size_t i = 0; i < index; i++) {
                temp[i] = std::move(m_data[i]);
            }
            temp[index] = item;
            for (std::size_t i = index; i < m_length; i++) {
                temp[i + 1] = std::move(m_data[i]);
            }

            m_length++;

            destruct_current();
            ::operator delete(m_data);
            m_data = temp;
        }

        void remove(std::size_t index) {
            if (m_length == 0 || index < 0 || index >= m_length) {
                throw std::out_of_range("index is out of range");
            }

            T* temp = static_cast<T*>(::operator new(sizeof(T) * m_length - 1));

            for (std::size_t i = 0; i < index; i++) {
                temp[i] = std::move(m_data[i]);
            }
            for (std::size_t i = index + 1; i < m_length; i++) {
                temp[i - 1] = std::move(m_data[i]);
            }

            destruct_current();
            ::operator delete(m_data);
            m_data = temp;
            m_length--;
        }

        void destruct_current() {
            for (std::size_t i = 0; i < m_length; i++) {
                m_data[i].~T();
            }
        }

        void print() const {
            for (std::size_t i = 0; i < m_length; i++) {
                std::cout << m_data[i] << " ";
            }
            std::cout << "\n";
        }
};