/* Copyright (C) 1996 Bjoern Beutel. */

/* Description. =============================================================*/

/* This program reads a project file and compiles all symbol files, rule files,
 * and lexicon files that must be updated. */

/* Includes. ================================================================*/

#define _POSIX_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <setjmp.h>
#include <glib.h>
#include "basic.h"
#include "files.h"
#include "malaga_files.h"
#include "input.h"
#ifdef POSIX
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#endif
#ifdef WIN32
#include <process.h>
#include <sys/stat.h>
#endif

/* Types. ===================================================================*/

typedef struct /* A file name on which a malaga file depends. */
{ 
  list_node_t *next; /* Next dependency in this list. */
  string_t name; /* File name of dependency. */
} dependency_t;

typedef struct /* Source file names and object file name of a Malaga file. */
{
  list_t dependencies;
  string_t source_file;
  string_t object_file;
} files_t;

/* Variables. ===============================================================*/

/* The names of the files needed by Malaga. */
static files_t symbol_files; 
static files_t extended_symbol_files;
static files_t lexicon_files;
static files_t allomorph_files;
static files_t morphology_files;
static files_t syntax_files;
static string_t prelex_file;

/* Functions. ===============================================================*/

static void 
execute( string_t command_name, string_t arg1, string_t arg2, string_t arg3,
         string_t file_name )
/* Execute a process and wait for successful termination. */
{
#ifdef POSIX
  pid_t pid;
  int status;

  if (file_name == NULL) 
    file_name = arg1;
  fprintf( stderr, "Compiling \"%s\".\n", name_in_path( file_name ) );
  switch (pid = fork()) 
  {
  case -1:
    complain( "Can't execute \"%s\".", command_name );
    break;
  case 0:
    execlp( command_name, command_name, arg1, arg2, arg3, NULL );
    complain( "Can't execute \"%s\".", command_name );
    break;
  default:
    waitpid( pid, &status, 0 );
    if (! WIFEXITED( status ) || WEXITSTATUS( status ) != 0) 
      complain( "Compilation failed." );
    break;
  }
#endif

#ifdef WIN32
  int status;
  string_t a1, a2, a3;

  a1 = (arg1 != NULL ? new_string_readable( arg1, NULL ) : NULL);
  a2 = (arg2 != NULL ? new_string_readable( arg2, NULL ) : NULL);
  a3 = (arg3 != NULL ? new_string_readable( arg3, NULL ) : NULL);

  if (file_name == NULL) 
    file_name = arg1;
  fprintf( stderr, "Compiling \"%s\".\n", name_in_path( file_name ) );
  status = _spawnlp( P_WAIT, command_name, command_name, a1, a2, a3, NULL );
  if (status == -1) 
    complain( "Can't execute \"%s\".", command_name );
  if (status != 0) 
    complain( "Compilation failed." );

  free( a1 ); free( a2 ); free( a3 );
#endif
}

/*---------------------------------------------------------------------------*/

static void 
free_files( files_t *files )
/* Free the space occupied by FILES. */
{
  dependency_t *file;
  
  free_mem( &files->source_file );
  free_mem( &files->object_file );
  FOREACH_FREE( file, files->dependencies ) 
    free_mem( &file->name );
}

/*---------------------------------------------------------------------------*/

static void 
free_all_files( void )
/* Free the space occupied by the file names. */
{
  free_files( &syntax_files );
  free_files( &morphology_files );
  free_files( &lexicon_files );
  free_files( &extended_symbol_files );
  free_files( &symbol_files );
  free_files( &allomorph_files );
}

/*---------------------------------------------------------------------------*/

static time_t 
file_time( string_t file_name )
/* Get the last modification time of FILE_NAME. */
{
  struct stat status;
  
  /* Assume earliest possible time for non-existing files. */
  if (stat( file_name, &status ) == 0) 
    return status.st_mtime;
  else
    return 0;
}

/*---------------------------------------------------------------------------*/

static void 
read_project_file( string_t project_file )
/* Read the project file. */
{
  FILE *project_stream;
  string_t project_line_p, argument, extension;
  char_t *project_line;
  files_t *files;
  dependency_t *dependency;
  string_t s;
  volatile int_t line_count;
  static bool_t err_pos_printed;

  err_pos_printed = FALSE;
  project_stream = open_stream( project_file, "r" );
  line_count = 0;
  while (TRUE) 
  { 
    project_line = read_line( project_stream );
    if (project_line == NULL) 
      break;
    line_count++;
    cut_comment( project_line );
    project_line_p = project_line;
    
    if (*project_line_p != EOS) 
    { 
      argument = NULL;
      TRY
      {
	argument = parse_word( &project_line_p );
	extension = NULL;
	files = NULL;
	if (strcmp_no_case( argument, "sym:" ) == 0) 
	{
	  extension = "sym";
	  files = &symbol_files;
	}
	else if (strcmp_no_case( argument, "esym:" ) == 0) 
	{
	  extension = "esym";
	  files = &extended_symbol_files;
	}
	else if (strcmp_no_case( argument, "all:" ) == 0) 
	{
	  extension = "all";
	  files = &allomorph_files;
	}
	else if (strcmp_no_case( argument, "lex:" ) == 0)
	{
	  extension = "lex";
	  files = &lexicon_files;
	}
	else if (strcmp_no_case( argument, "mor:" ) == 0)
	{
	  extension = "mor";
	  files = &morphology_files;
	}
	else if (strcmp_no_case( argument, "syn:" ) == 0)
	{
	  extension = "syn";
	  files = &syntax_files;
	}
	else if (strcmp_no_case( argument, "prelex:" ) == 0)
	{
	  s = parse_absolute_path( &project_line_p, project_file );
	  parse_end( &project_line_p );
	  if (! has_extension( s, "prelex" ))
	  {
	    complain( "\"%s\" should have extension \"prelex\".", 
		      name_in_path( s ) );
	  }
	  set_binary_file_name( &prelex_file, s );
	  free_mem( &s );
	}
	else if (strcmp_no_case( argument, "split-hangul-syllables:" ) == 0)
	{ 
	  split_hangul_syllables = parse_yes_no( &project_line_p );
	  parse_end( &project_line_p );
	}
	else if (strcmp_no_case( argument, "include:" ) == 0)
	{ 
	  s = parse_absolute_path( &project_line_p, project_file );
	  parse_end( &project_line_p );
	  read_project_file( s );
	  free_mem( &s );
	}
	free_mem( &argument );

	if (files != NULL) 
	{
	  /* Read the files in the current line
	   * and include them into the appropriate file_list. */
	  while (*project_line_p != EOS) 
	  { 
	    /* Read the next file name in the project file. */
	    argument = parse_absolute_path( &project_line_p, project_file );
	    
	    /* Add file to dependency list . */
	    dependency = new_node( &files->dependencies, 
				   sizeof( dependency_t ), LIST_END );
	    dependency->name = argument;
	    
	    /* Set the object file if it's the first source file. */
	    if (files->source_file == NULL) 
	    { 
	      if (! has_extension( argument, extension ))
	      {
		complain( "\"%s\" should have extension \"%s\".", 
			  name_in_path( argument ), extension );
	      }
	      set_file_name( &files->source_file, argument );
	      set_binary_file_name( &files->object_file, argument );
 	      if (! file_exists( files->source_file )
		  && ! file_exists( files->object_file ))
	      {
		complain( "Files \"%s\" and \"%s\" do not exist.", 
			  name_in_path( files->source_file ), 
			  name_in_path( files->object_file ) );
	      }
	    }
	    else if (file_exists( files->source_file )
		     && ! file_exists( argument ))
	    {
	      /* The source file exists, but the include file doesn't. */
	      complain( "File \"%s\" does not exist.", 
			name_in_path( argument ) );
	    }
	  }
	}
      }
      IF_ERROR
      {
	if (! err_pos_printed)
	{
	  print_text( error_text, " (\"%s\", line %d)",
		      name_in_path( project_file ), line_count );
	  err_pos_printed = TRUE;
	}
      }
      END_TRY;
    }
    free_mem( &project_line );
  }
  close_stream( &project_stream, project_file );
}

/*---------------------------------------------------------------------------*/

static bool_t 
current_code_version( string_t file, string_t binary_file )
/* Return if FILE is compiled with the current code version. */
{
  FILE *stream;
  common_header_t header;
  bool_t up_to_date;
  int_t min_code_version, max_code_version;
  struct stat status;

  if (has_extension( file, "sym" ) || has_extension( file, "esym" ))
  { 
    min_code_version = MIN_SYMBOL_CODE_VERSION;
    max_code_version = SYMBOL_CODE_VERSION;
  }
  else if (has_extension( file, "lex" ))
  {
    min_code_version = MIN_LEXICON_CODE_VERSION;
    max_code_version = LEXICON_CODE_VERSION;
  }
  else if (has_extension( file, "all" ) || has_extension( file, "mor" )
	   || has_extension( file, "syn" )) 
  { 
    min_code_version = MIN_RULE_CODE_VERSION;
    max_code_version = RULE_CODE_VERSION; 
  } 
  else 
    complain( "Internal error." );

  /* Assume earliest possible time for non-existing files. */
  if (stat( binary_file, &status ) == -1) 
    return FALSE;

  stream = open_stream( binary_file, "rb" );
  read_vector( &header, sizeof( header ), 1, stream, binary_file );
  up_to_date = (header.code_version >= min_code_version
		&& header.code_version <= max_code_version
		&& header.split_hangul_syllables == split_hangul_syllables);
  close_stream( &stream, binary_file );
  return up_to_date;
}

/*---------------------------------------------------------------------------*/

static bool_t 
is_obsolete( files_t *files, ... )
/* Check if FILES->OBJECT_FILE is older than one of FILES
 * or one of the files given as optional arguments. */
{
  va_list arg;
  time_t object_time;
  dependency_t *dependency;
  string_t file_name;
  bool_t obsolete;

  if (! file_exists( files->source_file )) 
    return FALSE;
  object_time = file_time( files->object_file );
  obsolete = FALSE;
  FOREACH( dependency, files->dependencies ) 
  { 
    if (object_time < file_time( dependency->name )) 
      obsolete = TRUE;
  }
  va_start( arg, files );
  for (file_name = va_arg( arg, string_t );
       file_name != NULL; 
       file_name = va_arg( arg, string_t )) 
  { 
    if (object_time < file_time( file_name )) 
      obsolete = TRUE;
  }
  va_end( arg );
  if (! current_code_version( files->source_file, files->object_file )) 
    obsolete = TRUE;

  return obsolete;
}

/*---------------------------------------------------------------------------*/

int 
main( int argc, char *argv[] )
/* The main function of "malmake". */
{
  string_t project_file;
  int_t i;
  bool_t recompile;

  init_basic( "malmake" );
  init_input();

  /* Parse arguments. */

  if (argc == 2) 
  { 
    if (strcmp_no_case( argv[1], "--version" ) == 0
	|| strcmp_no_case( argv[1], "-version" ) == 0
	|| strcmp_no_case( argv[1], "-v" ) == 0) 
    { 
      program_message();
      exit(0);
    } 
    else if (strcmp_no_case( argv[1], "--help" ) == 0
	     || strcmp_no_case( argv[1], "-help" ) == 0
	     || strcmp_no_case( argv[1], "-h" ) == 0) 
    { 
      printf( "Compile all files of a Malaga grammar.\n\n"
	      "Usage:\n"
	      "malmake PROJECT_FILE "
	      "-- Compile files in PROJECT_FILE if needed.\n"
	      "malmake -v[ersion]   "
	      "-- Print version information.\n"
	      "malmake -h[elp]      "
	      "-- Print this help.\n\n"
	      "Option \"-n[ew]\" makes malmake recompile the whole project.\n"
	      "PROJECT_FILE must end on \".pro\".\n" );
      exit(0);
    }
  }

  project_file = NULL;
  recompile = FALSE;
  for (i = 1; i < argc; i++) 
  { 
    if (has_extension( argv[i], "pro" )) 
      set_file_name( &project_file, argv[i] );
    else if (strcmp_no_case( argv[i], "-new" ) == 0 
	     || strcmp_no_case( argv[i], "-n" ) == 0)
    {
      recompile = TRUE;
    }
    else 
      complain( "Illegal argument \"%s\".", argv[i] );
  }

  if (project_file == NULL) 
    complain( "Missing project file name." );
  read_project_file( project_file );
  
  /* Test dependencies and compile files. */
  
  if (symbol_files.source_file == NULL) 
    complain( "Missing symbol file names in project file." );
  if (is_obsolete( &symbol_files, NULL ) || recompile) 
  {
    execute( "malsym", symbol_files.source_file, 
	     (split_hangul_syllables ? "-split-hangul-syllables" : NULL), 
	     NULL, NULL );
  }

  if (allomorph_files.source_file == NULL) 
    complain( "Missing allomorph file names in project file." );
  if (is_obsolete( &allomorph_files, symbol_files.object_file, NULL ) 
      || recompile) 
  { 
    execute( "malrul", allomorph_files.source_file, 
             symbol_files.source_file, NULL, NULL );
  }

  if (lexicon_files.source_file == NULL) 
    complain( "Missing lexicon file names in project file." );
  if (is_obsolete( &lexicon_files, symbol_files.object_file, 
		   allomorph_files.object_file, prelex_file, NULL )
      || recompile) 
  { 
    execute( "mallex", project_file, "-binary", NULL,
	     lexicon_files.source_file ); 
  }

  if (morphology_files.source_file != NULL
      && (is_obsolete( &morphology_files, symbol_files.object_file, NULL )
	  || recompile)) 
  { 
    execute( "malrul", morphology_files.source_file,
             symbol_files.source_file, NULL, NULL );
  }

  if (extended_symbol_files.source_file != NULL) 
  { 
    if (is_obsolete( &extended_symbol_files, symbol_files.object_file, NULL )
	|| recompile)
    { 
      execute( "malsym", extended_symbol_files.source_file, 
	       "-use", symbol_files.source_file, NULL );
    }
  }
  else
  { 
    extended_symbol_files.source_file 
      = new_string( symbol_files.source_file, NULL );
    extended_symbol_files.object_file 
      = new_string( symbol_files.object_file, NULL );
  }

  if (syntax_files.source_file != NULL 
      && (is_obsolete( &syntax_files, extended_symbol_files.object_file, NULL )
	  || recompile)) 
  { 
    execute( "malrul", syntax_files.source_file, 
             extended_symbol_files.source_file, NULL, NULL );
  }

  fprintf( stderr, "\"%s\" is up to date.\n", name_in_path( project_file ) );
  free_all_files();
  free_mem( &project_file );
  terminate_input();
  terminate_basic();

  return 0;
}

/* End of file. =============================================================*/
