struct proc_entry {
    VALUE proc;
    VALUE type;
};

enum proc_entry_type {
    T_PROC_MAP = 0,
    T_PROC_SELECT = 1
};

static VALUE
process_element(struct enumerator *e, VALUE result)
{
    struct proc_entry *entry;
    VALUE move_next = Qtrue;
    VALUE *procs = RARRAY_PTR(e->procs);
    long procs_number = RARRAY_LENINT(e->procs);
    long i = 0;

    for (i = 0; i < procs_number; i++) {
        Data_Get_Struct(procs[i], struct proc_entry, entry);
        switch ((enum proc_entry_type) entry->type) {
            case T_PROC_MAP:
                result = rb_funcall(entry->proc, rb_intern("call"), 1, result);
                break;
            case T_PROC_SELECT:
                if (RTEST(move_next)) {
                    move_next = rb_funcall(entry->proc, rb_intern("call"), 1, result);
                }
                break;
        }
    }

    if (RTEST(move_next)) {
        return result;
    } else {
        return Qundef;
    }
}

static VALUE
lazy_iterator_block(VALUE val, VALUE m, int argc, VALUE *argv)
{
    VALUE result;
    VALUE yielder = RARRAY_PTR(m)[0];

    result = process_element(enumerator_ptr(RARRAY_PTR(m)[1]), val);
    if (result != Qundef) {
        return yielder_yield(yielder, rb_ary_new3(1, result));
    } else {
        return Qnil;
    }
}

static VALUE
lazy_generator_block(VALUE val, VALUE m, int argc, VALUE *argv)
{
    VALUE obj = RARRAY_PTR(m)[0];
    VALUE enumerator = RARRAY_PTR(m)[1];

    return rb_block_call(obj, id_each, 0, Qnil,
            lazy_iterator_block, rb_ary_new3(2, val, enumerator));
}

static VALUE
lazy_initialize(int argc, VALUE *argv, VALUE self)
{
    VALUE generator;
    VALUE obj;
    struct enumerator *ptr;

    obj = argv[0];
    generator = generator_allocate(rb_cGenerator);
    rb_block_call(generator, rb_intern("initialize"), 0, Qnil,
            lazy_generator_block, rb_ary_new3(2, obj, self));
    enumerator_init(self, generator, sym_each, 0, Qnil);
    ptr = enumerator_ptr(self);
    ptr->lazy = Qtrue;
    ptr->procs = rb_ary_new();

    return self;
}

static VALUE
create_proc_entry(enum proc_entry_type proc_type)
{
    struct proc_entry *entry;
    VALUE entry_obj;

    entry_obj = Data_Make_Struct(rb_cObject, struct proc_entry, 0, RUBY_DEFAULT_FREE, entry);
    Data_Get_Struct(entry_obj, struct proc_entry, entry);
    entry->proc = rb_block_proc();
    entry->type = proc_type;

    return entry_obj;
}

static VALUE
lazy_map(VALUE obj)
{
    struct enumerator *e = enumerator_ptr(obj);

    rb_ary_push(e->procs, create_proc_entry(T_PROC_MAP));

    return obj;
}

static VALUE
lazy_select(VALUE obj)
{
    struct enumerator *e = enumerator_ptr(obj);

    rb_ary_push(e->procs, create_proc_entry(T_PROC_SELECT));

    return obj;
}

static VALUE
enumerable_lazy(VALUE self)
{
    return rb_class_new_instance(1, &self, rb_cLazy);
}
