#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 *pointer = nullptr) {
    if (pointer) {
      // this->core = new Core;
      this->core = std::make_shared<Core>();
      // this->core->scratch = this->core;
      this->core->pointer = pointer;
      this->core->sum_pointers = 1;
    } else {
      this->core = nullptr;
    }
  }
  // copy constructor
  SmartPointer(const SmartPointer &smart_p) {
    if (smart_p.core) {
      ++(smart_p.core->sum_pointers);
      this->core = smart_p.core;
      // this->core->scratch = this->core;
    } else {
      this->core = nullptr;
    }
  }

  // move constructor
  SmartPointer(SmartPointer &&smart_p) noexcept {
    if (smart_p.core) {
      this->core = smart_p.core;
      // this->core->scratch = this->core;
      smart_p.core = nullptr;
    } else {
      this->core = nullptr;
    }
  }

  // copy assigment
  SmartPointer &operator=(const SmartPointer &smart_p) {
    if (smart_p.core) {
      ++(smart_p.core->sum_pointers);
      this->core = smart_p.core;
      // this->core->scratch = this->core;
    } else {
      this->core = nullptr;
    }
    return *this;
  }

  // move assigment
  SmartPointer &operator=(SmartPointer &&smart_p)  noexcept {
    if (smart_p.core) {
      // this->core = new Core;
      this->core = std::make_shared<Core>();
      // this->core->scratch = this->core;
      this->core->pointer = smart_p.core->pointer;
      this->core->sum_pointers = smart_p.core->sum_pointers;
    } else {
      this->core = nullptr;
    }
    smart_p.core = nullptr;
    return *this;
  }

  SmartPointer &operator=(value_type *pointer) {
    if (pointer) {
      // this->core = new Core;
      this->core = std::make_shared<Core>();
      // this->core->scratch = this->core;
      this->core->sum_pointers = 1;
      this->core->pointer = pointer;
    } else {
      this->core = nullptr;
    }
    return *this;
  }

  ~SmartPointer() {
    if (core) {
      core->sum_pointers--;
      // delete core;
    }
  }

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

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

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

  // if pointer == nullptr => return false
  explicit operator bool() const {
    return this->core != nullptr;
  }

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

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

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

 private:
  class Core {
   public:
    value_type *pointer;
    size_t sum_pointers;
    //Core *scratch;
    ~Core() {
      //delete pointer;
      //delete scratch;
    }
  };
  std::shared_ptr<Core> core;
  // Core *core;
};
}  // namespace smart_pointer