/* This file contains the printer driver. It is a fairly simple driver,
 * supporting only one printer.  Characters that are written to the driver
 * are written to the printer without any changes at all.
 *
 * Changes:
 *	May 07, 2004	fix: wait until printer is ready  (Jorrit N. Herder)
 *	May 06, 2004	printer driver moved to user-space  (Jorrit N. Herder) 
 *
 * The valid messages and their parameters are:
 *
 *   DEV_OPEN:	initializes the printer
 *   DEV_CLOSE:	does nothing
 *   HARD_INT:	interrupt handler has finished current chunk of output
 *   DEV_WRITE:	a process wants to write on a terminal
 *   CANCEL:	terminate a previous incomplete system call immediately
 *
 *    m_type      TTY_LINE  USER_ENDPT  COUNT    ADDRESS
 * |-------------+---------+---------+---------+---------|
 * | DEV_OPEN    |         |         |         |         |
 * |-------------+---------+---------+---------+---------|
 * | DEV_CLOSE   |         | proc nr |         |         |
 * -------------------------------------------------------
 * | HARD_INT    |         |         |         |         |
 * |-------------+---------+---------+---------+---------|
 * | SYS_EVENT   |         |         |         |         |
 * |-------------+---------+---------+---------+---------|
 * | DEV_WRITE   |minor dev| proc nr |  count  | buf ptr |
 * |-------------+---------+---------+---------+---------|
 * | CANCEL      |minor dev| proc nr |         |         |
 * -------------------------------------------------------
 * 
 * Note: since only 1 printer is supported, minor dev is not used at present.
 */

#include <minix/endpoint.h>
#include <minix/drivers.h>
#include <minix/chardriver.h>

/* Control bits (in port_base + 2).  "+" means positive logic and "-" means
 * negative logic.  Most of the signals are negative logic on the pins but
 * many are converted to positive logic in the ports.  Some manuals are
 * misleading because they only document the pin logic.
 *
 *	+0x01	Pin 1	-Strobe
 *	+0x02	Pin 14	-Auto Feed
 *	-0x04	Pin 16	-Initialize Printer
 *	+0x08	Pin 17	-Select Printer
 *	+0x10	IRQ7 Enable
 *
 * Auto Feed and Select Printer are always enabled. Strobe is enabled briefly
 * when characters are output.  Initialize Printer is enabled briefly when
 * the task is started.  IRQ7 is enabled when the first character is output
 * and left enabled until output is completed (or later after certain
 * abnormal completions).
 */
#define ASSERT_STROBE   0x1D	/* strobe a character to the interface */
#define NEGATE_STROBE   0x1C	/* enable interrupt on interface */
#define PR_SELECT          0x0C	/* select printer bit */
#define INIT_PRINTER    0x08	/* init printer bits */

/* Status bits (in port_base + 2).
 *
 *	-0x08	Pin 15	-Error
 *	+0x10	Pin 13	+Select Status
 *	+0x20	Pin 12	+Out of Paper
 *	-0x40	Pin 10	-Acknowledge
 *	-0x80	Pin 11	+Busy
 */
#define BUSY_STATUS     0x10	/* printer gives this status when busy */
#define NO_PAPER        0x20	/* status bit saying that paper is out */
#define NORMAL_STATUS   0x90	/* printer gives this status when idle */
#define ON_LINE         0x10	/* status bit saying that printer is online */
#define STATUS_MASK	0xB0	/* mask to filter out status bits */ 

#define MAX_ONLINE_RETRIES 120  /* about 60s: waits 0.5s after each retry */

/* Centronics interface timing that must be met by software (in microsec).
 *
 * Strobe length:	0.5u to 100u (not sure about the upper limit).
 * Data set up:		0.5u before strobe.
 * Data hold:		0.5u after strobe.
 * Init pulse length:	over 200u (not sure).
 *
 * The strobe length is about 50u with the code here and function calls for
 * sys_outb() - not much to spare.  The 0.5u minimums will not be violated 
 * with the sys_outb() messages exchanged.
 */

static endpoint_t caller;	/* process to tell when printing done (FS) */
static int revive_pending;	/* set to true if revive is pending */
static int revive_status;	/* revive status */
static int done_status;	/* status of last output completion */
static int oleft;		/* bytes of output left in obuf */
static unsigned char obuf[128];	/* output buffer */
static unsigned const char *optr;	/* ptr to next char in obuf to print */
static int orig_count;		/* original byte count */
static int port_base;		/* I/O port for printer */
static endpoint_t proc_nr;	/* user requesting the printing */
static cp_grant_id_t grant_nr;	/* grant on which print happens */
static int user_left;		/* bytes of output left in user buf */
static vir_bytes user_vir_d;	/* offset in user buf */
int writing;		/* nonzero while write is in progress */
static int irq_hook_id;	/* id of irq hook at kernel */

static void do_cancel(message *m_ptr);
static void output_done(void);
static void do_write(message *m_ptr);
static void do_status(message *m_ptr);
static void prepare_output(void);
static int do_probe(void);
static void do_initialize(void);
static void reply(int code,int replyee,int proc,int status);
static void do_printer_output(void);

/* SEF functions and variables. */
static void sef_local_startup(void);
static int sef_cb_init_fresh(int type, sef_init_info_t *info);
EXTERN int sef_cb_lu_prepare(int state);
EXTERN int sef_cb_lu_state_isvalid(int state);
EXTERN void sef_cb_lu_state_dump(int state);
int is_status_msg_expected = FALSE;

/*===========================================================================*
 *				printer_task				     *
 *===========================================================================*/
int main(void)
{
/* Main routine of the printer task. */
  message pr_mess;		/* buffer for all incoming messages */
  int ipc_status;

  /* SEF local startup. */
  sef_local_startup();

  while (TRUE) {
	if(driver_receive(ANY, &pr_mess, &ipc_status) != OK) {
		panic("driver_receive failed");
	}

	if (is_ipc_notify(ipc_status)) {
		switch (_ENDPOINT_P(pr_mess.m_source)) {
			case HARDWARE:
				do_printer_output();
				break;
			default:
				reply(TASK_REPLY, pr_mess.m_source,
						pr_mess.USER_ENDPT, EINVAL);
		}
		continue;
	}

	switch(pr_mess.m_type) {
	    case DEV_OPEN:
                 do_initialize();		/* initialize */
	        /* fall through */
	    case DEV_CLOSE:
		reply(TASK_REPLY, pr_mess.m_source, pr_mess.USER_ENDPT, OK);
		break;
	    case DEV_WRITE_S:	do_write(&pr_mess);	break;
	    case DEV_STATUS:	do_status(&pr_mess);	break;
	    case CANCEL:	do_cancel(&pr_mess);	break;
	    default:
		reply(TASK_REPLY, pr_mess.m_source, pr_mess.USER_ENDPT,
			EINVAL);
	}
  }
}

/*===========================================================================*
 *			       sef_local_startup			     *
 *===========================================================================*/
static void sef_local_startup()
{
  /* Register init callbacks. */
  sef_setcb_init_fresh(sef_cb_init_fresh);
  sef_setcb_init_lu(sef_cb_init_fresh);
  sef_setcb_init_restart(sef_cb_init_fresh);

  /* Register live update callbacks. */
  sef_setcb_lu_prepare(sef_cb_lu_prepare);
  sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid);
  sef_setcb_lu_state_dump(sef_cb_lu_state_dump);

  /* Register signal callbacks. */
  sef_setcb_signal_handler(sef_cb_signal_handler_term);

  /* Let SEF perform startup. */
  sef_startup();
}

/*===========================================================================*
 *		            sef_cb_init_fresh                                *
 *===========================================================================*/
static int sef_cb_init_fresh(int UNUSED(type), sef_init_info_t *UNUSED(info))
{
/* Initialize the printer driver. */

  /* If no printer is present, do not start. */
  if (!do_probe())
	return ENODEV;	/* arbitrary error code */

  /* Announce we are up! */
  chardriver_announce();

  return OK;
}

/*===========================================================================*
 *				do_write				     *
 *===========================================================================*/
static void do_write(m_ptr)
register message *m_ptr;	/* pointer to the newly arrived message */
{
/* The printer is used by sending DEV_WRITE messages to it. Process one. */

    register int r = SUSPEND;
    int retries;
    u32_t status;

    /* Reject command if last write is not yet finished, the count is not
     * positive, or the user address is bad.
     */
    if (writing)  			r = EIO;
    else if (m_ptr->COUNT <= 0)  	r = EINVAL;

    /* Reply to FS, no matter what happened, possible SUSPEND caller. */
    reply(TASK_REPLY, m_ptr->m_source, m_ptr->USER_ENDPT, r);

    /* If no errors occurred, continue printing with SUSPENDED caller.
     * First wait until the printer is online to prevent stupid errors.
     */
    if (SUSPEND == r) { 	
	caller = m_ptr->m_source;
	proc_nr = m_ptr->USER_ENDPT;
	user_left = m_ptr->COUNT;
	orig_count = m_ptr->COUNT;
	user_vir_d = 0;				 	/* Offset. */
	writing = TRUE;
	grant_nr = (cp_grant_id_t) m_ptr->IO_GRANT;

        retries = MAX_ONLINE_RETRIES + 1;  
        while (--retries > 0) {
            if(sys_inb(port_base + 1, &status) != OK) {
		printf("printer: sys_inb of %x failed\n", port_base+1);
		panic("sys_inb failed");
	    }
            if ((status & ON_LINE)) {		/* printer online! */
	        prepare_output();
	        do_printer_output();
	        return;
            }
            micro_delay(500000);		/* wait before retry */
        }
        /* If we reach this point, the printer was not online in time. */
        done_status = status;
        output_done();
    }
}

/*===========================================================================*
 *				output_done				     *
 *===========================================================================*/
static void output_done()
{
/* Previous chunk of printing is finished.  Continue if OK and more.
 * Otherwise, reply to caller (FS).
 */
    register int status;

    if (!writing) return;	  	/* probably leftover interrupt */
    if (done_status != OK) {      	/* printer error occurred */
        status = EIO;
	if ((done_status & ON_LINE) == 0) { 
	    printf("Printer is not on line\n");
	} else if ((done_status & NO_PAPER)) { 
	    printf("Printer is out of paper\n");
	    status = EAGAIN;	
	} else {
	    printf("Printer error, status is 0x%02X\n", done_status);
	}
	/* Some characters have been printed, tell how many. */
	if (status == EAGAIN && user_left < orig_count) {
		status = orig_count - user_left;
	}
	oleft = 0;			/* cancel further output */
    } 
    else if (user_left != 0) {		/* not yet done, continue! */
	prepare_output();
	return;
    } 
    else {				/* done! report back to FS */
	status = orig_count;
    }
    is_status_msg_expected = TRUE;
    revive_pending = TRUE;
    revive_status = status;
    notify(caller);
}

/*===========================================================================*
 *				do_status				     *
 *===========================================================================*/
static void do_status(m_ptr)
register message *m_ptr;	/* pointer to the newly arrived message */
{
  if (revive_pending) {
	m_ptr->m_type = DEV_REVIVE;		/* build message */
	m_ptr->REP_ENDPT = proc_nr;
	m_ptr->REP_STATUS = revive_status;
	m_ptr->REP_IO_GRANT = grant_nr;

	writing = FALSE;			/* unmark event */
	revive_pending = FALSE;			/* unmark event */
  } else {
	m_ptr->m_type = DEV_NO_STATUS;
	
	is_status_msg_expected = FALSE;
  }
  send(m_ptr->m_source, m_ptr);			/* send the message */
}

/*===========================================================================*
 *				do_cancel				     *
 *===========================================================================*/
static void do_cancel(m_ptr)
register message *m_ptr;	/* pointer to the newly arrived message */
{
/* Cancel a print request that has already started.  Usually this means that
 * the process doing the printing has been killed by a signal.  It is not
 * clear if there are race conditions.  Try not to cancel the wrong process,
 * but rely on FS to handle the EINTR reply and de-suspension properly.
 */

  if (writing && m_ptr->USER_ENDPT == proc_nr) {
	oleft = 0;		/* cancel output by interrupt handler */
	writing = FALSE;
	revive_pending = FALSE;
  }
  reply(TASK_REPLY, m_ptr->m_source, m_ptr->USER_ENDPT, EINTR);
}

/*===========================================================================*
 *				reply					     *
 *===========================================================================*/
static void reply(code, replyee, process, status)
int code;			/* TASK_REPLY or REVIVE */
int replyee;			/* destination for message (normally FS) */
int process;			/* which user requested the printing */
int status;			/* number of  chars printed or error code */
{
/* Send a reply telling FS that printing has started or stopped. */

  message pr_mess;

  pr_mess.m_type = code;		/* TASK_REPLY or REVIVE */
  pr_mess.REP_STATUS = status;		/* count or EIO */
  pr_mess.REP_ENDPT = process;	/* which user does this pertain to */
  send(replyee, &pr_mess);		/* send the message */
}

/*===========================================================================*
 *				do_probe				     *
 *===========================================================================*/
static int do_probe(void)
{
/* See if there is a printer at all. */

  /* Get the base port for first printer. */
  if(sys_readbios(LPT1_IO_PORT_ADDR, &port_base, LPT1_IO_PORT_SIZE) != OK) {
	panic("do_initialize: sys_readbios failed");
  }

  /* If the port is zero, the parallel port is not available at all. */
  return (port_base != 0);
}

/*===========================================================================*
 *				do_initialize				     *
 *===========================================================================*/
static void do_initialize()
{
/* Set global variables and initialize the printer. */
  static int initialized = FALSE;
  if (initialized) return;
  initialized = TRUE;
  
  if(sys_outb(port_base + 2, INIT_PRINTER) != OK) {
	printf("printer: sys_outb of %x failed\n", port_base+2);
	panic("do_initialize: sys_outb init failed");
  }
  micro_delay(1000000/20);	/* easily satisfies Centronics minimum */
  if(sys_outb(port_base + 2, PR_SELECT) != OK) {
	printf("printer: sys_outb of %x failed\n", port_base+2);
	panic("do_initialize: sys_outb select failed");
  }
  irq_hook_id = 0;
  if(sys_irqsetpolicy(PRINTER_IRQ, 0, &irq_hook_id) != OK ||
     sys_irqenable(&irq_hook_id) != OK) {
	panic("do_initialize: irq enabling failed");
  }
}

/*==========================================================================*
 *		    	      prepare_output				    *
 *==========================================================================*/
static void prepare_output()
{
/* Start next chunk of printer output. Fetch the data from user space. */
  int s;
  register int chunk;

  if ( (chunk = user_left) > sizeof obuf) chunk = sizeof obuf;

  s=sys_safecopyfrom(caller, grant_nr, user_vir_d, (vir_bytes) obuf,
	chunk);

  if(s != OK) {
  	done_status = EFAULT;
  	output_done();
  	return;
  }
  optr = obuf;
  oleft = chunk;
}

/*===========================================================================*
 *				do_printer_output				     *
 *===========================================================================*/
static void do_printer_output()
{
/* This function does the actual output to the printer. This is called on an
 * interrupt message sent from the generic interrupt handler that 'forwards'
 * interrupts to this driver. The generic handler did not reenable the printer
 * IRQ yet! 
 */

  u32_t status;
  pvb_pair_t char_out[3];

  if (oleft == 0) {
	/* Nothing more to print.  Turn off printer interrupts in case they
	 * are level-sensitive as on the PS/2.  This should be safe even
	 * when the printer is busy with a previous character, because the
	 * interrupt status does not affect the printer.
	 */
	if(sys_outb(port_base + 2, PR_SELECT) != OK) {
		printf("printer: sys_outb of %x failed\n", port_base+2);
		panic("sys_outb failed");
	}
	if(sys_irqenable(&irq_hook_id) != OK) {
		panic("sys_irqenable failed");
	}
	return;
  }

  do {
	/* Loop to handle fast (buffered) printers.  It is important that
	 * processor interrupts are not disabled here, just printer interrupts.
	 */
	if(sys_inb(port_base + 1, &status) != OK) {
		printf("printer: sys_inb of %x failed\n", port_base+1);
		panic("sys_inb failed");
	}
	if ((status & STATUS_MASK) == BUSY_STATUS) {
		/* Still busy with last output.  This normally happens
		 * immediately after doing output to an unbuffered or slow
		 * printer.  It may happen after a call from prepare_output or
		 * pr_restart, since they are not synchronized with printer
		 * interrupts.  It may happen after a spurious interrupt.
		 */
		if(sys_irqenable(&irq_hook_id) != OK) {
			panic("sys_irqenable failed");
		}
		return;
	}
	if ((status & STATUS_MASK) == NORMAL_STATUS) {
		/* Everything is all right.  Output another character. */
		pv_set(char_out[0], port_base, *optr);	
		optr++;
		pv_set(char_out[1], port_base+2, ASSERT_STROBE);
		pv_set(char_out[2], port_base+2, NEGATE_STROBE);
		if(sys_voutb(char_out, 3) != OK) {
			/* request series of port outb */
			panic("sys_voutb failed");
		}

		user_vir_d++;
		user_left--;
	} else {
		/* Error.  This would be better ignored (treat as busy). */
		done_status = status;
		output_done();
		if(sys_irqenable(&irq_hook_id) != OK) {
			panic("sys_irqenable failed");
		}
		return;
	}
  }
  while (--oleft != 0);

  /* Finished printing chunk OK. */
  done_status = OK;
  output_done();
  if(sys_irqenable(&irq_hook_id) != OK) {
	panic("sys_irqenable failed");
  }
}

