/**
 * See Copyright Notice in picrin.h
 */

#include "picrin.h"
#include "object.h"
#include "state.h"

#ifndef EOF
# define EOF (-1)
#endif

pic_value
pic_funopen(pic_state *pic, void *cookie, const pic_port_type *type)
{
  struct port *port;

  port = (struct port *)pic_obj_alloc(pic, PIC_TYPE_PORT);
  port->file.cnt = 0;
  port->file.base = NULL;
  port->file.flag = type->read ? FILE_READ : FILE_WRITE;
  port->file.cookie = cookie;
  port->file.vtable = type;

  return obj_value(pic, port);
}

int
pic_fclose(pic_state *pic, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  if (fp->flag == 0)
    return 0;
  pic_fflush(pic, port);
  fp->flag = 0;
  if (fp->base != fp->buf)
    pic_free(pic, fp->base);
  return fp->vtable->close(pic, fp->cookie);
}

void
pic_clearerr(pic_state *PIC_UNUSED(pic), pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  fp->flag &= ~(FILE_EOF | FILE_ERR);
}

int
pic_feof(pic_state *PIC_UNUSED(pic), pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  return (fp->flag & FILE_EOF) != 0;
}

int
pic_ferror(pic_state *PIC_UNUSED(pic), pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  return (fp->flag & FILE_ERR) != 0;
}

static int
fillbuf(pic_state *pic, struct file *fp)
{
  int bufsize;

  if ((fp->flag & (FILE_READ|FILE_EOF|FILE_ERR)) != FILE_READ)
    return EOF;
  if (fp->base == NULL) {
    if ((fp->flag & FILE_UNBUF) == 0) {
      /* no buffer yet */
      if ((fp->base = pic_malloc(pic, PIC_BUFSIZ)) == NULL) {
        /* can't get buffer, try unbuffered */
        fp->flag |= FILE_UNBUF;
      }
    }
    if (fp->flag & FILE_UNBUF) {
      fp->base = fp->buf;
    }
  }
  bufsize = (fp->flag & FILE_UNBUF) ? sizeof(fp->buf) : PIC_BUFSIZ;

  fp->ptr = fp->base;
  fp->cnt = fp->vtable->read(pic, fp->cookie, fp->ptr, bufsize);

  if (--fp->cnt < 0) {
    if (fp->cnt == -1)
      fp->flag |= FILE_EOF;
    else
      fp->flag |= FILE_ERR;
    fp->cnt = 0;
    return EOF;
  }

  return (unsigned char) *fp->ptr++;
}

static int
flushbuf(pic_state *pic, int x, struct file *fp)
{
  int num_written=0, bufsize=0;
  char c = x;

  if ((fp->flag & (FILE_WRITE|FILE_EOF|FILE_ERR)) != FILE_WRITE)
    return EOF;
  if (fp->base == NULL && ((fp->flag & FILE_UNBUF) == 0)) {
    /* no buffer yet */
    if ((fp->base = pic_malloc(pic, PIC_BUFSIZ)) == NULL) {
      /* couldn't allocate a buffer, so try unbuffered */
      fp->flag |= FILE_UNBUF;
    } else {
      fp->ptr = fp->base;
      fp->cnt = PIC_BUFSIZ - 1;
    }
  }
  if (fp->flag & FILE_UNBUF) {
    /* unbuffered write */
    fp->ptr = fp->base = NULL;
    fp->cnt = 0;
    if (x == EOF)
      return EOF;
    num_written = fp->vtable->write(pic, fp->cookie, (const char *) &c, 1);
    bufsize = 1;
  } else {
    /* buffered write */
    assert(fp->ptr);
    if (x != EOF) {
      *fp->ptr++ = (unsigned char) c;
    }
    bufsize = (int)(fp->ptr - fp->base);
    while(bufsize - num_written > 0) {
      int t;
      t = fp->vtable->write(pic, fp->cookie, fp->base + num_written, bufsize - num_written);
      if (t < 0)
        break;
      num_written += t;
    }

    fp->ptr = fp->base;
    fp->cnt = PIC_BUFSIZ - 1;
  }

  if (num_written == bufsize) {
    return x;
  } else {
    fp->flag |= FILE_ERR;
    return EOF;
  }
}

int
pic_fflush(pic_state *pic, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;
  int retval;

  retval = 0;
  if ((fp->flag & FILE_WRITE) == 0)
    return -1;
  flushbuf(pic, EOF, fp);
  if (fp->flag & FILE_ERR)
    retval = -1;
  return retval;
}

#define getc_(pic, p)                           \
  ((--(p)->cnt >= 0)                            \
   ? (unsigned char) *(p)->ptr++                \
   : fillbuf((pic), p))
#define putc_(pic, x, p)                                                \
  ((--(p)->cnt >= 0 && !(((p)->flag & FILE_LNBUF) && (x) == '\n'))      \
   ? *(p)->ptr++ = (x)                                                  \
   : flushbuf((pic), (x), (p)))

int
pic_fputc(pic_state *pic, int x, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  return putc_(pic, x, fp);
}

int
pic_fgetc(pic_state *pic, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  return getc_(pic, fp);
}

int
pic_fputs(pic_state *pic, const char *s, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;

  const char *ptr = s;
  while(*ptr != '\0') {
    if (putc_(pic, *ptr, fp) == EOF)
      return EOF;
    ++ptr;
  }
  return (int)(ptr - s);
}

char *
pic_fgets(pic_state *pic, char *s, int size, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;
  int c = 0;
  char *buf;

  pic_fflush(pic, port);

  if (size == 0) {
    return NULL;
  }
  buf = s;
  while (--size > 0 && (c = getc_(pic, fp)) != EOF) {
    if ((*buf++ = c) == '\n')
      break;
  }
  *buf = '\0';

  return (c == EOF && buf == s) ? NULL : s;
}

int
pic_ungetc(pic_state *PIC_UNUSED(pic), int c, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;
  unsigned char uc = c;

  if (c == EOF || fp->base == fp->ptr) {
    return EOF;
  }
  fp->cnt++;
  return *--fp->ptr = uc;
}

size_t
pic_fread(pic_state *pic, void *ptr, size_t size, size_t count, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;
  char *bptr = ptr;
  long nbytes;
  int c;

  nbytes = size * count;
  while (nbytes > fp->cnt) {
    memcpy(bptr, fp->ptr, fp->cnt);
    fp->ptr += fp->cnt;
    bptr += fp->cnt;
    nbytes -= fp->cnt;
    if ((c = fillbuf(pic, fp)) == EOF) {
      return (size * count - nbytes) / size;
    } else {
      pic_ungetc(pic, c, port);
    }
  }
  memcpy(bptr, fp->ptr, nbytes);
  fp->ptr += nbytes;
  fp->cnt -= nbytes;
  return count;
}

size_t
pic_fwrite(pic_state *pic, const void *ptr, size_t size, size_t count, pic_value port)
{
  struct file *fp = &port_ptr(pic, port)->file;
  const char *bptr = ptr;
  long nbytes;

  nbytes = size * count;
  while (nbytes > fp->cnt) {
    memcpy(fp->ptr, bptr, fp->cnt);
    fp->ptr += fp->cnt;
    bptr += fp->cnt;
    nbytes -= fp->cnt;
    if (flushbuf(pic, EOF, fp) == EOF) {
      return (size * count - nbytes) / size;
    }
  }
  memcpy(fp->ptr, bptr, nbytes);
  fp->ptr += nbytes;
  fp->cnt -= nbytes;
  return count;
}

long
pic_fseek(pic_state *pic, pic_value port, long offset, int whence)
{
  struct file *fp = &port_ptr(pic, port)->file;
  long s;

  pic_fflush(pic, port);

  fp->ptr = fp->base;
  fp->cnt = 0;

  if ((s = fp->vtable->seek(pic, fp->cookie, offset, whence)) != 0)
    return s;
  fp->flag &= ~FILE_EOF;
  return 0;
}

int
pic_vfprintf(pic_state *pic, pic_value port, const char *fmt, va_list ap)
{
  const char *p;
  long start = pic_fseek(pic, port, 0, PIC_SEEK_CUR);

  for (p = fmt; *p; p++) {
    if (*p == '~') {
      switch (*++p) {
      default:
        pic_fputc(pic, *(p-1), port);
        break;
      case '%':
        pic_fputc(pic, '\n', port);
        break;
      case 'a':
        pic_void(pic, pic_funcall(pic, "display", 2, va_arg(ap, pic_value), port));
        break;
      case 's':
        pic_void(pic, pic_funcall(pic, "write", 2, va_arg(ap, pic_value), port));
        break;
      }
      continue;
    }
    if (*p == '%') {
      switch (*++p) {
      case 'd':
      case 'i': {
        int ival = va_arg(ap, int);
        pic_value str = pic_funcall(pic, "number->string", 1, pic_int_value(pic, ival));
        pic_fputs(pic, pic_str(pic, str, 0), port);
        break;
      }
      case 'f': {
        double f = va_arg(ap, double);
        pic_value str = pic_funcall(pic, "number->string", 1, pic_float_value(pic, f));
        pic_fputs(pic, pic_str(pic, str, 0), port);
        break;
      }
      case 'c': {
        int ival = va_arg(ap, int);
        pic_fputc(pic, ival, port);
        break;
      }
      case 's': {
        char *sval = va_arg(ap, char*);
        pic_fputs(pic, sval, port);
        break;
      }
      case 'p': {
        static const char digits[] = "0123456789abcdef";
        unsigned long vp = (unsigned long) va_arg(ap, void*);
        char buf[sizeof vp * CHAR_BIT / 4 + 1];
        size_t i;
        for (i = 0; i < sizeof buf - 1; ++i) {
          buf[sizeof buf - i - 2] = digits[vp % 16];
          vp /= 16;
        }
        buf[i] = '\0';
        pic_fputs(pic, "0x", port);
        pic_fputs(pic, buf, port);
        break;
      }
      case '%':
        pic_fputc(pic, *(p-1), port);
        break;
      default:
        pic_fputc(pic, '%', port);
        pic_fputc(pic, *(p-1), port);
        break;
      }
      continue;
    }
    pic_fputc(pic, *p, port);
  }
  return pic_fseek(pic, port, 0, PIC_SEEK_CUR) - start;
}

int
pic_fprintf(pic_state *pic, pic_value port, const char *fmt, ...)
{
  va_list ap;
  int n;

  va_start(ap, fmt);
  n = pic_vfprintf(pic, port, fmt, ap);
  va_end(ap);
  return n;
}

int
pic_printf(pic_state *pic, const char *fmt, ...)
{
  va_list ap;
  int n;

  va_start(ap, fmt);
  n = pic_vfprintf(pic, pic_stdout(pic), fmt, ap);
  va_end(ap);
  return n;
}

typedef struct { char *buf; long pos, end, capa; } xbuf_t;

static int
string_read(pic_state *PIC_UNUSED(pic), void *cookie, char *ptr, int size)
{
  xbuf_t *m = cookie;

  if (size > (int)(m->end - m->pos))
    size = (int)(m->end - m->pos);
  memcpy(ptr, m->buf + m->pos, size);
  m->pos += size;
  return size;
}

static int
string_write(pic_state *pic, void *cookie, const char *ptr, int size)
{
  xbuf_t *m = cookie;

  if (m->pos + size >= m->capa) {
    m->capa = (m->pos + size) * 2;
    m->buf = pic_realloc(pic, m->buf, m->capa);
  }
  memcpy(m->buf + m->pos, ptr, size);
  m->pos += size;
  if (m->end < m->pos)
    m->end = m->pos;
  return size;
}

static long
string_seek(pic_state *PIC_UNUSED(pic), void *cookie, long pos, int whence)
{
  xbuf_t *m = cookie;

  switch (whence) {
  case PIC_SEEK_SET:
    m->pos = pos;
    break;
  case PIC_SEEK_CUR:
    m->pos += pos;
    break;
  case PIC_SEEK_END:
    m->pos = m->end + pos;
    break;
  }

  return m->pos;
}

static int
string_close(pic_state *pic, void *cookie)
{
  xbuf_t *m = cookie;

  pic_free(pic, m->buf);
  pic_free(pic, m);
  return 0;
}

pic_value
pic_fmemopen(pic_state *pic, const char *data, int size, const char *mode)
{
  static const pic_port_type string_rd = { string_read, 0, string_seek, string_close };
  static const pic_port_type string_wr = { 0, string_write, string_seek, string_close };
  xbuf_t *m;

  m = pic_malloc(pic, sizeof(xbuf_t));
  m->buf = pic_malloc(pic, size);
  m->pos = 0;
  m->end = size;
  m->capa = size;

  if (*mode == 'r') {
    memcpy(m->buf, data, size);
    return pic_funopen(pic, m, &string_rd);
  } else {
    return pic_funopen(pic, m, &string_wr);
  }
}

int
pic_fgetbuf(pic_state *pic, pic_value port, const char **buf, int *len)
{
  struct file *fp = &port_ptr(pic, port)->file;
  xbuf_t *s;

  pic_fflush(pic, port);

  if (fp->vtable->write != string_write) {
    return -1;
  }
  s = fp->cookie;
  *len = s->end;
  *buf = s->buf;
  return 0;
}

bool
pic_port_p(pic_state *pic, pic_value obj, const pic_port_type *type)
{
  if (pic_type(pic, obj) != PIC_TYPE_PORT) {
    return false;
  }
  return type == NULL || port_ptr(pic, obj)->file.vtable == type;
}

static pic_value
pic_port_input_port_p(pic_state *pic)
{
  pic_value v;

  pic_get_args(pic, "o", &v);

  if (pic_port_p(pic, v, NULL) && (port_ptr(pic, v)->file.flag & FILE_READ) != 0) {
    return pic_true_value(pic);
  } else {
    return pic_false_value(pic);
  }
}

static pic_value
pic_port_output_port_p(pic_state *pic)
{
  pic_value v;

  pic_get_args(pic, "o", &v);

  if (pic_port_p(pic, v, NULL) && (port_ptr(pic, v)->file.flag & FILE_WRITE) != 0) {
    return pic_true_value(pic);
  }
  else {
    return pic_false_value(pic);
  }
}

static pic_value
pic_port_port_p(pic_state *pic)
{
  pic_value v;

  pic_get_args(pic, "o", &v);

  return pic_bool_value(pic, pic_port_p(pic, v, NULL));
}

static pic_value
pic_port_eof_object_p(pic_state *pic)
{
  pic_value v;

  pic_get_args(pic, "o", &v);

  return pic_bool_value(pic, pic_eof_p(pic, v));
}

static pic_value
pic_port_eof_object(pic_state *pic)
{
  pic_get_args(pic, "");

  return pic_eof_object(pic);
}

static pic_value
pic_port_port_open_p(pic_state *pic)
{
  pic_value port;

  pic_get_args(pic, "p", &port);

  return pic_bool_value(pic, port_ptr(pic, port)->file.flag != 0);
}

static pic_value
pic_port_close_port(pic_state *pic)
{
  pic_value port;

  pic_get_args(pic, "p", &port);

  pic_fclose(pic, port);

  return pic_undef_value(pic);
}

#define assert_port_profile(port, flags, caller) do {           \
    int flag = port_ptr(pic, port)->file.flag;                  \
    if ((flag & (flags)) != (flags)) {                          \
      switch (flags) {                                          \
      case FILE_WRITE:                                          \
        pic_error(pic, caller ": output port required", 0);     \
      case FILE_READ:                                           \
        pic_error(pic, caller ": input port required", 0);      \
      }                                                         \
    }                                                           \
    if (flag == 0) {                                            \
      pic_error(pic, caller ": open port required", 0);         \
    }                                                           \
  } while (0)

static pic_value
pic_port_open_input_bytevector(pic_state *pic)
{
  unsigned char *buf;
  int len;

  pic_get_args(pic, "b", &buf, &len);

  return pic_fmemopen(pic, (char *)buf, len, "r");
}

static pic_value
pic_port_open_output_bytevector(pic_state *pic)
{
  pic_get_args(pic, "");

  return pic_fmemopen(pic, NULL, 0, "w");
}

static pic_value
pic_port_get_output_bytevector(pic_state *pic)
{
  pic_value port = pic_stdout(pic);
  const char *buf;
  int len;

  pic_get_args(pic, "|p", &port);

  assert_port_profile(port, FILE_WRITE, "get-output-bytevector");

  if (pic_fgetbuf(pic, port, &buf, &len) < 0) {
    pic_error(pic, "port was not created by open-output-bytevector", 0);
  }
  return pic_blob_value(pic, (unsigned char *)buf, len);
}

static pic_value
pic_port_read_u8(pic_state *pic)
{
  pic_value port = pic_stdin(pic);
  int c;

  pic_get_args(pic, "|p", &port);

  assert_port_profile(port, FILE_READ, "read-u8");
  if ((c = pic_fgetc(pic, port)) == EOF) {
    return pic_eof_object(pic);
  }

  return pic_int_value(pic, c);
}

static pic_value
pic_port_peek_u8(pic_state *pic)
{
  int c;
  pic_value port = pic_stdin(pic);

  pic_get_args(pic, "|p", &port);

  assert_port_profile(port, FILE_READ, "peek-u8");

  c = pic_fgetc(pic, port);
  if (c == EOF) {
    return pic_eof_object(pic);
  }
  else {
    pic_ungetc(pic, c, port);
    return pic_int_value(pic, c);
  }
}

static pic_value
pic_port_read_bytevector_ip(pic_state *pic)
{
  pic_value port;
  unsigned char *buf;
  int n, start, end, i, len;

  n = pic_get_args(pic, "b|pii", &buf, &len, &port, &start, &end);

  switch (n) {
  case 1:
    port = pic_stdin(pic);
  case 2:
    start = 0;
  case 3:
    end = len;
  }

  VALID_RANGE(pic, len, start, end);
  assert_port_profile(port, FILE_READ, "read-bytevector!");

  i = pic_fread(pic, buf + start, 1, end - start, port);
  if (i == 0) {
    return pic_eof_object(pic);
  }
  return pic_int_value(pic, i);
}

static pic_value
pic_port_write_u8(pic_state *pic)
{
  int i;
  pic_value port = pic_stdout(pic);

  pic_get_args(pic, "i|p", &i, &port);

  assert_port_profile(port, FILE_WRITE, "write-u8");

  pic_fputc(pic, i, port);
  return pic_undef_value(pic);
}

static pic_value
pic_port_write_bytevector(pic_state *pic)
{
  pic_value port;
  unsigned char *buf;
  int n, start, end, len, done;

  n = pic_get_args(pic, "b|pii", &buf, &len, &port, &start, &end);

  switch (n) {
  case 1:
    port = pic_stdout(pic);
  case 2:
    start = 0;
  case 3:
    end = len;
  }

  VALID_RANGE(pic, len, start, end);
  assert_port_profile(port, FILE_WRITE, "write-bytevector");

  done = 0;
  while (done < end - start) {
    done += pic_fwrite(pic, buf + start + done, 1, end - start - done, port);
    /* FIXME: error check... */
  }
  return pic_undef_value(pic);
}

static pic_value
pic_port_flush(pic_state *pic)
{
  pic_value port = pic_stdout(pic);

  pic_get_args(pic, "|p", &port);

  assert_port_profile(port, FILE_WRITE, "flush-output-port");

  pic_fflush(pic, port);
  return pic_undef_value(pic);
}

void
pic_init_port(pic_state *pic)
{
  pic_defun(pic, "port?", pic_port_port_p);
  pic_defun(pic, "input-port?", pic_port_input_port_p);
  pic_defun(pic, "output-port?", pic_port_output_port_p);
  pic_defun(pic, "port-open?", pic_port_port_open_p);
  pic_defun(pic, "close-port", pic_port_close_port);

  pic_defun(pic, "eof-object?", pic_port_eof_object_p);
  pic_defun(pic, "eof-object", pic_port_eof_object);

  /* input */
  pic_defun(pic, "read-u8", pic_port_read_u8);
  pic_defun(pic, "peek-u8", pic_port_peek_u8);
  pic_defun(pic, "read-bytevector!", pic_port_read_bytevector_ip);

  /* output */
  pic_defun(pic, "write-u8", pic_port_write_u8);
  pic_defun(pic, "write-bytevector", pic_port_write_bytevector);
  pic_defun(pic, "flush-output-port", pic_port_flush);

  /* string I/O */
  pic_defun(pic, "open-input-bytevector", pic_port_open_input_bytevector);
  pic_defun(pic, "open-output-bytevector", pic_port_open_output_bytevector);
  pic_defun(pic, "get-output-bytevector", pic_port_get_output_bytevector);
}