#pragma once
#include <memory>
#include <typeinfo>
#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) {
            this->core = new Core(ptr);
        } else {
            core = nullptr;
        }
    }

    // copy constructor
    SmartPointer(const SmartPointer& otherPointer) {
        // changeOwners(-1);
        this->core = otherPointer.core;
        changeOwners(1);
    }

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

    // copy assigment
    SmartPointer& operator=(const SmartPointer& otherPointer) {
        changeOwners(-1);
        this->core = otherPointer.core;
        changeOwners(1);
        return *this;
    }

    // move assigment
    SmartPointer& operator=(SmartPointer&& oldPointer) {
        changeOwners(-1);
        this->core = oldPointer.core;
        oldPointer.core = nullptr;
        return *this;
    }
    //
    SmartPointer& operator=(value_type* ptr) {
        if (ptr != nullptr) {
            changeOwners(-1);
            this->core = new Core(ptr);
        } else {
            this->core = nullptr;
        }
        return *this;
    }
    ~SmartPointer() {
        if (this->core != nullptr) {
            if (this->core->count_owners == 1) {
                delete core;
            } else {
                this->core->count_owners -= 1;
            }
        } else {
            delete core;
        }
    }

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

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

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

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

    // if pointers points to the same address or both null => true
    template<typename U, typename AnotherAllocator>
    bool operator==(const SmartPointer<U,
        AnotherAllocator>& otherPointer) const {
        return (int64_t)(this->core) ==
            (int64_t)(otherPointer.core);
        return false;
    }

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

    // 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) return 0;
        return this->core->count_owners;
    }
    class Core {
     public:
        T* ptr;
        int64_t count_owners;
        explicit Core(T* ptr) {
            this->ptr = ptr;
            if (ptr != nullptr) {
                count_owners = 1;
            } else {
                count_owners = 0;
            }
        }
        bool operator==(const Core& other_core) {
            if (typeid(ptr) != typeid(other_core.ptr)) return false;
            return this->ptr == other_core.ptr;
        }
        bool operator!=(const Core& other_core) {
            if (typeid(ptr) == typeid(other_core.ptr)) {
                return this->ptr != other_core.ptr;
            }

            return true;
        }
    };
    Core* core;

 private:
        void changeOwners(int value = 1) {
            if (this->core != nullptr) {
                if (this->core->ptr != nullptr) {
                    this->core->count_owners += value;
                }
            }
        }
};
}  //  namespace smart_pointer