/******************************************************************************

hmmora (Hidden Markov Model ORAcle) is an oracle for string samples
generated from a mixture of HMMs (see the Development Guide of the
Sixth Annual DIMACS Implementation Challenge: Near Neighbor
Searches). Author: Alfons Juan, May 1998.

Licensing
=========
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 2 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.

Contact infomation
==================
Alfons Juan i Císcar
Institut Tecnològic d'Informàtica
Universitat Politècnica de València
Camí de Vera, s/n
46071 València
Spain
e-mail: ajuan@iti.upv.es

HMM format
==========

HMM <label> <n> <m> <prior_prob>
<i_1> <i_2> ... <i_<n>>
<a_{1,1}>   <a_{1,2}>   ... <a_{1,<n>}>   <a_{1,<n+1>>
<a_{2,1}>   <a_{2,2}>   ... <a_{2,<n>}>   <a_{2,<n+1>>
...
<a_{<n>,1}> <a_{<n>,2}> ... <a_{<n>,<n>}> <a_{<n>,<n+1>>
<b_{1,1}>   <b_{1,2}>   ... <b_{1,<m>}>
<b_{2,1}>   <b_{2,2}>   ... <b_{2,<m>}>
...
<b_{<n>,1}> <b_{<n>,2}> ... <b_{<n>,<m>}>

where
<n>       : number of states (plus a unique "final" state <n+1>)
<m>       : number of symbols
<i_j>     : initial state probability distribution
<a_{i,j}> : transition probability distribution in state i, j=1..<n>
<b_{i,j}> : observation symbol probability distribution in state i, j=1..<m>

Unexpected lines are ignored. A priori probabilities (proportions) can
be omitted for equally probable HMMs. See GetHMM for more details.

Version 1.0
===========

******************************************************************************/

#include <limits.h>
#include <float.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <values.h>
#include "oracle.h"
#include "server.h"

/*****************************************************************************/

#define MLL 16384                   /* Maximum Line Length */
#define IFS " \t\n"                 /* Input Fields Separators */

#define NDFUN 3                     /* number of distance functions */
#define INSDEL 0                    /* Insertion-Deletion distance */
#define EDIT 1                      /* Edit (Levenshtein) distance */
#define WEDIT 2                     /* Weighted Edit distance */

#define DEFPORT 5041                /* default port to use */
#define DEFN 100                    /* default number of data points */
#define DEFQ 10                     /* default number of queries */
#define DEFDFUN INSDEL              /* default distance function */
#define CHROMALPHA "=ABCDEabcde"    /* chromosomes alphabet */
#define DEFALPHA CHROMALPHA         /* default alphabet */

/*****************************************************************************/

char *prog;
int verbose;                         

/*****************************************************************************/

typedef struct
{
  int npoints;          /* number of data points (training set size) */
  int nquery;           /* number of queries (test set size) */
  int **string;         /* training and test sets (npoints+nquery strings) */ 
  int *length;          /* length of each string */
  int *color;           /* color (class) of each string */
  int dfun;             /* distance function */
  int nsymbols;         /* number of symbols */
  char *symbols;        /* symbols (characters) */
  double **weights;     /* Weighted Edit distance only */
} oracle_t;

/*****************************************************************************/

typedef struct
{
  char *label;           /* hmm name */
  int nstates;           /* number of states */
  int nsymbols;          /* number of symbols */
  double prior_prob;     /* a priori probability */
  double *initial_prob;  /* nstates probs */
  double **trans_prob;   /* nstates x (nstates+1) probs */
  double **symbol_prob;  /* nstates x nsymbols probs */
} hmm_t;

/*****************************************************************************/

void nonull(void *ptr)
{
  if (ptr==NULL) {
    fprintf(stderr,"%s: fatal error\n",__FILE__);
    exit(EXIT_FAILURE);
  }
}

/*****************************************************************************/

int InqNumPoints(Oracle *ora)
{
  oracle_t *o=ora;

  return o->npoints;
}

/*****************************************************************************/

int InqNumQuery(Oracle *ora)
{
  oracle_t *o=ora;

  return o->nquery;
}

/*****************************************************************************/

int InqNumFields(Oracle* ora, int p)
{
  oracle_t *o=ora;

  if ((p<0) || (p>=o->npoints+o->nquery)) return 0;
  else return 1+o->length[p];
}

/*****************************************************************************/

char* InqField(Oracle* ora, int p, int f)
{
  oracle_t *o=ora;

  if ((p<0) || (p>=o->npoints+o->nquery)) {
    fprintf(stderr,"%s: undefined point %d\n",prog,p);
    return NULL;
  }
  if ((f<0) || (f>o->length[p])) {
    fprintf(stderr,"%s: undefined field %d\n",prog,f);
    return NULL;
  }
  if (f==0) { /* the first field is the color (Development Guide 1.0-5.2) */
    char aux[16];
    if (p>=o->npoints)
      return strdup("?");
    sprintf(aux,"%d",o->color[p]);
    return strdup(aux);
  }
  { /* fields from 1 up to o->length[p] */
    char aux[2];
    aux[0]=o->symbols[o->string[p][f-1]];
    aux[1]='\0';
    return strdup(aux);
  }
}

/*****************************************************************************/

double InsDel(int *x, int lx, int *y, int ly)
{
  static int maxlx=0; /* maximum length of x */
  static int *g=NULL; /* optimized dynamic programming table (lx ints) */
  int deletion,substitution,insertion,aux,i,j;

  if (lx>maxlx) {maxlx=lx; nonull(g=realloc(g,(maxlx+1)*sizeof(int)));}
  g[0]=0;
  for (i=1;i<=lx;i++) g[i]=i; /*g[i-1]+1;*/
  for (j=1;j<=ly;j++) {
    aux=g[0];
    g[0]++;
    for (i=1;i<=lx;i++) {
      substitution=(x[i-1]==y[j-1])?aux:MAXINT;
      aux=g[i];
      insertion=aux+1;
      deletion=g[i-1]+1;
      if (substitution<=deletion)
	if (substitution<=insertion) g[i]=substitution;
	else g[i]=insertion;
      else
	if (insertion<=deletion) g[i]=insertion;
	else g[i]=deletion;
    }
  }
  return g[lx];
}

/*****************************************************************************/

double Edit(int *x, int lx, int *y, int ly) /* Levenshtein */
{
  static int maxlx=0; /* maximum length of x */
  static int *g=NULL; /* optimized dynamic programming table (lx ints) */
  int deletion,substitution,insertion,aux,i,j;

  if (lx>maxlx) {maxlx=lx; nonull(g=realloc(g,(maxlx+1)*sizeof(int)));}
  g[0]=0;
  for (i=1;i<=lx;i++) g[i]=i; /*g[i-1]+1;*/
  for (j=1;j<=ly;j++) {
    aux=g[0];
    g[0]++;
    for (i=1;i<=lx;i++) {
      substitution=aux+(x[i-1]!=y[j-1]);
      aux=g[i];
      insertion=aux+1;
      deletion=g[i-1]+1;
      if (substitution<=deletion)
	if (substitution<=insertion) g[i]=substitution;
	else g[i]=insertion;
      else
	if (insertion<=deletion) g[i]=insertion;
	else g[i]=deletion;
    }
  }
  return g[lx];
}

/*****************************************************************************/

double WEdit(double **w, int nosym, int *x, int lx, int *y, int ly)
{
  static int maxlx=0; /* maximum length of x */
  static int *g=NULL; /* optimized dynamic programming table (lx ints) */
  int deletion,substitution,insertion,aux,i,j;

  if (lx>maxlx) {maxlx=lx; nonull(g=realloc(g,(maxlx+1)*sizeof(int)));}
  g[0]=0;
  for (i=1;i<=lx;i++) g[i]=i; /*g[i-1]+1;*/
  for (j=1;j<=ly;j++) {
    aux=g[0];
    g[0]++;
    for (i=1;i<=lx;i++) {
      substitution=aux+w[x[i-1]][y[j-1]];
      aux=g[i];
      insertion=aux+w[nosym][y[j-1]];
      deletion=g[i-1]+w[x[i-1]][nosym];
      if (substitution<=deletion)
	if (substitution<=insertion) g[i]=substitution;
	else g[i]=insertion;
      else
	if (insertion<=deletion) g[i]=insertion;
	else g[i]=deletion;
    }
  }
  return g[lx];
}

/*****************************************************************************/

double InqDist(Oracle* ora, int p1, int p2)
{
  oracle_t *o=ora;

  if ((p1<0) || (p1>=o->npoints+o->nquery)) {
    fprintf(stderr,"%s: undefined point %d\n",prog,p1);
    return -1.0;
  }
  if ((p2<0) || (p2>=o->npoints+o->nquery)) {
    fprintf(stderr,"%s: undefined point %d\n",prog,p2);
    return -1.0;
  }
  switch (o->dfun) {
  case INSDEL:
    return InsDel(o->string[p1],o->length[p1],o->string[p2],o->length[p2]);
  case EDIT:
    return Edit(o->string[p1],o->length[p1],o->string[p2],o->length[p2]);
  case WEDIT:
    return WEdit(o->weights,o->nsymbols,o->string[p1],o->length[p1],
		 o->string[p2],o->length[p2]);
  default:
    fprintf(stderr,"%s: unknown distance function %d\n",prog,o->dfun);
    return -1.0;
  }
}

/*****************************************************************************/

double *GetVector(FILE *fp, int d, double *v)
{
  char line[MLL],*np,*ep;
  int i;
  double x;

  while (fgets(line,MLL,fp)!=NULL) {
    for (i=0,strtod(np=line,&ep);i<d && np!=ep;i++,strtod(np=ep,&ep)) ;
    if (i!=d) {
      if (verbose) fprintf(stderr,"ignoring line:%s",line);
      continue;
    }
    for (i=0,x=strtod(np=line,&ep);i<d;i++,x=strtod(np=ep,&ep)) v[i]=x;
    return v;
  }
  return NULL;
}

/*****************************************************************************/

hmm_t *GetHMM(FILE *fp)
{
  char line[MLL],*cp;
  int i,j;
  double *aux;
  hmm_t *mp;

  if (verbose) fprintf(stderr,"Reading HMM...\n");
  nonull(mp=malloc(sizeof(hmm_t)));
  mp->nstates=1;
  mp->nsymbols=2;
  mp->prior_prob=0.0;
  while (1) {
    if (fgets(line,MLL,fp)==NULL) {mp->nstates=0; break;}
    if ((cp=strtok(line,IFS))==NULL) continue;
    if (strcmp(cp,"HMM")!=0) continue;
    if ((cp=strtok(NULL,IFS))==NULL) continue;
    nonull(mp->label=malloc((strlen(cp)+1)*sizeof(char)));
    strcpy(mp->label,cp);
    if ((cp=strtok(NULL,IFS))==NULL) break;
    if ((mp->nstates=atoi(cp))<1) {free(mp->label); continue;}
    if ((cp=strtok(NULL,IFS))==NULL) break;
    if ((mp->nsymbols=atoi(cp))<1) {free(mp->label); continue;}
    if ((cp=strtok(NULL,IFS))!=NULL) mp->prior_prob=atof(cp);
    if (verbose) fprintf(stderr,"label=%s nstates=%d nsymbols=%d\n",
			 mp->label,mp->nstates,mp->nsymbols);
    break;
  }
  if (mp->nstates==0) {
    if (verbose) fprintf(stderr,"no more HMMs.\n");
    return NULL;
  }
  if (verbose) fprintf(stderr,"initial probs...\n");
  nonull(mp->initial_prob=malloc(mp->nstates*sizeof(double)));
  nonull(GetVector(fp,mp->nstates,mp->initial_prob));
  if (verbose) fprintf(stderr,"transition probs...\n");
  nonull(mp->trans_prob=malloc(mp->nstates*sizeof(double *)));
  for (i=0;i<mp->nstates;i++) {
    nonull(mp->trans_prob[i]=malloc((mp->nstates+1)*sizeof(double)));
    nonull(GetVector(fp,mp->nstates+1,mp->trans_prob[i]));
  }
  if (verbose) fprintf(stderr,"symbol probs...\n");
  nonull(mp->symbol_prob=malloc(mp->nstates*sizeof(double *)));
  for (i=0;i<mp->nstates;i++)
    nonull(mp->symbol_prob[i]=malloc(mp->nsymbols*sizeof(double)));
  nonull(aux=malloc(mp->nstates*sizeof(double)));
  for (i=0;i<mp->nsymbols;i++) {
    nonull(GetVector(fp,mp->nstates,aux));
    for (j=0;j<mp->nstates;j++)
      mp->symbol_prob[j][i]=aux[j];
  }
  free(aux);
  if (verbose) fprintf(stderr,"end HMM.\n");
  return mp;
}

/*****************************************************************************/

void FreeHMM(hmm_t *mp)
{
  int i;

  free(mp->label);
  free(mp->initial_prob);
  for (i=0;i<mp->nstates;i++) {
    free(mp->trans_prob[i]);
    free(mp->symbol_prob[i]);
  }
  free(mp);
}

/*****************************************************************************/

void PutHMM(FILE *fp, hmm_t *mp)
{
  int i,j;

  fprintf(fp,"HMM %s %d %d\n",mp->label,mp->nstates,mp->nsymbols);
  fprintf(fp,"InitialP\n");
  for (i=0;i<mp->nstates;i++)
    fprintf(fp," %10f",mp->initial_prob[i]);
  fprintf(fp,"\n");
  fprintf(fp,"TransP\n");
  for (i=0;i<mp->nstates;i++) {
    for (j=0;j<=mp->nstates;j++)
      fprintf(fp," %10f",mp->trans_prob[i][j]);
    fprintf(fp,"\n");
  }
  fprintf(fp,"SymbolP\n");
  for (i=0;i<mp->nsymbols;i++) {
    for (j=0;j<mp->nstates;j++)
      fprintf(fp," %10f",mp->symbol_prob[j][i]);
    fprintf(fp,"\n");
  }
}

/*****************************************************************************/

int SampleProbDist(int nprobs, double *dist)
{
  int i;
  double p=rand()/(double)RAND_MAX,sum;

  for (sum=dist[i=0];i<nprobs && sum<p;sum+=dist[++i]) ;
  if (i==nprobs) return nprobs-1;
  return i;
}

/*****************************************************************************/

int *SampleHMM(hmm_t *mp, int *length)
{
  int *s=NULL,q,i=0;
 
  q=SampleProbDist(mp->nstates,mp->initial_prob);
  do {
    nonull(s=realloc(s,(i+1)*sizeof(int)));
    s[i++]=SampleProbDist(mp->nsymbols,mp->symbol_prob[q]);
    q=SampleProbDist(mp->nstates+1,mp->trans_prob[q]);
  } while (q!=mp->nstates);
  *length=i;
  return s;
}

/*****************************************************************************/

void usage(void)
{
  fprintf(stderr,"\n\
usage: %s\n\n\
  [-help]             this message\n\
  [-trace]            have server dump trace of messages\n\
  [-port] <port>      port number to use (default %d)\n\
  [-seed] <seed>      select a specific seed for any randomization\n\
  [-n] <num>          number of training samples (data strings) (default %d)\n\
  [-q] <num>          number of test samples (query strings) (default %d)\n\
  [-dfun] <name>      distance function to use: InsDel (default) Edit WEdit\n\
  [-wedit] <filename> filename of editing weights\n\
  [-hmm] <filename>   filename of HMMs (default stdin)\n\
  [-alpha] <string>   HMMs alphabet\n",prog,DEFPORT,DEFN,DEFQ);
  exit(EXIT_FAILURE);
}

/*****************************************************************************/

void main(int argc, char *argv[])
{
  char *fweights=NULL;
  double p,sum;
  int port,nmodels,nstrings,i,j;
  hmm_t **hmm=NULL,*mp;
  FILE *fp;
  oracle_t ora;

  prog=argv[0]; 
  verbose=0;
  port=DEFPORT; 
  ora.npoints=DEFN;
  ora.nquery=DEFQ;
  ora.dfun=DEFDFUN;
  ora.symbols=NULL;
  fp=stdin;
  for (i=1;i<argc;i++)
    if (strncmp(argv[i],"-trace",2)==0) verbose=1;
    else if (strncmp(argv[i],"-help",3)==0) usage();
    else if (strncmp(argv[i],"-port",2)==0) port=atoi(argv[++i]);
    else if (strncmp(argv[i],"-seed",2)==0) srand(atoi(argv[++i]));
    else if (strncmp(argv[i],"-n",2)==0) ora.npoints=atoi(argv[++i]);
    else if (strncmp(argv[i],"-q",2)==0) ora.nquery=atoi(argv[++i]);
    else if (strncmp(argv[i],"-dfun",2)==0) { i++;
      if (strncmp(argv[i],"InsDel",1)==0) ora.dfun=INSDEL;
      else if (strncmp(argv[i],"Edit",1)==0) ora.dfun=EDIT;
      else if (strncmp(argv[i],"WEdit",1)==0) ora.dfun=WEDIT;
      else {
	fprintf(stderr,"%s: unknown distance function %s\n",prog,argv[i]);
	exit(EXIT_FAILURE);
      }
    }
    else if (strncmp(argv[i],"-wedit",2)==0) {
      ora.dfun=WEDIT;
      fweights=argv[++i];
    }
    else if (strncmp(argv[i],"-hmm",3)==0) {
      if ((fp=fopen(argv[++i],"r"))==NULL) {
	fprintf(stderr,"%s: couldn't open %s\n",prog,argv[i]);
	exit(EXIT_FAILURE);
      }
    }
    else if (strncmp(argv[i],"-alpha",2)==0) {
      ora.nsymbols=strlen(argv[++i]);
      nonull(ora.symbols=malloc((ora.nsymbols+1)*sizeof(char)));
      strcpy(ora.symbols,argv[i]);
    }
    else {
      fprintf(stderr,"%s: unknown option %s\n",prog,argv[i]);
      usage();
    }
  /* HMMs */
  for (nmodels=0;(mp=GetHMM(fp))!=NULL;hmm[nmodels++]=mp)
    nonull(hmm=realloc(hmm,(nmodels+1)*sizeof(hmm_t *)));
  if (!nmodels) {
    fprintf(stderr,"%s: couldn't read any model\n",prog);
    exit(EXIT_FAILURE);
  }
  /* alphabet */
  if (ora.symbols==NULL) {
    ora.nsymbols=hmm[0]->nsymbols;
    if (strlen(DEFALPHA)<ora.nsymbols) {
      fprintf(stderr,"%s: please choose an alphabet\n",prog);
      exit(EXIT_FAILURE);
    }
    nonull(ora.symbols=malloc((ora.nsymbols+1)*sizeof(char)));
    strncpy(ora.symbols,DEFALPHA,ora.nsymbols);
    ora.symbols[ora.nsymbols]='\0';
  }
  if (verbose) fprintf(stderr,"Using alphabet \"%s\" (%d symbols)\n",
		       ora.symbols,ora.nsymbols);
  /* HMMs checks */
  for (i=0;i<nmodels;i++)
    if (hmm[i]->nsymbols!=ora.nsymbols) {
      fprintf(stderr,"%s: incorrect number of symbols in HMM %s\n",prog,
	      hmm[i]->label);
      exit(EXIT_FAILURE);
    }
  /* Weighted Edit Distance weights */
  if (ora.dfun==WEDIT) {
    FILE *fwp;

    if (verbose) fprintf(stderr,"Distance weights...\n");
    if ((fwp=fopen(fweights,"r"))==NULL) {
      fprintf(stderr,"%s: couldn't open %s\n",prog,fweights);
      exit(EXIT_FAILURE);
    }
    nonull(ora.weights=malloc((ora.nsymbols+1)*sizeof(double *)));
    for (i=0;i<=ora.nsymbols;i++) {
      nonull(ora.weights[i]=malloc((ora.nsymbols+1)*sizeof(double)));
      nonull(GetVector(fwp,ora.nsymbols+1,ora.weights[i]));
    }
    fclose(fwp);
  }

  if (verbose) fprintf(stderr,"Data generation...\n");
  nstrings=ora.npoints+ora.nquery;
  nonull(ora.string=malloc(nstrings*sizeof(int *)));
  nonull(ora.length=malloc(nstrings*sizeof(int)));
  nonull(ora.color=malloc(nstrings*sizeof(int)));
  sum=0.0; for (i=0;i<nmodels;i++) sum+=hmm[i]->prior_prob;
  sum=(1.0-sum)/nmodels; for (i=0;i<nmodels;i++) hmm[i]->prior_prob+=sum;
  for (i=0;i<nstrings;i++) {
    p=rand()/(RAND_MAX+1.0);
    sum=0.0; 
    for (j=0;;j++) {
      sum+=hmm[j]->prior_prob;
      if (sum>p || j==nmodels-1) break;
    }
    ora.color[i]=j;
    ora.string[i]=SampleHMM(hmm[ora.color[i]],ora.length+i);
    if (verbose) {
      int *x=ora.string[i],lx=ora.length[i],j;
      for (j=0;j<lx;j++) fprintf(stderr,"%c",ora.symbols[x[j]]);
      fprintf(stderr," %s\n",hmm[ora.color[i]]->label);
    }
  }
  for (i=0;i<nmodels;i++) FreeHMM(hmm[i]);
  ServerOpen(port,(Oracle *)&ora,!verbose);
  exit(EXIT_SUCCESS);
}

/*****************************************************************************/




