#pragma once

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

namespace smart_pointer {
class exception : std::exception {
    using base_class = std::exception;
    using base_class::base_class;
};

template<
    typename T,
    typename Allocator
>

class SmartPointer {
ENABLE_CLASS_TESTS;

 public:
    using value_type = T;

    explicit SmartPointer(value_type* p = nullptr) {
        if (p == nullptr) {
            core = nullptr;
        } else {
            core = new Core(p);
        }
    }

    SmartPointer(const SmartPointer& other) : core(other.core) {
        if (other.core != nullptr) core->count++;
    }

    SmartPointer(SmartPointer&& other) : core(other.core) {
        other = nullptr;
    }

    SmartPointer& operator=(const SmartPointer& other) {
        if (&other == this)
            return *this;

        if (core != nullptr) {
            core->count -= 1;
            if (core->count == 0) {
                delete core;
            }
        }
        core = other.core;
        if (other.core != nullptr) {
            core->count++;
        }

        return *this;
    }

    SmartPointer& operator=(SmartPointer&& other) {
        if (&other == this)
            return *this;

        if (core != nullptr) {
            core->count -= 1;
            if (core->count == 0) {
                delete core;
            }
        }

        core = other.core;
        other.core = nullptr;

        return *this;
    }

    SmartPointer& operator=(value_type* p) {
        if (core != nullptr) {
            core->count -= 1;
            if (core->count == 0) {
                delete core;
            }
        }

        if (p != nullptr) {
            core = new Core(p);
        } else {
            core = nullptr;
        }
        return *this;
    }

    ~SmartPointer() {
    }

    value_type& operator*() {
        if (core != nullptr)
            return *(core->p);
        throw smart_pointer::exception();
    }
    const value_type& operator*() const {
        if (core != nullptr)
            return *(core->p);
        throw smart_pointer::exception();
    }

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

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

    operator bool() const {
        return core != nullptr;
    }

    template<typename U, typename AnotherAllocator>
    bool operator==(const SmartPointer<U,
        AnotherAllocator>& other) const {
        return (reinterpret_cast<void*>(this->get()) ==
            reinterpret_cast<void*>(other.get()));
    }

    template<typename U, typename AnotherAllocator>
    bool operator!=(const SmartPointer<U,
        AnotherAllocator>& other) const {
        return !(reinterpret_cast<void*>(this->get()) ==
            reinterpret_cast<void*>(other.get()));
    }

    std::size_t count_owners() const {
        if (core == nullptr) return 0;
        return core->count;
    }

 private:
    class Core {
     public:
        value_type* p;
        std::size_t count;

        explicit Core(value_type* ptr) : p(ptr), count(1) {}

        ~Core() {
            Allocator a;
            a.deallocate(p, sizeof(value_type));
            p = nullptr;
        }

        value_type get() {
            return *p;
        }
    };
    Core* core;
};
}  // namespace smart_pointer