/*
* keyboard input
*/
#include        "u.h"
#include        "../port/lib.h"
#include        "mem.h"
#include        "dat.h"
#include        "fns.h"
#include        "io.h"
#include        "../port/error.h"

enum {
       Data=           0x40+3,         /* data port */
       Cmd=            0x44+3,         /* command port (write) */
       Status=         0x44+3,         /* status port (read) */
        Inready=       0x01,           /*  input character ready */
        Outbusy=       0x02,           /*  output busy */
        Sysflag=       0x04,           /*  system flag */
        Cmddata=       0x08,           /*  cmd==0, data==1 */
        Inhibit=       0x10,           /*  keyboard/mouse inhibited */
        Minready=      0x20,           /*  mouse character ready */
        Rtimeout=      0x40,           /*  general timeout */
        Parity=        0x80,
};

enum
{
       /* controller command byte */
       Cscs1=          (1<<6),         /* scan code set 1 */
       Cauxdis=        (1<<5),         /* mouse disable */
       Ckbddis=        (1<<4),         /* kbd disable */
       Csf=            (1<<2),         /* system flag */
       Cauxint=        (1<<1),         /* mouse interrupt enable */
       Ckbdint=        (1<<0),         /* kbd interrupt enable */
};

enum {
       Qdir,
       Qscancode,
       Qleds,
};

static Dirtab kbdtab[] = {
       ".",            {Qdir, 0, QTDIR},       0,      0555,
       "scancode",     {Qscancode, 0},         0,      0440,
       "leds",         {Qleds, 0},             0,      0220,
};

static Lock i8042lock;
static uchar ccc, dummy;

static struct {
       Ref ref;
       Queue *q;
       uchar *io;
} kbd;


#define inb(r)          (dummy=kbd.io[r])
#define outb(r,b)       (kbd.io[r]=b)

/*
*  wait for output no longer busy
*/
static int
outready(void)
{
       int tries;

       for(tries = 0; (inb(Status) & Outbusy); tries++){
               if(tries > 500)
                       return -1;
               delay(2);
       }
       return 0;
}

/*
*  wait for input
*/
static int
inready(void)
{
       int tries;

       for(tries = 0; !(inb(Status) & Inready); tries++){
               if(tries > 500)
                       return -1;
               delay(2);
       }
       return 0;
}

/*
* set keyboard's leds for lock states (scroll, numeric, caps).
*
* at least one keyboard (from Qtronics) also sets its numeric-lock
* behaviour to match the led state, though it has no numeric keypad,
* and some BIOSes bring the system up with numeric-lock set and no
* setting to change that.  this combination steals the keys for these
* characters and makes it impossible to generate them: uiolkjm&*().
* thus we'd like to be able to force the numeric-lock led (and behaviour) off.
*/
static void
setleds(int leds)
{
       static int old = -1;

       if(!conf.keyboard || leds == old)
               return;
       leds &= 7;
       ilock(&i8042lock);
       for(;;){
               if(outready() < 0)
                       break;
               outb(Data, 0xed);               /* `reset keyboard lock states' */
               if(outready() < 0)
                       break;
               outb(Data, leds);
               if(outready() < 0)
                       break;
               old = leds;
               break;
       }
       iunlock(&i8042lock);
}

/*
*  keyboard interrupt
*/
static void
i8042intr(Ureg*, void*)
{
       extern void sgimouseputc(int);

       int s, c;
       uchar b;

       /*
        *  get status
        */
       ilock(&i8042lock);
       s = inb(Status);
       if(!(s&Inready)){
               iunlock(&i8042lock);
               return;
       }

       /*
        *  get the character
        */
       c = inb(Data);
       iunlock(&i8042lock);

       b = c & 0xff;

       /*
        *  if it's the aux port...
        */
       if(s & Minready){
               sgimouseputc(b);
               return;
       }

       qproduce(kbd.q, &b, 1);
}

static void
pollintr(void)
{
       i8042intr(nil, nil);
}

static Chan *
kbdattach(char *spec)
{
       return devattach(L'b', spec);
}

static Walkqid*
kbdwalk(Chan *c, Chan *nc, char **name, int nname)
{
       return devwalk(c, nc, name, nname, kbdtab, nelem(kbdtab), devgen);
}

static int
kbdstat(Chan *c, uchar *dp, int n)
{
       return devstat(c, dp, n, kbdtab, nelem(kbdtab), devgen);
}

static Chan*
kbdopen(Chan *c, int omode)
{
       if(!iseve())
               error(Eperm);
       if(c->qid.path == Qscancode){
               if(waserror()){
                       decref(&kbd.ref);
                       nexterror();
               }
               if(incref(&kbd.ref) != 1)
                       error(Einuse);
               c = devopen(c, omode, kbdtab, nelem(kbdtab), devgen);
               poperror();
               return c;
       }
       return devopen(c, omode, kbdtab, nelem(kbdtab), devgen);
}

static void
kbdclose(Chan *c)
{
       if((c->flag & COPEN) && c->qid.path == Qscancode)
               decref(&kbd.ref);
}

static Block*
kbdbread(Chan *c, long n, ulong off)
{
       if(c->qid.path == Qscancode)
               return qbread(kbd.q, n);
       else
               return devbread(c, n, off);
}

static long
kbdread(Chan *c, void *a, long n, vlong)
{
       if(c->qid.path == Qscancode)
               return qread(kbd.q, a, n);
       if(c->qid.path == Qdir)
               return devdirread(c, a, n, kbdtab, nelem(kbdtab), devgen);

       error(Egreg);
       return 0;
}

static long
kbdwrite(Chan *c, void *a, long n, vlong)
{
       char tmp[8+1], *p;

       if(c->qid.path != Qleds)
               error(Egreg);

       p = tmp + n;
       if(n >= sizeof(tmp))
               p = tmp + sizeof(tmp)-1;
       memmove(tmp, a, p - tmp);
       *p = 0;

       setleds(atoi(tmp));

       return n;
}


static char *initfailed = "i8042: kbdinit failed\n";

static int
outbyte(int port, int c)
{
       outb(port, c);
       if(outready() < 0) {
               print(initfailed);
               return -1;
       }
       return 0;
}

static void
kbdinit(void)
{
       int c, try;

       kbd.io = IO(uchar, HPC3_KBDMS);
       kbd.q = qopen(1024, Qcoalesce, 0, 0);
       if(kbd.q == nil)
               panic("kbdinit: qopen");
       qnoblock(kbd.q, 1);

       /* wait for a quiescent controller */
       try = 1000;
       while(try-- > 0 && (c = inb(Status)) & (Outbusy | Inready)) {
               if(c & Inready)
                       inb(Data);
               delay(1);
       }
       if (try <= 0) {
               print(initfailed);
               return;
       }

       /* get current controller command byte */
       outb(Cmd, 0x20);
       if(inready() < 0){
               print("i8042: kbdinit can't read ccc\n");
               ccc = 0;
       } else
               ccc = inb(Data);

       /* enable kbd xfers and interrupts */
       ccc &= ~Ckbddis;
       ccc |= Csf | Ckbdint | Cscs1;
       if(outready() < 0) {
               print(initfailed);
               return;
       }
       /* disable mouse */
       if (outbyte(Cmd, 0x60) < 0 || outbyte(Data, ccc) < 0){
               print("i8042: kbdinit mouse disable failed\n");
               return;
       }

       conf.keyboard = 1;
       addclock0link(pollintr, 5);
}

Dev kbddevtab = {
       L'b',
       "kbd",

       devreset,
       kbdinit,
       devshutdown,
       kbdattach,
       kbdwalk,
       kbdstat,
       kbdopen,
       devcreate,
       kbdclose,
       kbdread,
       kbdbread,
       kbdwrite,
       devbwrite,
       devremove,
       devwstat,
};