/* FileInput.C
 *
 * FileInput holds a series of routines to fetch tokens or free text from
 * the user LaTeX input.
 *
 * Copyright 1992 Jonathan Monsarrat. Permission given to freely distribute,
 * edit and use as long as this copyright statement remains intact.
 *
 */

#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "Global.h"
#include "Document.h"
#include "Font.h"

static int comma_delimiter_valid = 0;
static int parsing_length = FALSE;
static int parsing_command = FALSE;

static void usage()
{
   cout << "Usage: lametex [-p psfile] [-d psdir] [ -t ] texfile" << endl;
   cout
      << "Use -p psfile to specify the name of the default LameTeX page to use."
      << endl;
   cout << "Use -d psdir to specify an additional directory in which" << endl;
   cout << "  to search for LameTeX page definitions." << endl;
   cout << "Use -t to specify that LameTeX should produce a plain text output"
        << endl;
}

FileInput::FileInput(int argc, char *argv[])
{
   filename[0] = '\0';
   pspage[0] = '\0';
   current_pspage[0] = '\0';
   blankline_area = 1;  // Suppress any new paragraphs
   newline_in_this_blankline_area = 0;
   vspace_in_this_blankline_area = 0.0;
   readjust_vspace = 0.0;
   plain_text_output = 0;

   // Make an array of directories to look in for LameTeX page descriptions
   num_pagedirs = 0;
   add_pagedir("./");      // current directory
   add_pagedir(PAGEDIR);   // compiled-in directory from Makefile

   char pagedir_names[MAXSTRING];   // Read in environment variable.
   char *environment_variable;
   if((environment_variable = getenv("LAMETEX_PS_PATH")) != NULL)
      strcpy(pagedir_names, environment_variable);

   // Get all the pagedir paths from the environment variable.
   char *start = pagedir_names;
   char *p = strstr(start,":");
   while(p) {
      (*p) = '\0';
      add_pagedir(start);
      start = p+1;
      p = strstr(start,":");
   }
   add_pagedir(start);

   for(int arg=1; arg<argc; arg++) {
      if(argv[arg][0] == '-') {
	    switch (argv[arg][1]) {
	     case 'd':
	     case 'D':
	       add_pagedir(argv[++arg]);
	     case 'p':
	     case 'P':
	       strcpy(pspage,argv[++arg]);    // -p gives default pspage
	       break;
             case 't':
             case 'T':
               plain_text_output = 1;
	       break;
	     default:
	       usage ();
	    }
      }
      else
	 strcpy(filename,argv[arg]);
   }

   if(!filename[0]) {
      cerr << "No files given to process." << endl;
      exit(0);
   }

   filenum = 0;

   if(!pspage[0])       // The default page template
      strcpy(pspage,"page_latex.ps");

   /* Parse input file name to get output file name */
   strcpy(outfileroot,filename);
   p = strstr(outfileroot,".");
   if(p)
      (*p)='\0';
   p = strrchr(outfileroot,'/');
   if(p)
      sprintf(outfilename,"%s.PS",p+1);
   else
      sprintf(outfilename,"%s.PS",outfileroot);

   /* Open files for reading and writing */
   file[0] = new TextFile(filename);
   cerr << "Opening " << outfilename << " for temporary output..." << endl;
   outfile.open(outfilename);
}

// Gets a new token from a list of files. If impossible, marks token invalid.
void FileInput::get_token(Token &token)
{
   while(!token.isvalid()) {
      if(!file[filenum]->isvalid()) {
	 delete file[filenum];
	 if(filenum == 0)       /* Done processing the original file */
	    return;
	 filenum--;             /* Pop up to parent file */
      }
      file[filenum]->get_token(token);
   }
}

// Include a file at this point in the flow
void FileInput::include_file(char *filename)
{
   /* Has this include file been opened before? */
   for(int x=0; x <= filenum; x++)
      if(file[filenum]->match(filename)) {
	 char message[MAXSTRING];
	 sprintf(message, "Circular include loop while including file %s",
		 filename);
      fatal_error(message);
   }

   /* Are there too many files currently being processed? */
   if(filenum >= MAXFILES-1) {
	 char message[MAXSTRING];
	 sprintf(message, "Too much include file nesting while including %s",
		 filename);
      fatal_error(message);
   }

   file[++filenum] = new TextFile(filename);
}

// Prints an error message giving the current file and linenumber, and exits
void FileInput::fatal_error(char *errormsg)
{
   file[filenum]->fatal_error(errormsg);
}

// Prints a warning message giving the current file and linenumber
void FileInput::warning(char *errormsg)
{
   file[filenum]->warning(errormsg);
}

void FileInput::comma_delimiter(int value)
{
   comma_delimiter_valid = value;
}

void FileInput::set_parsing_length(int value)
{
   parsing_length = value;
}

void FileInput::add_pagedir(char *dirname)
{
   pagedir[num_pagedirs] = new char [ strlen(dirname) + 1 ];
   strcpy(pagedir[num_pagedirs], dirname);

  // Take off a trailing '/' if needed
   int length = strlen(pagedir[num_pagedirs]) -1;
   if(pagedir[num_pagedirs][length] == '/')
      pagedir[num_pagedirs][length] = '\0';

   num_pagedirs++;
}

void FileInput::use_pspage(char *psname)
{
   strcpy(pspage, psname);
}

// Force any pending vertical space or newlines to be printed
void FileInput::force_space()
{
   float parindent;
   if(Global::files->newline_in_this_blankline_area > 0) {
      // If this is a new section, don't indent the first line
      if(Global::stack->get(Environment::PDocument,
			    Document::JustDidSection,"")) {
	 parindent = Global::stack->get(Environment::PLength,
					Length::Parameter, "\\parindent");
	 Global::stack->set(Environment::PLength, Length::Parameter, 0.0,
			    "\\parindent");
      }

      if(Global::files->vspace_in_this_blankline_area > 0.0)
	 Global::files->outfile << endl << "/vspace "
	    << Global::files->vspace_in_this_blankline_area
	    << " def NEWPARA" << endl;
      else
	 Global::files->outfile << endl << "NEWPARA" << endl; 
	    

      if(Global::stack->get(Environment::PDocument,
			    Document::JustDidSection,""))
	 Global::stack->set(Environment::PLength, Length::Parameter,
			    parindent, "\\parindent");
      
      Global::files->readjust_vspace = 0.0;
   }
   Global::files->blankline_area = 0;
   Global::files->newline_in_this_blankline_area = 0; 
   Global::files->vspace_in_this_blankline_area = 0.0;
}

// Check to see if a page has been started, and if not, start one.
void FileInput::force_start_page()
{
   // Load new page description (if one has been defined)
   include_file_ps(pspage, TRUE); 
   
   // Start new page?
   if(!Stack::get(Environment::PDocument, Document::StartPage, "")) {
      outfile << endl;
      outfile << "STARTPAGE" << endl;
      Stack::set(Environment::PDocument, Document::StartPage, 1.0, "");
      blankline_area = 1;  // Suppress any new paragraphs
      newline_in_this_blankline_area = 0;
      vspace_in_this_blankline_area = 0.0;
      Global::files->readjust_vspace = 0.0;
   } else
      force_space();

   // Force a pending Font command to be executed, if there is one.
   Stack::set(Environment::PFont, Font::Pending, 0.0, "");
}

/* Includes a postscript file in the current output stream. Handles
 * page definitions properly if it is being asked to load a postscript
 * file that is a page definition.
 */
void FileInput::include_file_ps(char *filename, int page_definition)
{
   if(!filename[0] || plain_text_output)
      return;

   if(page_definition)
      if(strcmp(filename, Global::files->current_pspage)==0) {
	 pspage[0] = '\0';
	 return;
      }
      else
	 strcpy(current_pspage, pspage);

   // First, end the current "formatdict" dictionary on top of the stack
   outfile << endl << "end" << endl;


   // Convert the given filename into a full path filename
   char full_filename[MAXSTRING];   // Get the full pagename path.
   class stat fileinfo;
   int x;
   
   if(strstr(filename,"/")) {    //  Does this have any directories specified?
      if(full_filename[0]=='/')
	 strcpy(full_filename, filename);
      else
	 sprintf(full_filename, "./%s", filename);
   } else {   // Look for the file in the all given PostScript directories
      for(x=0; x < num_pagedirs; x++) {
         sprintf(full_filename, "%s/%s", pagedir[x], filename);
         if(stat(full_filename,&fileinfo)==0)     // Does this file exist?
   	    break;
      }
      
      if(x >= num_pagedirs) {  // Did not find postscript file
	 char message[MAXSTRING];
	 sprintf(message, "Cannot find PostScript file %s using path",
		 filename);
	 fatal_error(message);
      }
   }
   
   // Open the file for reading
   cerr << "Including PostScript file " << full_filename << endl;
   ifstream psfile(full_filename);
   if(!psfile) {           // Open file failed?
      cerr << "Unable to open postscript file " << full_filename << endl;
      exit (-1);
   }
   
   // Include the PostScript file in the current output
   char psline[MAXSTRING];
   
   psfile.getline(psline, MAXSTRING, '\n');
   while(!psfile.eof() && !psfile.fail()) {
      outfile << psline << endl;
      psfile.getline(psline, MAXSTRING, '\n');
   }
   
   if(page_definition)
      pspage[0] = '\0';

   // Now, start the current "formatdict" dictionary again
   outfile << "formatdict begin" << endl;
}

void FileInput::got_whitespace()
{
   file[filenum]->got_whitespace();
}

int FileInput::whitespace_next()
{
   return file[filenum]->whitespace_next();
}

int FileInput::whitespace_prev()
{
   return file[filenum]->whitespace_prev();
}

TextFile::TextFile(char *name)
{
   if(!name) {
      valid = FALSE;
      return;
   }

   char *p = strstr(name,".");
   if(!p)
      sprintf(filename,"%s.tex",name);
   else
      strcpy(filename,name);
   current_file.open(filename);
   if(!current_file) {           // Open file failed?
      cerr << "Unable to open LaTeX file " << filename << endl;
      exit (-1);
   }

   cerr << "Processing " << filename << "..." << endl;

   linenum=1;
   token_on_this_line = FALSE;
   just_got_a_newline = FALSE;
   just_got_whitespace = TRUE;
   previous_got_whitespace = TRUE;
   parsing_command = FALSE;
   valid = TRUE;
}

TextFile::~TextFile()
{
   current_file.close();
}

/* Gets a new token from a file.
 *    If impossible, leaves token marked "invalid".
 */
void TextFile::get_token(Token& token)
{
   char ch;

   // We want to set these two flags for every newline, but only
   // after the token flagged by the newline has been processed!

   if(just_got_a_newline) {
      Stack::set(Environment::PDocument, Document::Comment, 0.0, "");
      linenum++;
      just_got_a_newline = FALSE;
   }

   if(!isvalid())
      return;

   /* If we're in a postscript environment, dump postscript 'til it closes */
   if(Global::stack->get(Environment::PDocument, Document::PostScript, "")) {
      Global::files->force_start_page();  // Start a new page if not started.
      char commentline[MAXSTRING];
      char *end;
      int x, stop, comments;
      comments=0;
      do {
	 for(x=0, stop=FALSE; x < MAXSTRING && !stop; x++) {
	    current_file.get(ch);
	    switch(ch) {
	    case '\n':
	       comments=0;
	       just_got_a_newline = TRUE;
	       linenum++;
	       commentline[x] = '\0';
	       stop = TRUE;
	       break;
	    case ' ':
	       just_got_a_newline = FALSE;
	       commentline[x] = '\0';
	       stop = TRUE;
	       break;
	    default:
	       just_got_a_newline = FALSE;
	       commentline[x] = ch;
	       break;
	    }
	 }
         
	 end = strstr(commentline,"\\end{postscript}");
	 if(end) {
	    (*end) = '\0';
	 }
	 if(commentline[0] == '%' && !comments) {
	    Global::files->outfile << &commentline[1];  // Skip initial '%'
	    comments++;
	 }
	 else
	    Global::files->outfile << commentline;
	 Global::files->outfile << (char) ch;
      }
      while(!end && !current_file.eof() && !current_file.fail());
      Stack::pop(0, Document::End, 0.0, "\\postscript");
   }

   /* If we're in an ignore environment, do nothing 'til it closes */
   if(Global::stack->get(Environment::PDocument, Document::Ignore, "")) {
      char commentline[MAXSTRING];
      char *end;
      int x, stop, comments;
      comments=0;
      do {
	 for(x=0, stop=FALSE; x < MAXSTRING && !stop; x++) {
	    current_file.get(ch);
	    switch(ch) {
	    case '\n':
	       comments=0;
	       just_got_a_newline = TRUE;
	       linenum++;
	       commentline[x] = '\0';
	       stop = TRUE;
	       break;
	    case ' ':
	       just_got_a_newline = FALSE;
	       commentline[x] = '\0';
	       stop = TRUE;
	       break;
	    default:
	       just_got_a_newline = FALSE;
	       commentline[x] = ch;
	       break;
	    }
	 }
         
	 end = strstr(commentline,"\\end{ignore}");
      }
      while(!end && !current_file.eof() && !current_file.fail());
      Stack::pop(0, Document::End, 0.0, "\\ignore");
   }

   int pos = 0;
   int token_started = FALSE;
   int in_a_number = FALSE;
   previous_got_whitespace = just_got_whitespace;

   // Loop through characters in the file unless some file error occurs.
   for(current_file.get(ch); ch && !current_file.eof() && !current_file.fail();
       current_file.get(ch)){
      if(just_got_a_newline) {
	 Stack::set(Environment::PDocument, Document::Comment, 0.0, "");
	 linenum++;
	 just_got_a_newline = FALSE;
      }

      switch(ch) {
      case '\\':
      case '{':
      case '}':
      case '[':
      case ']':
	 break;
      default:
	 if(!parsing_command && ch != '\\')
	    just_got_whitespace = isspace(ch);
	 break;
      }
      switch(ch) {                         // What is the character?
      case '%':                            // The special comment character
	 if(token_started) {         // If currently inside a token, it's
	    current_file.putback(ch);
	    token_text[pos++] = '\0'; // interpreted as an end-token
	    token.make_text(token_text);   // Successfully got a token.
	    return;
	 } else
	    Stack::set(Environment::PDocument, Document::Comment, 1.0, "");
	 break;
      case '\n':
	 just_got_a_newline = TRUE;
         // End a \STEALTH with a newline
	 if(Stack::get(Environment::PDocument, Document::Stealth, "")==2.0)
	    Stack::set(Environment::PDocument, Document::Stealth, 0.0, "");

	 if(!token_on_this_line) {   // Is this text line entirely whitespace
	    token_text[0] = '\0';    // If so, return the blank line token "".
	    token.make_text(token_text);
	    return;
	 }
	 token_on_this_line = FALSE;
      case ' ':
      case '\t':
	 if(token_started) {               // Marks back or front of token?
	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
	    return;
	 }
         parsing_command = FALSE;
	 break;
      case '{':
      case '[':
      case '}':
      case ']':
	 token_on_this_line = TRUE;
	 if(token_started)                 // Marks back or front of token?
            if(pos==1 && token_text[0] == '\\') // Is the token "\{" or "\{" ?
               token_text[pos++] = ch;
            else { // We must do look-ahead to set just_got_whitespace.
                   // What is the first character after the '}' character(s)?
               current_file.putback(ch);
            }
	 else {
	    token_text[pos++] = ch;
            if(ch == '}' && !parsing_command) {
               for(int x=0; ch == '}'; x++)
                   current_file.get(ch);
               just_got_whitespace = isspace(ch);
               current_file.putback(ch);

               for(int y=0; y < x-1; y++)
                  current_file.putback('}');
            }
         }
         parsing_command = FALSE;
	 token_text[pos++] = '\0';
	 token.make_text(token_text);      // Successfully got a token.
	 return;
      case '\\':
	 parsing_command = TRUE;
         if(token_started) {
            current_file.putback(ch);
  	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
	    return;
         }
         token_started = TRUE;
         token_text[pos++] = ch;
         current_file.get(ch);   // Look for special 2 character commands.
         switch(ch) {
         case '\\':
         case '&':
         case '%':
         case '#':
         case '{':
         case '}':
         case '_':
            token_text[pos++] = ch;
  	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
            current_file.get(ch);
	    just_got_whitespace = isspace(ch);
            current_file.putback(ch);
	    return;
         default:
            current_file.putback(ch);
            break;
         }
         break;
      case ',':
         if(comma_delimiter_valid && token_started) {
	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
	    return;
	 }
      case '0': case '1': case '2': case '3': case '4': case '5': case '6':
      case '7': case '8': case '9': case '.': case '-':
         if(parsing_length && token_started && !in_a_number) {
	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
	    current_file.putback(ch);
	    return;
         }
         in_a_number = TRUE;
	 token_on_this_line = TRUE;
	 if(!token_started)
	    token_started = TRUE;
	 if(pos < MAXSTRING - 1)
	    token_text[pos++] = ch;
	 else {
	    cerr << "File contains words that are longer than "
		 << MAXSTRING << " characters!" << endl;
	    exit(-1);
	 }
	 break;
     // These are characters we want to skip, but they can define
     // the ends of tokens.
	case '$': case '#': case '~': case '&': case '^':

         if(token_started) {
  	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
	    parsing_command = FALSE;
	    return;
         }
         break;
      case ':': case ';': case '?': case '!': case '`': case '_':
      case '\'': case '(': case ')': case '/': case '*': case '@':
      case '+': case '=': case '|': case '<': case '>': case '"':
	 parsing_command = FALSE;
      case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G':
      case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N':
      case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U':
      case 'V': case 'W': case 'X': case 'Y': case 'Z':
      case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g':
      case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n':
      case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u':
      case 'v': case 'w': case 'x': case 'y': case 'z':
         if(parsing_length && token_started && in_a_number) {
	    token_text[pos++] = '\0';
	    token.make_text(token_text);   // Successfully got a token.
	    current_file.putback(ch);
	    return;
         }
	 token_on_this_line = TRUE;
	 if(!token_started)
	    token_started = TRUE;
	 if(pos < MAXSTRING - 1)
	    token_text[pos++] = ch;
	 else {
	    cerr << "File contains words that are longer than "
		 << MAXSTRING << " characters!" << endl;
	    exit(-1);
	 }
	 break;
      default:
	 cerr << "File contains illegal character " << (int) ch << endl;
	 exit(-1);
	 break;
      }
   }
   valid = FALSE;                          // Reached END OF FILE
   if(token_started) {
      token_text[pos++] = '\0';
      token.make_text(token_text);   // Successfully got a token.
   }
}

int TextFile::isvalid()
{
   return(valid);
}

int TextFile::match(char *name)
{
   return(!strcmp(name, filename));
}

void TextFile::fatal_error(char *errormsg)
{
   cerr << "\"" << filename 
	<< "\", line " << linenum
	<< ": error at \"" << token_text
	<< "\":  " << errormsg << endl;
   exit(-1);
}

void TextFile::warning(char *errormsg)
{
   cerr << "\"" << filename 
	<< "\", line " << linenum
	<< ": warning at \"" << token_text
	<< "\":  " << errormsg << endl;
}

void TextFile::got_whitespace()
{
   just_got_whitespace = 1;
}

int TextFile::whitespace_next()
{
   return just_got_whitespace;
}

int TextFile::whitespace_prev()
{
   return previous_got_whitespace;
}