#include <errno.h>
#include <string.h>
#include "mb.h"

typedef struct mb_pos_info_st {
  long offset;
  fpos_t base;
  int used;
  char buf[BUFSIZ];
  mb_info_t info;
} mb_pos_info_t;

static mb_pos_info_t **mb_info_r_v = NULL;
static size_t mb_info_r_n = 0;

static mb_pos_info_t **mb_info_w_v = NULL;
static size_t mb_info_w_n = 0;

static mb_pos_info_t *dummy;

static int
mb_find_info(FILE *fp, mb_pos_info_t **p_info_r, mb_pos_info_t **p_info_w)
{
  int n = 0;
  int fd = fileno(fp);

  if (!p_info_r)
    p_info_r = &dummy;

  if (!p_info_w)
    p_info_w = &dummy;

  *p_info_r = *p_info_w = NULL;

  if (fd >= 0) {
    if (fd < mb_info_r_n && mb_info_r_v[fd] && mb_info_r_v[fd]->used) {
      ++n;
      *p_info_r = mb_info_r_v[fd];
    }

    if (fd < mb_info_w_n && mb_info_w_v[fd] && mb_info_w_v[fd]->used) {
      ++n;
      *p_info_w = mb_info_w_v[fd];
    }
  }

  return n;
}

int
mb_fclose(FILE *fp)
{
  mb_pos_info_t *info_r, *info_w;

  mb_find_info(fp, &info_r, &info_w);

  if (info_r)
    memset(info_r, 0, sizeof(*info_r));

  if (info_w) {
    mb_store_char_noconv(EOF, &info_w->info);
    mb_flush(&info_w->info);
    memset(info_w, 0, sizeof(*info_w));
  }

  return fclose(fp);
}

static mb_setup_t mb_fsetup_r;
static mb_setup_t mb_fsetup_w;

void
mb_vfsetup_default(const char *op, va_list ap)
{
  if (op[strcspn(op, "r+")])
    mb_vsetsetup(&mb_fsetup_r, op, ap);

  if (op[strcspn(op, "aw+")])
    mb_vsetsetup(&mb_fsetup_w, op, ap);
}

void
mb_fsetup_default(const char *op, ...)
{
  va_list ap;

  va_start(ap, op);
  mb_vfsetup_default(op, ap);
  va_end(ap);
}

void
mb_vfsetup(FILE *fp, const char *op, va_list ap)
{
  if (fp) {
    mb_pos_info_t *info_r, *info_w;

    mb_find_info(fp, &info_r, &info_w);

    if (op[strcspn(op, "r+")] && info_r)
      mb_vsetup(&info_r->info, &mb_fsetup_r, op, ap);

    if (op[strcspn(op, "aw+")] && info_w)
      mb_vsetup(&info_w->info, &mb_fsetup_w, op, ap);
  }
}

void
mb_fsetup(FILE *fp, const char *op, ...)
{
  va_list ap;

  va_start(ap, op);
  mb_vfsetup(fp, op, ap);
  va_end(ap);
}

static size_t
mb_call_fread(char *s, size_t n, void *fp)
{
  size_t m = fread(s, 1, n, fp);

  if (m > 0) {
    mb_pos_info_t *info_r;

    if (mb_find_info(fp, &info_r, NULL))
      info_r->offset += m;
  }

  return m;
}

static size_t
mb_call_fwrite(const char *s, size_t n, void *fp)
{
  size_t m = fwrite(s, 1, n, fp);

  if (m > 0) {
    mb_pos_info_t *info_w;

    if (mb_find_info(fp, NULL, &info_w))
      info_w->offset += m;
  }

  return m;
}

static mb_info_t *
mb_new_pos_info(mb_pos_info_t ***p_v, size_t *p_n, FILE *fp)
{
  int fd = fileno(fp);
  mb_pos_info_t *pi;

  if (fd < 0)
    return NULL;

  if (fd >= *p_n) {
    size_t nn;
    size_t nsize;
    mb_pos_info_t **nv;

    nn = (fd / 2 + 1) * 3;
    nsize = nn * sizeof(mb_pos_info_t *);

    if (!(nv = *p_v ? alt_call_realloc(*p_v, nsize) : alt_call_malloc(nsize)))
      return NULL;

    memset(&nv[*p_n], 0, nsize - *p_n * sizeof(mb_pos_info_t *));
    *p_n = nn;
    *p_v = nv;
  }

  if (!(pi = (*p_v)[fd])) {
    if (!((*p_v)[fd] = pi = alt_call_malloc(sizeof(mb_pos_info_t))))
      return NULL;

    memset(pi, 0, sizeof(*pi));
  }

  pi->used = 1;
  pi->info.buf = pi->buf;
  pi->info.size = sizeof(pi->buf);
  return &(*p_v)[fd]->info;
}

FILE *
mb_vfbind(FILE *fp, const char *op, va_list ap)
{
  if (fp) {
    mb_info_t *info;

    if (op[strcspn(op, "r+")] && (info = mb_new_pos_info(&mb_info_r_v, &mb_info_r_n, fp)))
      mb_vinit_r(info, fp, mb_call_fread, &mb_fsetup_r, op, ap);

    if (op[strcspn(op, "aw+")] && (info = mb_new_pos_info(&mb_info_w_v, &mb_info_w_n, fp)))
      mb_vinit_w(info, fp, mb_call_fwrite, &mb_fsetup_w, op, ap);
  }

  return fp;
}

FILE *
mb_fbind(FILE *fp, const char *op, ...)
{
  va_list ap;

  va_start(ap, op);
  fp = mb_vfbind(fp, op, ap);
  va_end(ap);
  return fp;
}

#define MB_IO_MODE ("rwa+b")

static void
mb_parsemode(const char *mode, char *f)
{
  size_t i;

  for (i = 0 ; i < sizeof(MB_IO_MODE) - 1 && mode[i] ; ++i)
    if (strchr(MB_IO_MODE, mode[i]))
      *f++ = mode[i];

  *f++ = '\0';
}

FILE *
mb_vfopen(const char *fn, const char *mode, va_list ap)
{
  char f_mode[sizeof(MB_IO_MODE)];

  mb_parsemode(mode, f_mode);
  return mb_vfbind(fopen(fn, f_mode), mode, ap);
}

FILE *
mb_fopen(const char *fn, const char *mode, ...)
{
  va_list ap;
  FILE *fp;

  va_start(ap, mode);
  fp = mb_vfopen(fn, mode, ap);
  va_end(ap);
  return fp;
}

FILE *
mb_vfreopen(const char *fn, const char *mode, FILE *fp, va_list ap)
{
  char f_mode[sizeof(MB_IO_MODE)];

  mb_parsemode(mode, f_mode);
  return mb_vfbind(freopen(fn, f_mode, fp), mode, ap);
}

FILE *
mb_freopen(const char *fn, const char *mode, FILE *fp, ...)
{
  va_list ap;

  va_start(ap, fp);
  fp = mb_vfreopen(fn, mode, fp, ap);
  va_end(ap);
  return fp;
}

FILE *
mb_vfdopen(int fd, const char *mode, va_list ap)
{
  char f_mode[sizeof(MB_IO_MODE)];

  mb_parsemode(mode, f_mode);
  return mb_vfbind(fdopen(fd, f_mode), mode, ap);
}

FILE *
mb_fdopen(int fd, const char *mode, ...)
{
  va_list ap;
  FILE *fp;

  va_start(ap, mode);
  fp = mb_vfdopen(fd, mode, ap);
  va_end(ap);
  return fp;
}

static int
mb_do_fgetpos(FILE *fp, fpos_t *p_pos, mb_pos_info_t *pip)
{
  int res = fseek(fp, -pip->info.e, SEEK_CUR);

  if (!res) {
    pip->info.e = 0;

    if (!(res = fgetpos(fp, p_pos))) {
      pip->offset = 0;
      pip->base = *p_pos;
    }
  }

  return res;
}

int
mb_fgetpos(FILE *fp, fpos_t *p_pos)
{
  mb_pos_info_t *info_r, *info_w;

  mb_find_info(fp, &info_r, &info_w);
  errno = 0;

  if (info_r)
    return mb_do_fgetpos(fp, p_pos, info_r);

  if (info_w)
    return mb_do_fgetpos(fp, p_pos, info_w);

  return fgetpos(fp, p_pos);
}

int
mb_fsetpos(FILE *fp, const fpos_t *p_pos)
{
  mb_pos_info_t *info_r, *info_w;
  int res;

  mb_find_info(fp, &info_r, &info_w);

  if (info_w)
    mb_flush(&info_w->info);

  if (!(res = fsetpos(fp, p_pos))) {
    if (info_r) {
      info_r->base = *p_pos;
      info_r->offset = 0;
      info_r->info.b = info_r->info.i = info_r->info.e = 0;
    }

    if (info_w) {
      info_w->base = *p_pos;
      info_w->offset = 0;
      info_w->info.b = info_w->info.i = info_w->info.e = 0;
    }
  }

  return res;
}

long
mb_ftell(FILE *fp)
{
  mb_pos_info_t *info_r, *info_w;
  fpos_t dummy;

  mb_find_info(fp, &info_r, &info_w);
  errno = 0;

  if (info_r)
    if (mb_do_fgetpos(fp, &dummy, info_r))
      return -1;

  if (info_w)
    if (mb_do_fgetpos(fp, &dummy, info_w))
      return -1;

  return ftell(fp);
}

int
mb_fseek(FILE *fp, long off, int mode)
{
  mb_pos_info_t *info_r, *info_w;
  int res;

  mb_find_info(fp, &info_r, &info_w);

  if (info_w)
    mb_flush(&info_w->info);

  if (!(res = fseek(fp, off, mode))) {
    fpos_t pos;

    if (!fgetpos(fp, &pos)) {
      if (info_r) {
	info_r->base = pos;
	info_r->offset = 0;
	info_r->info.b = info_r->info.i = info_r->info.e = 0;
      }

      if (info_w) {
	info_w->base = pos;
	info_r->offset = 0;
	info_w->info.b = info_w->info.i = info_w->info.e = 0;
      }
    }
  }

  return res;
}

size_t
mb_fread(char *d, size_t n, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info)
    return mb_getmem(d, n, &info->info);
  else
    return fread(d, 1, n, fp);
}

size_t
mb_fwrite(const char *s, size_t n, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, NULL, &info);

  if (info)
    return mb_putmem(s, n, &info->info);
  else
    return fwrite(s, 1, n, fp);
}

size_t
mb_fputs(const char *s, FILE *fp)
{
  mb_fwrite(s, strlen(s), fp);
  return ferror(fp) ? 1 : EOF;
}

int
mb_fgetc(FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info)
    return mb_getc(&info->info);
  else
    return fgetc(fp);
}

mb_wchar_t
mb_fgetwc(FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info)
    return mb_fetch_wchar(&info->info);
  else {
    int c = fgetc(fp);

    return c == EOF ? mb_notchar_eof : c & ~MB_SBC_UNIT_MASK ? MB_WORD_SBC_ENC(MB_CTL_FC, c & MB_SBC_UNIT_MASK) : c;
  }
}

size_t
mb_fgetmbc(char *d, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info) {
    return mb_getmbc(d, &info->info);
  }
  else {
    int c;

    if ((c = fgetc(fp)) == EOF)
      return 0;
    else {
      *d = c;
      return 1;
    }
  }
}

int
mb_ungetc(int c, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info)
    return mb_unfetch_char(c, &info->info);
  else
    return ungetc(c, fp);
}

char *
mb_fgets(char *d, size_t n, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info)
    return mb_getline(d, n, &info->info);
  else
    return fgets(d, n, fp);
}

int
mb_fputc(int c, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, NULL, &info);

  if (info)
    return mb_putc(c, &info->info);
  else
    return fputc(c, fp);
}

mb_wchar_t
mb_fputwc(mb_wchar_t wc, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, NULL, &info);

  if (info) {
    mb_store_wchar(wc, &info->info);
    return wc;
  }
  else {
    if (wc & ~MB_SBC_UNIT_MASK) 
      wc = ((wc - MB_WORD_SBC_ENC(MB_CTL_FC, 0U)) & MB_SBC_UNIT_MASK) | MB_SBC_UNIT;

    return fputc(wc, fp);
  }
}

int
mb_vfprintf(FILE *fp, const char *format, va_list ap)
{
  mb_pos_info_t *info;

  mb_find_info(fp, NULL, &info);

  if (info)
    return mb_vprintf(&info->info, format, ap);
  else
    return vfprintf(fp, format, ap);
}

int
mb_fprintf(FILE *fp, const char *format, ...)
{
  va_list ap;
  int n;

  va_start(ap, format);
  n = mb_vfprintf(fp, format, ap);
  va_end(ap);
  return n;
}

int
mb_fflush(FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, NULL, &info);

  if (info)
    mb_flush(&info->info);

  return fflush(fp);
}

size_t
mb_fread_fromto(char *buf, size_t n, FILE *fp, long *p_from, long to)
{
  long from = *p_from;
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info) {
    int c;
    long origin = info->offset - info->info.e;
    long end = to - from;
    size_t i;

    for (i = 0 ; i < n && info->offset - info->info.e - origin < end ;) {
      c = mb_getc(&info->info);

      if (c == EOF)
	break;

      buf[i++] = c;
    }

    *p_from = from + (info->offset - info->info.e - origin);
    return i;
  }
  else {
    if (n > to - from)
      n = to - from;

    if ((n = fread(buf, 1, n, fp)) > 0)
      *p_from = from + n;

    return n;
  }
}

mb_cs_detector_t *
mb_fbind_cs_detector(mb_cs_detector_t *p, FILE *fp)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info) {
    mb_bind_cs_detector(p, &info->info);
    return p;
  }
  else
    return NULL;
}

mb_cs_detector_t *
mb_falloc_cs_detector(FILE *fp, size_t init, size_t limit)
{
  mb_pos_info_t *info;

  mb_find_info(fp, &info, NULL);

  if (info)
    return mb_alloc_cs_detector(&info->info, init, limit);
  else
    return NULL;
}

int
mb_finfo(FILE *fp, mb_info_t **p_info_r, mb_info_t **p_info_w)
{
  mb_pos_info_t *pos_info_r, *pos_info_w;
  int n = mb_find_info(fp, &pos_info_r, &pos_info_w);

  if (p_info_r)
    *p_info_r = pos_info_r ? &pos_info_r->info : NULL;

  if (p_info_w)
    *p_info_w = pos_info_w ? &pos_info_w->info : NULL;

  return n;
}
