/*
  sga2bed.c

  Convert SGA file to BED format.
  
  # Arguments:
  #   SGA File
  # Options:
  #   Set Path to locate chr_NC_gi file 
  #   (used for NCBI id to chromosome name conversion)
  #   Set Read length  (def=0)
  #   Set Score factor for BED score field (5th field) (def=1) 
  #   Create BED format without annotation track header lines 
  #   Transfer SGA optional field(s) to BED field(s)  
  #   Expand SGA lines into multiple BED lines
  #   Set BED track name 
  #   Set BED track description 
  #   Set BED track color 

  Giovanna Ambrosini, EPFL/ISREC, Giovanna.Ambrosini@epfl.ch

  Copyright (c) 2014 EPFL and Swiss Institute of Bioinformatics.

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/
/*
#define DEBUG 
*/
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <getopt.h>
#include <assert.h>
#include <math.h>
#include <sys/stat.h>
#include <limits.h>
#include "hashtable.h"
#ifdef DEBUG
#include <mcheck.h>
#endif

#include "version.h"

#define BUF_SIZE 8192
#define LINE_SIZE 1024
#define FT_MAX  64
#define SEQ_ID  32
#define CHR_NB 18
#define AC_MAX 18
#define POS_MAX 16
#define CNT_MAX 16
#define EXT_MAX 256
#define FIELD_MAX 64

typedef struct _options_t {
  char *dbPath;
  char *extField;
  int help;
  int debug;
  int db;
  int readLen;
  int scoreFact;
  int normFact;
  int noHdr;
  int expand;
  int extend;
  char *trackName;
  char *trackDesc;
  char *trackColor;
} options_t;

static options_t options;

static hash_table_t *ac_table = NULL;
static hash_table_t *ext_fields = NULL;

int extIdx[10];
int extLen = 0;

int
process_ac()
{
  FILE *input;
  int c;
  char buf[LINE_SIZE];
  char *chrFile;
  char chrom[12];
  int cLen;

  if (options.db) {
      cLen = (int)strlen(options.dbPath) + 12;
      if ((chrFile = (char*)malloc(cLen * sizeof(char))) == NULL) {
        perror("process_ac: malloc");
        exit(1);
      }
      strcpy(chrFile, options.dbPath);
  } else {
      cLen = 21 + 12;
      if ((chrFile = (char*)malloc(cLen * sizeof(char))) == NULL) {
        perror("process_ac: malloc");
        exit(1);
      }
      strcpy(chrFile, "/home/local/db/genome");
  }
  strcat(chrFile, "/chr_NC_gi");

  input = fopen(chrFile, "r");
  if (input == NULL) {
    fprintf(stderr, "Could not open file %s: %s(%d)\n",
            chrFile, strerror(errno), errno);
    return 1;
  }

  do {
      c = fgetc(input);
  } while(c != '\n');

  ac_table = hash_table_new(MODE_COPY);
  while (fgets(buf, LINE_SIZE, input) != NULL) {
    char *s;
    char chr_nb[CHR_NB] = "";
    char ncbi_ac[AC_MAX] = "";
    int i = 0;
    int nb_len = 0;
    int ac_len = 0;
    /*int valid = 1;*/
    s = buf;
    /* Check line */
    /* Get first character: if # skip line */
    if (*s == '#')
      continue; 
    /* Chrom NB */
    while (*s != 0 && !isspace(*s)) {
      if (i >= CHR_NB) {
        fprintf(stderr, "AC too long in %s\n", s);
        fclose(input);
        exit(1);
      }
      chr_nb[i++] = *s++;
    }
    if (i < CHR_NB)
      chr_nb[i] = 0;
    nb_len = i + 1;
    while (isspace(*s))
      s++;
    /* Chromosome NCBI AC */
    i = 0;
    while (*s != 0 && !isspace(*s)) {
      if (i >= AC_MAX) {
        fprintf(stderr, "AC too long \"%s\" \n", s);
        fclose(input);
        exit(1);
      }
      ncbi_ac[i++] = *s++;
    }
    if (i < AC_MAX)
      ncbi_ac[i] = 0;
    ac_len = i + 1;
    /* Check if chrom contains valid numeric characters */
    /*
    for (i = 0; i < strlen(chr_nb); ++i) {
      if (!isdigit(chr_nb[i])) {
        valid = 0;
        break;
      }
    }
    if (valid) {
      strcpy(chrom, "chr");
      strcat(chrom, chr_nb);
    } else {
      strcpy(chrom, chr_nb);
    } */
    strcpy(chrom, "chr");
    strcat(chrom, chr_nb);
    nb_len = (int)strlen(chrom) + 1;
    /* printf("adding key %s (len = %d) value %s (ac) (len = %d) to hash table\n", chrom, nb_len, ncbi_ac, ac_len); */ 
    /* Store both NCBI identifier to chrom number and chrom number to chrom number keys */
    hash_table_add(ac_table, ncbi_ac, (size_t)ac_len, chrom, (size_t)nb_len);
    if (options.debug) {
      char *cn = hash_table_lookup(ac_table, ncbi_ac, (size_t)ac_len);
      fprintf (stderr, " AC Hash table: %s (len = %d) -> %s (len = %d)\n", ncbi_ac, ac_len, cn, nb_len);
    }
    hash_table_add(ac_table, chrom, (size_t)nb_len, chrom, (size_t)nb_len);
    if (options.debug) {
      char *cn = hash_table_lookup(ac_table, chrom, (size_t)nb_len);
      fprintf (stderr, " AC Hash table: %s (len = %d) -> %s (len = %d)\n", chrom, nb_len, cn, nb_len);
    }
  }
  return 0;
}

char** str_split(char* a_str, const char a_delim)
{
    char** result = 0;
    size_t count = 0;
    char* tmp = a_str;
    char* last_comma = 0;
    char delim[2];
    delim[0] = a_delim;
    delim[1] = 0;
    /* Count how many elements will be extracted. */
    while (*tmp) {
        if (a_delim == *tmp) {
            count++;
            last_comma = tmp;
        }
        tmp++;
    }
    /* Add space for trailing token. */
    count += last_comma < (a_str + strlen(a_str) - 1);
    /* Add space for terminating null string so caller
       knows where the list of returned strings ends. */
    count++;
    result = malloc(sizeof(char*) *count);

    if (result) {
        size_t idx  = 0;
        char* token = strtok(a_str, delim);

        while (token) {
            assert(idx < count);
            *(result + idx++) = strdup(token);
            token = strtok(0, delim);
        }
        assert(idx == count - 1);
        *(result + idx) = 0;
    }
    return result;
}

int
process_sga(FILE *input, char *iFile) 
{
  unsigned long start, end;
  unsigned long pos;
  int cnt;
  char *name = NULL;
  int score;
  int value;
  char *ext_str = NULL;
  int first = 1;
  char *s, *res, *buf;
  size_t bLen = LINE_SIZE;

  if (options.debug)
    fprintf(stderr, " Processing SGA file %s\n", iFile);
  if ((s = malloc(bLen * sizeof(char))) == NULL) {
    perror("process_sga: malloc");
    exit(1);
  }
#ifdef DEBUG
  int c = 1; 
#endif
  while ((res = fgets(s, (int) bLen, input)) != NULL) {
    size_t cLen = strlen(s);
    char seq_id[SEQ_ID] = "";
    char ft[FT_MAX] = "";
    char position[POS_MAX] = "";
    char count[CNT_MAX] = "";
    char strand = '\0';
    char ext[EXT_MAX];
    char sga_ext_f[12][FIELD_MAX];
    int first_name_ext = 1;
    int first_val_ext = 1;
    int i = 0;
    int id_len = 0;
    char *cn;
    char** tokens;
    int no_strand = 0;

    memset(ext, 0, (size_t)EXT_MAX);
    while (cLen + 1 == bLen && s[cLen - 1] != '\n') {
      bLen *= 2;
      if ((s = realloc(s, bLen)) == NULL) {
        perror("process_file: realloc");
        exit(1);
      }
      res = fgets(s + cLen, (int) (bLen - cLen), input);
      cLen = strlen(s);
    }
    if (s[cLen - 1] == '\n')
      s[cLen - 1] = 0;

    buf = s;
    /* Get SGA fields */
    /* SEQ ID */
    while (*buf != 0 && !isspace(*buf)) {
      if (i >= SEQ_ID) {
        fprintf(stderr, "Seq ID is too long \"%s\" \n", buf);
        exit(1);
      }
      seq_id[i++] = *buf++;
    }
    while (isspace(*buf))
      buf++;
    /* FEATURE */
    i = 0;
    while (*buf != 0 && !isspace(*buf)) {
      if (i >= FT_MAX) {
        fprintf(stderr, "Feature is too long \"%s\" \n", buf);
        exit(1);
      }
      ft[i++] = *buf++;
    }
    while (isspace(*buf))
      buf++;
    /* Position */
    i = 0;
    while (isdigit(*buf)) {
      if (i >= POS_MAX) {
        fprintf(stderr, "Position is too large \"%s\" \n", buf);
        exit(1);
     }
      position[i++] = *buf++;
    }
    position[i] = 0;
    pos = atoi(position);
    while (isspace(*buf))
      buf++;
    /* Strand */
    strand = *buf++;
    while (isspace(*buf))
      buf++;
    /* Counts */
    i = 0;
    while (isdigit(*buf)) {
      if (i >= CNT_MAX) {
        fprintf(stderr, "Count is too large \"%s\" \n", buf);
        exit(1);
      }
      count[i++] = *buf++;
    }
    count[i] = 0;
    cnt = atoi(count);
    while (isspace(*buf))
      buf++;
    /* SGA Extension */
    i = 0;
    while (*buf != 0) {
      if (i >= EXT_MAX) {
        fprintf(stderr, "Extension is too long \"%s\" \n", buf);
        exit(1);
      }
      ext[i++] = *buf++;
    }
#ifdef DEBUG
    printf(" [%d] seq ID: %s   Feat: %s (%c)  Pos: %d  Cnts: %d Ext: %s\n", c++, seq_id, ft, strand, pos, cnt, ext);
#endif
    //printf(" seq ID: %s   Feat: %s (%c)  Pos: %d  Cnts: %d Ext: %s\n", seq_id, ft, strand, pos, cnt, ext);
    /* Set BED annotation track line */
    if (!options.noHdr) {
      if (first) {
        if (options.trackName == NULL) {
          if ( (options.trackName =  malloc((strlen(ft) + 1) * sizeof(char))) == NULL) {
            perror("process_sga: malloc Feature");
             exit(1);
          }
          strcpy(options.trackName, ft);
        }
        printf("track name=\"%s\" description=\"%s\" visibility=1 color=%s\n", options.trackName, options.trackDesc, options.trackColor);
        first = 0;
      }
    }
    /* Set BED start/end positions and strand */
    if (options.readLen == 0) {
      start = pos - 1;
      end = pos; 
    } else {
      if (strand == '+') {
        start = pos - 1;
        end = start + options.readLen; 
      } else if (strand == '-') {
        start = pos - options.readLen;
        end = pos; 
      } else {
        start = pos - 1  - (unsigned long)((long double)options.readLen/2 + 0.5);
        end = pos - 1 + (unsigned long)((long double)options.readLen/2 + 0.5); 
      }
    } 
    if (strand == '0') {
      no_strand = 1;
      strand ='.';
    }
    /* Set BED name field (4th) with SGA feature */
    if ((name = malloc((strlen(ft) + 1) * sizeof(char))) == NULL) {
      perror("process_sga: malloc Name field");
       exit(1);
    }
    strcpy(name, ft);
    /* Set BED score field */
    score = cnt * options.scoreFact;
    /* Check extension flag - if present only redefine BED fields 4 and/or 5 and/or 7 */
    if (options.extend) { 
      /* Extract SGA extended fields */
      tokens = str_split(ext, '\t');
      if (tokens) {
        for (i = 0; *(tokens + i); i++) {
          strcpy (sga_ext_f[i], *(tokens + i));
          //printf(" SGA ext field %s\n", sga_ext_f[i]);
          free(*(tokens + i));
        }
        free(tokens);
      }
      /* Extract extension keys from ext_fields hash table */
      void **keys = NULL;
      size_t key_num = hash_table_get_keys(ext_fields, &keys);
      //for (i = 0; i < key_num; i++) {
      for (i = key_num - 1; i >= 0; i--) {
        hash_table_element_t *el = keys[i];
        int *sga_f = el->key;
        int *bed_f = NULL;
        bed_f = (int* ) HT_LOOKUP(ext_fields, sga_f);
        //fprintf(stderr, " Ext fields: found SGA field %d -> BED field %d\n", *sga_f, *bed_f);
        if (*bed_f == 4) {
          if (*sga_f > 5) {
            if (sga_ext_f[*sga_f - 6] != NULL) {
              if ((name = realloc(name, (strlen(sga_ext_f[*sga_f - 6]) + 1) * sizeof(char))) == NULL) {
                perror("process_sga: malloc SGA field");
                exit(1);
              }
            }
          } else {
            perror("process_sga: SGA extension field must be > 5");
            exit(1);
          }
          if (first_name_ext) {
            strcpy(name, sga_ext_f[*sga_f - 6]);
            first_name_ext = 0;
          } else {
            strcat(name, " ");
            strcat(name, sga_ext_f[*sga_f - 6]);
          }
        } else if (*bed_f == 5) { /* Set BED score field  */
          if (*sga_f > 5) {
            if (sga_ext_f[*sga_f - 6] != NULL) {
              score = round(atof(sga_ext_f[*sga_f - 6])) * options.scoreFact;
            } 
          } else {
            perror("process_sga: SGA extension field must be > 5");
            exit(1);
          }
        } else if (*bed_f == 7) { /* Set Extention fields, from 7 and on...  */
          if (*sga_f > 5) {
            if (sga_ext_f[*sga_f - 6] != NULL) {
              value = round(atof(sga_ext_f[*sga_f - 6])*options.normFact);
              if (value == INT_MIN || value == INT_MAX)
                value = 0;
              //printf("value = %d  \n", value);
            }
            if (first_val_ext) {
              if ((ext_str = malloc((strlen(sga_ext_f[*sga_f - 6]) + 1) * sizeof(char))) == NULL) {
                perror("process_sga: malloc ext String");
                exit(1);
              }
              sprintf(ext_str, "%d", value);
              first_val_ext = 0;
              //printf("Extension String: %s\n", ext_str);
            } else {
              if ((ext_str = realloc(ext_str, (strlen(sga_ext_f[*sga_f - 6]) + 1) * sizeof(char))) == NULL) {
                perror("process_sga: malloc SGA field");
                exit(1);
              }
              sprintf(ext_str, "%s\t%d", ext_str, value);
            }
          } else {
            perror("process_sga: SGA extension field must be > 5");
            exit(1);
          }
        } else {
          perror("process_sga: Allowed BED extension fields are 4, 5, and 7");
          exit(1);
        }
      }
    }

    id_len = (int)strlen(seq_id) + 1;
    cn = hash_table_lookup(ac_table, seq_id, (size_t)id_len);
    if (cn == NULL)
      continue;
    /* Print out BED line */
    if (options.expand) {
      for (i = 0; i < cnt; i++) {
        if (ext_str == NULL) {
          if (no_strand)
            printf("%s\t%lu\t%lu\t%s\t%d\n", cn, start, end, name, 0);
          else 
            printf("%s\t%lu\t%lu\t%s\t%d\t%c\n", cn, start, end, name, 0, strand);
        } else {
            printf("%s\t%lu\t%lu\t%s\t%d\t%c\t%s\n", cn, start, end, name, 0, strand, ext_str);
        }
      }
    } else {
      if (ext_str == NULL) {
        if (no_strand)
          printf("%s\t%lu\t%lu\t%s\t%d\n", cn, start, end, name, score);
        else
          printf("%s\t%lu\t%lu\t%s\t%d\t%c\n", cn, start, end, name, score, strand);
      } else {
        printf("%s\t%lu\t%lu\t%s\t%d\t%c\t%s\n", cn, start, end, name, score, strand, ext_str);
      }
    }
  } /* End of While */
  if (input != stdin) {
    fclose(input);
  }
  return 0;
}

int
main(int argc, char *argv[])
{
  char** tokens_1;
  char** tokens_2;
#ifdef DEBUG
  mcheck(NULL);
  mtrace();
#endif
  FILE *input;
  int i = 0;
  int j = 0;
  int usage = 0;

  options.scoreFact = 1;
  options.normFact = 1;

  static struct option long_options[] =
    {
      /* These options may or may not set a flag.
         We distinguish them by their indices. */
      {"debug",      no_argument,       0, 'd'},
      {"help",       no_argument,       0, 'h'},
      {"db",         required_argument, 0, 'i'},
      {"readlen",    required_argument, 0, 'l'},
      {"score",      required_argument, 0, 'c'},
      {"norm",       required_argument, 0, 'n'},
      {"extend",     required_argument, 0, 'e'},
      {"nohdr",      no_argument,       0, 'r'},
      {"expand",     no_argument,       0, 'x'},
      {"name",       required_argument, 0,  0 },
      {"desc",       required_argument, 0,  0 },
      {"color",      required_argument, 0,  0 },
      {0, 0, 0, 0}
    };

  int option_index = 0;

  while (1) {
    int c = getopt_long(argc, argv, "dhi:l:c:e:n:rx", long_options, &option_index);
    if (c == -1)
      break;
    switch (c) {
      case 'd':
        options.debug = 1;
        break;
      case 'h':
        options.help = 1;
        break;
      case 'i':
        options.dbPath = optarg;
        options.db = 1;
        break;
      case 'l':
        options.readLen = atoi(optarg);
	break;
      case 'c':
        options.scoreFact = atoi(optarg);
        break;
      case 'e':
        options.extField = optarg;
        options.extend = 1;
        break;
      case 'n':
        options.normFact = atoi(optarg);
        break;
      case 'r':
        options.noHdr = 1;
        break;
      case 'x':
        options.expand = 1;
        break;
      case 0:
        /* This option is to set the annotation track line */
        if (strcmp(long_options[option_index].name, "name") == 0) {
          options.trackName = optarg;
        }
        if (strcmp(long_options[option_index].name, "desc") == 0) {
          options.trackDesc = optarg;
        }
        if (strcmp(long_options[option_index].name, "color") == 0) {
          options.trackColor = optarg;
        }
        break;
      default:
        printf ("?? getopt returned character code 0%o ??\n", c);
        usage = 1;
    }
  }
  /*printf("optind: %d argc: %d\n", optind, argc); */
  if (optind > argc || options.help == 1 || usage) {
    fprintf(stderr, "Usage: %s [options] [<] <SGA file|stdin>\n"
             "      - version %s\n"
             "      where options are:\n"
	     "  \t\t -d|--debug                Produce Debug information\n"
	     "  \t\t -h|--help                 Show this Help text\n"
             "  \t\t -i|--db <path>            Use <path> to locate the chr_NC_gi file\n" 
             "  \t\t                           [default is: $HOME/db/genome]\n"
             "  \t\t -l|--readlen <len>        Set Read length <len>\n"
             "  \t\t                           Unoriented SGA files are extended by +/-<len>/2\n"
             "  \t\t -c|--score <score>        Normalisation factor for BED score field (5th) [score=1]\n"
             "  \t\t -n|--norm  <normf>        Normalisation factor for BED score field (7th) [normf=1]\n"
             "  \t\t -e|--extend f1:F1[,f2:F2] Set SGA optional field(s) f1(,f2,...) to BED field(s) F1,(F2,..)\n"
             "  \t\t                           Fields are specified by column numbers\n"
             "  \t\t                           Accepted BED field values are 4, 5, and 7\n"
             "  \t\t                           Except BED field 5 (score field), BED fields 4 and 7\n"
             "  \t\t                           can be used to set multiple extension values from SGA\n"
             "  \t\t                           Fields 5 and 7 convert into numerical values whereas\n"
             "  \t\t                           BED field 4 takes character strings as they are\n"
             "  \t\t -r|--nohdr                BED format without annotation track header lines\n"
             "  \t\t -x|--expand               Expand SGA lines into multiple BED lines\n"
             "  \t\t --name <name>             Set name for track name field [def. name=SGA-feature]\n"
             "  \t\t --desc <desc>             Set track description field [def. desc=\"ChIP-Seq Custom data\"]\n"
             "  \t\t --color <col>             Define the track color in comma-separated RGB values [def. 100,100,100]\n"
	     "\n\tConvert SGA format into BED format.\n\n",
	     argv[0], VERSION);
      return 1;
  }
  /*printf("argc: %d optind: %d\n", argc, optind);*/
  if (argc > optind) {
      if(!strcmp(argv[optind],"-")) {
          input = stdin;
      } else {
          input = fopen(argv[optind], "r");
          if (NULL == input) {
              fprintf(stderr, "Unable to open '%s': %s(%d)\n",
                  argv[optind], strerror(errno), errno);
             exit(EXIT_FAILURE);
          }
          if (options.debug)
             fprintf(stderr, "Processing file %s\n", argv[optind]);
      }
  } else {
      input = stdin;
  }
  if (options.trackDesc == NULL) {
    if ((options.trackDesc = malloc(32 * sizeof(char))) == NULL) {
      perror("main: malloc trackDesc");
      exit(1);
    }
    strcpy(options.trackDesc, "ChIP-Seq Custom data");
  } 
  if (options.trackColor == NULL) {
    if ((options.trackColor = malloc(12 * sizeof(char))) == NULL) {
      perror("main: malloc trackColor");
      exit(1);
    }
    strcpy(options.trackColor, "100,100,100");
  }
  if (options.extend) {
    int ht_el[2];
    ext_fields = hash_table_new(MODE_COPY);
    if (options.debug)
      fprintf(stderr, " Extend SGA specs: %s\n", options.extField);
    tokens_1 = str_split(options.extField, ',');
    if (tokens_1) {
      for (i = 0; *(tokens_1 + i); i++) {
        tokens_2 = str_split(*(tokens_1 + i), ':'); 
        if (tokens_2) {
          for (j = 0; *(tokens_2 + j); j++) {
            ht_el[j] = atoi(*(tokens_2 + j));
          }
          HT_ADD(ext_fields, &ht_el[0], &ht_el[1]);
          free(*(tokens_2 + j));
        }
        free(*(tokens_1 + i));
      }
      free(tokens_1);
    }
    if (options.debug) {
      void **keys = NULL;
      size_t key_num = hash_table_get_keys(ext_fields, &keys);
      for (i = 0; i < key_num; i++) {
        hash_table_element_t *el = keys[i];
        int *key = el->key;
        int *val = NULL;
        val = (int* ) HT_LOOKUP(ext_fields, key);
        fprintf(stderr, " Ext fields: found (key) SGA field %d -> (val) BED field %d\n", *key, *val);
      }
    }
  }
  if (options.debug) {
    fprintf(stderr, " Arguments:\n");
    fprintf(stderr, " Read length : %d\n", options.readLen);
    fprintf(stderr, " Score factor : %d\n", options.scoreFact);
    fprintf(stderr, " Normalization factor (BED 7th fiels) : %d\n", options.normFact);
    fprintf(stderr, " Extend SGA [on/off]: %d\n", options.extend);
    fprintf(stderr, " Expand SGA [on/off]: %d\n", options.expand);
    fprintf(stderr, " Track name: %s\n", options.trackName);
    fprintf(stderr, " Track description: %s\n", options.trackDesc);
    fprintf(stderr, " Track color: %s\n", options.trackColor);
    fprintf(stderr, "\n");
  }
  if (process_ac() == 0) {
    if (options.debug)
      fprintf(stderr, " HASH Table for chromosome access identifier initialized\n");
  } else {
    return 1;
  }
  if (process_sga(input, argv[optind++]) != 0) {
    return 1;
  }
  return 0;
}
