/*  Utils.c: Miscellaneous support routines for xprzmodem.library;
    Version 1.0, 29 July 1989, by Rick Huebner.
    Released to the Public Domain; do as you like with this code.  */


#include <exec/memory.h>
#include "aztec.h"
#include "xproto.h"
#include "zmodem.h"
#include "defs.h"

/* Label and version info for .library file */
char XPRname[] = "xprzmodem.library";
char XPRid[]   = "xprzmodem 1.0, 29 July 89\r\n";
short XPRrevision = 0;  /* Version number is XPRVERSION in rtag.asm */

/* Transfer options to use if XProtocolSetup not called */
char Default_Config[] = "T?,ON,B16,F0";

#ifdef DEBUG
UBYTE DebugName[] = "Log:ZDebug.log";
long DebugLog = NULL;
#endif


/* Called by terminal program to set transfer options */
long XProtocolSetup(io)
register struct XPR_IO *io;
{
  UBYTE buf[256], t, o;
  register UBYTE *p;
  register long len, b, f;

  /* Allocate memory for transfer options string */
  if (!io->xpr_data) {
    io->xpr_data = AllocMem((long)CONFIGLEN,0L);
    if (!io->xpr_data) {
      ioerr(io,"Not enough memory for ZModem config string");
      return 0;
    }
    /* Start out with default options; merge user changes into defaults */
    strcpy(io->xpr_data,Default_Config);
  }

  /* Extract current settings from options string */
  t = *(strchr(io->xpr_data,'T')+1);
  o = *(strchr(io->xpr_data,'O')+1);
  b = atol(strchr(io->xpr_data,'B')+1);
  f = atol(strchr(io->xpr_data,'F')+1);

  /* If config string passed by term prog, use it; else prompt user */
  if (io->xpr_filename) strcpy(buf,io->xpr_filename);
  else {
    /* Start buffer with current settings so user can see/edit them in place */
    strcpy(buf,io->xpr_data);
    if (io->xpr_gets) callaa(io->xpr_gets,"ZModem options:",buf);
  }
  /* Upshift config string for easier parsing */
  for (p=buf; *p; ++p)
    *p = toupper(*p);

  /* Merge new T(ext) option into current settings if given */
  /* "TY" = Force Text mode on,
     "TN" = Force Text mode off,
     "T?" = Use other end's text mode suggestion (default to binary) */
  if (p = strchr(buf,'T')) {
    ++p;
    if (*p == 'Y' || *p == 'N' || *p == '?') t = *p;
    else ioerr(io,"Invalid T flag ignored; should be Y, N, or ?");
  }

  /* Merge new O(verwrite) option into current settings if given */
  /* "OY" = Yes, delete old file and replace with new one,
     "ON" = No, prevent overwrite by appending ".dup" to avoid name collision,
     "OR" = Resume transfer at end of existing file,
     "OS" = Skip file if it already exists; go on to next */
  if (p = strchr(buf,'O')) {
    ++p;
    if (*p == 'R' && !io->xpr_finfo) ioerr(io,"Can't Resume; xpr_finfo() not supported");
    else if (*p == 'Y' || *p == 'N' || *p == 'R' || *p == 'S') o = *p;
    else ioerr(io,"Invalid O flag ignored; should be Y, N, R, or S");
  }

  /* Merge new B(uffer) setting into current settings if given */
  /* Size of file I/O buffer in kilobytes */
  if (p = strchr(buf,'B')) {
    len = atol(++p);
    if (len < 1) len = 1;
    b = len;
  }

  /* Merge new F(ramelength) setting into other settings if given */
  /* Number of bytes we're willing to send or receive between ACKs.
     0 = unlimited; nonstop streaming data */
  if (p = strchr(buf,'F')) {
    len = atol(++p);
    if (len < 0) len = 0;
    if (len > 0 && len < MINBLOCK) len = MINBLOCK;
    f = len;
  }

  /* Update config string with new settings */
  sprintf(io->xpr_data,"T%c,O%c,B%ld,F%ld",t,o,b,f);

  return 1;
}


/* Called by terminal program to give us a chance to clean up before program ends */
long XProtocolCleanup(io)
register struct XPR_IO *io;
{
  /* Release config option memory, if any */
  if (io->xpr_data) {
    FreeMem(io->xpr_data,(long)CONFIGLEN);
    io->xpr_data = NULL;
  }

  return 1;
}


/* Perform setup and initializations common to both Send and Receive routines */
struct Vars *setup(io)
register struct XPR_IO *io;
{
  static long bauds[] = { 110,300,1200,2400,4800,9600,19200,38400,38400,57600,76800,115200 };
  register struct Vars *v;
  register long newstatus;

  /* Make sure terminal program supports the required call-back functions */
  if (!io->xpr_update) return NULL;
  if (!io->xpr_fopen || !io->xpr_fclose || !io->xpr_fread || !io->xpr_fwrite ||
      !io->xpr_fseek || !io->xpr_sread || !io->xpr_swrite) {
    ioerr(io,"Term prog missing required function(s); see docs");
    return NULL;
  }

  /* Hook in default transfer options if XProtocolSetup wasn't called */
  if (!io->xpr_data) {
    io->xpr_data = AllocMem((long)CONFIGLEN,0L);
    if (!io->xpr_data) {
      ioerr(io,"Not enough memory for ZModem config string");
      return NULL;
    }
    strcpy(io->xpr_data,Default_Config);
  }

  /* Allocate memory for our unshared variables, to provide reentrancy */  
  if (!(v = AllocMem((long)sizeof(struct Vars),MEMF_CLEAR))) {
nomem:
    ioerr(io,"Not enough memory for xprzmodem");
    return NULL;
  }

  /* Allocate memory for our file I/O buffer; if we can't get as much as
     requested, keep asking for less until we hit minimum before giving up */
  v->Filebufmax = atol(strchr(io->xpr_data,'B')+1) * 1024;
  while (!(v->Filebuf = AllocMem(v->Filebufmax,0L))) {
    if (v->Filebufmax > 1024) v->Filebufmax -= 1024;
    else {
      FreeMem(v,(long)sizeof(struct Vars));
      goto nomem;
    }
  }
  
  /* Copy caller's io struct into our Vars for easier passing */
  v->io = *io;

#ifdef DEBUG
  if (!DebugLog) DebugLog = callaa(v->io.xpr_fopen,DebugName,"w");
#endif

  /* Initialize Vars as required */
  switch(*(strchr(io->xpr_data,'T')+1)) {
    case 'Y':
      v->Rxascii = TRUE;
      v->Rxbinary = FALSE;
      v->Lzconv = ZCNL;
      break;
    case 'N':
      v->Rxascii = FALSE;
      v->Rxbinary = TRUE;
      v->Lzconv = ZCBIN;
      break;
    case '?':
      v->Rxascii = v->Rxbinary = FALSE;
      v->Lzconv = 0;
      break;
  }
  v->Tframlen = atol(strchr(io->xpr_data,'F')+1);

  /* Get baud rate; set serial port mode if necessary (and possible) */  
  if (v->io.xpr_setserial) {
    v->Oldstatus = calld(v->io.xpr_setserial,-1L);
    /* ZModem requires 8 data bits, no parity (full transparency), 
       leave other settings alone */
    newstatus = v->Oldstatus & 0xFFFFE0BC;
    /* newstatus |= on_flags; */
    if (newstatus != v->Oldstatus) calld(v->io.xpr_setserial,newstatus);
    v->Baud = bauds[(newstatus>>16) & 0xFF];
#ifdef DEBUG
    sprintf(v->Msgbuf,"Old serial status = %lx, new = %lx, baud = %ld\n",v->Oldstatus,newstatus,v->Baud);
    dlog(v,v->Msgbuf);
#endif
  /* If no xpr_setserial(), muddle along with most likely guess */
  } else v->Baud = 2400;

  return v;
}


/* send cancel string to get the other end to shut up */
void canit(v)
register struct Vars *v;
{
  static char canistr[] = { 24,24,24,24,24,24,24,24,24,24,8,8,8,8,8,8,8,8,8,8,0 };

  zmputs(v,canistr);
}


/* Send a string to the modem, with processing for \336 (sleep 1 sec)
   and \335 (break signal, ignored since XPR spec doesn't support it) */
void zmputs(v,s)
register struct Vars *v;
register UBYTE *s;
{
  register short c;

  while (*s) {
    switch (c = *s++) {
      case '\336':
        TimeOut(50L);
      case '\335':
        break;
      default:
        sendline(v,c);
    }
  }
}


/* Write one character to the modem */
void xsendline(v,c)
register struct Vars *v;
UWORD c;
{
  UBYTE buf = c;

  callad(v->io.xpr_swrite,&buf,1L);
}


/* Get a byte from the modem;
   return TIMEOUT if no read within timeout tenths of a second,
   return RCDO if carrier lost (supposedly; XPR spec doesn't support carrier detect, though).
   Added in some buffering so we wouldn't hammer the system with single-byte
   serial port reads.  Also, the buffering makes char_avail() a lot easier to implement. */
short readock(v,tenths)
register struct Vars *v;
short tenths;
{
  /* If there's data waiting in the buffer, return next byte */
  if (v->Modemcount) {
gotdata:
    --v->Modemcount;
    return *v->Modemchar++;
  }

  /* Buffer is empty; try to read more data into it */
  v->Modemcount = calladd(v->io.xpr_sread,v->Modembuf,(long)sizeof(v->Modembuf),tenths*100000L);
  if (v->Modemcount < 1) {   /* Didn't get anything within time limit; timeout */
    v->Modemcount = 0;
    return TIMEOUT;
  } else {                   /* Got something; return first byte of it */
    v->Modemchar = v->Modembuf;  /* Reset buffer pointer to start of data */
    goto gotdata;
  }
}


/* Check if there's anything available to read from the modem */
char char_avail(v)
register struct Vars *v;
{
  if (v->Modemcount) return TRUE;

  /* No data in our buffer; check system's input buffer */  
  v->Modemcount = calladd(v->io.xpr_sread,v->Modembuf,(long)sizeof(v->Modembuf),0L);
  if (v->Modemcount < 1) {  /* Nothing in system buffer either */
    v->Modemcount = 0;
    return FALSE;
  } else {                  /* System buffer had something waiting for us */
    v->Modemchar = v->Modembuf;
    return TRUE;
  }
}


/* Update the elapsed time, expected total time, and effective data
   transfer rate values for status display */
void update_rate(v)
register struct Vars *v;
{
  static char *timefmt = "%2d:%02d:%02d";
  register long sent, elapsed, expect;
  register short hr, min;

  /* Compute effective data rate so far in characters per second */
  sent = v->xpru.xpru_bytes - v->Strtpos; /* Actual number of chars transferred */
  elapsed = time(NULL) - v->Starttime;    /* Time it took to send them */
  if (elapsed < 1) elapsed = 1;
  /* If we haven't transferred anything yet (just starting), make reasonable
     guess (95% throughput); otherwise, compute actual effective transfer rate */
  v->xpru.xpru_datarate = (sent) ? sent / elapsed : v->Baud * 95L / 1000;

  /* Compute expected total transfer time based on data rate so far */
  if (v->xpru.xpru_filesize < 0) expect = 0; /* Don't know filesize; display time=0 */
  else expect = (v->xpru.xpru_filesize - v->Strtpos) / v->xpru.xpru_datarate;
  hr = expect / (60*60);   /* How many whole hours */
  expect -= hr * (60*60);  /* Remainder not counting hours */
  min = expect / 60;       /* How many whole minutes */
  expect -= min * 60;      /* Remaining seconds */
  sprintf(v->Msgbuf,timefmt,hr,min,(short)expect);
  v->xpru.xpru_expecttime = (char *)v->Msgbuf;

  /* Compute elapsed time for this transfer so far */
  hr = elapsed / (60*60);
  elapsed -= hr * (60*60);
  min = elapsed / 60;
  elapsed -= min * 60;
  sprintf(v->Msgbuf+20,timefmt,hr,min,(short)elapsed);
  v->xpru.xpru_elapsedtime = (char *)v->Msgbuf+20;
}


/* Buffered file I/O fopen() interface routine */
long bfopen(v,mode)
register struct Vars *v;
UBYTE *mode;
{
  /* Initialize file-handling variables */
  v->Filebufpos = v->Filebuflen = v->Filebufcnt = 0;
  v->Fileflush = FALSE;
  v->Filebufptr = v->Filebuf;
  /* Open the file */
  return callaa(v->io.xpr_fopen,v->Filename,mode);
}


/* Buffered file I/O fclose() interface routine */
void bfclose(v)
register struct Vars *v;
{
  /* If bfwrite() left data lingering in buffer, flush it out before closing */
  if (v->Fileflush) calladda(v->io.xpr_fwrite,v->Filebuf,1L,v->Filebufcnt,v->File);
  /* Close the file */
  calla(v->io.xpr_fclose,v->File);
  v->File = NULL;
}


/* Buffered file I/O fseek() interface routine */
void bfseek(v,pos)
register struct Vars *v;
register long pos;
{
  register long offset;

  /* If new file position is within currently buffered section, reset pointers */
  if (pos >= v->Filebufpos && pos <= v->Filebufpos + v->Filebuflen - 1) {
    offset = pos - v->Filebufpos;
    v->Filebufptr = v->Filebuf + offset;
    v->Filebufcnt = v->Filebuflen - offset;
  /* Otherwise, fseek() file and discard buffer contents to force new read */
  } else {
    calladd(v->io.xpr_fseek,v->File,pos,0L);
    v->Filebuflen = v->Filebufcnt = 0;
    v->Filebufpos = pos;
  }
}


/* Buffered file I/O fread() interface routine */
long bfread(v,buf,length)
register struct Vars *v;
UBYTE *buf;
register long length;
{
  register long count, total = 0;

  /* If there's already data buffered up, try to get what we need from there */
  if (v->Filebufcnt) {
readmore:
    count = (length <= v->Filebufcnt) ? length : v->Filebufcnt;
    CopyMem(v->Filebufptr,buf,count);
#ifdef DEBUG
    sprintf(v->Msgbuf,"bfread got %ld bytes from buffer\n",count);
    dlog(v,v->Msgbuf);
#endif
    total += count;
    v->Filebufptr += count;
    v->Filebufcnt -= count;
  }

  /* If there wasn't enough in the buffer, read next buffer's worth and try again */
  if (total < length) {
    v->Filebufpos += v->Filebuflen;
    v->Filebufptr = v->Filebuf;
    v->Filebufcnt = v->Filebuflen = calladda(v->io.xpr_fread,v->Filebuf,1L,v->Filebufmax,v->File);
#ifdef DEBUG
    sprintf(v->Msgbuf,"bfread read %ld bytes\n",v->Filebuflen);
    dlog(v,v->Msgbuf);
#endif
    if (v->Filebufcnt) goto readmore;
    /* else we couldn't read as much as requested; return partial count */
  }

  return total;
}


/* Buffered file I/O fwrite() interface routine */
long bfwrite(v,buf,length)
register struct Vars *v;
register UBYTE *buf;
register long length;
{
  register long count, total = 0;

  /* Keep going until entire request completed */
  while (length > 0) {
    /* Copy as much as will fit into the buffer */
    count = v->Filebufmax - v->Filebufcnt;
    if (length < count) count = length;
    CopyMem(buf,v->Filebufptr,count);
#ifdef DEBUG
    sprintf(v->Msgbuf,"bfwrite buffered %ld bytes\n",count);
    dlog(v,v->Msgbuf);
#endif
    buf += count;
    total += count;
    length -= count;
    v->Filebufptr += count;
    v->Filebufcnt += count;
    v->Fileflush = TRUE;

    /* If we've filled the buffer, write it out */
    if (v->Filebufcnt == v->Filebufmax) {
      count = calladda(v->io.xpr_fwrite,v->Filebuf,1L,v->Filebufcnt,v->File);
#ifdef DEBUG
      sprintf(v->Msgbuf,"bfwrite wrote %ld bytes\n",count);
      dlog(v,v->Msgbuf);
#endif
      if (count < v->Filebufcnt) return -1;
      v->Filebufptr = v->Filebuf;
      v->Filebufcnt = 0;
      v->Fileflush = FALSE;
    }
  }

  return total;
}
    

/* Have the terminal program display an error message for us, using a
   temporary XPR_UPDATE structure; used to display errors before Vars 
   gets allocated */
void ioerr(io,msg)
register struct XPR_IO *io;
char *msg;
{
  struct XPR_UPDATE xpru;

  if (io->xpr_update) {
    xpru.xpru_updatemask = XPRU_ERRORMSG;
    xpru.xpru_errormsg = msg;
    calla(io->xpr_update,&xpru);
  }
}


/* Have the terminal program display an error message for us, using the
   normal XPR_IO structure allocated in Vars */
void upderr(v,msg)
register struct Vars *v;
char *msg;
{
  v->xpru.xpru_updatemask = XPRU_ERRORMSG;
  v->xpru.xpru_errormsg = msg;
  calla(v->io.xpr_update,&v->xpru);
#ifdef DEBUG
  dlog(v,msg);
  dlog(v,"\n");
#endif
}


/* Have the terminal program display a normal message for us */
void updmsg(v,msg)
register struct Vars *v;
char *msg;
{
  v->xpru.xpru_updatemask = XPRU_MSG;
  v->xpru.xpru_msg = msg;
  calla(v->io.xpr_update,&v->xpru);
#ifdef DEBUG
  dlog(v,msg);
  dlog(v,"\n");
#endif
}


/* Figure out how many bytes are free on the drive we're uploading to.
   Stubbed out for now; not supported by XPR spec. */
long getfree() {
  return 0x7FFFFFFF;
}


/* Check whether file already exists; used to detect potential overwrites */
char exist(v)
register struct Vars *v;
{
  register long file;

  file = callaa(v->io.xpr_fopen,v->Filename,"r");
  if (file) {
    calla(v->io.xpr_fclose,file);
    return TRUE;
  } else return FALSE;
}


#ifdef DEBUG
/* Write a message to the debug log */
dlog(v,s)
register struct Vars *v;
register UBYTE *s;
{
  /* Open the debug log if it isn't already open */
  if (!DebugLog) DebugLog = callaa(v->io.xpr_fopen,DebugName,"a");
  calladda(v->io.xpr_fwrite,s,1L,(long)strlen(s),DebugLog);
  /* Close file to flush output buffer; comment these two lines out if
     you aren't crashing your system and don't mind waiting until the
     transfer finishes to look at your log file. */
  calla(v->io.xpr_fclose,DebugLog);
  DebugLog = NULL;
}
#endif


/**
*
*   The following functions setup the proper registers for the call-back 
*   functions.
*
**/
#asm
        public  _calla
_calla:
        movea.l 8(sp),a0                ; Second argument goes in a0
        ; Clever trick to allow indirect JSR without using register
        move.l  4(sp),-(sp)             ; Push address of function to call
        rts                             ; "Return" to new function; its rts will...
                                        ; ...return to function who called us
        public  _callaa
_callaa:
        movea.l 8(sp),a0                ; Second argument goes in a0
        movea.l 12(sp),a1               ; Third  argument goes in a1
        move.l  4(sp),-(sp)             ; First  argument is function
        rts

        public _callad
_callad:
        movea.l 8(sp),a0                ; Second argument goes in a0
        move.l  12(sp),d0               ; Third  argument goes in d0
        move.l  4(sp),-(sp)             ; First  argument is function
        rts

        public  _calladd
_calladd:
        movea.l 8(sp),a0                ; Second argument goes in a0
        move.l  12(sp),d0               ; Third  argument goes in d0
        move.l  16(sp),d1               ; Fourth argument goes in d1
        move.l  4(sp),-(sp)             ; First  argument is function
        rts

        public  _calladda
_calladda:
        movea.l 8(sp),a0                ; Second argument goes in a0
        move.l  12(sp),d0               ; Third  argument goes in d0
        move.l  16(sp),d1               ; Fourth argument goes in d1
        movea.l 20(sp),a1               ; Fifth  argument goes in a1
        move.l  4(sp),-(sp)             ; First  argument is function
        rts

        public  _calld
_calld:
        move.l  8(sp),d0                ; Second argument goes in d0
        move.l  4(sp),-(sp)             ; First  argument is function
        rts

        public  _calldaa
_calldaa:
        move.l  8(sp),d0                ; Second argument goes in d0
        movea.l 12(sp),a0               ; Third  argument goes in a0
        movea.l 16(sp),a1               ; Fourth argument goes in a1
        move.l  4(sp),-(sp)             ; First  argument is function
        rts
#endasm
