#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->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 = new Core;
            this->core = smart_p.core;
          } else {
            this->core = nullptr;
          }
        }

        // move constructor
        SmartPointer(SmartPointer &&smart_p) noexcept {
          if (smart_p.core) {
            this->core = smart_p.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;
          } else {
            this->core = nullptr;
          }
          return *this;
        }

        // move assigment
        SmartPointer &operator=(SmartPointer &&smart_p)  noexcept {
          if (smart_p.core) {
            this->core = new 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->sum_pointers = 1;
            this->core->pointer = pointer;
          } else {
            this->core = nullptr;
          }
          return *this;
        }

        ~SmartPointer() = default;

        // 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() {
            delete pointer;
          }
        };
        Core *core;
    };
}