#define _BSD_EXTENSION
#define _NET_EXTENSION
#define _POSIX_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <errno.h>
#include <select.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/wait.h>

extern	int dial_debug;
extern	int dial(char*, char*, char*, int*);


/* debug = 0 for no debugging */
/* debug = 1 for readprinter debugging */
/* debug = 2 for sendprinter debugging */
/* debug = 3 for full debugging, its hard to read the messages */

int debug = 0;
#define READTIMEOUT	300
#define	RCVSELTIMEOUT	30
#define	SNDSELTIMEOUT	300

void
rdtmout(void) {
	fprintf(stderr, "read timeout occurred, check printer\n");
}

int
getline(int fd, char *buf, int len) {
	char *bp, c;
	int i = 0, n;

	bp = buf;
	while (alarm(READTIMEOUT),(n=read(fd, bp, 1)) == 1) {
		alarm(0);
		if (*bp == '\r') continue;
		i += n;

		c = *bp++;
		if (c == '\n' || c == '\004' || i >= len-1)
			break;
	}
	alarm(0);
	if (n < 0)
		return(n);
	*bp = '\0';
	return(i);
}

typedef struct {
	char	*state;			/* printer's current status */
	int	val;			/* value returned by getstatus() */
} Status;

/* printer states */
#define	INITIALIZING	0
#define	IDLE		1
#define	BUSY		2
#define	WAITING		3
#define	PRINTING	4
#define	PRINTERERROR	5
#define	ERROR		6
#define	FLUSHING	7
#define UNKNOWN		8

/* protocol requests and program states */
#define START	'S'
unsigned char Start[] = { START };
#define ID_LE		'L'
unsigned char Id_le[] = { ID_LE };
#define	REQ_STAT	'T'
unsigned char Req_stat[] = { REQ_STAT };
#define	SEND_DATA	'D'
unsigned char Send_data[] = { SEND_DATA };
#define	SENT_DATA	'A'
unsigned char Sent_data[] = { SENT_DATA };
#define	WAIT_FOR_EOJ	'W'
unsigned char Wait_for_eoj[] = { WAIT_FOR_EOJ };
#define END_OF_JOB	'E'
unsigned char End_of_job[] = { END_OF_JOB };
#define FATAL_ERROR	'F'
unsigned char Fatal_error[] = { FATAL_ERROR };
#define	WAIT_FOR_IDLE	'I'
unsigned char Wait_for_idle[] = { WAIT_FOR_IDLE };
#define	OVER_AND_OUT	'O'
unsigned char Over_and_out[] = { OVER_AND_OUT };

Status	statuslist[] = {
	"initializing", INITIALIZING,
	"idle", IDLE,
	"busy", BUSY,
	"waiting", WAITING,
	"printing", PRINTING,
	"printererror", PRINTERERROR,
	"Error", ERROR,
	"flushing", FLUSHING,
	NULL, UNKNOWN
};


/* find returns a pointer to the location of string str2 in string str1,
 * if it exists.  Otherwise, it points to the end of str1.
 */
char *
find(char *str1, char *str2) {
	char *s1, *s2;

	for (; *str1!='\0'; str1++) {
		for (s1=str1,s2=str2; *s2!='\0'&&*s1==*s2; s1++,s2++) ;
		if ( *s2 == '\0' )
	    		break;
	}

	return(str1);
}

#define	MESGSIZE	16384
int blocksize = 1920;		/* 19200/10, with 1 sec delay between transfers
				 * this keeps the queues from building up.
				 */
char	mesg[MESGSIZE];			/* exactly what came back on ttyi */

int
parsmesg(char *buf) {
	static char sbuf[MESGSIZE];
	char	*s;		/* start of printer messsage in mesg[] */
	char	*e;		/* end of printer message in mesg[] */
	char	*key, *val;	/* keyword/value strings in sbuf[] */
	char	*p;		/* for converting to lower case etc. */
	int	i;		/* where *key was found in statuslist[] */

	if (*(s=find(buf, "%[ "))!='\0' && *(e=find(s, " ]%"))!='\0') {
		strcpy(sbuf, s+3);	/* don't change mesg[] */
		sbuf[e-(s+3)] = '\0';	/* ignore the trailing " ]%" */

		for (key=strtok(sbuf, " :"); key != NULL; key=strtok(NULL, " :"))  {
			if (strcmp(key, "Error") == 0)
				return(ERROR);
			if ((val=strtok(NULL, ";")) != NULL && strcmp(key, "status") == 0)
				key = val;

	    		for (; *key == ' '; key++) ;	/* skip any leading spaces */
			for (p = key; *p; p++)		/* convert to lower case */
				if (*p == ':')  {
					*p = '\0';
					break;
				} else if (isupper(*p)) *p = tolower(*p);

			for (i=0; statuslist[i].state != NULL; i++) {
				if (strcmp(statuslist[i].state, key) == 0)
					return(statuslist[i].val);
			}
		}
	}
	return(UNKNOWN);
}

char buf[MESGSIZE];
fd_set readfds, writefds, exceptfds;
struct timeval rcvtimeout = { RCVSELTIMEOUT, 0 };
struct timeval sndtimeout = { SNDSELTIMEOUT, 0 };

int
readprinter(int printerfd, int pipefd)
{
	unsigned char proto;
	int progstate = START;
	int print_wait_msg = 0;
	int tocount = 0;
	int c, printstat, lastprintstat, n, nfds;


	nfds = ((pipefd>printerfd)?pipefd:printerfd) + 1;
	printstat = 0;
	signal(SIGALRM, rdtmout);
	do {

reselect:
		/* ask sending process to request printer status */
		if (write(pipefd, Req_stat, 1) != 1) {
			fprintf(stderr, "request status failed\n");
			progstate = FATAL_ERROR;
			continue;
		}
		FD_ZERO(&readfds);	/* lets be anal */
		FD_SET(printerfd, &readfds);
		FD_SET(pipefd, &readfds);
		FD_ZERO(&exceptfds);
		FD_SET(printerfd, &exceptfds);
		FD_SET(pipefd, &exceptfds);
		n = select(nfds, &readfds, (fd_set *)0, &exceptfds, &rcvtimeout);
		if (debug&0x1) fprintf(stderr, "readprinter select returned %d\n", n);
		if (n == 0) {
			/* a timeout occurred */
			if (++tocount > 4) {
				fprintf(stderr, "printer appears to be offline.\nHP4m printers may be out of paper.\n");
				tocount = 0;
			}
			goto reselect;
		}
		if (n > 0 && FD_ISSET(printerfd, &exceptfds)) {
			/* printer problem */
			fprintf(stderr, "printer exception\n");
			if (write(pipefd, Fatal_error, 1) != 1) {
				fprintf(stderr, "'fatal error' write to pipe failed\n");
			}
			progstate = FATAL_ERROR;
			continue;
		}
		if (n > 0 && FD_ISSET(pipefd, &exceptfds)) {
			/* pipe problem */
			fprintf(stderr, "pipe exception\n");
			progstate = FATAL_ERROR;
			continue;
		}
		if (n > 0 && FD_ISSET(pipefd, &readfds)) {
			/* protocol pipe wants to be read */
			if (debug&0x1) fprintf(stderr, "pipe wants to be read\n");
			if (read(pipefd, &proto, 1) != 1) {
				fprintf(stderr, "read protocol pipe failed\n");
				progstate = FATAL_ERROR;
				continue;
			}
			if (debug&0x1) fprintf(stderr, "readprinter: proto=%c\n", proto);
			/* change state? */
			switch (proto) {
			case SENT_DATA:
				break;
			case WAIT_FOR_EOJ:
				if (!print_wait_msg) {
					print_wait_msg = 1;
					fprintf(stderr, "waiting for end of job\n");
				}
				progstate = proto;
				break;
			default:
				fprintf(stderr, "received unknown protocol request <%c> from sendfile\n", proto);
				break;
			}
			n--;
		}
		if (n > 0 && FD_ISSET(printerfd, &readfds)) {
			/* printer wants to be read */
			if (debug&0x1) fprintf(stderr, "printer wants to be read\n");
			if ((c=getline(printerfd, buf, MESGSIZE)) < 0) {
				fprintf(stderr, "read printer failed\n");
				progstate = FATAL_ERROR;
				continue;
			}
			if (debug&0x1) fprintf(stderr, "%s\n", buf);
			if (c==1 && *buf == '\004') {
				if (progstate == WAIT_FOR_EOJ) {
					if (debug&0x1) fprintf(stderr, "progstate=%c, ", progstate);
					fprintf(stderr, "%%[ status: endofjob ]%%\n");
/*					progstate = WAIT_FOR_IDLE; */
					progstate = OVER_AND_OUT;
					if (write(pipefd, Over_and_out, 1) != 1) {
						fprintf(stderr, "'fatal error' write to pipe failed\n");
					}
					continue;
				} else {
					if (printstat == ERROR) {
						progstate = FATAL_ERROR;
						continue;
					}
					if (progstate != START && progstate != WAIT_FOR_IDLE)
						fprintf(stderr, "warning: EOF received; program status is '%c'\n", progstate);

				}
				continue;
			}

			/* figure out if it was a status line */
			lastprintstat = printstat;
			printstat = parsmesg(buf);
			if (printstat == UNKNOWN || printstat == ERROR
			    || lastprintstat != printstat) {
				/* print whatever it is that was read */
				fprintf(stderr, buf);
				fflush(stderr);
				if (printstat == UNKNOWN) {
					printstat = lastprintstat;
					continue;
				}
			}
			switch (printstat) {
			case UNKNOWN:
				continue;	/* shouldn't get here */
			case FLUSHING:
			case ERROR:
				progstate = FATAL_ERROR;
				/* ask sending process to die */
				if (write(pipefd, Fatal_error, 1) != 1) {
					fprintf(stderr, "Fatal_error mesg write to pipe failed\n");
				}
				continue;
			case INITIALIZING:
			case PRINTERERROR:
				sleep(1);
				break;
			case IDLE:
				if (progstate == WAIT_FOR_IDLE) {
					progstate = OVER_AND_OUT;
					if (write(pipefd, Over_and_out, 1) != 1) {
						fprintf(stderr, "'fatal error' write to pipe failed\n");
					}
					continue;
				}
				progstate = SEND_DATA;

				goto dowait;
			case BUSY:
			case WAITING:
			default:
				sleep(1);
dowait:
				switch (progstate) {
				case WAIT_FOR_IDLE:
				case WAIT_FOR_EOJ:
				case START:
					sleep(5);
					break;

				case SEND_DATA:
					if (write(pipefd, Send_data, 1) != 1) {
						fprintf(stderr, "send data write to pipe failed\n");
						progstate = FATAL_ERROR;
						continue;
					}
					break;
				default:
					fprintf(stderr, "unexpected program state %c\n", progstate);
					exit(1);
				}
				break;
			}
			n--;
		}
		if (n > 0) {
			fprintf(stderr, "more fds selected than requested!\n");
			exit(1);
		};
	} while ((progstate != FATAL_ERROR) && (progstate != OVER_AND_OUT));

	if (progstate == FATAL_ERROR)
		return(1);
	else
		return(0);
}

int
sendfile(int infd, int printerfd, int pipefd)
{
	unsigned char proto;
	int progstate = START;
	int i, n, nfds;
	int bytesread,  bytesent = 0;

	nfds = ((pipefd>printerfd)?pipefd:printerfd) + 1;

	if (write(printerfd, "\004", 1)!=1) {
		perror("sendfile:write:");
		progstate = FATAL_ERROR;
	}
	do {
		FD_ZERO(&readfds);	/* lets be anal */
		FD_SET(pipefd, &readfds);
		n = select(nfds, &readfds, (fd_set *)0, (fd_set *)0, &sndtimeout);
		if (debug&02) fprintf(stderr, "sendfile select returned %d\n", n);
		if (n > 0 && FD_ISSET(pipefd, &readfds)) {
			/* protocol pipe wants to be read */
			if (read(pipefd, &proto, 1) != 1) {
				fprintf(stderr, "read protocol pipe failed\n");
				return(1);
			}
			/* change state? */
			if (debug&02) fprintf(stderr, "sendfile command - <%c>\n", proto);
			switch (proto) {
			case OVER_AND_OUT:
			case END_OF_JOB:
				progstate = proto;
				break;
			case SEND_DATA:
				bytesread = 0;
				do {
					i = read(infd, &buf[bytesread], blocksize-bytesread);
					if (debug&02) fprintf(stderr, "read %d bytes\n", i);
					if (i > 0)
						bytesread += i;
				} while((i > 0) && (bytesread < blocksize));
				if (i < 0) {
					fprintf(stderr, "input file read error\n");
					progstate = FATAL_ERROR;
					break;	/* from switch */
				}
				if (bytesread > 0) {
					if (debug&02) fprintf(stderr, "writing %d bytes\n", bytesread);
					if (write(printerfd, buf, bytesread)!=bytesread) {
						perror("sendfile:write:");
						progstate = FATAL_ERROR;
					} else if (write(pipefd, Sent_data, 1)!=1) {
						perror("sendfile:write:");
						progstate = FATAL_ERROR;
					} else {
						bytesent += bytesread;
					}
					fprintf(stderr, "%d sent\n", bytesent);
					fflush(stderr);

				/* we have reached the end of the input file */
				}
				if (i == 0) {
					if (progstate != WAIT_FOR_EOJ) {
						if (write(printerfd, "\004", 1)!=1) {
							perror("sendfile:write:");
							progstate = FATAL_ERROR;
						} else if (write(pipefd, Wait_for_eoj, 1)!=1) {
							perror("sendfile:write:");
							progstate = FATAL_ERROR;
						} else {
							progstate = WAIT_FOR_EOJ;
						}
					}
				}
				break;
			case REQ_STAT:
				if (write(printerfd, "\024", 1)!=1) {
					fprintf(stderr, "write to printer failed\n");
					progstate = FATAL_ERROR;
				}
				if (debug&02) fprintf(stderr, "^T");
				break;
			case FATAL_ERROR:
				progstate = FATAL_ERROR;
			}
		} else if (n < 0) {
			perror("sendfile:select:");
			progstate = FATAL_ERROR;
		} else if (n == 0) {
			sleep(1);
			fprintf(stderr, "sendfile timeout\n");
			progstate = FATAL_ERROR;
		}
	} while ((progstate != FATAL_ERROR) && (progstate != OVER_AND_OUT));
	if (write(printerfd, "\004", 1)!=1) {
		perror("sendfile:write:");
		progstate = FATAL_ERROR;
	}

	if (debug&02) fprintf(stderr, "%d bytes sent\n", bytesent);
	if (progstate == FATAL_ERROR)
		return(1);
	else
		return(0);
}

void main(int argc, char *argv[]) {
	int c, usgflg=0, infd, printerfd;
	int cpid, sprv;
	int pipefd[2];
	char *dialstr;
	unsigned long rprv;

	dialstr = 0;

	while ((c = getopt(argc, argv, "b:d:")) != -1)
		switch (c) {
		case 'b':
			blocksize = atoi(optarg)/10;
			if (blocksize > MESGSIZE || blocksize < 1)
				blocksize = MESGSIZE;
			break;
		case 'd':
			debug = atoi(optarg);
			dial_debug = debug;
			break;
		case '?':
			fprintf(stderr, "unknown option %c\n", c);
			usgflg++;
			break;
		}
	if (optind < argc)
		dialstr = argv[optind++];
	else
		usgflg++;
	if (usgflg) {
		fprintf(stderr, "usage: %s [-b baudrate] net!host!service [infile]\n",
			argv[0]);
		exit (2);
	}
	if (optind < argc) {
		infd = open(argv[optind], 0);
		if (infd < 0) {
			fprintf(stderr, "cannot open %s\n", argv[optind]);
			exit(1);
		}
		optind++;
	} else
		infd = 0;

	if (debug & 02)
		fprintf(stderr, "blocksize=%d\n", blocksize);
	if (debug)
		fprintf(stderr, "dialing address=%s\n", dialstr);
	printerfd = dial(dialstr, 0, 0, 0);
	if (printerfd < 0)
		exit(1);

	fprintf(stderr, "printer startup\n");

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipefd) < 0) {
		perror("socketpair");
		exit(1);
	}
	switch(cpid = fork()){
	case -1:
		perror("fork error");
		exit(1);
	case 0:				/* child - to printer */
		close(pipefd[1]);
		sprv = sendfile(infd, printerfd, pipefd[0]);
		if (debug)
			fprintf(stderr, "to remote - exiting\n");
		exit(sprv);
	default:			/* parent - from printer */
		close(pipefd[0]);
		rprv = readprinter(printerfd, pipefd[1]);
		if (debug)
			fprintf(stderr, "from remote - exiting\n");
		while(wait(&sprv) != cpid)
			;
		exit(rprv|sprv);
	}
}
