#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;

    // constructor
    explicit SmartPointer(value_type* p = nullptr) : core(new Core(p)) {
        //  
        core->AddRef();
    }

    // copy constructor
    SmartPointer(const SmartPointer& optr) : core(optr.core) { core->AddRef(); }

    // move constructor
    SmartPointer(SmartPointer&&) = default;

    // copy assigment
    SmartPointer& operator=(const SmartPointer&) = default;

    // move assigment
    SmartPointer& operator=(SmartPointer&& sp) {
        if (this != &sp) {
            if (core->Release() == 0) {
                delete core;
            }

            core = sp.core;
            core->AddRef();
        }
        return *this;
    }

    //
    SmartPointer& operator=(value_type* other) {
        core->p = other;
        core->AddRef();
        return *this;
    }

    ~SmartPointer() {
        if (core->Release() == 0) delete core;
    }

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

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

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

    value_type* get() const { return core->p; }

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

    // if pointers points to the same address or both null => true
    template <typename U, typename AnotherAllocator>
    bool operator==(const SmartPointer<U, AnotherAllocator>& pp) const {
        return static_cast<void*>(core->p) == static_cast<void*>(pp.core->p);
    }

    // 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 static_cast<void*>(other.core->p) != static_cast<void*>(core->p);
    }

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

 public:
    class Core {
     public:
        value_type* p;
        size_t c;

        explicit Core(value_type* _p = nullptr) : p(_p), c(0) {}

        void AddRef() { ++c; }

        int Release() { return --c; }

        ~Core() {
            if (p != nullptr) delete p;
        }
    };
    Core* core;
};
}  // namespace smart_pointer