#pragma once

#include <boost/python/object/iterator.hpp>

#include <functional>
#include <iterator>

namespace bp = boost::python;

namespace maps {
namespace wiki {

template<typename PopFn, typename Target, typename Buffer>
class QueueIterator
{
public:
    QueueIterator(bp::back_reference<Target&> target, PopFn popFn)
        : target_(target),
          popFn_(popFn)
    { }

    typedef typename Buffer::value_type Value;

    Value next()
    {
        if (currentIt_ == std::end(buffer_)
                || currentIt_ == typename Buffer::const_iterator()) {
            buffer_ = popFn_(target_.get());
            currentIt_ = std::begin(buffer_);
        }

        if (buffer_.empty()) {
            bp::objects::stop_iteration_error();
        }

        const Value& ret = *currentIt_;
        ++currentIt_;
        return ret;
    }

private:
    bp::back_reference<Target&> target_;
    PopFn popFn_;
    Buffer buffer_;
    typename Buffer::const_iterator currentIt_;
};

namespace detail {

template <class Iterator, class NextPolicies>
bp::object demandIteratorClass(char const* name,
                               Iterator* = 0,
                               NextPolicies const& policies = NextPolicies())
{
    // Check the registry. If one is already registered, return it.
    bp::handle<> class_obj(
        bp::objects::registered_class_object(bp::type_id<Iterator>()));

    if (class_obj.get() != 0) {
        return bp::object(class_obj);
    }

    return bp::class_<Iterator>(name, bp::no_init)
        .def("__iter__", bp::objects::identity_function())
        .def("next", make_function(
             &Iterator::next,
             policies,
             boost::mpl::vector2<typename Iterator::Value, Iterator&>()));
}

template<typename PopFn,
         typename NextPolicies,
         typename Target,
         typename Buffer>
class InstantiateQueueIterator
{
public:
    InstantiateQueueIterator(PopFn popFn)
        : popFn_(std::move(popFn))
    { }

    QueueIterator<PopFn, Target, Buffer>
    operator()(bp::back_reference<Target&> x) const
    {
        demandIteratorClass("iterator",
                            (QueueIterator<PopFn, Target, Buffer>*)0,
                            NextPolicies());
        return QueueIterator<PopFn, Target, Buffer>(x, popFn_);
    }

private:
    PopFn popFn_;
};

} // detail

template<typename Target, typename Buffer,
         typename NextPolicies = bp::objects::default_iterator_call_policies>
bp::object makeQueueIterator(std::function<Buffer(Target&)> popFunction)
{
    using PopFn = std::function<Buffer(Target&)>;

    return make_function(
        detail::InstantiateQueueIterator<PopFn, NextPolicies, Target, Buffer>
            (std::move(popFunction)),
        bp::default_call_policies(),
        boost::mpl::vector2<QueueIterator<PopFn, Target, Buffer>,
                            bp::back_reference<Target&>>());
}

} // wiki
} // maps
