#ifndef TILE_BASE_MAYBE_OWNING_H
#define TILE_BASE_MAYBE_OWNING_H

#pragma once

#include "tile/base/internal/logging.h"
#include "tile/base/internal/macro.h"
#include "tile/base/internal/meta.h"
// #include "gtest/gtest_prod.h"
#include <memory>
#include <utility>

namespace tile {
constexpr struct owning_t {
    explicit owning_t() = default;
} owning;

constexpr struct non_owning_t {
    explicit non_owning_t() = default;
} non_owning;

template<typename T>
class MaybeOwning {
public:
    template<typename U>
    struct is_convertible_from {
        static constexpr bool value = std::is_convertible<U *, T *>::value
            && (std::is_same<typename std::remove_cv<T>::type, typename std::remove_cv<U>::type>::value
                || std::has_virtual_destructor<T>::value);
    };

    constexpr MaybeOwning() noexcept : owning_(false), ptr_(nullptr) {}

    MaybeOwning(std::nullptr_t) noexcept : MaybeOwning() {}

    constexpr MaybeOwning(owning_t, T *ptr) noexcept : MaybeOwning(ptr, true) {}

    constexpr MaybeOwning(non_owning_t, T *ptr) noexcept : MaybeOwning(ptr, false) {}

    constexpr MaybeOwning(T *ptr, bool owning) noexcept : owning_(owning), ptr_(ptr) {}

    template<typename U, typename = enable_if_t<is_convertible_from<U>::value>>
    constexpr MaybeOwning(std::unique_ptr<U> ptr) noexcept
        : owning_(!!ptr),
          ptr_(ptr.release())
    {}

    template<typename U, typename = enable_if_t<is_convertible_from<U>::value>>
    MaybeOwning(MaybeOwning<U> &&ptr) noexcept
        : owning_(ptr.owning_),
          ptr_(ptr.ptr_)
    {
        ptr.owning_ = false;
        ptr.ptr_    = nullptr;
    }

    MaybeOwning(MaybeOwning &&other) noexcept : owning_(other.owning_), ptr_(other.ptr_)
    {
        other.owning_ = false;
        other.ptr_    = nullptr;
    }

    MaybeOwning &operator=(MaybeOwning &&other) noexcept
    {
        if (&other != this) {
            Reset();
            std::swap(owning_, other.owning_);
            std::swap(ptr_, other.ptr_);
        }
        return *this;
    }

    MaybeOwning(const MaybeOwning &)            = delete;
    MaybeOwning &operator=(const MaybeOwning &) = delete;

    MaybeOwning &operator=(std::nullptr_t) noexcept
    {
        Reset();
        return *this;
    }

    ~MaybeOwning()
    {
        if (owning_) { delete ptr_; }
    }

    constexpr T *Get() const noexcept { return ptr_; }

    constexpr T *operator->() const noexcept { return Get(); }

    constexpr T &operator*() const noexcept { return Get(); }

    constexpr explicit operator bool() const noexcept { return !!ptr_; }

    constexpr bool Owning() const noexcept { return owning_; }

    TILE_NODISCARD T *Leak() noexcept
    {
        TILE_CHECK(owning_, "Calling `Leak()` on non-owning `MaybeOwning<T>` is undefined");
        owning_ = false;
        T *rc   = nullptr;
        std::swap(rc, ptr_);
        return rc;
    }

    void Reset(owning_t, T *ptr) noexcept { Reset(ptr, true); }

    void Reset(non_owning_t, T *ptr) noexcept { Reset(ptr, false); }

private:
    // for gtest
    // FRIEND_TEST(MaybeOwning, Reset);
    friend class MaybeOwning_Reset_Test;

    void Reset() noexcept { Reset(nullptr, false); }

    void Reset(T *ptr, bool owning) noexcept
    {
        if (owning_) {
            TILE_CHECK(ptr_);
            delete ptr_;
        }

        // don't allow owning a `nullptr`
        TILE_CHECK(!owning || ptr_,
                   "Pasing a `nullptr` to `ptr` while specifying `owning` as `true` does "
                   "not make much sense, I think");

        owning_ = owning;
        ptr_    = ptr;
    }

private:
    template<typename U>
    friend class MaybeOwning;

    bool owning_;
    T *ptr_;
};

template<typename T>
class MaybeOwning<T[]>;// Not implemented

template<typename T>
class MaybeOwningArgument {
public:
    template<typename U, typename = decltype(MaybeOwning<T>(std::declval<U &&>()))>
    MaybeOwningArgument(U &&ptr) : ptr_(std::forward<U>(ptr))
    {}

    template<typename U, typename = enable_if_t<MaybeOwning<T>::template is_convertible_from<U>::value>>
    MaybeOwningArgument(U *ptr) : ptr_(non_owning, ptr)
    {}

    operator MaybeOwning<T>() && noexcept { return std::move(ptr_); }

private:
    MaybeOwning<T> ptr_;
};
}// namespace tile

#endif// TILE_BASE_MAYBE_OWNING_H
