#pragma once

#include <utility>
#include <type_traits>

namespace frostbite2D {

template <typename T, typename RefPolicy>
class RefBasePtr : protected RefPolicy {
public:
  using value_type = T;
  using pointer_type = T*;
  using const_pointer_type = const T*;
  using reference_type = T&;
  using const_reference_type = const T&;

  RefBasePtr() noexcept : ptr_(nullptr) {}
  RefBasePtr(std::nullptr_t) noexcept : ptr_(nullptr) {}
  
  RefBasePtr(pointer_type p) : ptr_(p) {
    RefPolicy::Retain(ptr_);
  }
  
  RefBasePtr(const RefBasePtr& other) : ptr_(other.ptr_) {
    RefPolicy::Retain(ptr_);
  }
  
  RefBasePtr(RefBasePtr&& other) noexcept : ptr_(nullptr) {
    Swap(other);
  }
  
  ~RefBasePtr() {
    Tidy();
  }

  template <typename U, 
            typename std::enable_if<std::is_convertible<U*, T*>::value, int>::type = 0>
  RefBasePtr(const RefBasePtr<U, RefPolicy>& other) {
    ptr_ = dynamic_cast<pointer_type>(other.Get());
    RefPolicy::Retain(ptr_);
  }

  pointer_type Get() const noexcept { return ptr_; }
  
  pointer_type* GetAddressOfAndRelease() {
    Tidy();
    return &ptr_;
  }
  
  void Reset(pointer_type ptr = nullptr) {
    if (ptr) {
      RefBasePtr(ptr).Swap(*this);
    } else {
      Tidy();
    }
  }
  
  void Swap(RefBasePtr& other) noexcept {
    std::swap(ptr_, other.ptr_);
  }

  pointer_type operator->() { return ptr_; }
  const_pointer_type operator->() const { return ptr_; }
  reference_type operator*() { return *ptr_; }
  const_reference_type operator*() const { return *ptr_; }
  pointer_type* operator&() { return this->GetAddressOfAndRelease(); }
  
  operator bool() const noexcept { return ptr_ != nullptr; }
  bool operator!() const noexcept { return ptr_ == nullptr; }

  RefBasePtr& operator=(const RefBasePtr& other) {
    if (other.ptr_ != ptr_) {
      RefBasePtr(other).Swap(*this);
    }
    return *this;
  }
  
  RefBasePtr& operator=(RefBasePtr&& other) noexcept {
    if (other.ptr_ != ptr_) {
      other.Swap(*this);
    }
    return *this;
  }
  
  RefBasePtr& operator=(pointer_type p) {
    if (p != ptr_) {
      RefBasePtr(p).Swap(*this);
    }
    return *this;
  }

  template <typename U,
            typename std::enable_if<std::is_convertible<U*, T*>::value, int>::type = 0>
  RefBasePtr& operator=(const RefBasePtr<U, RefPolicy>& other) {
    if (other.Get() != ptr_) {
      RefBasePtr(dynamic_cast<pointer_type>(other.Get())).Swap(*this);
    }
    return *this;
  }
  
  RefBasePtr& operator=(std::nullptr_t) {
    Tidy();
    return *this;
  }

private:
  void Tidy() {
    RefPolicy::Release(ptr_);
    ptr_ = nullptr;
  }

  pointer_type ptr_;
};

template <class T, class U, class RefPolicy>
bool operator==(const RefBasePtr<T, RefPolicy>& lhs, 
                const RefBasePtr<U, RefPolicy>& rhs) noexcept {
  return lhs.Get() == rhs.Get();
}

template <class T, class RefPolicy>
bool operator==(const RefBasePtr<T, RefPolicy>& lhs, T* rhs) noexcept {
  return lhs.Get() == rhs;
}

template <class T, class RefPolicy>
bool operator==(T* lhs, const RefBasePtr<T, RefPolicy>& rhs) noexcept {
  return lhs == rhs.Get();
}

template <class T, class RefPolicy>
bool operator==(const RefBasePtr<T, RefPolicy>& lhs, std::nullptr_t) noexcept {
  return !static_cast<bool>(lhs);
}

template <class T, class RefPolicy>
bool operator==(std::nullptr_t, const RefBasePtr<T, RefPolicy>& rhs) noexcept {
  return !static_cast<bool>(rhs);
}

template <class T, class U, class RefPolicy>
bool operator!=(const RefBasePtr<T, RefPolicy>& lhs, 
                const RefBasePtr<U, RefPolicy>& rhs) noexcept {
  return !(lhs == rhs);
}

template <class T, class RefPolicy>
bool operator!=(const RefBasePtr<T, RefPolicy>& lhs, T* rhs) noexcept {
  return lhs.Get() != rhs;
}

template <class T, class RefPolicy>
bool operator!=(T* lhs, const RefBasePtr<T, RefPolicy>& rhs) noexcept {
  return lhs != rhs.Get();
}

template <class T, class RefPolicy>
bool operator!=(const RefBasePtr<T, RefPolicy>& lhs, std::nullptr_t) noexcept {
  return static_cast<bool>(lhs);
}

template <class T, class RefPolicy>
bool operator!=(std::nullptr_t, const RefBasePtr<T, RefPolicy>& rhs) noexcept {
  return static_cast<bool>(rhs);
}

template <class T, class U, class RefPolicy>
bool operator<(const RefBasePtr<T, RefPolicy>& lhs, 
               const RefBasePtr<U, RefPolicy>& rhs) noexcept {
  return lhs.Get() < rhs.Get();
}

template <class T, class RefPolicy>
void swap(RefBasePtr<T, RefPolicy>& lhs, RefBasePtr<T, RefPolicy>& rhs) noexcept {
  lhs.Swap(rhs);
}

}
