/*-----------------------------------------*
 | File: DT.c - Main disk testing routines |
 +-----------------------------------------+---------------*
 | Author:  Maurizio Loreti, aka MLO or I3NOO.             |
 | Address: University of Padova - Department of Physics   |
 |          Via F. Marzolo, 8 - 35131 PADOVA - Italy       |
 | Phone:   (39)(49) 844-313         FAX: (39)(49) 844-245 |
 | E-Mail:  loreti@padova.infn.it (TCP/IP)                 |
 | Home: Via G. Donizetti 6 - 35010 CADONEGHE (PD) - Italy |
 *---------------------------------------------------------*/

/**
 | #includes
**/

#include <stddef.h>                       /* Standard library */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <exec/types.h>                   /* Amiga specific */
#include <exec/memory.h>
#include <devices/trackdisk.h>
#include <workbench/startup.h>
#include <clib/exec_protos.h>
#include <clib/gadtools_protos.h>
#include <clib/intuition_protos.h>
#include <clib/alib_protos.h>
#include <clib/dos_protos.h>
#include <clib/wb_protos.h>
#include "main.h"
#include "dt.h"
#include "ext.h"

int CXBRK(void)
{
/**
 | This procedure is in lieu of the default SAS-C break handling
 | routine. Here, if a CTRL-C is detected from the operating system,
 | we silently defer all handling until CheckBreak() is called.
**/

  abortDT |= BRK_DETECTED;
  return 0;
}

void EventLoop(void)
{
/**
 | Main event loop.
 | First, we initialise the AppWindow environment; then we Wait()
 | for a signal from IDCMP or the AppWindow, and take the appropriate
 | action.
**/

  ULONG signalSet;

  if ((appWinPort = CreateMsgPort()) == NULL) {
    Error("Error from CreateMsgPort");
  }

  if ((pAWind = AddAppWindow(AW_ID, 0, pWind, appWinPort,
                             TAG_END)) == NULL) {
    Error("Error from AddAppWindow");
  }

  signalSet =
    (1L << appWinPort->mp_SigBit)  |  (1L << pWind->UserPort->mp_SigBit);

  FOREVER {
    struct IntuiMessage *pIM;
    struct AppMessage *pAM;

    (void) Wait(signalSet);

    while ((pIM = GT_GetIMsg(pWind->UserPort)) != NULL) {
      ULONG class;
      UWORD code;
      struct Gadget *pG;

      class = pIM->Class;
      code  = pIM->Code;
      pG    = (struct Gadget *) pIM->IAddress;
      GT_ReplyIMsg(pIM);

      switch (class) {
        case GADGETDOWN:
        case GADGETUP:
          switch (pG->GadgetID) {
            case BUT_QUIT:
              Cleanup();
              break;
            case BUT_1:
            case BUT_2:
            case BUT_3:
            case BUT_4:
              StartTest();
              DiskCheck(pG->GadgetID);
              EndTest();
              break;
            case BUT_FN:
/* NO see RKM listFilenames = (BOOL)(pG->Flags & GFLG_SELECTED; */
              listFileNames = (BOOL)(!listFileNames);
              break;
            case SCROLLER:
              SetTopLine(code);
              break;
            default:
              break;
          }
          break;
        case MOUSEMOVE:
          SetTopLine(code);
          break;
        case REFRESHWINDOW:
          GT_BeginRefresh(pWind);
          RefreshView(TRUE);
	        GT_EndRefresh(pWind, TRUE);
          break;
        default:
          break;
      }
    }

    while ((pAM = (struct AppMessage *) GetMsg(appWinPort)) != NULL) {
      BPTR dirLock;
      char dirName[FILENAME_MAX];
      char filName[FILENAME_MAX];

      strcpy(filName, pAM->am_ArgList->wa_Name);
      if (filName[0]) {
        dirLock = DupLock(pAM->am_ArgList->wa_Lock);
      } else {
        (void) NameFromLock(pAM->am_ArgList->wa_Lock,
                            dirName, FILENAME_MAX);
      }
      ReplyMsg((struct Message *) pAM);

      GetBuffer(FILBUF_DIM);
      StartTest();
      OutLine(FALSE, "File integrity check ...");

      if (filName[0]) {
        BPTR oldLock;

        oldLock = CurrentDir(dirLock);
        (void) NameFromLock(dirLock, dirName, FILENAME_MAX);
        OutLine(FALSE, "  Sitting in directory %s ...", dirName);
        CheckFile(filName);
        (void) CurrentDir(oldLock);
      } else {
        char *pc;

        pc = dirName + strlen(dirName) - 1;
        CheckDir(dirName, (BOOL)(*pc == ':'));
      }
      EndTest();
    }
  }
}

/*-------------------------- Local Procedures --------------------------*/

static unsigned CheckBreak(void)
{
/**
 | Called from time to time to check for user breaks; checks the
 | CTRL-C signal (actually unused) and the "break window" gadgets.
 | The returned value is the "abortDT" global variable itself.
**/

  if (!abortDT  &&
      (SetSignal(0L, SIGBREAKF_CTRL_C) & SIGBREAKF_CTRL_C))
          abortDT |= BRK_DETECTED;

  if (pBWind != NULL) {
    struct IntuiMessage *pIM;

    if ((pIM = GT_GetIMsg(pBWind->UserPort)) != NULL) {
      GT_ReplyIMsg(pIM);
      abortDT |= BRK_DETECTED;
    }
  }

  if (abortDT  &&  !(abortDT & WARN_PRINTED)) {
    OutLine(FALSE, "*** DiskTest: BREAK ***");
    abortDT |= WARN_PRINTED;
  }

  return abortDT;
}

static void CheckDir(
  char *path,
  const BOOL root
){
/**
 | This procedure checks (recursively) a directory.
 | "path" contains the full directory name, and "root" is non-zero if
 | this directory is at the top level (the difference is in the special
 | handling of the ':', and in the replacement of the device name (e.g.
 | "DH0:") with its label (e.g. "Workbench:"), in general more clear).
 |
 | CheckDir() scans the wanted directory, checking immediately the 'true'
 | files; the subdirectories (if any) are checked recursively at the
 | end, one by one.
 | If an error is detected from CheckDir(), the directory/file test is
 | stopped and the global flag "abortDT" is set; this makes possible, in
 | the further steps, to recursively free() all the memory that has been
 | allocated in order to store the subdirectory names.
 |
 | First: obtain a lock on the wanted directory, and Examine() the lock;
 | since only one directory is being scanned at a time, we can use a
 | single FileInfoBlock global buffer.
**/

  dirEntry *rdE = NULL;
  BPTR dlock;

  if ((dlock = Lock(path, ACCESS_READ)) == 0) {
    OutLine(FALSE, "* Can't access directory %s !", path);
    abortDT = INTERNAL_ERR;
    nErFil++;
  } else {
    if (!Examine(dlock, pFIB)) {
      OutLine(FALSE,
              "* Error return from Examine(), directory %s", path);
      abortDT = INTERNAL_ERR;
      nErFil++;
    } else {

/**
 | Prepare in "fileName" the full directory name - to which local
 | filenames will be appended.
**/

      char fileName[FILENAME_MAX];
      char *pc;

      if (root) {
        pc = strcpy(fileName, pFIB->fib_FileName);
        pc += strlen(fileName);
        *pc++ = ':';
        *pc = '\0';
        OutLine(FALSE,
                "  checking files in root directory %s ...", fileName);
      } else {
        pc = strcpy(fileName, path);
        pc += strlen(fileName);
        OutLine(FALSE,
                "  checking files in directory %s ...", fileName);
        *pc++ = '/';
      }

      nDirs++;

/**
 | Now, loop over all directory entries. As already said, all the
 | 'real' files are immediately checked; the subdirectory names are
 | stored in a linked list to be examined at the end. This list is
 | implemented as a LIFO tree (the simplest type).
**/

      while (ExNext(dlock, pFIB)) {
        (void) strcpy(pc, pFIB->fib_FileName);
        if (pFIB->fib_DirEntryType < 0) {
          CheckFile(fileName);
        } else {

/**
 | If a memory allocation error is detected when asking space for
 | our linked list, we exit the "while" loop; setting the "abortDT"
 | flag before exiting ensures that all the memory we had from
 | these malloc()'s will later be free()-ed recursively.
**/

          dirEntry *pdE;

          if ((pdE = malloc(sizeof(dirEntry) + strlen(fileName))) == NULL) {
            OutLine(FALSE, "* Can't allocate heap memory!");
            abortDT = INTERNAL_ERR;
            break;
          }

          (void) strcpy(pdE->name, fileName);
          pdE->next = rdE;
          rdE = pdE;
        }

        if (CheckBreak()) break;
      }

/**
 | We should check if ExNext() has failed, or if the last
 | entry has been found.
**/

      if (!abortDT) {
        int errno;

        if ((errno = IoErr()) != ERROR_NO_MORE_ENTRIES) {
          OutLine(FALSE, "* Error %d reading directory %s !",
                  errno, path);
          nErFil++;
        }
      }
    }
    UnLock(dlock);
  }

/**
 | Now, loop over all detected subdirectories (if any);
 | freeing in the same time the memory used to store their names.
**/

  while (rdE != NULL) {
    dirEntry *pdE;

    if (!abortDT) CheckDir(rdE->name, FALSE);

    pdE = rdE->next;
    free(rdE);
    rdE = pdE;
  }
}

static void CheckFile(
  char *name
){
/**
 | Check a file, opening and reading it record by record.
**/

  BPTR pFH;

  nFiles++;
  if (listFileNames) OutLine(FALSE, "    file %s ...", name);

  if ((pFH = Open(name, MODE_OLDFILE)) == 0) {
    OutLine(FALSE, "* Error %d opening file \"%s\".", IoErr(), name);
    nErFil++;
  } else {
    long ier;

    while (!abortDT  &&
           (ier = Read(pFH, diskBuffer, (long) diskBufferLength)) > 0) {
      (void) CheckBreak();
    }
    (void) Close(pFH);

    if (ier < 0) {
      OutLine(FALSE, "* Error %d reading file \"%s\".", IoErr(), name);
      nErFil++;
    }
  }
}

static void DiskCheck(
  const int drive
){
/**
 | Full test for the floppy unit "drive".
 | Properly opened the device, we check for a disk in drive and,
 | obtain the floppy disk parameters from various status commands;
 | then obtain memory for our internal buffers.
**/

  char driveName[] = "DF0:";
  int error;
  int cyl;
  int head;
  ULONG total;

  if ((diskPort = CreatePort(NULL, 0L)) == NULL) {
    Error("Error from CreatePort");
  }

  if ((diskReq = (struct IOExtTD *)
      CreateExtIO(diskPort, sizeof(struct IOExtTD))) == NULL) {
    Error("Error from CreateExtIo");
  }

  driveName[2] += (char) drive;

  if ((error =
      OpenDevice(TD_NAME, (ULONG) drive,
                 (struct IORequest *) diskReq, 0L)) != 0) {
    sprintf(slate, "Error 0x%X from OpenDevice", error);
    Error(slate);
  }

  diskReq->iotd_Req.io_Command = TD_CHANGESTATE;
  (void) DoIO((struct IORequest *) diskReq);
  if (diskReq->iotd_Req.io_Actual) {
    OutLine(FALSE, "No disk in drive %d ...", drive);
    return;
  }

  diskReq->iotd_Req.io_Command = TD_CHANGENUM;
  (void) DoIO((struct IORequest *) diskReq);

  diskReq->iotd_Req.io_Command = TD_GETGEOMETRY;
  diskReq->iotd_Req.io_Data = &drGeom;
  (void) DoIO((struct IORequest *) diskReq);

  cylSize = drGeom.dg_TrackSectors * drGeom.dg_SectorSize;
  GetBuffer(cylSize);
  total = drGeom.dg_TotalSectors * drGeom.dg_SectorSize;
  total >>= 10;
  OutLine(FALSE,
          "Change number for drive %s (%ld KBytes) is %d;",
          driveName, total,
          (diskChangeCount = diskReq->iotd_Req.io_Actual));

#ifdef DT_DEBUG
  OutLine(FALSE, "%ld heads, %ld cylinders",
          drGeom.dg_Heads, drGeom.dg_Cylinders);
  OutLine(FALSE, "%ld sec/cyl, %ld sec/trk, %ld bytes/sec",
          drGeom.dg_CylSectors, drGeom.dg_TrackSectors,
          drGeom.dg_SectorSize);
  OutLine(FALSE, "%ld total sectors, %ld bytes/trk",
          drGeom.dg_TotalSectors, cylSize);
#endif

/**
 | Pass 1
 | Seek over the floppy.
**/

  Motor(ON);
  SeekFullRange(1);
  if (nErFil) {
    Motor(OFF);
    OutLine(FALSE, "* %d hard errors detected seeking drive %s",
            nErFil, driveName);
    return;
  } else {
    OutLine(FALSE, "  no errors detected seeking over full disk range.");
  }

/**
 | Pass 2
 | Read all disk tracks.
**/

  OutLine(FALSE, "Checking all disk tracks:");
  for (cyl=0; cyl<(int)drGeom.dg_Cylinders; cyl++) {
    for (head=0; head<(int)drGeom.dg_Heads; head++) {
      OutLine(TRUE,
              "  reading cylinder %d, head %d ...", cyl, head);
      if (CheckBreak()) {
        Motor(OFF);
        return;
      }

      ReadCyl(cyl, head);
      if ((error = diskReq->iotd_Req.io_Error) != 0) {
        OutLine(FALSE, "* Error 0x%X detected for cylinder %d, head %d",
            error, cyl, head);
        nErFil++;
      }
    }
  }
  Motor(OFF);

  if (nErFil) {
    OutLine(FALSE, "* %d hard errors detected reading drive %s.",
        nErFil, driveName);
    return;
  } else {
    OutLine(FALSE, "  no errors detected reading drive %s.", driveName);
  }

/**
 | Pass 3
 | File integrity check.
**/

  OutLine(FALSE, "Checking all files in drive %s", driveName);
  CheckDir(driveName, TRUE);
}

static void EndTest(void)
{
/**
 | This procedure outputs some statistics on the test;
 | and frees all unneeded resources. The buffer only is
 | retained, just in case we want to check some other thing...
**/

  if (pBWind != NULL) {
    struct IntuiMessage *pIM;

    while ((pIM = GT_GetIMsg(pBWind->UserPort)) != NULL) {
      GT_ReplyIMsg(pIM);
    }
    CloseWindow(pBWind);
    pBWind = NULL;
  }

  if (diskReq != NULL) {
    CloseDevice((struct IORequest *) diskReq);
    DeleteExtIO((struct IORequest *) diskReq);
    diskReq = NULL;
  }

  if (diskPort != NULL) {
    DeletePort(diskPort);
    diskPort = NULL;
  }

  if (nDirs + nFiles)
    OutLine(FALSE,
            "%d director%s and %d file%s checked: %d error%s detected.",
            nDirs,  (nDirs  == 1 ? "y" : "ies"),
            nFiles, (nFiles == 1 ? ""  : "s"),
            nErFil, (nErFil == 1 ? ""  : "s"));

  BusyState(FALSE);
}

static void GetBuffer(
  const ULONG size
){
/**
 | If no buffer has been previously allocated, GetBuffer() queries
 | for memory from the heap; if the previously allocated buffer is
 | too small, it is released and reallocated.
 | N.B.: we ask for chip memory, because the buffer has to be used
 |       for disk reading too; for file reading, MEMF_PUBLIC should
 |       be enough.
**/

  if (diskBuffer != NULL) {
    if (diskBufferLength >= size) return;
    FreeMem(diskBuffer, diskBufferLength);
  }

  if ((diskBuffer = AllocMem(size, MEMF_CHIP)) == NULL) {
    Error("Error from AllocMem");
  }
  diskBufferLength = size;
}

static void Motor(
  const ULONG action
){
/**
 | Issues to the drive a MOTOR ON or OFF request; "action"
 | is the preprocessor symbol ON or OFF.
**/

  diskReq->iotd_Req.io_Length = action;
  diskReq->iotd_Req.io_Command = TD_MOTOR;
  (void) DoIO((struct IORequest *) diskReq);
}

static void OutLine(
  const BOOL last,
  char *fmt,
  ...
){
/**
 | Formatted output on the screen; if "last" is non zero, the
 | "special mode" for the output to be discarded is used.
 | Currently only the track test calls OutLine() with last=TRUE.
**/

  va_list vl;
  size_t n;

  va_start(vl, fmt);
  n = (size_t) vsprintf(slate, fmt, vl);
  va_end(vl);

  if (last) {
    LastLine(slate, n);
  } else {
    AddLine(slate, n, TRUE);
  }
}

static void ReadCyl(
  const int cyl,
  const int hd
){
/**
 | Issues the command to read a whole cylinder on the
 | internal buffer.
**/

  diskReq->iotd_Req.io_Length = cylSize;
  diskReq->iotd_Req.io_Data = (APTR) diskBuffer;
  diskReq->iotd_Req.io_Command = ETD_READ;
  diskReq->iotd_Count = diskChangeCount;
  diskReq->iotd_Req.io_Offset =
    drGeom.dg_SectorSize * (drGeom.dg_TrackSectors *
      ((ULONG) hd + drGeom.dg_Heads * (ULONG) cyl));
  (void) DoIO((struct IORequest *) diskReq);
}

static void SeekFullRange(
  const SHORT howmany
){
/**
 | Performs "howmany" seeks first to the highest
 | cylinder of the drive, and then to the lowest.
**/

  int i;
  SHORT error;

  for (i=1; i<=howmany; i++) {
    diskReq->iotd_Req.io_Offset =
      ((drGeom.dg_Cylinders - 1) * drGeom.dg_TrackSectors *
        drGeom.dg_Heads - 1) * drGeom.dg_SectorSize;
    diskReq->iotd_Req.io_Command = TD_SEEK;
    (void) DoIO((struct IORequest *) diskReq);
    if ((error = diskReq -> iotd_Req.io_Error) != 0) {
      OutLine(FALSE, "* Seek cycle %d, error 0x%X ...", i, error);
      nErFil++;
    }

    diskReq->iotd_Req.io_Offset = 0;
    diskReq->iotd_Req.io_Command = TD_SEEK;
    (void) DoIO((struct IORequest *) diskReq);
    if ((error = diskReq->iotd_Req.io_Error) != 0) {
      OutLine(FALSE, "* Seek cycle %d, error 0x%X ...", i, error);
      nErFil++;
    }
  }
}

static void StartTest(void)
{
/**
 | Zeroes the statistics variables, the break flag,
 | and clears the output window. Then creates the
 | "break" window.
**/

  nDirs = nFiles = nErFil = 0;
  abortDT = 0;
  ClearText(TRUE);
  BusyState(TRUE);

  if ((pBWind = OpenWindowTags(NULL,
                               WA_Left,         pWind->LeftEdge + BKW_LEFT,
                               WA_Top,          pWind->TopEdge  + BKW_TOP,
                               WA_Width,        BKW_WIDTH,
                               WA_Height,       BKW_HEIGHT,
                               WA_Activate,     TRUE,
                               WA_DragBar,      TRUE,
                               WA_DepthGadget,  TRUE,
                               WA_SmartRefresh, TRUE,
                               WA_IDCMP,        BKW_IDCMP,
                               WA_Title,        PROG_NAME " v" REVISION,
                               WA_Gadgets,      pBGad,
                               WA_PubScreen,    pScr,
                               TAG_DONE)) == NULL) {
    Error("Couldn't open the break window");
  }
  GT_RefreshWindow(pBWind, NULL);
}
