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

  explicit SmartPointer(value_type *ptr = nullptr) {
    if (ptr != nullptr) {
      core = new Core(ptr);
    } else {
      core = nullptr;
    }
  }

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

  // move constructor
  SmartPointer(SmartPointer &&other) {
    core = other.core;
    other.core = nullptr;
  }

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

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

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

  ~SmartPointer() {
    if (core != nullptr) {
      core->count_--;
      if (core->count_ == 0) {
        delete core;
      }
    }
  }

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

  // return pointer to the object of class/type T
  value_type *operator->() const {
    if (core != nullptr) {
      return core->ptr_;
    } else {
      return nullptr;
    }
  }

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

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

  // if pointers points to the same address or both null => true
  template<typename U, typename AnotherAllocator>
  friend
  class SmartPointer;

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

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

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

 private:
  class Core {
   public:
    explicit Core(T *ptr = 0) {
      ptr_ = ptr;
      if (ptr != 0) {
        count_ = 1;
      } else {
        count_ = 0;
      }
    }
    ~Core() {
      delete ptr_;
    }
    size_t count_;
    T *ptr_;
  };
  Core *core;
};
} // namespace smart_pointer