static struct pe_watcher_vtbl pe_io_vtbl;

static pe_ring IOWatch;
static int IOWatchCount;
static int IOWatch_OK;

static void pe_sys_io_add (pe_io *ev);
static void pe_sys_io_del (pe_io *ev);

static pe_watcher *pe_io_allocate(HV *stash, SV *temple) {
    pe_io *ev;
    EvNew(4, ev, 1, pe_io);
    ev->base.vtbl = &pe_io_vtbl;
    pe_watcher_init(&ev->base, stash, temple);
    PE_RING_INIT(&ev->tm.ring, ev);
    PE_RING_INIT(&ev->ioring, ev);
    ev->fd = -1;
    ev->timeout = 0;
    ev->handle = &PL_sv_undef;
    ev->poll = PE_R;
    ev->tm_callback = 0;
    ev->tm_ext_data = 0;
    WaINVOKE1_off(ev);
    WaREPEAT_on(ev);
    return (pe_watcher*) ev;
}

static void pe_io_dtor(pe_watcher *_ev) {
    pe_io *ev = (pe_io*) _ev;
    if (WaTMPERLCB(ev))
	SvREFCNT_dec(ev->tm_callback);
    PE_RING_DETACH(&ev->ioring);
    SvREFCNT_dec(ev->handle);
    pe_watcher_dtor(_ev);
    EvFree(4, _ev);
}

static char *pe_io_start(pe_watcher *_ev, int repeat) {
    STRLEN n_a;
    int ok=0;
    pe_io *ev = (pe_io*) _ev;
    if (SvOK(ev->handle))
	ev->fd = pe_sys_fileno(ev->handle, SvPV(ev->base.desc, n_a));

    /* On Unix, it is possible to set the 'fd' in C code without
       assigning anything to the 'handle'.  This should be more
       officially supported but maybe it is too unix specific. */

    if (ev->fd >= 0 && (ev->poll & ~PE_T)) {
	if (!ev->base.callback)
	    return "without io callback";
	PE_RING_UNSHIFT(&ev->ioring, &IOWatch);
	pe_sys_io_add(ev);
	++IOWatchCount;
	IOWatch_OK = 0;
	++ok;
    }
    if (ev->timeout) {
	if (!ev->base.callback && !ev->tm_callback) {
	    assert(!ok);
	    return "without timeout callback";
	}
	ev->poll |= PE_T;
	ev->tm.at = NVtime() + ev->timeout;  /* too early okay */
	pe_timeable_start(&ev->tm);
	++ok;
    } else {
	ev->poll &= ~PE_T;
    }
    return ok? 0 : "because there is nothing to watch";
}

static void pe_io_stop(pe_watcher *_ev) {
    pe_io *ev = (pe_io*) _ev;
    pe_timeable_stop(&ev->tm);
    if (!PE_RING_EMPTY(&ev->ioring)) {
        pe_sys_io_del(ev);
	PE_RING_DETACH(&ev->ioring);
	--IOWatchCount;
	IOWatch_OK = 0;
    }
}

static void pe_io_alarm(pe_watcher *_wa, pe_timeable *hit) {
    pe_io *wa = (pe_io*) _wa;
    NV now = NVtime();
    NV left = (_wa->cbtime + wa->timeout) - now;
    if (left < IntervalEpsilon) {
	pe_ioevent *ev;
	if (WaREPEAT(wa)) {
	    wa->tm.at = now + wa->timeout;
	    pe_timeable_start(&wa->tm);
	} else {
	  wa->timeout = 0; /*RESET*/
	}
	ev = (pe_ioevent*) (*_wa->vtbl->new_event)(_wa);
	++ev->base.hits;
	ev->got |= PE_T;
	if (wa->tm_callback) {
	    if (WaTMPERLCB(wa)) {
		pe_anyevent_set_perl_cb(&ev->base, wa->tm_callback);
	    } else {
		pe_anyevent_set_cb(&ev->base, wa->tm_callback, wa->tm_ext_data);
	    }
	}
	queueEvent((pe_event*) ev);
    }
    else {
	/* ++TimeoutTooEarly;
	   This branch is normal behavior and does not indicate
	   poor clock accuracy. */
	wa->tm.at = now + left;
	pe_timeable_start(&wa->tm);
    }
}

static void _io_restart(pe_watcher *ev) {
    if (!WaPOLLING(ev)) return;
    pe_watcher_off(ev);
    pe_watcher_on(ev, 0);
}

static void pe_io_reset_handle(pe_watcher *ev) {  /* used by unix_io */
    pe_io *io = (pe_io*)ev;
    SvREFCNT_dec(io->handle);
    io->handle = &PL_sv_undef;
    io->fd = -1;
    _io_restart(ev);
}

WKEYMETH(_io_poll) {
    pe_io *io = (pe_io*)ev;
    if (nval) {
	int nev = sv_2events_mask(nval, PE_R|PE_W|PE_E|PE_T);
	if (io->timeout) nev |=  PE_T;
	else             nev &= ~PE_T;
	if (io->poll != nev) {
	    io->poll = nev;
	    _io_restart(ev);
	}
    }
    {
	dSP;
	XPUSHs(sv_2mortal(events_mask_2sv(io->poll)));
	PUTBACK;
    }
}

WKEYMETH(_io_handle) {
    pe_io *io = (pe_io*)ev;
    if (nval) {
	SV *old = io->handle;
	io->handle = SvREFCNT_inc(nval);
	SvREFCNT_dec(old);
	io->fd = -1;
	_io_restart(ev);
    }
    {
	dSP;
	XPUSHs(io->handle);
	PUTBACK;
    }
}

WKEYMETH(_io_timeout) {
    pe_io *io = (pe_io*)ev;
    if (nval) {
	io->timeout = SvOK(nval)? SvNV(nval) : 0;  /*undef is ok*/
	_io_restart(ev);
    }
    {
	dSP;
	XPUSHs(sv_2mortal(newSVnv(io->timeout)));
	PUTBACK;
    }
}

WKEYMETH(_io_timeout_cb) {
    pe_io *io = (pe_io*)ev;
    if (nval) {
	AV *av;
	SV *sv;
	SV *old=0;
	if (WaTMPERLCB(ev))
	    old = (SV*) io->tm_callback;
	if (!SvOK(nval)) {
	    WaTMPERLCB_off(ev);
	    io->tm_callback = 0;
	    io->tm_ext_data = 0;
	} else if (SvROK(nval) && (SvTYPE(sv=SvRV(nval)) == SVt_PVCV)) {
	    WaTMPERLCB_on(ev);
	    io->tm_callback = SvREFCNT_inc(nval);
	} else if (SvROK(nval) &&
		   (SvTYPE(av=(AV*)SvRV(nval)) == SVt_PVAV) &&
		   av_len(av) == 1 &&
		   !SvROK(sv=*av_fetch(av, 1, 0))) {
	    WaTMPERLCB_on(ev);
	    io->tm_callback = SvREFCNT_inc(nval);
	} else {
	    if (SvIV(DebugLevel) >= 2)
		sv_dump(sv);
	    croak("Callback must be a code ref or [$object, $method_name]");
	}
	if (old)
	    SvREFCNT_dec(old);
    }
    {
	SV *ret = (WaTMPERLCB(ev)?
		   (SV*) io->tm_callback :
		   (io->tm_callback?
		    sv_2mortal(newSVpvf("<FPTR=0x%x EXT=0x%x>",
					io->tm_callback, io->tm_ext_data)) :
		    &PL_sv_undef));
	dSP;
	XPUSHs(ret);
	PUTBACK;
    }
}

static void boot_io() {
    pe_watcher_vtbl *vt = &pe_io_vtbl;
    memcpy(vt, &pe_watcher_base_vtbl, sizeof(pe_watcher_base_vtbl));
    vt->dtor = pe_io_dtor;
    vt->start = pe_io_start;
    vt->stop = pe_io_stop;
    vt->alarm = pe_io_alarm;
    PE_RING_INIT(&IOWatch, 0);
    IOWatch_OK = 0;
    IOWatchCount = 0;
    pe_register_vtbl(vt, gv_stashpv("Event::io",1), &ioevent_vtbl);
}
