#pragma once

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

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

    template <typename T, typename Allocator>
    class SmartPointer {
        ENABLE_CLASS_TESTS;

     public:
        using value_type = T;

        explicit SmartPointer(value_type* ptr_ = nullptr) {
            if (ptr_ != nullptr) {
                core = new Core(ptr_);
                core->owners++;
            } else
                core = nullptr;
        }

        SmartPointer(const SmartPointer& other) {
            core = other.core;
            if (core != nullptr) core->owners++;
        }

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

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

        SmartPointer& operator=(SmartPointer&& other) {
            other.core = nullptr;
            return *this;
        }

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

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

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

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

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

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

        operator bool() const {
          if (core == nullptr) return false;
          return true;
        }

        template <typename U, typename AnotherAllocator>
        bool operator==(const SmartPointer<U, AnotherAllocator>& other) const {
            if (core != nullptr)
              return (core->ptr == reinterpret_cast<value_type*>(other.get()));
            else
              return (other.get() == nullptr);
        }

        template <typename U, typename AnotherAllocator>
        bool operator!=(const SmartPointer<U, AnotherAllocator>& other) const {
            if (*this == other) return false;
            return true;
        }

        std::size_t count_owners() const {
            if (core != nullptr) return core->owners;
            return 0;
        }

     private:
        class Core {
         public:
            explicit Core(value_type* ptr_) : ptr(ptr_) {}
            size_t owners = 0;
            value_type* ptr;
        };
        Core* core;
    };
}  // namespace smart_pointer