#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) {
             this->core = nullptr;
         } else {
             this->core = new Core();
             this->core->ptr = ptr;
             this->core->count = 1;
         }
     }

     ~SmartPointer() {
         if (this->core != nullptr && this->core->count == 1) {
             delete this->core;
         }
     }

     SmartPointer(const SmartPointer& obj) {
         this->core = obj.core;

         if (obj.core != nullptr) {
             this->core->count++;
         }
     }

     SmartPointer(SmartPointer&& dyingObj) {
         this->core = dyingObj.core;
         dyingObj.core = nullptr;
     }

     SmartPointer& operator=(const SmartPointer& other) {
         if (this->core != nullptr && this->core->count == 1) {
             delete this->core;
         }

         this->core = other.core;

         if (other.core != nullptr) {
             this->core->count++;
         }

         return *this;
     }

     SmartPointer& operator=(SmartPointer&& dyingObj) {
         if (this->core != nullptr) {
             delete this->core;
         }

         this->core = dyingObj.core;
         dyingObj.core = nullptr;
         return *this;
     }

     SmartPointer& operator=(value_type* other) {
         if (this->core != nullptr && this->core->count == 1) {
             delete this->core;
         }

         if (other != nullptr) {
             this->core = new Core();
             this->core->ptr = other;
             this->core->count = 1;
         } else {
             this->core = nullptr;
         }

         return *this;
     }

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

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

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

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

     operator bool() const {
         return this->core != nullptr;
     }

     template<typename U, typename AnotherAllocator>
     bool operator==(const SmartPointer<U, AnotherAllocator>& ptr) const {
         return static_cast<void*>(this->get()) == static_cast<void*>
             (ptr.get());
     }

     template<typename U, typename AnotherAllocator>
     bool operator!=(const SmartPointer<U, AnotherAllocator>& ptr) const {
         return static_cast<void*>(this->get()) != static_cast<void*>
             (ptr.get());
     }

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

 private:
     class Core {
      public:
          size_t count;
          value_type* ptr;

          ~Core() {
              Allocator del;
              del.deallocate(ptr, sizeof(value_type));
              this->ptr = nullptr;
          }
     };

     Core* core;
};
}  // namespace smart_pointer