/*
 * besm6_tty.c: BESM-6 teletype device
 *
 * Copyright (c) 2009-2017, Leo Broukhis
 * Copyright (c) 2009, Serge Vakulenko
 * 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
 * SERGE VAKULENKO OR LEONID BROUKHIS 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 Leonid Broukhis or
 * Serge Vakulenko shall not be used in advertising or otherwise to promote
 * the sale, use or other dealings in this Software without prior written
 * authorization from Leonid Broukhis and Serge Vakulenko.
 */

#include "besm6_defs.h"
#include "sim_sock.h"
#include "sim_tmxr.h"

#define TTY_MAX         24              /* Serial TTY lines */
#define LINES_MAX       TTY_MAX + 2     /* Including parallel "Consul" typewriters */
/*
 * According to a table in http://ru.wikipedia.org/wiki/МТК-2
 */
char * rus[] = { 0, "Т", "\r", "О", " ", "Х", "Н", "М", "\n", "Л", "Р", "Г", "И", "П", "Ц", "Ж",
                 "Е", "З", "Д", "Б", "С", "Ы", "Ф", "Ь", "А", "В", "Й", 0, "У", "Я", "К", 0 };

char * lat[] = { 0, "T", "\r", "O", " ", "H", "N", "M", "\n", "L", "R", "G", "I", "P", "C", "V",
                 "E", "Z", "D", "B", "S", "Y", "F", "X", "A", "W", "J", 0, "U", "Q", "K", 0 };

/* $ = Кто там? */
char * dig[] = { 0, "5", "\r", "9", " ", "Щ", ",", ".", "\n", ")", "4", "Ш", "8", "0", ":", "=",
                 "3", "+", "$", "?", "'", "6", "Э", "/", "-", "2", "Ю", 0, "7", "1", "(", 0 };

char ** reg = 0;

char *  process (int sym)
{
    /* Inversion is required for Baudot TTYs */
    sym ^= 31;
    switch (sym) {
    case 0:
        reg = rus;
        break;
    case 27:
        reg = dig;
        break;
    case 31:
        reg = lat;
        break;
    default:
        return reg[sym];
    }
    return "";
}

/* For serial lines */
int tty_active [TTY_MAX+1], tty_sym [TTY_MAX+1];
int tty_typed [TTY_MAX+1], tty_instate [TTY_MAX+1];

/* For all lines */
time_t tty_last_time [LINES_MAX+1];
int tty_idle_count [LINES_MAX+1];
int tty_lnorder[LINES_MAX+1] = { -1 };

/* The serial interrupt generator frequency, common for all VT lines */
int tty_rate = 300;

/* Interrupt generator mode: 1 - model time, 0 - wallclock time */
int tty_turbo = 1;

uint32 vt_sending, vt_receiving;
uint32 tt_sending, tt_receiving;

// Attachments survive the reset
uint32 tt_mask = 0, vt_mask = 0;

uint32 TTY_OUT = 0, TTY_IN = 0, vt_idle = 0;
uint32 CONSUL_IN[2];

uint32 CONS_CAN_PRINT[2] = { 01000, 00400 };
uint32 CONS_HAS_INPUT[2] = { 04000, 02000 };

uint32 CONS_READY[2] = { 0200, 040 };
/* Command line buffers for TELNET mode. */
char vt_cbuf [CBUFSIZE] [LINES_MAX+1];
char *vt_cptr [LINES_MAX+1];

void tt_print();
void consul_receive();
t_stat vt_clk(UNIT *);
extern const char *get_sim_sw (const char *cptr);

int attached_console;

UNIT tty_unit [] = {
    { UDATA (vt_clk, UNIT_DIS|UNIT_IDLE, 0) },       /* fake unit, clock */
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    /* The next two units are parallel interface */
    { UDATA (NULL, UNIT_SEQ, 0) },
    { UDATA (NULL, UNIT_SEQ, 0) },
    { 0 }
};

REG tty_reg[] = {
    { 0 }
};

/*
 * Line descriptors for the TMXR multiplexor.
 * The .conn field contains the socket number and denotes a used line.
 * For local terminals, .conn = 1.
 * To match line indexes with TTY numbers (1-based),
 * line 0 is kept used (.conn = 1).
 * The .rcve field is set to 1 for network connections.
 * For local connections, it is 0.
 */
TMLN tty_line [LINES_MAX+1];
TMXR tty_desc = { LINES_MAX+1, 0, 0, tty_line, tty_lnorder };        /* mux descriptor */

#define TTY_UNICODE_CHARSET     0
#define TTY_KOI7_JCUKEN_CHARSET (1<<UNIT_V_UF)
#define TTY_KOI7_QWERTY_CHARSET (2<<UNIT_V_UF)
#define TTY_RAW_CHARSET         (3<<UNIT_V_UF)
#define TTY_CHARSET_MASK        (3<<UNIT_V_UF)
#define TTY_OFFLINE_STATE       0
#define TTY_TELETYPE_STATE      (1<<(UNIT_V_UF+2))
#define TTY_VT340_STATE         (2<<(UNIT_V_UF+2))
#define TTY_CONSUL_STATE        (3<<(UNIT_V_UF+2))
#define TTY_STATE_MASK          (3<<(UNIT_V_UF+2))
#define TTY_DESTRUCTIVE_BSPACE  0
#define TTY_AUTHENTIC_BSPACE    (1<<(UNIT_V_UF+4))
#define TTY_BSPACE_MASK         (1<<(UNIT_V_UF+4))
#define TTY_CMDLINE_MASK        (1<<(UNIT_V_UF+5))

static void reset_line(int num)
{
    /* Reset to a sensible default */
    tty_unit[num].flags &= ~(TTY_CHARSET_MASK|TTY_BSPACE_MASK|TTY_CMDLINE_MASK);
    tty_typed[num] = -1;
    tty_instate[num] = 0;
}

t_stat tty_reset (DEVICE *dptr)
{
    memset(tty_active, 0, sizeof(tty_active));
    memset(tty_sym, 0, sizeof(tty_sym));
    memset(tty_typed, 0, sizeof(tty_typed));
    memset(tty_instate, 0, sizeof(tty_instate));
    vt_sending = vt_receiving = 0;
    TTY_IN = TTY_OUT = 0;
    CONSUL_IN[0] = CONSUL_IN[1] = 0;
    reg = rus;
    READY2 |= CONS_READY[0] | CONS_READY[1];
    vt_idle = 1;
    tty_line[0].conn = 1;                   /* faked, always busy */
    /* Forcing a ready interrupt. */
    PRP |= CONS_CAN_PRINT[0] | CONS_CAN_PRINT[1];
    // Schedule the very first TTY interrupt to match the next clock interrupt.
    return sim_clock_coschedule (tty_unit, 0);
}

/* Bit 19 of GRP, should be <tty_rate> Hz */
t_stat vt_clk (UNIT * this)
{
    int num;

    GRP |= MGRP & GRP_SERIAL;

    /* Polling receiving from sockets */
    tmxr_poll_rx (&tty_desc);

    vt_print();
    vt_receive();
    consul_receive();

    /* Are there any new network connections? */
    num = tmxr_poll_conn (&tty_desc);
    if (num > 0 && num <= LINES_MAX) {
        char buf [80];
        TMLN *t = &tty_line [num];
        besm6_debug ("*** tty%d: a new connection from %s",
                     num, t->ipad);
        reset_line (num);
        t->rcve = 1;
        tty_unit[num].flags &= ~TTY_STATE_MASK;
        tty_unit[num].flags |= TTY_VT340_STATE;
        if (num <= TTY_MAX) {
            vt_mask |= 1 << (TTY_MAX - num);
        }
        switch (tty_unit[num].flags & TTY_CHARSET_MASK) {
        case TTY_KOI7_JCUKEN_CHARSET:
            tmxr_linemsg (t, "Encoding is KOI-7 (jcuken)\r\n");
            break;
        case TTY_KOI7_QWERTY_CHARSET:
            tmxr_linemsg (t, "Encoding is KOI-7 (qwerty)\r\n");
            break;
        case TTY_RAW_CHARSET:
            tmxr_linemsg (t, "Encoding is RAW\r\n");
            break;
        case TTY_UNICODE_CHARSET:
            tmxr_linemsg (t, "Encoding is UTF-8\r\n");
            break;
        }
        if (sim_int_char < 040 || sim_int_char == 0177) {
            sprintf (buf, "WRU – Break to sim> prompt character - is ^%c\r\n",
                     sim_int_char ^ 0100);
        } else {
            sprintf (buf, "WRU – Break to sim> prompt character - is %c\r\n",
                     sim_int_char);
        }
        tmxr_linemsg (t, buf);
        tty_idle_count[num] = 0;
        tty_last_time[num] = sim_get_time (0);
        sprintf (buf, "%.24s from %s\r\n",
                 ctime (&tty_last_time[num]),
                 t->ipad);
        tmxr_linemsg (t, buf);

        /* Entering ^C (ETX) to get a prompt. */
        t->rxb [t->rxbpi++] = '\3';
        if (num <= TTY_MAX) {
            vt_mask |= 1 << (TTY_MAX - num);
        }
    }

    /*
     * It the operator console is remote, we still need to probe the local keyboard
     * for a WRU, say, 10 times a second.
     */
    if (!attached_console) {
        static int divider;
        if (++divider == CLK_TPS/10) {
            divider = 0;
            sim_poll_kbd();
        }
    }

    /* Polling sockets for transmission. */
    tmxr_poll_tx (&tty_desc);
    /* If the TTY system is not idle, schedule the next interrupt
     * by instruction count using the target interrupt rate of 300 Hz;
     * otherwise we can wait for a roughly equivalent wallclock time period,
     * e.g. until the next 250 Hz wallclock interrupt, but making sure
     * that the model time interval between GRP_SERIAL interrupts
     * is never less than expected.
     */
    if (vt_is_idle()) {
        /* When idle, slow down the TTY interrupts to match the clock interrupts. */
        return sim_clock_coschedule (this, 0);
    } else if (tty_turbo) {
        /* In "turbo" mode, the TTY works at the model speed */
        return sim_activate(this, 1000*MSEC/tty_rate);
    } else {
        /* In "non-turbo" mode, the TTY interrupts imitate the true feel of the speed */
        return sim_activate_after(this, 1000*MSEC/tty_rate);
    }
}

t_stat tty_setmode (UNIT *u, int32 val, CONST char *cptr, void *desc)
{
    int num = u - tty_unit;
    TMLN *t = &tty_line [num];
    uint32 mask = 1 << (TTY_MAX - num);

    switch (val & TTY_STATE_MASK) {
    case TTY_OFFLINE_STATE:
        if (t->conn) {
            if (t->rcve) {
                tmxr_reset_ln (t);
                t->rcve = 0;
            } else
                t->conn = 0;
            if (num <= TTY_MAX) {
                tty_sym[num] =
                    tty_active[num] =
                    tty_typed[num] =
                    tty_instate[num] = 0;
                vt_mask &= ~mask;
                tt_mask &= ~mask;
            }
        }
        break;
    case TTY_TELETYPE_STATE:
        if (num > TTY_MAX)
            return SCPE_NXPAR;
        t->conn = 1;
        t->rcve = 0;
        tt_mask |= mask;
        vt_mask &= ~mask;
        break;
    case TTY_VT340_STATE:
        t->conn = 1;
        t->rcve = 0;
        if (num <= TTY_MAX) {
            vt_mask |= mask;
            tt_mask &= ~mask;
        }
        break;
    case TTY_CONSUL_STATE:
        if (num <= TTY_MAX)
            return SCPE_NXPAR;
        t->conn = 1;
        t->rcve = 0;
        break;
    }
    return SCPE_OK;
}

/*
 * Allowing telnet connections is done with
 *      attach tty <port>
 * Where <port> is the port number for telnet, e.g. 4199.
 */
t_stat tty_attach (UNIT *u, CONST char *cptr)
{
    int num = u - tty_unit;
    char gbuf[CBUFSIZE];
    int r, m, n;

    /* All arguments but the magic words "console" and "none" are passed
     * to tmxr_attach().
     */
    get_glyph (cptr, gbuf, 0);
    /* Disallowing future connections to a line */
    if (strcmp (gbuf, "NONE") == 0) {
        /* Marking the TTY as unusable. */
        tty_line[num].conn = 1;
        tty_line[num].rcve = 0;
        if (num <= TTY_MAX) {
            vt_mask &= ~(1 << (TTY_MAX - num));
            tt_mask &= ~(1 << (TTY_MAX - num));
        }
        besm6_debug ("*** turning off T%03o", num);
        return SCPE_OK;
    }
    if (strcmp (gbuf, "CONSOLE")) {
        /* Saving and restoring all .conn,
         * because tmxr_attach() zeroes them. */
        for (m=0, n=1; n<=LINES_MAX; ++n)
            if (tty_line[n].conn)
                m |= 1 << (LINES_MAX-n);
        /* The unit number is ignored for the port assignment */
        r = tmxr_attach (&tty_desc, &tty_unit[0], cptr);
        for (n=1; n<=LINES_MAX; ++n)
            if (m >> (LINES_MAX-n) & 1)
                tty_line[n].conn = 1;
        return r;
    } else {
        /* Attaching SIMH console to a particular terminal. */
        u->flags &= ~TTY_STATE_MASK;
        u->flags |= TTY_VT340_STATE;
        tty_line[num].conn = 1;
        tty_line[num].rcve = 0;
        if (num <= TTY_MAX)
            vt_mask |= 1 << (TTY_MAX - num);
        besm6_debug ("*** console on T%03o", num);
        attached_console = 1;
        return SCPE_OK;
    }
    return SCPE_ALATT;
}

t_stat tty_detach (UNIT *u)
{
    return tmxr_detach (&tty_desc, &tty_unit[0]);
}

t_stat tty_showrate (FILE *f, UNIT *up, int32 v, CONST void *dp) {
    fprintf(f, "%d Baud", tty_rate);
    return SCPE_OK;
}

t_stat tty_showturbo (FILE *f, UNIT *up, int32 v, CONST void *dp) {
    fprintf(f, tty_turbo ? "Turbo" : "Authentic feel");
    return SCPE_OK;
}

t_stat tty_setrate (UNIT *up, int32 v, CONST char *cp, void *dp) {
    int rate;
    if (cp)
        rate = atoi(cp);
    else
        return SCPE_MISVAL;
    if (rate <= 0 || rate > 19200 || rate % 300 != 0)
        return SCPE_ARG;
    rate /= 300;
    if ((rate-1) & rate)
        return SCPE_ARG;
    tty_rate = rate * 300;
    return SCPE_OK;
}

t_stat tty_setturbo (UNIT *up, int32 v, CONST char *cp, void *dp) {
    if (!cp)
        return SCPE_MISVAL;
    if (!MATCH_CMD("ON", cp))
        tty_turbo = 1;
    else if (!MATCH_CMD("OFF", cp))
        tty_turbo = 0;
    else
        return SCPE_ARG;
    return SCPE_OK;
}

/*
 * TTY control:
 * set ttyN unicode     - selecting UTF-8 encoding
 * set ttyN jcuken      - selecting KOI-7 encoding, JCUKEN layout
 * set ttyN qwerty      - selecting KOI-7 encoding, QWERTY layout
 * set ttyN raw         - selecting transmission of raw chars
 * set ttyN off         - disconnecting a line
 * set ttyN tt          - a Baudot TTY
 * set ttyN vt          - a Videoton-340 terminal
 * set ttyN consul      - a "Consul-254" typewriter
 * set ttyN destrbs     - destructive (erasing) backspace
 * set ttyN authbs      - authentic backspace (cursor left)
 * set tty disconnect=N - forceful termination of a telnet connection
 * set tty rate=N       - I/O rate in Hz
 * set tty turbo={ON,OFF} - TTY interrupts use model time (on) or wallclock (0ff)
 * show tty             - showing modes and types
 * show tty connections - showing IP-addresses and connection times
 * show tty statistics  - showing TX/RX byte counts
 */
MTAB tty_mod[] = {
    { TTY_CHARSET_MASK, TTY_UNICODE_CHARSET, "UTF-8 input",
      "UNICODE" },
    { TTY_CHARSET_MASK, TTY_KOI7_JCUKEN_CHARSET, "KOI7 (jcuken) input",
      "JCUKEN" },
    { TTY_CHARSET_MASK, TTY_KOI7_QWERTY_CHARSET, "KOI7 (qwerty) input",
      "QWERTY" },
    { TTY_CHARSET_MASK, TTY_RAW_CHARSET, "RAW input/output",
      "RAW" },
    { TTY_STATE_MASK, TTY_OFFLINE_STATE, "offline",
      "OFF", &tty_setmode },
    { TTY_STATE_MASK, TTY_TELETYPE_STATE, "Teletype",
      "TT", &tty_setmode },
    { TTY_STATE_MASK, TTY_VT340_STATE, "Videoton-340",
      "VT", &tty_setmode },
    { TTY_STATE_MASK, TTY_CONSUL_STATE, "Consul-254",
      "CONSUL", &tty_setmode },
    { TTY_BSPACE_MASK, TTY_DESTRUCTIVE_BSPACE, "destructive backspace",
      "DESTRBS" },
    { TTY_BSPACE_MASK, TTY_AUTHENTIC_BSPACE, NULL,
      "AUTHBS" },
    { MTAB_XTD | MTAB_VDV | MTAB_VALR, 1, NULL,
      "DISCONNECT", &tmxr_dscln, NULL, (void*) &tty_desc, "terminates telnet connection" },
    { MTAB_XTD | MTAB_VDV | MTAB_VALR, 1, "RATE",
      "RATE", &tty_setrate, &tty_showrate, NULL, "{300,600,1200,2400,4800,9600,19200}" },
    { MTAB_XTD | MTAB_VDV | MTAB_VALR, 1, "TURBO",
      "TURBO", &tty_setturbo, &tty_showturbo, NULL, "{ON, OFF}"},
    { MTAB_XTD | MTAB_VDV, 0, "SUMMARY",
      NULL, NULL, &tmxr_show_summ, (void*) &tty_desc },
    { MTAB_XTD | MTAB_VDV | MTAB_NMO, 1, "CONNECTIONS",
      NULL, NULL, &tmxr_show_cstat, (void*) &tty_desc },
    { MTAB_XTD | MTAB_VDV | MTAB_NMO, 0, "STATISTICS",
      NULL, NULL, &tmxr_show_cstat, (void*) &tty_desc },
    { MTAB_XTD | MTAB_VDV | MTAB_NMO | MTAB_VALR,   0,   "LINEORDER",
      "LINEORDER",   &tmxr_set_lnorder, &tmxr_show_lnorder, (void *) &tty_desc,
      "Line connection order, e.g. 26;20-24;1-10" },
    { MTAB_XTD | MTAB_VUN | MTAB_NC, 0, NULL,
      "LOG", &tmxr_set_log, &tmxr_show_log, (void*) &tty_desc },
    { MTAB_XTD | MTAB_VUN | MTAB_NC, 0, NULL,
      "NOLOG", &tmxr_set_nolog, NULL, (void*) &tty_desc },
    { 0 }
};

DEVICE tty_dev = {
    "TTY", tty_unit, tty_reg, tty_mod,
    27, 2, 1, 1, 2, 1,
    NULL, NULL, &tty_reset, NULL, &tty_attach, &tty_detach,
    NULL, DEV_MUX|DEV_DEBUG
};

void tty_send (uint32 mask)
{
    /* besm6_debug ("*** TTY: transmit %08o", mask); */

    TTY_OUT = mask;
}

/*
 * Sending a character to a terminal with the given number.
 */
void vt_putc (int num, int c)
{
    TMLN *t = &tty_line [num];

    if (! t->conn)
        return;
    if (t->rcve) {
        /* A telnet connection. */
        tmxr_putc_ln (t, c);
    } else {
        /* Console output. */
        sim_putchar(c);
    }
}

/*
 * Sending a string to a terminal with the given number.
 */
void vt_puts (int num, const char *s)
{
    TMLN *t = &tty_line [num];

    if (! t->conn)
        return;
    if (t->rcve) {
        /* A telnet connection. */
        tmxr_linemsg (t, s);
    } else {
        /* Console output. */
        while (*s) sim_putchar(*s++);
    }
}

const char * koi7_rus_to_unicode [32] = {
    "Ю", "А", "Б", "Ц", "Д", "Е", "Ф", "Г",
    "Х", "И", "Й", "К", "Л", "М", "Н", "О",
    "П", "Я", "Р", "С", "Т", "У", "Ж", "В",
    "Ь", "Ы", "З", "Ш", "Э", "Щ", "Ч", "\0x7f",
};

/* Videoton-340 employed single byte control codes rather than ESC sequences. */
void vt_send(int num, uint32 sym)
{
    if ((tty_unit[num].flags & TTY_CHARSET_MASK) == TTY_RAW_CHARSET) {
        vt_putc(num, sym);
    } else if (sym < 0x60) {
        switch (sym) {
        case '\031':
            /* Up */
            vt_puts (num, "\033[");
            sym = 'A';
            break;
        case '\032':
            /* Down */
            vt_puts (num, "\033[");
            sym = 'B';
            break;
        case '\030':
            /* Right */
            vt_puts (num, "\033[");
            sym = 'C';
            break;
        case '\b':
            /* Left */
            vt_puts (num, "\033[");
            if ((tty_unit[num].flags & TTY_BSPACE_MASK) == TTY_DESTRUCTIVE_BSPACE) {
                /* Erasing the previous char. */
                vt_puts (num, "D \033[");
            }
            sym = 'D';
            break;
        case '\v':
        case '\033':
        case '\0':
            /* Sending the actual char */
            break;
        case '\037':
            /* Clear screen */
            vt_puts (num, "\033[H\033[");
            sym = 'J';
            break;
        case '\n':
            /* Also does carriage return */
            vt_putc (num, '\r');
            sym = '\n';
            break;
        case '\f':
            /* Home */
            vt_puts(num, "\033[");
            sym = 'H';
            break;
        case '\r':
        case '\003':
            /* Not displayed */
            sym = 0;
            break;
        default:
            if (sym < ' ') {
                /* Other control chars were displayed as dimmed. */
                vt_puts (num, "\033[2m");
                vt_putc (num, sym | 0x40);
                vt_puts (num, "\033[");
                /* Terminating the ESC sequence */
                sym = 'm';
            }
        }
        if (sym)
            vt_putc (num, sym);
    } else
        vt_puts (num, koi7_rus_to_unicode[sym - 0x60]);
}

/*
 * Handling output to all connected terminals.
 */
void vt_print()
{
    uint32 workset = (TTY_OUT & vt_mask) | vt_sending;
    int num;

    if (workset == 0) {
        ++vt_idle;
        return;
    }
    for (num = besm6_highest_bit (workset) - TTY_MAX;
         workset; num = besm6_highest_bit (workset) - TTY_MAX) {
        int mask = 1 << (TTY_MAX - num);
        int c = (TTY_OUT & mask) != 0;
        switch (tty_active[num]*2+c) {
        case 0: /* idle */
            besm6_debug ("Warning: inactive ttys should have been screened");
            continue;
        case 1: /* start bit */
            vt_sending |= mask;
            tty_active[num] = 1;
            break;
        case 18: /* stop bit */
            tty_sym[num] = ~tty_sym[num] & 0x7f;
            vt_send (num, tty_sym[num]);
            tty_active[num] = 0;
            tty_sym[num] = 0;
            vt_sending &= ~mask;
            break;
        case 19: /* framing error */
            vt_putc (num, '#');
            break;
        default:
            /* little endian ordering */
            if (c) {
                tty_sym[num] |= 1 << (tty_active[num]-1);
            }
            ++tty_active[num];
            break;
        }
        workset &= ~mask;
    }
    vt_idle = 0;
}

/* Input from Baudot TTYs not implemented. Output may require some additional work.
 */
void tt_print()
{
    uint32 workset = (TTY_OUT & tt_mask) | tt_sending;
    int num;

    if (workset == 0) {
        return;
    }

    for (num = besm6_highest_bit (workset) - TTY_MAX;
         workset; num = besm6_highest_bit (workset) - TTY_MAX) {
        int mask = 1 << (TTY_MAX - num);
        int c = (TTY_OUT & mask) != 0;
        switch (tty_active[num]*2+c) {
        case 0: /* idle */
            break;
        case 1: /* start bit */
            tt_sending |= mask;
            tty_active[num] = 1;
            break;
        case 12: /* stop bit */
            vt_puts (num, process (tty_sym[num]));
            tty_active[num] = 0;
            tty_sym[num] = 0;
            tt_sending &= ~mask;
            break;
        case 13: /* framing error */
            vt_putc (num, '#');
            break;
        default:
            /* big endian ordering */
            if (c) {
                tty_sym[num] |= 1 << (5-tty_active[num]);
            }
            ++tty_active[num];
            break;
        }
        workset &= ~mask;
    }
    vt_idle = 0;
}

/*
 * Converting from Unicode to KOI-7.
 * Returns -1 if unsuccessful.
 */
static int unicode_to_koi7 (unsigned val)
{
    if (val <= '_') return val;
    else if ('a' <= val && val <= 'z') return val + 'Z' - 'z';
    else switch (val) {
        case 0x007f:              return 0x7f;
        case 0x0410: case 0x0430: return 0x61;
        case 0x0411: case 0x0431: return 0x62;
        case 0x0412: case 0x0432: return 0x77;
        case 0x0413: case 0x0433: return 0x67;
        case 0x0414: case 0x0434: return 0x64;
        case 0x0415: case 0x0435: return 0x65;
        case 0x0416: case 0x0436: return 0x76;
        case 0x0417: case 0x0437: return 0x7a;
        case 0x0418: case 0x0438: return 0x69;
        case 0x0419: case 0x0439: return 0x6a;
        case 0x041a: case 0x043a: return 0x6b;
        case 0x041b: case 0x043b: return 0x6c;
        case 0x041c: case 0x043c: return 0x6d;
        case 0x041d: case 0x043d: return 0x6e;
        case 0x041e: case 0x043e: return 0x6f;
        case 0x041f: case 0x043f: return 0x70;
        case 0x0420: case 0x0440: return 0x72;
        case 0x0421: case 0x0441: return 0x73;
        case 0x0422: case 0x0442: return 0x74;
        case 0x0423: case 0x0443: return 0x75;
        case 0x0424: case 0x0444: return 0x66;
        case 0x0425: case 0x0445: return 0x68;
        case 0x0426: case 0x0446: return 0x63;
        case 0x0427: case 0x0447: return 0x7e;
        case 0x0428: case 0x0448: return 0x7b;
        case 0x0429: case 0x0449: return 0x7d;
        case 0x042b: case 0x044b: return 0x79;
        case 0x042c: case 0x044c: return 0x78;
        case 0x042d: case 0x044d: return 0x7c;
        case 0x042e: case 0x044e: return 0x60;
        case 0x042f: case 0x044f: return 0x71;
        }
    return -1;
}

/*
 * Set command
 */
static t_stat cmd_set (int32 num, CONST char *cptr)
{
    char gbuf [CBUFSIZE];
    int len;

    cptr = (CONST char *)get_sim_sw (cptr);
    if (! cptr)
        return SCPE_INVSW;
    if (! *cptr)
        return SCPE_NOPARAM;
    cptr = get_glyph (cptr, gbuf, 0);
    if (*cptr)
        return SCPE_2MARG;

    len = strlen (gbuf);
    if (strncmp ("UNICODE", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_CHARSET_MASK;
        tty_unit[num].flags |= TTY_UNICODE_CHARSET;
    } else if (strncmp ("JCUKEN", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_CHARSET_MASK;
        tty_unit[num].flags |= TTY_KOI7_JCUKEN_CHARSET;
    } else if (strncmp ("QWERTY", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_CHARSET_MASK;
        tty_unit[num].flags |= TTY_KOI7_QWERTY_CHARSET;
    } else if (strncmp ("RAW", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_CHARSET_MASK;
        tty_unit[num].flags |= TTY_RAW_CHARSET;
    } else if (strncmp ("TT", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_STATE_MASK;
        tty_unit[num].flags |= TTY_TELETYPE_STATE;
    } else if (strncmp ("VT", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_STATE_MASK;
        tty_unit[num].flags |= TTY_VT340_STATE;
    } else if (strncmp ("CONSUL", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_STATE_MASK;
        tty_unit[num].flags |= TTY_CONSUL_STATE;
    } else if (strncmp ("DESTRBS", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_BSPACE_MASK;
        tty_unit[num].flags |= TTY_DESTRUCTIVE_BSPACE;
    } else if (strncmp ("AUTHBS", gbuf, len) == 0) {
        tty_unit[num].flags &= ~TTY_BSPACE_MASK;
        tty_unit[num].flags |= TTY_AUTHENTIC_BSPACE;
    } else {
        return SCPE_NXPAR;
    }
    return SCPE_OK;
}

/*
 * Show command
 */
static t_stat cmd_show (int32 num, CONST char *cptr)
{
    TMLN *t = &tty_line [num];
    char gbuf [CBUFSIZE];
    MTAB *m;
    int len;

    cptr = (CONST char *)get_sim_sw (cptr);
    if (! cptr)
        return SCPE_INVSW;
    if (! *cptr) {
        sprintf (gbuf, "TTY%d", num);
        tmxr_linemsg (t, gbuf);
        for (m=tty_mod; m->mask; m++) {
            if (m->pstring &&
                (tty_unit[num].flags & m->mask) == m->match) {
                tmxr_linemsg (t, ", ");
                tmxr_linemsg (t, m->pstring);
            }
        }
        if (t->txlog)
            tmxr_linemsg (t, ", log");
        tmxr_linemsg (t, "\r\n");
        return SCPE_OK;
    }
    cptr = get_glyph (cptr, gbuf, 0);
    if (*cptr)
        return SCPE_2MARG;

    len = strlen (gbuf);
    if (strncmp ("STATISTICS", gbuf, len) == 0) {
        sprintf (gbuf, "line %d: input queued/total = %d/%d, "
                 "output queued/total = %d/%d\r\n", num,
                 t->rxbpi - t->rxbpr, t->rxcnt,
                 t->txbpi - t->txbpr, t->txcnt);
        tmxr_linemsg (t, gbuf);
    } else {
        return SCPE_NXPAR;
    }
    return SCPE_OK;
}

/*
 * Exit command
 */
static t_stat cmd_exit (int32 num, CONST char *cptr)
{
    return SCPE_EXIT;
}

static t_stat cmd_help (int32 num, CONST char *cptr);

static CTAB cmd_table[] = {
    { "SET", &cmd_set, 0,
      "set unicode              select UTF-8 encoding\r\n"
      "set jcuken               select KOI7 encoding, 'jcuken' keymap\r\n"
      "set qwerty               select KOI7 encoding, 'qwerty' keymap\r\n"
      "set raw                  select no I/O conversions\r\n"
      "set tt                   use Teletype mode\r\n"
      "set vt                   use Videoton-340 mode\r\n"
      "set consul               use Consul-254 mode\r\n"
      "set destrbs              destructive backspace\r\n"
      "set authbs               authentic backspace\r\n"
    },
    { "SHOW", &cmd_show, 0,
      "sh{ow}                   show modes of the terminal\r\n"
      "sh{ow} s{tatistics}      show network statistics\r\n"
    },
    { "EXIT", &cmd_exit, 0,
      "exi{t} | q{uit} | by{e}  exit from simulation\r\n"
    },
    { "QUIT", &cmd_exit, 0, NULL
    },
    { "BYE", &cmd_exit, 0, NULL
    },
    { "HELP", &cmd_help, 0,
      "h{elp}                   type this message\r\n"
      "h{elp} <command>         type help for command\r\n"
    },
    { 0 }
};

/*
 * Find command routine
 */
static CTAB *lookup_cmd (char *command)
{
    CTAB *c;
    int len;

    len = strlen (command);
    for (c=cmd_table; c->name; c++) {
        if (strncmp (command, c->name, len) == 0)
            return c;
    }
    return 0;
}

/*
 * Help command
 */
static t_stat cmd_help (int32 num, CONST char *cptr)
{
    TMLN *t = &tty_line [num];
    char gbuf [CBUFSIZE];
    CTAB *c;

    cptr = (CONST char *)get_sim_sw (cptr);
    if (! cptr)
        return SCPE_INVSW;
    if (! *cptr) {
        /* Listing all commands. */
        tmxr_linemsg (t, "Commands may be abbreviated.  Commands are:\r\n\r\n");
        for (c=cmd_table; c && c->name; c++)
            if (c->help)
                tmxr_linemsg (t, c->help);
        return SCPE_OK;
    }
    cptr = get_glyph (cptr, gbuf, 0);
    if (*cptr)
        return SCPE_2MARG;
    c = lookup_cmd (gbuf);
    if (! c)
        return SCPE_ARG;
    /* Describing a command. */
    tmxr_linemsg (t, c->help);
    return SCPE_OK;
}

/*
 * Executing a command.
 */
void vt_cmd_exec (int num)
{
    TMLN *t = &tty_line [num];
    char gbuf [CBUFSIZE];
    CONST char *cptr;
    CTAB *cmdp;
    t_stat err;

    cptr = get_glyph (vt_cbuf [num], gbuf, 0);      /* get command glyph */
    cmdp = lookup_cmd (gbuf);                       /* lookup command */
    if (! cmdp) {
        tmxr_linemsg (t, sim_error_text (SCPE_UNK));
        tmxr_linemsg (t, "\r\n");
        return;
    }
    err = cmdp->action (num, cptr);                 /* if found, exec */
    if (err >= SCPE_BASE) {                         /* error? */
        tmxr_linemsg (t, sim_error_text (err));
        tmxr_linemsg (t, "\r\n");
    }
    if (err == SCPE_EXIT) {                         /* close telnet session */
        tmxr_reset_ln (t);
    }
}

/*
 * Command line interface mode.
 */
void vt_cmd_loop (int num, int c)
{
    TMLN *t = &tty_line [num];
    char *cbuf, **cptr;

    cbuf = vt_cbuf [num];
    cptr = &vt_cptr [num];

    switch (c) {
    case '\r':
    case '\n':
        tmxr_linemsg (t, "\r\n");
        if (*cptr <= cbuf) {
            /* An empty line - returning to terminal emulation. */
            tty_unit[num].flags &= ~TTY_CMDLINE_MASK;
            break;
        }
        /* Executing. */
        **cptr = 0;
        vt_cmd_exec (num);
        tmxr_linemsg (t, "sim>");
        *cptr = vt_cbuf[num];
        break;
    case '\b':
    case 0177:
        /* Backspace. */
        if (*cptr <= cbuf)
            break;
        tmxr_linemsg (t, "\b \b");
        while (*cptr > cbuf) {
            --*cptr;
            if (! (**cptr & 0x80))
                break;
        }
        break;
    case 'U' & 037:
        /* Erase line. */
        erase_line:     while (*cptr > cbuf) {
            --*cptr;
            if (! (**cptr & 0x80))
                tmxr_linemsg (t, "\b \b");
        }
        break;
    case 033:
        /* Escape [ X. */
        if (tmxr_getc_ln (t) != '[' + TMXR_VALID)
            break;
        switch (tmxr_getc_ln (t) - TMXR_VALID) {
        case 'A': /* Up arrow */
            if (*cptr <= cbuf) {
                *cptr = cbuf + strlen (cbuf);
                if (*cptr > cbuf)
                    tmxr_linemsg (t, cbuf);
            }
            break;
        case 'B': /* Down arrow */
            goto erase_line;
        }
        break;
    default:
        if (c < ' ' || *cptr > cbuf+CBUFSIZE-5)
            break;
        *(*cptr)++ = c;
        tmxr_putc_ln (t, c);
        break;
    }
}

/*
 * Getting a char from a terminal with the given number.
 * Returns -1 if there is no char to input.
 */
int vt_getc (int num)
{
    TMLN *t = &tty_line [num];
    extern int32 sim_int_char;
    int c;
#ifdef REMOTE_TIMEOUT
    time_t now;
#endif
    if (! t->conn) {
        /* Пользователь отключился. */
        if (t->ipad) {
            besm6_debug ("*** tty%d: disconnecting %s",
                         num,
                         t->ipad);
            t->ipad = NULL;
        }
        tty_setmode (tty_unit+num, TTY_OFFLINE_STATE, 0, 0);
        tty_unit[num].flags &= ~TTY_STATE_MASK;
        return -2;
    }
    if (t->rcve) {
        /* A telnet line. */
        c = tmxr_getc_ln (t);
        if (! (c & TMXR_VALID)) {
#ifdef REMOTE_TIMEOUT
            now = sim_get_time (0);
            if (now > tty_last_time[num] + 5*60) {
                ++tty_idle_count[num];
                if (tty_idle_count[num] > 3) {
                    tmxr_linemsg (t, "\r\nSIMH: END OF SESSION\r\n");
                    tmxr_reset_ln (t);
                    return -1;
                }
                tmxr_linemsg (t, "\r\nSIMH: WAKE UP!\r\n");
                tty_last_time[num] = now;
            }
#endif
            return -1;
        }
        tty_idle_count[num] = 0;
        tty_last_time[num] = sim_get_time (0);

        if (tty_unit[num].flags & TTY_CMDLINE_MASK) {
            /* Continuing CLI mode. */
            vt_cmd_loop (num, c & 0377);
            return -1;
        }
        if ((c & 0377) == sim_int_char) {
            /* Entering CLI mode. */
            tty_unit[num].flags |= TTY_CMDLINE_MASK;
            tmxr_linemsg (t, "sim>");
            vt_cptr[num] = vt_cbuf[num];
            return -1;
        }
    } else {
        /* Console (keyboard) input. */
        c = sim_poll_kbd();
        if (! (c & SCPE_KFLAG))
            return -1;
    }
    return c & 0377;
}

/*
 * Reading UTF-8, returning KOI-7.
 * The resulting char is in the range 0..0177.
 * If no input, returns -1.
 */
static int vt_kbd_input_unicode (int num)
{
    int c1, c2, c3, r;
  again:
    r = vt_getc (num);
    if (r < 0 || r > 0377)
        return r;
    c1 = r & 0377;
    if (! (c1 & 0x80))
        return unicode_to_koi7 (c1);

    r = vt_getc (num);
    if (r < 0 || r > 0377)
        return r;
    c2 = r & 0377;
    if (! (c1 & 0x20))
        return unicode_to_koi7 ((c1 & 0x1f) << 6 | (c2 & 0x3f));

    r = vt_getc (num);
    if (r < 0 || r > 0377)
        return r;
    c3 = r & 0377;
    if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
        /* Skip zero width no-break space. */
        goto again;
    }
    return unicode_to_koi7 ((c1 & 0x0f) << 12 | (c2 & 0x3f) << 6 |
                            (c3 & 0x3f));
}

/*
 * Alternatively, entering Cyrillics can be done without switching keyboard layouts.
 * Period and comma are entered with shift, less-than and greater-than are mapped to tilde-grave.
 * Semicolon is }, quote is |.
 */
static int vt_kbd_input_koi7 (int num)
{
    int r;

    r = vt_getc (num);
    if (r < 0 || r > 0377)
        return r;
    r &= 0377;
    switch (r) {
    case '\r': return '\003';
    case 'q': return 'j';
    case 'w': return 'c';
    case 'e': return 'u';
    case 'r': return 'k';
    case 't': return 'e';
    case 'y': return 'n';
    case 'u': return 'g';
    case 'i': return '{';
    case 'o': return '}';
    case 'p': return 'z';
    case '[': return 'h';
    case '{': return '[';
    case 'a': return 'f';
    case 's': return 'y';
    case 'd': return 'w';
    case 'f': return 'a';
    case 'g': return 'p';
    case 'h': return 'r';
    case 'j': return 'o';
    case 'k': return 'l';
    case 'l': return 'd';
    case ';': return 'v';
    case '}': return ';';
    case '\'': return '|';
    case '|': return '\'';
    case 'z': return 'q';
    case 'x': return '~';
    case 'c': return 's';
    case 'v': return 'm';
    case 'b': return 'i';
    case 'n': return 't';
    case 'm': return 'x';
    case ',': return 'b';
    case '<': return ',';
    case '.': return '`';
    case '>': return '.';
    case '~': return '>';
    case '`': return '<';
    default: return r;
    }
}

int odd_parity(unsigned char c)
{
    c = (c & 0x55) + ((c >> 1) & 0x55);
    c = (c & 0x33) + ((c >> 2) & 0x33);
    c = (c & 0x0F) + ((c >> 4) & 0x0F);
    return c & 1;
}

/*
 * Converting Enter and Backspace to conventional values,
 * unless the mode is RAW.
 */
int vt_fix(int num, int c) {
    if ((tty_unit[num].flags & TTY_CHARSET_MASK) != TTY_RAW_CHARSET) {
        switch (c) {
        case '\r': case '\n':
            return 3;     /* ETX is used as Enter */
        case '\177':
            return '\b';  /* ASCII DEL -> BS */
        }
    }
    return c;
}

/*
 * Handling input from all connected terminals.
 */
void vt_receive()
{
    uint32 workset = vt_mask;
    int num;

    TTY_IN = 0;
    for (num = besm6_highest_bit (workset) - TTY_MAX;
         workset; num = besm6_highest_bit (workset) - TTY_MAX) {
        uint32 mask = 1 << (TTY_MAX - num);
        switch (tty_instate[num]) {
        case 0:
            if (tty_typed[num] <= -2) {
                /* A "physically" disconnected line; upon reconnecting,
                 * there will be no echo and no reaction to commands;
                 * <enter>HYC<enter> needs to be typed
                 * to re-initialize the line
                 */
                TTY_IN |= mask;         /* "long start" */
                break;
            }
            switch (tty_unit[num].flags & TTY_CHARSET_MASK) {
            case TTY_KOI7_JCUKEN_CHARSET:
                tty_typed[num] = vt_kbd_input_koi7 (num);
                break;
            case TTY_RAW_CHARSET:
            case TTY_KOI7_QWERTY_CHARSET:
                tty_typed[num] = vt_getc (num);
                break;
            case TTY_UNICODE_CHARSET:
                tty_typed[num] = vt_kbd_input_unicode (num);
                break;
            default:
                tty_typed[num] = '?';
                break;
            }
            if (tty_typed[num] < 0) {
                break;
            }
            if (tty_typed[num] <= 0177) {
                tty_typed[num] = vt_fix (num, tty_typed[num]);
                tty_instate[num] = 1;
                TTY_IN |= mask;         /* start bit */
                GRP |= GRP_TTY_START;   /* not used ? */
                /* auto-enabling the interrupt just in case
                 * (seems to be unneeded as the interrupt is never disabled)
                 */
                MGRP |= GRP_SERIAL;
                vt_receiving |= mask;
            }
            break;
        case 1: case 2: case 3: case 4: case 5: case 6: case 7:
            /* need inverted byte */
            TTY_IN |= (tty_typed[num] & (1 << (tty_instate[num]-1))) ? 0 : mask;
                tty_instate[num]++;
                break;
        case 8:
            TTY_IN |= odd_parity(tty_typed[num]) ? 0 : mask;        /* even parity of inverted */
            tty_instate[num]++;
            break;
        case 9: case 10: case 11:
            /* stop bits are 0 */
            tty_instate[num]++;
            break;
        case 12:
            tty_instate[num] = 0;   /* ready for the next char */
            vt_receiving &= ~mask;
            break;
        }
        workset &= ~mask;
    }
    if (vt_receiving)
        vt_idle = 0;
}

/*
 * Checking if all terminals are idle.
 * SIMH should not enter idle mode until they are.
 */
int vt_is_idle ()
{
    return (tt_mask ? vt_idle > 300 : vt_idle > 10);
}

int tty_query ()
{
    /*      besm6_debug ("*** TTY: query");*/
    return TTY_IN;
}

static char cons_is_printing[2];

void consul_print (int dev_num, uint32 cmd)
{
    extern unsigned short gost_to_unicode(unsigned char);
    extern void uni2utf8(unsigned short ch, char buf[5]);
    int uni;
    char buf[5];
    int line_num = dev_num + TTY_MAX + 1;
    if (tty_dev.dctrl)
        besm6_debug(">>> CONSUL%o: %03o", line_num, cmd & 0377);
    
    READY2 &= ~CONS_READY[dev_num]; /* temporarily not ready  */
    
    switch (tty_unit[line_num].flags & TTY_STATE_MASK) {
    case TTY_VT340_STATE:
        vt_send (line_num, cmd & 0177);
        break;
    case TTY_CONSUL_STATE:
        uni = gost_to_unicode(cmd & 0177);
        uni2utf8(uni, buf);
        vt_puts(line_num, buf);
        break;
    }
    cons_is_printing[dev_num] = 1;
    vt_idle = 0;
}

void consul_receive ()
{
    int c, line_num, dev_num;

    for (dev_num = 0; dev_num < 2; ++dev_num){
        if (cons_is_printing[dev_num]) {
            READY2 |= CONS_READY[dev_num];
            PRP |= CONS_CAN_PRINT[dev_num];
            cons_is_printing[dev_num] = 0;
        }
        line_num = dev_num + TTY_MAX + 1;
        if (! tty_line[line_num].conn)
            continue;
        switch (tty_unit[line_num].flags & TTY_CHARSET_MASK) {
        case TTY_KOI7_JCUKEN_CHARSET:
            c = vt_kbd_input_koi7 (line_num);
            break;
        case TTY_RAW_CHARSET:
        case TTY_KOI7_QWERTY_CHARSET:
            c = vt_getc (line_num);
            break;
        case TTY_UNICODE_CHARSET:
            c = vt_kbd_input_unicode (line_num);
            break;
        default:
            c = '?';
            break;
        }
        if (c >= 0 && c <= 0177) {
            c = vt_fix (line_num, c);
            CONSUL_IN[dev_num] = odd_parity(c) ? c | 0200 : c;
            PRP |= CONS_HAS_INPUT[dev_num];
            vt_idle = 0;
        }
    }
}

uint32 consul_read (int num)
{
    if (tty_dev.dctrl)
        besm6_debug("<<< CONSUL%o: %03o", num+TTY_MAX+1, CONSUL_IN[num]);
    return CONSUL_IN[num];
}
