#pragma once

#include <memory>
#include "Test.h"

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;

 private:
  class Core;
  Core *core = nullptr;
  using zero = std::size_t;
 public:
  using value_type = T;

  explicit SmartPointer(value_type *p = nullptr) {
    if (!p) {
      core = nullptr;
    } else {
      core = new Core;
      core->refCount = {1};
      core->ptr_ = p;
    }
  }

  // copy constructor
  SmartPointer(const SmartPointer &p) {
    if (core != nullptr) {
      if (core->refCount > 1) {
        core->refCount -= 1;
      } else {
        delete core->ptr_;
        delete core;
        core = new Core;
      }
    }
    core = p.core;

    if (core != nullptr)
      core->refCount++;

  }

  // move constructor
  SmartPointer(SmartPointer &&p) {
    if (core != nullptr) {
      if (core->refCount > 1) {
        core->refCount -= 1;
      } else {
        delete core->ptr_;
        delete core;
        core = new Core;
      }
    }
    core = p.core;
    if (p.core != nullptr)
      p.core = nullptr;

  }

  // copy assigment
  SmartPointer &operator=(const SmartPointer &p) {
    if (core != nullptr) {
      if (core->refCount > 1) {
        core->refCount--;
      } else {
        delete this->core->ptr_;
        delete this->core;
        core = nullptr;
      }
    }
    core = p.core;
    if (core != nullptr)
      core->refCount += 1;
    return *this;
  }

  // move assigment
  SmartPointer &operator=(SmartPointer &&p) {
    if (core != nullptr) {
      if (core->refCount > 1) {
        core->refCount--;
      } else {
        delete core->ptr_;
        delete core;
      }
    }
    core = p.core;

    (core ? p.core = nullptr : nullptr);
    return *this;
  }

  //
  SmartPointer &operator=(value_type *p) {
    if (core != nullptr) {
      if (core->refCount > 1) {
        core->refCount--;
      } else {
        delete core->ptr_;
        delete core;
        core = new Core;
      }
    } else {
      core = new Core;
    }
    if (p != nullptr) {
      core->ptr_ = p;
    } else {
      delete core;
      core = {nullptr};
    }
    if (core)
      core->refCount = {1};

    return *this;
  }

  ~SmartPointer() {
    if (!core) {
      delete core;
    } else if (core->refCount > 1) {
      core->refCount -= 1;
    } else {
      if (core->ptr_ != nullptr)
        delete core->ptr_;

      delete core;
    }
  }

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

  const value_type &operator*() const {
    if (get() == nullptr) {
      throw exception();
    } else {
      return *core->ptr_;
    }
  }

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

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

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

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

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

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

 private:
  class Core {
   public:
    value_type *ptr_;
    zero refCount{0};
  };
};
}