#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* x = nullptr) {
            if (x != nullptr) {
                core = new Core(x);
                core->c++;
            } else {
                core = nullptr;
            }
        }

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

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

        // copy assigment
        SmartPointer &operator=(const SmartPointer& other) {
            if (core != nullptr) {
                if (core->c > 1) {
                    core->c--;
                } else {
                    delete (core);
                }
            }
            core = other.core;
            if (core != nullptr) {
                other.core->c++;
            }
            return *this;
        }

        // move assigment
        SmartPointer &operator=(SmartPointer&& other) {
            core = other.core;
            delete other.core->p;
            other.core = nullptr;
            return *this;
        }

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

        ~SmartPointer() {
            if (core != nullptr) {
                core = nullptr;
            }
        }

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

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

        // 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
        explicit 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 {
            if (core != nullptr) {
                return (core->p == reinterpret_cast<value_type*>(other.get()));
            } else {
                return (other.get() == nullptr);
            }
        }

        // 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 !(*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;
            }
            return core->c;
        }

     private:
        class Core {
         public:
            explicit Core(value_type* x) : p(x) {}
            size_t c = 0;
            value_type* p;
        };
        Core* core;
    };
}