/*
 *  drivers/s390/char/hwc_rw.c
 *     driver: reading from and writing to system console on S/390 via HWC
 *
 *  S390 version
 *    Copyright (C) 1999 IBM Deutschland Entwicklung GmbH, IBM Corporation
 *    Author(s): Martin Peschke <mpeschke@de.ibm.com>
 *
 * 
 *
 * 
 * 
 * 
 */

#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/ctype.h>
#include <linux/mm.h>
#include <linux/timer.h>

#include <asm/ebcdic.h>
#include <asm/uaccess.h>
#include <asm/types.h>
#include <asm/bitops.h>
#include <asm/setup.h>
#include <asm/page.h>
#include <asm/s390_ext.h>
#include <asm/irq.h>

#ifndef MIN
#define MIN(a,b) (((a<b) ? a : b))
#endif

#define HWC_ASCEBC(x) ((MACHINE_IS_VM ? _ascebc[x] : _ascebc_500[x]))

#define HWC_EBCASC_STR(s,c) ((MACHINE_IS_VM ? EBCASC(s,c) : EBCASC_500(s,c)))

#define HWC_RW_PRINT_HEADER "hwc low level driver: "

#define  USE_VM_DETECTION

#define  DEFAULT_CASE_DELIMITER '%'

#define DUMP_HWC_INIT_ERROR

#define DUMP_HWC_WRITE_ERROR

#define DUMP_HWC_WRITE_LIST_ERROR

#define DUMP_HWC_READ_ERROR

#undef DUMP_HWCB_INPUT

#undef BUFFER_STRESS_TEST

#undef MEASURE_HWC_OUTPUT

typedef struct {
	unsigned char *next;
	unsigned short int mto_char_sum;
	unsigned char mto_number;
	unsigned char times_lost;
	unsigned short int mto_number_lost;
	unsigned long int mto_char_sum_lost;
} __attribute__ ((packed)) 

hwcb_list_t;

#define MAX_HWCB_ROOM (PAGE_SIZE - sizeof(hwcb_list_t))

#define MAX_MESSAGE_SIZE (MAX_HWCB_ROOM - sizeof(write_hwcb_t))

#define BUF_HWCB hwc_data.hwcb_list_tail
#define OUT_HWCB hwc_data.hwcb_list_head
#define ALL_HWCB_MTO hwc_data.mto_number
#define ALL_HWCB_CHAR hwc_data.mto_char_sum

#define _LIST(hwcb) ((hwcb_list_t*)(&(hwcb)[PAGE_SIZE-sizeof(hwcb_list_t)]))

#define _HWCB_CHAR(hwcb) (_LIST(hwcb)->mto_char_sum)

#define _HWCB_MTO(hwcb) (_LIST(hwcb)->mto_number)

#define _HWCB_CHAR_LOST(hwcb) (_LIST(hwcb)->mto_char_sum_lost)

#define _HWCB_MTO_LOST(hwcb) (_LIST(hwcb)->mto_number_lost)

#define _HWCB_TIMES_LOST(hwcb) (_LIST(hwcb)->times_lost)

#define _HWCB_NEXT(hwcb) (_LIST(hwcb)->next)

#define BUF_HWCB_CHAR _HWCB_CHAR(BUF_HWCB)

#define BUF_HWCB_MTO _HWCB_MTO(BUF_HWCB)

#define BUF_HWCB_NEXT _HWCB_NEXT(BUF_HWCB)

#define OUT_HWCB_CHAR _HWCB_CHAR(OUT_HWCB)

#define OUT_HWCB_MTO _HWCB_MTO(OUT_HWCB)

#define OUT_HWCB_NEXT _HWCB_NEXT(OUT_HWCB)

#define BUF_HWCB_CHAR_LOST _HWCB_CHAR_LOST(BUF_HWCB)

#define BUF_HWCB_MTO_LOST _HWCB_MTO_LOST(BUF_HWCB)

#define OUT_HWCB_CHAR_LOST _HWCB_CHAR_LOST(OUT_HWCB)

#define OUT_HWCB_MTO_LOST _HWCB_MTO_LOST(OUT_HWCB)

#define BUF_HWCB_TIMES_LOST _HWCB_TIMES_LOST(BUF_HWCB)

#include  "hwc.h"

#define __HWC_RW_C__
#include "hwc_rw.h"
#undef __HWC_RW_C__

static unsigned char _obuf[MAX_HWCB_ROOM];

static unsigned char
 _page[PAGE_SIZE] __attribute__ ((aligned (PAGE_SIZE)));

typedef unsigned long kmem_pages_t;

#define MAX_KMEM_PAGES (sizeof(kmem_pages_t) << 3)

#define HWC_WTIMER_RUNS	1
#define HWC_FLUSH		2
#define HWC_INIT		4
#define HWC_BROKEN		8
#define HWC_INTERRUPT		16
#define HWC_PTIMER_RUNS	32

static struct {

	hwc_ioctls_t ioctls;

	hwc_ioctls_t init_ioctls;

	unsigned char *hwcb_list_head;

	unsigned char *hwcb_list_tail;

	unsigned short int mto_number;

	unsigned int mto_char_sum;

	unsigned char hwcb_count;

	unsigned long kmem_start;

	unsigned long kmem_end;

	kmem_pages_t kmem_pages;

	unsigned char *obuf;

	unsigned short int obuf_cursor;

	unsigned short int obuf_count;

	unsigned short int obuf_start;

	unsigned char *page;

	u32 current_servc;

	unsigned char *current_hwcb;

	unsigned char write_nonprio:1;
	unsigned char write_prio:1;
	unsigned char read_nonprio:1;
	unsigned char read_prio:1;
	unsigned char read_statechange:1;

	unsigned char flags;

	hwc_high_level_calls_t *calls;

	spinlock_t lock;

	struct timer_list write_timer;

	struct timer_list poll_timer;
} hwc_data =
{
	{
	},
	{
		8,
		    0,
		    80,
		    1,
		    MAX_KMEM_PAGES,
		    MAX_KMEM_PAGES,

		    0,

		    0x6c,

		    0,
		    0,
		    0
	},
	    NULL,
	    NULL,
	    0,
	    0,
	    0,
	    0,
	    0,
	    0,
	    _obuf,
	    0,
	    0,
	    0,
	    _page,
	    0,
	    NULL,
	    0,
	    0,
	    0,
	    0,
	    0,
	    0,
	    NULL

};

static unsigned long cr0 __attribute__ ((aligned (8)));
static unsigned long cr0_save __attribute__ ((aligned (8)));
static unsigned char psw_mask __attribute__ ((aligned (8)));

#define DELAYED_WRITE 0
#define IMMEDIATE_WRITE 1

static signed int do_hwc_write (int from_user, unsigned char *,
				unsigned int,
				unsigned char);

static asmlinkage int 
internal_print (char write_time, char *fmt,...)
{
	va_list args;
	int i;
	unsigned char buf[512];

	va_start (args, fmt);
	i = vsprintf (buf, fmt, args);
	va_end (args);
	return do_hwc_write (0, buf, i, write_time);
}

int 
hwc_printk (const char *fmt,...)
{
	va_list args;
	int i;
	unsigned char buf[512];
	unsigned long flags;
	int retval;

	spin_lock_irqsave (&hwc_data.lock, flags);

	i = vsprintf (buf, fmt, args);
	va_end (args);
	retval = do_hwc_write (0, buf, i, IMMEDIATE_WRITE);

	spin_unlock_irqrestore (&hwc_data.lock, flags);

	return retval;
}

#ifdef DUMP_HWCB_INPUT

static void 
dump_storage_area (unsigned char *area, unsigned short int count)
{
	unsigned short int index;
	ioctl_nl_t old_final_nl;

	if (!area || !count)
		return;

	old_final_nl = hwc_data.ioctls.final_nl;
	hwc_data.ioctls.final_nl = 1;

	internal_print (DELAYED_WRITE, "\n%8x   ", area);

	for (index = 0; index < count; index++) {

		if (area[index] <= 0xF)
			internal_print (DELAYED_WRITE, "0%x", area[index]);
		else
			internal_print (DELAYED_WRITE, "%x", area[index]);

		if ((index & 0xF) == 0xF)
			internal_print (DELAYED_WRITE, "\n%8x   ",
					&area[index + 1]);
		else if ((index & 3) == 3)
			internal_print (DELAYED_WRITE, " ");
	}

	internal_print (IMMEDIATE_WRITE, "\n");

	hwc_data.ioctls.final_nl = old_final_nl;
}
#endif

static inline u32 
service_call (
		     u32 hwc_command_word,
		     unsigned char hwcb[])
{
	unsigned int condition_code = 1;

	__asm__ __volatile__ ("L 1, 0(%0) \n\t"
			      "LRA 2, 0(%1) \n\t"
			      ".long 0xB2200012 \n\t"
			      :
			      :"a" (&hwc_command_word), "a" (hwcb)
			      :"1", "2", "memory");

	__asm__ __volatile__ ("IPM %0 \n\t"
			      "SRL %0, 28 \n\t"
			      :"=r" (condition_code));

	return condition_code;
}

static inline unsigned long 
hwc_ext_int_param (void)
{
	u32 param;

	__asm__ __volatile__ ("L %0,128\n\t"
			      :"=r" (param));

	return (unsigned long) param;
}

static int 
prepare_write_hwcb (void)
{
	write_hwcb_t *hwcb;

	if (!BUF_HWCB)
		return -ENOMEM;

	BUF_HWCB_MTO = 0;
	BUF_HWCB_CHAR = 0;

	hwcb = (write_hwcb_t *) BUF_HWCB;

	memcpy (hwcb, &write_hwcb_template, sizeof (write_hwcb_t));

	return 0;
}

static int 
sane_write_hwcb (void)
{
	unsigned short int lost_msg;
	unsigned int lost_char;
	unsigned char lost_hwcb;
	unsigned char *bad_addr;
	unsigned long page;
	int page_nr;

	if (!OUT_HWCB)
		return -ENOMEM;

	if ((unsigned long) OUT_HWCB & 0xFFF) {

		bad_addr = OUT_HWCB;

#ifdef DUMP_HWC_WRITE_LIST_ERROR
		__asm__ ("LHI 1,0xe30\n\t"
			 "LRA 2,0(%0) \n\t"
			 "J .+0 \n\t"
	      :
	      :	 "a" (bad_addr)
	      :	 "1", "2");
#endif

		hwc_data.kmem_pages = 0;
		if ((unsigned long) BUF_HWCB & 0xFFF) {

			lost_hwcb = hwc_data.hwcb_count;
			lost_msg = ALL_HWCB_MTO;
			lost_char = ALL_HWCB_CHAR;

			OUT_HWCB = NULL;
			BUF_HWCB = NULL;
			ALL_HWCB_MTO = 0;
			ALL_HWCB_CHAR = 0;
			hwc_data.hwcb_count = 0;
		} else {

			lost_hwcb = hwc_data.hwcb_count - 1;
			lost_msg = ALL_HWCB_MTO - BUF_HWCB_MTO;
			lost_char = ALL_HWCB_CHAR - BUF_HWCB_CHAR;
			OUT_HWCB = BUF_HWCB;
			ALL_HWCB_MTO = BUF_HWCB_MTO;
			ALL_HWCB_CHAR = BUF_HWCB_CHAR;
			hwc_data.hwcb_count = 1;
			page = (unsigned long) BUF_HWCB;

			if (page >= hwc_data.kmem_start &&
			    page <= hwc_data.kmem_end) {

				page_nr = (int)
				    ((page - hwc_data.kmem_start) >> 12);
				set_bit (page_nr, &hwc_data.kmem_pages);
			}
		}

		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
		       "found invalid HWCB at address 0x%lx. List corrupted. "
			   "Lost %i HWCBs with %i characters within up to %i "
			   "messages. Saved %i HWCB with last %i characters i"
				       "within up to %i messages.\n",
				       (unsigned long) bad_addr,
				       lost_hwcb, lost_char, lost_msg,
				       hwc_data.hwcb_count,
				       ALL_HWCB_CHAR, ALL_HWCB_MTO);
	}
	return 0;
}

static int 
reuse_write_hwcb (void)
{
	int retval;

	if (hwc_data.hwcb_count < 2)
#ifdef DUMP_HWC_WRITE_LIST_ERROR
		__asm__ ("LHI 1,0xe31\n\t"
			 "LRA 2,0(%0)\n\t"
			 "LRA 3,0(%1)\n\t"
			 "J .+0 \n\t"
	      :
	      :	 "a" (BUF_HWCB), "a" (OUT_HWCB)
	      :	 "1", "2", "3");
#else
		return -EPERM;
#endif

	if (hwc_data.current_hwcb == OUT_HWCB) {

		if (hwc_data.hwcb_count > 2) {

			BUF_HWCB_NEXT = OUT_HWCB_NEXT;

			BUF_HWCB = OUT_HWCB_NEXT;

			OUT_HWCB_NEXT = BUF_HWCB_NEXT;

			BUF_HWCB_NEXT = NULL;
		}
	} else {

		BUF_HWCB_NEXT = OUT_HWCB;

		BUF_HWCB = OUT_HWCB;

		OUT_HWCB = OUT_HWCB_NEXT;

		BUF_HWCB_NEXT = NULL;
	}

	BUF_HWCB_TIMES_LOST += 1;
	BUF_HWCB_CHAR_LOST += BUF_HWCB_CHAR;
	BUF_HWCB_MTO_LOST += BUF_HWCB_MTO;
	ALL_HWCB_MTO -= BUF_HWCB_MTO;
	ALL_HWCB_CHAR -= BUF_HWCB_CHAR;

	retval = prepare_write_hwcb ();

	if (hwc_data.hwcb_count == hwc_data.ioctls.max_hwcb)
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "reached my own limit of "
			    "allowed buffer space for output (%i HWCBs = %li "
			  "bytes), skipped content of oldest HWCB %i time(s) "
				       "(%i lines = %i characters)\n",
				       hwc_data.ioctls.max_hwcb,
				       hwc_data.ioctls.max_hwcb * PAGE_SIZE,
				       BUF_HWCB_TIMES_LOST,
				       BUF_HWCB_MTO_LOST,
				       BUF_HWCB_CHAR_LOST);
	else
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "page allocation failed, "
			   "could not expand buffer for output (currently in "
			     "use: %i HWCBs = %li bytes), skipped content of "
			"oldest HWCB %i time(s) (%i lines = %i characters)\n",
				       hwc_data.hwcb_count,
				       hwc_data.hwcb_count * PAGE_SIZE,
				       BUF_HWCB_TIMES_LOST,
				       BUF_HWCB_MTO_LOST,
				       BUF_HWCB_CHAR_LOST);

	return retval;
}

static int 
allocate_write_hwcb (void)
{
	unsigned char *page;
	int page_nr;

	if (hwc_data.hwcb_count == hwc_data.ioctls.max_hwcb)
		return -ENOMEM;

	page_nr = find_first_zero_bit (&hwc_data.kmem_pages, MAX_KMEM_PAGES);
	if (page_nr < hwc_data.ioctls.kmem_hwcb) {

		page = (unsigned char *)
		    (hwc_data.kmem_start + (page_nr << 12));
		set_bit (page_nr, &hwc_data.kmem_pages);
	} else
		page = (unsigned char *) __get_free_page (GFP_ATOMIC);

	if (!page)
		return -ENOMEM;

	if (!OUT_HWCB)
		OUT_HWCB = page;
	else
		BUF_HWCB_NEXT = page;

	BUF_HWCB = page;

	BUF_HWCB_NEXT = NULL;

	hwc_data.hwcb_count++;

	prepare_write_hwcb ();

	BUF_HWCB_TIMES_LOST = 0;
	BUF_HWCB_MTO_LOST = 0;
	BUF_HWCB_CHAR_LOST = 0;

#ifdef BUFFER_STRESS_TEST

	internal_print (
			       DELAYED_WRITE,
			       "*** " HWC_RW_PRINT_HEADER
			    "page #%i at 0x%x for buffering allocated. ***\n",
			       hwc_data.hwcb_count, page);

#endif

	return 0;
}

static int 
release_write_hwcb (void)
{
	unsigned long page;
	int page_nr;

	if (!hwc_data.hwcb_count)
		return -ENODATA;

	if (hwc_data.hwcb_count == 1) {

		prepare_write_hwcb ();

		ALL_HWCB_CHAR = 0;
		ALL_HWCB_MTO = 0;
		BUF_HWCB_TIMES_LOST = 0;
		BUF_HWCB_MTO_LOST = 0;
		BUF_HWCB_CHAR_LOST = 0;
	} else {
		page = (unsigned long) OUT_HWCB;

		ALL_HWCB_MTO -= OUT_HWCB_MTO;
		ALL_HWCB_CHAR -= OUT_HWCB_CHAR;
		hwc_data.hwcb_count--;

		OUT_HWCB = OUT_HWCB_NEXT;

		if (page >= hwc_data.kmem_start &&
		    page <= hwc_data.kmem_end) {
			/*memset((void *) page, 0, PAGE_SIZE); */

			page_nr = (int) ((page - hwc_data.kmem_start) >> 12);
			clear_bit (page_nr, &hwc_data.kmem_pages);
		} else
			free_page (page);
#ifdef BUFFER_STRESS_TEST

		internal_print (
				       DELAYED_WRITE,
				       "*** " HWC_RW_PRINT_HEADER
			 "page at 0x%x released, %i pages still in use ***\n",
				       page, hwc_data.hwcb_count);

#endif
	}
	return 0;
}

static int 
add_mto (
		unsigned char *message,
		unsigned short int count)
{
	unsigned short int mto_size;
	write_hwcb_t *hwcb;
	mto_t *mto;
	void *dest;

	if (!BUF_HWCB)
		return -ENOMEM;

	if (BUF_HWCB == hwc_data.current_hwcb)
		return -ENOMEM;

	mto_size = sizeof (mto_t) + count;

	hwcb = (write_hwcb_t *) BUF_HWCB;

	if ((MAX_HWCB_ROOM - hwcb->length) < mto_size)
		return -ENOMEM;

	mto = (mto_t *) (((unsigned long) hwcb) + hwcb->length);

	memcpy (mto, &mto_template, sizeof (mto_t));

	dest = (void *) (((unsigned long) mto) + sizeof (mto_t));

	memcpy (dest, message, count);

	mto->length += count;

	hwcb->length += mto_size;
	hwcb->msgbuf.length += mto_size;
	hwcb->msgbuf.mdb.length += mto_size;

	BUF_HWCB_MTO++;
	ALL_HWCB_MTO++;
	BUF_HWCB_CHAR += count;
	ALL_HWCB_CHAR += count;

#ifdef MEASURE_HWC_OUTPUT

	hwc_data.ioctls.measured_lines++;
	hwc_data.ioctls.measured_chars += count;
#endif

	return count;
}

static int write_event_data_1 (void);

static void 
do_poll_hwc (unsigned long data)
{
	unsigned long flags;

	spin_lock_irqsave (&hwc_data.lock, flags);

	write_event_data_1 ();

	spin_unlock_irqrestore (&hwc_data.lock, flags);
}

void 
start_poll_hwc (void)
{
	init_timer (&hwc_data.poll_timer);
	hwc_data.poll_timer.function = do_poll_hwc;
	hwc_data.poll_timer.data = (unsigned long) NULL;
	hwc_data.poll_timer.expires = jiffies + 2 * HZ;
	add_timer (&hwc_data.poll_timer);
	hwc_data.flags |= HWC_PTIMER_RUNS;
}

static int 
write_event_data_1 (void)
{
	unsigned short int condition_code;
	int retval;
	write_hwcb_t *hwcb = (write_hwcb_t *) OUT_HWCB;

	if ((!hwc_data.write_prio) &&
	    (!hwc_data.write_nonprio) &&
	    hwc_data.read_statechange)
		return -EOPNOTSUPP;

	if (hwc_data.current_servc)
		return -EBUSY;

	retval = sane_write_hwcb ();
	if (retval < 0)
		return -EIO;

	if (!OUT_HWCB_MTO)
		return -ENODATA;

	if (!hwc_data.write_nonprio && hwc_data.write_prio)
		hwcb->msgbuf.type = ET_PMsgCmd;
	else
		hwcb->msgbuf.type = ET_Msg;

	condition_code = service_call (HWC_CMDW_WRITEDATA, OUT_HWCB);

#ifdef DUMP_HWC_WRITE_ERROR
	if (condition_code != HWC_COMMAND_INITIATED)
		__asm__ ("LHI 1,0xe20\n\t"
			 "L 2,0(%0)\n\t"
			 "LRA 3,0(%1)\n\t"
			 "J .+0 \n\t"
	      :
	      :	 "a" (&condition_code), "a" (OUT_HWCB)
	      :	 "1", "2", "3");
#endif

	switch (condition_code) {
	case HWC_COMMAND_INITIATED:
		hwc_data.current_servc = HWC_CMDW_WRITEDATA;
		hwc_data.current_hwcb = OUT_HWCB;
		retval = condition_code;
#ifdef MEASURE_HWC_OUTPUT
		hwc_data.ioctls.measured_wcalls++;
#endif
		break;
	case HWC_BUSY:
		retval = -EBUSY;
		break;
	case HWC_NOT_OPERATIONAL:
		start_poll_hwc ();
	default:
		retval = -EIO;
	}

	return retval;
}

static void 
flush_hwcbs (void)
{
	while (hwc_data.hwcb_count > 1)
		release_write_hwcb ();

	release_write_hwcb ();

	hwc_data.flags &= ~HWC_FLUSH;
}

static int 
write_event_data_2 (u32 ext_int_param)
{
	write_hwcb_t *hwcb;
	int retval = 0;

#ifdef DUMP_HWC_WRITE_ERROR
	if ((ext_int_param & HWC_EXT_INT_PARAM_ADDR)
	    != (unsigned long) hwc_data.current_hwcb) {
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "write_event_data_2 : "
				       "HWCB address does not fit "
				       "(expected: 0x%lx, got: 0x%lx).\n",
				       (unsigned long) hwc_data.current_hwcb,
				       ext_int_param);
		return -EINVAL;
	}
#endif

	hwcb = (write_hwcb_t *) OUT_HWCB;

#ifdef DUMP_HWC_WRITE_LIST_ERROR
	if (((unsigned char *) hwcb) != hwc_data.current_hwcb) {
		__asm__ ("LHI 1,0xe22\n\t"
			 "LRA 2,0(%0)\n\t"
			 "LRA 3,0(%1)\n\t"
			 "LRA 4,0(%2)\n\t"
			 "LRA 5,0(%3)\n\t"
			 "J .+0 \n\t"
	      :
	      :	 "a" (OUT_HWCB),
			 "a" (hwc_data.current_hwcb),
			 "a" (BUF_HWCB),
			 "a" (hwcb)
	      :	 "1", "2", "3", "4", "5");
	}
#endif

#ifdef DUMP_HWC_WRITE_ERROR
	if (hwcb->response_code != 0x0020) {
		__asm__ ("LHI 1,0xe21\n\t"
			 "LRA 2,0(%0)\n\t"
			 "LRA 3,0(%1)\n\t"
			 "LRA 4,0(%2)\n\t"
			 "LH 5,0(%3)\n\t"
			 "SRL 5,8\n\t"
			 "J .+0 \n\t"
	      :
	      :	 "a" (OUT_HWCB), "a" (hwc_data.current_hwcb),
			 "a" (BUF_HWCB),
			 "a" (&(hwc_data.hwcb_count))
	      :	 "1", "2", "3", "4", "5");
	}
#endif

	switch (hwcb->response_code) {
	case 0x0020:

		retval = OUT_HWCB_CHAR;
		release_write_hwcb ();
		break;
	case 0x0040:
	case 0x0340:
	case 0x40F0:
		if (!hwc_data.read_statechange) {
			hwcb->response_code = 0;
			start_poll_hwc ();
		}
		retval = -EIO;
		break;
	default:
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "write_event_data_2 : "
				       "failed operation "
				       "(response code: 0x%x "
				       "HWCB address: 0x%x).\n",
				       hwcb->response_code,
				       hwcb);
		retval = -EIO;
	}

	if (retval == -EIO) {

		hwcb->control_mask[0] = 0;
		hwcb->control_mask[1] = 0;
		hwcb->control_mask[2] = 0;
		hwcb->response_code = 0;
	}
	hwc_data.current_servc = 0;
	hwc_data.current_hwcb = NULL;

	if (hwc_data.flags & HWC_FLUSH)
		flush_hwcbs ();

	return retval;
}

static void 
do_put_line (
		    unsigned char *message,
		    unsigned short count)
{

	if (add_mto (message, count) != count) {

		if (allocate_write_hwcb () < 0)
			reuse_write_hwcb ();

#ifdef DUMP_HWC_WRITE_LIST_ERROR
		if (add_mto (message, count) != count)
			__asm__ ("LHI 1,0xe32\n\t"
				 "LRA 2,0(%0)\n\t"
				 "L 3,0(%1)\n\t"
				 "LRA 4,0(%2)\n\t"
				 "LRA 5,0(%3)\n\t"
				 "J .+0 \n\t"
		      :
		      :	 "a" (message), "a" (&hwc_data.kmem_pages),
				 "a" (BUF_HWCB), "a" (OUT_HWCB)
		      :	 "1", "2", "3", "4", "5");
#else
		add_mto (message, count);
#endif
	}
}

static void 
put_line (
		 unsigned char *message,
		 unsigned short count)
{

	if ((!hwc_data.obuf_start) && (hwc_data.flags & HWC_WTIMER_RUNS)) {
		del_timer (&hwc_data.write_timer);
		hwc_data.flags &= ~HWC_WTIMER_RUNS;
	}
	hwc_data.obuf_start += count;

	do_put_line (message, count);

	hwc_data.obuf_start -= count;
}

static void 
set_alarm (void)
{
	write_hwcb_t *hwcb;

	if ((!BUF_HWCB) || (BUF_HWCB == hwc_data.current_hwcb))
		allocate_write_hwcb ();

	hwcb = (write_hwcb_t *) BUF_HWCB;
	hwcb->msgbuf.mdb.mdb_body.go.general_msg_flags |= GMF_SndAlrm;
}

static void 
hwc_write_timeout (unsigned long data)
{
	unsigned long flags;

	spin_lock_irqsave (&hwc_data.lock, flags);

	hwc_data.obuf_start = hwc_data.obuf_count;
	if (hwc_data.obuf_count)
		put_line (hwc_data.obuf, hwc_data.obuf_count);
	hwc_data.obuf_start = 0;

	hwc_data.obuf_cursor = 0;
	hwc_data.obuf_count = 0;

	write_event_data_1 ();

	spin_unlock_irqrestore (&hwc_data.lock, flags);
}

static int 
do_hwc_write (
		     int from_user,
		     unsigned char *msg,
		     unsigned int count,
		     unsigned char write_time)
{
	unsigned int i_msg = 0;
	unsigned short int spaces = 0;
	unsigned int processed_characters = 0;
	unsigned char ch;
	unsigned short int obuf_count;
	unsigned short int obuf_cursor;
	unsigned short int obuf_columns;

	if (hwc_data.obuf_start) {
		obuf_cursor = 0;
		obuf_count = 0;
		obuf_columns = MIN (hwc_data.ioctls.columns,
				    MAX_MESSAGE_SIZE - hwc_data.obuf_start);
	} else {
		obuf_cursor = hwc_data.obuf_cursor;
		obuf_count = hwc_data.obuf_count;
		obuf_columns = hwc_data.ioctls.columns;
	}

	for (i_msg = 0; i_msg < count; i_msg++) {
		if (from_user)
			get_user (ch, msg + i_msg);
		else
			ch = msg[i_msg];

		processed_characters++;

		if ((obuf_cursor == obuf_columns) &&

		    (ch != '\n') &&

		    (ch != '\t')) {
			put_line (&hwc_data.obuf[hwc_data.obuf_start],
				  obuf_columns);
			obuf_cursor = 0;
			obuf_count = 0;
		}
		switch (ch) {

		case '\n':

			put_line (&hwc_data.obuf[hwc_data.obuf_start],
				  obuf_count);
			obuf_cursor = 0;
			obuf_count = 0;
			break;

		case '\a':

			hwc_data.obuf_start += obuf_count;
			set_alarm ();
			hwc_data.obuf_start -= obuf_count;

			break;

		case '\t':

			do {
				if (obuf_cursor < obuf_columns) {
					hwc_data.obuf[hwc_data.obuf_start +
						      obuf_cursor]
					    = HWC_ASCEBC (' ');
					obuf_cursor++;
				} else
					break;
			} while (obuf_cursor % hwc_data.ioctls.width_htab);

			break;

		case '\f':
		case '\v':

			spaces = obuf_cursor;
			put_line (&hwc_data.obuf[hwc_data.obuf_start],
				  obuf_count);
			obuf_count = obuf_cursor;
			while (spaces) {
				hwc_data.obuf[hwc_data.obuf_start +
					      obuf_cursor - spaces]
				    = HWC_ASCEBC (' ');
				spaces--;
			}

			break;

		case '\b':

			if (obuf_cursor)
				obuf_cursor--;
			break;

		case '\r':

			obuf_cursor = 0;
			break;

		case 0x00:

			put_line (&hwc_data.obuf[hwc_data.obuf_start],
				  obuf_count);
			obuf_cursor = 0;
			obuf_count = 0;
			goto out;

		default:

			if (isprint (ch))
				hwc_data.obuf[hwc_data.obuf_start +
					      obuf_cursor++]
				    = HWC_ASCEBC (ch);
		}
		if (obuf_cursor > obuf_count)
			obuf_count = obuf_cursor;
	}

	if (obuf_cursor) {

		if (hwc_data.obuf_start ||
		    (hwc_data.ioctls.final_nl == 0)) {

			put_line (&hwc_data.obuf[hwc_data.obuf_start],
				  obuf_count);
			obuf_cursor = 0;
			obuf_count = 0;
		} else {

			if (hwc_data.ioctls.final_nl > 0) {

				if (hwc_data.flags & HWC_WTIMER_RUNS) {

					mod_timer (&hwc_data.write_timer,
						   jiffies + hwc_data.ioctls.final_nl * HZ / 10);
				} else {

					init_timer (&hwc_data.write_timer);
					hwc_data.write_timer.function =
					    hwc_write_timeout;
					hwc_data.write_timer.data =
					    (unsigned long) NULL;
					hwc_data.write_timer.expires =
					    jiffies +
					    hwc_data.ioctls.final_nl * HZ / 10;
					add_timer (&hwc_data.write_timer);
					hwc_data.flags |= HWC_WTIMER_RUNS;
				}
			} else;

		}
	} else;

      out:

	if (!hwc_data.obuf_start) {
		hwc_data.obuf_cursor = obuf_cursor;
		hwc_data.obuf_count = obuf_count;
	}
	if (write_time == IMMEDIATE_WRITE)
		write_event_data_1 ();

	return processed_characters;
}

signed int 
hwc_write (int from_user, const unsigned char *msg, unsigned int count)
{
	unsigned long flags;
	int retval;

	spin_lock_irqsave (&hwc_data.lock, flags);

	retval = do_hwc_write (from_user, (unsigned char *) msg,
			       count, IMMEDIATE_WRITE);

	spin_unlock_irqrestore (&hwc_data.lock, flags);

	return retval;
}

unsigned int 
hwc_chars_in_buffer (unsigned char flag)
{
	unsigned short int number = 0;
	unsigned long flags;

	spin_lock_irqsave (&hwc_data.lock, flags);

	if (flag & IN_HWCB)
		number += ALL_HWCB_CHAR;

	if (flag & IN_WRITE_BUF)
		number += hwc_data.obuf_cursor;

	spin_unlock_irqrestore (&hwc_data.lock, flags);

	return number;
}

static inline int 
nr_setbits (kmem_pages_t arg)
{
	int i;
	int nr = 0;

	for (i = 0; i < (sizeof (arg) << 3); i++) {
		if (arg & 1)
			nr++;
		arg >>= 1;
	}

	return nr;
}

unsigned int 
hwc_write_room (unsigned char flag)
{
	unsigned int number = 0;
	unsigned long flags;
	write_hwcb_t *hwcb;

	spin_lock_irqsave (&hwc_data.lock, flags);

	if (flag & IN_HWCB) {

		if (BUF_HWCB) {
			hwcb = (write_hwcb_t *) BUF_HWCB;
			number += MAX_HWCB_ROOM - hwcb->length;
		}
		number += (hwc_data.ioctls.kmem_hwcb -
			   nr_setbits (hwc_data.kmem_pages)) *
		    (MAX_HWCB_ROOM -
		     (sizeof (write_hwcb_t) + sizeof (mto_t)));
	}
	if (flag & IN_WRITE_BUF)
		number += MAX_HWCB_ROOM - hwc_data.obuf_cursor;

	spin_unlock_irqrestore (&hwc_data.lock, flags);

	return number;
}

void 
hwc_flush_buffer (unsigned char flag)
{
	unsigned long flags;

	spin_lock_irqsave (&hwc_data.lock, flags);

	if (flag & IN_HWCB) {
		if (hwc_data.current_servc != HWC_CMDW_WRITEDATA)
			flush_hwcbs ();
		else
			hwc_data.flags |= HWC_FLUSH;
	}
	if (flag & IN_WRITE_BUF) {
		hwc_data.obuf_cursor = 0;
		hwc_data.obuf_count = 0;
	}
	spin_unlock_irqrestore (&hwc_data.lock, flags);
}

unsigned short int 
seperate_cases (unsigned char *buf, unsigned short int count)
{

	unsigned short int i_in;

	unsigned short int i_out = 0;

	unsigned char _case = 0;

	for (i_in = 0; i_in < count; i_in++) {

		if (buf[i_in] == hwc_data.ioctls.delim) {

			if ((i_in + 1 < count) &&
			    (buf[i_in + 1] == hwc_data.ioctls.delim)) {

				buf[i_out] = hwc_data.ioctls.delim;

				i_out++;

				i_in++;

			} else
				_case = ~_case;

		} else {

			if (_case) {

				if (hwc_data.ioctls.tolower)
					buf[i_out] = _ebc_toupper[buf[i_in]];

				else
					buf[i_out] = _ebc_tolower[buf[i_in]];

			} else
				buf[i_out] = buf[i_in];

			i_out++;
		}
	}

	return i_out;
}

#ifdef DUMP_HWCB_INPUT

static int 
gds_vector_name (u16 id, unsigned char name[])
{
	int retval = 0;

	switch (id) {
	case GDS_ID_MDSMU:
		name = "Multiple Domain Support Message Unit";
		break;
	case GDS_ID_MDSRouteInfo:
		name = "MDS Routing Information";
		break;
	case GDS_ID_AgUnWrkCorr:
		name = "Agent Unit of Work Correlator";
		break;
	case GDS_ID_SNACondReport:
		name = "SNA Condition Report";
		break;
	case GDS_ID_CPMSU:
		name = "CP Management Services Unit";
		break;
	case GDS_ID_RoutTargInstr:
		name = "Routing and Targeting Instructions";
		break;
	case GDS_ID_OpReq:
		name = "Operate Request";
		break;
	case GDS_ID_TextCmd:
		name = "Text Command";
		break;

	default:
		name = "unknown GDS variable";
		retval = -EINVAL;
	}

	return retval;
}
#endif

inline static gds_vector_t *
find_gds_vector (
			gds_vector_t * start, void *end, u16 id)
{
	gds_vector_t *vec;
	gds_vector_t *retval = NULL;

	vec = start;

	while (((void *) vec) < end) {
		if (vec->gds_id == id) {

#ifdef DUMP_HWCB_INPUT
			int retval_name;
			unsigned char name[64];

			retval_name = gds_vector_name (id, name);
			internal_print (
					       DELAYED_WRITE,
					       HWC_RW_PRINT_HEADER
					  "%s at 0x%x up to 0x%x, length: %d",
					       name,
					       (unsigned long) vec,
				      ((unsigned long) vec) + vec->length - 1,
					       vec->length);
			if (retval_name < 0)
				internal_print (
						       IMMEDIATE_WRITE,
						       ", id: 0x%x\n",
						       vec->gds_id);
			else
				internal_print (
						       IMMEDIATE_WRITE,
						       "\n");
#endif

			retval = vec;
			break;
		}
		vec = (gds_vector_t *) (((unsigned long) vec) + vec->length);
	}

	return retval;
}

inline static gds_subvector_t *
find_gds_subvector (
			   gds_subvector_t * start, void *end, u8 key)
{
	gds_subvector_t *subvec;
	gds_subvector_t *retval = NULL;

	subvec = start;

	while (((void *) subvec) < end) {
		if (subvec->key == key) {
			retval = subvec;
			break;
		}
		subvec = (gds_subvector_t *)
		    (((unsigned long) subvec) + subvec->length);
	}

	return retval;
}

inline static int 
get_input (void *start, void *end)
{
	int count;

	count = ((unsigned long) end) - ((unsigned long) start);

	if (hwc_data.ioctls.tolower)
		EBC_TOLOWER (start, count);

	if (hwc_data.ioctls.delim)
		count = seperate_cases (start, count);

	HWC_EBCASC_STR (start, count);

	if (hwc_data.ioctls.echo)
		do_hwc_write (0, start, count, IMMEDIATE_WRITE);

	if (hwc_data.calls != NULL)
		if (hwc_data.calls->move_input != NULL)
			(hwc_data.calls->move_input) (start, count);

	return count;
}

inline static int 
eval_selfdeftextmsg (gds_subvector_t * start, void *end)
{
	gds_subvector_t *subvec;
	void *subvec_data;
	void *subvec_end;
	int retval = 0;

	subvec = start;

	while (((void *) subvec) < end) {
		subvec = find_gds_subvector (subvec, end, 0x30);
		if (!subvec)
			break;
		subvec_data = (void *)
		    (((unsigned long) subvec) +
		     sizeof (gds_subvector_t));
		subvec_end = (void *)
		    (((unsigned long) subvec) + subvec->length);
		retval += get_input (subvec_data, subvec_end);
		subvec = (gds_subvector_t *) subvec_end;
	}

	return retval;
}

inline static int 
eval_textcmd (gds_subvector_t * start, void *end)
{
	gds_subvector_t *subvec;
	gds_subvector_t *subvec_data;
	void *subvec_end;
	int retval = 0;

	subvec = start;

	while (((void *) subvec) < end) {
		subvec = find_gds_subvector (
					 subvec, end, GDS_KEY_SelfDefTextMsg);
		if (!subvec)
			break;
		subvec_data = (gds_subvector_t *)
		    (((unsigned long) subvec) +
		     sizeof (gds_subvector_t));
		subvec_end = (void *)
		    (((unsigned long) subvec) + subvec->length);
		retval += eval_selfdeftextmsg (subvec_data, subvec_end);
		subvec = (gds_subvector_t *) subvec_end;
	}

	return retval;
}

inline static int 
eval_cpmsu (gds_vector_t * start, void *end)
{
	gds_vector_t *vec;
	gds_subvector_t *vec_data;
	void *vec_end;
	int retval = 0;

	vec = start;

	while (((void *) vec) < end) {
		vec = find_gds_vector (vec, end, GDS_ID_TextCmd);
		if (!vec)
			break;
		vec_data = (gds_subvector_t *)
		    (((unsigned long) vec) + sizeof (gds_vector_t));
		vec_end = (void *) (((unsigned long) vec) + vec->length);
		retval += eval_textcmd (vec_data, vec_end);
		vec = (gds_vector_t *) vec_end;
	}

	return retval;
}

inline static int 
eval_mdsmu (gds_vector_t * start, void *end)
{
	gds_vector_t *vec;
	gds_vector_t *vec_data;
	void *vec_end;
	int retval = 0;

	vec = find_gds_vector (start, end, GDS_ID_CPMSU);
	if (vec) {
		vec_data = (gds_vector_t *)
		    (((unsigned long) vec) + sizeof (gds_vector_t));
		vec_end = (void *) (((unsigned long) vec) + vec->length);
		retval = eval_cpmsu (vec_data, vec_end);
	}
	return retval;
}

static int 
eval_evbuf (gds_vector_t * start, void *end)
{
	gds_vector_t *vec;
	gds_vector_t *vec_data;
	void *vec_end;
	int retval = 0;

	vec = find_gds_vector (start, end, GDS_ID_MDSMU);
	if (vec) {
		vec_data = (gds_vector_t *)
		    (((unsigned long) vec) + sizeof (gds_vector_t));
		vec_end = (void *) (((unsigned long) vec) + vec->length);
		retval = eval_mdsmu (vec_data, vec_end);
	}
	return retval;
}

static inline int 
eval_hwc_receive_mask (_hwcb_mask_t mask)
{

	hwc_data.write_nonprio
	    = ((mask & ET_Msg_Mask) == ET_Msg_Mask);

	hwc_data.write_prio
	    = ((mask & ET_PMsgCmd_Mask) == ET_PMsgCmd_Mask);

	if (hwc_data.write_prio || hwc_data.write_nonprio) {
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "can write messages\n");
		return 0;
	} else {
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "can not write messages\n");
		return -1;
	}
}

static inline int 
eval_hwc_send_mask (_hwcb_mask_t mask)
{

	hwc_data.read_statechange
	    = ((mask & ET_StateChange_Mask) == ET_StateChange_Mask);
	if (hwc_data.read_statechange)
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				     "can read state change notifications\n");
	else
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				 "can not read state change notifications\n");

	hwc_data.read_nonprio
	    = ((mask & ET_OpCmd_Mask) == ET_OpCmd_Mask);
	if (hwc_data.read_nonprio)
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "can read commands\n");

	hwc_data.read_prio
	    = ((mask & ET_PMsgCmd_Mask) == ET_PMsgCmd_Mask);
	if (hwc_data.read_prio)
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				       "can read priority commands\n");

	if (hwc_data.read_prio || hwc_data.read_nonprio) {
		return 0;
	} else {
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
				     "can not read commands from operator\n");
		return -1;
	}
}

static int 
eval_statechangebuf (statechangebuf_t * scbuf)
{
	int retval = 0;

	internal_print (
			       DELAYED_WRITE,
			       HWC_RW_PRINT_HEADER
			       "HWC state change detected\n");

	if (scbuf->validity_hwc_active_facility_mask) {

	}
	if (scbuf->validity_hwc_receive_mask) {

		if (scbuf->mask_length != 4) {
#ifdef DUMP_HWC_INIT_ERROR
			__asm__ ("LHI 1,0xe50\n\t"
				 "LRA 2,0(%0)\n\t"
				 "J .+0 \n\t"
		      :
		      :	 "a" (scbuf)
		      :	 "1", "2");
#endif
		} else {

			retval += eval_hwc_receive_mask
			    (scbuf->hwc_receive_mask);
		}
	}
	if (scbuf->validity_hwc_send_mask) {

		if (scbuf->mask_length != 4) {
#ifdef DUMP_HWC_INIT_ERROR
			__asm__ ("LHI 1,0xe51\n\t"
				 "LRA 2,0(%0)\n\t"
				 "J .+0 \n\t"
		      :
		      :	 "a" (scbuf)
		      :	 "1", "2");
#endif
		} else {

			retval += eval_hwc_send_mask
			    (scbuf->hwc_send_mask);
		}
	}
	if (scbuf->validity_read_data_function_mask) {

	}
	return retval;
}

static int 
process_evbufs (void *start, void *end)
{
	int retval = 0;
	evbuf_t *evbuf;
	void *evbuf_end;
	gds_vector_t *evbuf_data;

	evbuf = (evbuf_t *) start;
	while (((void *) evbuf) < end) {
		evbuf_data = (gds_vector_t *)
		    (((unsigned long) evbuf) + sizeof (evbuf_t));
		evbuf_end = (void *) (((unsigned long) evbuf) + evbuf->length);
		switch (evbuf->type) {
		case ET_OpCmd:
		case ET_CntlProgOpCmd:
		case ET_PMsgCmd:
#ifdef DUMP_HWCB_INPUT

			internal_print (
					       DELAYED_WRITE,
					       HWC_RW_PRINT_HEADER
					       "event buffer "
					   "at 0x%x up to 0x%x, length: %d\n",
					       (unsigned long) evbuf,
					       (unsigned long) (evbuf_end - 1),
					       evbuf->length);
			dump_storage_area ((void *) evbuf, evbuf->length);
#endif
			retval += eval_evbuf (evbuf_data, evbuf_end);
			break;
		case ET_StateChange:
			retval += eval_statechangebuf
			    ((statechangebuf_t *) evbuf);
			break;
		default:
			internal_print (
					       DELAYED_WRITE,
					       HWC_RW_PRINT_HEADER
					       "unconditional read: "
					       "unknown event buffer found, "
					       "type 0x%x",
					       evbuf->type);
			retval = -ENOSYS;
		}
		evbuf = (evbuf_t *) evbuf_end;
	}
	return retval;
}

static int 
unconditional_read_1 (void)
{
	unsigned short int condition_code;
	read_hwcb_t *hwcb = (read_hwcb_t *) hwc_data.page;
	int retval;

#if 0

	if ((!hwc_data.read_prio) && (!hwc_data.read_nonprio))
		return -EOPNOTSUPP;

	if (hwc_data.current_servc)
		return -EBUSY;
#endif

	memset (hwcb, 0x00, PAGE_SIZE);
	memcpy (hwcb, &read_hwcb_template, sizeof (read_hwcb_t));

	condition_code = service_call (HWC_CMDW_READDATA, hwc_data.page);

#ifdef DUMP_HWC_READ_ERROR
	if (condition_code == HWC_NOT_OPERATIONAL)
		__asm__ ("LHI 1,0xe40\n\t"
			 "L 2,0(%0)\n\t"
			 "LRA 3,0(%1)\n\t"
			 "J .+0 \n\t"
	      :
	      :	 "a" (&condition_code), "a" (hwc_data.page)
	      :	 "1", "2", "3");
#endif

	switch (condition_code) {
	case HWC_COMMAND_INITIATED:
		hwc_data.current_servc = HWC_CMDW_READDATA;
		hwc_data.current_hwcb = hwc_data.page;
		retval = condition_code;
		break;
	case HWC_BUSY:
		retval = -EBUSY;
		break;
	default:
		retval = -EIO;
	}

	return retval;
}

static int 
unconditional_read_2 (u32 ext_int_param)
{
	read_hwcb_t *hwcb = (read_hwcb_t *) hwc_data.page;

#ifdef DUMP_HWC_READ_ERROR
	if ((hwcb->response_code != 0x0020) &&
	    (hwcb->response_code != 0x0220) &&
	    (hwcb->response_code != 0x60F0) &&
	    (hwcb->response_code != 0x62F0))
		__asm__ ("LHI 1,0xe41\n\t"
			 "LRA 2,0(%0)\n\t"
			 "L 3,0(%1)\n\t"
			 "J .+0\n\t"
	      :
	      :	 "a" (hwc_data.page), "a" (&(hwcb->response_code))
	      :	 "1", "2", "3");
#endif

	hwc_data.current_servc = 0;
	hwc_data.current_hwcb = NULL;

	switch (hwcb->response_code) {

	case 0x0020:
	case 0x0220:
		return process_evbufs (
		     (void *) (((unsigned long) hwcb) + sizeof (read_hwcb_t)),
			    (void *) (((unsigned long) hwcb) + hwcb->length));

	case 0x60F0:
	case 0x62F0:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
				       "unconditional read: "
				     "got interrupt and tried to read input, "
				  "but nothing found (response code=0x%x).\n",
				       hwcb->response_code);
		return 0;

	case 0x0100:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
			 "unconditional read: HWCB boundary violation - this "
			 "must not occur in a correct driver, please contact "
				       "author\n");
		return -EIO;

	case 0x0300:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
				       "unconditional read: "
			"insufficient HWCB length - this must not occur in a "
				   "correct driver, please contact author\n");
		return -EIO;

	case 0x01F0:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
				       "unconditional read: "
			 "invalid command - this must not occur in a correct "
				       "driver, please contact author\n");
		return -EIO;

	case 0x40F0:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
			       "unconditional read: invalid function code\n");
		return -EIO;

	case 0x70F0:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
			      "unconditional read: invalid selection mask\n");
		return -EIO;

	case 0x0040:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
				 "unconditional read: HWC equipment check\n");
		return -EIO;

	default:
		internal_print (
				       IMMEDIATE_WRITE,
				       HWC_RW_PRINT_HEADER
			"unconditional read: invalid response code %x - this "
			 "must not occur in a correct driver, please contact "
				       "author\n",
				       hwcb->response_code);
		return -EIO;
	}
}

static int 
write_event_mask_1 (void)
{
	unsigned int condition_code;
	int retval;

	condition_code = service_call (HWC_CMDW_WRITEMASK, hwc_data.page);

#ifdef DUMP_HWC_INIT_ERROR

	if (condition_code == HWC_NOT_OPERATIONAL)
		__asm__ ("LHI 1,0xe10\n\t"
			 "L 2,0(%0)\n\t"
			 "LRA 3,0(%1)\n\t"
			 "J .+0\n\t"
	      :
	      :	 "a" (&condition_code), "a" (hwc_data.page)
	      :	 "1", "2", "3");
#endif

	switch (condition_code) {
	case HWC_COMMAND_INITIATED:
		hwc_data.current_servc = HWC_CMDW_WRITEMASK;
		hwc_data.current_hwcb = hwc_data.page;
		retval = condition_code;
		break;
	case HWC_BUSY:
		retval = -EBUSY;
		break;
	default:
		retval = -EIO;
	}

	return retval;
}

static int 
write_event_mask_2 (u32 ext_int_param)
{
	init_hwcb_t *hwcb = (init_hwcb_t *) hwc_data.page;
	int retval = 0;

	if (hwcb->response_code != 0x0020) {
#ifdef DUMP_HWC_INIT_ERROR
		__asm__ ("LHI 1,0xe11\n\t"
			 "LRA 2,0(%0)\n\t"
			 "L 3,0(%1)\n\t"
			 "J .+0\n\t"
	      :
	      :	 "a" (hwcb), "a" (&(hwcb->response_code))
	      :	 "1", "2", "3");
#else
		retval = -1;
#endif
	} else {
		if (hwcb->mask_length != 4) {
#ifdef DUMP_HWC_INIT_ERROR
			__asm__ ("LHI 1,0xe52\n\t"
				 "LRA 2,0(%0)\n\t"
				 "J .+0 \n\t"
		      :
		      :	 "a" (hwcb)
		      :	 "1", "2");
#endif
		} else {
			retval += eval_hwc_receive_mask
			    (hwcb->hwc_receive_mask);
			retval += eval_hwc_send_mask (hwcb->hwc_send_mask);
		}
	}

	hwc_data.current_servc = 0;
	hwc_data.current_hwcb = NULL;

	return retval;
}

static int 
set_hwc_ioctls (hwc_ioctls_t * ioctls, char correct)
{
	int retval = 0;
	hwc_ioctls_t tmp;

	if (ioctls->width_htab > MAX_MESSAGE_SIZE) {
		if (correct)
			tmp.width_htab = MAX_MESSAGE_SIZE;
		else
			retval = -EINVAL;
	} else
		tmp.width_htab = ioctls->width_htab;

	tmp.echo = ioctls->echo;

	if (ioctls->columns > MAX_MESSAGE_SIZE) {
		if (correct)
			tmp.columns = MAX_MESSAGE_SIZE;
		else
			retval = -EINVAL;
	} else
		tmp.columns = ioctls->columns;

	tmp.final_nl = ioctls->final_nl;

	if (ioctls->max_hwcb < 2) {
		if (correct)
			tmp.max_hwcb = 2;
		else
			retval = -EINVAL;
	} else
		tmp.max_hwcb = ioctls->max_hwcb;

	tmp.tolower = ioctls->tolower;

	if (ioctls->kmem_hwcb > ioctls->max_hwcb) {
		if (correct)
			tmp.kmem_hwcb = ioctls->max_hwcb;
		else
			retval = -EINVAL;
	} else
		tmp.kmem_hwcb = ioctls->kmem_hwcb;

	if (ioctls->kmem_hwcb > MAX_KMEM_PAGES) {
		if (correct)
			ioctls->kmem_hwcb = MAX_KMEM_PAGES;
		else
			retval = -EINVAL;
	}
	if (ioctls->kmem_hwcb < 2) {
		if (correct)
			ioctls->kmem_hwcb = 2;
		else
			retval = -EINVAL;
	}
	tmp.delim = ioctls->delim;

	tmp.measured_lines = ioctls->measured_lines;
	tmp.measured_chars = ioctls->measured_chars;
	tmp.measured_wcalls = ioctls->measured_wcalls;

	if (!(retval < 0))
		hwc_data.ioctls = tmp;

	return retval;
}

int 
do_hwc_init (void)
{
	int retval;

	memcpy (hwc_data.page, &init_hwcb_template, sizeof (init_hwcb_t));

	do {

		retval = write_event_mask_1 ();

		if (retval == -EBUSY) {

			hwc_data.flags |= HWC_INIT;

			asm volatile ("STCTL 0,0,%0":"=m" (cr0));
			cr0_save = cr0;
			cr0 |= 0x00000200;
			cr0 &= 0xFFFFF3AC;
			asm volatile ("LCTL 0,0,%0"::"m" (cr0):"memory");

			asm volatile ("STOSM %0,0x01"
				      :"=m" (psw_mask)::"memory");

			while (!(hwc_data.flags & HWC_INTERRUPT))
				barrier ();

			asm volatile ("STNSM %0,0xFE"
				      :"=m" (psw_mask)::"memory");

			asm volatile ("LCTL 0,0,%0"
				      ::"m" (cr0_save):"memory");

			hwc_data.flags &= ~HWC_INIT;
		}
	} while (retval == -EBUSY);

	if (retval == -EIO) {
		hwc_data.flags |= HWC_BROKEN;
		printk (HWC_RW_PRINT_HEADER "HWC not operational\n");
	}
	return retval;
}

void hwc_interrupt_handler (struct pt_regs *regs, __u16 code);

int 
hwc_init (unsigned long *kmem_start)
{
	int retval;

#ifdef BUFFER_STRESS_TEST

	init_hwcb_t *hwcb;
	int i;

#endif

	if (register_external_interrupt (0x2401, hwc_interrupt_handler) != 0)
		panic ("Couldn't request external interrupts 0x2401");

	spin_lock_init (&hwc_data.lock);

#ifdef USE_VM_DETECTION

	if (MACHINE_IS_VM) {

		if (hwc_data.init_ioctls.columns > 76)
			hwc_data.init_ioctls.columns = 76;
		hwc_data.init_ioctls.tolower = 1;
		if (!hwc_data.init_ioctls.delim)
			hwc_data.init_ioctls.delim = DEFAULT_CASE_DELIMITER;
	} else {
		hwc_data.init_ioctls.tolower = 0;
		hwc_data.init_ioctls.delim = 0;
	}
#endif
	retval = set_hwc_ioctls (&hwc_data.init_ioctls, 1);

	*kmem_start = (*kmem_start + PAGE_SIZE - 1) & -4096L;
	hwc_data.kmem_start = *kmem_start;
	*kmem_start += hwc_data.ioctls.kmem_hwcb * PAGE_SIZE;
	hwc_data.kmem_end = *kmem_start - 1;

	retval = do_hwc_init ();

	ctl_set_bit (0, 9);

#ifdef BUFFER_STRESS_TEST

	internal_print (
			       DELAYED_WRITE,
			       HWC_RW_PRINT_HEADER
			       "use %i bytes for buffering.\n",
			       hwc_data.ioctls.kmem_hwcb * PAGE_SIZE);
	for (i = 0; i < 2000; i++) {
		hwcb = (init_hwcb_t *) BUF_HWCB;
		internal_print (
				       DELAYED_WRITE,
				       HWC_RW_PRINT_HEADER
			  "This is stress test message #%i, free: %i bytes\n",
				       i,
			     MAX_HWCB_ROOM - (hwcb->length + sizeof (mto_t)));
	}

#endif

	return /*retval */ 0;
}

signed int 
hwc_register_calls (hwc_high_level_calls_t * calls)
{
	if (calls == NULL)
		return -EINVAL;

	if (hwc_data.calls != NULL)
		return -EBUSY;

	hwc_data.calls = calls;
	return 0;
}

signed int 
hwc_unregister_calls (hwc_high_level_calls_t * calls)
{
	if (hwc_data.calls == NULL)
		return -EINVAL;

	if (calls != hwc_data.calls)
		return -EINVAL;

	hwc_data.calls = NULL;
	return 0;
}

void 
hwc_do_interrupt (u32 ext_int_param)
{
	u32 finished_hwcb = ext_int_param & HWC_EXT_INT_PARAM_ADDR;
	u32 evbuf_pending = ext_int_param & HWC_EXT_INT_PARAM_PEND;

	if (hwc_data.flags & HWC_PTIMER_RUNS) {
		del_timer (&hwc_data.poll_timer);
		hwc_data.flags &= ~HWC_PTIMER_RUNS;
	}
	if (finished_hwcb) {

		if ((unsigned long) hwc_data.current_hwcb != finished_hwcb) {
			internal_print (
					       DELAYED_WRITE,
					       HWC_RW_PRINT_HEADER
					       "interrupt: mismatch: "
					       "ext. int param. (0x%x) vs. "
					       "current HWCB (0x%x)\n",
					       ext_int_param,
					       hwc_data.current_hwcb);
		} else {

			switch (hwc_data.current_servc) {

			case HWC_CMDW_WRITEMASK:

				write_event_mask_2 (ext_int_param);
				break;

			case HWC_CMDW_WRITEDATA:

				write_event_data_2 (ext_int_param);
				break;

			case HWC_CMDW_READDATA:

				unconditional_read_2 (ext_int_param);
				break;
			default:
			}
		}
	} else {

		if (hwc_data.current_hwcb) {
			internal_print (
					       DELAYED_WRITE,
					       HWC_RW_PRINT_HEADER
					       "interrupt: mismatch: "
					       "ext. int. param. (0x%x) vs. "
					       "current HWCB (0x%x)\n",
					       ext_int_param,
					       hwc_data.current_hwcb);
		}
	}

	if (evbuf_pending) {

		unconditional_read_1 ();
	} else {

		write_event_data_1 ();
	}

	if (!hwc_data.calls || !hwc_data.calls->wake_up)
		return;
	(hwc_data.calls->wake_up) ();
}

void 
hwc_interrupt_handler (struct pt_regs *regs, __u16 code)
{
	u32 ext_int_param = hwc_ext_int_param ();
	int cpu = smp_processor_id ();

	irq_enter (cpu, 0x2401);

	if (hwc_data.flags & HWC_INIT) {

		hwc_data.flags |= HWC_INTERRUPT;
	} else if (hwc_data.flags & HWC_BROKEN) {

		if (!do_hwc_init ()) {
			hwc_data.flags &= ~HWC_BROKEN;
			internal_print (DELAYED_WRITE,
					HWC_RW_PRINT_HEADER
					"delayed HWC setup after"
					" temporary breakdown"
					" (ext. int. parameter=0x%x)\n",
					ext_int_param);
		}
	} else {
		spin_lock (&hwc_data.lock);
		hwc_do_interrupt (ext_int_param);
		spin_unlock (&hwc_data.lock);
	}
	irq_exit (cpu, 0x2401);
}

void 
hwc_unblank (void)
{

	spin_lock (&hwc_data.lock);
	spin_unlock (&hwc_data.lock);

	asm volatile ("STCTL 0,0,%0":"=m" (cr0));
	cr0_save = cr0;
	cr0 |= 0x00000200;
	cr0 &= 0xFFFFF3AC;
	asm volatile ("LCTL 0,0,%0"::"m" (cr0):"memory");

	asm volatile ("STOSM %0,0x01":"=m" (psw_mask)::"memory");

	while (ALL_HWCB_CHAR)
		barrier ();

	asm volatile ("STNSM %0,0xFE":"=m" (psw_mask)::"memory");

	asm volatile ("LCTL 0,0,%0"::"m" (cr0_save):"memory");
}

int 
hwc_ioctl (unsigned int cmd, unsigned long arg)
{
	hwc_ioctls_t tmp = hwc_data.ioctls;
	int retval = 0;
	unsigned long flags;
	unsigned int obuf;

	spin_lock_irqsave (&hwc_data.lock, flags);

	switch (cmd) {

	case TIOCHWCSMEAS:
		hwc_data.ioctls.measured_lines = 0;
		hwc_data.ioctls.measured_chars = 0;
		hwc_data.ioctls.measured_wcalls = 0;
		break;

	case TIOCHWCSHTAB:
		if (get_user (tmp.width_htab, (ioctl_htab_t *) arg))
			goto fault;
		break;

	case TIOCHWCSECHO:
		if (get_user (tmp.echo, (ioctl_echo_t *) arg))
			goto fault;
		break;

	case TIOCHWCSCOLS:
		if (get_user (tmp.columns, (ioctl_cols_t *) arg))
			goto fault;
		break;

	case TIOCHWCSNL:
		if (get_user (tmp.final_nl, (ioctl_nl_t *) arg))
			goto fault;
		break;

	case TIOCHWCSOBUF:
		if (get_user (obuf, (unsigned int *) arg))
			goto fault;
		if (obuf & 0xFFF)
			tmp.max_hwcb = (((obuf | 0xFFF) + 1) >> 12);
		else
			tmp.max_hwcb = (obuf >> 12);
		break;

	case TIOCHWCSCASE:
		if (get_user (tmp.tolower, (ioctl_case_t *) arg))
			goto fault;
		break;

	case TIOCHWCSDELIM:
		if (get_user (tmp.delim, (ioctl_delim_t *) arg))
			goto fault;
		break;

	case TIOCHWCSINIT:
		retval = set_hwc_ioctls (&hwc_data.init_ioctls, 1);
		break;

	case TIOCHWCGMEASL:
		if (put_user (tmp.measured_lines, (ioctl_meas_t *) arg))
			goto fault;
		break;

	case TIOCHWCGMEASC:
		if (put_user (tmp.measured_chars, (ioctl_meas_t *) arg))
			goto fault;
		break;

	case TIOCHWCGMEASS:
		if (put_user (tmp.measured_wcalls, (ioctl_meas_t *) arg))
			goto fault;
		break;

	case TIOCHWCGHTAB:
		if (put_user (tmp.width_htab, (ioctl_htab_t *) arg))
			goto fault;
		break;

	case TIOCHWCGECHO:
		if (put_user (tmp.echo, (ioctl_echo_t *) arg))
			goto fault;
		break;

	case TIOCHWCGCOLS:
		if (put_user (tmp.columns, (ioctl_cols_t *) arg))
			goto fault;
		break;

	case TIOCHWCGNL:
		if (put_user (tmp.final_nl, (ioctl_nl_t *) arg))
			goto fault;
		break;

	case TIOCHWCGOBUF:
		if (put_user (tmp.max_hwcb, (ioctl_obuf_t *) arg))
			goto fault;
		break;

	case TIOCHWCGKBUF:
		if (put_user (tmp.kmem_hwcb, (ioctl_obuf_t *) arg))
			goto fault;
		break;

	case TIOCHWCGCASE:
		if (put_user (tmp.tolower, (ioctl_case_t *) arg))
			goto fault;
		break;

	case TIOCHWCGDELIM:
		if (put_user (tmp.delim, (ioctl_delim_t *) arg))
			goto fault;
		break;
#if 0

	case TIOCHWCGINIT:
		if (put_user (&hwc_data.init_ioctls, (hwc_ioctls_t *) arg))
			goto fault;
		break;

	case TIOCHWCGCURR:
		if (put_user (&hwc_data.ioctls, (hwc_ioctls_t *) arg))
			goto fault;
		break;
#endif

	default:
		goto noioctlcmd;
	}

	if (_IOC_DIR (cmd) == _IOC_WRITE)
		retval = set_hwc_ioctls (&tmp, 0);

	goto out;

      fault:
	retval = -EFAULT;
	goto out;
      noioctlcmd:
	retval = -ENOIOCTLCMD;
      out:
	spin_unlock_irqrestore (&hwc_data.lock, flags);
	return retval;
}
