/*
* interface to scsi devices via scsi(2) to sd(3),
* which does not implement LUNs.
*/
#include "all.h"
#include "io.h"

enum {
       Ninquiry        = 255,
       Nsense          = 255,

       CMDtest         = 0x00,
       CMDreqsense     = 0x03,
       CMDread6        = 0x08,
       CMDwrite6       = 0x0A,
       CMDinquiry      = 0x12,
       CMDstart        = 0x1B,
       CMDread10       = 0x28,
       CMDwrite10      = 0x2A,
};

typedef struct {
       Target  target[NTarget];
} Ctlr;
static Ctlr scsictlr[MaxScsi];

extern int scsiverbose;

void
scsiinit(void)
{
       Ctlr *ctlr;
       int ctlrno, targetno;
       Target *tp;

       scsiverbose = 1;
       for(ctlrno = 0; ctlrno < MaxScsi; ctlrno++){
               ctlr = &scsictlr[ctlrno];
               memset(ctlr, 0, sizeof(Ctlr));
               for(targetno = 0; targetno < NTarget; targetno++){
                       tp = &ctlr->target[targetno];

                       qlock(tp);
                       qunlock(tp);
                       sprint(tp->id, "scsictlr#%d.%d", ctlrno, targetno);

                       tp->ctlrno = ctlrno;
                       tp->targetno = targetno;
                       tp->inquiry = ialloc(Ninquiry, 0);
                       tp->sense = ialloc(Nsense, 0);
               }
       }
}

static uchar lastcmd[16];
static int lastcmdsz;

static int
sense2stcode(uchar *sense)
{
       switch(sense[2] & 0x0F){
       case 6:                                         /* unit attention */
               /*
                * 0x28 - not ready to ready transition,
                *        medium may have changed.
                * 0x29 - power on, RESET or BUS DEVICE RESET occurred.
                */
               if(sense[12] != 0x28 && sense[12] != 0x29)
                       return STcheck;
               /*FALLTHROUGH*/
       case 0:                                         /* no sense */
       case 1:                                         /* recovered error */
               return STok;
       case 8:                                         /* blank data */
               return STblank;
       case 2:                                         /* not ready */
               if(sense[12] == 0x3A)                   /* medium not present */
                       return STcheck;
               /*FALLTHROUGH*/
       default:
               /*
                * If unit is becoming ready, rather than not ready,
                * then wait a little then poke it again; should this
                * be here or in the caller?
                */
               if((sense[12] == 0x04 && sense[13] == 0x01)) {
                       // delay(500);
                       // scsitest(tp, lun);
                       fprint(2, "sense2stcode: unit becoming ready\n");
                       return STcheck;                 /* not exactly right */
               }
               return STcheck;
       }
}

/*
* issue the SCSI command via scsi(2).  lun must already be in cmd[1].
*/
static int
doscsi(Target* tp, int rw, uchar* cmd, int cbytes, void* data, int* dbytes)
{
       int lun, db = 0;
       uchar reqcmd[6], reqdata[Nsense], dummy[1];
       Scsi *sc;

       sc = tp->sc;
       if (sc == nil)
               panic("doscsi: nil tp->sc");
       lun = cmd[1] >> 5;      /* save lun in case we need it for reqsense */

       /* cope with zero arguments */
       if (dbytes != nil)
               db = *dbytes;
       if (data == nil)
               data = dummy;

       if (scsi(sc, cmd, cbytes, data, db, rw) >= 0)
               return STok;

       /* cmd failed, get whatever sense data we can */
       memset(reqcmd, 0, sizeof reqcmd);
       reqcmd[0] = CMDreqsense;
       reqcmd[1] = lun<<5;
       reqcmd[4] = Nsense;
       memset(reqdata, 0, sizeof reqdata);
       if (scsicmd(sc, reqcmd, sizeof reqcmd, reqdata, sizeof reqdata,
           Sread) < 0)
               return STharderr;

       /* translate sense data to ST* codes */
       return sense2stcode(reqdata);
}

static int
scsiexec(Target* tp, int rw, uchar* cmd, int cbytes, void* data, int* dbytes)
{
       int s;

       /*
        * issue the SCSI command.  lun must already be in cmd[1].
        */
       s = doscsi(tp, rw, cmd, cbytes, data, dbytes);
       switch(s){

       case STcheck:
               memmove(lastcmd, cmd, cbytes);
               lastcmdsz = cbytes;
               /*FALLTHROUGH*/

       default:
               /*
                * It's more complicated than this.  There are conditions which
                * are 'ok' but for which the returned status code is not 'STok'.
                * Also, not all conditions require a reqsense, there may be a
                * need to do a reqsense here when necessary and making it
                * available to the caller somehow.
                *
                * Later.
                */
               break;
       }

       return s;
}

static int
scsitest(Target* tp, char lun)
{
       uchar cmd[6];

       memset(cmd, 0, sizeof cmd);
       cmd[0] = CMDtest;
       cmd[1] = lun<<5;
       return scsiexec(tp, SCSIread, cmd, sizeof cmd, 0, 0);

}

static int
scsistart(Target* tp, char lun, int start)
{
       uchar cmd[6];

       memset(cmd, 0, sizeof cmd);
       cmd[0] = CMDstart;
       cmd[1] = lun<<5;
       if(start)
               cmd[4] = 1;
       return scsiexec(tp, SCSIread, cmd, sizeof cmd, 0, 0);
}

static int
scsiinquiry(Target* tp, char lun, int* nbytes)
{
       uchar cmd[6];

       memset(cmd, 0, sizeof cmd);
       cmd[0] = CMDinquiry;
       cmd[1] = lun<<5;
       *nbytes = Ninquiry;
       cmd[4] = *nbytes;
       return scsiexec(tp, SCSIread, cmd, sizeof cmd, tp->inquiry, nbytes);
}

static char *key[] =
{
       "no sense",
       "recovered error",
       "not ready",
       "medium error",
       "hardware error",
       "illegal request",
       "unit attention",
       "data protect",
       "blank check",
       "vendor specific",
       "copy aborted",
       "aborted command",
       "equal",
       "volume overflow",
       "miscompare",
       "reserved"
};

static int
scsireqsense(Target* tp, char lun, int* nbytes, int quiet)
{
       char *s;
       int n, status, try;
       uchar cmd[6], *sense;

       sense = tp->sense;
       for(try = 0; try < 20; try++) {
               memset(cmd, 0, sizeof cmd);
               cmd[0] = CMDreqsense;
               cmd[1] = lun<<5;
               cmd[4] = Ninquiry;
               memset(sense, 0, Ninquiry);

               *nbytes = Ninquiry;
               status = scsiexec(tp, SCSIread, cmd, sizeof cmd, sense, nbytes);
               if(status != STok)
                       return status;
               *nbytes = sense[0x07]+8;

               switch(sense[2] & 0x0F){
               case 6:                                 /* unit attention */
                       /*
                        * 0x28 - not ready to ready transition,
                        *        medium may have changed.
                        * 0x29 - power on, RESET or BUS DEVICE RESET occurred.
                        */
                       if(sense[12] != 0x28 && sense[12] != 0x29)
                               goto buggery;
                       /*FALLTHROUGH*/
               case 0:                                 /* no sense */
               case 1:                                 /* recovered error */
                       return STok;
               case 8:                                 /* blank data */
                       return STblank;
               case 2:                                 /* not ready */
                       if(sense[12] == 0x3A)           /* medium not present */
                               goto buggery;
                       /*FALLTHROUGH*/
               default:
                       /*
                        * If unit is becoming ready, rather than not ready,
                        * then wait a little then poke it again; should this
                        * be here or in the caller?
                        */
                       if((sense[12] == 0x04 && sense[13] == 0x01)){
                               delay(500);
                               scsitest(tp, lun);
                               break;
                       }
                       goto buggery;
               }
       }

buggery:
       if(quiet == 0){
               s = key[sense[2]&0x0F];
               fprint(2, "%s: reqsense: '%s' code #%2.2ux #%2.2ux\n",
                       tp->id, s, sense[12], sense[13]);
               fprint(2, "%s: byte 2: #%2.2ux, bytes 15-17: #%2.2ux #%2.2ux #%2.2ux\n",
                       tp->id, sense[2], sense[15], sense[16], sense[17]);
               fprint(2, "lastcmd (%d): ", lastcmdsz);
               for(n = 0; n < lastcmdsz; n++)
                       fprint(2, " #%2.2ux", lastcmd[n]);
               fprint(2, "\n");
       }

       return STcheck;
}

static Target*
scsitarget(Device* d)
{
       int ctlrno, targetno;

       ctlrno = d->wren.ctrl;
       if(ctlrno < 0 || ctlrno >= MaxScsi /* || scsictlr[ctlrno].io == nil */)
               return 0;
       targetno = d->wren.targ;
       if(targetno < 0 || targetno >= NTarget)
               return 0;
       return &scsictlr[ctlrno].target[targetno];
}

static void
scsiprobe(Device* d)
{
       Target *tp;
       int nbytes, s;
       uchar *sense;
       int acount;

       if((tp = scsitarget(d)) == 0)
               panic("scsiprobe: device = %Z", d);

       acount = 0;
again:
       s = scsitest(tp, d->wren.lun);
       if(s < STok){
               fprint(2, "%s: test, status %d\n", tp->id, s);
               return;
       }

       /*
        * Determine if the drive exists and is not ready or
        * is simply not responding.
        * If the status is OK but the drive came back with a 'power on' or
        * 'reset' status, try the test again to make sure the drive is really
        * ready.
        * If the drive is not ready and requires intervention, try to spin it
        * up.
        */
       s = scsireqsense(tp, d->wren.lun, &nbytes, acount);
       sense = tp->sense;
       switch(s){
       case STok:
               if ((sense[2] & 0x0F) == 0x06 &&
                   (sense[12] == 0x28 || sense[12] == 0x29))
                       if(acount == 0){
                               acount = 1;
                               goto again;
                       }
               break;
       case STcheck:
               if((sense[2] & 0x0F) == 0x02){
                       if(sense[12] == 0x3A)
                               break;
                       if(sense[12] == 0x04 && sense[13] == 0x02){
                               fprint(2, "%s: starting...\n", tp->id);
                               if(scsistart(tp, d->wren.lun, 1) == STok)
                                       break;
                               s = scsireqsense(tp, d->wren.lun, &nbytes, 0);
                       }
               }
               /*FALLTHROUGH*/
       default:
               fprint(2, "%s: unavailable, status %d\n", tp->id, s);
               return;
       }

       /*
        * Inquire to find out what the device is.
        * Hardware drivers may need some of the info.
        */
       s = scsiinquiry(tp, d->wren.lun, &nbytes);
       if(s != STok) {
               fprint(2, "%s: inquiry failed, status %d\n", tp->id, s);
               return;
       }
       fprint(2, "%s: %s\n", tp->id, (char*)tp->inquiry+8);
       tp->ok = 1;
}

int
scsiio(Device* d, int rw, uchar* cmd, int cbytes, void* data, int dbytes)
{
       Target *tp;
       int e, nbytes, s;

       if((tp = scsitarget(d)) == 0)
               panic("scsiio: device = %Z", d);

       qlock(tp);
       if(tp->ok == 0)
               scsiprobe(d);
       qunlock(tp);

       s = STinit;
       for(e = 0; e < 10; e++){
               for(;;){
                       nbytes = dbytes;
                       s = scsiexec(tp, rw, cmd, cbytes, data, &nbytes);
                       if(s == STok)
                               break;
                       s = scsireqsense(tp, d->wren.lun, &nbytes, 0);
                       if(s == STblank && rw == SCSIread) {
                               memset(data, 0, dbytes);
                               return STok;
                       }
                       if(s != STok)
                               break;
               }
               if(s == STok)
                       break;
       }
       if(e)
               fprint(2, "%s: retry %d cmd #%x\n", tp->id, e, cmd[0]);
       return s;
}

void
newscsi(Device *d, Scsi *sc)
{
       Target *tp;

       if((tp = scsitarget(d)) == nil)
               panic("newscsi: device = %Z", d);
       tp->sc = sc;            /* connect Target to Scsi */
}