/* This little utility reads barcodes from /dev/cuecat and runs different
   programs according to the type of barcode */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>



/* Definitions */
#define CUECAT_DEV_FILE "/dev/cuecat"
#define DEFAULT_CFG_FILE "~/.cueactrc"
#define MAX_BUF_SIZE 4096
#define MAX_BARCODE_TYPES 128	/* We assume that no more than 128 types of
				   barcodes exist (?) */
#define MAX_BARCODE_LEN 256	/* We assume that barcodes can't be longer than
				   256 bytes (?) */
#define MAX_PRG_ARGS 256	/* We assume that a program can only have a max
				   of 256 arguments (?) */



/* Global variables */
char run_once=0;
char silent=0;
char verbose=0;
char help=0;
char config_filename[FILENAME_MAX]=DEFAULT_CFG_FILE;
char *barcodes2cmds[MAX_BARCODE_TYPES][2];



/* Function prototypes */
char parse_cmdline(int argc,char *argv[]);
char parse_cfg_file(char *config_filename);
void free_barcodes2cmds(void);
void trim_spaces(char *str);
void wait_barcode(int fd,char *cuecat_id,char *barcode_type,char *barcode);
void filename_explode_HOME(char *filename);
void substitute_vars(char *command,
                     char *cuecat_id,char *barcode_type,char *barcode);
void extract_command_args(char *command,char *full_path_command,
                          char *command_args[]);



/* Functions */
int main(int argc,char *argv[])
{
  int fd;
  int invalid_parameter;
  char cuecat_id[19];
  char barcode_type[4];
  char barcode[MAX_BARCODE_LEN];
  char command[4096];
  char full_path_command[FILENAME_MAX];
  char *command_args[MAX_PRG_ARGS];
  int i;

  /* Parse the command line parameters */
  if((invalid_parameter=parse_cmdline(argc,argv)))
  {
    /* Invalid parameter */
    printf("%s : error : invalid or unknow parameter \"%s\"\n",
           argv[0],argv[invalid_parameter]);
    printf("Try %s -h for help\n",argv[0]);

    return(-1);
  }

  /* Does the user want help ? */
  if(help)
  {
    printf("\n");
    printf("%s options :\n",argv[0]);
    printf("\n");
    printf("  -f <filename> : specify alternative configuration file\n");
    printf("                  (default is %s)\n",DEFAULT_CFG_FILE);
    printf("  -h            : this help screen\n");
    printf("  -o            : read and process only one barcode\n");
    printf("  -s            : run silently\n");
    printf("  -v            : be verbose\n");
    printf("\n");

    return(0);
  }

  /* Does the user want us to be both silent and verbose ? */
  if(silent && verbose)
  {
    printf("%s : error : you want me to be silent AND verbose ??\n",argv[0]);

    return(-1);
  }

  /* Read and parse the configuration file */
  if(parse_cfg_file(config_filename))
  {
    if(!silent)
      printf("%s : error reading config file \"%s\"\n",argv[0],
             config_filename);

    return(-1);
  }

  /* Open the cuecat device file */
  /* Open the cfg file */
  if((fd=open(CUECAT_DEV_FILE,O_RDONLY))<0)
  {
    if(!silent)
      printf("%s : error : cannot open %s. Maybe another process uses it ?\n",
             argv[0],CUECAT_DEV_FILE);

    return(-1);
  }

  /* Wait for a valid barcode */
  do
  {
    wait_barcode(fd,cuecat_id,barcode_type,barcode);

    /* Inform the user */
    if(!silent)
      printf("%s : Barcode read\n",argv[0]);
    if(verbose)
    {
      printf("  Cuecat ID    : %s\n",cuecat_id);
      printf("  barcode type : %s\n",barcode_type);
      printf("  barcode      : %s\n",barcode);
      printf("\n");
    }
    
    /* Look for this barcode type in the table */
    i=0;
    while(barcodes2cmds[i][0]!=NULL && strcmp(barcodes2cmds[i][0],barcode_type))
      i++;

    if(barcodes2cmds[i][0]!=NULL)	/* Have we found a matching type ? */
    {
      if(verbose)
        printf("  Found barcode type %s\n\n",barcode_type);

      /* Make sure we have a corresponding command to execute */
      if(barcodes2cmds[i][1]!=NULL)
      {
        /* Interpret and substitute the variables in the command */
        strcpy(command,barcodes2cmds[i][1]);
        substitute_vars(command,cuecat_id,barcode_type,barcode);

        if(verbose)
          printf("  The command with substituted variables is :\n  %s\n\n",
                 command);

        /* extract the command's arguments */
        extract_command_args(command,full_path_command,command_args);

        if(verbose)
        {
          printf("  The command's full path is :\n  %s\n\n",full_path_command);
          printf("  The command's broken down arguments are :\n");
          for(i=0;command_args[i]!=NULL;i++)
            printf("    arg %d : %s\n",i,command_args[i]);
          printf("\n");
        }

        /* Execute the command */
        switch(fork())
        {
        case -1:			/* Fork error */
          if(!silent)
            printf("%s : fork () error : cannot execute command\n",
                   argv[0]);
          break;

        case 0:				/* I'm the child */
          execvp(full_path_command,command_args);
          printf("%s : fork () error : error executing command\n",argv[0]);
          _exit(-1);
        }
      }
      else
        if(verbose)
          printf("  Error : no command associated with barcode type %s\n",
                 barcode_type);
      
    }
      
    
  }
  while(!run_once);

  close(fd);
  free_barcodes2cmds();
  return(0);
}



/* Parse the command line parameters */
char parse_cmdline(int argc,char *argv[])
{
  int i,j;
  char got_dash_f;
  char got_parameter;

  /* Parse all the arguments */
  got_dash_f=0;
  for(i=1;i<argc;i++)
  {
    got_parameter=0;
    j=0;
    if(!got_dash_f)
    {
      while(argv[i][j])
      {
        if(argv[i][j]=='-')
        {
          switch(argv[i][j+1])
          {
          case 0:			/* '-' with no letter means nothing */
          case ' ':			/* Neither does "- " */
            return(i);
            break;
  
          case 'f':			/* New configuration filename follows */
            got_dash_f=1;
            break;
  
          case 'h':			/* Help wanted */
            help=1;
            break;
  
          case 'o':			/* Run once only */
            run_once=1;
            break;
  
          case 's':			/* Silent mode */
            silent=1;
            break;
  
          case 'v':			/* Verbose mode */
            verbose=1;
            break;

          default:			/* We don't know this option */
            return(i);
            break;
          }

          j++;
        }
        else
          if(argv[i][j]!=' ')
            return(i);
  
        j++;
      }
    }
    else				/* The argument is the cfg filename */
    {
      strcpy(config_filename,argv[i]);
      got_dash_f=0;
    }
  }

  if(got_dash_f)
    return(i-1);

  return(0);
}



/* Parse the config file and store the barcode types and their associated
   commands */
char parse_cfg_file(char *config_filename)
{
  int fd;
  char read_buffer[MAX_BUF_SIZE];
  char str_buf[MAX_BUF_SIZE];
  char *ptr;
  int nb_chars_read;
  int parser_state;
  int current_barcode_type;
  int i;

  /* Initialize the barcode type to command table */
  for(i=0;i<MAX_BARCODE_TYPES;i++)
    barcodes2cmds[i][0]=barcodes2cmds[i][1]=NULL;

  /* Explode tildas in the value of HOME in the config filename */
  filename_explode_HOME(config_filename);

  /* Open the cfg file */
  if((fd=open(config_filename,O_RDONLY))<0)
    return(-1);

  /* Read the cfg file */
  *str_buf=0;
  current_barcode_type=0;
  parser_state=0;			/* We look for the start of a barcode
					   type */
  while((nb_chars_read=read(fd,read_buffer,MAX_BUF_SIZE))>0)
  {
    for(i=0;i<nb_chars_read;i++)
    {
      switch(read_buffer[i])
      {
      case ':':
        switch(parser_state)
        {
        case 0:				/* Are we looking for the start of a
					   barcode type ? */
          /* A ':' is invalid in a barcode type */
          free_barcodes2cmds();
          close(fd);
          return(-1);
          break;

        case 1:				/* Are we recording a barcode type ? */
          /* Remove leading and trailing spaces */
          trim_spaces(str_buf);

          /* store the barcode type if it's valid */
          if(current_barcode_type>=MAX_BARCODE_TYPES ||
             strlen(str_buf)!=3 ||	/* A barcode type should be 3 chars */
	     (ptr=(char *)malloc(4))==NULL)
          {
            free_barcodes2cmds();
            close(fd);
            return(-1);
          }

	  strcpy(ptr,str_buf);
          barcodes2cmds[current_barcode_type][0]=ptr;
	  parser_state=2;		/* We now look for the start of the
					   command associated with this type */
          *str_buf=0;
          break;

        case 2:				/* Are we looking for the start of a
					   command ? */
          parser_state=3;		/* We are now recording a command */
        case 3:				/* Are we recording a command ? */
        default:
          /* Do we have room left in the buffer ? */
          if(strlen(str_buf)>=MAX_BUF_SIZE-1)
          {
            free_barcodes2cmds();
            close(fd);
            return(-1);
          }

          /* Store this char */
          str_buf[strlen(str_buf)+1]=0;
          str_buf[strlen(str_buf)]=read_buffer[i];
          break;
        }
        break;

      case '\n':			/* Newling or ... */
      case '\r':			/* ... carriage return ? */
        switch(parser_state)
        {
        case 0:				/* Are we looking for the start of a
					   barcode type ? */
          /* Just ignore it */
          break;

        case 1:				/* Are we recording a barcode type ? */
        case 2:				/* Are we looking for the start of a
					   command ? */
          /* The line is too short */
          free_barcodes2cmds();
          close(fd);
          return(-1);
          break;

        default:
          /* Remove leading and trailing spaces */
          trim_spaces(str_buf);

          /* store the command if it's valid */
          if(current_barcode_type>=MAX_BARCODE_TYPES ||
             !*str_buf ||		/* The command can't be null */
	     (ptr=(char *)malloc(strlen(str_buf)+1))==NULL)
          {
            free_barcodes2cmds();
            close(fd);
            return(-1);
          }

	  strcpy(ptr,str_buf);
          barcodes2cmds[current_barcode_type++][1]=ptr;
	  parser_state=0;		/* We now look for the start of a new
					   barcode type */
          *str_buf=0;
          break;
        }
        break;

      case ' ':				/* SPACE ? */
        if(parser_state==0 || parser_state==2)
          break;			/* Don't record leading spaces */
       
      default:				/* Any other char or SPACEs if already
					   recording */
        if(parser_state==0 || parser_state==2)
          parser_state++;

        /* Do we have room left in the buffer ? */
        if(strlen(str_buf)>=MAX_BUF_SIZE-1)
        {
          free_barcodes2cmds();
          close(fd);
          return(-1);
        }

        /* Store this char */
        str_buf[strlen(str_buf)+1]=0;
        str_buf[strlen(str_buf)]=read_buffer[i];
        break;
      }
    }
  }
 
  /* Error while reading ? */
  if(nb_chars_read<0)
  {
    close(fd);
    return(-1);
  }

  /* Everything is peachy */
  close(fd);
  return(0);
}



/* Free the malloc'ed zones pointed to in the barcodes to commands table */
void free_barcodes2cmds(void)
{
  int i,j;

  for(i=0;i<MAX_BARCODE_TYPES;i++)
    for(j=0;j<1;j++)
      free(barcodes2cmds[i][j]);
}



/* Remove leading and trailing spaces out of a string */
void trim_spaces(char *str)
{
  char *ptr;
  int i;

  /* Do we need to do anything ? */
  if(!*str)
    return;

  /* Remove trailing spaces first */
  ptr=str+strlen(str)-1;
  while(ptr>=str && *ptr==' ')
    ptr--;
  ptr[1]=0;

  /* Remove leading spaces */
  ptr=str;
  while(*ptr && *ptr==' ')
    ptr++;
  if(*ptr && ptr>str)
  {
    for(i=0;ptr[i];i++)
      str[i]=ptr[i];
    str[i]=0;
  }
}



/* Read the cuecat device file, wait for and decode valid barcodes */
void wait_barcode(int fd,char *cuecat_id,char *barcode_type,char *barcode)
{
  char read_buffer[MAX_BUF_SIZE];
  char str_buf[MAX_BUF_SIZE];
  int nb_chars_read;
  int parser_state;
  int i;

  /* Read the device file */
  *str_buf=0;
  parser_state=0;			/* We are reading the header */
  while((nb_chars_read=read(fd,read_buffer,MAX_BUF_SIZE))>0)
  {
    for(i=0;i<nb_chars_read;i++)
    {
      switch(read_buffer[i])
      {
      case ':':
        if(parser_state==0)		/* Are we recording the header ? */
        {
          /* Check the header */
          if(!strcmp(str_buf,"BARCODE"))/* No match ? */
            parser_state=1;		/* We now record the cuecat ID */
          *str_buf=0;			/* Reset the buffer */
        }
        else
        {
          /* Do we have room left in the buffer ? */
          if(strlen(str_buf)>=MAX_BUF_SIZE-1)
          {
            parser_state=0;		/* We are now recording the header */
            *str_buf=0;			/* Reset the buffer */
          }

          /* Store this char */
          str_buf[strlen(str_buf)+1]=0;
          str_buf[strlen(str_buf)]=read_buffer[i];
        }
        break;

      case ',':
        switch(parser_state)
        {
        case 0:				/* Are we recording the header ? */
          *str_buf=0;			/* Reset the buffer */
          break;

        case 1:				/* Are we recording the cuecat ID ? */
          if(strlen(str_buf)==18)	/* Do we have 18 digits ? */
          {
            /* Store the cuecat ID */
            strcpy(cuecat_id,str_buf);
            parser_state=2;		/* We now record the barcode type */
          }
          else
            parser_state=0;		/* We now record the header */

          *str_buf=0;			/* Reset the buffer */
          break;

        case 2:				/* Are we recording the barcode type ?*/
          if(strlen(str_buf)==3)	/* Do we have 3 letters ? */
          {
            /* Store the barcode type */
            strcpy(barcode_type,str_buf);
            parser_state=3;		/* We now record the barcode */
          }
          else
            parser_state=0;		/* We now record the header */

          *str_buf=0;			/* Reset the buffer */
          break;

        case 3:				/* Are we recording the barcode ? */
        default:
          /* Do we have room left in the buffer ? */
          if(strlen(str_buf)>=MAX_BUF_SIZE-1)
          {
            parser_state=0;		/* We are now recording the header */
            *str_buf=0;			/* Reset the buffer */
          }

          /* Store this char */
          str_buf[strlen(str_buf)+1]=0;
          str_buf[strlen(str_buf)]=read_buffer[i];
          break;
        }
        break;

      case '\n':			/* Newling ? */
        if(parser_state!=3 ||		/* Are we recording the barcode ? */
           !*str_buf)			/* Is the barcode null ? */
        {
          parser_state=0;		/* We are now recording the header */
          *str_buf=0;			/* Reset the buffer */
        }
        else
        {
          /* Store the barcode */
          strcpy(barcode,str_buf);
          return;
        }
        break;

      default:				/* Any other character ? */
        /* Do we have room left in the buffer ? */
        if(strlen(str_buf)>=MAX_BUF_SIZE-1)
        {
          parser_state=0;		/* We are now recording the header */
          *str_buf=0;			/* Reset the buffer */
        }

        /* Store this char */
        str_buf[strlen(str_buf)+1]=0;
        str_buf[strlen(str_buf)]=read_buffer[i];
        break;
      }
    }
  } 
}



/* Replace all instances of '~' (tilda) in a filename by the value of the
   HOME environment variable */
void filename_explode_HOME(char *filename)
{
  int i,j;
  char *homedir;

  /* Get the HOME variable */
  homedir=getenv("HOME");

  /* Replace '~'s in the config file name by the HOME */
  for(i=0;i<strlen(filename);i++)
    if(filename[i]=='~')
    {
      for(j=strlen(filename);j>i;j--)
        filename[j+strlen(homedir)-1]=filename[j];
      for(j=0;j<strlen(homedir);j++)
        filename[i+j]=homedir[j];
    }
}



/* Substitute variables with the cuecat ID, the barcode type or the barcode.
   The syntax of the variables in the command is :

[<v>,<a>,<b>] --> will be substituted by the variable <v> from char number <a>
                  with the number <b> of subsequent chars. If <a> is omitted, 
                  it is replaced by 0 (1st char). If <b> is omitted, it is
                  replaced by the length of the remaining string from <a>
<v> can be : i --> the variable is the cuecat ID
             t --> the variable is the barcode type
             b --> the variable is the barcode

examples :

[i,,] will be replaced by the entire cuecat ID
[b,,5] will be replaced by the 5 first chars of the barcode
[b,4,] will be replaced by the all the chars of the barcode, except the 3 firsts

To escape [ (or any character), use '\'.
*/
void substitute_vars(char *command,
                     char *cuecat_id,char *barcode_type,char *barcode)
{
  char v;
  int a,b;
  int i;
  char got_backslash;
  char var_recog_state;
  int start_var;
  int start_v;
  int start_a;
  int start_b;
  int end_var;
  char *ptr;

  do
  {
    var_recog_state=0;			/* We're looking for a '[' */
    got_backslash=0;
    start_var=start_v=start_a=start_b=end_var=0;
 
    /* Look for the next var to substitute */
    i=0;
    do
    {
      switch(command[i])
      {
      case '\\':
        got_backslash=1;
        break;

      case '[':
        if(got_backslash)
        {
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
          break;
        }
        var_recog_state=1;		/* We're now looking for the 1st ','*/
        start_var=i;
        start_v=i+1;
        break;

      case ',':
        if(got_backslash)
        {
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
          break;
        }

        switch(var_recog_state)
        {
        case 1:				/* Are we looking for the 1st ',' ? */
          if(i==start_v)		/* We need v */
          {
            var_recog_state=0;		/* We're now looking for a '[' */
            got_backslash=0;
            start_var=start_v=start_a=start_b=end_var=0;
            break;
          }
          var_recog_state=2;		/* We're now looking for the 2nd ','*/
          start_a=i+1;
          break;

        case 2:				/* Are we looking for the 2nd ',' ? */
          var_recog_state=3;		/* We're now looking for the ']'*/
          start_b=i+1;
          break;

        default:			/* Any other state are invalid */
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
          break;
        }
        break;

      case ']':
        if(got_backslash ||
           var_recog_state!=3)
        {
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
          break;
        }

        end_var=i;
        break;

      case 'i':
      case 't':
      case 'b':
        if(var_recog_state>1 || i!=start_v)
        {
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
          break;
        }
        break;
      default:				/* Any other character ? */
        if(command[i]==' ' && var_recog_state>0)
        {
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
          break;
        }

        if((command[i]<'0' || command[i]>'9') &&
            var_recog_state>0)
        {
          var_recog_state=0;		/* We're now looking for a '[' */
          got_backslash=0;
          start_var=start_v=start_a=start_b=end_var=0;
        }
        break;
      }

      i++;
    }
    while(i<strlen(command) && !end_var);

    /* Have we found a variable ? */
    if(end_var)
    {
      /* Fetch v, a and b */
      v=command[start_v];

      if(command[start_a]!=',')
      {
        command[start_b-1]=0;
        a=atoi(command+start_a);
        command[start_b-1]=',';
      }
      else
        a=0;

      if(command[start_b]!=']')
      {
        command[end_var]=0;
        b=atoi(command+start_b);
        command[end_var]=']';
      }
      else
        b=-1;

      switch(v)      
      {
      case 'i':ptr=cuecat_id;break;
      case 't':ptr=barcode_type;break;
      case 'b':ptr=barcode;break;
      default:ptr=barcode;break;
      }

      /* Make sure a and b aren't out of bound */
      if(a>strlen(ptr))
        a=strlen(ptr);
      if(b==-1 || a+b>strlen(ptr))
        b=strlen(ptr)-a;

      /* Substitute the variable */
      /* Zap the variable */
      for(i=end_var+1;command[i];i++)
        command[i-end_var+start_var-1]=command[i];
      command[i-end_var+start_var-1]=0;

      /* Open up a space for the substitution string */
      for(i=strlen(command);i>=start_var;i--)
        command[i+b]=command[i];

      /* Copy the substitution string in the hole */
      i=0;
      while(i<b)
      {
        command[start_var+i]=ptr[a+i];
        i++;
      }
    }
  }
  while(end_var);

  /* Remove all backslashes */
  i=0;
  while(i<strlen(command))
  {
    if(command[i]=='\\')
      for(a=i;command[a];a++)
        command[a]=command[a+1];
    else
      i++;
  }
  return;
}



/* Extract the command's arguments, the first being toe program name. Also
   extract the command with the full path.
   NOTE : strings between quotes are isolated, thus they can contain spaces. */
void extract_command_args(char *command,char *full_path_command,
                          char *command_args[])
{
  int argno=0;
  char *arg_start=NULL;
  int i=0,j,k;
  int in_isolation=0;

  /* Initialize the table of arguments */
  for(i=0;i<MAX_PRG_ARGS;i++)
    command_args[i]=NULL;

  /* Kill spaces and map the arguments */
  for(i=0;command[i];i++)
    switch(command[i])
    {
    case ' ':
      if(!in_isolation)
      {
        command[i]=0;
        if(i>0 && command[i-1])
          command_args[argno++]=arg_start;
      }
      break;

    case '\'':
    case '\"':
      in_isolation=!in_isolation;
    default:
      if(i==0 || !command[i-1])
        arg_start=command+i;
      break;
    }
  command_args[argno++]=arg_start;

  /* Kill quotes in all the args */
  for(i=0;command_args[i]!=NULL;i++)
  {
    j=0;
    while(j<strlen(command_args[i]))
    {
      if(command_args[i][j]=='\'' || command_args[i][j]=='\"')
        for(k=j;command_args[i][k];k++)
          command_args[i][k]=command_args[i][k+1];
      else
        j++;
    }
  }

  /* Copy the 1st argument in the full path command var */
  strcpy(full_path_command,command_args[0]);

  /* Explode tildas in the value of HOME in the full path command */
  filename_explode_HOME(full_path_command);

  /* Extract the command name out of the first argument */
  i=strlen(command_args[0]);
  while(i>=0 && command_args[0][i]!='/')
    i--;
  if(i>=0)
    command_args[0]+=i+1;
}
