/*  altairz80_sio: MITS Altair serial I/O card

    Copyright (c) 2002-2005, Peter Schorn

    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
    PETER SCHORN 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 Peter Schorn shall not
    be used in advertising or otherwise to promote the sale, use or other dealings
    in this Software without prior written authorization from Peter Schorn.

    Based on work by Charles E Owen (c) 1997

    These functions support a simulated MITS 2SIO interface card.
    The card had two physical I/O ports which could be connected
    to any serial I/O device that would connect to a current loop,
    RS232, or TTY interface. Available baud rates were jumper
    selectable for each port from 110 to 9600.

    All I/O is via programmed I/O. Each device has a status port
    and a data port. A write to the status port can select
    some options for the device (0x03 will reset the port).
    A read of the status port gets the port status:

    +---+---+---+---+---+---+---+---+
    | X | X | X | X | X | X | O | I |
    +---+---+---+---+---+---+---+---+

    I - A 1 in this bit position means a character has been received
        on the data port and is ready to be read.
    O - A 1 in this bit means the port is ready to receive a character
        on the data port and transmit it out over the serial line.

    A read to the data port gets the buffered character, a write
    to the data port writes the character to the device.
*/

#include <ctype.h>

#include "altairz80_defs.h"
#include "sim_sock.h"
#include "sim_tmxr.h"
#include <time.h>

#define UNIT_V_ANSI         (UNIT_V_UF + 0)     /* ANSI mode, strip bit 8 on output             */
#define UNIT_ANSI           (1 << UNIT_V_ANSI)
#define UNIT_V_UPPER        (UNIT_V_UF + 1)     /* upper case mode                              */
#define UNIT_UPPER          (1 << UNIT_V_UPPER)
#define UNIT_V_BS           (UNIT_V_UF + 2)     /* map delete to backspace                      */
#define UNIT_BS             (1 << UNIT_V_BS)
#define UNIT_V_SIO_VERBOSE  (UNIT_V_UF + 3)     /* verbose mode, i.e. show error messages       */
#define UNIT_SIO_VERBOSE    (1 << UNIT_V_SIO_VERBOSE)
#define UNIT_V_MAP          (UNIT_V_UF + 4)     /* mapping mode on                              */
#define UNIT_MAP            (1 << UNIT_V_MAP)

#define UNIT_V_SIMH_VERBOSE (UNIT_V_UF + 0)     /* verbose mode for SIMH pseudo device          */
#define UNIT_SIMH_VERBOSE   (1 << UNIT_V_SIMH_VERBOSE)
#define UNIT_V_SIMH_TIMERON (UNIT_V_UF + 1)     /* SIMH pseudo device timer generate interrupts */
#define UNIT_SIMH_TIMERON   (1 << UNIT_V_SIMH_VERBOSE)

#define Terminals       4                       /* lines per mux                                */

#define BACKSPACE_CHAR  0x08                    /* backspace character                          */
#define DELETE_CHAR     0x7f                    /* delete character                             */
#define CONTROLZ_CHAR   0x1a                    /* control Z character                          */

static void resetSIOWarningFlags(void);
static t_stat sio_set_verbose       (UNIT *uptr, int32 value, char *cptr, void *desc);
static t_stat simh_dev_set_timeron  (UNIT *uptr, int32 value, char *cptr, void *desc);
static t_stat simh_dev_set_timeroff (UNIT *uptr, int32 value, char *cptr, void *desc);
static t_stat sio_svc(UNIT *uptr);
static t_stat sio_reset(DEVICE *dptr);
static t_stat sio_attach(UNIT *uptr, char *cptr);
static t_stat sio_detach(UNIT *uptr);
static t_stat ptr_reset(DEVICE *dptr);
static t_stat ptp_reset(DEVICE *dptr);
int32 nulldev   (const int32 port, const int32 io, const int32 data);
int32 sr_dev    (const int32 port, const int32 io, const int32 data);
int32 simh_dev  (const int32 port, const int32 io, const int32 data);
int32 sio0d     (const int32 port, const int32 io, const int32 data);
int32 sio0s     (const int32 port, const int32 io, const int32 data);
int32 sio1d     (const int32 port, const int32 io, const int32 data);
int32 sio1s     (const int32 port, const int32 io, const int32 data);
static void reset_sio_terminals(const int32 useDefault);
static t_stat simh_dev_reset(DEVICE *dptr);
static t_stat simh_svc(UNIT *uptr);
static int32 simh_in(const int32 port);
static int32 simh_out(const int32 port, const int32 data);
static void attachCPM(UNIT *uptr);
static void setClockZSDOS(void);
static void setClockCPM3(void);
static time_t mkCPM3Origin(void);
static int32 toBCD(const int32 x);
static int32 fromBCD(const int32 x);
void printMessage(void);
static void warnNoRealTimeClock(void);

extern t_stat sim_activate(UNIT *uptr, int32 interval);
extern t_stat sim_cancel(UNIT *uptr);
extern t_stat sim_poll_kbd(void);
extern t_stat sim_putchar(int32 out);
extern t_stat attach_unit(UNIT *uptr, char *cptr);
extern int32 getBankSelect(void);
extern void setBankSelect(int32 b);
extern uint32 getCommon(void);
extern t_bool rtc_avail;
extern FILE *sim_log;
extern int32 PCX;
extern int32 sim_switches;
extern uint32 sim_os_msec(void);
extern const char *scp_error_messages[];
extern int32 SR;
extern uint8 GetBYTEWrapper(register uint32 Addr);
extern UNIT cpu_unit;

/* SIMH pseudo device status registers                                                                          */
/* ZSDOS clock definitions                                                                                      */
static time_t ClockZSDOSDelta       = 0;        /* delta between real clock and Altair clock                    */
static int32 setClockZSDOSPos       = 0;        /* determines state for receiving address of parameter block    */
static int32 setClockZSDOSAdr       = 0;        /* address in M of 6 byte parameter block for setting time      */
static int32 getClockZSDOSPos       = 0;        /* determines state for sending clock information               */

/* CPM3 clock definitions                                                                                       */
static time_t ClockCPM3Delta        = 0;        /* delta between real clock and Altair clock                    */
static int32 setClockCPM3Pos        = 0;        /* determines state for receiving address of parameter block    */
static int32 setClockCPM3Adr        = 0;        /* address in M of 5 byte parameter block for setting time      */
static int32 getClockCPM3Pos        = 0;        /* determines state for sending clock information               */
static int32 daysCPM3SinceOrg       = 0;        /* days since 1 Jan 1978                                        */

/* interrupt related                                                                                            */
static uint32 timeOfNextInterrupt;              /* time when next interrupt is scheduled                        */
       int32 timerInterrupt         = FALSE;    /* timer interrupt pending                                      */
       int32 timerInterruptHandler  = 0x0fc00;  /* default address of interrupt handling routine                */
static int32 setTimerInterruptAdrPos= 0;        /* determines state for receiving timerInterruptHandler         */
static int32 timerDelta             = 100;      /* interrupt every 100 ms                                       */
static int32 setTimerDeltaPos       = 0;        /* determines state for receiving timerDelta                    */

/* stop watch and timer related                                                                                 */
static uint32 stopWatchDelta        = 0;        /* stores elapsed time of stop watch                            */
static int32 getStopWatchDeltaPos   = 0;        /* determines the state for receiving stopWatchDelta            */
static uint32 stopWatchNow          = 0;        /* stores starting time of stop watch                           */
static int32 markTimeSP             = 0;        /* stack pointer for timer stack                                */

/* miscellaneous                                                                                                */
static int32 versionPos             = 0;        /* determines state for sending device identifier               */
static int32 lastCPMStatus          = 0;        /* result of last attachCPM command                             */
static int32 lastCommand            = 0;        /* most recent command processed on port 0xfeh                  */
static int32 getCommonPos           = 0;        /* determines state for sending the 'common' register           */

/* SIO status registers                                                                                         */
static int32 warnLevelSIO           = 3;        /* display at most 'warnLevelSIO' times the same warning        */
static int32 warnUnattachedPTP      = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and output to PTP without an attached file              */
static int32 warnUnattachedPTR      = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and attempt to read from PTR without an attached file   */
static int32 warnPTREOF             = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and attempt to read from PTR past EOF                   */
static int32 warnUnassignedPort     = 0;        /* display a warning message if < warnLevel and SIO set to
                                                VERBOSE and attempt to perform IN or OUT on an unassigned PORT  */
struct sio_terminal {
    int32 data;             /* data for this terminal                   */
    int32 status;           /* status information for this terminal     */
    int32 statusPort;       /* status port of this terminal             */
    int32 dataPort;         /* data port of this terminal               */
    int32 defaultStatus;    /* default status value for this terminal   */
};

typedef struct sio_terminal SIO_TERMINAL;

static SIO_TERMINAL sio_terminals[Terminals] = {
    {0, 0, 0x10, 0x11, 0x02},
    {0, 0, 0x14, 0x15, 0x00},
    {0, 0, 0x16, 0x17, 0x00},
    {0, 0, 0x18, 0x19, 0x00}
};
static TMLN TerminalLines[Terminals] = {    /* four terminals       */
    {0}
};
static TMXR altairTMXR = {                  /* mux descriptor       */
    Terminals, 0, 0, TerminalLines
};

static UNIT sio_unit = { UDATA (&sio_svc, UNIT_ATTABLE + UNIT_MAP, 0), KBD_POLL_WAIT };

static REG sio_reg[] = {
    { HRDATA (DATA0,    sio_terminals[0].data,      8)  },
    { HRDATA (STAT0,    sio_terminals[0].status,    8)  },
    { HRDATA (DATA1,    sio_terminals[1].data,      8)  },
    { HRDATA (STAT1,    sio_terminals[1].status,    8)  },
    { HRDATA (DATA2,    sio_terminals[2].data,      8)  },
    { HRDATA (STAT2,    sio_terminals[2].status,    8)  },
    { HRDATA (DATA3,    sio_terminals[3].data,      8)  },
    { HRDATA (STAT3,    sio_terminals[3].status,    8)  },
    { DRDATA (SIOWL,    warnLevelSIO,               32) },
    { DRDATA (WUPTP,    warnUnattachedPTP,          32) },
    { DRDATA (WUPTR,    warnUnattachedPTR,          32) },
    { DRDATA (WPTREOF,  warnPTREOF,                 32) },
    { DRDATA (WUPORT,   warnUnassignedPort,         32) },
    { NULL }
};

static MTAB sio_mod[] = {
    { UNIT_ANSI,        0,                  "TTY",      "TTY",      NULL }, /* keep bit 8 as is for output              */
    { UNIT_ANSI,        UNIT_ANSI,          "ANSI",     "ANSI",     NULL }, /* set bit 8 to 0 before output             */
    { UNIT_UPPER,       0,                  "ALL",      "ALL",      NULL }, /* do not change case of input characters   */
    { UNIT_UPPER,       UNIT_UPPER,         "UPPER",    "UPPER",    NULL }, /* change input characters to upper case    */
    { UNIT_BS,          0,                  "BS",       "BS",       NULL }, /* map delete to backspace                  */
    { UNIT_BS,          UNIT_BS,            "DEL",      "DEL",      NULL }, /* map backspace to delete                  */
    { UNIT_SIO_VERBOSE, 0,                  "QUIET",    "QUIET",    NULL }, /* quiet, no error messages                 */
    { UNIT_SIO_VERBOSE, UNIT_SIO_VERBOSE,   "VERBOSE",  "VERBOSE",  &sio_set_verbose },
                                                                            /* verbose, display warning messages        */
    { UNIT_MAP,         0,                  "NOMAP",    "NOMAP",    NULL }, /*  disable character mapping               */
    { UNIT_MAP,         UNIT_MAP,           "MAP",      "MAP",      NULL }, /*  enable all character mapping            */
    { 0 }
};

DEVICE sio_dev = {
    "SIO", &sio_unit, sio_reg, sio_mod,
    1, 10, 31, 1, 8, 8,
    NULL, NULL, &sio_reset,
    NULL, &sio_attach, &sio_detach,
    NULL, 0, 0,
    NULL, NULL, NULL };

static UNIT ptr_unit = {
    UDATA (NULL, UNIT_SEQ + UNIT_ATTABLE + UNIT_ROABLE, 0), KBD_POLL_WAIT
};

static REG ptr_reg[] = {
    { HRDATA (DATA, ptr_unit.buf,   8)  },
    { HRDATA (STAT, ptr_unit.u3,    8)  },
    { DRDATA (POS,  ptr_unit.pos,   32) },
    { NULL }
};

DEVICE ptr_dev = {
    "PTR", &ptr_unit, ptr_reg, NULL,
    1, 10, 31, 1, 8, 8,
    NULL, NULL, &ptr_reset,
    NULL, NULL, NULL,
    NULL, 0, 0,
    NULL, NULL, NULL
};

static UNIT ptp_unit = {
    UDATA (NULL, UNIT_SEQ + UNIT_ATTABLE, 0), KBD_POLL_WAIT
};

static REG ptp_reg[] = {
    { HRDATA (DATA, ptp_unit.buf,   8)  },
    { HRDATA (STAT, ptp_unit.u3,    8)  },
    { DRDATA (POS,  ptp_unit.pos,   32) },
    { NULL }
};

DEVICE ptp_dev = {
    "PTP", &ptp_unit, ptp_reg, NULL,
    1, 10, 31, 1, 8, 8,
    NULL, NULL, &ptp_reset,
    NULL, NULL, NULL,
    NULL, 0, 0,
    NULL, NULL, NULL
};

/*  Synthetic device SIMH for communication
    between Altair and SIMH environment using port 0xfe */
static UNIT simh_unit = {
    UDATA (&simh_svc, 0, 0), KBD_POLL_WAIT
};

static REG simh_reg[] = {
    { DRDATA (CZD,      ClockZSDOSDelta,        32)             },
    { DRDATA (SCZP,     setClockZSDOSPos,       8),     REG_RO  },
    { HRDATA (SCZA,     setClockZSDOSAdr,       16),    REG_RO  },
    { DRDATA (GCZP,     getClockZSDOSPos,       8),     REG_RO  },

    { DRDATA (CC3D,     ClockCPM3Delta,         32)             },
    { DRDATA (SC3DP,    setClockCPM3Pos,        8),     REG_RO  },
    { HRDATA (SC3DA,    setClockCPM3Adr,        16),    REG_RO  },
    { DRDATA (GC3DP,    getClockCPM3Pos,        8),     REG_RO  },
    { DRDATA (D3DO,     daysCPM3SinceOrg,       32),    REG_RO  },

    { DRDATA (TOFNI,    timeOfNextInterrupt,    32),    REG_RO  },
    { DRDATA (TIMI,     timerInterrupt,         3)              },
    { HRDATA (TIMH,     timerInterruptHandler,  16)             },
    { DRDATA (STIAP,    setTimerInterruptAdrPos,8),     REG_RO  },
    { DRDATA (TIMD,     timerDelta,             32)             },
    { DRDATA (STDP,     setTimerDeltaPos,       8),     REG_RO  },

    { DRDATA (STPDT,    stopWatchDelta,         32),    REG_RO  },
    { DRDATA (STPOS,    getStopWatchDeltaPos,   8),     REG_RO  },
    { DRDATA (STPNW,    stopWatchNow,           32),    REG_RO  },
    { DRDATA (MTSP,     markTimeSP,             8),     REG_RO  },

    { DRDATA (VPOS,     versionPos,             8),     REG_RO  },
    { DRDATA (LCPMS,    lastCPMStatus,          8),     REG_RO  },
    { DRDATA (LCMD,     lastCommand,            8),     REG_RO  },
    { DRDATA (CPOS,     getCommonPos,           8),     REG_RO  },
    { NULL }
};

static MTAB simh_mod[] = {
    /* quiet, no warning messages           */
    { UNIT_SIMH_VERBOSE,    0,                  "QUIET",    "QUIET",    NULL                    },
    /* verbose, display warning messages    */
    { UNIT_SIMH_VERBOSE,    UNIT_SIMH_VERBOSE,  "VERBOSE",  "VERBOSE",  NULL                    },
    /* timer generated interrupts are off   */
    { UNIT_SIMH_TIMERON,    0,                  "TIMEROFF", "TIMEROFF", &simh_dev_set_timeroff  },
    /* timer generated interrupts are on    */
    { UNIT_SIMH_TIMERON,    UNIT_SIMH_TIMERON,  "TIMERON",  "TIMERON",  &simh_dev_set_timeron   },
    { 0 }
};

DEVICE simh_device = {
    "SIMH", &simh_unit, simh_reg, simh_mod,
    1, 10, 31, 1, 16, 4,
    NULL, NULL, &simh_dev_reset,
    NULL, NULL, NULL,
    NULL, 0, 0,
    NULL, NULL, NULL
};

char messageBuffer[256];

void printMessage(void) {
    printf(messageBuffer);
#if defined(__NetBSD__) || defined (__OpenBSD__) || defined (__FreeBSD__) || defined (__APPLE__)
/* need to make sure that carriage return is executed - ttrunstate() of scp_tty.c
    has disabled \n translation */
    printf("\r\n");
#else
    printf("\n");
#endif
    if (sim_log) {
        fprintf(sim_log, messageBuffer);
        fprintf(sim_log,"\n");
    }
}

static void resetSIOWarningFlags(void) {
    warnUnattachedPTP   = 0;
    warnUnattachedPTR   = 0;
    warnPTREOF          = 0;
    warnUnassignedPort  = 0;
}

static t_stat sio_set_verbose(UNIT *uptr, int32 value, char *cptr, void *desc) {
    resetSIOWarningFlags();
    return SCPE_OK;
}

static t_stat sio_attach(UNIT *uptr, char *cptr) {
    reset_sio_terminals(FALSE);
    return tmxr_attach(&altairTMXR, uptr, cptr);    /* attach mux */
}

static void reset_sio_terminals(const int32 useDefault) {
    int32 i;
    for (i = 0; i < Terminals; i++) {
        sio_terminals[i].status = useDefault ? sio_terminals[i].defaultStatus : 0;  /* status   */
        sio_terminals[i].data   = 0x00;                                             /* data     */
    }
}

/* detach */
static t_stat sio_detach(UNIT *uptr) {
    reset_sio_terminals(TRUE);
    return tmxr_detach(&altairTMXR, uptr);
}

/* service routines to handle simulator functions */

/* service routine - actually gets char & places in buffer */

static t_stat sio_svc(UNIT *uptr) {
    int32 temp;

    sim_activate(&sio_unit, sio_unit.wait);         /* continue poll        */

    if (sio_unit.flags & UNIT_ATT) {
        if (sim_poll_kbd() == SCPE_STOP) {          /* listen for ^E        */
            return SCPE_STOP;
        }
        temp = tmxr_poll_conn(&altairTMXR);         /* poll connection      */
        if (temp >= 0) {
            TerminalLines[temp].rcve = 1;           /* enable receive       */
        }
        tmxr_poll_rx(&altairTMXR);                  /* poll input           */
        tmxr_poll_tx(&altairTMXR);                  /* poll output          */
    }
    else {
        if ((temp = sim_poll_kbd()) < SCPE_KFLAG) {
            return temp;                            /* no char or error?    */
        }
        sio_terminals[0].data = temp & 0xff;        /* save character       */
        sio_terminals[0].status |= 0x01;            /* set status           */
    }
    return SCPE_OK;
}


/* reset routines */

static t_stat sio_reset(DEVICE *dptr) {
    int32 i;
    resetSIOWarningFlags();
    if (sio_unit.flags & UNIT_ATT) {
        for (i = 0; i < Terminals; i++) {
            if (TerminalLines[i].conn > 0) {
                tmxr_reset_ln(&TerminalLines[i]);
            }
        }
        reset_sio_terminals(FALSE);
    }
    else {
        reset_sio_terminals(TRUE);
    }
    sim_activate(&sio_unit, sio_unit.wait); /* activate unit   */
    return SCPE_OK;
}

static t_stat ptr_reset(DEVICE *dptr) {
    resetSIOWarningFlags();
    ptr_unit.buf    = 0;
    ptr_unit.u3 = 0;
    ptr_unit.pos    = 0;
    if (ptr_unit.flags & UNIT_ATT)  {       /* attached?       */
        rewind(ptr_dev.units -> fileref);
    }
    sim_cancel(&ptp_unit);                  /* deactivate unit */
    return SCPE_OK;
}

static t_stat ptp_reset(DEVICE *dptr) {
    resetSIOWarningFlags();
    ptp_unit.buf    = 0;
    ptp_unit.u3     = 0x02;
    sim_cancel(&ptp_unit);                  /* deactivate unit */
    return SCPE_OK;
}


/*  I/O instruction handlers, called from the CPU module when an
    IN or OUT instruction is issued.

    Each function is passed an 'io' flag, where 0 means a read from
    the port, and 1 means a write to the port. On input, the actual
    input is passed as the return value, on output, 'data' is written
    to the device.

    Port 1 controls console I/O. We distinguish two cases:
    1) SIO attached to a port (i.e. Telnet console I/O)
    2) SIO not attached to a port (i.e. "regular" console I/O)
*/

int32 sio0s(const int32 port, const int32 io, const int32 data) {
    int32 ti;
    for (ti = 0; ti < Terminals; ti++) {
        if (sio_terminals[ti].statusPort == port) {
            break;
        }
    }
    if (io == 0) { /* IN */
        if (sio_unit.flags & UNIT_ATT) {
            sio_terminals[ti].status =
            (((tmxr_rqln(&TerminalLines[ti]) > 0 ? 0x01 : 0) |
            /* read possible if character available             */
            ((TerminalLines[ti].conn) && (TerminalLines[ti].xmte) ? 0x02 : 0x00)));
            /* write possible if connected and transmit enabled */
        }
        return sio_terminals[ti].status;
    }
    else { /* OUT */
        if (sio_unit.flags & UNIT_ATT) {
            if (data == 0x03) { /* reset port! */
                sio_terminals[ti].status    = 0x00;
                sio_terminals[ti].data      = 0;
            }
        }
        else {
            if (data == 0x03) { /* reset port! */
                sio_terminals[ti].status    = sio_terminals[ti].defaultStatus;
                sio_terminals[ti].data      = 0;
            }
        }
        return 0;   /* ignored since OUT */
    }
}

int32 sio0d(const int32 port, const int32 io, const int32 data) {
    int32 ti;
    for (ti = 0; ti < Terminals; ti++) {
        if (sio_terminals[ti].dataPort == port) {
            break;
        }
    }
    if (io == 0) { /* IN */
        if (sio_unit.flags & UNIT_ATT) {
            sio_terminals[ti].data = tmxr_getc_ln(&TerminalLines[ti]) & 0xff;
        }
        sio_terminals[ti].status &= 0xfe;
        if (sio_unit.flags & UNIT_MAP) {
            if (sio_unit.flags & UNIT_BS) {
                if (sio_terminals[ti].data == BACKSPACE_CHAR) {
                    sio_terminals[ti].data = DELETE_CHAR;
                }
            }
            else {
                if (sio_terminals[ti].data == DELETE_CHAR) {
                    sio_terminals[ti].data = BACKSPACE_CHAR;
                }
            }
        }
        return ((sio_unit.flags & UNIT_UPPER) && (sio_unit.flags & UNIT_MAP)) ?
            toupper(sio_terminals[ti].data) : sio_terminals[ti].data;
    }
    else { /* OUT */
        int32 d = sio_unit.flags & UNIT_ANSI ? data & 0x7f : data;
        if (sio_unit.flags & UNIT_ATT) {
            tmxr_putc_ln(&TerminalLines[ti], d); /* status ignored */
        }
        else {
            sim_putchar(d);
        }
        return 0;   /* ignored since OUT */
    }
}

/* port 2 controls the PTR/PTP devices */

int32 sio1s(const int32 port, const int32 io, const int32 data) {
    if (io == 0) {
        /* reset I bit iff PTR unit not attached or no more data available. */
        /* O bit is always set since write always possible.                 */
        if ((ptr_unit.flags & UNIT_ATT) == 0) {
            if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnattachedPTR < warnLevelSIO)) {
                warnUnattachedPTR++;
/*06*/          message1("Attempt to test status of unattached PTR. 0x02 returned.");
            }
            return 0x02;
        }
        return ptr_unit.u3 ? 0x02 : 0x03;
    }
    else { /* OUT */
        if (data == 0x03) {
            ptr_unit.u3     = 0;
            ptr_unit.buf    = 0;
            ptr_unit.pos    = 0;
            ptp_unit.u3     = 0;
            ptp_unit.buf    = 0;
            ptp_unit.pos    = 0;
        }
        return 0;   /* ignored since OUT */
    }
}

int32 sio1d(const int32 port, const int32 io, const int32 data) {
    int32 temp;
    if (io == 0) {  /* IN */
        if (ptr_unit.u3) { /* no more data available */
            if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnPTREOF < warnLevelSIO)) {
                warnPTREOF++;
/*07*/          message1("PTR attempted to read past EOF. 0x00 returned.");
            }
            return 0;
        }
        if ((ptr_unit.flags & UNIT_ATT) == 0) { /* not attached */
            if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnattachedPTR < warnLevelSIO)) {
                warnUnattachedPTR++;
/*08*/          message1("Attempt to read from unattached PTR. 0x00 returned.");
            }
            return 0;
        }
        if ((temp = getc(ptr_dev.units -> fileref)) == EOF) {   /* end of file? */
            ptr_unit.u3 = 0x01;
            return CONTROLZ_CHAR;   /* control Z denotes end of text file in CP/M */
        }
        ptr_unit.pos++;
        return temp & 0xff;
    }
    else { /* OUT */
        if (ptp_unit.flags & UNIT_ATT) {    /* unit must be attached    */
            putc(data, ptp_dev.units -> fileref);
        }                                   /* else ignore data         */
        else if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnattachedPTP < warnLevelSIO)) {
            warnUnattachedPTP++;
/*09*/      message2("Attempt to output '0x%02x' to unattached PTP - ignored.", data);
        }
        ptp_unit.pos++;
        return 0;   /* ignored since OUT */
    }
}

int32 nulldev(const int32 port, const int32 io, const int32 data) {
    if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnassignedPort < warnLevelSIO)) {
        warnUnassignedPort++;
        if (io == 0) {
            message2("Unassigned IN(%2xh) - ignored.", port);
        }
        else {
            message3("Unassigned OUT(%2xh) -> %2xh - ignored.", port, data);
        }
    }
    return io == 0 ? 0xff : 0;
}

int32 sr_dev(const int32 port, const int32 io, const int32 data) {
    return io == 0 ? SR : 0;
}

static int32 toBCD(const int32 x) {
    return (x / 10) * 16 + (x % 10);
}

static int32 fromBCD(const int32 x) {
    return 10 * ((0xf0 & x) >> 4) + (0x0f & x);
}

/*  Z80 or 8080 programs communicate with the SIMH pseudo device via port 0xfe.
        The following principles apply:

    1)  For commands that do not require parameters and do not return results
        ld  a,<cmd>
        out (0feh),a
        Special case is the reset command which needs to be send 128 times to make
        sure that the internal state is properly reset.

    2)  For commands that require parameters and do not return results
        ld  a,<cmd>
        out (0feh),a
        ld  a,<p1>
        out (0feh),a
        ld  a,<p2>
        out (0feh),a
        ...
        Note: The calling program must send all parameter bytes. Otherwise
        the pseudo device is left in an unexpected state.

    3)  For commands that do not require parameters and return results
        ld  a,<cmd>
        out (0feh),a
        in  a,(0feh)    ; <A> contains first byte of result
        in  a,(0feh)    ; <A> contains second byte of result
        ...
        Note: The calling program must request all bytes of the result. Otherwise
        the pseudo device is left in an unexpected state.

    4)  Commands requiring parameters and returning results do not exist currently.

*/

enum simhPseudoDeviceCommands { /* do not change order or remove commands, add only at the end          */
    printTimeCmd,               /*  0 print the current time in milliseconds                            */
    startTimerCmd,              /*  1 start a new timer on the top of the timer stack                   */
    stopTimerCmd,               /*  2 stop timer on top of timer stack and show time difference         */
    resetPTRCmd,                /*  3 reset the PTR device                                              */
    attachPTRCmd,               /*  4 attach the PTR device                                             */
    detachPTRCmd,               /*  5 detach the PTR device                                             */
    getSIMHVersionCmd,          /*  6 get the current version of the SIMH pseudo device                 */
    getClockZSDOSCmd,           /*  7 get the current time in ZSDOS format                              */
    setClockZSDOSCmd,           /*  8 set the current time in ZSDOS format                              */
    getClockCPM3Cmd,            /*  9 get the current time in CP/M 3 format                             */
    setClockCPM3Cmd,            /* 10 set the current time in CP/M 3 format                             */
    getBankSelectCmd,           /* 11 get the selected bank                                             */
    setBankSelectCmd,           /* 12 set the selected bank                                             */
    getCommonCmd,               /* 13 get the base address of the common memory segment                 */
    resetSIMHInterfaceCmd,      /* 14 reset the SIMH pseudo device                                      */
    showTimerCmd,               /* 15 show time difference to timer on top of stack                     */
    attachPTPCmd,               /* 16 attach PTP to the file with name at beginning of CP/M command line*/
    detachPTPCmd,               /* 17 detach PTP                                                        */
    hasBankedMemoryCmd,         /* 18 determines whether machine has banked memory                      */
    setZ80CPUCmd,               /* 19 set the CPU to a Z80                                              */
    set8080CPUCmd,              /* 20 set the CPU to an 8080                                            */
    startTimerInterruptsCmd,    /* 21 start timer interrupts                                            */
    stopTimerInterruptsCmd,     /* 22 stop timer interrupts                                             */
    setTimerDeltaCmd,           /* 23 set the timer interval    in which interrupts occur               */
    setTimerInterruptAdrCmd,    /* 24 set the address to call by timer interrupts                       */
    resetStopWatchCmd,          /* 25 reset the millisecond stop watch                                  */
    readStopWatchCmd            /* 26 read the millisecond stop watch                                   */
};

#define cpmCommandLineLength    128
#define splimit             10  /* stack depth of timer stack   */
static uint32 markTime[splimit];/* timer stack                  */
static struct tm currentTime;
static int32 currentTimeValid = FALSE;
static char version[] = "SIMH002";

static t_stat simh_dev_reset(DEVICE *dptr) {
    currentTimeValid        = FALSE;
    ClockZSDOSDelta         = 0;
    setClockZSDOSPos        = 0;
    getClockZSDOSPos        = 0;
    ClockCPM3Delta          = 0;
    setClockCPM3Pos         = 0;
    getClockCPM3Pos         = 0;
    getStopWatchDeltaPos    = 0;
    getCommonPos            = 0;
    setTimerDeltaPos        = 0;
    setTimerInterruptAdrPos = 0;
    markTimeSP              = 0;
    versionPos              = 0;
    lastCommand             = 0;
    lastCPMStatus           = SCPE_OK;
    timerInterrupt          = FALSE;
    if (simh_unit.flags & UNIT_SIMH_TIMERON) {
        simh_dev_set_timeron(NULL, 0, NULL, NULL);
    }
    return SCPE_OK;
}

static void warnNoRealTimeClock(void) {
    if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
        printf("Sorry - no real time clock available.\n");
    }
}

static t_stat simh_dev_set_timeron(UNIT *uptr, int32 value, char *cptr, void *desc) {
    if (rtc_avail) {
        timeOfNextInterrupt = sim_os_msec() + timerDelta;
        return sim_activate(&simh_unit, simh_unit.wait);    /* activate unit */
    }
    else {
        warnNoRealTimeClock();
        return SCPE_ARG;
    }
}

static t_stat simh_dev_set_timeroff(UNIT *uptr, int32 value, char *cptr, void *desc) {
    timerInterrupt = FALSE;
    sim_cancel(&simh_unit);
    return SCPE_OK;
}

static t_stat simh_svc(UNIT *uptr) {
    uint32 n = sim_os_msec();
    if (n >= timeOfNextInterrupt) {
        timerInterrupt = TRUE;
        timeOfNextInterrupt += timerDelta;
        if (n >= timeOfNextInterrupt) { /* time of next interrupt is not in the future */
            timeOfNextInterrupt = n + timerDelta; /* make sure it is in the future! */
        }
    }
    if (simh_unit.flags & UNIT_SIMH_TIMERON) {
        sim_activate(&simh_unit, simh_unit.wait);   /* activate unit */
    }
    return SCPE_OK;
}

/* The CP/M command line is used as the name of a file and UNIT* uptr is attached to it. */
static void attachCPM(UNIT *uptr) {
    char cpmCommandLine[cpmCommandLineLength];
    uint32 i, len = (GetBYTEWrapper(0x80) & 0x7f) - 1; /* 0x80 contains length of command line, discard first char */
    for (i = 0; i < len; i++) {
        cpmCommandLine[i] = (char)GetBYTEWrapper(0x82 + i); /* the first char, typically ' ', is discarded */
    }
    cpmCommandLine[i] = 0; /* make C string */
    if (uptr == &ptr_unit) {
        sim_switches = SWMASK('R');
    }
    else if (uptr == &ptp_unit) {
        sim_switches = SWMASK('W');
    }
    lastCPMStatus = attach_unit(uptr, cpmCommandLine);
    if ((lastCPMStatus != SCPE_OK) && (simh_unit.flags & UNIT_SIMH_VERBOSE)) {
        message3("Cannot open '%s' (%s).", cpmCommandLine, scp_error_messages[lastCPMStatus - SCPE_BASE]);
    }
}

/* setClockZSDOSAdr points to 6 byte block in M: YY MM DD HH MM SS in BCD notation */
static void setClockZSDOS(void) {
    struct tm newTime;
    int32 year = fromBCD(GetBYTEWrapper(setClockZSDOSAdr));
    newTime.tm_year = year < 50 ? year + 100 : year;
    newTime.tm_mon  = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 1)) - 1;
    newTime.tm_mday = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 2));
    newTime.tm_hour = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 3));
    newTime.tm_min  = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 4));
    newTime.tm_sec  = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 5));
    ClockZSDOSDelta = mktime(&newTime) - time(NULL);
}

#define secondsPerMinute    60
#define secondsPerHour      (60 * secondsPerMinute)
#define secondsPerDay       (24 * secondsPerHour)
static time_t mkCPM3Origin(void) {
    struct tm date;
    date.tm_year    = 77;
    date.tm_mon     = 11;
    date.tm_mday    = 31;
    date.tm_hour    = 0;
    date.tm_min     = 0;
    date.tm_sec     = 0;
    return mktime(&date);
}

/* setClockCPM3Adr points to 5 byte block in M:
    0 - 1 int16:    days since 31 Dec 77
        2 BCD byte: HH
        3 BCD byte: MM
        4 BCD byte: SS                              */
static void setClockCPM3(void) {
    ClockCPM3Delta = mkCPM3Origin()                                                                 +
    (GetBYTEWrapper(setClockCPM3Adr) + GetBYTEWrapper(setClockCPM3Adr + 1) * 256) * secondsPerDay   +
    fromBCD(GetBYTEWrapper(setClockCPM3Adr + 2)) * secondsPerHour                                   +
    fromBCD(GetBYTEWrapper(setClockCPM3Adr + 3)) * secondsPerMinute                             +
    fromBCD(GetBYTEWrapper(setClockCPM3Adr + 4)) - time(NULL);
}

static int32 simh_in(const int32 port) {
    int32 result = 0;
    switch(lastCommand) {

        case attachPTRCmd:

        case attachPTPCmd:
            result = lastCPMStatus;
            lastCommand = 0;
            break;

        case getClockZSDOSCmd:
            if (currentTimeValid) {
                switch(getClockZSDOSPos) {

                    case 0:
                        result = toBCD(currentTime.tm_year > 99 ?
                            currentTime.tm_year - 100 : currentTime.tm_year);
                        getClockZSDOSPos = 1;
                        break;

                    case 1:
                        result = toBCD(currentTime.tm_mon + 1);
                        getClockZSDOSPos = 2;
                        break;

                    case 2:
                        result = toBCD(currentTime.tm_mday);
                        getClockZSDOSPos = 3;
                        break;

                    case 3:
                        result = toBCD(currentTime.tm_hour);
                        getClockZSDOSPos = 4;
                        break;

                    case 4:
                        result = toBCD(currentTime.tm_min);
                        getClockZSDOSPos = 5;
                        break;

                    case 5:
                        result = toBCD(currentTime.tm_sec);
                        getClockZSDOSPos = lastCommand = 0;
                        break;
                }
            }
            else {
                result = getClockZSDOSPos = lastCommand = 0;
            }
            break;

        case getClockCPM3Cmd:
            if (currentTimeValid) {
                switch(getClockCPM3Pos) {
                    case 0:
                        result = daysCPM3SinceOrg & 0xff;
                        getClockCPM3Pos = 1;
                        break;

                    case 1:
                        result = (daysCPM3SinceOrg >> 8) & 0xff;
                        getClockCPM3Pos = 2;
                        break;

                    case 2:
                        result = toBCD(currentTime.tm_hour);
                        getClockCPM3Pos = 3;
                        break;

                    case 3:
                        result = toBCD(currentTime.tm_min);
                        getClockCPM3Pos = 4;
                        break;

                    case 4:
                        result = toBCD(currentTime.tm_sec);
                        getClockCPM3Pos = lastCommand = 0;
                        break;
                }
            }
            else {
                result = getClockCPM3Pos = lastCommand = 0;
            }
            break;

        case getSIMHVersionCmd:
            result = version[versionPos++];
            if (result == 0) {
                versionPos = lastCommand = 0;
            }
            break;

        case getBankSelectCmd:
            if (cpu_unit.flags & UNIT_BANKED) {
                result = getBankSelect();
            }
            else {
                result = 0;
                if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
                    message1("Get selected bank ignored for non-banked memory.");
                }
            }
            lastCommand = 0;
            break;

        case getCommonCmd:
            if (getCommonPos == 0) {
                result = getCommon() & 0xff;
                getCommonPos = 1;
            }
            else {
                result = (getCommon() >> 8) & 0xff;
                getCommonPos = lastCommand = 0;
            }
            break;

        case hasBankedMemoryCmd:
            result = cpu_unit.flags & UNIT_BANKED ? MAXBANKS : 0;
            lastCommand = 0;
            break;

        case readStopWatchCmd:
            if (getStopWatchDeltaPos == 0) {
                result = stopWatchDelta & 0xff;
                getStopWatchDeltaPos = 1;
            }
            else {
                result = (stopWatchDelta >> 8) & 0xff;
                getStopWatchDeltaPos = lastCommand = 0;
            }
            break;

        default:
            if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
                message2("Unnecessary IN from SIMH pseudo device on port %03xh ignored.",
                    port);
            }
            result = lastCommand = 0;
    }
    return result;
}

static int32 simh_out(const int32 port, const int32 data) {
    time_t now;
    switch(lastCommand) {

        case setClockZSDOSCmd:
            if (setClockZSDOSPos == 0) {
                setClockZSDOSAdr = data;
                setClockZSDOSPos = 1;
            }
            else {
                setClockZSDOSAdr |= (data << 8);
                setClockZSDOS();
                setClockZSDOSPos = lastCommand = 0;
            }
            break;

        case setClockCPM3Cmd:
            if (setClockCPM3Pos == 0) {
                setClockCPM3Adr = data;
                setClockCPM3Pos = 1;
            }
            else {
                setClockCPM3Adr |= (data << 8);
                setClockCPM3();
                setClockCPM3Pos = lastCommand = 0;
            }
            break;

        case setBankSelectCmd:
            if (cpu_unit.flags & UNIT_BANKED) {
                setBankSelect(data & BANKMASK);
            }
            else if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
                message2("Set selected bank to %i ignored for non-banked memory.", data & 3);
            }
            lastCommand = 0;
            break;

        case setTimerDeltaCmd:
            if (setTimerDeltaPos == 0) {
                timerDelta          = data;
                setTimerDeltaPos    = 1;
            }
            else {
                timerDelta |= (data << 8);
                setTimerDeltaPos = lastCommand = 0;
            }
            break;

        case setTimerInterruptAdrCmd:
            if (setTimerInterruptAdrPos == 0) {
                timerInterruptHandler       = data;
                setTimerInterruptAdrPos     = 1;
            }
            else {
                timerInterruptHandler |= (data << 8);
                setTimerInterruptAdrPos = lastCommand = 0;
            }
            break;

        default:
            lastCommand = data;
            switch(data) {

                case printTimeCmd:  /* print time */
                    if (rtc_avail) {
                        message2("Current time in milliseconds = %d.", sim_os_msec());
                    }
                    else {
                        warnNoRealTimeClock();
                    }
                    break;

                case startTimerCmd: /* create a new timer on top of stack */
                    if (rtc_avail) {
                        if (markTimeSP < splimit) {
                            markTime[markTimeSP++] = sim_os_msec();
                        }
                        else {
                            message1("Timer stack overflow.");
                        }
                    }
                    else {
                        warnNoRealTimeClock();
                    }
                    break;

                case stopTimerCmd:  /* stop timer on top of stack and show time difference */
                    if (rtc_avail) {
                        if (markTimeSP > 0) {
                            uint32 delta = sim_os_msec() - markTime[--markTimeSP];
                            message2("Timer stopped. Elapsed time in milliseconds = %d.", delta);
                        }
                        else {
                            message1("No timer active.");
                        }
                    }
                    else {
                        warnNoRealTimeClock();
                    }
                    break;

                case resetPTRCmd:   /* reset ptr device */
                    ptr_reset(NULL);
                    break;

                case attachPTRCmd:  /* attach ptr to the file with name at beginning of CP/M command line */
                    attachCPM(&ptr_unit);
                    break;

                case detachPTRCmd:  /* detach ptr */
                    detach_unit(&ptr_unit);
                    break;

                case getSIMHVersionCmd:
                    versionPos = 0;
                    break;

                case getClockZSDOSCmd:
                    time(&now);
                    now += ClockZSDOSDelta;
                    currentTime = *localtime(&now);
                    currentTimeValid = TRUE;
                    getClockZSDOSPos = 0;
                    break;

                case setClockZSDOSCmd:
                    setClockZSDOSPos = 0;
                    break;

                case getClockCPM3Cmd:
                    time(&now);
                    now += ClockCPM3Delta;
                    currentTime = *localtime(&now);
                    currentTimeValid = TRUE;
                    daysCPM3SinceOrg = (now - mkCPM3Origin()) / secondsPerDay;
                    getClockCPM3Pos = 0;
                    break;

                case setClockCPM3Cmd:
                    setClockCPM3Pos = 0;
                    break;

                case getBankSelectCmd:

                case setBankSelectCmd:

                case getCommonCmd:

                case hasBankedMemoryCmd:
                    break;

                case resetSIMHInterfaceCmd:
                    markTimeSP  = 0;
                    lastCommand = 0;
                    break;

                case showTimerCmd:  /* show time difference to timer on top of stack */
                    if (rtc_avail) {
                        if (markTimeSP > 0) {
                            uint32 delta = sim_os_msec() - markTime[markTimeSP - 1];
                            message2("Timer running. Elapsed in milliseconds = %d.", delta);
                        }
                        else {
                            message1("No timer active.");
                        }
                    }
                    else {
                        warnNoRealTimeClock();
                    }
                    break;

                case attachPTPCmd:  /* attach ptp to the file with name at beginning of CP/M command line */
                    attachCPM(&ptp_unit);
                    break;

                case detachPTPCmd:  /* detach ptp */
                    detach_unit(&ptp_unit);
                    break;

                case setZ80CPUCmd:
                    cpu_unit.flags |= UNIT_CHIP;
                    break;

                case set8080CPUCmd:
                    cpu_unit.flags &= ~UNIT_CHIP;
                    break;

                case startTimerInterruptsCmd:
                    if (simh_dev_set_timeron(NULL, 0, NULL, NULL) == SCPE_OK) {
                        timerInterrupt = FALSE;
                        simh_unit.flags |= UNIT_SIMH_TIMERON;
                    }
                    break;

                case stopTimerInterruptsCmd:
                    simh_unit.flags &= ~UNIT_SIMH_TIMERON;
                    simh_dev_set_timeroff(NULL, 0, NULL, NULL);
                    break;

                case setTimerDeltaCmd:
                    setTimerDeltaPos = 0;
                    break;

                case setTimerInterruptAdrCmd:
                    setTimerInterruptAdrPos = 0;
                    break;

                case resetStopWatchCmd:
                    stopWatchNow = rtc_avail ? sim_os_msec() : 0;
                    break;

                case readStopWatchCmd:
                    getStopWatchDeltaPos = 0;
                    stopWatchDelta = rtc_avail ? sim_os_msec() - stopWatchNow : 0;
                    break;

                default:
                    if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
                        message3("Unknown command (%i) to SIMH pseudo device on port %03xh ignored.",
                            data, port);
                    }
                }
    }
    return 0; /* ignored, since OUT */
}

/* port 0xfe is a device for communication SIMH <--> Altair machine */
int32 simh_dev(const int32 port, const int32 io, const int32 data) {
    return io == 0 ? simh_in(port) : simh_out(port, data);
}
