#pragma once

#include <memory>
#include "Test.hpp"

namespace smart_pointer {
class exception : std::exception {
    using base_class = std::exception;
    using base_class::base_class;
};

template<typename T, typename Allocator>
    class SmartPointer {
    ENABLE_CLASS_TESTS;

        public:
            using value_type = T;
            
            explicit SmartPointer(value_type* ptr = nullptr) {
                if (ptr == nullptr) {
                    this->core = nullptr;
                } else {
                    this->core = new Core();
                    this->core->ptr = ptr;
                    this->core->count = 1;
                }
            }

            ~SmartPointer() {
                if (this->core != nullptr && this->core->count == 1) {
                    delete this->core;
                }
            }
            
            SmartPointer(const SmartPointer& obj) {
                this->core = obj.core;
                
                if (obj.core != nullptr) {
                    this->core->count++;
                }
            }
            
            SmartPointer(SmartPointer&& dyingObj) {
                this->core = dyingObj.core;
                dyingObj.core = nullptr;
            }
             
            SmartPointer& operator=(const SmartPointer& other) {
                if (this->core != nullptr && this->core->count == 1) {
                    delete this->core;
                }

                this->core = other.core;

                if (other.core != nullptr) {
                    this->core->count++;
                }

                return *this;
            }
            
            SmartPointer& operator=(SmartPointer&& dyingObj) {
                if (this->core != nullptr) {
                    delete this->core;
                }

                this->core = dyingObj.core;
                dyingObj.core = nullptr;
                return *this;
            }

            SmartPointer& operator=(value_type* other) {
                if (this->core != nullptr && this->core->count == 1) {
                    delete this->core;
                }
            
                if (other != nullptr) {
                    this->core = new Core();
                    this->core->ptr = other;
                    this->core->count = 1;
                } else {
                    this->core = nullptr;
                }

                return *this;
            }
            
            value_type& operator*() {
                if (this->core == nullptr) {
                    throw smart_pointer::exception();
                } else {
                    return *this->core->ptr;
                }
            }

            const value_type& operator*() const {
                if (this->core == nullptr) {
                    throw smart_pointer::exception();
                } else {
                    return *this->core->ptr;
                }
            }
            
            value_type* operator->() const {
                if (this->core != nullptr) {
                    return this->core->ptr;
                } else {
                    return nullptr;
                }
            }
            
            value_type* get() const {
                if (this->core != nullptr) {
                    return this->core->ptr;
                } else {
                    return nullptr;
                }
            }
            
            operator bool() const {
                return this->core != nullptr;
            }
            
            template<typename U, typename AnotherAllocator>
            bool operator==(const SmartPointer<U, AnotherAllocator>& ptr) const {
                return static_cast<void*>(this->get()) == static_cast<void*>
                    (ptr.get());
            }
            
            template<typename U, typename AnotherAllocator>
            bool operator!=(const SmartPointer<U, AnotherAllocator>& ptr) const {
                return static_cast<void*>(this->get()) != static_cast<void*>
                    (ptr.get());
            }
            
            std::size_t count_owners() const {
                if (this->core != nullptr) {
                    return this->core->count;
                } else {
                    return 0;
                }
            }

        private:
            class Core {
            public:
                size_t count;
                value_type* ptr;

                ~Core() {
                    Allocator del;
                    del.deallocate(ptr, sizeof(value_type));
                    this->ptr = nullptr;
                }
            };

            Core* core;
    };
}  // namespace smart_pointer