/* pdp18b_g2tty.c: PDP-7/9 Bell Labs "GRAPHIC-2" subsystem as a TTY via TELNET
   from 13-Sep-15 version of pdp18b_tt1.c

   Copyright (c) 1993-2015, Robert M Supnik
   Copyright (c) 2016, Philip L Budne

   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 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
   ROBERT M SUPNIK 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.

   Except as contained in this notice, the name of Robert M Supnik shall not be
   used in advertising or otherwise to promote the sale, use or other dealings
   in this Software without prior written authorization from Robert M Supnik.

   Doug McIlroy had this to say about the Bell Labs PDP-7 Ken Thompson
   created UNIX on:

      The pdp7 was cast off by the visual and acoustics research department.
      Bill Ninke et al. built graphic II on it -- a graphics attachment as big
      as the pdp7 itself. The disk was an amazing thing about 2' in diameter,
      mounted on a horizontal axis. Mystery crashes bedeviled it until somebody
      realized that the axis was perpendicular to the loading dock 4 floors
      below. A 90-degree turn solved the problem.

   GRAPHICS-2 was a command list based graphics display system,
   and included a light pen, a "button box" and status bits
   for a "dataphone" interface to speak to a GECOS system.

   The UNIX-7 system driver only uses text display, and reserves 269
   words (holding two characters each; the buffer is 273 words, but
   three contain display "setup" commands, and the final word in the
   buffer must be a display "TRAP" instruction that ends the display
   list).

   The UNIX system code triggers a refresh every 10 60Hz "ticks" of
   the real time clock.  This driver attempts to do detect new text
   and send it to a user who has TELNETed in.

   Thoughts on implementing a web interface:

   538 characters redisplaying at 6Hz (every 10 "ticks") gives a
   bandwith requirement of only 26Kbit, and most refreshes won't
   change the screen and could be suppressed.  So it seems like it
   would be reasonable to create a web interface.

   Make a SIMH TCP server port which implements a tiny HTTP server.
   The base URL serves up a skeletal page with (lighted) buttons,
   and a "display window".

   And either:

   1) Use "AJAX": an (invisible) <iframe> on in the HTML served by the
      "base URL" keeps a never-ending connection that is sent "script"
      tags to alter the "screen" (div) contents, and light push buttons.
      Keypresses and buttons could be implemented using "onclick"
      actions which trigger a GET (or a POST) on a URLs which
      open new (temporary) HTTP connections to SIMH.  The key/button
      press URL could contain a session UUID which has to match
      a value sent in the initial page.

   2) Have the "home" page HTML establish a bi-directional WebSocket
      connection to Encapsulate all the traffic (screen contents,
      button lighting, key & button presses).

   The graphics system responds as ten PDP-7 "devices";
   UNIX only uses six, and only three of the six are simulated here
   (and *JUST* enough of those to figure out the text being displayed),
   as two SIMH DEVICES, G2OUT and G2IN:

   G2OUT:
       G2D1     005     GRAPHICS-2 display output
   G2IN:
       G2KB     043     GRAPHICS-2 keyboard
       G2BB     044     GRAPHICS-2 button box (lighted bush buttons)

   20-Mar-16    PLB     Works, mostly
   19-Mar-16    PLB     Working (up to a screen full)
   17-Mar-16    PLB     Cloned from 13-Sep-15 version of pdp18b_tt1.c
*/

/*

 * GRAPHICS-2 was vector graphics hardware, UNIX-7 uses it as a
 * "Glass TTY" for a "second seat".
 *
 * This simulation ONLY handles text display; the one program
 * found that uses the "capt" (capture?) call to set a user
 * supplied display list will not work here.
 *
 * When the display buffer or screen is filled, the UNIX "display"
 * driver lights "push button 7" (PB7), and waits for the user to
 * press the button.  UNIX then clears the screen, and output
 * continues.  If the program outputs a "Form Feed" character the
 * display is also cleared.
 *
 * This simulation automatically presses PB7 when lit, without
 * bothering the user (a more accurate simulation might prompt
 * the user to press any key)!
 */

#include "pdp18b_defs.h"
#ifdef GRAPHICS2
#include "sim_tmxr.h"

int debug = 0;

/* hardware registers */
uint8 g2kb_done = 0;                    /* keyboard flag */
uint32 g2kb_buf = 0;                    /* keyboard buffer */
uint8 g2bb_flag = 0;                    /* button flag */
uint32 g2bb_bbuf = 0;                   /* button buffer */
uint32 g2bb_lbuf = 0;                   /* button lights buffer */
uint32 g2out_addr = 0;                  /* display address */
#define PB7 02000

/* not hardware registers: */
uint32 g2out_count = 0;
uint8 g2out_stuffcr = 0;                /* need to stuff a CR */


/* keep old and new version of characters to display
 * a count & checksum of the "old" screen contents might suffice,
 * time will tell....
 */
uint8 g2out_which = 0;
#define OLD g2out_which
#define NEW (g2out_which ^ 1)

#define MAXBUFCHARS 700                 /* larger than kernel display list */
static struct dspbuf {
    uint16 count;
    char buffer[MAXBUFCHARS];           /* 7-bit ASCII */
} g2out_dspbufs[2];

/* terminal mux data */
TMLN g2_ldsc = { 0 };                   /* line descriptor */
TMXR g2_desc = { 1, 0, 0, &g2_ldsc };   /* mux descriptor */

/* kernel display lists always start like this: */
static const int32 g2_expect[3] = {
    0065057, /* PARAM: clear blink, clear light pen, scale=1, intensity=3 */
    0147740, /* X-Y: invisible, no delay, Y=01740 (992) */
    0160000  /* X-Y: invisible, settling delay, X=0 */
};

extern int32 *M;
extern int32 int_hwre[API_HLVL+1];
extern int32 api_vec[API_HLVL][32];
extern int32 tmxr_poll;
extern int32 stop_inst;

/* SIMH G2IN DEVICE */
t_bool g2kb_test_done ();
void g2kb_set_done ();
void g2kb_clr_done ();
int32 g2kb_iot (int32 dev, int32 pulse, int32 dat); /* device 043 */

t_bool g2bb_test_flag ();
void g2bb_set_flag ();
void g2bb_clr_flag ();
int32 g2bb_iot (int32 dev, int32 pulse, int32 dat); /* device 044 */

t_stat g2in_svc (UNIT *uptr);

/* SIMH G2OUT DEVICE */
int32 g2d1_iot (int32 dev, int32 pulse, int32 dat); /* device 05 */
static void g2out_clear ();
static void g2out_process_display_list ();
static int g2out_send_new ();

/* both G2IN/G2OUT: */
t_stat g2_attach (UNIT *uptr, CONST char *cptr);
t_stat g2_detach (UNIT *uptr);
t_stat g2_reset (DEVICE *dptr);

/****************************************************************
 * SIMH G2IN (keyboard/buttons) DEVICE data structures
 *
 *  g2in_dev     G2IN device descriptor
 *  g2in_unit    G2IN unit descriptor
 *  g2in_reg     G2IN register list
 *  g2in_mod     G2IN modifiers list
 */

DIB g2in_dib = { DEV_G2KB, 2, NULL, { &g2kb_iot, &g2bb_iot } };

UNIT g2in_unit = {
    UDATA (&g2in_svc, UNIT_IDLE|UNIT_ATTABLE, 0), KBD_POLL_WAIT
};

REG g2in_reg[] = {
    { ORDATA (KBBUF, g2kb_buf, 1) },
    { ORDATA (KBDONE, g2kb_done, 1) },
    { FLDATA (INT, int_hwre[API_G2], INT_V_G2) },
    { DRDATA (TIME, g2in_unit.wait, 24), REG_NZ + PV_LEFT },
    { ORDATA (BBBBUF, g2bb_bbuf, 1) },  /* button box button buffer */
    { ORDATA (BBFLAG, g2bb_flag, 1) },  /* button box IRQ */
    { ORDATA (BBLBUF, g2bb_lbuf, 1) },  /* button box lights buffer */
    { NULL }
    };

MTAB g2in_mod[] = {
    { UNIT_ATT, UNIT_ATT, "summary", NULL,
      NULL, &tmxr_show_summ, (void *) &g2_desc },
    { MTAB_XTD | MTAB_VDV, 1, NULL, "DISCONNECT",
      &tmxr_dscln, NULL, (void *) &g2_desc },
    { MTAB_XTD | MTAB_VDV | MTAB_NMO, 1, "CONNECTIONS", NULL,
      NULL, &tmxr_show_cstat, (void *) &g2_desc },
    { MTAB_XTD | MTAB_VDV | MTAB_NMO, 0, "STATISTICS", NULL,
      NULL, &tmxr_show_cstat, (void *) &g2_desc },
    { MTAB_XTD|MTAB_VUN|MTAB_NC, 0, "LOG", "LOG",
      &tmxr_set_log, &tmxr_show_log, &g2_desc },
    { MTAB_XTD|MTAB_VUN|MTAB_NC, 0, NULL, "NOLOG",
      &tmxr_set_nolog, NULL, &g2_desc },
    { MTAB_XTD|MTAB_VDV, 0, "DEVNO", "DEVNO",
      NULL, &show_devno, NULL },
    { 0 }
    };

/* SIMH G2IN device descriptor (GRAPHICS-2 keyboard & button box) */
DEVICE g2in_dev = {
    "G2IN",                             /* name */
    &g2in_unit,                         /* units */
    g2in_reg, g2in_mod,                 /* registers, modifiers */
    1,                                  /* numunits */
    10, 31,                             /* aradix, awidth */
    1, 8, 8,                            /* aincr, dradix, dwidth */
    &tmxr_ex, &tmxr_dep, &g2_reset,     /* examine, deposit, reset */
    NULL, &g2_attach, &g2_detach,       /* boot, attach, detach */
    &g2in_dib, DEV_MUX | DEV_DISABLE    /* ctxt, flags */
    };

/****************************************************************
 * SIMH G2OUT (display output) DEVICE data structures
 * Only needed to hold "iot" routine, since DIB's can't represent
 * devices with register sets as sparse as GRAPHICS-2
 *
 *  g2out_dev     G2OUT device descriptor
 *  g2out_unit    G2OUT unit descriptor
 *  g2out_reg     G2OUT register list
 *  g2out_mod     G2OUT modifiers list
 */

DIB g2out_dib = { DEV_G2D1, 1, NULL, { &g2d1_iot } };

UNIT g2out_unit = { UDATA (NULL, 0, 0) };

REG g2out_reg[] = {
    { ORDATA (DPYADDR, g2out_addr, 1) },
    { NULL }
    };

MTAB g2out_mod[] = {
    { MTAB_XTD|MTAB_VUN, 0, NULL, "DISCONNECT",
      &tmxr_dscln, NULL, &g2_desc },
    { MTAB_XTD|MTAB_VDV, 0, "DEVNO", "DEVNO",
      NULL, &show_devno, NULL },
    { 0 }
    };

/* SIMH G2OUT device descriptor (simulates just one of many display IOTs!) */
DEVICE g2out_dev = {
    "G2OUT",                            /* name */
    &g2out_unit,                        /* units */
    g2out_reg, g2out_mod,               /* registers, modifiers */
    1,                                  /* numunits */
    10, 31,                             /* aradix, awidth */
    1, 8, 8,                            /* aincr, dradix, dwidth */
    NULL, NULL, &g2_reset,              /* examine, deposit, reset */
    NULL, NULL, NULL,                   /* boot, attach, detach */
    &g2out_dib, DEV_DISABLE             /* ctxt, flags */
    };

/****************************************************************
 * IOT routines
 */

/* Keyboard input IOT routine */
/* real device could have done bitwise decode?! */
int32 g2kb_iot (int32 dev, int32 pulse, int32 dat)
{
if (pulse == 001) {                     /* sck */
    if (g2kb_done) {
        dat = dat | IOT_SKP;
        }
    }
else if (pulse == 002) {                /* lck */
    dat = dat | g2kb_buf;               /* return buffer */
    }
else if (pulse == 004) {                /* cck */
    g2kb_clr_done ();                   /* clear flag */
    }
return dat;
}

/* Button Box IOT routine */
int32 g2bb_iot (int32 dev, int32 pulse, int32 dat)
{
if (pulse == 001) {                     /* "spb" -- skip on push button flag */
    if (g2bb_flag)
        dat = dat | IOT_SKP;
    }
else if (pulse == 002)                  /* "lpb"/"opb" -- or push buttons */
    dat = dat | g2bb_bbuf;              /* return buttons */
else if (pulse == 004) {                /* "cpb" -- clear push button flag */
    g2bb_clr_flag ();                   /* clear flag */
    }
else if (pulse == 024) {                /* "wbl" -- write buttons lights */
    if (dat == 0)
        g2out_clear();                  /* UNIX has ack'ed button press */
    g2bb_lbuf = dat;
    }
return dat;
}

/* Input side Unit service */
t_stat g2in_svc (UNIT *uptr)
{
int32 ln, c;

if ((uptr->flags & UNIT_ATT) == 0)              /* attached? */
    return SCPE_OK;

if (g2bb_lbuf & PB7) {                          /* button 7 lit? */
    /* yes: try sending anything new */
    g2out_process_display_list();
    g2out_send_new();

    g2bb_bbuf |= PB7;                           /* press it to clear screen! */
    g2bb_set_flag ();
    }
sim_clock_coschedule (uptr, tmxr_poll);         /* continue poll */
ln = tmxr_poll_conn (&g2_desc);                 /* look for connect */
if (ln >= 0)                                    /* got one? rcv enab */
    g2_ldsc.rcve = 1;
tmxr_poll_rx (&g2_desc);                        /* poll for input */
if (g2_ldsc.conn) {                             /* connected? */
    tmxr_poll_tx (&g2_desc);                    /* PLB: poll xmt */
    if ((c = tmxr_getc_ln (&g2_ldsc))) {        /* get char */
        if (c & SCPE_BREAK)                     /* break? */
            c = 0;
        else {
            c &= 0177;
            if (c == '\r')                      /* translate CR but not ESC! */
                c = '\n';
            else if ((c & 0155) == 055)         /* kernel swaps around -?/= */
                c ^= 020;                       /* pre-swap!!  */
            }
        g2kb_buf = c;
        g2kb_set_done ();
        }
    } /* connected */
else {
    /* not connected; next connection sees entire "screen" */
    g2out_stuffcr = 0;
    }
return SCPE_OK;
}

/* Interrupt handling routines */

t_bool g2kb_test_done ()
{
return g2kb_done != 0;
}

void g2kb_set_done ()
{
g2kb_done = 1;
SET_INT (G2);
return;
}

void g2kb_clr_done ()
{
g2kb_done = 0;
CLR_INT (G2);
return;
}

t_bool g2bb_test_flag ()
{
return g2bb_flag != 0;
}

void g2bb_set_flag ()
{
g2bb_flag = 1;
SET_INT (G2);
return;
}

void g2bb_clr_flag ()
{
g2bb_flag = 0;
CLR_INT (G2);
return;
}

/****************************************************************
 * SIMH G2OUT (Display Output) DEVICE routines
 */

/* helper to put 7-bit display character */
static void g2pc(char c) {
    //if (debug) putchar(c);
    tmxr_putc_ln (&g2_ldsc, c);
}

/* send a character from the display. adds CR after LF */
/* returns 1 if "c" was sent; 0 means try again later */
static int g2out_putchar(char c)
{
if (!g2_ldsc.conn || !g2_ldsc.xmte)     /* connected, tx enabled? */
    return 0;

if (g2out_stuffcr) {                    /* need to stuff a CR? */
    g2pc ('\r');
    g2out_stuffcr = 0;
    if (!g2_ldsc.xmte)                 /* full? */
        return 0;                      /* yes: wait until next time */
    }

g2pc (c);

if (c == '\n') {                        /* was it a NL? */
    if (g2_ldsc.xmte)                   /* transmitter enabled? */
        g2pc ('\r');                    /* send CR now */
    else
        g2out_stuffcr = 1;              /* wait until next time */
    }
return 1;
}

/* Device 05 IOT routine */
int32 g2d1_iot (int32 dev, int32 pulse, int32 dat)
{
/*
 * UNIX text display command lists always end with a TRAP
 * and display output is restarted periodicly in timer PI service code
 */
if (g2_ldsc.conn && g2_ldsc.xmte && pulse == 047) { /* conn&ready, "beg" */
    g2out_addr = dat & 017777;
    g2out_process_display_list();
    g2out_send_new();
    g2out_which ^= 1;                   /* swap buffers */
    } /* beg IOT */
return dat;
}

/****************
 * display buffer management/process
 * we're informed when UNIX wants to clear the screen (PB7 lit)
 * we then press the button
 * UNIX does a "cpb" to ACK/clear the interrupt.
 *
 * *BUT* UNIX clears the screen when a FF (014) char is output,
 * which just resets the buffer (and not issuing any IOTs)
 */

static void g2out_clear() {
    g2out_stuffcr = 0;
    g2out_which = 0;
    g2out_count = 0;
    g2out_dspbufs[0].count = g2out_dspbufs[1].count = 0;
}

/* interpret display list; save characters into "new" dspbuf
 * quits early if display list doesn't conform to what's expected
 */
static void g2out_process_display_list() {
    uint32 i;
    struct dspbuf *dp = g2out_dspbufs + NEW;

    dp->count = 0;
    for (i = g2out_addr; i < 020000; i++) {
        uint32 w = M[i] & 0777777;
        int offset = i - g2out_addr;
        char c;

        if (w & 0400000)                /* TRAP (end of display list) */
            return;

        /* check first three words for expected setup commands */
        if (offset < sizeof(g2_expect)/sizeof(g2_expect[0])) {
            if (w != g2_expect[offset])
                return;
            continue;
        }
        if (w & 0300000)                /* not characters? */
            return;

        c = (w >> 7) & 0177;
        if (c)
            dp->buffer[dp->count++] = c;
        c = w & 0177;
        if (c)
            dp->buffer[dp->count++] = c;
    }
}

/* figure out what to send on TELNET connection
 * truncates new->count to the number sent so far
 * returns number of new characters sent
 */
static int g2out_send_new() {
    struct dspbuf *old = g2out_dspbufs + OLD;
    struct dspbuf *New = g2out_dspbufs + NEW;
    char *cp = New->buffer;
    int cur = 0;
    int start;

    /* nothing in newest refresh?
     * COULD have had undisplayed stuff on last screen before it was cleared??
     * would need to have a transmit queue??
     */
    if (New->count == 0)
        return 0;

    if (old->count &&                   /* have old chars */
        memcmp(old->buffer, New->buffer, old->count) == 0) { /* and a prefix */
        cur = old->count;
        cp += cur;
    }
    /* loop for chars while connected & tx enabled */
    start = cur;
    while (cur < New->count && g2_ldsc.conn && g2_ldsc.xmte) {
        if (g2out_putchar(*cp)) {
            cp++;
            cur++;
        }
    }
    New->count = cur;                   /* only remember what's been sent */
    return cur - start;                 /* remember number sent */
}

/****************************************************************
 * subsystem common routines (used by both G2IN and G2OUT SIMH DEVICEs)
 */

/* Reset routine */
t_stat g2_reset (DEVICE *dptr)
{
if (dptr->flags & DEV_DIS) {                            /* sync enables */
    g2in_dev.flags = g2in_dev.flags | DEV_DIS;
    g2out_dev.flags = g2out_dev.flags | DEV_DIS;
    }
else {
    g2in_dev.flags = g2in_dev.flags & ~DEV_DIS;
    g2out_dev.flags = g2out_dev.flags & ~DEV_DIS;
    }
if (g2in_unit.flags & UNIT_ATT)                         /* if attached, */
    sim_activate (&g2in_unit, tmxr_poll);               /* activate */
else sim_cancel (&g2in_unit);                           /* else stop */

g2kb_buf = 0;                                           /* clear buf */
g2kb_clr_done ();                                       /* clear done */

g2bb_bbuf = 0;                                          /* clear buttons */
g2bb_lbuf = 0;                                          /* clear lights */
g2bb_clr_flag ();

g2out_addr = 0;
g2out_clear();
sim_cancel (&g2out_unit);                                /* stop poll */
return SCPE_OK;
}

/* Attach master unit */
t_stat g2_attach (UNIT *uptr, CONST char *cptr)
{
t_stat r;

r = tmxr_attach (&g2_desc, uptr, cptr); /* attach */
if (r != SCPE_OK)                       /* error */
    return r;
sim_activate (uptr, 0);                 /* start poll at once */
return SCPE_OK;
}

/* Detach master unit */

t_stat g2_detach (UNIT *uptr)
{
t_stat r = tmxr_detach (&g2_desc, uptr); /* detach */
sim_cancel (uptr);                       /* stop poll */
g2_ldsc.rcve = 0;
return r;
}
#endif
