char version[12] = "2015-01-02";

/*  Copyright (C) 2014-15 R. D. Tennent School of Computing,
 *  Queen's University, rdt@cs.queensu.ca
 *
 *  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.
 *
 *  You should have received a copy of the GNU General
 *  Public License along with this program; if not, write to
 *  the Free Software Foundation, Inc., 51 Franklin Street,
 *  Fifth Floor, Boston, MA 02110-1301, USA.
 */

/*  msxlint - detects incorrectly formatted notes lines 
 *            in a MusiXTeX source file.
 *
 *  Usage: msxlint [-v | --version | -h | --help] 
 *         msxlint [-d | --debug] { infile[.tex] } ...
 *
 */

# include <stdlib.h>
# include <string.h>
# include <stdio.h>
# include <getopt.h>
# include <time.h>

# define LINE_LEN 1024

# define bool    _Bool
# define true    1
# define false   0

int debug = 0;
char infilename[LINE_LEN];
char *infilename_n = infilename;

FILE *infile;

char line[LINE_LEN];            /* line of input   */
int lineno;                     /* line number     */
int ninstr;                     /* number of instruments */
int staffs[12]; /* staffs[i] is the number of staffs
                   for instrument i                      */                             
char terminator[8];   /* "\en" or "\enotes" or "" (either)  */
int nerrors;          /* number of error messages */
# define NERRORSMAX  10

void
warning (char msg[])    /* output warning message msg to stderr                 */
{
  fprintf (stderr, "Warning: %s\n", msg);
  return;
}

void
error (char msg[])      /* abort with stderr message msg                        */
{
  fprintf (stderr, "Error: %s\n", msg);
  exit (EXIT_FAILURE);
}

bool
prefix (const char *cs, const char *ct)
/*
   is string cs[] a prefix of ct[]?  
*/ 
{
  char *pcs = (char *) cs;
  char *pct = (char *) ct;
  while (*pcs != '\0')
    {
      if (*pcs != *pct) return false;
      pcs++; pct++;
    }
  return true;
}

bool
suffix (const char *cs, const char *ct)
/* is string cs[] a suffix of ct[]?  */
{
  char *pcs = (char *) cs;
  char *pct = (char *) ct + strlen (ct) - strlen (cs);
  while (*pcs != '\0')
    {
      if (*pct != *pcs)
        return false;
      pcs++;
      pct++;
    }
  return true;
}

size_t
append (char *dst, char **offset, const char *src, size_t n)
/*  Copies src to *offset and updates *offset accordingly (if possible). 
 *  Assumes *offset is dst if offset == NULL.
 *  Execution aborts if **offset != '\0'. The src string must be null-terminated. 
 *  Returns (original offset - dst) + strlen(src);  if >= n, the string was truncated.
 */
{
  const char *s = src;
  char *d;
  char *off_orig = ( offset != NULL ? *offset : dst );
  d = off_orig;
  if (*d != '\0')
  {
    char msg[LINE_LEN + 24] = {'\0'};
    char *msg_n = msg;
    append (msg, &msg_n, "\nNull character expected at offset specified for string ", sizeof (msg));
    append (msg, &msg_n, src, sizeof (msg));
    error (msg);
  }
  n = n - 1 - (off_orig - dst); /* number of available slots (leaving room for \0)  */
  while (n > 0 && *s != '\0')
    {
      *d = *s;                  /* copy non-null character */
      d++; s++; n--;
    }
  *d = '\0';                    /* null-terminate */
  if (offset != NULL)
    *offset = d;                /* update *offset (if possible) */
  while (*s != '\0')            /* traverse rest of src */
    s++;                        
  return (off_orig - dst) + (s - src);  /* length of the untruncated string */
}

void
checkc (char *s, char c)
{ if (*s != c) 
    printf ("Expected %c but found %c in line %d:\n%s\n", c, *s, lineno, line);
}

void
checkn (char *s)
{ if (strpbrk (s, "0123456789") != s) 
    printf ("Expected digit but found %c in line %d:\n%s\n", *s, lineno, line);
}

void
usage (void)
{
  puts ("Usage: msxlint [-v | --version | -h | --help]");
  puts ("       msxlint [-d | --debug] ... { infile[.tex] } ...");
}

void
analyze_notes ( char **ln) {
  int i,j;
  int newlines = 0; 
  char *s; char *t;
  s = strpbrk (*ln+1, "|&\\\n"); /* skip initial command      */
  while (true)
  { /* look for \en */
    t = strstr(s, "\\en");
    if (t != NULL) break;
    /* replace EOL by a blank and append another line of input */
    { t = strchr (s, '\n');
      *t = ' ';
      if (debug > 2) printf ("  Reading additional line of input:\n");
      fgets (t+1, LINE_LEN - (t - line), infile);
      if (debug > 2) printf ("  %s\n", t+1);
      newlines++;
    }
  }
  for (i=1; i <= ninstr; i++)
  { 
    for (j=1; j < staffs[i]; j++)
    { s = strpbrk (s, "|&");
      if (s == NULL || *s == '&' || s > t) 
      {
        printf ("Too few staffs for instrument %d in notes on line %d:\n%s", i, lineno, line);
        nerrors++;
        *ln = t+3;
        return;
      }
      checkc (s, '|'); s++;
    }
    if (i == ninstr) break;
    s = strpbrk (s, "&|");
    if (s == NULL || s > t) 
    {
      printf ("Too few instruments in notes on line %d:\n%s", lineno, line);
      nerrors++;
      *ln = t+3;
      return;
    }
    if (*s == '|') 
    {
      printf ("Too many staffs for instrument %d in notes on line %d:\n%s", i, lineno, line);
      nerrors++;
      *ln = t+3;
      return;
    }
    checkc (s, '&'); s++;
  }
  s = strpbrk (s, "|&");
  if (s != NULL && s < t)
  {
    printf ("Too many fields in notes on line %d:\n%s", lineno, line);
    nerrors++;
    *ln = t+3;
    return;
  }
  s = t; /* \en */
  if (terminator[0] != '\0') 
  { if (!prefix (terminator, s) )
    {
      printf ("Expected %s at end of notes on line %d:\n%s", terminator, lineno, line);
      nerrors++;
      t = strchr(s, '\\'); 
      if (t == NULL) t = *ln + strlen (*ln);
      *ln = t;
      return;
    }
    if ( terminator[3] == '\0' && prefix ("otes", s+3) )
    {
      printf ("Expected %s at end of notes on line %d:\n%s", terminator, lineno, line);
      nerrors++;
      t = strchr(s, '\\'); 
      if (t == NULL) t = *ln + strlen (*ln);
      *ln = t;
      return;
    }
  }
  lineno = lineno + newlines;
  t = strchr(s+1, '\\'); 
  if (t == NULL) t = *ln + strlen (*ln);
  *ln = t;
  return;
}

void
process_command (char **ln)
{ char *s, *t;
  if ( prefix("\\instrumentnumber", *ln) )
  { 
    if (debug > 1) printf ("%s\n", " Processing \\instrumentnumber"); 
    s = strpbrk (*ln, "123456789");
    if ( s == NULL ) error ("\\instrumentnumber command unreadable.");
    ninstr = atoi (s);
    t = strpbrk (*ln+1, "\\");
    if (t == NULL) 
    { *ln = *ln + strlen (*ln); return; }
    *ln = t;
  }

  if ( prefix("\\def\\nbinstrument", *ln) )
  { 
    if (debug > 1) printf ("%s\n", " Processing \\def\\nbinstrument"); 
    s = strpbrk (*ln, "123456789");
    if ( s == NULL ) error ("\\def\\nbinstrument command unreadable.");
    ninstr = atoi (s);
    t = strpbrk (*ln+1, "\\");
    if (t == NULL) 
    { *ln = *ln + strlen (*ln); return; }
    *ln = t;
  }

  else if ( prefix("\\setstaffs", *ln) )
  { 
    int n, p;
    if (debug > 1) printf ("%s\n", " Processing \\setstaffs"); 
    s = strpbrk (*ln, "123456789");
    if ( s == NULL ) error ("\\setstaffs command unreadable.");
    n = (int)(*s) - (int)('0'); /* instrument number  */
    s = strpbrk (s+1, "123456789");
    if ( s == NULL ) error ("\\setstaffs command unreadable.");
    p = (int)(*s) - (int)('0'); /* number of staffs */
    staffs[n] = p;
    t = strpbrk (*ln+1, "\\");
    if (t == NULL) 
    { *ln = *ln + strlen (*ln); return; }
    *ln = t;
  }

  else if ( prefix("\\def\\vnotes#1\\elemskip", *ln) ) 
  { 
    if (debug > 1) printf ("%s\n", " Processing \\def\\vnotes"); 
    s = *ln + 22; /* first parameter */
    ninstr = 0;
    while (true)
    { ninstr++;
      staffs[ninstr] = 1;
      checkc (s, '#'); s++;
      checkn (s); s++; 
      while (*s == '|')
      { staffs[ninstr]++; 
        checkc (s, '|'); s++;
        checkc (s, '#'); s++;
        checkn (s); s++; 
      }
      if (*s != '&') break;
      checkc (s, '&'); s++;
    }
    t = &terminator[0];
    while (*s != '{')
    {  *t = *s; s++; t++; }
    *t = '\0';
    *ln = strchr (*ln, '}') ;
    (*ln)++;
  }

  else if ( prefix("\\TransformNotes", *ln) ) 
  { 
    if (debug > 1) printf ("%s\n", " Processing \\TransformNotes"); 
    s = *ln + 16; /* first parameter */
    ninstr = 0;
    while (true)
    { ninstr++;
      staffs[ninstr] = 1;
      checkc (s, '#'); s++;
      checkn (s); s++; 
      while (*s == '|')
      { staffs[ninstr]++; 
        checkc (s, '|'); s++;
        checkc (s, '#'); s++;
        checkn (s); s++; 
      }
      if (*s != '&') break;
      checkc (s, '&'); s++;
    }
    t = &terminator[0];
    terminator[0] = '\0';
    append ( t, &t, "\\en", sizeof (terminator) );
    do s++; while (*s != '{');
    *ln = strchr (s, '}') ;
    if (*ln == NULL) error ("Can't find '}' after \\TransformNotes.");
    (*ln)++;
  }

  else if ( prefix("\\notes", *ln) || 
            prefix("\\Notes", *ln) ||
            prefix("\\NOtes", *ln) ||
            prefix("\\NOTes", *ln) ||
            prefix("\\NOTEs", *ln) ||
            prefix("\\nnotes", *ln) ||
            prefix("\\vnotes", *ln) ||
            prefix("\\znotes", *ln) 
          )
  { 
    if (debug > 1) printf ("%s\n", " Processing notes"); 
    analyze_notes (ln);
    t = strpbrk (*ln, "\\\n");
    if (t == NULL) 
    { *ln = *ln + strlen (*ln); return; }
    *ln = t;
  }

  else if ( prefix("\\def\\atnextbar", *ln))
  { 
    if (debug > 1) printf ("%s\n", " Processing \\def\\atnextbar"); 
    s = strstr (*ln, "\\znotes");
    if (s != NULL) 
    {
      *ln = s;
      analyze_notes (ln); 
    }
    t = strpbrk (*ln, "\\");
    if (t == NULL) 
    { *ln = *ln + strlen (*ln); return; }
    *ln = t;
  }

  else if ( prefix("\\def", *ln) )
    *ln = *ln + strlen (*ln); 

  else  /* anything else  */
  { 
    if (debug > 1) printf (" Processing "); 
    t = strpbrk (*ln+1, "\\\n");
    if (t == NULL) t = *ln + strlen (*ln);
    if (debug > 1)
    { char *s = *ln; do {putchar (*s); s++;} while (s != t); puts ("");}
    *ln = t;
  }
}

void
process_line ()
{
  char  *ln; 
  ln = &line[0];
  while ( *ln != '\0') 
  {
    while (*ln == ' ') { ln++;}
    if (*ln == '%' || *ln == '\n') return;
    process_command (&ln);
  }     /* *ln == '\0'  */
}

void 
process_score ()
/* process .tex file */
{
  int c; int i;
  lineno = 0;
  ninstr = 1;
  nerrors = 0;
  staffs[0] = 0;
  for (i = 1; i<12; i++) staffs[i] = 1;
  terminator[0] = '\0';  /* either \en or \enotes  */
  c = getc (infile);
  while ( c != EOF )
  {
    ungetc (c, infile);
    fgets(line, LINE_LEN, infile); 
    lineno++;
    if (debug > 0) printf ("Processing line %d.\n", lineno);
    process_line ();
    if (nerrors > NERRORSMAX) {
      puts ("\nToo many errors; processing of this file aborted.");
      return;
    }
    c = getc (infile);
  }     /* c == EOF  */
}     


int 
main (int argc, char *argv[])
{
  int c;
  char today[12];
  time_t mytime; 
# define NOPTS 4
  struct option longopts[NOPTS] =
  {  { "help", 0, NULL, 'h'},
     { "version", 0, NULL, 'v'},
     { "debug", 0, NULL, 'd'},
     { NULL, 0, NULL, 0}
  };
  
  time (&mytime);
  strftime (today, 11, "%Y-%m-%d", localtime (&mytime) );
  printf ("This is msxlint, version %s.\n", version);
  c = getopt_long (argc, argv, "hvd", longopts, NULL);
  while (c != -1)
    {
      switch (c)
        {
        case 'h':
          usage ();
          puts ( "Please report bugs to rdt@cs.queensu.ca." );
          exit (0);
        case 'v':
          exit (0);
        case 'd':
          debug++;
          break;
        case '?':
          exit (EXIT_FAILURE);
        default:
          fprintf (stderr,"Function getopt returned character code 0%o.\n",
                  (unsigned int) c);
          exit (EXIT_FAILURE);
        }
      c = getopt_long (argc, argv, "hvd", longopts, NULL);
    }
  puts ( "Copyright (C) 2014-15  R. D. Tennent" );
  puts ( "School of Computing, Queen's University, rdt@cs.queensu.ca" );
  puts ( "License GNU GPL version 2 or later <http://gnu.org/licences/gpl.html>." );
  puts ( "There is NO WARRANTY, to the extent permitted by law." );

  if (optind == argc)
  {
    infile = stdin;
    printf ("\nProcessing stdin.\n\n");
    process_score ();
  }
  else
  do
  {
    infilename[0] = '\0';
    infilename_n = infilename;
    append (infilename, &infilename_n, argv[optind], sizeof (infilename));
    if (!suffix (".tex", infilename))
      append (infilename, &infilename_n, ".tex", sizeof (infilename));
    infile = fopen (infilename, "r");
    if (infile == NULL)
      {
        fprintf (stderr,"Can't open %s\n", infilename);
        optind++;
        continue;
      }
    printf ("\nProcessing %s.\n\n", infilename);
    process_score ();
    optind++;
  }
  while (optind < argc);

  return 0;
}
