#pragma once

#include <memory>
#include <type_traits>
#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 *value = nullptr) {
            if (value)
                core = new Core(value);
            else
                core = nullptr;
        }
        // copy constructor
        SmartPointer(const SmartPointer &test) {
            core = test.core;
            if (*this){
                core->count++;
            }
        }
        // move constructor
        SmartPointer(SmartPointer &&test) {
            core = test.core;
            test.core = nullptr;
        }
        // copy assigment
        SmartPointer &operator=(const SmartPointer &test) {
            core = test.core;
            if (*this) {
                core->count++;
            }
            return *this;
        }
        // move assigment
        SmartPointer &operator=(SmartPointer &&test) {
            core = test.core;
            test.core = nullptr;
            return *this;
        }
        //
        SmartPointer &operator=(value_type *test) {
            if (test){
                core = new Core(test);
            }
            else{
                core = nullptr;
            }
            return *this;
        }

        ~SmartPointer() {
            if (*this) {
                core->count--;
                if (core->count == 0)
                    delete core;
            }
        }

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

        value_type *get() const {
            if (*this)
                return core->ptr;
            return nullptr;
        }
        // if pointer == nullptr => return false
        operator bool() const {
            if(core == nullptr || core->ptr == nullptr) {
                return false;
            } else {
                return true;
            }
        }

        // if pointers points to the same address or both null => true
        template<typename U, typename AnotherAllocator>
        bool operator==(const SmartPointer<U, AnotherAllocator> &test) const {
            if (!*this && !test)
                return true;
            if (!std::is_same<T, U>::value)
                return false;
            return reinterpret_cast<void*>(get()) ==
                   reinterpret_cast<void*>(test.get());
        }

        // if pointers points to the same address or both null => false
        template<typename U, typename AnotherAllocator>
        bool operator!=(const SmartPointer<U, AnotherAllocator> &test) const {
            if (!*this && !test)
                return false;
            if (!std::is_same<T, U>::value)
                return true;
            return reinterpret_cast<void*>(get()) !=
                   reinterpret_cast<void*>(test.get());
        }

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

    private:
        class Core {
        public:
            explicit Core(value_type *value) {
                this->ptr = value;
                this->count = 1;
            }
            value_type *ptr;
            std::size_t count;
        };
        Core *core;
};
}