#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include "Test.hpp"

namespace smart_pointer {

// `exception` class definition
class exception : std::exception {
  using base_class = std::exception;
  using base_class::base_class;
};

template<
    typename T,
    typename Allocator
>
class SmartPointer {
  // don't remove this macro
  ENABLE_CLASS_TESTS;

 public:
  using value_type = T;

  explicit SmartPointer(value_type *core = nullptr) {
    this->core = core != nullptr ? new Core(core) : nullptr;
  }

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

  // move constructor
  SmartPointer(SmartPointer &&core) {
    this->core = core.core;
    core.core = nullptr;
  }

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

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

  //
  SmartPointer &operator=(value_type *core) {
    if (this->core != nullptr) {
      this->core->count_copy--;
      if (this->core->count_copy == 0) {
        delete this->core;
        this->core = nullptr;
      }
    }
    if (this->core == nullptr && core != nullptr)
      this->core = new Core(core);
    else if (this->core != nullptr && core == nullptr)
      this->core = nullptr;
    else {
    if (this->core != nullptr && core != nullptr) {
        this->core->TEMP = core;
        this->core->count_copy++;
    }
    }
    return *this;
  }

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

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

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

  value_type *get() const {
    return this->core == nullptr ? nullptr : this->core->TEMP;
  }

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

  // if pointers points to the same address or both null => true
  template<typename U, typename AnotherAllocator>
  bool operator==(const SmartPointer<U, AnotherAllocator> &core) const {
    auto obj_core = core.get();
    if ((obj_core == nullptr) &&
        (this->core == nullptr || this->core->TEMP == nullptr))
      return true;
    if (this->core == nullptr) return false;
    if (typeid(obj_core) != typeid(this->core->TEMP)) return false;
    return (reinterpret_cast<value_type *>(obj_core) == this->core->TEMP);
  }

  // if pointers points to the same address or both null => false
  template<typename U, typename AnotherAllocator>
  bool operator!=(const SmartPointer<U, AnotherAllocator> &core) const {
    auto obj_core = core.get();
    if ((obj_core == nullptr) &&
        (this->core == nullptr || this->core->TEMP == nullptr))
      return false;
    if (this->core == nullptr) return true;
    if (typeid(obj_core) != typeid(this->core->TEMP)) return true;
    return (reinterpret_cast<value_type *>(obj_core) != this->core->TEMP);
  }

  // if smart pointer contains non-nullptr => return count owners
  // if smart pointer contains nullptr => return 0
  std::size_t count_owners() const {
    if (this->core == nullptr || this->core->TEMP == nullptr) return 0;
    return this->core->count_copy;
  }

 private:
  class Core {
   public:
    value_type *TEMP;
    int count_copy;
    explicit Core(value_type *TEMP = nullptr) {
      this->TEMP = TEMP;
      this->count_copy = 1;
    }
    ~Core() {
      if (count_copy == 0)
        delete TEMP;
    }
  };
  Core *core;
};
};  // namespace smart_pointer