/*

   Copyright (c) 2015-2016, John Forecast

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

*/

/* cdc1700_io.c: CDC1700 I/O subsystem
 */

#include "cdc1700_defs.h"

extern char INTprefix[];

extern t_bool inProtectedMode(void);
extern uint16 dev1INTR(DEVICE *);
extern uint16 cpuINTR(DEVICE *);
extern uint16 dcINTR(void);

extern void RaiseExternalInterrupt(DEVICE *);

extern enum IOstatus fw_doIO(DEVICE *, t_bool);

extern uint16 Areg, Mreg, Preg, OrigPreg, Qreg, Pending, IOAreg, IOQreg, M[];
extern uint8 Protected, INTflag;
extern t_uint64 Instructions;

extern t_bool FirstRejSeen;
extern uint32 CountRejects;

extern DEVICE cpu_dev, dca_dev, dcb_dev, dcc_dev, tti_dev, tto_dev,
  ptr_dev, ptp_dev;

static const char *status[] = {
  "REPLY", "REJECT", "INTERNALREJECT"
};

/*
 * The I/O sub-system uses the Q-register to provide controller addressing:
 * 
 *       15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
 *      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 *      |       W      |     E     |       Command      |
 *      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 *
 * If W is non-zero, it addresses a 1706-A buffered data channel. If W
 * is zero, it addresses a non-buffered controller.
 *
 * Note that buffered operations (DMA) can be performed by certain controllers
 * (e.g. The Disk Pack Controller) using DSA (Direct Storage Access).
 */

typedef enum IOstatus devIO(DEVICE *, t_bool);

/*
 * There can be up to 16 equipment addresses.
 */
devIO *IOcall[16];
DEVICE *IOdev[16];
devINTR *IOintr[16];

/*
 * Display equipment/station address, buffered data channel and optional
 * additional information:
 *
 *      Stop on Reject status
 *      Protected status
 */
t_stat show_addr(FILE *st, UNIT *uptr, int32 val, CONST void *desc)
{
  DEVICE *dptr;
  IO_DEVICE *iod;

  if (uptr == NULL)
    return SCPE_IERR;

  dptr = find_dev_from_unit(uptr);
  if (dptr == NULL)
    return SCPE_IERR;

  iod = (IO_DEVICE *)dptr->ctxt;
  fprintf(st, "\n\tequip: 0x");
  fprint_val(st, (t_value)iod->iod_equip, DEV_RDX, 16, PV_LEFT);
  if (iod->iod_station != 0xFF) {
    fprintf(st, ", station: ");
    fprint_val(st, (t_value)iod->iod_station, DEV_RDX, 8, PV_LEFT);
  }
  if (iod->iod_dc != 0)
    fprintf(st, ", Buffered Data Channel: %c", '0' + iod->iod_dc);

  if ((dptr->flags & DEV_REJECT) != 0)
    fprintf(st, ", Stop on Reject");
  if ((dptr->flags & DEV_PROTECTED) != 0)
    fprintf(st, ", Protected");
  return SCPE_OK;
}

/*
 * Device stop on reject handling.
 */
t_stat set_stoponrej(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
  DEVICE *dptr;

  if (cptr != NULL)
    return SCPE_ARG;

  dptr = find_dev_from_unit(uptr);
  if (dptr == NULL)
    return SCPE_IERR;

  dptr->flags |= DEV_REJECT;
  return SCPE_OK;
}

t_stat clr_stoponrej(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
  DEVICE *dptr;

  if (cptr != NULL)
    return SCPE_ARG;

  dptr = find_dev_from_unit(uptr);
  if (dptr == NULL)
    return SCPE_IERR;

  dptr->flags &= ~DEV_REJECT;
  return SCPE_OK;
}

/*
 * Protected device.
 */
t_stat set_protected(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
  DEVICE *dptr;

  if (cptr != NULL)
    return SCPE_ARG;

  dptr = find_dev_from_unit(uptr);
  if (dptr == NULL)
    return SCPE_IERR;

  dptr->flags |= DEV_PROTECTED;
  return SCPE_OK;
}

t_stat clear_protected(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
  DEVICE *dptr;

  if (cptr != NULL)
    return SCPE_ARG;

  dptr = find_dev_from_unit(uptr);
  if (dptr == NULL)
    return SCPE_IERR;

  dptr->flags &= ~DEV_PROTECTED;
  return SCPE_OK;
}

/*
 * Device interrupt handling
 */

/*
 * Interrupt status for a non-existent device
 */
uint16 noneINTR(DEVICE *dptr)
{
  return 0;
}

/*
 * Generic device interrupt status
 */
uint16 deviceINTR(DEVICE *dptr)
{
  IO_DEVICE *iod = (IO_DEVICE *)dptr->ctxt;

  if ((iod->iod_flags & STATUS_ZERO) != 0)
    return 0;

  return (DEVSTATUS(iod) & IO_ST_INT) != 0 ? iod->iod_interrupt : 0;
}

/*
 * Rebuild the pending interrupt status based on the current status of
 * each device.
 */
void rebuildPending(void)
{
  int i;

  /*
   * Leave the CPU interrupt pending bit alone.
   */
  Pending &= 1;

  for (i = 0; i < 16; i++) {
    devINTR *rtn = IOintr[i];

    Pending |= rtn(IOdev[i]);
  }

  Pending |= dcINTR();
}

/*
 * Handle generic director function(s) for a device. The function request is
 * in IOAreg and the bits will be cleared in IOAreg as they are processed.
 * Return TRUE if an explicit change was made to the device interrupt mask.
 */
t_bool doDirectorFunc(DEVICE *dptr, t_bool allowStacked)
{
  IO_DEVICE *iod = (IO_DEVICE *)dptr->ctxt;

  /*
   * Mask out unsupported commands
   */
  IOAreg &= iod->iod_dmask;

  if ((IOAreg & (IO_DIR_CINT | IO_DIR_CCONT)) != 0) {
    if ((IOAreg & IO_DIR_CCONT) != 0) {
      /*
       * Preferentially use a device specific "Clear Controller" routine
       * over the device reset routine.
       */
      if (iod->iod_clear != NULL) {
        iod->iod_clear(dptr);
      } else {
        if (dptr->reset != NULL)
          dptr->reset(dptr);
      }
    }

    /*
     * Clear all interrupt enables.
     */
    iod->iod_ienable = 0;
    iod->iod_oldienable = 0;

    /*
     * Clear all pending interrupts.
     */
    DEVSTATUS(iod) &= ~iod->iod_cmask;

    rebuildPending();

    /*
     * The device may allow other commands to be stacked along with Clear
     * Interrupts and Clear Controller.
     */
    if (!allowStacked) {
      IOAreg = 0;
      return FALSE;
    }
    IOAreg &= ~(IO_DIR_CINT | IO_DIR_CCONT);
  }

  if ((IOAreg & iod->iod_imask) != 0) {
    /*
     * This request is enabling one or more interrupts.
     */
    iod->iod_oldienable = iod->iod_ienable;
    iod->iod_ienable |= Areg & iod->iod_imask;
    IOAreg &= ~iod->iod_imask;
    return TRUE;
  }
  return FALSE;
}

/*
 * Perform an I/O operation. Note that the "Continue" bit is only supported
 * on the 1706 buffered data channel devices since it is not relevant in the
 * emulation environment.
 */
enum IOstatus doIO(t_bool output, DEVICE **device)
{
  enum IOstatus result;
  DEVICE *dev;
  devIO *rtn;
  const char *name;
  IO_DEVICE *iod;

  /*
   * Make a private copy of Areg and Qreg for use by I/O routines
   */
  IOAreg = Areg;
  IOQreg = Qreg;

  /*
   * Get the target device and access routine
   */
  dev = IOdev[((IOQreg & IO_EQUIPMENT) >> 7) & 0xF];
  rtn = IOcall[((IOQreg & IO_EQUIPMENT) >> 7) & 0xF];

  if ((((IOQreg & IO_EQUIPMENT) >> 7) & 0xF) == 1) {
    /*
     * Device address 1 requires special processing. This address
     * multiplexes the console teletypewriter, the paper tape reader and
     * punch and the card reader using different station addresses:
     *
     *  001     - 1711/1712/1713 teletypewriter
     *  010     - 1721/1722 paper tape reader
     *  100     - 1723/1724 paper tape punch
     *  110     - 1729 card reader (not implemented on device address 1)
     */
    switch ((IOQreg >> 4) & 0x7) {
    case 0x01:
      dev = &tti_dev;
      break;
      
    case 0x02:
      dev = &ptr_dev;
      break;

    case 0x04:
      dev = &ptp_dev;
      break;

    default:
      return IO_INTERNALREJECT;
    }
  }

  if ((IOQreg & IO_W) != 0) {
    /*
     * Buffered data channel access.
     */

    /*
     * Check if this device is only accessible on the AQ channel.
     */
    if (dev != NULL) {
      iod = (IO_DEVICE *)dev->ctxt;

      if ((iod->iod_flags & AQ_ONLY) != 0) {
        *device = dev;
        return IO_INTERNALREJECT;
      }
    }

    switch (IOQreg & IO_W) {
      /*
       * 1706-A Channel #1
       */
      case IO_1706_1_A:
      case IO_1706_1_B:
      case IO_1706_1_C:
      case IO_1706_1_D:
        dev = &dca_dev;
        break;

      /*
       * 1706-A Channel #2
       */
      case IO_1706_2_A:
      case IO_1706_2_B:
      case IO_1706_2_C:
      case IO_1706_2_D:
        dev = &dcb_dev;
        break;

      /*
       * 1706-A Channel #3
       */
      case IO_1706_3_A:
      case IO_1706_3_B:
      case IO_1706_3_C:
      case IO_1706_3_D:
        dev = &dcc_dev;
        break;

      default:
        return IO_INTERNALREJECT;
    }
    rtn = fw_doIO;
  }

  *device = dev;

  if (dev != NULL) {
    iod = (IO_DEVICE *)dev->ctxt;
    name = iod->iod_name != NULL ? iod->iod_name : dev->name;

    if ((dev->dctrl & DBG_DTRACE) != 0) {
      if (!FirstRejSeen) {
        /* Trace I/O before operation */
        if ((Qreg & IO_W) != 0)
          fprintf(DBGOUT,
                  "%s[%s: %s, A: %04X, Q: %04X (%04X/%04X), M: %04X, I: %c]\r\n",
                  INTprefix, name, output ? "OUT" : "INP", Areg, Qreg,
                  Qreg & IO_W, Qreg & (IO_EQUIPMENT | IO_COMMAND), Mreg,
                  INTflag ? '1' : '0');
        else fprintf(DBGOUT,
                     "%s[%s: %s, A: %04X, Q: %04X, M: %04X, I: %c]\r\n",
                     INTprefix, name, output ? "OUT" : "INP", Areg, Qreg,
                     Mreg, INTflag ? '1' : '0');
        if ((dev->dctrl & DBG_DSTATE) != 0) {
          if (iod->iod_state != NULL)
            (*iod->iod_state)("before", dev, iod);
        }
      }
    }

    if ((dev->dctrl & DBG_DLOC) != 0) {
      if (!FirstRejSeen) {
        /*
         * Trace location of the I/O instruction + instruction count
         */
        fprintf(DBGOUT, "%s[%s: P: %04X, Inst: %llu]\r\n",
                INTprefix, name, OrigPreg, Instructions);
      }
    }

    /*
     * Reject I/O requests from non-protected instructions to protected
     * devices unless it is a status register read.
     */
    if (inProtectedMode()) {
      if (!Protected) {
        if ((dev->flags & DEV_PROTECT) != 0) {
          if ((dev->flags & DEV_PROTECTED) == 0) {
            if (output || ((Qreg & iod->iod_rmask) != 1)) {
              if ((cpu_dev.dctrl & DBG_PROTECT) != 0) {
                fprintf(DBGOUT,
                        "%sProtect REJECT\r\n", INTprefix);
              }
              return IO_REJECT;
            }
          }
        }
      }
    }
  }
  result = rtn(dev, output);
  if (dev != NULL) {
    if ((dev->dctrl & DBG_DTRACE) != 0) {
      if (!FirstRejSeen || (result == IO_REPLY)) {
        /* Trace I/O after operation */
        if ((dev->dctrl & DBG_DSTATE) != 0) {
          if (iod->iod_state != NULL)
            (*iod->iod_state)("after", dev, iod);
        }
        if (output)
          fprintf(DBGOUT, "%s[%s: => %s]\r\n", 
                  INTprefix, name, status[result]);
        else fprintf(DBGOUT, "%s[%s: => %s, A: %04X]\r\n",
                     INTprefix, name, status[result], Areg);
      }
    }
  }
  return result;
}

/*
 * Default I/O routine for devices which are not present
 */
static enum IOstatus notPresent(DEVICE *dev, t_bool output)
{
  if ((cpu_dev.dctrl & DBG_MISSING) != 0) {
    fprintf(DBGOUT,
            "%sAccess to missing device (Q: %04X, Equipment: %2u)\r\n",
            INTprefix, Qreg, (Qreg & 0x7800) >> 7);
  }
  return IO_INTERNALREJECT;
}

/*
 * Build the I/O call table according to the enabled devices
 */
void buildIOtable(void)
{
  DEVICE *dptr;
  int i;

  /*
   * By default, all devices are marked "not present"
   */
  for (i = 0; i < 16; i++) {
    IOdev[i] = NULL;
    IOcall[i] = notPresent;
    IOintr[i] = noneINTR;
  }

  /*
   * Scan the device table and add equipment devices.
   */
  i = 0;
  while ((dptr = sim_devices[i++]) != NULL) {
    if ((dptr->flags & (DEV_NOEQUIP | DEV_DIS)) == 0) {
      IO_DEVICE *iod = (IO_DEVICE *)dptr->ctxt;

      IOdev[iod->iod_equip] = dptr;
      IOcall[iod->iod_equip] = fw_doIO;
      IOintr[iod->iod_equip] = 
        iod->iod_raised != NULL ? iod->iod_raised : deviceINTR;
    }
  }

  /*
   * Set up fixed equipment code devices
   */
  IOcall[1] = fw_doIO;
  IOintr[1] = dev1INTR;

  IOintr[0] = cpuINTR;
}

/*
 * Load bootstrap code into memory
 */
void loadBootstrap(uint16 *code, int len, uint16 base, uint16 start)
{
  while (len--) {
    M[base++] = *code++;
  }
  Preg = start;
}
