/*	$OpenBSD: hilkbd.c,v 1.13 2006/08/10 23:43:45 miod Exp $	*/
/*
 * Copyright (c) 2003, Miodrag Vallat.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/ioctl.h>
#include <sys/kernel.h>
#include <sys/timeout.h>

#include <machine/autoconf.h>
#include <machine/bus.h>
#include <machine/cpu.h>

#include <dev/hil/hilreg.h>
#include <dev/hil/hilvar.h>
#include <dev/hil/hildevs.h>

#include <dev/wscons/wsconsio.h>
#include <dev/wscons/wskbdvar.h>
#include <dev/wscons/wsksymdef.h>
#include <dev/wscons/wsksymvar.h>
#ifdef WSDISPLAY_COMPAT_RAWKBD
#include <dev/wscons/wskbdraw.h>
#endif

#include <dev/hil/hilkbdmap.h>

struct hilkbd_softc {
	struct hildev_softc sc_hildev;

	int		sc_numleds;
	int		sc_ledstate;
	int		sc_enabled;
	int		sc_console;
	int		sc_lastarrow;

	struct device	*sc_wskbddev;

#ifdef WSDISPLAY_COMPAT_RAWKBD
	int		sc_rawkbd;
	int		sc_nrep;
	char		sc_rep[HILBUFSIZE * 2];
	struct timeout	sc_rawrepeat_ch;
#define	REP_DELAY1	400
#define	REP_DELAYN	100
#endif
};

int	hilkbdprobe(struct device *, void *, void *);
void	hilkbdattach(struct device *, struct device *, void *);
int	hilkbddetach(struct device *, int);

struct cfdriver hilkbd_cd = {
	NULL, "hilkbd", DV_DULL
};

struct cfattach hilkbd_ca = {
	sizeof(struct hilkbd_softc), hilkbdprobe, hilkbdattach, hilkbddetach,
};

int	hilkbd_enable(void *, int);
void	hilkbd_set_leds(void *, int);
int	hilkbd_ioctl(void *, u_long, caddr_t, int, struct proc *);

const struct wskbd_accessops hilkbd_accessops = {
	hilkbd_enable,
	hilkbd_set_leds,
	hilkbd_ioctl,
};

void	hilkbd_cngetc(void *, u_int *, int *);
void	hilkbd_cnpollc(void *, int);
void	hilkbd_cnbell(void *, u_int, u_int, u_int);

const struct wskbd_consops hilkbd_consops = {
	hilkbd_cngetc,
	hilkbd_cnpollc,
	hilkbd_cnbell,
};

struct wskbd_mapdata hilkbd_keymapdata = {
	hilkbd_keydesctab,
#ifdef HILKBD_LAYOUT
	HILKBD_LAYOUT,
#else
	KB_US,
#endif
};

struct wskbd_mapdata hilkbd_keymapdata_ps2 = {
	hilkbd_keydesctab_ps2,
#ifdef HILKBD_LAYOUT
	HILKBD_LAYOUT,
#else
	KB_US,
#endif
};

void	hilkbd_bell(struct hil_softc *, u_int, u_int, u_int);
void	hilkbd_callback(struct hildev_softc *, u_int, u_int8_t *);
void	hilkbd_decode(struct hilkbd_softc *, u_int8_t, u_int *, int *, int);
int	hilkbd_is_console(int);
void	hilkbd_rawrepeat(void *);

int	seen_hilkbd_console;

int
hilkbdprobe(struct device *parent, void *match, void *aux)
{
	struct hil_attach_args *ha = aux;

	if (ha->ha_type != HIL_DEVICE_KEYBOARD &&
	    ha->ha_type != HIL_DEVICE_BUTTONBOX)
		return (0);

	return (1);
}

void
hilkbdattach(struct device *parent, struct device *self, void *aux)
{
	struct hilkbd_softc *sc = (void *)self;
	struct hil_attach_args *ha = aux;
	struct wskbddev_attach_args a;
	u_int8_t layoutcode;
	int ps2;

	sc->hd_code = ha->ha_code;
	sc->hd_type = ha->ha_type;
	sc->hd_infolen = ha->ha_infolen;
	bcopy(ha->ha_info, sc->hd_info, ha->ha_infolen);
	sc->hd_fn = hilkbd_callback;

	if (ha->ha_type == HIL_DEVICE_KEYBOARD) {
		/*
		 * Determine the keyboard language configuration, but don't
		 * override a user-specified setting.
		 */
		layoutcode = ha->ha_id & (MAXHILKBDLAYOUT - 1);
#ifndef HILKBD_LAYOUT
		if (layoutcode < MAXHILKBDLAYOUT &&
		    hilkbd_layouts[layoutcode] != -1)
			hilkbd_keymapdata.layout =
			hilkbd_keymapdata_ps2.layout =
			    hilkbd_layouts[layoutcode];
#endif

		printf(", layout %x", layoutcode);
	}

	/*
	 * Interpret the identification bytes, if any
	 */
	if (ha->ha_infolen > 2 && (ha->ha_info[1] & HIL_IOB) != 0) {
		/* HILIOB_PROMPT is not always reported... */
		sc->sc_numleds = (ha->ha_info[2] & HILIOB_PMASK) >> 4;
		if (sc->sc_numleds != 0)
			printf(", %d leds", sc->sc_numleds);
	}

	printf("\n");

	/*
	 * Red lettered keyboards come in two flavours, the old one
	 * with only one control key, to the left of the escape key,
	 * and the modern one which has a PS/2 like layout, and leds.
	 *
	 * Unfortunately for us, they use the same device ID range.
	 * We'll differentiate them by looking at the leds property.
	 */
	ps2 = (sc->sc_numleds != 0);

#ifdef WSDISPLAY_COMPAT_RAWKBD
	timeout_set(&sc->sc_rawrepeat_ch, hilkbd_rawrepeat, sc);
#endif

	/* Do not consider button boxes as console devices. */
	if (ha->ha_type == HIL_DEVICE_BUTTONBOX)
		a.console = 0;
	else
		a.console = hilkbd_is_console(ha->ha_console);
	a.keymap = ps2 ? &hilkbd_keymapdata_ps2 : &hilkbd_keymapdata;
	a.accessops = &hilkbd_accessops;
	a.accesscookie = sc;

	if (a.console) {
		sc->sc_console = sc->sc_enabled = 1;
		wskbd_cnattach(&hilkbd_consops, sc, a.keymap);
	} else {
		sc->sc_console = sc->sc_enabled = 0;
	}

	sc->sc_wskbddev = config_found(self, &a, wskbddevprint);

	/*
	 * If this is an old keyboard with a numeric pad but no ``num lock''
	 * key, simulate it being pressed so that the keyboard runs in
	 * numeric mode.
	 */
	if (!ps2 && sc->sc_wskbddev != NULL) {
		wskbd_input(sc->sc_wskbddev, WSCONS_EVENT_KEY_DOWN, 80);
		wskbd_input(sc->sc_wskbddev, WSCONS_EVENT_KEY_UP, 80);
	}
}

int
hilkbddetach(struct device *self, int flags)
{
	struct hilkbd_softc *sc = (void *)self;

	/*
	 * Handle console keyboard for the best. It should have been set
	 * as the first device in the loop anyways.
	 */
	if (sc->sc_console) {
		wskbd_cndetach();
		seen_hilkbd_console = 0;
	}

	if (sc->sc_wskbddev != NULL)
		return config_detach(sc->sc_wskbddev, flags);

	return (0);
}

int
hilkbd_enable(void *v, int on)
{
	struct hilkbd_softc *sc = v;

	if (on) {
		if (sc->sc_enabled)
			return (EBUSY);
	} else {
		if (sc->sc_console)
			return (EBUSY);
	}

	sc->sc_enabled = on;

	return (0);
}

void
hilkbd_set_leds(void *v, int leds)
{
	struct hilkbd_softc *sc = v;
	int changemask;

	if (sc->sc_numleds == 0)
		return;

	changemask = leds ^ sc->sc_ledstate;
	if (changemask == 0)
		return;

	/* We do not handle more than 3 leds here */
	if (changemask & WSKBD_LED_SCROLL)
		send_hildev_cmd((struct hildev_softc *)sc,
		    (leds & WSKBD_LED_SCROLL) ? HIL_PROMPT1 : HIL_ACK1,
		    NULL, NULL);
	if (changemask & WSKBD_LED_NUM)
		send_hildev_cmd((struct hildev_softc *)sc,
		    (leds & WSKBD_LED_NUM) ? HIL_PROMPT2 : HIL_ACK2,
		    NULL, NULL);
	if (changemask & WSKBD_LED_CAPS)
		send_hildev_cmd((struct hildev_softc *)sc,
		    (leds & WSKBD_LED_CAPS) ? HIL_PROMPT3 : HIL_ACK3,
		    NULL, NULL);

	sc->sc_ledstate = leds;
}

int
hilkbd_ioctl(void *v, u_long cmd, caddr_t data, int flag, struct proc *p)
{
	struct hilkbd_softc *sc = v;

	switch (cmd) {
	case WSKBDIO_GTYPE:
		*(int *)data = WSKBD_TYPE_HIL;
		return 0;
	case WSKBDIO_SETLEDS:
		hilkbd_set_leds(v, *(int *)data);
		return 0;
	case WSKBDIO_GETLEDS:
		*(int *)data = sc->sc_ledstate;
		return 0;
#ifdef WSDISPLAY_COMPAT_RAWKBD
	case WSKBDIO_SETMODE:
		sc->sc_rawkbd = *(int *)data == WSKBD_RAW;
		timeout_del(&sc->sc_rawrepeat_ch);
		return 0;
#endif
	case WSKBDIO_COMPLEXBELL:
#define	d ((struct wskbd_bell_data *)data)
		hilkbd_bell((struct hil_softc *)sc->hd_parent,
		    d->pitch, d->period, d->volume);
#undef d
		return 0;
	}

	return -1;
}

void
hilkbd_cngetc(void *v, u_int *type, int *data)
{
	struct hilkbd_softc *sc = v;
	u_int8_t c, stat;

	for (;;) {
		while (hil_poll_data((struct hildev_softc *)sc, &stat, &c) != 0)
			;

		/*
		 * Disregard keyboard data packet header.
		 * Note that no key generates it, so we're safe.
		 */
		if (c != HIL_KBDBUTTON)
			break;
	}

	hilkbd_decode(sc, c, type, data, HIL_KBDBUTTON);
}

void
hilkbd_cnpollc(void *v, int on)
{
	struct hilkbd_softc *sc = v;

	hil_set_poll((struct hil_softc *)sc->hd_parent, on);
}

void
hilkbd_cnbell(void *v, u_int pitch, u_int period, u_int volume)
{
	struct hilkbd_softc *sc = v;

	hilkbd_bell((struct hil_softc *)sc->hd_parent,
	    pitch, period, volume);
}

void
hilkbd_bell(struct hil_softc *sc, u_int pitch, u_int period, u_int volume)
{
	u_int8_t buf[2];

	/* XXX there could be at least a pitch -> HIL pitch conversion here */
#define	BELLDUR		80	/* tone duration in msec (10-2560) */
#define	BELLFREQ	8	/* tone frequency (0-63) */
	buf[0] = ar_format(period - 10);
	buf[1] = BELLFREQ;
	send_hil_cmd(sc, HIL_SETTONE, buf, 2, NULL);
}

void
hilkbd_callback(struct hildev_softc *dev, u_int buflen, u_int8_t *buf)
{
	struct hilkbd_softc *sc = (struct hilkbd_softc *)dev;
	u_int type;
	int kbdtype, key;
	int i, s;

	/*
	 * Ignore packet if we don't need it
	 */
	if (sc->sc_enabled == 0)
		return;

	if (buflen == 0)
		return;
	switch ((kbdtype = *buf & HIL_KBDDATA)) {
	case HIL_BUTTONBOX:
	case HIL_KBDBUTTON:
		break;
	default:
		return;
	}

#ifdef WSDISPLAY_COMPAT_RAWKBD
	if (sc->sc_rawkbd) {
		u_char cbuf[HILBUFSIZE * 2];
		int c, j, npress;

		npress = j = 0;
		for (i = 1, buf++; i < buflen; i++) {
			hilkbd_decode(sc, *buf++, &type, &key, kbdtype);
			c = hilkbd_raw[key];
			if (c == RAWKEY_Null)
				continue;
			/* fake extended scancode if necessary */
			if (c & 0x80)
				cbuf[j++] = 0xe0;
			cbuf[j] = c & 0x7f;
			if (type == WSCONS_EVENT_KEY_UP)
				cbuf[j] |= 0x80;
			else {
				/* remember pressed keys for autorepeat */
				if (c & 0x80)
					sc->sc_rep[npress++] = 0xe0;
				sc->sc_rep[npress++] = c & 0x7f;
			}
			j++;
		}

		s = spltty();
		wskbd_rawinput(sc->sc_wskbddev, cbuf, j);
		splx(s);
		timeout_del(&sc->sc_rawrepeat_ch);
		sc->sc_nrep = npress;
		if (npress != 0) {
			timeout_add(&sc->sc_rawrepeat_ch,
			    (hz * REP_DELAY1) / 1000);
		}
	} else
#endif
	{
		s = spltty();
		for (i = 1, buf++; i < buflen; i++) {
			hilkbd_decode(sc, *buf++, &type, &key, kbdtype);
			if (sc->sc_wskbddev != NULL)
				wskbd_input(sc->sc_wskbddev, type, key);
		}
		splx(s);
	}
}

void
hilkbd_decode(struct hilkbd_softc *sc, u_int8_t data, u_int *type, int *key,
    int kbdtype)
{
	if (kbdtype == HIL_BUTTONBOX) {
		if (data == 0x02)	/* repeat arrow */
			data = sc->sc_lastarrow;
		else if (data >= 0xf8)
			sc->sc_lastarrow = data;
	}

	*type = (data & 1) ? WSCONS_EVENT_KEY_UP : WSCONS_EVENT_KEY_DOWN;
	*key = data >> 1;
}

int
hilkbd_is_console(int hil_is_console)
{
	/* if not first hil keyboard, then not the console */
	if (seen_hilkbd_console)
		return (0);

	/* if PDC console does not match hil bus path, then not the console */
	if (hil_is_console == 0)
		return (0);

	seen_hilkbd_console = 1;
	return (1);
}

#ifdef WSDISPLAY_COMPAT_RAWKBD
void
hilkbd_rawrepeat(void *v)
{
	struct hilkbd_softc *sc = v;
	int s;

	s = spltty();
	wskbd_rawinput(sc->sc_wskbddev, sc->sc_rep, sc->sc_nrep);
	splx(s);
	timeout_add(&sc->sc_rawrepeat_ch, (hz * REP_DELAYN) / 1000);
}
#endif