#define PERL_NO_GET_CONTEXT 1
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "perliol.h"

#ifndef PERL_UNUSED_ARG
# define PERL_UNUSED_ARG(x) PERL_UNUSED_VAR(x)
#endif /* !PERL_UNUSED_ARG */

#ifndef Newx
# define Newx(v,n,t) New(0,v,n,t)
#endif /* !Newx */

struct PerlIOrewindable {
	struct _PerlIO base;
	Size_t bufsize;
	Size_t filled;
	Size_t position;
	U8 *buffer;
};

static IV PerlIOrewindable_pushed(pTHX_ PerlIO *f, char const *mode, SV *arg,
	PerlIO_funcs *funcs)
{
	struct PerlIOrewindable *rw = PerlIOSelf(f, struct PerlIOrewindable);
	PERL_UNUSED_ARG(arg);
	{
		IV result = PerlIOBase_pushed(aTHX_ f, mode, NULL, funcs);
		if(result != 0) return result;
	}
	rw->bufsize = 1;
	rw->filled = 0;
	rw->position = 0;
	Newx(rw->buffer, 1, U8);
	return 0;
}

static IV PerlIOrewindable_popped(pTHX_ PerlIO *f)
{
	struct PerlIOrewindable *rw = PerlIOSelf(f, struct PerlIOrewindable);
	if(rw->position != rw->filled) {
		PerlIOBase_unread(aTHX_ PerlIONext(f),
			rw->buffer + rw->position, rw->filled - rw->position);
	}
	Safefree(rw->buffer);
	return 0;
}

static SSize_t PerlIOrewindable_read(pTHX_ PerlIO *f, void *vbuf, Size_t count)
{
	struct PerlIOrewindable *rw = PerlIOSelf(f, struct PerlIOrewindable);
	U8 *cbuf = vbuf;
	Size_t pos = rw->position;
	SSize_t done = 0;
	if(pos != rw->filled) {
		Size_t avail = rw->filled - pos;
		if(avail > count) avail = count;
		Copy(rw->buffer + pos, cbuf, avail, U8);
		pos += avail;
		cbuf += avail;
		count -= avail;
		done = avail;
	}
	if(count) {
		SSize_t avail = PerlIO_read(PerlIONext(f), cbuf, count);
		Size_t endpos;
		if(avail < 0)
			return avail;
		endpos = pos + avail;
		if(endpos > rw->bufsize) {
			Size_t bufsize = rw->bufsize;
			do {
				bufsize <<= 1;
			} while(endpos > bufsize);
			Renew(rw->buffer, bufsize, U8);
			rw->bufsize = bufsize;
		}
		Copy(cbuf, rw->buffer + pos, avail, U8);
		rw->filled = pos = endpos;
		done += avail;
	}
	rw->position = pos;
	return done;
}

static IV PerlIOrewindable_seek(pTHX_ PerlIO *f, Off_t off, int whence)
{
	struct PerlIOrewindable *rw = PerlIOSelf(f, struct PerlIOrewindable);
	switch(whence) {
		case 1: {
			off += rw->position;
		} /* fall through */
		case 0: {
			if(off < 0 || off > (Off_t)rw->filled) {
				errno = EINVAL;
				return -1;
			}
			rw->position = (Size_t)off;
			return 0;
		} break;
		default: {
			errno = EINVAL;
			return -1;
		} break;
	}
}

static Off_t PerlIOrewindable_tell(pTHX_ PerlIO *f)
{
	struct PerlIOrewindable *rw = PerlIOSelf(f, struct PerlIOrewindable);
	return rw->position;
}

static PerlIO_funcs PerlIOrewindable_funcs = {
	sizeof(PerlIO_funcs),
	"rewindable",
	sizeof(struct PerlIOrewindable),
	0,
	PerlIOrewindable_pushed,
	PerlIOrewindable_popped,
	NULL /*open*/,
	NULL /*binmode*/,
	NULL /*getarg*/,
	NULL /*fileno*/,
	NULL /*dup*/,
	PerlIOrewindable_read,
	NULL /*unread*/,
	NULL /*write*/,
	PerlIOrewindable_seek,
	PerlIOrewindable_tell,
	NULL /*close*/,
	NULL /*flush*/,
	NULL /*fill*/,
	NULL /*eof*/,
	NULL /*error*/,
	NULL /*clearerr*/,
	NULL /*setlinebuf*/,
	NULL /*get_base*/,
	NULL /*get_bufsiz*/,
	NULL /*get_ptr*/,
	NULL /*get_cnt*/,
	NULL /*set_ptrcnt*/,
};

MODULE = PerlIO::rewindable PACKAGE = PerlIO::rewindable

PROTOTYPES: DISABLE

BOOT:
	PerlIO_define_layer(aTHX_ &PerlIOrewindable_funcs);