/* -------------------------------------------------------------------------- *
 *          Program to generate the Bacon number files from 1 to 8            *
 *            By Anthony Jaroslav Truhlar (21/02/1999-07/04/1999)             *
 *                       Email: ajt97c@cs.nott.ac.uk                          *
 *                    Group Email: gp-dfb2@cs.nott.ac.uk                      *
 * -------------------------------------------------------------------------- *
 */

/* N.B. This code will only work with the AMD tools in $USER's path */

#define _GNU_SOURCE

#include <errno.h>
#include <ctype.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

/* Declare global constants */
#define FALSE 0
#define TRUE 1
#define FILENAME_LENGTH 100
#define TEMP_FN_LENGTH 14
#define LINE_LENGTH 80
#define BUFFER_LENGTH 160
#define AMD_OUT_MAX_SIZE 65536
#define NULL_CHAR '\0'
#define SHELL "/bin/sh"

static char *progname; /* program name needed in all functions */
volatile sig_atomic_t terminate_in_progress = 0;

char *allocate(size_t size) { /* Safe malloc checking for available VM */

  register char *value = (char *) malloc(size);
  if ( value == NULL ) {
    (void) fprintf(stderr, "FATAL error in this process (ID = %d): Virtual memory exhausted!\n", (int) getpid());
    exit (EXIT_FAILURE);
  }
  return value;

}

FILE *open_stream(char *dest, const char *access_mode, int pipe) {

  FILE *stream;

  errno = 0; /* don't output anything to stderr, we'll do that */

  stream = (pipe) ? popen( dest, access_mode ):fopen( dest, access_mode );
  if ( stream == NULL ) {
    (void) fprintf( stderr, "%s: Unrecoverable error opening %s; %s.\n", progname, dest, strerror(errno) );
    exit (EXIT_FAILURE); /* can't read file - abort execution */
  }

  return stream;

}

void delete_file(char *filename) {

  errno = 0; /* don't output anything on stderr, we'll do that */

  if ( unlink( filename ) == -1 ) {
    (void) fprintf(stderr, "%s: Cannot delete file %s; %s.\n", progname, filename, strerror(errno));
  }

  return; /* return under all conditions - not fatal */

}

void execute(char *shellcommand) {

  int status;
  pid_t pid;

  pid = fork(); /* fork off subprocess and return process ID or error status */

  errno = 0;

  if ( pid == (pid_t) 0 ) {
    /* This is the child process, therefore execute the shell command */
    (void) execl( SHELL, SHELL, "-c", shellcommand, NULL );
    _exit (EXIT_FAILURE);
  } else {
    if ( pid < (pid_t) 0 ) {
      /* Unable to fork off a new process, report failure */
      status = -1;
    } else {
      /* Wait for child to complete, -1 indicates a failure in the child */
      if ( waitpid( pid, &status, 0 ) != pid ) status = -1;
    }
  }

  if ( status == -1 ) {
    (void) fprintf( stderr, "%s: Unable to execute shell command \"%s\"; %s!\n", progname, shellcommand, strerror(errno));
    exit (EXIT_FAILURE);
  } else {
    return;
  }

}

void terminate_signal( int signal ) {

  char *cleanupcommand = allocate(LINE_LENGTH);
  char *pid_s = allocate(6);

  /* Protect against further errors/other signals whilst in signal handler */
  if( terminate_in_progress ) (void) raise( signal );
  terminate_in_progress = 1;

  /* Clean up */
  (void) strcpy( cleanupcommand, "rm -f /tmp/*" );
  (void) sprintf( pid_s, "%d", (int) getpid() );
  (void) strcat( cleanupcommand, pid_s );
  (void) strcat( cleanupcommand, "*" );
  execute( cleanupcommand );
  free( cleanupcommand );
  free( pid_s );
  
  /* Re-raise signal, which since blocked will be handled in the default way */
  (void) raise(signal);

}

void films(char *actor, char *films) {

  /* Allocate memory, and initialise streams */
  char *command = allocate(BUFFER_LENGTH);
  char *linebuf = allocate(BUFFER_LENGTH);
  char *command_cpy = command;
  FILE *cmdout;               /* stream for output from AMD toola */
  char *commandcpy, *linebufcpy;

  /* Keep track of start of arrays */
  commandcpy = command;
  linebufcpy = linebuf;

  /* Nullify strings */
  *command = NULL_CHAR;
  *linebuf = NULL_CHAR;
  *films = NULL_CHAR; /* ensure it's free for use */

  /* Construct AMD command for given actor */
  (void) strcpy( command, "list -cast \"" );
  (void) strcat( command, actor );
  (void) strcat( command, "\"" );

  /* Execute command and capture output */
  cmdout = open_stream( command_cpy, "r", TRUE );
  while( fgets( linebuf, BUFFER_LENGTH, cmdout ) != NULL ) {
    /* only copy cinema films */
    if ( ( strstr( linebuf, "(TV)" ) == NULL ) && ( strstr( linebuf, "(V)" ) == NULL ) ) {
      (void) strcat( films, linebuf );
    }
  } /* copy lines into memory */
  pclose( cmdout );

  /* Deallocate memory */
  free(commandcpy);
  free(linebufcpy); 
  return;

} /* end of function: films */

void stars(char *movie, char *actors) {

  /* Allocate memory, and initialise streams */
  char *command = allocate(BUFFER_LENGTH);
  char *linebuf = allocate(BUFFER_LENGTH);
  char *command_cpy = command;
  FILE *cmdout;               /* stream for output from AMD toola */
  char *commandcpy, *linebufcpy;

  /* Keep track of start of arrays */
  commandcpy = command;
  linebufcpy = linebuf;
  
  /* Nullify strings */
  *command = NULL_CHAR;
  *linebuf = NULL_CHAR;
  *actors = NULL_CHAR;

  /* Construct AMD command for given actor */
  (void) strcpy( command, "title -t \"" );
  (void) strcat( command, movie );
  (void) strcat( command, "\"" );

  /* Execute command and capture output */
  cmdout = open_stream( command_cpy, "r", TRUE );
  while( fgets( linebuf, BUFFER_LENGTH, cmdout ) != NULL ) {
    (void) strcat( actors, linebuf );
  } /* copy lines into memory */
  pclose( cmdout );

  /* Demallocte memory */
  free(commandcpy);
  free(linebufcpy); 
  return;

} /* end of fn: stars */

int isroman(char ch) {

  return ( ch == 'I' || ch == 'X' || ch == 'V' );

} /* end of fn: isroman */

void stripdesc(char *tostrip, char *stripped, int films) {

  int round = FALSE, square = FALSE, nl = 0; /* nl - keep track of nesting level */
  int sql = 0;
  int brkt = (films) ? FALSE:TRUE; /* flag for year/(I etc) tags */
  char *temp = stripped;

  /* scan through memory and remove junk */
  do {

    if ( *tostrip == '(' ) nl++;

    if ( *tostrip == '[' ) sql++;

    /* only year and actor number information can be bracketed */
    if ( *tostrip == '(' && (!isdigit(*(tostrip+1))) && (!isroman(*(tostrip+1))) && brkt ) {
      round = TRUE;
      stripped -= ( ! square ) ? 1:0; /* remove uneccessary spaces before bracketing */
      stripped += ( nl >= 2 ) ? 1:0; /* nested brackets aren't preceeded by spaces */
    }

    if ( *tostrip == '[' ) {
      square = TRUE;
      stripped -= ( ! round ) ? 2:0;
      stripped += ( sql >= 2 ) ? 2:0;
    }

    /* Deal with characters that have special significance to the shell ($) */
    if ( *tostrip == '$' && !round && !square ) {
      *stripped++ = '\\';
      *stripped++ = '$';
      tostrip++;
    }

    if ( *tostrip == '`' && !round && !square ) {
      *stripped++ = '\\';
      *stripped++ = '`';
      tostrip++;
    }

    if ( ! round && ! square && *tostrip != ']' ) *stripped++ = *tostrip;

    if ( *tostrip == ')' && nl > 0 ) nl--;

    if ( *tostrip == '\n' && nl > 0 ) {
      nl = 0;
      round = FALSE;
      *stripped++ = '\n';
    } /* Deal with untermined nesting of parentheses */

    if ( *tostrip == ']' && sql > 0 ) sql--;

    if ( *tostrip == '\n' && sql > 0 ) {
      sql = 0;
      square = FALSE;
      *stripped++ = '\n';
    } /* Same use for square brackets as above */

    if ( *tostrip == '\n' && films ) brkt = FALSE; /* only turn off at eol for films, don't allow for actors */

    if ( *tostrip == ')' && *(tostrip+1) != ')' && nl == 0 ) {
      round = FALSE;
      brkt = TRUE;
    }

    if ( *tostrip == ']' && *(tostrip+1) != ']' && sql == 0 ) square = FALSE;

  } while( *tostrip++ != NULL_CHAR );

  stripped = temp;
  return;
  
} /* end of fn: stripdesc */

int bacon(int min, int max, int optimise, char *path) {

  /* Allocate memory for filenames */
  char *prevlvlfn = allocate(FILENAME_LENGTH);
  char *currlvlfn = allocate(FILENAME_LENGTH);
  char *tmpfile = allocate(TEMP_FN_LENGTH);
  char *films1 = allocate(TEMP_FN_LENGTH);
  char *films2 = allocate(TEMP_FN_LENGTH);
  char *actor1 = allocate(TEMP_FN_LENGTH);
  char *actor2 = allocate(TEMP_FN_LENGTH);
  char *diff1 = allocate(TEMP_FN_LENGTH);
  char *diff2 = allocate(TEMP_FN_LENGTH);
  char *filmfiles = allocate(BUFFER_LENGTH);
  char *filmfilescpy = allocate(BUFFER_LENGTH);

  /* Allocate memory for commands */
  char *shellcommand = allocate(BUFFER_LENGTH);

  /* Allocate memory for title & list output */
  char *out = allocate(AMD_OUT_MAX_SIZE);
  char *out2 = allocate(AMD_OUT_MAX_SIZE);
 
  char *num = allocate(3);
  char *token;
  FILE *prevlvl, *currlvl, *tmp;
  char *linebuf = allocate(BUFFER_LENGTH);

  char *outcpy = out;
  char *out2cpy = out2;

  int i, j;
  int first = TRUE;

  *out = NULL_CHAR;
  *out2 = NULL_CHAR;
  *diff1 = NULL_CHAR;
  *diff2 = NULL_CHAR;

  errno = 0;

  for( i = min; i <= max; i++ ) {

    /* Construct filenames for BN lists */
    *prevlvlfn = NULL_CHAR;
    *currlvlfn = NULL_CHAR;
    (void) strcpy( prevlvlfn, path );
    if ( *(path+(strlen(path)-2)) != '/' ) (void) strcat( prevlvlfn, "/" );
    (void) strcat( prevlvlfn, "BN" );
    (void) sprintf( num, "%d", i-1 );
    (void) strcat( prevlvlfn, num );
    (void) strcat( prevlvlfn, ".txt" );
    (void) strcpy( currlvlfn, path );
    if ( *(path+(strlen(path)-2)) != '/' ) (void) strcat( currlvlfn, "/" );
    (void) strcat( currlvlfn, "BN" );
    (void) sprintf( num, "%d", i );
    (void) strcat( currlvlfn, num );
    (void) strcat( currlvlfn, ".txt" );

    /* Construct temporary filenames  */
    *films1 = NULL_CHAR;
    *films2 = NULL_CHAR;
    *actor1 = NULL_CHAR;
    *actor2 = NULL_CHAR;
    films1 = tempnam( "/tmp", "films" );
    films2 = tempnam( "/tmp", "films" );
    actor1 = tempnam( "/tmp", "actor" );
    actor2 = tempnam( "/tmp", "actor" );

    (void) strcpy( filmfilescpy, filmfiles );
    first = TRUE;

    /* Construct list of films from previous level file */
    prevlvl = open_stream( prevlvlfn, "r", FALSE );
    tmp = open_stream( films1, "w", FALSE );
    while( fgets( linebuf, BUFFER_LENGTH, prevlvl ) != NULL ) {
      *(linebuf+(strlen(linebuf))-1) = NULL_CHAR;
      films(linebuf,out);
      stripdesc(out,out2,TRUE);
      (void) fputs(out2,tmp);
    }
    fclose(tmp);
    fclose(prevlvl);

    /* Construct sort command for films list */
    *shellcommand = NULL_CHAR;
    (void) strcpy( shellcommand, "sort -uf " );
    (void) strcat( shellcommand, films1 );
    (void) strcat( shellcommand, " > " );
    (void) strcat( shellcommand, films2 );

    /* Sort list of films removing duplicates */
    execute( shellcommand );

    /* Remove unsorted films list */
    delete_file(films1);

    /* Remove films that have been referenced at previous levels */
    if( optimise && ((max - i) < (max - 1)) ) {
      token = (char *) strtok( filmfilescpy, "$" );
      while ( token ) {

	/* Construct temporary filenames for this iteration */
	if ( !first ) {
	  diff1 = diff2;
	} else {
	  diff1 = films2;
	  first = FALSE;
	}
	diff2 = tempnam( "/tmp", "diff" );

	/* Construct and execute shell commands */
	*shellcommand = NULL_CHAR;
	(void) strcpy( shellcommand, "diff " );
	(void) strcat( shellcommand, token );
	(void) strcat( shellcommand, " " );
	(void) strcat( shellcommand, diff1 );
	(void) strcat( shellcommand, " | grep \">\" | sed -e \"s/> //\" > " );
	token = (char *) strtok( NULL, "$" );
	if ( !token  ) { 
	  films2 = tempnam( "/tmp", "films" );
	  (void) strcat( shellcommand, films2 );
	} else {
	  (void) strcat( shellcommand, diff2 );
	}
	execute( shellcommand );

	/* Remove remaining temporary files from this iteration */
	delete_file(diff1);
      }
      
      first = TRUE; /* reset first flag for next iteration */

    } /* only do if optimise is flagged */

    /* Generate list of actors from films list */
    tmp = open_stream( films2, "r", FALSE );
    currlvl = open_stream( actor1, "w", FALSE );
    while( fgets( linebuf, BUFFER_LENGTH, tmp ) != NULL ) {
      *(linebuf+(strlen(linebuf))-1) = NULL_CHAR;
      stars(linebuf,out);
      stripdesc(out,out2,FALSE);
      (void) fputs(out2,currlvl);
    }
    fclose(tmp);
    fclose(currlvl);

    /* Remove sorted films list if optimised not flagged - not needed now */
    if ( !optimise ) delete_file(films2);

    /* Construct sort command for films list */
    *shellcommand = NULL_CHAR; /* nullify */
    (void) strcpy( shellcommand, "sort -uf " );
    (void) strcat( shellcommand, actor1 );
    (void) strcat( shellcommand, " > " );
    (void) strcat( shellcommand, actor2 );

    /* Sort temporary file, ready for removal of exiting BN's */
    execute( shellcommand );

    /* Remove unsorted actors list */
    delete_file(actor1);

    *diff1 = NULL_CHAR;
    *diff2 = NULL_CHAR;

    /* Remove existing BN's */
    for( j = i; j >= 1; j-- ) {

      /* Construct temporary filenames for this iteration */
      if ( j != i ) {
	diff1 = diff2;
      } else {
	diff1 = actor2;
      }
      diff2 = tempnam( "/tmp", "diff" );

      /* Construct and execute shell commands */
      *shellcommand = NULL_CHAR;
      (void) strcpy( shellcommand, "diff ");
      (void) strcat( shellcommand, path );
      if ( *(path+(strlen(path)-2)) != '/' ) (void) strcat( shellcommand, "/" );
      (void) strcat( shellcommand, "BN" );
      (void) sprintf( num, "%d", j-1 );
      (void) strcat( shellcommand, num );
      (void) strcat( shellcommand, ".txt " );
      (void) strcat( shellcommand, diff1 );
      (void) strcat( shellcommand, " | grep \">\" | grep -v \"[ ]\\{3\\}\" | grep -v \"^> \\-*\\$\" | sed -e \"s/> //\" > " );
      if ( j == 1 ) { 
	(void) strcat( shellcommand, currlvlfn );
      } else {
	(void) strcat( shellcommand, diff2 );
      }
      execute( shellcommand );

      /* Remove remaining temporary files from this iteration */
      if ( j == 1 ) delete_file(actor2);
      if ( j != i ) delete_file(diff1);

    }

    /* Make note of film list filename from this iteraion */
    (void) strcat( filmfiles, films2 );
    (void) strcat( filmfiles, "$" );

  } /* end loop */

  (void) strcpy( filmfilescpy, filmfiles );

  /* Remove any remaining temporary files (from optimised run) */
  if ( optimise ) {
    token = (char *) strtok( filmfilescpy, "$" );
    while ( token ) {
      delete_file( token );
      token = (char *) strtok( NULL, "$" );
    }
  }

  /* Deallocate memory */
  free(prevlvlfn);
  free(currlvlfn);
  free(tmpfile);
  free(films1);
  free(films2);
  free(actor1);
  free(actor2);
  free(diff1);
  free(diff2);
  free(filmfiles);
  free(filmfilescpy);
  free(shellcommand);
  free(outcpy);
  free(out2cpy);
  free(num);
  free(linebuf);
  return EXIT_SUCCESS;

}

/* ================================= MAIN ================================== */

/* Options:
   ========
   No flags - generate all lists from BN1 to BN8
   -a actor - specify an actor other than K.B (e.g. generate Connery numbers)
   -o       - optimise generation; uses slightly more space (rmdups on films)
   -p path  - search for and generate the files in the specified directory
   -s n m   - generates a set of files from BNn.txt to BNm.txt (assumes all
              other files that are needed are present).
   -i p q r - generates the individual files BNp.txt, BNq.txt and BNr.txt ...
*/

int main(int argc, char **argv) {

  /* Declare variables */
  char *shortname = allocate(LINE_LENGTH);
  char *fullname = allocate(LINE_LENGTH);
  char *filename = allocate(FILENAME_LENGTH);
  char *actor = allocate(LINE_LENGTH);
  char *path = allocate(LINE_LENGTH);
  char *set = allocate(LINE_LENGTH);
  char *individual = allocate(LINE_LENGTH);
  char *token;
  FILE *file;
  int optimise = FALSE;
  int aflag = FALSE, pflag = FALSE, sflag = FALSE, iflag = FALSE;
  int min = 1, max = 8;
  int c, tmp;

  /* Set up signal handlers */
  (void) signal( SIGINT, terminate_signal );
  (void) signal( SIGHUP, terminate_signal );
  (void) signal( SIGTERM, terminate_signal );
  (void) signal( SIGPIPE, terminate_signal );
  (void) signal( SIGABRT, terminate_signal );

  /* Set default path to current directory */
  (void) strcpy( path, "." );

  /* Strip path from argv[0] to get program_invocation_short_name */
  (void) strcpy( fullname, argv[0] );
  token = (char *) strtok( fullname, "/" );
  (void) strcpy( shortname, token );
  while( token ) {
    (void) strcpy( shortname, token );
    token = (char *) strtok( NULL, "/" );
  }

  progname = shortname; /* make program name available to other functions */

  opterr = FALSE; /* Do NOT print option parsing errors to stderr */
  errno = 0;

  /* Parse command line options */
  while ((c = getopt(argc, argv, "a:i:op:s:")) != -1) {

    switch(c) {

      case 'a':
	(void) strcpy( actor, optarg );
	aflag = ( strstr( actor, "Bacon, Kevin" ) ) ? FALSE:TRUE;
	break;

      case 'i':
	if( iflag == 2 ) {
	  (void) strcat( individual, "," );
	  (void) strcat( individual, optarg );
	} else {
	  (void) strcpy( individual, optarg );
	}
	iflag = 2;
	break;

      case 'o':
	optimise = TRUE;
        break;

      case 'p':
	*path = NULL_CHAR;
	(void) strcpy( path, optarg );
	pflag = TRUE;
	break;

      case 's':
	(void) strcpy( set, optarg );
	sflag = TRUE;
	break;

      case '?':
	if (isprint (optopt)) {
	  if ( optopt == 'a' || optopt == 'i' || optopt == 's' || optopt == 'p' ) {
	    (void) fprintf(stderr,"%s: Option '-%c' has a mandatory parameter missing!\n",shortname,optopt);
	  } else {
	    (void) fprintf(stderr,"%s: Invalid option: '-%c'.\n",shortname,optopt);
	  }
	} else {
	  (void) fprintf(stderr,"%s: Unknown option character '\\x%x'.\n",shortname,optopt);
	}
       
	return EXIT_FAILURE;

    }

  }

  /* initialise variables & files */
  /* ============================ */

  /* construct low level file if not readable */
  (void) strcpy( filename, path );
  if( *(path+(strlen(path)-2)) != '/' ) (void) strcat( filename, "/" );
  (void) strcat( filename, "BN0.txt" );

  file = open_stream( filename, "w", FALSE );
  if ( aflag ) {
    (void) fprintf( file, "%s\n", actor );    
  } else {
    (void) fprintf( file, "%s\n", "Bacon, Kevin" );
  }
  fclose(file);

  /* generate any individual files */
  if ( iflag ) {
    token = (char *) strtok( individual, "," );
    while( token ) {
      tmp = atoi(token);
      if ( tmp > 0 ) (void) bacon( tmp, tmp, FALSE, path );
      token = (char *) strtok( NULL, "," );
    }
  }

  /* generate any sets of files */
  if ( sflag ) {
    token = (char *) strtok( set, "," );
    min = atoi(token);
    token = (char *) strtok( NULL, "," );
    max = atoi(token);
    if ( min > 0 && max >= min ) (void) bacon( min, max, optimise, path );
  }

  /* if neither i or s flags set, do BN1-8 */
  if ( !iflag && !sflag ) (void) bacon( min, max, optimise, path );

  /* clean up - deallocate memory, etc. */
  free(shortname);
  free(fullname);
  free(filename);
  free(actor);
  free(path);
  free(set);
  free(individual);

  return EXIT_SUCCESS;

}

