/*
 * forall.c
 * Execute a command for matching filenames on a specific path, sort of thing
 *  Musus Umbra 1997
 */


#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdarg.h>

#include "ReadDir.h"
#include "MiscCode.h"

#include "kernel.h"			/* for _kernel_oscli() */

#include "messages.c"		/* makes this file more readable :-) */

#define FIE(X) die_fie((X),__FILE__,__LINE__)
#define OOM(X) die_oom((X),__FILE__,__LINE__)


/* BUFFER_SIZE controls the buffer malloc'd to read directories.  Typically	*/
/* one file/sub-dir will require 32 bytes of buffer, or thereabouts.		*/
#define BUFFER_SIZE 4096

/* SCRAP_SIZE controls the scratch space allocation.  Must be large enough	*/
/* for the longest pathname / command generated during execution			*/
#define SCRAP_SIZE 4096


/* Macro to extract the filetype info from a load address.  Untyped files	*/
/* are given the type 0x1000												*/
#define FILETYPE(X) ( (((X) & 0xfff00000)==0xfff00000) ? \
									(((X) & 0xfff00)>>8) : 0x1000 )

/*--------------------------------------------------------------------------*/
/* General purpose linked list node											*/
/*--------------------------------------------------------------------------*/

typedef struct lnode
{
	struct	lnode	*next;
	char			*name;
} lnode;


/*--------------------------------------------------------------------------*/
/* Look up tables for the command line options								*/
/*--------------------------------------------------------------------------*/

static char *option_names[] =
{
	"path", "file", "type", "recurse", "silent",
	"noexecute", "images", "verbose", "command",
	"help", "ignore", "obey", "oscli"
};

typedef enum
{
	OPTN_PATH=0, OPTN_FILE, OPTN_TYPE, OPTN_RECURSE, OPTN_SILENT,
	OPTN_NOEXECUTE, OPTN_IMAGES, OPTN_VERBOSE, OPTN_COMMAND,
	OPTN_HELP, OPTN_IGNORE, OPTN_OBEY, OPTN_OSCLI,
	OPTN_UNKNOWN,
	OPTN_AMBIG
} option_value;



/*--------------------------------------------------------------------------*/
/* Options																	*/
/*--------------------------------------------------------------------------*/

static char	*path_spec		= NULL;		/* default search path */
static char	*file_spec		= NULL;		/* default file_spec */
static char	*comm_spec		= NULL;		/* command spec */
static int	images			= 0;		/* treat as files by default */
static int	recurse			= 0;		/* don't recurse by default */
static int	silent			= 0;		/* echo by default */
static int	verbose			= 0;		/* quiet by default */
static int	execute			= 1;		/* execute commands by default */
static int	ignore			= 0;		/* continue after errors? */
static char	*obey			= NULL;		/* create an obey file */
static int	oscli			= 0;		/* Use OS_CLI to start commands */
static unsigned int	match_types[129];	/* NB: this is a bit array! */



/*--------------------------------------------------------------------------*/
/* Global state																*/
/*--------------------------------------------------------------------------*/

static int match_all_types = 1;			/* ignore the type array? */
static int exclusion_list  = 0;			/* any ~types? */
static lnode *match_queue_head = NULL;	/* head of the matched files Q */
static lnode *match_queue_tail = NULL;	/* tail of the matched files Q */
static lnode *dir_queue_head = NULL;	/* directories left to process */
static lnode *dir_queue_tail = NULL;	/* directories left to process */
static char buffer[BUFFER_SIZE];		/* buffer for ReadDir */
static char scrap[SCRAP_SIZE];			/* scratch space */


/*--------------------------------------------------------------------------*/
/* Error report functions													*/
/*--------------------------------------------------------------------------*/

static void die( char *fmt, ... )
{
	va_list	ap;
	va_start(ap,fmt);
	vfprintf(stderr,fmt,ap);
	fprintf(stderr,"\n");
	va_end(ap);
	exit(1);
}

static void die_fie( char *err, char *file, int line )
{
	fprintf(stderr,"forall: fatal internal error (%s) [%s/%d]\n",err,file,line);
	exit(2);
}

static void die_oom( int s, char *file, int line )
{
	fprintf(stderr,"forall: out of memory (claiming %d bytes) [%s/%d]\n",
																s,file,line);
	exit(2);
}

static void message( FILE *stream, char **m )
{
	while ( *m )
		fprintf(stream,"%s\n",*m++);
}


/*--------------------------------------------------------------------------*/
/* Queue / List manipulation stuff											*/
/*--------------------------------------------------------------------------*/

static void add_dir_to_queue(char *name)
{
	lnode *new_node;
	new_node = malloc(sizeof(lnode));
	if ( !new_node ) { OOM(sizeof(lnode)); }
	new_node->name = malloc(strlen(name)+1);
	if ( !new_node->name ) { OOM(strlen(name)+1); }
	strcpy(new_node->name,name);
	new_node->next = NULL;
	if ( !dir_queue_tail )
		dir_queue_head = new_node;
	else
		dir_queue_tail->next = new_node;
	dir_queue_tail = new_node;
}

/* NB: returned string was claimed with malloc and should be free'd */
/* by the user!														*/
static char *pop_dir_from_queue( void )
{
	lnode *n;
	char *s;
	if ( !dir_queue_head ) { return NULL; }
	n = dir_queue_head;
	dir_queue_head = dir_queue_head->next;
	if ( !dir_queue_head ) { dir_queue_tail = NULL; }
	s = n->name;
	free(n);
	return s;
}

static void add_file_to_queue(char *name)
{
	lnode *new_node;
	new_node = malloc(sizeof(lnode));
	if ( !new_node ) { OOM(sizeof(lnode)); }
	new_node->name = malloc(strlen(name)+1);
	if ( !new_node->name ) { OOM(strlen(name)+1); }
	strcpy(new_node->name,name);
	new_node->next = NULL;
	if ( !match_queue_tail )
		match_queue_head = new_node;
	else
		match_queue_tail->next = new_node;
	match_queue_tail = new_node;
}

/* NB: returned string was claimed with malloc and should be free'd */
/* by the user!														*/
static char *pop_file_from_queue( void )
{
	lnode *n;
	char *s;
	if ( !match_queue_head ) { return NULL; }
	n = match_queue_head;
	match_queue_head = match_queue_head->next;
	if ( !match_queue_head ) { match_queue_tail = NULL; }
	s = n->name;
	free(n);
	return s;
}






/*--------------------------------------------------------------------------*/
/* We implement a bit array as an array of unsigned ints					*/
/*--------------------------------------------------------------------------*/
static int READ_ARRAY_BIT(unsigned int *array, int address)
{
	return array[address>>5] & ( 1<<(address & 31) );
}

static void SET_ARRAY_BIT(unsigned int *array, int address, int v )
{
	array[address>>5] &= ~( 1<<(address & 31) );
	if (v) { array[address>>5] |= ( 1<<(address & 31) ); };
}



/*--------------------------------------------------------------------------*/
/* Miscellanea																*/
/*--------------------------------------------------------------------------*/


static void clear_types_array( unsigned int to )
{
	int i;
	to = to ? 0xffffffff : 0;
	for ( i=0 ; i<129 ; i++ )
		match_types[i] = to;
}


static void print_type_array(int w)
{
	int i,j,c;
	char name[16];
	os_error *e;
	c = 0;
	if ( match_all_types )
	{
		printf("all typed files");
		if ( !exclusion_list ) { putchar('\n'); return; }
		printf(" except:\n");
		for ( i=0; i<128 ; i++ )
		{
			if ( match_types[i] != 0xffffffff )
			{
				for ( j=0 ; j<32 ; j++ )
				{
					if ( !(match_types[i] & (1<<j)) )
					{
						e = FiletypeName( (i<<5) + j, name );
						if (e) { die("can't decode filetype %d",(i<<6)+j); }
						c += strlen(name)+1;
						if (c>w) { putchar('\n'); c=strlen(name)+1; }
						printf("%s ",name);
					}
				}
			}
		}
	}
	else
	{
		for ( i=0; i<128 ; i++ )
		{
			if ( match_types[i] )
			{
				for ( j=0 ; j<32 ; j++ )
				{
					if ( match_types[i] & (1<<j) )
					{
						e = FiletypeName( (i<<5) + j, name );
						if (e) { die("can't decode filetype %d",(i<<6)+j); }
						c += strlen(name)+1;
						if (c>w) { putchar('\n'); c=strlen(name)+1; }
						printf("%s ",name);
					}
				}
			}
		}
		if ( READ_ARRAY_BIT(match_types,0x1000) )
		{
			if (c) { putchar('\n'); c=0; }
			printf("Untyped files\n");
		}
	}
	if (c) { putchar('\n'); }
}



/* NB: 'b' is assumed to already be in lower case! */
static int lowcmp( char *a, char *b )
{
	while ( tolower(*a) == *b )
		if ( *a++ == 0 ) { return 0; } else { b++; }
	return tolower(*a) - *b;
}



/*--------------------------------------------------------------------------*/
/* Command line processing functions										*/
/*--------------------------------------------------------------------------*/

static option_value decode_option( char *option )
{
	option_value r;
	option_value f = 0;
	char **o = option_names;
	char *s,*t;
	int l = strlen(option)-1;
	if ( *option != '-' ) { return OPTN_UNKNOWN; }
	s = malloc(l+1); if (!s) { OOM(l+1); }
	for ( t = s; *option ; *t++ = tolower(*++option) )
		;
	for ( r = 0 ; *o ; r++, o++ )
		if ( !strncmp(s,*o,l) ) { if (!f) { f = r; } else { f = OPTN_AMBIG; } }
	free(s);
	return f;
}


static int filetype_from_name( char *type_name )
{
	os_error *e;
	int t;
	if ( !lowcmp(type_name,"untyped") ) { return 0x1000; }
	e = DecodeFiletype( type_name, &t );
	if (e) { die("unrecognised filetype '%s'",type_name); }
	return t;
}



static int process_type_args( int argc, char *argv[], int i )
{
	int type;
	char *name;
	for ( ; i<argc ; i++ )
	{
		name = argv[i];
		if ( decode_option(name) != OPTN_UNKNOWN ) { break; }
		if ( *name == '~' )
		{
			exclusion_list = 1;
			type = filetype_from_name( name+1 );
			SET_ARRAY_BIT(match_types,type,0);
		}
		else
		{
			type = filetype_from_name( name );
			if ( match_all_types ) { clear_types_array(0); match_all_types=0; }
			SET_ARRAY_BIT(match_types,type,1);
		}
	}
	return i-1;
}


static int process_command_args( int argc, char *argv[], int i )
{
	int j,l;
	char *d;
	l = 0;
	/* Find out how long the command spec is: */
	for ( j = i ; j<argc ; j++ )
		l += strlen(argv[j])+1;
	/* Claim memory for it */
	comm_spec = malloc( l+1 ); if ( !comm_spec ) { OOM(l+1); }
	/* Copy it */
	d = comm_spec;
	for ( j = i ; j<argc ; j++ )
	{
		strcpy(d,argv[j]);
		d += strlen(argv[j]);
		*d++ = ' ';
	}
	/* Correctly terminate */
	*(d-1) = 0;
	/* indicate that we've consumed all the arguments */
	return argc;
}


static void process_arguments( int argc, char *argv[] )
{
	int i;
	option_value o;

	clear_types_array(1);		/* match everything by default */
	SET_ARRAY_BIT(match_types,0x1000,0);	/* except untyped files */

	for ( i=1 ; i<argc ; i++ )
	{
		o = decode_option(argv[i]);
		switch (o)
		{
			/* Help / errors first: */
			case OPTN_UNKNOWN :
				fprintf(stderr,"forall: bad option '%s'\n",argv[i]);
				message(stderr,syntax_message);
				exit(1);
			case OPTN_AMBIG :
				fprintf(stderr,"forall: ambiguous option '%s'\n",argv[i]);
				message(stderr,syntax_message);
				exit(1);
			case OPTN_HELP :
				message(stdout,help_message);
				exit(1);

			/* Now we do the booleans */
			case OPTN_VERBOSE : verbose = 1; break;
			case OPTN_RECURSE : recurse = 1; break;
			case OPTN_NOEXECUTE : silent = execute = 0; break;
			case OPTN_SILENT : silent = 1; break;
			case OPTN_IMAGES : images = 1; break;
			case OPTN_IGNORE : ignore = 1; break;
			case OPTN_OSCLI  : oscli = 1; break;

			/* and now the specs */
			case OPTN_PATH :
			case OPTN_FILE :
			case OPTN_COMMAND :
			case OPTN_TYPE :
			case OPTN_OBEY :
				i++;
				break;

			/* Moron trap */
			default : FIE("Adny's a moron");
		}

		/* Okay, now we need to do extra processing if the option was a */
		/* spec (eg. -file)												*/
		/* First, check we haven't hit the end of the command line:		*/
		if ( i >= argc )
		{
			fprintf(stderr,"forall: missing argument: '%s'\n",argv[i-1]);
			message(stderr,syntax_message);
			exit(1);
		}
		/* Now, do any extra work required */
		switch (o)
		{
			case OPTN_PATH :
				if ( path_spec )
				{
					fprintf(stderr,"forall: only one -Path is allowed\n");
					message(stderr,syntax_message);
					exit(1);
				}
				path_spec = argv[i];
				break;
			case OPTN_FILE :
				if ( file_spec )
				{
					fprintf(stderr,"forall: only one -File is allowed\n");
					message(stderr,syntax_message);
					exit(1);
				}
				file_spec = argv[i];
				break;
			case OPTN_OBEY :
				if ( obey )
				{
					fprintf(stderr,"forall: only one -Obey is allowed\n");
					message(stderr,syntax_message);
					exit(1);
				}
				obey = argv[i];
				break;
			case OPTN_TYPE :
				i = process_type_args(argc,argv,i);
				break;
			case OPTN_COMMAND :
				i = process_command_args(argc,argv,i);
				break;
		}
	}
}






/*--------------------------------------------------------------------------*/
/* Building the match list													*/
/*--------------------------------------------------------------------------*/

static void scan_dir( char *dir )
{
	os_error *e;
	readdir_block rb;
	readdir_record *r;
	int i;

	/* Set up the block */
	rb.dir = dir;					/* directory to read */
	rb.buffer = (void*) buffer;		/* (buffer is global, remember?) */
	rb.read = BUFFER_SIZE / 32;		/* roughly how many we want to read */
	rb.buffer_size = BUFFER_SIZE;	/* 'nuff said */

	/* Check for subdirectories first */
	rb.start_pos = 0;				/* initial call */
	rb.obj_spec = 0;				/* ie. '*' */
	if ( recurse )
	{
		while ( rb.start_pos != -1 )
		{
			rb.read = BUFFER_SIZE / 32;		/* roughly how many to read */
			e = ReadDir( 1, &rb );
			if (e) { die("Error (scanning '%s') : %s",dir,e->errmess); }
			r = (readdir_record*) buffer;
			for ( i = rb.read ; i>0 ; i-- )
			{
				if ( (r->type == 2) || (images && (r->type==3)) )
				{
					sprintf(scrap,"%s.%s",dir,r->filename);
					add_dir_to_queue(scrap);
				}
				r = next_record(r);
			}
		}
	}

	/* Now check for matching files */
	rb.start_pos = 0;				/* initial call */
	rb.obj_spec = file_spec;				/* global, innit */
	while ( rb.start_pos != -1 )
	{
		rb.read = BUFFER_SIZE / 32;		/* roughly how many we want to read */
		e = ReadDir( 1, &rb );
		if (e) { die("Error (scanning '%s') : %s",dir,e->errmess); }
		r = (readdir_record*) buffer;
		for ( i = rb.read ; i>0 ; i-- )
		{
			if ( (r->type == 1) || (!images && (r->type==3)) )
			{
				if ( READ_ARRAY_BIT(match_types,FILETYPE(r->load)) )
				{
					sprintf(scrap,"%s.%s",dir,r->filename);
					add_file_to_queue(scrap);
				}
			}
			r = next_record(r);
		}
	}
}


static void scan( void )
{
	char *next;
	add_dir_to_queue( path_spec );
	next = pop_dir_from_queue();
	while (next)
	{
		scan_dir(next);
		free(next);
		next = pop_dir_from_queue();
	}
}


/*--------------------------------------------------------------------------*/
/* Gumph!																	*/
/*--------------------------------------------------------------------------*/

static int count_matches( void )
{
	int m;
	lnode *h = match_queue_head;
	for ( m = 0 ; h ; h = h->next )
		m++;
	return m;
}

static char *build_command( char *match )
{
	char *d = scrap;
	char *s = comm_spec;
	char *l = match + strlen(match) - 1;
	char *p;
	while ( l>match )
		if ( *l=='.' || *l==':' ) { break; } else { l--; }
	while ( *s )
	{
		if ( *s != '%' )
			*d++ = *s++;
		else
			switch ( *++s )
			{
				case '%' : *d++ = *s++; break;
				case 'o' : case 'O' :				/* full object name */
					p = match;
					while ( (*d++ = *p++) != NULL ) { ; }
					d--; s++;
					break;
				case 'p' : case 'P' :				/* path name */
					p = match;
					while ( p<l ) { *d++ = *p++; }
					s++;
					break;
				case 'f' : case 'F' :				/* leaf name */
					p = l;
					while ( (*d++ = *++p) != NULL ) { ; }
					d--; s++;
					break;
				case 'd' : case 'D' :				/* leaf of path */
					p = l-1;
					while ( p>=match )
						if ( *p=='.' || *p==':' ) { break; } else { p--; }
					while ( p<l ) { *d++ = *++p; }
					s++; d--;
					break;
				case 0 :
					die("Incomplete %% conversion");
				default :
					die("Bad %% conversion '%c'",*s);
			}
	}
	*d = 0;
	return scrap;
}



static void execute_commands( void )
{
	FILE *obey_fp;
	char *next;
	if ( obey )
	{
		obey_fp = fopen(obey,"wb");
		if ( !obey_fp ) { die("Can't create obey file '%s'",obey); }
	}
	next = pop_file_from_queue();
	while (next)
	{
		char *c = build_command( next );
		int e = 0;
		if ( !silent ) { printf("*%s\n",c); }
		if ( obey ) { fprintf(obey_fp,"%s\n",c); }
		if ( execute )
		{
			if ( oscli )
				e = _kernel_oscli(c)==-2;
			else
				e = _kernel_system(c,0)==-2;
		}
		if ( ignore ) { e = 0; }
		if (e)
		{
			if ( obey ) { fclose(obey_fp); SetType(obey,0xfeb); }
			die("Command failed (%d): '%s'",e,c);
		}
		free(next);
		next = pop_file_from_queue();
	}
	if ( obey ) { fclose(obey_fp); SetType(obey,0xfeb); }
}









/*--------------------------------------------------------------------------*/
/* Main																		*/
/*--------------------------------------------------------------------------*/



int main( int argc, char *argv[] )
{
	process_arguments(argc,argv);
	if ( !comm_spec || (silent && !execute && !obey) )
	{
		printf("Warning: forall command with no effect.\n");
		exit(0);
	}

	/* supply default args if necessary */
	if ( !path_spec ) { path_spec = "@"; }
	if ( !file_spec ) { file_spec = "*"; }

	/* Verify that the -command isn't dodgy */
	build_command("a.b");

	if ( verbose )
	{
		printf("forall '%s' on path '%s'\n",path_spec,file_spec);
		printf("Matching types:\n");
		print_type_array(80);
		printf("execute '%s'\n",comm_spec);
		printf("\nScanning path '%s' ... ",path_spec);
	}
	scan();
	if ( verbose )
	{
		printf("done.\n");
		printf("Matched %d files.\n",count_matches());
		if ( obey )
			printf("Building obey file ...\n");
		else
			printf("Executing ...\n");
	}
	execute_commands();
	if ( verbose )
		printf("done.\n");
}
