/*
 * Copyright 1990,91 by Thomas Roell, Dinkelscherben, Germany
 * Copyright 1993 by David Dawes <dawes@XFree86.org>
 * Copyright 1999 by David Holland <davidh@iquest.net)
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that copyright
 * notice and this permission notice appear in supporting documentation, and
 * that the names of Thomas Roell, David Dawes, and David Holland not be used
 * in advertising or publicity pertaining to distribution of the software
 * without specific, written prior permission.  Thomas Roell, David Dawes, and
 * David Holland make no representations about the suitability of this software
 * for any purpose.  It is provided "as is" without express or implied
 * warranty.
 *
 * THOMAS ROELL, DAVID DAWES, AND DAVID HOLLAND DISCLAIM ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS.  IN NO EVENT SHALL THOMAS ROELL, DAVID DAWES, OR DAVID HOLLAND
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
/*
 * Copyright (c) 2004-2009, Oracle and/or its affiliates. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <xorg-server.h>
#include "xf86.h"
#include "xf86Priv.h"
#include "xf86_OSlib.h"
#include "xf86OSKbd.h"
#include "sun_kbd.h"

#include <sys/stropts.h>
#include <sys/vuid_event.h>
#include <sys/kbd.h>
#include <sys/note.h>	/* needed before including older versions of hid.h */
#include <sys/usb/clients/hid/hid.h>

static int KbdOn(InputInfoPtr pInfo, int what);
static Bool OpenKeyboard(InputInfoPtr pInfo);
static void CloseKeyboard(InputInfoPtr pInfo);

static void
sunKbdSetLeds(InputInfoPtr pInfo, int leds)
{
    int i;
    uchar_t setleds = (uchar_t) (leds & 0xFF);

    SYSCALL(i = ioctl(pInfo->fd, KIOCSLED, &setleds));
    if (i < 0) {
	xf86Msg(X_ERROR, "%s: Failed to set keyboard LED's: %s\n",
                pInfo->name, strerror(errno));
    }
}


static int
sunKbdGetLeds(InputInfoPtr pInfo)
{
    int i;
    uchar_t leds = 0;

    SYSCALL(i = ioctl(pInfo->fd, KIOCGLED, &leds));
    if (i < 0) {
        xf86Msg(X_ERROR, "%s: Failed to get keyboard LED's: %s\n",
                pInfo->name, strerror(errno));
    }
    return (int) leds;
}


/*
 * Save initial keyboard state.  This is called at the start of each server
 * generation.
 */
static int
KbdInit(InputInfoPtr pInfo, int what)
{
    KbdDevPtr pKbd = (KbdDevPtr) pInfo->private;
    sunKbdPrivPtr priv = (sunKbdPrivPtr) pKbd->private;
    pointer options = pInfo->options;
    
    int	ktype, klayout, i;
    const char *ktype_name;

    priv->kbdActive	= FALSE;
    priv->otranslation 	= -1;
    priv->odirect 	= -1;

    if (options != NULL) {
	priv->strmod = xf86SetStrOption(options, "StreamsModule", NULL);
    } else {
	priv->strmod 		= NULL;
    }

    i = KbdOn(pInfo, DEVICE_INIT);
    if (i != Success) {
	return i;
    }

    SYSCALL(i = ioctl(pInfo->fd, KIOCTYPE, &ktype));
    if (i < 0) {
	xf86Msg(X_ERROR, "%s: Unable to determine keyboard type: %s\n", 
		pInfo->name, strerror(errno));
	return BadImplementation;
    }
    
    SYSCALL(i = ioctl(pInfo->fd, KIOCLAYOUT, &klayout));
    if (i < 0) {	
	xf86Msg(X_ERROR, "%s: Unable to determine keyboard layout: %s\n", 
		pInfo->name, strerror(errno));
	return BadImplementation;
    }
    
    switch (ktype) {
    case KB_SUN3:
	ktype_name = "Sun Type 3"; break;
    case KB_SUN4:
	ktype_name = "Sun Type 4/5/6"; break;
    case KB_USB:
	ktype_name = "USB"; break;
    case KB_PC:
	ktype_name = "PC"; break;
    default:
	ktype_name = "Unknown"; break;
    }

    xf86Msg(X_PROBED, "%s: Keyboard type: %s (%d)\n",
	    pInfo->name, ktype_name, ktype);
    xf86Msg(X_PROBED, "%s: Keyboard layout: %d\n", pInfo->name, klayout);

    priv->ktype 	= ktype;

    return Success;
}


static int
KbdOn(InputInfoPtr pInfo, int what)
{
    KbdDevPtr pKbd = (KbdDevPtr) pInfo->private;
    sunKbdPrivPtr priv = (sunKbdPrivPtr) pKbd->private;

    int	ktrans, kdirect, i;
    int io_get_direct = KIOCGDIRECT;
    int io_set_direct = KIOCSDIRECT;

    if (priv->kbdActive) {
	return Success;
    }

    if (pInfo->fd == -1) {
	if (!OpenKeyboard(pInfo)) {
	    return BadImplementation;
	}
    }

    if (priv->strmod) {
	/* Check to see if module is already pushed */
	SYSCALL(i = ioctl(pInfo->fd, I_FIND, priv->strmod));

	if (i == 0) { /* Not already pushed */
	    SYSCALL(i = ioctl(pInfo->fd, I_PUSH, priv->strmod));
	    if (i < 0) {
		xf86Msg(X_ERROR, "%s: cannot push module '%s' onto "
			"keyboard device: %s\n",
			pInfo->name, priv->strmod, strerror(errno));
	    }
	}

#ifdef HIDIOCKMSDIRECT
	if (strcmp(priv->strmod, "usbkbm") == 0) {
	    io_get_direct = HIDIOCKMGDIRECT;
	    io_set_direct = HIDIOCKMSDIRECT;
	}
#endif
    }

    SYSCALL(i = ioctl(pInfo->fd, io_get_direct, &kdirect));
    if (i < 0) {
	xf86Msg(X_ERROR, 
		"%s: Unable to determine keyboard direct setting: %s\n", 
		pInfo->name, strerror(errno));
	return BadImplementation;
    }

    priv->odirect = kdirect;
    kdirect = 1;

    SYSCALL(i = ioctl(pInfo->fd, io_set_direct, &kdirect));
    if (i < 0) {
	xf86Msg(X_ERROR, "%s: Failed turning keyboard direct mode on: %s\n",
			pInfo->name, strerror(errno));
	return BadImplementation;
    }

    /* Setup translation */

    SYSCALL(i = ioctl(pInfo->fd, KIOCGTRANS, &ktrans));
    if (i < 0) {
	xf86Msg(X_ERROR, 
		"%s: Unable to determine keyboard translation mode: %s\n", 
		pInfo->name, strerror(errno));
	return BadImplementation;
    }

    priv->otranslation = ktrans;
    ktrans = TR_UNTRANS_EVENT;

    SYSCALL(i = ioctl(pInfo->fd, KIOCTRANS, &ktrans));
    if (i < 0) {	
	xf86Msg(X_ERROR, "%s: Failed setting keyboard translation mode: %s\n",
			pInfo->name, strerror(errno));
	return BadImplementation;
    }

    priv->oleds	= sunKbdGetLeds(pInfo);

    /* Allocate here so we don't alloc in ReadInput which may be called
       from SIGIO handler. */
    priv->remove_timer = TimerSet(priv->remove_timer, 0, 0, NULL, NULL);

    priv->kbdActive = TRUE;
    return Success;
}

static int
KbdOff(InputInfoPtr pInfo, int what)
{
    KbdDevPtr pKbd = (KbdDevPtr) pInfo->private;
    sunKbdPrivPtr priv = (sunKbdPrivPtr) pKbd->private;

    int i;
    int io_set_direct, kdirect;

    if (priv->remove_timer) {
	TimerFree(priv->remove_timer);
	priv->remove_timer = NULL;
    }

    if (!priv->kbdActive) {
	return Success;
    }

    if (pInfo->fd == -1) {
	priv->kbdActive = FALSE;
	return Success;
    }

    /* restore original state */

    if (priv->oleds != -1) {
	sunKbdSetLeds(pInfo, priv->oleds);
	priv->oleds = -1;
    }
    
    if (priv->otranslation != -1) {
        SYSCALL(i = ioctl(pInfo->fd, KIOCTRANS, &priv->otranslation));
	if (i < 0) {
	    xf86Msg(X_ERROR,
		    "%s: Unable to restore keyboard translation mode: %s\n",
		    pInfo->name, strerror(errno));
	    return BadImplementation;
	}
	priv->otranslation = -1;
    }

    io_set_direct = KIOCSDIRECT;
    kdirect = priv->odirect;

#ifdef HIDIOCKMSDIRECT
    if ((priv->strmod != NULL) && (strcmp(priv->strmod, "usbkbm") == 0)) {
	io_set_direct = HIDIOCKMSDIRECT;
	kdirect = 0;
    }
#endif

    if (kdirect != -1) {
	SYSCALL(i = ioctl(pInfo->fd, io_set_direct, &kdirect));
	if (i < 0) {
	    xf86Msg(X_ERROR,
		    "%s: Unable to restore keyboard direct setting: %s\n",
		    pInfo->name, strerror(errno));
	    return BadImplementation;
	}
	priv->odirect = -1;
    }

    if (priv->strmod) {
	SYSCALL(i = ioctl(pInfo->fd, I_POP, priv->strmod));
	if (i < 0) {
            xf86Msg(X_WARNING,
		    "%s: cannot pop module '%s' off keyboard device: %s\n",
		    pInfo->name, priv->strmod, strerror(errno));
	}
    }

    CloseKeyboard(pInfo);
    return Success;
}


static void
SoundKbdBell(InputInfoPtr pInfo, int loudness, int pitch, int duration)
{
    int	kbdCmd, i;
#ifdef KIOCMKTONE
    int cycles;
    int mktonevalue;
#endif

    if (loudness && pitch)
    {
#ifdef KIOCMKTONE
	if (pitch == 0)
	    cycles = UINT16_MAX;
	else if (pitch >= UINT16_MAX)
	    cycles = 0;
	else {
	    cycles = (PIT_HZ + pitch / 2) / pitch;
	    if (cycles > UINT16_MAX)
		cycles = UINT16_MAX;
	}

	mktonevalue = cycles | (((duration * loudness * 20) / 1000) << 16);

	errno = 0;
	SYSCALL(i = ioctl (pInfo->fd, KIOCMKTONE, mktonevalue));
	if (i == 0)
	    return;

	if (errno != EINVAL) {
	    if (errno != EAGAIN)
		xf86Msg(X_ERROR, "%s: Failed to activate bell: %s\n",
			pInfo->name, strerror(errno));
	    return;
	}
#endif

 	kbdCmd = KBD_CMD_BELL;
		
	SYSCALL(i = ioctl (pInfo->fd, KIOCCMD, &kbdCmd));
	if (i < 0) {
	    xf86Msg(X_ERROR, "%s: Failed to activate bell: %s\n",
                pInfo->name, strerror(errno));
	}
	
	usleep(duration * loudness * 20);
	
	kbdCmd = KBD_CMD_NOBELL;
	SYSCALL(i = ioctl (pInfo->fd, KIOCCMD, &kbdCmd));
	if (i < 0) {
	     xf86Msg(X_ERROR, "%s: Failed to deactivate bell: %s\n",
                pInfo->name, strerror(errno));
	}
    }
}

static void
SetKbdLeds(InputInfoPtr pInfo, int leds)
{
    int real_leds = sunKbdGetLeds(pInfo);

    real_leds &= ~(LED_CAPS_LOCK | LED_NUM_LOCK | LED_SCROLL_LOCK | LED_COMPOSE);

    if (leds & XLED1)  real_leds |= LED_CAPS_LOCK;
    if (leds & XLED2)  real_leds |= LED_NUM_LOCK;
    if (leds & XLED3)  real_leds |= LED_SCROLL_LOCK;
    if (leds & XLED4)  real_leds |= LED_COMPOSE;
    
    sunKbdSetLeds(pInfo, real_leds);
}

static int
GetKbdLeds(InputInfoPtr pInfo)
{
    int leds = 0;
    int real_leds = sunKbdGetLeds(pInfo);

    if (real_leds & LED_CAPS_LOCK)	leds |= XLED1;
    if (real_leds & LED_NUM_LOCK)	leds |= XLED2;
    if (real_leds & LED_SCROLL_LOCK)	leds |= XLED3;
    if (real_leds & LED_COMPOSE)	leds |= XLED4;
	
    return leds;
}

static void
CloseKeyboard(InputInfoPtr pInfo)
{
    KbdDevPtr pKbd = (KbdDevPtr) pInfo->private;
    sunKbdPrivPtr priv = (sunKbdPrivPtr) pKbd->private;

    close(pInfo->fd);
    pInfo->fd = -1;
    priv->kbdActive = FALSE;
}

/* Called from OsTimer callback, since removing a device from the device
   list or changing pInfo->fd while xf86Wakeup is looping through the list
   causes server crashes */
static CARD32
RemoveKeyboard(OsTimerPtr timer, CARD32 time, pointer arg)
{
    InputInfoPtr pInfo = (InputInfoPtr) arg;

    CloseKeyboard(pInfo);
    xf86DisableDevice(pInfo->dev, TRUE);

    return 0;  /* All done, don't set to run again */
}

static void
ReadInput(InputInfoPtr pInfo)
{
    KbdDevPtr pKbd = (KbdDevPtr) pInfo->private;
    sunKbdPrivPtr priv = (sunKbdPrivPtr) pKbd->private;
    Firm_event event[64];
    int        nBytes, i;

    while (TRUE) {
	/* I certainly hope it's not possible to read partial events */
	nBytes = read(pInfo->fd, (char *)event, sizeof(event));
	if (nBytes > 0) {
	    for (i = 0; i < (nBytes / sizeof(Firm_event)); i++) {
		pKbd->PostEvent(pInfo, event[i].id & 0xFF,
				event[i].value == VKEY_DOWN ? TRUE : FALSE);
	    }
	} else if (nBytes == -1) {
	    switch (errno) {
		case EAGAIN: /* Nothing to read now */
		    return;
		case EINTR:  /* Interrupted, try again */
		    break;
		case ENODEV: /* May happen when USB kbd is unplugged */
		    /* We use X_NONE here because it didn't alloc since we
		       may be called from SIGIO handler. No longer true for
		       sigsafe logging, but matters for older servers  */
		    LogMessageVerbSigSafe(X_NONE, 0,
					  "%s: Device no longer present - removing.\n",
					  pInfo->name);
		    xf86RemoveEnabledDevice(pInfo);
		    priv->remove_timer = TimerSet(priv->remove_timer, 0, 1,
						  RemoveKeyboard, pInfo);
		    return;
		default:     /* All other errors */
		    /* We use X_NONE here because it didn't alloc since we
		       may be called from SIGIO handler. No longer true for
		       sigsafe logging, but matters for older servers  */
		    LogMessageVerbSigSafe(X_NONE, 0, "%s: Read error: %s\n", pInfo->name,
					  strerror(errno));
		    return;
	    }
	} else { /* nBytes == 0, so nothing more to read */
	    return;
	}
    }
}

static Bool
OpenKeyboard(InputInfoPtr pInfo)
{
    char *kbdPath = xf86SetStrOption(pInfo->options, "Device", "/dev/kbd");
    Bool ret;

    pInfo->fd = open(kbdPath, O_RDONLY | O_NONBLOCK);
    
    if (pInfo->fd == -1) {
	xf86Msg(X_ERROR, "%s: cannot open \"%s\"\n", pInfo->name, kbdPath);
	ret = FALSE;
    } else {
	xf86MsgVerb(X_INFO, 3, "%s: Opened device \"%s\"\n", pInfo->name,
		    kbdPath);
	pInfo->read_input = ReadInput;
	ret = TRUE;
	/* in case it wasn't set and we fell back to default */
	xf86ReplaceStrOption(pInfo->options, "Device", kbdPath);
    }

    free(kbdPath);
    return ret;
}

_X_EXPORT Bool
xf86OSKbdPreInit(InputInfoPtr pInfo)
{
    KbdDevPtr pKbd = pInfo->private;

    pKbd->KbdInit       = KbdInit;
    pKbd->KbdOn         = KbdOn;
    pKbd->KbdOff        = KbdOff;
    pKbd->Bell          = SoundKbdBell;
    pKbd->SetLeds       = SetKbdLeds;
    pKbd->GetLeds       = GetKbdLeds;
    pKbd->KbdGetMapping = KbdGetMapping;

    pKbd->RemapScanCode = NULL;

    pKbd->OpenKeyboard = OpenKeyboard;

    pKbd->private = calloc(sizeof(sunKbdPrivRec), 1);
    if (pKbd->private == NULL) {
       xf86Msg(X_ERROR,"can't allocate keyboard OS private data\n");
       return FALSE;
    } else {
	sunKbdPrivPtr priv = (sunKbdPrivPtr) pKbd->private;
	priv->otranslation = -1;
	priv->odirect = -1;
    }

    return TRUE;
}