/* ----------------------------------------------------------------------------- 
 * This file is part of SWIG, which is licensed as a whole under version 3 
 * (or any later version) of the GNU General Public License. Some additional
 * terms also apply to certain portions of SWIG. The full details of the SWIG
 * license and copyrights can be found in the LICENSE and COPYRIGHT files
 * included with the SWIG source code as distributed by the SWIG developers
 * and at https://www.swig.org/legal.html.
 *
 * fio.c
 *
 *     This file implements a number of standard I/O operations included
 *     formatted output, readline, and splitting.
 * ----------------------------------------------------------------------------- */

#include "dohint.h"

#define OBUFLEN  512

static DOH *encodings = 0;	/* Encoding hash */

/* -----------------------------------------------------------------------------
 * Writen()
 *
 * Writes N characters of output and retries until all characters are
 * written.  This is useful should a write operation encounter a spurious signal.
 * ----------------------------------------------------------------------------- */

static int Writen(DOH *out, void *buffer, int len) {
  int nw = len, ret;
  char *cb = (char *) buffer;
  while (nw) {
    ret = Write(out, cb, nw);
    if (ret < 0)
      return -1;
    nw = nw - ret;
    cb += ret;
  }
  return len;
}

/* -----------------------------------------------------------------------------
 * DohEncoding()
 *
 * Registers a new printf encoding method.  An encoder function should accept
 * two file-like objects and operate as a filter.
 * ----------------------------------------------------------------------------- */

void DohEncoding(const char *name, DOH *(*fn) (DOH *s)) {
  DohFuncPtr_t fp;

  if (!encodings)
    encodings = NewHash();

  fp.func = fn;
  Setattr(encodings, (void *) name, NewVoid(fp.p, 0));
}

/* internal function for processing an encoding */
static DOH *encode(char *name, DOH *s) {
  DOH *handle, *ns;
  DohFuncPtr_t fp;
  long pos;
  char *cfmt = strchr(name, ':');
  DOH *tmp = 0;
  if (cfmt) {
    tmp = NewString(cfmt + 1);
    Append(tmp, s);
    Setfile(tmp, Getfile((DOH *) s));
    Setline(tmp, Getline((DOH *) s));
    *cfmt = '\0';
  }
  if (!encodings || !(handle = Getattr(encodings, name))) {
    return Copy(s);
  }
  if (tmp)
    s = tmp;
  pos = Tell(s);
  Seek(s, 0, SEEK_SET);

  fp.p = Data(handle);
  ns = (*fp.func) (s);
  assert(pos != -1);
  (void)Seek(s, pos, SEEK_SET);
  if (tmp)
    Delete(tmp);
  return ns;
}

/* -----------------------------------------------------------------------------
 * DohvPrintf()
 *
 * DOH implementation of printf.  Output can be directed to any file-like object
 * including bare FILE * objects.  The same formatting codes as printf are
 * recognized with two extensions:
 *
 *       %s          - Prints a "char *" or the string representation of any
 *                     DOH object.  This will implicitly result in a call to
 *                     Str(obj).
 *
 *       %(encoder)* - Filters the output through an encoding function registered
 *                     with DohEncoder().
 *
 * Note: This function is not particularly memory efficient with large strings.
 * It's better to use Dump() or some other method instead.
 * ----------------------------------------------------------------------------- */

int DohvPrintf(DOH *so, const char *format, va_list ap) {
  static const char *fmt_codes = "dioxXucsSfeEgGpn";
  int state = 0;
  const char *p = format;
  char newformat[256];
  char obuffer[OBUFLEN];
  char *fmt = 0;
  char temp[64];
  int widthval = 0;
  int precval = 0;
  int maxwidth;
  char *w = 0;
  char *stemp;
  int nbytes = 0;
  char encoder[128], *ec = 0;
  int plevel = 0;

  memset(newformat, 0, sizeof(newformat));

  while (*p) {
    switch (state) {
    case 0:			/* Ordinary text */
      if (*p != '%') {
	Putc(*p, so);
	nbytes++;
      } else {
	fmt = newformat;
	widthval = 0;
	precval = 0;
	*(fmt++) = *p;
	encoder[0] = 0;
	state = 10;
      }
      break;
    case 10:			/* Look for a width and precision */
      if (isdigit((int) *p) && (*p != '0')) {
	w = temp;
	*(w++) = *p;
	*(fmt++) = *p;
	state = 20;
      } else if (strchr(fmt_codes, *p)) {
	/* Got one of the formatting codes */
	p--;
	state = 100;
      } else if (*p == '*') {
	/* Width field is specified in the format list */
	widthval = va_arg(ap, int);
	sprintf(temp, "%d", widthval);
	for (w = temp; *w; w++) {
	  *(fmt++) = *w;
	}
	state = 30;
      } else if (*p == '%') {
	Putc(*p, so);
	fmt = newformat;
	nbytes++;
	state = 0;
      } else if (*p == '(') {
	++plevel;
	ec = encoder;
	state = 60;
      } else {
	*(fmt++) = *p;
      }
      break;

    case 20:			/* Hmmm. At the start of a width field */
      if (isdigit((int) *p)) {
	*(w++) = *p;
	*(fmt++) = *p;
      } else if (strchr(fmt_codes, *p)) {
	/* Got one of the formatting codes */
	/* Figure out width */
	*w = 0;
	widthval = atoi(temp);
	p--;
	state = 100;
      } else if (*p == '.') {
	*w = 0;
	widthval = atoi(temp);
	w = temp;
	*(fmt++) = *p;
	state = 40;
      } else {
	/* ??? */
	*w = 0;
	widthval = atoi(temp);
	state = 50;
      }
      break;

    case 30:			/* Parsed a width from an argument.  Look for a . */
      if (*p == '.') {
	w = temp;
	*(fmt++) = *p;
	state = 40;
      } else if (strchr(fmt_codes, *p)) {
	/* Got one of the formatting codes */
	/* Figure out width */
	p--;
	state = 100;
      } else {
	/* hmmm. Something else. */
	state = 50;
      }
      break;

    case 40:
      /* Start of precision expected */
      if (isdigit((int) *p) && (*p != '0')) {
	*(fmt++) = *p;
	*(w++) = *p;
	state = 41;
      } else if (*p == '*') {
	/* Precision field is specified in the format list */
	precval = va_arg(ap, int);
	sprintf(temp, "%d", precval);
	for (w = temp; *w; w++) {
	  *(fmt++) = *w;
	}
	state = 50;
      } else if (strchr(fmt_codes, *p)) {
	p--;
	state = 100;
      } else {
	*(fmt++) = *p;
	state = 50;
      }
      break;
    case 41:
      if (isdigit((int) *p)) {
	*(fmt++) = *p;
	*(w++) = *p;
      } else if (strchr(fmt_codes, *p)) {
	/* Got one of the formatting codes */
	/* Figure out width */
	*w = 0;
	precval = atoi(temp);
	p--;
	state = 100;
      } else {
	*w = 0;
	precval = atoi(temp);
	*(fmt++) = *p;
	state = 50;
      }
      break;
      /* Hang out, wait for format specifier */
    case 50:
      if (strchr(fmt_codes, *p)) {
	p--;
	state = 100;
      } else {
	*(fmt++) = *p;
      }
      break;

      /* Got an encoding header */
    case 60:
      if (*p == '(') {
	++plevel;
	*ec = *p;
	ec++;
      } else if (*p == ')') {
	--plevel;
	if (plevel <= 0) {
	  *ec = 0;
	  state = 10;
	} else {
	  *ec = *p;
	  ec++;
	}
      } else {
	*ec = *p;
	ec++;
      }
      break;
    case 100:
      /* Got a formatting code */
      if (widthval < precval)
	maxwidth = precval;
      else
	maxwidth = widthval;
      if ((*p == 's') || (*p == 'S')) {	/* Null-Terminated string */
	DOH *doh;
	DOH *Sval;
	DOH *enc = 0;
	doh = va_arg(ap, DOH *);
	if (DohCheck(doh)) {
	  /* Is a DOH object. */
	  if (DohIsString(doh)) {
	    Sval = doh;
	  } else {
	    Sval = Str(doh);
	  }
	  if (strlen(encoder)) {
	    enc = encode(encoder, Sval);
	    maxwidth = maxwidth + (int)strlen(newformat) + Len(enc);
	  } else {
	    maxwidth = maxwidth + (int)strlen(newformat) + Len(Sval);
	  }
	  *(fmt++) = 's';
	  *fmt = 0;
	  if ((maxwidth + 1) < OBUFLEN) {
	    stemp = obuffer;
	  } else {
	    stemp = (char *) DohMalloc(maxwidth + 1);
	  }
	  if (enc) {
	    nbytes += sprintf(stemp, newformat, Data(enc));
	  } else {
	    nbytes += sprintf(stemp, newformat, Data(Sval));
	  }
	  if (Writen(so, stemp, (int)strlen(stemp)) < 0)
	    return -1;
	  if ((DOH *) Sval != doh) {
	    Delete(Sval);
	  }
	  if (enc)
	    Delete(enc);
	  if (*p == 'S') {
	    Delete(doh);
	  }
	  if (stemp != obuffer) {
	    DohFree(stemp);
	  }
	} else {
	  if (!doh)
	    doh = (char *) "";

	  if (strlen(encoder)) {
	    DOH *s = NewString(doh);
	    Seek(s, 0, SEEK_SET);
	    enc = encode(encoder, s);
	    Delete(s);
	    doh = Char(enc);
	  } else {
	    enc = 0;
	  }
	  maxwidth = maxwidth + (int)strlen(newformat) + (int)strlen((char *) doh);
	  *(fmt++) = 's';
	  *fmt = 0;
	  if ((maxwidth + 1) < OBUFLEN) {
	    stemp = obuffer;
	  } else {
	    stemp = (char *) DohMalloc(maxwidth + 1);
	  }
	  nbytes += sprintf(stemp, newformat, doh);
	  if (Writen(so, stemp, (int)strlen(stemp)) < 0)
	    return -1;
	  if (stemp != obuffer) {
	    DohFree(stemp);
	  }
	  if (enc)
	    Delete(enc);
	}
      } else {
	*(fmt++) = *p;
	*fmt = 0;
	maxwidth = maxwidth + (int)strlen(newformat) + 64;

	/* Only allocate a buffer if it is too big to fit.  Shouldn't have to do
	   this very often */

	if (maxwidth < OBUFLEN)
	  stemp = obuffer;
	else
	  stemp = (char *) DohMalloc(maxwidth + 1);
	switch (*p) {
	case 'd':
	case 'i':
	case 'o':
	case 'u':
	case 'x':
	case 'X':
	  if (p[-1] == 'l') {
	    if (p[-2] == 'l') {
	      long long llvalue = va_arg(ap, long long);
	      nbytes += sprintf(stemp, newformat, llvalue);
	      break;
	    }
	    long lvalue = va_arg(ap, long);
	    nbytes += sprintf(stemp, newformat, lvalue);
	    break;
	  }
	  /* FALLTHRU */
	case 'c': {
	  int ivalue = va_arg(ap, int);
	  nbytes += sprintf(stemp, newformat, ivalue);
	  break;
	}
	case 'f':
	case 'g':
	case 'e':
	case 'E':
	case 'G': {
	  double dvalue = va_arg(ap, double);
	  nbytes += sprintf(stemp, newformat, dvalue);
	  break;
	}
	case 'p': {
	  void *pvalue = va_arg(ap, void *);
	  nbytes += sprintf(stemp, newformat, pvalue);
	  break;
	}
	default:
	  break;
	}
	if (Writen(so, stemp, (int)strlen(stemp)) < 0)
	  return -1;
	if (stemp != obuffer)
	  DohFree(stemp);
      }
      state = 0;
      break;
    }
    p++;
  }
  if (state) {
    int r;
    *fmt = 0;
    r = Writen(so, fmt, (int)strlen(fmt));
    if (r < 0)
      return -1;
    nbytes += r;
  }
  return nbytes;
}

/* -----------------------------------------------------------------------------
 * DohPrintf()
 *
 * Variable length argument entry point to Printf
 * ----------------------------------------------------------------------------- */

int DohPrintf(DOH *obj, const char *format, ...) {
  va_list ap;
  int ret;
  va_start(ap, format);
  ret = DohvPrintf(obj, format, ap);
  va_end(ap);
  return ret;
}

/* -----------------------------------------------------------------------------
 * DohPrintv()
 * 
 * Print a null-terminated variable length list of DOH objects
 * ----------------------------------------------------------------------------- */

int DohPrintv(DOHFile * f, ...) {
  va_list ap;
  int ret = 0;
  DOH *obj;
  va_start(ap, f);
  while (1) {
    obj = va_arg(ap, void *);
    if ((!obj) || (obj == DohNone))
      break;
    if (DohCheck(obj)) {
      ret += DohDump(obj, f);
    } else {
      ret += DohWrite(f, obj, (int)strlen((char *) obj));
    }
  }
  va_end(ap);
  return ret;
}

/* ----------------------------------------------------------------------------- 
 * DohCopyto()
 *
 * Copies all of the input from an input stream to an output stream. Returns the
 * number of bytes copied.
 * ----------------------------------------------------------------------------- */

int DohCopyto(DOH *in, DOH *out) {
  int nbytes = 0, ret;
  int nwrite = 0, wret;
  char *cw;
  char buffer[16384];

  if ((!in) || (!out))
    return 0;
  while (1) {
    ret = Read(in, buffer, 16384);
    if (ret > 0) {
      nwrite = ret;
      cw = buffer;
      while (nwrite) {
	wret = Write(out, cw, nwrite);
	if (wret < 0) {
	  nbytes = -1;
	  break;
	}
	nwrite = nwrite - wret;
	cw += wret;
      }
      nbytes += ret;
    } else {
      break;
    }
  }
  return nbytes;
}


/* -----------------------------------------------------------------------------
 * DohSplit()
 *
 * Split an input stream into a list of strings delimited by the specified
 * character.  Optionally accepts a maximum number of splits to perform.
 * ----------------------------------------------------------------------------- */

DOH *DohSplit(DOH *in, char ch, int nsplits) {
  DOH *list;
  DOH *str;
  int c;

  list = NewList();

  if (DohIsString(in)) {
    Seek(in, 0, SEEK_SET);
  }

  while (1) {
    str = NewStringEmpty();
    do {
      c = Getc(in);
    } while ((c != EOF) && (c == ch));
    if (c != EOF) {
      Putc(c, str);
      while (1) {
	c = Getc(in);
	if ((c == EOF) || ((c == ch) && (nsplits != 0)))
	  break;
	Putc(c, str);
      }
      nsplits--;
    }
    Append(list, str);
    Delete(str);
    if (c == EOF)
      break;
  }
  return list;
}

/* -----------------------------------------------------------------------------
 * DohSplitLines()
 *
 * Split an input stream into a list of strings delimited by newline characters.
 * ----------------------------------------------------------------------------- */

DOH *DohSplitLines(DOH *in) {
  DOH *list;
  DOH *str;
  int c = 0;

  list = NewList();

  if (DohIsString(in)) {
    Seek(in, 0, SEEK_SET);
  }

  while (c != EOF) {
    str = NewStringEmpty();
    while ((c = Getc(in)) != '\n' && c != EOF) {
      Putc(c, str);
    }
    Append(list, str);
    Delete(str);
  }
  return list;
}


/* -----------------------------------------------------------------------------
 * DohReadline()
 *
 * Read a single input line and return it as a string.
 * ----------------------------------------------------------------------------- */

DOH *DohReadline(DOH *in) {
  char c;
  int n = 0;
  DOH *s = NewStringEmpty();
  while (1) {
    if (Read(in, &c, 1) < 0) {
      if (n == 0) {
	Delete(s);
	s = 0;
      }
      break;
    }
    if (c == '\n')
      break;
    if (c == '\r')
      continue;
    Putc(c, s);
    n++;
  }
  return s;
}
