/*
    Tango/Weevil - A WEB Tangler and Weaver
    Copyright (C) 1995 Corey Minyard

    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., 675 Mass Ave, Cambridge, MA 02139, USA.

    Corey Minyard - minyard@metronet.com
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>


#define SKIP_DOCUMENTATION	1
#define OUTPUT_CODE		2

#define GOT_LINE 1
#define END_OF_FILE 0

#define MAX_FILENAME_LENGTH		200

#include "tango.h"

char *
r_strtok(char *data,
	 char *sstr,
	 char **next_data)
{
   char *retval;


   while (   (*data != '\0')
	  && (strchr(sstr, *data) != NULL))
   {
      data++;
   }
   
   retval = data;
   while (   (*data != '\0')
	  && (strchr(sstr, *data) == NULL))
   {
      data++;
   }

   if (retval == data)
   {
      retval = NULL;
   }
   else
   {
      if (*data != '\0')
      {
	 *data = '\0';
	 data++;
      }

      if (next_data != NULL)
      {
	 *next_data = data;
      }
   }

   return(retval);
}

char *
stralloc(char *str,
	 int  length)
{
   char *retval;

   if (length == 0)
   {
      length = strlen(str);
   }

   retval = malloc(length+1);
   if (retval == NULL)
   {
      fprintf(stderr,
	      "Fatal: Unable to allocate memory\n");
      exit(1);
   }

   strncpy(retval, str, length);
   retval[length] = '\0';
   return(retval);
}

void
free_namelist_item(t_lptangodat *lptd,
		   t_namelist   *item)
{
   if (item->name != NULL)
   {
      free(item->name);
   }
   if (item != NULL)
   {
      free(item);
   }
}

t_namelist *
create_namelist_item(t_lptangodat *lptd,
		     char         *str,
		     int          length)
{
   t_namelist *retval;

   retval = malloc(sizeof(*retval));
   if (retval == NULL)
   {
      fprintf(stderr,
	      "Fatal: Unable to allocate memory\n");
      exit(1);
   }

   retval->name = stralloc(str, length);

   return(retval);
}

t_namelist *
find_name_in_list(t_lptangodat *lptd,
		  t_namelist *list,
		  t_namelist *item)
{
   while (list != NULL)
   {
      if (strcmp(list->name, item->name) == 0)
      {
	 break;
      }

      list = list->next;
   }

   return(list);
}

void
list_insert_unique(t_lptangodat *lptd,
		   t_namelist   **list,
		   t_namelist   *item)
{
   if (find_name_in_list(lptd, *list, item) == NULL)
   {
      item->next = *list;
      *list = item;
   }
   else
   {
      free_namelist_item(lptd, item);
   }
}

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

static int
get_input_line(t_lptangodat *lptd)
{
#define FLUSHSIZE 20
   int retval;
   int length;
   char flushbuf[FLUSHSIZE];
   int flushlength;


   (lptd->curr_lineno)++;

   if (fgets(lptd->line, lptd->maxlinesize, lptd->infile) == NULL)
   {
      retval = END_OF_FILE; /* Did not get a line, end of file. */
   }
   else
   {
      retval = GOT_LINE;
      length = strlen(lptd->line);

      /* If the line was too long (it was the max size and the last character
	 was not a newline), print a log and flush to end of line. */
      if (   (length == (lptd->maxlinesize - 1))
	  && (lptd->line[length-1] != '\n'))
      {
	 lptd->line[length-1] = '\n';

	 fprintf(stderr,
		 "Warning, line %d too long, flushing to end of line\n",
		 lptd->curr_lineno);
	 if (fgets(flushbuf, FLUSHSIZE, lptd->infile) == NULL)
	 {
	    flushlength = 0;
	 }
	 else
	 {
	    flushlength = strlen(flushbuf);
	 }
	 while (   (flushlength == (FLUSHSIZE - 1))
		&& (lptd->line[flushlength-1] != '\n'))
	 {
	    if (fgets(flushbuf, FLUSHSIZE, lptd->infile) == NULL)
	    {
	       flushlength = 0;
	    }
	    else
	    {
	       flushlength = strlen(flushbuf);
	    }
	 }
      }
   }

   return(retval);
}

static void
process_cmd(t_lptangodat *lptd)
{
   char *strhold;
   char *cmd;
   char *name;
   int  namelength;


   cmd = r_strtok(&(lptd->line[1]), " \t\n", &strhold);
   if (cmd == NULL)
   {
      fprintf(stderr,
	      "Error on line %d: Missing command\n",
	      lptd->curr_lineno);
      lptd->retcode = 2;
   }
   else if (strcmp(cmd, "code") == 0)
   {
      if (lptd->outstate == OUTPUT_CODE)
      {
	 fprintf(stderr,
		 "Error on line %d: Macro begin when already processing a macro\n",
		 lptd->curr_lineno);
         lptd->retcode = 2;
      }
      else
      {
	 lptd->curr_macro = malloc(sizeof(*(lptd->curr_macro)));
	 if (lptd->curr_macro == NULL)
	 {
	    fprintf(stderr,
		    "Fatal: Unable to allocate memory\n");
	    exit(1);
	 }

	 while ((*strhold == ' ') || (*strhold == '\t') || (*strhold == '\n'))
	 {
	    strhold++;
	 }

	 if (*strhold != '<')
	 {
	    fprintf(stderr,
		    "Error on line %d: Macro name must be surrounded by <>\n",
		    lptd->curr_lineno);
            lptd->retcode = 2;
	 }
	 else
	 {
	    strhold++;
	 }

	 name = r_strtok(strhold, ">", &strhold);
	 lptd->curr_macro->name = stralloc(name, 0);
	 lptd->curr_macro->filename = lptd->curr_filename;
	 lptd->curr_macro->startline = lptd->curr_lineno + 1;
	 lptd->curr_macro->firstline = NULL;
	 lptd->curr_macro->lastline = NULL;
	 lptd->curr_macro->staticdefs = NULL;
	 lptd->curr_macro->globaldefs = NULL;
	 lptd->curr_macro->pounddefs = NULL;
	 lptd->curr_macro->uses = NULL;

	 lptd->outstate = OUTPUT_CODE;
      }
   }
   else if (strcmp(cmd, "file") == 0)
   {
      if (lptd->outstate == OUTPUT_CODE)
      {
	 fprintf(stderr,
		 "Error on line %d: File name change not allowed in code\n",
		 lptd->curr_lineno);
         lptd->retcode = 2;
      }
      else
      {
	 name = r_strtok(strhold, " \t\n", &strhold);
	 if (name == NULL)
	 {
	    fprintf(stderr,
		 "Error on line %d: File name not given with file command\n",
		 lptd->curr_lineno);
            lptd->retcode = 2;
	 }
	 lptd->curr_filename = stralloc(name, 0);
	 namelength = strlen(name);

	 name = r_strtok(strhold, " \t\n", &strhold);
	 if (name == NULL)
	 {
	    lptd->curr_lineno = 0;
	 }
	 else
	 {
	    lptd->curr_lineno = atoi(name) - 1;
	 }
      }
   }
   else if (strcmp(cmd, "line") == 0)
   {
      if (lptd->outstate == OUTPUT_CODE)
      {
	 fprintf(stderr,
		 "Error on line %d: Line number change not allowed in code\n",
		 lptd->curr_lineno);
         lptd->retcode = 2;
      }
      else
      {
	 name = r_strtok(strhold, " \t\n", &strhold);
	 if (name == NULL)
	 {
	    fprintf(stderr,
		    "Error on line %d: Line number not specified in line command\n",
		    lptd->curr_lineno);
            lptd->retcode = 2;
	 }
	 else
	 {
	    lptd->curr_lineno = atoi(name) - 1;
	 }
      }
   }
   else if (   (strcmp(cmd, "includefile") == 0)
            || (strcmp(cmd, "reffile") == 0))
   {
      if (lptd->outstate == OUTPUT_CODE)
      {
	 fprintf(stderr,
		 "Error on line %d: %s not allowed in code\n",
		 lptd->curr_lineno,
		 cmd);
         lptd->retcode = 2;
      }
      /* This is ignored in tangling. */
   }
   else
   {
      fprintf(stderr,
	      "Error on line %d: Invalid command\n",
	      lptd->curr_lineno);
      lptd->retcode = 2;
   }
}

static void
process_codeline(t_lptangodat *lptd)
{
   t_linelist *thisline;

   thisline = malloc(sizeof(*thisline));
   if (thisline == NULL)
   {
      fprintf(stderr,
	      "Fatal: Unable to allocate memory\n");
      exit(1);
   }

   thisline->line = stralloc(lptd->line, 0);
   thisline->next = NULL;
   if (lptd->curr_macro->lastline == NULL)
   {
      lptd->curr_macro->lastline = thisline;
      lptd->curr_macro->firstline = thisline;
   }
   else
   {
      lptd->curr_macro->lastline->next = thisline;
      lptd->curr_macro->lastline = thisline;
   }
}

static void output_macro(t_lptangodat *lptd,
			 t_lpmac      *outmac);

static void
output_line(t_lptangodat *lptd,
	    t_linelist   *outline,
	    char         *filename,
	    int          lineno)
{
   t_lpmac *cm;
   t_lpmac *prev_macro;
   char *cp;
   int currchar;
   char *name;


   currchar = 0;
   cp = outline->line;

   while (cp[currchar] != '\0')
   {
      if (cp[currchar] == '@')
      {
	 if (currchar > 0)
	 {
	    fwrite(cp, 1, currchar, lptd->outfile);
            lptd->scan_input(lptd, cp, currchar, lineno);
	 }
         if (   (lptd->instring)
	     || (lptd->in_comment))
         {
	    fputc('@', lptd->outfile);
            lptd->scan_input(lptd, "@", 1, lineno);
            currchar++;
            cp = &(cp[currchar]);
            currchar = 0;
         }
         else
         {
	    currchar++;
	    if (cp[currchar] == '@')
	    {
	       fputc('@', lptd->outfile);
               lptd->scan_input(lptd, "@", 1, lineno);
               currchar++;
               cp = &(cp[currchar]);
               currchar = 0;
	    }
	    else if (cp[currchar] == '<')
	    {
	       fputc('\n', lptd->outfile);
	       currchar++;
	       name = r_strtok(&(cp[currchar]), ">", &cp);
	       currchar = 0;
	    
	       cm = lptd->macros;
	       while (   (cm != NULL)
		      && (strcmp(cm->name, name) != 0))
	       {
	          cm = cm->next;
	       }

	       if (cm == NULL)
	       {
	          fprintf(stderr,
		          "Error: macro %s used on line %d but not found\n",
		          name,
		            lineno);
                  lptd->retcode = 2;
	       }
	       else
	       {
                  prev_macro = lptd->curr_macro;
                  lptd->curr_macro = cm;
	          output_macro(lptd, cm);
                  lptd->curr_macro = prev_macro;
                  if (lptd->do_linenums)
                  {
                     lptd->output_linenum(lptd, lineno, filename);
                  }
	       }
	    }
	    else
	    {
	          fprintf(stderr,
		          "Error on line %d: Invalid operation \"@%c\"\n",
		          lineno,
		          cp[currchar]);
                  lptd->retcode = 2;
	    }
         }
      }
      else
      {
	 currchar++;
      }
   }

   if (currchar > 0)
   {
      fwrite(cp, 1, currchar, lptd->outfile);
      lptd->scan_input(lptd, cp, currchar, lineno);
   }
}

static void
output_macro(t_lptangodat *lptd,
	     t_lpmac *outmac)
{
   t_linelist *cl;
   int        lineno;


   lineno = outmac->startline;
   cl = outmac->firstline;
   if (lptd->do_linenums)
   {
      lptd->output_linenum(lptd, lineno, outmac->filename);
   }
   while (cl != NULL)
   {
      lptd->curr_line = cl;
      output_line(lptd, cl, outmac->filename, lineno);
      lineno++;
      cl = cl->next;
   }
}

static void
output_macros(t_lptangodat *lptd)
{
   t_lpmac *cm;

   cm = lptd->macros;
   while (   (cm != NULL)
	  && (strcmp(cm->name, lptd->start_macro) != 0))
   {
      cm = cm->next;
   }

   if (cm == NULL)
   {
      fprintf(stderr,
	      "Fatal: Start macro %s not found\n",
	      lptd->start_macro);
      exit(1);
   }

   lptd->curr_macro = cm;
   output_macro(lptd, cm);
}

static char *
get_arg_str(int *argidx,
	    int argc,
	    char *argv[],
	    int start_offset)
{
   char *retval;


   if (argv[*argidx][start_offset] == '\0')
   {
      (*argidx)++;
      if ((*argidx) == argc)
      {
	 retval = NULL;
      }
      else
      {
	 retval = argv[*argidx];
      }
   }
   else
   {
      retval = &(argv[*argidx][start_offset]);
   }

   return(retval);
}

void c_scan_input(t_lptangodat *lptd,
	          char         *line,
	          int          length,
	          int          lineno);

void c_output_linenum(t_lptangodat *lptd,
                      int          lineno,
                      char         *filename);

void init_c_lang(t_lptangodat *lptd);

static struct s_langs
{
   char *lang_name;
   input_scanner scanner;
   linenum_output output_linenum;
   scanner_init  init;
} langs[] =
{
   { "c", &c_scan_input, &c_output_linenum, &init_c_lang }
};

static const num_langs = sizeof(langs) / sizeof(struct s_langs);

static void
process_args(t_lptangodat *lptd,
	     int argc,
	     char *argv[],
	     int *argidx)
{
   char *str;
   int  i;


   *argidx = 1;
   while (argv[*argidx][0] == '-')
   {
      if (strcmp(argv[*argidx], "--") == 0)
      {
	 (*argidx)++;
	 break;
      }
      else if (strncmp(argv[*argidx], "-autoxref", 5) == 0)
      {
	 lptd->do_xref = TRUE;
	 lptd->auto_xref = TRUE;
      }
      else if (strncmp(argv[*argidx], "-xref", 5) == 0)
      {
	 lptd->do_xref = TRUE;
      }
      else if (strncmp(argv[*argidx], "-lang", 5) == 0)
      {
	 if (lptd->scan_input != NULL)
	 {
	    fprintf(stderr,
		    "%s: Error: only 1 -lang is allowed\n",
		    argv[0]);
	    exit(1);
	 }

	 str = get_arg_str(argidx, argc, argv, 5);
	 if (str == NULL)
	 {
	    fprintf(stderr,
		    "%s: Error: -lang specified but no lang given\n",
		    argv[0]);
	    exit(1);
	 }

	 for (i=0; i<num_langs; i++)
	 {
	    if (strcmp(str, langs[i].lang_name) == 0)
	    {
	       lptd->scan_input = langs[i].scanner;
	       lptd->output_linenum = langs[i].output_linenum;
	       langs[i].init(lptd);
	       break;
	    }
	 }

	 if (lptd->scan_input == NULL)
	 {
	    fprintf(stderr,
		    "%s: Error: Invalid lang given: %s\n",
		    argv[0], str);
	    exit(1);
	 }
      }
      else if (strncmp(argv[*argidx], "-nolinenum", 10) == 0)
      {
	 lptd->do_linenums = FALSE;
      }
      else if (strncmp(argv[*argidx], "-xreffile", 9) == 0)
      {
	 lptd->do_xref = TRUE;
	 str = get_arg_str(argidx, argc, argv, 9);
	 if (str == NULL)
	 {
	    fprintf(stderr,
		    "%s: Error: -xreffile specified but no file given\n",
		    argv[0]);
	    exit(1);
	 }

	 lptd->xreffile = fopen(str, "w");
	 if (lptd->xreffile == NULL)
	 {
	    fprintf(stderr,
		    "%s: Error: Could not open cross ref file: %s\n",
		    argv[0],
		    str);
	    exit(1);
	 }
      }
      else if (strncmp(argv[*argidx], "-start_macro", 11) == 0)
      {
	 lptd->start_macro = get_arg_str(argidx, argc, argv, 11);
	 if (lptd->start_macro == NULL)
	 {
	    fprintf(stderr,
		    "Error: -s specified but no start macro given\n");
	    exit(1);
	 }
      }
      else
      {
	 fprintf(stderr,
	         "Error: Invalid option specified: %s\n", argv[*argidx]);
	 exit(1);
      }

      (*argidx)++;
   }
}

static void
print_list(t_lptangodat *lptd,
	   char         *prefix,
	   t_namelist   *names)
{
   while (names != NULL)
   {
      fprintf(lptd->xreffile, "%s %s\n", prefix, names->name);
      names = names->next;
   }
}

static void
output_xref(t_lptangodat *lptd)
{
   t_lpmac *macros;

   fprintf(lptd->xreffile, "f %s\n", lptd->curr_filename);
   macros = lptd->macros;
   while (macros != NULL)
   {
      fprintf(lptd->xreffile, "m %s\n", macros->name);
      print_list(lptd, "d", macros->pounddefs);
      print_list(lptd, "e", macros->globaldefs);
      print_list(lptd, "s", macros->staticdefs);
      print_list(lptd, "u", macros->uses);

      macros = macros->next;
   }
}

int
main(int argc,
     char *argv[])
{
   t_lptangodat *lptd;
   int argidx;
   char *str;
   char fname[MAX_FILENAME_LENGTH+1];
   int  len;
   char *name;
   t_namelist *item;


   lptd = malloc(sizeof(*lptd));
   if (lptd == NULL)
   {
      fprintf(stderr, "Unable to allocate enough memory\n");
      exit(1);
   }

   lptd->scan_input = NULL;

   lptd->maxlinesize = MAXLINESIZE;
   lptd->curr_lineno = 0;
   lptd->macros = NULL;
   lptd->start_macro = "*";
   lptd->outfile = stdout;
   lptd->xreffile = NULL;
   lptd->do_xref = FALSE;
   lptd->auto_xref = FALSE;
   lptd->in_comment = FALSE;
   lptd->do_linenums = TRUE;
   lptd->instring = FALSE;
   lptd->retcode = 0;

   process_args(lptd, argc, argv, &argidx);

   if (lptd->scan_input == NULL)
   {
      fprintf(stderr, "%s: No lang specified\n", argv[0]);
      exit(1);
   }

   if (argc != (argidx + 1))
   {
      fprintf(stderr, "%s: No input file specified\n", argv[0]);
      exit(1);
   }
   else
   {
      lptd->infile = fopen(argv[argidx], "r");
      if (lptd->infile == NULL)
      {
	 fprintf(stderr, "%s: Unable to open file %s\n",
		 argv[0], argv[argidx]);
	 exit(1);
      }
      lptd->curr_filename = argv[argidx];
   }

   if ((lptd->do_xref) && (lptd->xreffile == NULL))
   {
      str = strrchr(argv[argidx], '.');
      if (str != NULL)
      {
	 len = str - argv[argidx];
      }
      else
      {
	 len = strlen(argv[argidx]);
      }

      if ((len+4) >= MAX_FILENAME_LENGTH) /* Add 4 for the .xfr */
      {
	 fprintf(stderr, "filename to long, no xref file generated\n");
      }
      else
      {
	 memcpy(fname, argv[argidx], len);
	 strcpy(&(fname[len]), ".xfr");

	 lptd->xreffile = fopen(fname, "w");
	 if (lptd->xreffile == NULL)
	 {
	    fprintf(stderr,
		    "%s: Error: Could not open cross ref file: %s\n",
		    argv[0],
		    str);
	    exit(1);
	 }
      }
   }

   lptd->outstate = SKIP_DOCUMENTATION;

   while (get_input_line(lptd) == GOT_LINE)
   {
      if (lptd->outstate == OUTPUT_CODE)
      {
	 if (strncmp(lptd->line, "@endcode", 8) == 0)
	 {
	    lptd->curr_macro->next = lptd->macros;
	    lptd->macros = lptd->curr_macro;
	    
	    lptd->outstate = SKIP_DOCUMENTATION;
	 }
         else if (strncmp(lptd->line, "@uses", 5) == 0)
         {
            if (lptd->outstate != OUTPUT_CODE)
            {
	       fprintf(stderr,
		       "Error on line %d: %s not allowed outside of code\n",
		       lptd->curr_lineno,
		       lptd->line);
               lptd->retcode = 2;
	    }
	    else
	    {
	       name = r_strtok(&(lptd->line[5]), " \t\n", NULL);
	       if (name == NULL)
	       {
		  fprintf(stderr,
			  "Error on line %d: name not given with uses command\n",
			  lptd->curr_lineno);
                  lptd->retcode = 2;
	       }
	       else
	       {
		  item = create_namelist_item(lptd,
					      name,
					      0);
		  list_insert_unique(lptd, &(lptd->curr_macro->uses), item);
	       }
	    }

            strcpy(lptd->line, "\n");
	    process_codeline(lptd);
	 }
	 else if (strncmp(lptd->line, "@defines", 8) == 0)
	 {
	    if (lptd->outstate != OUTPUT_CODE)
	    {
	       fprintf(stderr,
		       "Error on line %d: %s not allowed outside of code\n",
		       lptd->curr_lineno,
		       lptd->line);
               lptd->retcode = 2;
	    }
	    else
	    {
	       name = r_strtok(&(lptd->line[8]), " \t\n", NULL);
	       if (name == NULL)
	       {
		  fprintf(stderr,
			  "Error on line %d: name not given with defines command\n",
			  lptd->curr_lineno);
                  lptd->retcode = 2;
	       }
	       else
	       {
		  item = create_namelist_item(lptd,
					      name,
					      0);
		  list_insert_unique(lptd, &(lptd->curr_macro->pounddefs), item);
	       }
	    }

            strcpy(lptd->line, "\n");
	    process_codeline(lptd);
	 }
	 else if (strncmp(lptd->line, "@externdecls", 12) == 0)
	 {
	    if (lptd->outstate != OUTPUT_CODE)
	    {
	       fprintf(stderr,
		       "Error on line %d: %s not allowed outside of code\n",
		       lptd->curr_lineno,
		       lptd->line);
               lptd->retcode = 2;
	    }
	    else
	    {
	       name = r_strtok(&(lptd->line[12]), " \t\n", NULL);
	       if (name == NULL)
	       {
		  fprintf(stderr,
			  "Error on line %d: name not given with externdecls command\n",
			  lptd->curr_lineno);
                  lptd->retcode = 2;
	       }
	       else
	       {
		  item = create_namelist_item(lptd,
					      name,
					      0);
		  list_insert_unique(lptd, &(lptd->curr_macro->globaldefs), item);
	       }
	    }

            strcpy(lptd->line, "\n");
	    process_codeline(lptd);
	 }
	 else if (strncmp(lptd->line, "@staticdecls", 12) == 0)
	 {
	    if (lptd->outstate != OUTPUT_CODE)
	    {
	       fprintf(stderr,
		       "Error on line %d: %s not allowed outside of code\n",
		       lptd->curr_lineno,
		       lptd->line);
               lptd->retcode = 2;
	    }
	    else
	    {
	       name = r_strtok(&(lptd->line[12]), " \t\n", NULL);
	       if (name == NULL)
	       {
		  fprintf(stderr,
			  "Error on line %d: name not given with staticdecls command\n",
			  lptd->curr_lineno);
                  lptd->retcode = 2;
	       }
	       else
	       {
		  item = create_namelist_item(lptd,
					      name,
					      0);
		  list_insert_unique(lptd, &(lptd->curr_macro->staticdefs), item);
	       }
	    }

            strcpy(lptd->line, "\n");
	    process_codeline(lptd);
	 }
	 else
	 {
	    process_codeline(lptd);
	 }
      }
      else if (lptd->line[0] == '@')
      {
	 process_cmd(lptd);
      }
      /* Ignore documentation. */
   }

   if (lptd->outstate == OUTPUT_CODE)
   {
      fprintf(stderr,
	      "Warning: File %s ended while outputting code",
	      lptd->curr_filename);
      lptd->retcode = 2;
      lptd->outstate = SKIP_DOCUMENTATION;
   }

   output_macros(lptd);

   if (lptd->xreffile != NULL)
   {
      output_xref(lptd);
   }

   return(lptd->retcode);
}
