#pragma once
 
#include <memory>
#include <type_traits>
#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 *value = nullptr) {
    if (value )
      core = new Core(value);
    else
      core = nullptr;
  }
 
  SmartPointer(const SmartPointer &A) {
    core = A.core;
    if (*this)
      core->Increment();
  }
 
  SmartPointer(SmartPointer &&A) {
    core = A.core;
    A.core = nullptr;
  }
 
  SmartPointer &operator=(const SmartPointer &A) {
    this->~SmartPointer();
    core = A.core;
    if (*this)
      core->Increment();
    return *this;
  }
 
  SmartPointer &operator=(SmartPointer &&A) {
    this->~SmartPointer();
    core = A.core;
    A.core = nullptr;
    return *this;
  }
 
  SmartPointer &operator=(value_type *A) {
    this->~SmartPointer();
    if (A)
      core = new Core(A);
    else
      core = nullptr;
    return *this;
  }
 
  ~SmartPointer() {
    if (*this) {
      core->Decrement();
      if (core->CountOwners() == 0)
        delete core;
    }
  }
 
  value_type &operator*() {
    if (!*this)
      throw smart_pointer::exception();
    return *(core->Value());
  }
 
  const value_type &operator*() const {
    if (!*this)
      throw smart_pointer::exception();
    return *(core->Value());
  }
 
  value_type *operator->() const {
    if (*this)
      return core->Value();
    return nullptr;
  }
 
  value_type *get() const {
    if (*this)
      return core->Value();
    return nullptr;
  }
 
  operator bool() const {
    return !(core == nullptr || core->Value() == nullptr);
  }
 
  template<typename U, typename AnotherAllocator>
  bool operator==(const SmartPointer<U, AnotherAllocator> &A) const {
    if (!*this && !A)
      return true;
    if (!std::is_same<T, U>::value)
      return false;
    return reinterpret_cast<void*>(get()) ==
        reinterpret_cast<void*>(A.get());
  }
 
  template<typename U, typename AnotherAllocator>
  bool operator!=(const SmartPointer<U, AnotherAllocator> &A) const {
    if (!*this && !A)
      return false;
    if (!std::is_same<T, U>::value)
      return true;
    return reinterpret_cast<void*>(get()) !=
        reinterpret_cast<void*>(A.get());
  }
 
  std::size_t count_owners() const {
    if (*this)
      return core->CountOwners();
    return 0;
  }
 
 private:
  class Core {
   public:
    explicit Core(value_type *value) : value_(value), count_owners_(0) {
      Increment();
    }
 
    ~Core() {
      delete value_;
      value_ = nullptr;
    }
 
    value_type *Value() {
      return value_;
    }
 
    value_type *Value() const {
      return value_;
    }
 
    void Increment() {
      count_owners_++;
    }
 
    void Decrement() {
      count_owners_--;
    }
 
    std::size_t CountOwners() const {
      return count_owners_;
    }
 
   private:
    value_type *value_;
    std::size_t count_owners_;
  };
  Core *core;
};
}  // namespace smart_pointer