#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include "tabdelimfile.h"

#ifndef NEWLINE
#define NEWLINE "\n\r"
#endif

#define DELIMITER "\t"
#define DELIMSIZE 1

#define YES 1
#define NO  0

#define INITFIELDS 1024/sizeof(char *)

/* TabDelimFile data structure, members should never be accessed directly */
struct tabdelimfile_t {
   char buff[TABDELIMBUFF];       /* buffer for use while splitting up lines */
   char newline[3];               /* newline sequence, once the first        **
                                  **  instance is found                      */
   char comment[3];               /* comment sequence, if any                */
   unsigned int skipBlankLines:1; /* flag to indicate whether or not to skip **
                                  **  blank lines when getting the next line */
   unsigned int skipComments:1;   /* flag to indicate whether or not to skip **
                                  **  comments                               */
   char *curBuffPos;              /* current position within the buffer      **
                                  **  if NULL or past buffer end, buffer is  **
                                  **  invalid and should be refilled before  **
                                  **  use                                    */
   FILE *file;                    /* pointer to a file open for reading that **
                                  **  is in tab-delimited format             */
   char *line;                    /* pointer to the current line split into  **
                                  **  fields                                 */
   char **fields;                 /* pointers to the beginning of each field **
                                  **  in the current line                    */
   unsigned int curLine;          /* the line number of the current line     **
                                  **  includes comment lines and blank lines **
                                  **  even if they are skipped. Useful for   **
                                  **  more friendly error reporting          */
   unsigned int lineEnd;          /* the index of the end of the current     **
                                  **  line                                   */
   unsigned int lineSize;         /* the current size of the line array      */
   unsigned int numFields;        /* number of fields in the current line    */
   unsigned int fieldsSize;       /* current size of the fields array        */
};

/* getLine- function to get the next line of a tab delimited file
**          ignores comments and comment lines if a delimiter is set.
**          The line is then split up at each occurence of the delimiter
*/
static int getLine(TabDelimFile *tdf);

/* fillBuff- fills the buffer with bytes from the file replacing any
**           newline sequences with null zeroes. If a newline sequence
**           has been previously found, then it is used to determine a newline
**           sequence, otherwise, '\n' (unix/linux), '\r' (apple), or
**           "\r\n" (windows) are all viable candidates. buffer is null
**           terminated. At end of file, buffer is filled with '\n' characters
*/
static int fillBuff(TabDelimFile *tdf);

/* appendToLine- appends the buffer to the current line up to the first
**               occuring null zero. will resize the line array as necessary.
*/
static int appendToLine(TabDelimFile *tdf);

TabDelimFile *initTabDelimFile(FILE *file) {
   TabDelimFile *tdf;

   if(file == NULL) {
      return NULL;
   }
   if((tdf = (TabDelimFile *)malloc(sizeof(TabDelimFile))) == NULL) {
      return NULL;
   }

   tdf->newline[0] = '\0';
   tdf->newline[1] = '\0';
   tdf->newline[2] = '\0';
   tdf->skipBlankLines = NO;
   tdf->skipComments = NO;
   tdf->curBuffPos = NULL;
   tdf->file = file;
   tdf->line = (char *)malloc(sizeof(char)*TABDELIMBUFF);
   tdf->curLine = 0;
   tdf->lineEnd = 0;
   tdf->lineSize = TABDELIMBUFF;
   tdf->fields = (char **)malloc(sizeof(char *)*INITFIELDS);
   tdf->numFields = 0;
   tdf->fieldsSize = INITFIELDS;

   if(tdf->line == NULL || tdf->fields == NULL) {
      free(tdf->line);
      free(tdf->fields);
      free(tdf);
      return NULL;
   }

   return tdf;
}

TabDelimFile *openTabDelimFile(const char *fn) {
   FILE *file;
   TabDelimFile *tdf;

   if(fn == NULL) {
      return NULL;
   }
   if((file = fopen(fn, "r")) == NULL) {
      return NULL;
   }
   if((tdf = initTabDelimFile(file)) == NULL) {
      fclose(file);
      return NULL;
   }

  return tdf;
}

void freeTabDelimFile(TabDelimFile *tdf) {
   if(tdf == NULL) return;

   fclose(tdf->file);
   free(tdf->line);
   free(tdf->fields);
   free(tdf);
}

void tdf_setCommentDelim(TabDelimFile *tdf, const char *str) {
   int len;

   if(tdf == NULL) return;

   if(str == NULL || ((len = strlen(str)) == 0)) {
      tdf->skipComments = NO;
   } else if(len <= 2) {
      tdf->skipComments = YES;
      strcpy(tdf->comment, str);
   }
}

void tdf_skipBlankLines(TabDelimFile *tdf) {
   if(tdf != NULL) tdf->skipBlankLines = YES;
}

void tdf_keepBlankLines(TabDelimFile *tdf) {
   if(tdf != NULL) tdf->skipBlankLines = NO;
}

int tdf_getLine(TabDelimFile *tdf) {
   int ret;

   if(tdf == NULL) return -2;

   if(tdf->skipBlankLines == YES) {
      do {
         if((ret = getLine(tdf)) < 0) {
            return ret;
         }
      } while((tdf->numFields == 1 && strlen(tdf->fields[0]) == 0));
   } else {
      return getLine(tdf);
   }

   return 0;
}

const char *tdf_getField(TabDelimFile *tdf, int idx) {
   if(tdf == NULL) return NULL;
   if(idx >= (long)tdf->numFields || idx < 0) {
      return NULL;
   }

   return tdf->fields[idx];
}

char tdf_getFieldAsChar(TabDelimFile *tdf, int idx, int *err) {
   if(err != NULL) {
      *err = 0;
   }
   if(tdf == NULL || (idx >= (long)tdf->numFields || idx < 0)) {
      if(err != NULL) {
         *err = -1;
      }
      return 0;
   }

   if(strlen(tdf->fields[idx]) != 1) {
      if(err != NULL) {
         *err = -2;
      }
      return 0;
   }

   return *(tdf->fields[idx]);
}

long tdf_getFieldAsInt(TabDelimFile *tdf, int idx, int *err) {
   long tmp;
   char *endptr;
   if(err != NULL) {
      *err = 0;
   }
   if(tdf == NULL || (idx >= (long)tdf->numFields || idx < 0)) {
      if(err != NULL) {
         *err = -1;
      }
      return 0;
   }
   if(*(tdf->fields[idx]) == 0) {
      if(err != NULL) {
         *err = -2;
      }
      return 0;
   }

   errno = 0;
   tmp = strtol(tdf->fields[idx], &endptr, 10);
   if(errno || *endptr != '\0' || isspace((int)*(tdf->fields[idx]))) {
      return -3;
   }

   return tmp;
}

double tdf_getFieldAsFloat(TabDelimFile *tdf, int idx, int *err) {
   double tmp;
   char *endptr;
   if(err != NULL) {
      *err = 0;
   }
   if(tdf == NULL || (idx >= (long)tdf->numFields || idx < 0)) {
      if(err != NULL) {
         *err = -1;
      }
      return 0;
   }
   if(*(tdf->fields[idx]) == 0) {
      if(err != NULL) {
         *err = -2;
      }
      return 0;
   }

   errno = 0;
   tmp = strtod(tdf->fields[idx], &endptr);
   if(errno || *endptr != '\0' || isspace((int)*(tdf->fields[idx]))) {
      return -3;
   }

   return tmp;
}

unsigned int tdf_getCurLine(TabDelimFile *tdf) {
   if(tdf == NULL) return 0;
   return tdf->curLine;
}

unsigned int tdf_getNumFields(TabDelimFile *tdf) {
   if(tdf == NULL) return 0;
   return tdf->numFields;
}

/********* private functions *********/

static int getLine(TabDelimFile *tdf) {
   char *pos;
   char **tmp;
   int len;


   if(tdf->curBuffPos != NULL && *(tdf->curBuffPos) == NEWLINE[0]) {
      return EOF;
   }

   do {
      tdf->numFields = 0;
      tdf->lineEnd = 0;

      tdf->curLine++;
      if(tdf->curBuffPos != NULL && tdf->curBuffPos < tdf->buff+TABDELIMBUFF) {
         if(appendToLine(tdf) < 0) {
            return -3;
         }
      }
      while(tdf->curBuffPos == NULL) {
         tdf->curBuffPos = tdf->buff;
         if(fillBuff(tdf) < 0) {
            return -2;
         }
         if(appendToLine(tdf) < 0) {
            return -3;
         }
      }
      if(tdf->curBuffPos >= tdf->buff+TABDELIMBUFF) {
         tdf->curBuffPos = NULL;
      }

      len = strlen(tdf->line);
      if(tdf->skipComments == YES ) {
         if((pos = strstr(tdf->line, tdf->comment)) != NULL) {
            *pos = '\0';
            pos--;
            while(pos >= tdf->line && isspace((int)*pos)) {
               *pos = '\0';
               pos--;
            }
         }
      }
   } while(len > 0 && strlen(tdf->line) == 0);

   pos = tdf->line;
   tdf->fields[0] = pos;
   tdf->numFields = 1;
   while((pos = strstr(pos, DELIMITER)) != NULL) {
      *pos = '\0';
      pos+= DELIMSIZE;

      if(tdf->numFields >= tdf->fieldsSize) {
         if(tdf->fieldsSize >= tdf->fieldsSize + INITFIELDS) {
            return -3;
         }
         tdf->fieldsSize+= INITFIELDS;
         tmp = (char **)realloc(tdf->fields, tdf->fieldsSize*sizeof(char *));
         if(tmp == NULL) {
            tdf->fieldsSize-= INITFIELDS;
            return -3;
         }
         tdf->fields = tmp;
      }
      tdf->fields[tdf->numFields] = pos;
      tdf->numFields++;
   }

   return 0;
}

static int fillBuff(TabDelimFile *tdf) {
   int len, idx, nll = strlen(tdf->newline);
   char *pos, *maxpos;
   unsigned char tmp;

   len = fread(tdf->buff, sizeof(char), TABDELIMBUFF-1, tdf->file);
   tdf->buff[len] = '\0';
   maxpos = &(tdf->buff[len]);
   if(len > 0) {
      pos = tdf->buff;
      if(tdf->newline[0] == '\0') {
         if((idx = strcspn(pos, NEWLINE)) < len) {
            tdf->newline[0] = pos[idx];
            pos[idx] = '\0';
            if(tdf->newline[0] == '\r') {
               if(idx < len-1) {
                  if(pos[idx+1] == '\n') {
                     tdf->newline[1] = pos[idx+1];
                  }
               } else {
                  if((tmp = (unsigned char)fgetc(tdf->file)) == '\n') {
                     tdf->newline[1] = tmp;
                  } else {
                     ungetc(tmp, tdf->file);
                  }
               }
            }
            nll = strlen(tdf->newline);
            pos+= idx + nll;
         } else {
            pos = maxpos;
         }
      }
      while(pos < maxpos && (pos = strstr(pos, tdf->newline)) != NULL) {
         *pos = '\0';
         pos+= nll;
      }
      if(nll > 1 && tdf->buff[len-1] == tdf->newline[0]) {
         if((tmp = (unsigned char)fgetc(tdf->file)) == tdf->newline[1]) {
            tdf->buff[len-1] = '\0';
         } else {
            ungetc(tmp, tdf->file);
         }
      }
   }
   if(len != TABDELIMBUFF-1) {
      if(ferror(tdf->file)) {
         return -1;
      }
      memset(&(tdf->buff[len+1]), NEWLINE[0], TABDELIMBUFF-(len+1));
   }
   return 0;
}

static int appendToLine(TabDelimFile *tdf) {
   unsigned int len, idx;
   char *tmp;

   len = strlen(tdf->curBuffPos);

   if(tdf->lineSize - tdf->lineEnd <= len) {
      if(tdf->lineSize >= tdf->lineSize+(len<<1)) {
         return -1;
      }
      tdf->lineSize+= len<<1;
      tmp = (char *)realloc(tdf->line, tdf->lineSize);
      if(tmp == NULL) {
         tdf->lineSize-= len<<1;
         return -1;
      }
      if(tmp != tdf->line) {
         for(idx = 0; idx < tdf->numFields; idx++) {
            tdf->fields+= tmp - tdf->line;
         }
         tdf->line = tmp;
      }
   }
   strncpy(&(tdf->line[tdf->lineEnd]), tdf->curBuffPos, len+1);
   tdf->lineEnd+= len;
   if(tdf->curBuffPos + len + 1 >= tdf->buff + TABDELIMBUFF) {
      tdf->curBuffPos = NULL;
   } else {
      tdf->curBuffPos+= len + strlen(tdf->newline);
   }

   return 0;
}
