#pragma once

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

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

    // `SmartPointer` class declaration
    template<
        typename T,
        typename Allocator
    >

        class SmartPointer {
        // don't remove this macro
        ENABLE_CLASS_TESTS;
        
         public:
            using value_type = T;

            explicit SmartPointer(value_type* p = nullptr) {
                if (p == nullptr) core = nullptr;
                else {
                    core = new Core(p);
                }
            }

            // copy constructor
            SmartPointer(const SmartPointer& other) : core(other.core) {
                if (other.core != nullptr) core->count++;
            }

            // move constructor
            SmartPointer(SmartPointer&& other) : core(other.core) {
                other = nullptr;
            }

            // copy assigment
            SmartPointer& operator=(const SmartPointer& other) {
                if (&other == this) return *this;

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

                return *this;
            }

            // move assigment
            SmartPointer& operator=(SmartPointer&& other) {
                if (&other == this) return *this;

                if (core != nullptr) {
                    core->count -= 1;
                    if (core->count == 0) {
                        delete core;
                    }
                }

                core = other.core;
                other.core = nullptr;

                return *this;
            }

            //
            SmartPointer& operator=(value_type* p) {
                if (core != nullptr) {
                    core->count -= 1;
                    if (core->count == 0) {
                        delete core;
                    }
                }

                if (p != nullptr) core = new Core(p);
                else {
                    core = nullptr;
                }
                return *this;
            }

            ~SmartPointer() {
            }

            // return reference to the object of class/type T
            // if SmartPointer contains nullptr throw `SmartPointer::exception`
            value_type& operator*() {
                if (core != nullptr) return *(core->p);
                throw smart_pointer::exception();
            }
            const value_type& operator*() const {
                if (core != nullptr) return *(core->p);
                throw smart_pointer::exception();

            }

            // return pointer to the object of class/type T
            value_type* operator->() const {
                if (core != nullptr) return core->p;
                return nullptr;
            }

            value_type* get() const {
                if (core != nullptr) return core->p;
                return nullptr;
            }

            // if pointer == nullptr => return false
            operator bool() const {
                return core != nullptr;
            }

            // if pointers points to the same address or both null => true
            template<typename U, typename AnotherAllocator>
            bool operator==(const SmartPointer<U, AnotherAllocator>& other) const {
                return ((void*)this->get() == (void*)other.get() || (!this && !other));
            }

            // if pointers points to the same address or both null => false
            template<typename U, typename AnotherAllocator>
            bool operator!=(const SmartPointer<U, AnotherAllocator>& other) const {
                return !((void*)this->get() == (void*)other.get() || (!this && !other));
            }

            // if smart pointer contains non-nullptr => return count owners
            // if smart pointer contains nullptr => return 0
            std::size_t count_owners() const {
                if (core == nullptr) return 0;
                else return core->count;
            };

         private:
            class Core {
             public:
                value_type* p;
                std::size_t count;

                Core(value_type* ptr) : p(ptr), count(1) {}

                ~Core() {
                    Allocator a;
                    a.deallocate(p, sizeof(value_type));
                    p = nullptr;
                }

                value_type get() {
                    return *p;
                }
            };
            Core* core;
    };

}