#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include <nllibc.h>

struct nllibc_FILE {
  int fd;
#define __NLCC_FILE_FLAG_EOF (1 << 0)
  unsigned int flags;
  int error;
#define __NLCC_FILE_BUFFER_SIZE 64
  char *buffer;
  char *rbuffer;
  int size;
  int rsize;
};

static struct nllibc_FILE __nlstdin;
static struct nllibc_FILE __nlstdout;
static struct nllibc_FILE __nlstderr;

FILE *stdin  = (FILE *)&__nlstdin;
FILE *stdout = (FILE *)&__nlstdout;
FILE *stderr = (FILE *)&__nlstderr;

static char stdbuf[3][__NLCC_FILE_BUFFER_SIZE];
static char stdrbuf[3][__NLCC_FILE_BUFFER_SIZE];

static void stdio_exit(void)
{
  fflush(stdout);
  fflush(stderr);
}

static int stdio_init(void)
{
  static int initialized = 0;
  if (!initialized) {
    atexit(stdio_exit);
    memset(&__nlstdin,  0, sizeof(__nlstdin));
    memset(&__nlstdout, 0, sizeof(__nlstdout));
    memset(&__nlstderr, 0, sizeof(__nlstderr));
    __nlstdin.fd  = STDIN_FILENO;
    __nlstdout.fd = STDOUT_FILENO;
    __nlstderr.fd = STDERR_FILENO;
    __nlstdin.buffer  = stdbuf[0];
    __nlstdout.buffer = stdbuf[1];
    __nlstderr.buffer = stdbuf[2];
    __nlstdin.rbuffer  = stdrbuf[0];
    __nlstdout.rbuffer = stdrbuf[1];
    __nlstderr.rbuffer = stdrbuf[2];
    initialized = 1;
  }
  return 0;
}

int feof(FILE *stream)
{
  struct nllibc_FILE *fp;
  stdio_init();
  fp = (struct nllibc_FILE *)stream;
  return (fp->flags & __NLCC_FILE_FLAG_EOF) ? 1 : 0;
}

int ferror(FILE *stream)
{
  struct nllibc_FILE *fp;
  stdio_init();
  fp = (struct nllibc_FILE *)stream;
  return fp->error ? 1 : 0;
}

int fileno(FILE *stream)
{
  struct nllibc_FILE *fp;
  stdio_init();
  fp = (struct nllibc_FILE *)stream;
  return fp->fd;
}

static FILE *_fopen(int fd, const char *path, const char *mode)
{
  struct nllibc_FILE *fp = NULL;
  int m = 0, flags = 0;

  stdio_init();

  while (*mode != '\0') {
    switch (*(mode++)) {
    case 'r': m |= O_RDWR; flags = 0; break;
    case 'w': m |= O_WRONLY; flags = O_CREAT | O_TRUNC; break;
    case 'a': m |= O_WRONLY; flags = O_CREAT | O_APPEND; break;
    case '+': m |= O_WRONLY | O_RDWR; break;
    }
  }

  if (m & O_RDWR) {
    if (m & O_WRONLY)
      m = O_RDWR;
    else
      m = O_RDONLY;
  }

  flags |= m;

  fp = malloc(sizeof(*fp) + __NLCC_FILE_BUFFER_SIZE * 2);
  if (fp == NULL)
    goto err;
  memset(fp, 0, sizeof(*fp) + __NLCC_FILE_BUFFER_SIZE * 2);

  if (path != NULL) {
    fd = open(path, flags, 0644);
    if (fd < 0)
      goto err;
  }

  fp->fd = fd;
  fp->flags = 0;
  fp->error = 0;
  fp->buffer = (char *)fp + sizeof(*fp);
  fp->rbuffer = fp->buffer + __NLCC_FILE_BUFFER_SIZE;

  return (FILE *)fp;

err:
  if (fp != NULL)
    free(fp);
  if ((path != NULL) && (fd >= 0))
    close(fd);
  return NULL;
}

FILE *fopen(const char *path, const char *mode)
{
  return _fopen(-1, path, mode);
}

FILE *fdopen(int fildes, const char *mode)
{
  return _fopen(fildes, NULL, mode);
}

int fclose(FILE *stream)
{
  struct nllibc_FILE *fp;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  if (fp == NULL)
    return EOF;
  if (fp->fd >= 0) {
    fflush(stream);
    close(fp->fd);
  }
  free(fp);

  return 0;
}

static char *_utostr(unsigned long val)
{
  static char s[32];
  char *p;

  p = s + sizeof(s);
  *(--p) = '\0';
  while (val) {
    *(--p) = "0123456789"[val % 10];
    val /= 10;
  }
  if (*p == '\0')
    *(--p) = '0';

  return p;
}

static char *_itostr(long val)
{
  char *p;

  p = _utostr((val < 0) ? -val : val);
  if (val < 0)
    *(--p) = '-';

  return p;
}

static char *_otostr(unsigned long val)
{
  static char s[32];
  char *p;

  p = s + sizeof(s);
  *(--p) = '\0';
  while (val) {
    *(--p) = "01234567"[val & 0x7];
    val >>= 3;
  }
  if (*p == '\0')
    *(--p) = '0';

  return p;
}

static char *_xtostr(unsigned long val, int upper, int suffix)
{
  static char s[32];
  char *p;
  char *x;

  x = upper ? "0123456789ABCDEF" : "0123456789abcdef";

  p = s + sizeof(s);
  *(--p) = '\0';
  while (val) {
    *(--p) = x[val & 0xf];
    val >>= 4;
  }
  if (*p == '\0')
    *(--p) = '0';

  if (suffix) {
    *(--p) = 'x';
    *(--p) = '0';
  }

  return p;
}

static int bufread(struct nllibc_FILE *fp, char *buffer, int size)
{
  char *p;
  int r, s;

  if (fp->fd < 0)
    return -1;

  p = buffer;
  while (1) {
    if (fp->rsize > 0) {
      s = size;
      if (s > fp->rsize)
	s = fp->rsize;
      memcpy(p, fp->rbuffer, s);
      fp->rsize -= s;
      memmove(fp->rbuffer, fp->rbuffer + s, fp->rsize);
      p += s;
      size -= s;
    }
    if (!size)
      break;
    if (!fp->rsize) {
      r = read(fp->fd, fp->rbuffer, __NLCC_FILE_BUFFER_SIZE);
      if (r == 0)
	fp->flags |= __NLCC_FILE_FLAG_EOF;
      if (r <= 0)
	return r;
      fp->rsize = r;
    }
  }

  return p - buffer;
}

static int bufwrite(struct nllibc_FILE *fp, const char *buffer, int size)
{
  const char *p, *np = NULL;
  int s, i;

  if (fp->fd < 0)
    return -1;

  p = buffer;
  while (size > 0) {
    s = __NLCC_FILE_BUFFER_SIZE - fp->size;
    if (s > size)
      s = size;
    memcpy(fp->buffer + fp->size, p, s);
    p += s;
    fp->size += s;
    size -= s;
    if (size > 0) {
      if (write(fp->fd, fp->buffer, fp->size) < 0)
	fp->error = errno;
      fp->size = 0;
    } else {
      for (i = 0; i < s; i++) {
	np = &(fp->buffer[fp->size - 1 - i]);
	if (*np == '\n')
	  break;
	np = NULL;
      }
    }
  }

  if (np != NULL) {
    s = np - fp->buffer + 1;
    if (write(fp->fd, fp->buffer, s) < 0)
      fp->error = errno;
    memmove(fp->buffer, fp->buffer + s, fp->size - s);
    fp->size -= s;
  }

  return p - buffer;
}

static char *outc(char *p, FILE *stream, int c)
{
  struct nllibc_FILE *fp;
  char ch = c;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  if (p) {
    *(p++) = c;
    *p = '\0';
  }
  if (fp != NULL) {
    bufwrite(fp, &ch, 1);
  }

  return p;
}

static char *outs(char *p, FILE *stream, char *s, int col, int pad)
{
  struct nllibc_FILE *fp;
  int len;
  char ch;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  if (s == NULL)
    s = "(null)";
  len = strlen(s);

  ch = pad;
  for (; col > len; col--) {
    if (p) {
      *(p++) = ch;
      *p = '\0';
    }
    if (fp != NULL)
      bufwrite(fp, &ch, 1);
  }

  if (p) {
    strcpy(p, s);
    p += len;
  }
  if (fp != NULL) {
    bufwrite(fp, s, len);
  }

  return p;
}

static int va_sprintf(char *str, FILE *fp, const char *format, va_list ap)
{
  char *p = str, *sv;
  const char *f;
  int col = 0, pad = ' ';
  enum {
    MODE_NORMAL = 0,
    MODE_INT = 1,
    MODE_LONG = 2,
    MODE_QUAD = 3,
  } mode;

  stdio_init();

  mode = MODE_NORMAL;

  if (p)
    *p = '\0';

  for (f = format; *f; f++) {
    if (mode != MODE_NORMAL) {
      switch (*f) {
      case '0':
	if (!col) {
	  pad = '0';
	  continue;
	}
	/* fall through */
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
	col = col * 10 + (*f - '0');
	continue;
      case 'c':
	p = outc(p, fp, va_arg(ap, int));
	break;
      case 's':
	p = outs(p, fp, va_arg(ap, char *), col, pad);
	col = 0; pad = ' ';
	break;
      case 'l':
	switch (mode) {
	case MODE_INT:  mode = MODE_LONG; break;
	case MODE_LONG: mode = MODE_QUAD; break;
	default: break;
	}
	continue;
      case 'q':
	switch (mode) {
	case MODE_INT:
	case MODE_LONG: mode = MODE_QUAD; break;
	default: break;
	}
	continue;
      case 'd':
	switch (mode) {
	case MODE_LONG:
	case MODE_QUAD: sv = _itostr(va_arg(ap, long)); break;
	case MODE_INT:
	default:        sv = _itostr(va_arg(ap, int)); break;
	}
	p = outs(p, fp, sv, col, pad);
	col = 0; pad = ' ';
	break;
      case 'u':
	switch (mode) {
	case MODE_LONG:
	case MODE_QUAD: sv = _utostr(va_arg(ap, unsigned long)); break;
	case MODE_INT:
	default:        sv = _utostr(va_arg(ap, unsigned int)); break;
	}
	p = outs(p, fp, sv, col, pad);
	col = 0; pad = ' ';
	break;
      case 'o':
	switch (mode) {
	case MODE_LONG:
	case MODE_QUAD: sv = _otostr(va_arg(ap, unsigned long)); break;
	case MODE_INT:
	default:        sv = _otostr(va_arg(ap, unsigned int)); break;
	}
	p = outs(p, fp, sv, col, pad);
	col = 0; pad = ' ';
	break;
      case 'x':
	switch (mode) {
	case MODE_LONG:
	case MODE_QUAD: sv = _xtostr(va_arg(ap, unsigned long), 0, 0); break;
	case MODE_INT:
	default:        sv = _xtostr(va_arg(ap, unsigned int), 0, 0); break;
	}
	p = outs(p, fp, sv, col, pad);
	col = 0; pad = ' ';
	break;
      case 'X':
	switch (mode) {
	case MODE_LONG:
	case MODE_QUAD: sv = _xtostr(va_arg(ap, unsigned long), 1, 0); break;
	case MODE_INT:
	default:        sv = _xtostr(va_arg(ap, unsigned int), 1, 0); break;
	}
	p = outs(p, fp, sv, col, pad);
	col = 0; pad = ' ';
	break;
      case 'p':
	p = outs(p, fp, _xtostr((unsigned long)va_arg(ap, char *), 0, 1), col, pad);
	col = 0; pad = ' ';
	break;
      case 'f':
	break;
      case '%':
	p = outc(p, fp, '%');
	break;
      default:
	break;
      }
      mode = MODE_NORMAL;
      continue;
    }

    if (*f == '%') {
      mode = MODE_INT;
      continue;
    }

    p = outc(p, fp, *f);
  }

  return p - str;
}

int vsprintf(char *str, const char *format, va_list ap)
{
  return va_sprintf(str, NULL, format, ap);
}

int vfprintf(FILE *stream, const char *format, va_list ap)
{
  return va_sprintf(NULL, stream, format, ap);
}

int vprintf(const char *format, va_list ap)
{
  return va_sprintf(NULL, stdout, format, ap);
}

int sprintf(char *str, const char *format, ...)
{
  va_list arg;
  int done;

  va_start(arg, format);
  done = va_sprintf(str, NULL, format, arg);
  va_end(arg);

  return done;
}

int fprintf(FILE *stream, const char *format, ...)
{
  va_list arg;
  int done;

  va_start(arg, format);
  done = va_sprintf(NULL, stream, format, arg);
  va_end(arg);

  return done;
}

int printf(const char *format, ...)
{
  va_list arg;
  int done;

  va_start(arg, format);
  done = va_sprintf(NULL, stdout, format, arg);
  va_end(arg);

  return done;
}

int fgetc(FILE *stream)
{
  struct nllibc_FILE *fp;
  int r;
  char ch;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  r = bufread(fp, &ch, 1);
  if (r < 0)
    fp->error = errno;
  if (r <= 0)
    return EOF;

  return ch;
}

int fputc(int c, FILE *stream)
{
  struct nllibc_FILE *fp;
  int r;
  char ch = c;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  r = bufwrite(fp, &ch, 1);
  if (r < 0) {
    fp->error = errno;
    return EOF;
  }

  return (unsigned char)ch;
}

int puts(const char *str)
{
  struct nllibc_FILE *fp;
  int r;

  stdio_init();

  fp = (struct nllibc_FILE *)stdout;

  r = bufwrite(fp, str, strlen(str));
  if (r < 0) {
    fp->error = errno;
    return EOF;
  }

  if (bufwrite(fp, "\n", 1) < 0) {
    fp->error = errno;
    return EOF;
  }
  r++;

  return r;
}

char *fgets(char *str, int size, FILE *stream)
{
  struct nllibc_FILE *fp;
  int r;
  char ch, *p;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  p = str;
  while (1) {
    if (p + 1 - str >= size)
      break;
    r = bufread(fp, &ch, 1);
    if (r < 0)
      fp->error = errno;
    if (r <= 0) {
      if (p - str == 0)
	return NULL;
      break;
    }
    *(p++) = ch;
    if (ch == '\n')
      break;
  }

  *p = '\0';

  return str;
}

int fputs(const char *str, FILE *stream)
{
  struct nllibc_FILE *fp;
  int r;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  r = bufwrite(fp, str, strlen(str));
  if (r < 0) {
    fp->error = errno;
    return EOF;
  }

  return r;
}

int fflush(FILE *stream)
{
  struct nllibc_FILE *fp;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  if (fp->fd < 0) {
    errno = EBADF;
    return EOF;
  }

  if (fp->size > 0) {
    if (write(fp->fd, fp->buffer, fp->size) < 0)
      fp->error = errno;
    fp->size = 0;
  }

  return 0;
}

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
  struct nllibc_FILE *fp;
  int i, r;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  for (i = 0; i < nmemb; i++) {
    r = bufread(fp, (char *)ptr + size * i, size);
    if (r < 0)
      fp->error = errno;
    if (r < size)
      break;
  }

  return i;
}

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
{
  struct nllibc_FILE *fp;
  int i, r;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  for (i = 0; i < nmemb; i++) {
    r = bufwrite(fp, (const char *)ptr + size * i, size);
    if (r < 0)
      fp->error = errno;
    if (r < size)
      break;
  }

  return i;
}

int fseek(FILE *stream, long offset, int whence)
{
  struct nllibc_FILE *fp;

  stdio_init();

  fp = (struct nllibc_FILE *)stream;

  if (lseek(fp->fd, offset, whence) < 0)
    return -1;

  return 0;
}
