#pragma once
#include <utility>
#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* xPtr = nullptr) :
                core((xPtr == nullptr ? nullptr : new Core(xPtr))) {
            }

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

            // move constructor
            SmartPointer(SmartPointer&& other) :
                core(std::exchange(other.core, nullptr)) { }

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

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

            //
            SmartPointer& operator=(value_type* p) {
                this->~SmartPointer();
                if (p == nullptr) {
                    this->core = nullptr;
                } else {
                    this->core = new Core(p);
                }
                return *this;
            }

            ~SmartPointer() {
                if (this->core != nullptr) {
                    if (this->core->owners <= 1) {
                        delete this->core;
                    } else {
                        this->core->owners--;
                    }
                }
            }

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

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

            value_type* get() const {
                if (this->core == nullptr) {
                    return nullptr;
                } else {
                    return this->core->valPtr;
                }
            }

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

            decltype(auto) getCore() const {
                return static_cast<void*>(this->core);
            }

            // 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 this->getCore() == other.getCore();
            }

            // 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 !(SmartPointer::operator==(other));
            }

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

 private:
            class Core {
 public:
                explicit Core(value_type* x) :
                    valPtr(x),
                    owners(1) { }
                ~Core() {
                    if (valPtr != nullptr) delete valPtr;
                }

                value_type* valPtr;
                std::size_t owners;
            };
            Core* core;
    };
};