/*
 * lpdev.c: installs the "/dev/lp" device. It is a
 * buffered Centronics device. This program is free software; see the
 * file "COPYING" for details.
 *
 * This file must be compiled with 16-bit integers.
 *
 * Author:  Thierry Bousch (bousch@suntopo.matups.fr)
 * Version: 0.6 (july 93)
 *
 * Revision history:
 *  0.1: First attempt, using SLEEP instead of NAP: it didn't work.
 *  0.2: Added version number, napping in lp_write(), and the TIOCFLUSH
 *        ioctl function. Cleaned up things a bit.
 *  0.3: Introduced spl7() and spl() to fix competition problems. Added
 *        a few tests before installation, to check that MiNT is running
 *        and the device is not already installed.
 *  0.4: Added file locking. This is completely untested, so be
 *        careful. More cleanup and sanity checks during installation.
 *        Modified the sleep conditions in lp_write and lp_select.
 *  0.5: Deleted the now unnecessary stuff about low and high water marks.
 *        More comments added.
 *  0.6: Added support for the O_NDELAY and O_LOCK flags, inlined spl7/spl.
 *        Moved the definitions to lpdev.h.
 */

#include "lpdev.h"
#define  LP_VERSION	"0.6"

/*
 * Global variables
 */
 
char *buffer_start, *buffer_end, *buffer_tail;
volatile char *buffer_head;
volatile long buffer_contents;
long selector = 0L;
struct kerinfo *kernel;
struct flock our_lock = { F_WRLCK, 0, 0L, 0L, -1 };

/*
 * Forward declarations of the device driver functions
 */

long	lp_open		(FILEPTR *f);
long	lp_write	(FILEPTR *f, char *buf, long bytes);
long	lp_read		(FILEPTR *f, char *buf, long bytes);
long	lp_lseek	(FILEPTR *f, long where, int whence);
long	lp_ioctl	(FILEPTR *f, int mode, void *buf);
long	lp_datime	(FILEPTR *f, int *timeptr, int rwflag);
long	lp_close	(FILEPTR *f, int pid);
long	lp_select	(FILEPTR *f, long proc, int mode);
void	lp_unselect	(FILEPTR *f, long proc, int mode);

DEVDRV lp_device = {
	lp_open, lp_write, lp_read, lp_lseek, lp_ioctl,
	lp_datime, lp_close, lp_select, lp_unselect
};

struct dev_descr devinfo = { &lp_device };

/* Initializes the circular buffer */
 
void reset_buffer (void)
{
	int sr = spl7();
	buffer_head = buffer_tail = buffer_start;
	buffer_end = buffer_start + BUFSIZE;
	buffer_contents = 0L;
	spl(sr);
}

/* Copyright information */

void Version (void)
{
	Cconws("Spooled Centronics device driver, by T.Bousch (version "
	LP_VERSION ").\r\n"
	"This program is FREE SOFTWARE, and comes with NO WARRANTY.\r\n"
	"See the file \"COPYING\" for more information.\r\n");
}

/* 
 * Installs everything, returns 0 on success. Must be executed in
 * supervisor mode.
 */

long install_things (void)
{
	if (Syield() == EINVFN) {
		Cconws("lpdev: MiNT is not running\r\n");
		return EACCDN;
	}
	if (Fsfirst(DEVNAME, 0) == 0) {
		Cconws("lpdev: device \"" DEVNAME "\" already installed\r\n");
		return EACCDN;
	}
	if (*(char*)0xFFFFFA09 &	/* IERB */
	    *(char*)0xFFFFFA15 & 1) {	/* IMRB */
		Cconws("lpdev: Centronics interrupt already in use\r\n");
		return EACCDN;
	}
	buffer_start = (char *)Malloc(BUFSIZE);
	if (!buffer_start) {
		Cconws("lpdev: not enough memory\r\n");
		return ENSMEM;
	}
	reset_buffer();

	kernel = (struct kerinfo *)Dcntl(DEV_INSTALL, DEVNAME, &devinfo);
	if ((long)kernel <= 0L) {
		Cconws("lpdev: unable to install device\r\n");
		return EACCDN;
	}
	/* Finally! */
	Mfpint(0, new_centr_vector);
	Jenabint(0);

	return 0;
}

/*
 * The main routine is very simple now, it just calls install_things
 * and remains resident if everything went well
 */

int main()
{
	long ret;
	
	ret = Supexec(install_things);
	if (ret < 0)
		return ret;

	/* Installation is complete */
	Version();
	Ptermres(256L + _base->p_tlen + _base->p_dlen + _base->p_blen, 0);
	return -999;	/* never reached, just to make Gcc happy */
}

/*
 * Will wake any process select'ing the printer;
 * this routine is called by the interrupt handler, but also when the
 * buffer is flushed.
 */

void wake_up (void)
{
	if (selector)
		WAKESELECT(selector);	/* wake selector */
}

/*
 * Sends as many bytes as possible (usually one) to the printer until
 * he gets busy. This routine is called by lp_write and by the
 * interrupt handler, so it _must_ be multi-thread. It will not work if
 * you remove the spl7()/spl() pair.
 *
 * On a more general note, it is safest to disable all interrupts before
 * modifying the volatile variables (buffer_contents and buffer_head).
 */

#define PRINTER_BUSY	(*(char*)0xFFFFFA01 & 1)

void print_head (void)
{
	int sr = spl7();
	while (!PRINTER_BUSY && buffer_contents) {
		print_byte( *buffer_head );
		--buffer_contents;
		if (++buffer_head >= buffer_end)
			buffer_head -= BUFSIZE;
	}
	spl(sr);
}

/*
 * Copies a linear buffer into the circular one. We assume that's there
 * enough room for this operation, ie
 * nbytes + buffer_contents <= BUFSIZE
 *
 * Note: the while() loop will be executed at most twice.
 * Note2: the instruction "buffer_contents += N" looks atomic, but it
 *   isn't (the Gcc outputs several assembly instructions). Therefore it
 *   must be wrapped in spl7()/spl().
 */

void print_tail (char *buf, long nbytes)
{
	long N;
	int sr;
	
	while (nbytes) {
		N = buffer_end - buffer_tail;
		if (N > nbytes)
			N = nbytes;
		bcopy (buf, buffer_tail, N);
		buf += N; nbytes -= N;
		sr = spl7();
		buffer_contents += N;
		spl(sr);
		buffer_tail += N;
		if (buffer_tail >= buffer_end)
			buffer_tail -= BUFSIZE;
	}
	print_head();	/* To initiate printing */
}

/*
 * Here are the actual device driver functions
 */
 
#define  LP_LOCKED	(our_lock.l_pid >= 0)

long lp_open (FILEPTR *f)
{
	TRACE(("lp: open device"));
	return 0;
}

long lp_close (FILEPTR *f, int pid)
{
	TRACE(("lp: close device"));
	if ((f->flags & O_LOCK) && our_lock.l_pid == pid) {
		TRACE(("lp: releasing lock on close"));
		f->flags &= ~O_LOCK;
		our_lock.l_pid = -1;
		WAKE(IO_Q, (long)&our_lock);
	}
	return 0;
}

long lp_read (FILEPTR *f, char *buf, long bytes)
{
	TRACE(("lp: foolish attempt to read"));
	return 0;
}

long lp_datime (FILEPTR *f, int *timeptr, int rwflag)
{
	if (rwflag) {
		DEBUG(("lp: can't modify date/time"));
		return EACCDN;
	}
	TRACE(("lp: read time and date"));
	*timeptr++ = TGETTIME();
	*timeptr   = TGETDATE();
	return 0;
}

long lp_lseek (FILEPTR *f, long where, int whence)
{
	TRACE(("lp: foolish attempt to seek"));
	if (whence < 0 || whence > 2)
		return EINVFN;
	return where ? ERANGE : 0L;
}

long lp_ioctl (FILEPTR *f, int mode, void *buf)
{
	struct flock *g;

	if (mode == FIONREAD) {
		TRACE(("lp: ioctl(FIONREAD)"));
		*(long *)buf = 0L;
	}
	else if (mode == FIONWRITE) {
		TRACE(("lp: ioctl(FIONWRITE)"));
		*(long *)buf = BUFSIZE - buffer_contents;
	}
	else if (mode == TIOCFLUSH) {
		TRACE(("lp: clear buffer"));
		reset_buffer();
		wake_up();	/* Wake up any select'ing process */
	}
	else if (mode == F_GETLK) {
		g = (struct flock *) buf;

		if (LP_LOCKED) {
			TRACE(("lp: get_lock succeeded"));
			*g = our_lock;
		} else {
			TRACE(("lp: get_lock failed"));
			g->l_type = F_UNLCK;
		}
	}
	else if (mode == F_SETLK || mode == F_SETLKW) {
		g = (struct flock *) buf;

		switch (g->l_type) {
		case F_UNLCK:
		    if (!(f->flags & O_LOCK) || g->l_pid != our_lock.l_pid) {
			DEBUG(("lp: no such lock"));
			return ENSLOCK;
		    } else {
			TRACE(("lp: remove lock"));
			f->flags &= ~O_LOCK;
			our_lock.l_pid = -1;
			WAKE(IO_Q, (long)&our_lock);
		    }
		    return 0;
		case F_RDLCK:
		    TRACE(("lp: read locks are ignored"));
		    return 0;
		case F_WRLCK:
		    while (LP_LOCKED) {
			DEBUG(("lp: conflicting locks"));
			if (mode == F_SETLK) {
				*g = our_lock;
				return ELOCKED;
			}
			SLEEP(IO_Q, (long)&our_lock);
		    }
		    TRACE(("lp: set lock"));
		    f->flags |= O_LOCK;
		    our_lock.l_pid = g->l_pid;
		    return 0;
		default:
		    DEBUG(("lp: invalid lock type"));
		    return EINVFN;
		}
	}
	else {
		DEBUG(("lp: invalid ioctl mode"));
		return EINVFN;
	}
	return 0;
}

long lp_write (FILEPTR *f, char *buf, long bytes)
{
	long _bytes = bytes;
	long N;
	int  ndel = (f->flags & O_NDELAY);	/* don't wait */

	while (bytes) {
		N = BUFSIZE - buffer_contents;
		/*
		 * If the data won't fit into the buffer,
		 * and if the buffer itself is almost full, we won't
		 * be able to copy much. So we better sleep a bit (unless
		 * the O_NDELAY flag is set).
		 */
		if (N < bytes && N < BUFSIZE/4 && !ndel) {
			TRACE(("lp: napping in lp_write"));
			NAP(200);	/* let's wait 200 milliseconds */
			continue;	/* and try again */
		}
		if (bytes < N)
			N = bytes;
		/* Now N contains the number of bytes we want to copy
		 * into the circular buffer */
		print_tail(buf, N);
		buf += N;
		bytes -= N;
		/*
		 * If the O_NDELAY flag is set, we don't make a second
		 * attempt to write the remaining "bytes" bytes.
		 */
		if (ndel)  break;
	}
	TRACE(("lp: wrote %ld bytes, skipped %ld", _bytes-bytes, bytes));
	return _bytes - bytes;
}

/* Bug: only one process can select the printer */

long lp_select (FILEPTR *f, long proc, int mode)
{
	if (buffer_contents == BUFSIZE && !selector) {
		TRACE(("lp: select returned 0"));
		selector = proc;
		return 0;
	}
	TRACE(("lp: select returned 1"));
	return 1;
}

void lp_unselect (FILEPTR *f, long proc, int mode)
{
	TRACE(("lp: unselect"));
	selector = 0L;
}
