/*  
  prerex.c  -- interactive editor of prerequisite-chart descriptions
  Copyright (c) 2005-08  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.
*/

/* 

The key functions in this program are as follows:

  analyze_tex_file():  reads a TeX file and builds an internal representation
    of the boxes, arrows, and minis for the chart in linked lists accessed 
    through global pointer variables first_box, first_arrow, and first_mini,
    resp.  Non-chart elements of the TeX file are preserved in temporary files 
    pretext, posttext, and comments. The Boolean variable grid records whether a 
    background coordinate grid should be displayed. 

  regenerate_tex_file():  re-constructs a TeX file from the temporary files
    pretext, posttext and comments, and the linked lists of nodes and arrows;
    merge_lists() implements the output-ordering strategy.

  analyze_user_command(): processes editing and other commands entered at the 
    interactive prompt. The current stacks of "cuts" and "deletions" are
    accessed through pointer variables first_cut and first_deletion, which
    are global only to allow free_lists() to garbage-collect the cuts
    and other lists when necessary.


Changelog:

Version 4.1, 2008-05-16

  Implement shift/raise of specified diagram elements.
  Eliminate unnecessary dependence on libtermcap.

Version 4.0.1, 2008-02-28

  Corrected txt field of struct text to have size LINE_LEN+1

Version 4.0, 2008-02-13

  Test for curvatures > 100.
  Implement \text command.

Version 3.8.1, 2007-03-17

  Sleep for KILL_WAIT seconds before terminating the pdf viewer.

Version I.8, 2007-02-08

  Semicolon appended to most user commands suppresses regeneration and reprocessing.
  ^C saves to file.tex. Use quit; to exit without saving.
  Final processing uses latex -> dvips -> ps2pdf to produce a smaller final pdf.
  Attempts to display the pdf during editing using value of environment variable 
    PDFVIEWER. 

Version 3.7, 2006-12-27

  latex->dvips->ps2pdf processing restored as an option.
  Warns about quitting with unpasted cuts.

Version 3.6, 2006-11-29

  Corrects two minor logical errors in code for arrow deletion and box editing. 
  Generates progress messages when analyzing/saving/processing.
  Re-loads after every shell escape, so we can discard the Edit command.
  Applies shift/raise to cut and deleted nodes.
  Processes tex files using pdflatex instead of latex -> dvips -> ps2pdf.

Version 3.5, 2006-11-20
  
  simpler and more flexible implementation of cut/paste, delete/undelete
  generate undelete messages
  automatic re-generation and processing after editing operation

Version 3.4, 2006-11-15

  deal with files that are (initially) read-only
  deal with attempts to cut arrows

Version 3.3, 2006-11-07

  preserve comment lines within the chart environment
  correct option-handling bug introduced in 3.0

Version 3.2, 2006-11-03
Version 3.1, 2006-10-29

  introduce Edit command
  restore write-access to TeX file during Edit or shell-command execution
  try to display error message in log file if LaTeX processing fails

Version 3.0, 2006-10-19

  replaced calls to make file.pdf by successive calls to latex, dvips, ps2pdf
  removed all references to pdf viewer, user must start one him/herself
  replaced strncpy/cat by strlcpy/cat (my implementation of the OpenBSD functions)
  produce warning message for truncated course code, timetable, title

Version 2.4, 2006-04-12

  added undelete command
  no warnings using -Wall -Wextra
  allow for cut/paste of minis that are targets of arrow
  test for non-existence of source/target box/mini for new arrow
  handle more signals than SIGINT (such as SIGSEGV)

Version 2.3, 2006-03-23

  remove "which" checking of pdfviewer to allow, e.g., xpdf -remote label
  use geometry.sty for new blank TeX file
  declarations of optarg, optind moved to prerex.h

Version 2.2.2, 2006-03-08

  Free linked structures and readline strings.
  Turn grid off initially before restoring a backup.

Version 2.2.1, 2006-02-18

  Don't try to remove/restore access to an empty filename.

Version 2.2, 2006-02-15

  Inputs and outputs \begin{chart} ... \end{chart} instead of \chart{ ... }

Version 2.1.1, 2006-02-13

  Add designation PRIVATE ( = static) to definitions of functions and variables.

Version 2.1, 2006-02-09

  Trap SIGINT in order to restore write-access after ^C.
  Add Backup command.

Version 2.0.3, 2006-02-07

  New arrow should have default curvature.

Version 2.0.1, 2006-02-06

 Free list structures before Restoring.

Version 2.0, 2006-02-03

  Partition into separate modules prerex.c, inout.c, edit.c

Version 1.2.2, 2006-02-02

  Cleanup curvature code.
  Use %s to output % in regenerate_tex_file.

Version 1.2.1, 2006-02-02

  Allow for curved recommended and co-requisite arrows.

Version 1.2.0, 2006-02-01

  Restore command.

Version 1.1.2, 2006-01-31

  Create backup of Tex file before editing.
  Re-open TeX file for writing before regenerating.

Version 1.1.1, 2006-01-30

  TeX file read-only during editing (except when re-generating it).

Version 1.1.0, 2006-01-29

  Corrected use of cut stacks.
  Eliminated cut arrow command.
  Introduced delete box/mini and delete arrow.
  Checked for duplicate boxes, minis or arrows (e.g., when pasting).

Version 1.0.5, 2006-01-28

  Keep stacks of cut boxes, minis, and arrows.

Version 1.0.4, 2006-01-27

  Add option -p.

Version 1.0.3, 2006-01-26

  Replace system calls to "xpdf" by calls to pdfviewer (if defined).

Version 1.0.2, 2006-01-25

  Replace system call to "pdflatex filename" by call to "make filename.pdf"
    to provide more flexibility (e.g., LaTeX vs pdfLaTeX).

Version 1.0.1, 2006-01-24
  Error message for pdflatex failure now refers to filename.log.
  Inserted checks for lp or cp increasing too far in read_textfield 
    and read_bracketed_textfield.
  Process TeX file before analyzing it (as a syntax sanity pre-check).
  Defer arrow output if source box not previously output.

*/

# include "prerex.h"

char *pdfviewer; 

element *first_node;
element *first_arrow;
element *first_cut;		/* stack of cut nodes */
element *first_deletion;	/* stack of deleted nodes/arrows */

bool grid;			/* true if coordinate grid should be shown in background */
bool useLatex = false;          /* true if latex -> dvips -> ps2pdf should be used */
bool reprocess = true;          /* false if regenerate_and_process should do nothing */ 

char line[LINE_LEN];
char *command_line;
char *lp;			/* pointer into line or command_line */
char *cp;			/* pointer into text fields */

char filename[FILE_LEN];
FILE * tex_file, *pretext,	/* whatever is in the tex_file before the chart */
  *posttext,			/* whatever is in the tex_file after the chart */
  *comments;			/* comment lines in the chart environment */
char backup_filename[FILE_LEN + 1];
FILE *backup_tex_file;

char deftext[LINE_LEN];		/* buffer for default input; set using set_deftext */

pid_t pid = 0;                  /* process id for pdfviewer  */

void
remove_write_access (void)	/* to prevent interference */
{
  char command[64];
  strlcpy (command, "chmod a-w ", sizeof (command));
  strlcat (command, filename, sizeof (command));
  system (command);
}

void
restore_write_access (void)	/* to regenerate or restore the tex_file */
{
  char command[64];
  if (filename == NULL)
    return;
  strlcpy (command, "chmod ug+w ", sizeof (command));
  strlcat (command, filename, sizeof (command));
  system (command);
}

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

size_t
strlcpy (char *dst, const char *src, size_t n)
/*  Copies src to dst of size n. Returns strlen(src).  
 *  If strlen(src) >= n, the string is truncated.
 */
{
  char *d = dst;
  const char *s = src;
  if (n == 0)
    return strlen (src);
  while (n > 1 && *s != '\0')
    {
      *d = *s;			/* copy non-null character */
      d++;
      s++;
      n--;
    }
  *d = '\0';			/* null-terminate */
  while (*s != '\0')
    s++;			/* traverse rest of src */
  return s - src;
}

size_t
strlcat (char *dst, const char *src, size_t n)
/*  Appends src to dst of size n. Returns strlen(dst) + strlen(src).  
 *  If strlen(dst) + strlen(src) >= n, the string is truncated. 
 */
{
  char *d = dst;
  const char *s = src;
  size_t dst_length;
  if (n == 0)
    return strlen (src);
  while (n > 1 && *d != '\0')
    {
      d++;
      n--;
    }
  dst_length = d - dst;
  while (n > 1 && *s != '\0')
    {
      *d = *s;			/* copy non-null character */
      d++;
      s++;
      n--;
    }
  *d = '\0';			/* null-terminate */
  while (*s != '\0')
    s++;			/* traverse rest of src */
  return dst_length + (s - src);
}


int
set_deftext (void)		/* used by readline to output defaults */
{
  rl_insert_text (&deftext[0]);
  deftext[0] = '\0';
  rl_startup_hook = (rl_hook_func_t *) NULL;
  return 0;
}

PRIVATE void
sigproc (int i)  /* signal handler */
{
  i = 0;	/* just to suppress a gcc warning message */
  grid = false;
  puts("");
  regenerate_tex_file(); 
  restore_write_access ();
  if (pid > 0) kill(pid, SIGTERM); /* terminate pdfviewer process */
  exit (0);
}


PRIVATE void
create_blank_tex_file (void)
{
  tex_file = fopen (filename, "w+");
  if (tex_file == NULL)
    return;
  fprintf (tex_file, "%s\n", "\\documentclass{article}");
  fprintf (tex_file, "%s\n", "\\usepackage{geometry}");
  fprintf (tex_file, "%s\n", "\\geometry{noheadfoot, margin=0.5in}");
  fprintf (tex_file, "%s\n", "\\usepackage{prerex}");
  fprintf (tex_file, "%s\n", "\\begin{document}");
  fprintf (tex_file, "%s\n", "\\thispagestyle{empty}");
  fprintf (tex_file, "%s\n", "\\begin{chart}");
  fprintf (tex_file, "%s\n", "\\grid");
  fprintf (tex_file, "%s\n", "\\end{chart}");
  fprintf (tex_file, "%s\n", "\\end{document}");
  rewind (tex_file);
}


PRIVATE void
open_tex_file (void)
{
  char command[LINE_LEN];
  tex_file = fopen (filename, "r+");
  while (tex_file == NULL)
    {
      char code[16];
      strlcpy (deftext, "y", sizeof (deftext));
      rl_startup_hook = set_deftext;
      tex_file = fopen (filename, "r");
      if (tex_file != NULL)
	{
	  strlcpy 
            (command,
	     "This file is currently read-only; change permissions and continue (y/n)? ",
	      sizeof (command));
	  command_line = readline (command);
	  sscanf (command_line, "%15s", code);
	  free (command_line);
	  if (code[0] == 'y')
	    {
	      restore_write_access ();
	      tex_file = fopen (filename, "r+");
	      continue;
	    }
	}
      strlcpy (command, "Create new prerex file ", sizeof (deftext));
      strlcat (command, filename, sizeof (command));
      strlcat (command, " (y/n)? ", sizeof (command));
      command_line = readline (command);
      sscanf (command_line, "%15s", code);
      free (command_line);
      if (code[0] == 'y')
	{
	  create_blank_tex_file ();
	}
      else
	{
	  if (code[0] != 'n')
	    puts ("Response not recognized; 'n' assumed.");
	  command_line = readline ("Enter another file name: ");
	  sscanf (command_line, "%127s", filename);
	  free (command_line);
	  if (!suffix (".tex", filename))
	    strlcat (filename, ".tex", sizeof (filename));
	  tex_file = fopen (filename, "r+");
	}
    }
  remove_write_access ();
}

void
create_backup (void)
{
  strlcpy (backup_filename, ".", sizeof (backup_filename));
  strlcat (backup_filename, filename, sizeof (backup_filename));
  backup_tex_file = fopen (backup_filename, "w+");
  if (backup_tex_file == NULL)
    {
      puts ("Can't open backup file.");
      return;
    }
  copy (tex_file, backup_tex_file);
  fclose (backup_tex_file);
}

void
regenerate_and_process (void)
{
  if (!reprocess) return;
  regenerate_tex_file ();
  process_tex_file ();
}

void
process_tex_file (void)
{ /*  generates system calls to make file.pdf */
  char command[LINE_LEN];
  char filename_root[FILE_LEN + 3];
  char *p;
  rewind (tex_file);
  strlcpy (filename_root, filename, sizeof (filename_root));
  p = strstr (filename_root, ".tex");
  *p = '\0';
  if (useLatex)
    {
      printf ("Processing %s using LaTeX, dvips, and ps2pdf.\n", filename);
      strlcpy (command, "latex ", sizeof (command));
    }
  else
    {
      printf ("Processing %s using pdfLaTeX.\n", filename);
      strlcpy (command, "pdflatex ", sizeof (command));
    }
  strlcat (command,
           "-halt-on-error -interaction batchmode ", sizeof (command));
  strlcat (command, filename_root, sizeof (command));
  strlcat (command, " > /dev/null", sizeof (command));
  if (system (command))
    { /* try to display error message using PAGER or tail: */
      char *pager = getenv ("PAGER");
      if (pager == NULL)
        {
          puts ("");
          puts ("    ...    [log file lines deleted] ");
          puts ("");
          strlcpy (command, "tail -25 ", sizeof (command));
        }
      else if (strcmp (pager, "more") == 0)
        strlcpy (command, "more +/^! ", sizeof (command));
      else if (strcmp (pager, "less") == 0)
        strlcpy (command, "less -p^! ", sizeof (command));
      else
        {
          puts ("    ...    [log file lines deleted] ");
          puts ("");
          strlcpy (command, "tail -n 25 ", sizeof (command));
        }
      strlcat (command, filename_root, sizeof (command));
      strlcat (command, ".log", sizeof (command));
      system (command);
      puts ("\nProcessing failed so a PDF could not be generated.");
      return;
    }
    if (useLatex)
      {
        strlcpy (command, "dvips -q ", sizeof (command));
        strlcat (command, filename_root, sizeof (command));
        strlcat (command, " -f | ps2pdf - ", sizeof (command));
        strlcat (command, filename_root, sizeof (command));
        strlcat (command, ".pdf", sizeof (command));
        if (system (command))
            puts ("dvips | ps2pdf processing fails.");
      }
    puts("");
}

void
open_viewer (void)
{
  char pdf_filename[FILE_LEN];
  char *p;
  strlcpy (pdf_filename, filename, sizeof (pdf_filename));
  p = strstr (pdf_filename, ".tex");
  *p = '\0';
  strlcat (pdf_filename, ".pdf", sizeof (pdf_filename));
  printf ("Opening %s with %s.\n", pdf_filename, pdfviewer);
  pid = fork();
  if (pid == 0)  /* child process */
  {
     char *argv[3] = {pdfviewer, pdf_filename, (char *) NULL};
     execvp(argv[0], argv); 
     /* the call above returns only in case of an error */
     printf("%s fails to start.\n", pdfviewer); 
     _exit (0);
  }
  else if (pid == -1) perror("fork failed");
  return;
}

int
main (int argc, char *argv[])
{
  int ch;
  const char version[] = "4.1, 2008-05-16";
  char prompt[FILE_LEN + 8];
  printf ("This is prerex, version %s.\n", version);
  ch = getopt (argc, argv, "hvl");
  while (ch != -1)
    {
      switch (ch)
	{
	case 'h':
	  usage ();
	  exit (0);
	case 'v':
	  exit (0);
        case 'l':
          useLatex = true;
          break;
	case '?':
	  exit (EXIT_FAILURE);
	default:
	  printf ("Function getopt returned character code 0%o.\n",
		  (unsigned int) ch);
	  exit (EXIT_FAILURE);
	}
      ch = getopt (argc, argv, "hvl");
    }
  puts ("Copyright (c) 2005-08  R. D. Tennent");
  puts ("School of Computing, Queen's University, rdt@cs.queensu.ca");
  puts
    ("This program comes with NO WARRANTY. You can redistribute prerex and/or");
  puts
    ("modify it under the terms of the GNU General Public License version 2.");
  puts ("");
  if (optind == argc)
    {
      puts ("");
      do
	{
	  command_line = readline ("Please enter a file name: ");
	  sscanf (command_line, "%127s", filename);
	  free (command_line);
	}
      while (filename[0] == '\0');
    }
  else
    strlcpy (filename, argv[optind], sizeof (filename));
  if (!suffix (".tex", filename))
    strlcat (filename, ".tex", sizeof (filename));
  open_tex_file ();

  pdfviewer = getenv("PDFVIEWER");
  if (pdfviewer == NULL)
    puts("PDFVIEWER is not defined in your environment.");

  pretext = tmpfile ();
  posttext = tmpfile ();
  comments = tmpfile ();
  first_node = NULL;
  first_arrow = NULL;
  first_cut = NULL;
  first_deletion = NULL;
  analyze_tex_file ();

  grid = true;			/* turn on background coordinate grid  */
  regenerate_and_process (); 
  if (pdfviewer) open_viewer(); 
  create_backup ();  
  help (); 
  signal (SIGHUP, &sigproc);
  signal (SIGINT, &sigproc); 
  signal (SIGQUIT, &sigproc);
  signal (SIGILL, &sigproc);
  signal (SIGSEGV, &sigproc);
  signal (SIGTERM, &sigproc);  

  /* command processing loop:   */
  do
    {
      strlcpy (prompt, filename, sizeof (prompt));
      strlcat (prompt, "> ", sizeof (prompt));
      command_line = readline (prompt);
      if (command_line == NULL)
	break;			/* EOF */
      if (command_line[0] != '\0')
	{
	  add_history (command_line);
          reprocess = true;
          if (suffix(";", command_line))
          {
            /* suppress possible regeneration and processing after this command */
            reprocess = false;
            command_line[strlen(command_line)-1] = '\0';
          }
	  analyze_user_command ();
	}
      free (command_line);
    }
  while (true);			/* exit via break or call to exit or error()  */
  putchar ('\n');
  free (command_line); 
  grid = false;
  useLatex = true;
  regenerate_and_process ();
  restore_write_access ();
  sleep(KILL_WAIT);
  if (pid > 0) kill(pid, SIGTERM);     /* terminate pdfviewer */
  return 0;
}
