/* i650_dsk.c: IBM 650 RAMAC Disk Dotrage

   Copyright (c) 2018, Roberto Sancho

   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
   ROBERTO SANCHO 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.

   
*/

#include "i650_defs.h"

#define UNIT_DSK       UNIT_ATTABLE | UNIT_DISABLE | UNIT_FIX

#define DISK_SIZE     (12*60*100)    // a physical disk plate size: 12 bytes per word x 60 words per track x 100 tracks per disk
                                    // there are 100 like this in each unit

#define UPDATE_RAMAC        10        // update ramac arm movement each 10 msec of simulted time
                                    // time pregress as drum wordcount progresses

/* Definitions */
uint32              dsk_cmd(int opcode, int32 addr, uint16 fast);
t_stat              dsk_srv(UNIT *);
void                dsk_ini(UNIT *, t_bool f);
t_stat              dsk_reset(DEVICE *);
t_stat              dsk_attach(UNIT *, CONST char *);
t_stat              dsk_detach(UNIT *);
t_stat              dsk_help (FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr);
const char          *dsk_description (DEVICE *dptr);

UNIT                dsk_unit[4] = {
    {UDATA(&dsk_srv, UNIT_DSK, 0), 0}, /* 0 */
    {UDATA(&dsk_srv, UNIT_DSK, 0), 0}, /* 1 */
    {UDATA(&dsk_srv, UNIT_DSK, 0), 0}, /* 2 */
    {UDATA(&dsk_srv, UNIT_DSK, 0), 0}, /* 3 */
};

DEVICE              dsk_dev = {
    "DSK", dsk_unit, NULL, NULL,
    4, 8, 15, 1, 8, 8,
    NULL, NULL, &dsk_reset, NULL, &dsk_attach, &dsk_detach,
    &dsk_dib, DEV_DISABLE | DEV_DEBUG, 0, dev_debug,
    NULL, NULL, &dsk_help, NULL, NULL, &dsk_description
};


// array for disc units (4) arm's positions (3 arms per unit)
struct armrec {    
    int current_disk, current_track;        // current disk plate/track where the arm is positioned
    int dest_disk, dest_track;              // destination position where the arm should go
    int cmd;                                // opcode being executed (OP_SDS, OP_RDS, OP_WDS) 
    t_int64 InitTime;                       // timestamp using global wordTime counter when operation starts
    struct armmov {
        int disk, track;                    // disk plate/track where the arm is positioned in this point of movement sequence
        int msec;                            // time in msec arm stay in this position
    } seq[1+100+50+100+1];                  // sequeece of arm movement. If =0 -> end of sequence
} Arm[4][3];


int dsk_read_numeric_word(char * buf, t_int64 * d, int * ZeroNeg)
{
    int i, neg; 
    char c;

    neg = 0;
    *d = 0;
    if (ZeroNeg != NULL) *ZeroNeg = 0;
    for (i=0;i<10;i++) {
        c = *buf++;
        if ((c < '0') || (c > '9')) c='0';
        *d = *d * 10 + (c - '0');
    }
    if (*buf++ == '-') neg=1;
    if (neg) *d = -*d;
    if (ZeroNeg != NULL) *ZeroNeg = ((neg) && (*d == 0)) ? 1:0;
    return 0;
}



void dsk_write_numeric_word(char * buf, t_int64 d, int ZeroNeg)
{
    int i, neg; 
    char c;

    neg = 0;
    if (d < 0) {neg=1; d=-d;}
    if (ZeroNeg) neg=1;
    for (i=0;i<10;i++) {
        c = Shift_Digits(&d,1) + '0';
        *buf++ = c;
    }
    *buf++ = neg ? '-':'+';
}


// perform the operation (Read, Write) on RAMAC unit file
// init file if len=0 (flat format)
// 
t_stat dsk_operation(int cmd, int unit, int arm, int disk, int track)
{

    FILE *f;
    int flen, i, ic, ZeroNeg;
    char buf[DISK_SIZE+1]; 
    t_int64 d;
    char s[6];
                           // buf holds a full disk

    if ((unit < 0) || (unit > 3)) return 0;
    if ((arm < 0)  || (arm > 2) ) return 0;
    if ((disk < 0) || (disk > 99)) return 0;
    if ((track < 0) || (track > 99)) return 0;

    f = dsk_unit[unit].fileref; // get disk file from unit; 

    flen = sim_fsize(f);
    if (flen == 0) {
        // new file, fill it with blanks
        memset(buf, 32, sizeof(buf)); // fill with space
        for (i=1;i<1000;i++) buf[i*12*6-1]=13; // ad some cr lo allow text editor to vire ramac file
        buf[sizeof(buf)-1]=0;         // add string terminator
        for(i=0;i<100;i++) sim_fwrite(buf, 1, DISK_SIZE, f);
    }
    sim_fseek(f, DISK_SIZE * disk, SEEK_SET);
    sim_fread(buf, 1, DISK_SIZE, f); // read the entire disc (100 tracks)
    ic = 12 * 60 * track;            // ic is char at beginning of track
    sim_debug(DEBUG_DETAIL, &cpu_dev, "... RAMAC file at fseek %d, ic %d\n", DISK_SIZE * disk, ic);
    if (cmd==OP_RDS) {
        for(i=0;i<60;i++) {
            dsk_read_numeric_word(&buf[ic], &d, &ZeroNeg);
            ic += 12; // 12 bytes per word
            // store into IAS
            IAS[i] = d;
            IAS_NegativeZeroFlag[i] = ZeroNeg;
            sim_debug(DEBUG_DETAIL, &cpu_dev, "... RAMAC to IAS %04d: %06d%04d%c '%s'\n", 
                        i+9000, printfw(d,ZeroNeg), 
                        word_to_ascii(s, 1, 5, d));
        }
        // set IAS_TimingRing. Nothing said in RAMAC manual, but needed to make supersoap CDD pseudo op work properly
        IAS_TimingRing=0;
    } else if (cmd==OP_WDS) {
        for(i=0;i<60;i++) {
            // read IAS
            d = IAS[i];
            ZeroNeg = IAS_NegativeZeroFlag[i];
            sim_debug(DEBUG_DETAIL, &cpu_dev, "... IAS %04d to RAMAC: %06d%04d%c '%s'\n", 
                        i+9000, printfw(d,ZeroNeg), 
                        word_to_ascii(s, 1, 5, d));
            // write numeric to disk buf
            dsk_write_numeric_word(&buf[ic], d, ZeroNeg);
            ic += 12;
        }
        // set IAS_TimingRing. Nothing said in RAMAC manual, but needed to make supersoap CDD pseudo op work properly
        IAS_TimingRing=0;
        // write back disk to ramac unit file
          sim_fseek(f, DISK_SIZE * disk, SEEK_SET);
        sim_fwrite(buf, 1, DISK_SIZE, f); // write the entire disc (100 tracks)
    }
    // don't know if Seek Opcode (SDS) also sets TimingRing to zero

    return SCPE_OK;
}


// return 1 if disk unit n (0..3) and arm (0..2) is ready to receive  a command
int dsk_ready(int unit, int arm)
{
    if ((unit < 0) || (unit > 3)) return 0;
    if ((arm < 0)  || (arm > 2) ) return 0;
    if (Arm[unit][arm].cmd == 0) return 1; // arm has no cmd to execute -> it is ready to receive new command
    return 0;
}

void dsk_set_mov_seq(int unit,int arm)
{
    // set arm movement sequence to its destination
    //
    // arm timing
    //    seek: 50 msec setup time 
    //          on same disk:
    //              2 msec per track in same disk (0-99)
    //              25 msec  sensing track gap (that identifies the start of track pos) a mean between 0-50 msec or
    //                       to extract arm outside disk for arm to go to another disk
    //          going to another physical disk:
    //               200 msec start arm vertical motion
    //                9  msec per physical disk (0 to 49)
    //               200 msec stop arm vertical motion
    //               
    //   read: 110 msec
    //   write: 135 msec
    //

    int cmd, nseq, i, d1, d2, dy, tr;

    cmd = Arm[unit][arm].cmd;
    nseq = 0;

    // seek or read/write but current arm pos not the addr selected for 
    // read/write -> must do a seek cycle 
    if ((cmd == OP_SDS) || 
        (Arm[unit][arm].current_disk  != Arm[unit][arm].dest_disk) ||
        (Arm[unit][arm].current_track != Arm[unit][arm].dest_track)) {
        // start seek sequence at current arm pos
        Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].current_disk;
        Arm[unit][arm].seq[nseq].track = tr = Arm[unit][arm].current_track;
        Arm[unit][arm].seq[nseq++].msec = 50; // msec needed for seek setup time
           // is arm already accessing physical destination disk?
        if ((d1=(Arm[unit][arm].current_disk % 50)) != (d2=(Arm[unit][arm].dest_disk % 50))) {
            // not yet, should move arm up or down
            // is arm outside physical disk stack?
            if (Arm[unit][arm].current_track >= 0) {
                // not yet, should move arm outside physical disk (up to -1)
                // move out arm track to track until outside of physical disk
                for (i=Arm[unit][arm].current_track;i>=0;i--) {
                    Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].current_disk;
                    Arm[unit][arm].seq[nseq].track = i;
                    Arm[unit][arm].seq[nseq++].msec = 2; // msec needed for horizontal arm movement of 1 track                    
                }
            }
            // now arm is outside disk stack, can move up and down
            Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].current_disk;
            Arm[unit][arm].seq[nseq].track = -1;
            Arm[unit][arm].seq[nseq++].msec = 200; // msec needed to setup vertical arm movement 
            // move out up/down on disk stack up to destination disk
            dy = (d1 < d2) ? +1:-1;
            i = Arm[unit][arm].current_disk;
            for (;;) {
                if (i % 50 == d2) break;
                Arm[unit][arm].seq[nseq].disk = i;
                Arm[unit][arm].seq[nseq].track = -1;
                Arm[unit][arm].seq[nseq++].msec = 9; // msec needed for vertical arm movement of 1 physical disk
                i=i+dy;
            }
            // stop motion and select destination disk (not physical disk)
            Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].dest_disk;
            Arm[unit][arm].seq[nseq].track = tr = -1;
            Arm[unit][arm].seq[nseq++].msec = 200; // msec needed to stop vertical arm movement 
        }
           // now arm accessing physical destination disk
        // is arm at destination track?
        if (tr != (d2=Arm[unit][arm].dest_track)) {
            // not yet, should move arm horizontally
            dy = (tr < d2) ? +1:-1;
            for (;;) {
                if (tr == d2) break;
                Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].dest_disk;
                Arm[unit][arm].seq[nseq].track = tr;
                Arm[unit][arm].seq[nseq++].msec = 2; // msec needed for horizontal arm movement of 1 track                    
                tr=tr+dy;
            }
        }
        // now arm is positioned on destination track, disk
        // sense the track gap to finish seek operation
        Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].dest_disk;
        Arm[unit][arm].seq[nseq].track = Arm[unit][arm].dest_track;
        Arm[unit][arm].seq[nseq++].msec = 25; // msec needed for sensing track gap
    }

    // read operation
    if (cmd == OP_RDS) {
        Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].dest_disk;
        Arm[unit][arm].seq[nseq].track = Arm[unit][arm].dest_track;
        Arm[unit][arm].seq[nseq++].msec = 110; // msec needed for reading entire track
    } else if (cmd == OP_WDS) {
        Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].dest_disk;
        Arm[unit][arm].seq[nseq].track = Arm[unit][arm].dest_track;
        Arm[unit][arm].seq[nseq++].msec = 135; // msec needed for writing entire track
    }
    // set end of sequence
    Arm[unit][arm].seq[nseq].disk = Arm[unit][arm].dest_disk;
    Arm[unit][arm].seq[nseq].track = Arm[unit][arm].dest_track;
    Arm[unit][arm].seq[nseq++].msec = 0; // end of sequence mark
}

/* Start off a RAMAC command */
uint32 dsk_cmd(int cmd, int32 addr, uint16 fast)
{
    DEVICE             *dptr;
    UNIT               *uptr;
    int                 unit, disk, track, arm; 
    int                    time; 
    int                    bFastMode; 

    unit =(addr / 100000) % 10;
    disk =(addr / 1000)   % 100,     
    track=(addr /   10)   % 100,     
    arm  =(addr %   10);          

    time = 0;
    /* Make sure addr unit number */
    if ((unit > 3) || (unit < 0)) return STOP_ADDR;
    if ((arm  > 2) || (arm  < 0)) return STOP_ADDR;

    uptr = &dsk_unit[unit];
    dptr = find_dev_from_unit(uptr);
    
    // init IBM 652 Control Unit internal registers
    bFastMode = fast;

    /* If disk unit disabled return error */
    if (uptr->flags & UNIT_DIS) {
        sim_debug(DEBUG_EXP, dptr, "RAMAC command attempted on disabled unit %d\n", unit);
        // not stated in manual: what happends if command to non existant disk?
        // option 1 -> cpu halt (used this)
        // option 2 -> indictor flag set 
        return STOP_IO;
    }
    /* If disk unit has no file attached return error */
    if ((uptr->flags & UNIT_ATT) == 0) {
        sim_debug(DEBUG_EXP, dptr, "RAMAC command attempted on unit %d that has no file attached\n", unit);
        return STOP_IO;
    }
    // init arm operation
    Arm[unit][arm].cmd = cmd;        // the command to execute: can be OP_SDS, OP_RDS, OP_WDS
    Arm[unit][arm].dest_disk  = disk;   // the destination address
    Arm[unit][arm].dest_track = track;
    sim_debug(DEBUG_CMD, dptr, "RAMAC unit %d, arm %d: %s on disk %d, track %d started\n", 
                                unit, arm, 
                                (cmd == OP_SDS) ? "SEEK" : (cmd == OP_RDS) ? "READ" : "WRITE",
                                Arm[unit][arm].dest_disk, Arm[unit][arm].dest_track);

    if (bFastMode) {
        time = 0; // no movement sequence. Just go to destination pos inmediatelly and exec command
        Arm[unit][arm].InitTime = -1; 
    } else {
        time = msec_to_wordtime(UPDATE_RAMAC); // sampling disk arm movement sequence each 10 msec
        Arm[unit][arm].InitTime = GlobalWordTimeCount; // when the movement sequence starts (in word time counts)
        // calculate the movement seqnece
        dsk_set_mov_seq(unit,arm);
    }
    // schedule command execution
    sim_cancel(uptr);
    sim_activate(uptr, time);
    return SCPE_OK_INPROGRESS;
}

/* Handle processing of disk requests. */
t_stat dsk_srv(UNIT * uptr)
{
    DEVICE             *dptr = find_dev_from_unit(uptr);
    int                 unit = (uptr - dptr->units);
    int                 time, msec, arm, cmd, nseq;
    t_int64                InitTime; 
    int                    bSequenceInProgress=0; 
    int                    bFastMode; 
    t_stat                r;

    // init IBM 652 Control Unit internal registers
    bFastMode = 0;
    // update arm movement for this unit
    for (arm=0;arm<3;arm++) {
        cmd = Arm[unit][arm].cmd;
        if (cmd == 0) continue; // RAMAC arm for this disk unit is stoped (=ready). 
                                // continue to Process next arm of this unit

        // arm in movement (=busy)
        // calc time in msec elapsed from start of comand execution
        InitTime=Arm[unit][arm].InitTime;
        if (InitTime<0) {
            bFastMode=1;
        } else {
            time=msec_elapsed(Arm[unit][arm].InitTime); 
               // examine sequence of arm movements to determine what is the current position 
            // or arm at this point of time
            nseq=0;
            for(;;) {
                msec=Arm[unit][arm].seq[nseq].msec;
                if (msec==0) break; // exit beacuse end of sequence
                time=time-msec;
                if (time<0) break; // exit beacuse we are at this point of sequence
                nseq++; 
            }
            if (time <0) {
                // sequence not finisehd: set current arm pos 
                Arm[unit][arm].current_disk=Arm[unit][arm].seq[nseq].disk;
                Arm[unit][arm].current_track=Arm[unit][arm].seq[nseq].track;
                bSequenceInProgress=1; // there is an arm in movement
                // arm not arrived to its destination yet. contiinue proceed with next arm
                sim_debug(DEBUG_CMD, dptr, "RAMAC unit %d, arm %d: now at disk %d, track %d\n", 
                                unit, arm, 
                                Arm[unit][arm].current_disk, Arm[unit][arm].current_track);
                continue; 
            }
        }
        // arm arrived to its destination position
        Arm[unit][arm].current_disk=Arm[unit][arm].dest_disk;
        Arm[unit][arm].current_track=Arm[unit][arm].dest_track;
        // execute command
        sim_debug(DEBUG_DETAIL, &cpu_dev, "... RAMAC unit %d, arm %d: %s on disk %d, track %d start execution \n", 
                                    unit, arm, 
                                    (cmd == OP_SDS) ? "SEEK" : (cmd == OP_RDS) ? "READ" : "WRITE",
                                    Arm[unit][arm].dest_disk, Arm[unit][arm].dest_track);
        r = dsk_operation(cmd, unit, arm, Arm[unit][arm].dest_disk, Arm[unit][arm].dest_track);
        if (r != SCPE_OK) return STOP_IO;
        // cmd execution finished, can free IAS interlock 
        sim_debug(DEBUG_DETAIL, &cpu_dev, "... RAMAC unit %d, arm %d: %s on disk %d, track %d finished\n", 
                                    unit, arm, 
                                    (cmd == OP_SDS) ? "SEEK" : (cmd == OP_RDS) ? "READ" : "WRITE",
                                    Arm[unit][arm].dest_disk, Arm[unit][arm].dest_track);
        if (((cmd==OP_RDS) || (cmd==OP_WDS)) && (InterLockCount[IL_IAS])) {
            // remove IAS Interlock
            InterLockCount[IL_IAS] = 0;
            sim_debug(DEBUG_CMD, dptr, "RAMAC unit %d, arm %d: free IAS interlock\n", unit, arm);
        }
        // set arm as ready, so it can accept new commands
        Arm[unit][arm].cmd = 0;
        sim_debug(DEBUG_CMD, dptr, "RAMAC unit %d, arm %d READY\n", unit, arm);
    }
    // if there is any arm in movement, re-schedulle event 
    sim_cancel(uptr);
    if (bSequenceInProgress) {
        if (bFastMode) {
            time = 0; // no movement sequence. Just go to destination pos inmediatelly and exec command
        } else {
            time = msec_to_wordtime(UPDATE_RAMAC); // sampling disk arm movement sequence each 10 msec
        }
        sim_activate(uptr, time);
    }
    return SCPE_OK;
}

void dsk_ini(UNIT * uptr, t_bool f)
{
    DEVICE             *dptr = find_dev_from_unit(uptr);
    int                 unit = (uptr - dptr->units);

    memset(&Arm[unit], 0, sizeof(Arm[unit])); // zeroes arm info for this unit
} 

t_stat dsk_reset(DEVICE * dptr)
{
    int i;
    for (i = 0; i < 4; i++) {
        dsk_ini(&dsk_unit[i], 0);
    }
    return SCPE_OK;
}

t_stat dsk_attach(UNIT * uptr, CONST char *file)
{
    t_stat              r;
    int                    flen;

    if ((r = attach_unit(uptr, file)) != SCPE_OK) return r;
    flen=sim_fsize(uptr->fileref);
    if ((flen > 0) && (flen != DISK_SIZE * 100)) {
        sim_messagef (SCPE_IERR, "Invalid RAMAC Unit file size\n");
        detach_unit (uptr); 
    }
    dsk_ini(uptr, 0);
    return SCPE_OK;
}

t_stat dsk_detach(UNIT * uptr)
{
    sim_cancel(uptr); // cancel any pending command
    dsk_ini(uptr, 0);
    return detach_unit (uptr);                             /* detach unit */
}

t_stat
dsk_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr)
{
   fprintf (st, "%s\n\n", dsk_description(dptr));
   fprintf (st, "RAMAC Magnetic storage disk.\n\n");
   fprint_set_help(st, dptr);
   fprint_show_help(st, dptr);
   return SCPE_OK;
}

const char *
dsk_description(DEVICE *dptr)
{
   return "IBM 355 RAMAC Disk Storage Unit";
}


