#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include "verilog_preprocessor.h"
#include "types.h"

/* Globals */
struct veri_Includes veri_includes;
struct veri_Defines veri_defines;

/*
 * Initialize the preprocessor by allocating sufficient memory and setting sane values
 */
int init_veri_preproc()
{
	veri_includes.included_files = (veri_include **) calloc(DefaultSize, sizeof(veri_include *));
	if (veri_includes.included_files == NULL) 
	{
		perror("veri_includes.included_files : calloc ");
		return -1;
	}
	veri_includes.current_size = DefaultSize;
	veri_includes.current_index = 0;
	
	veri_defines.defined_constants = (veri_define **) calloc(DefaultSize, sizeof(veri_define *));
	if (veri_defines.defined_constants == NULL) 
	{
		perror("veri_defines.defined_constants : calloc ");
		return -1;
	}
	veri_defines.current_size = DefaultSize;
	veri_defines.current_index = 0;
	return 0;
}

/*
 * Cleanup allocated memory
 */
int cleanup_veri_preproc() 
{
	//fprintf(stderr, "Cleaning up the verilog preprocessor\n");
	
	veri_define *def_iterator = veri_defines.defined_constants[0];
	veri_include *inc_iterator = veri_includes.included_files[0];
	int i;
	
	for (i = 0; i < veri_defines.current_index && i < veri_defines.current_size; def_iterator = veri_defines.defined_constants[++i]) 
	{
		clean_veri_define(def_iterator);
	}
	def_iterator = NULL;
	veri_defines.current_index = 0;
	veri_defines.current_size = 0;
	free(veri_defines.defined_constants);

	for (i = 0; i < veri_includes.current_index && i < veri_includes.current_size; inc_iterator = veri_includes.included_files[++i]) 
	{
		clean_veri_include(inc_iterator);
	}
	inc_iterator = NULL;
	veri_includes.current_index = 0;
	veri_includes.current_size = 0;
	free(veri_includes.included_files);
	
	//fprintf(stderr, " --- Finished\n");

	return 0;
}

/*
 * Free memory for a symbol in the define table
 */
void clean_veri_define(veri_define *current) 
{
	if (current != NULL) 
	{
		//fprintf(stderr, "\tCleaning Symbol: %s, ", current->symbol);
		free(current->symbol);
		//fprintf(stderr, "Value: %s ", current->value);
		free(current->value);
		
		current->defined_in = NULL;
		
		free(current);
		current=NULL;
		//fprintf(stderr, "...done\n");
	}
}

/*
 * Free memory for a symbol in the include table 
 */
void clean_veri_include(veri_include *current) 
{
	if (current != NULL)
	{
		//fprintf(stderr, "\tCleaning Include: %s ", current->path);
		free(current->path);
		
		free(current);
		current = NULL;
		//fprintf(stderr, "...done\n");
	}
}

/* 
 * add_veri_define returns a non negative value on success, a -1 if creation of the define failed 
 * due to a lack of memory and -2 if the symbol was previously defined and the values conflict
 */
int add_veri_define(char *symbol, char *value, int line, veri_include *defined_in) 
{
	int i;
	veri_define *def_iterator = veri_defines.defined_constants[0];
	veri_define *new_def = (veri_define *)malloc(sizeof(veri_define));
	if (new_def == NULL) 
	{
		perror("new_def : malloc ");
		return -1;
	}
	
	/* Check to see if there's enough space in our lookup table and reallocate if not. */
	if (veri_defines.current_index == veri_defines.current_size) 
	{
		veri_defines.defined_constants = (veri_define **)realloc(veri_defines.defined_constants, (size_t)(veri_defines.current_size * 2) * sizeof(veri_define *));
		//In a perfect world there is a check here to make sure realloc succeded
		veri_defines.current_size *= 2; 
	}
	
	/* Check previously defined values for collisions. */
	for (i = 0; i < veri_defines.current_index && i < veri_defines.current_size; def_iterator = veri_defines.defined_constants[++i]) 
	{
		if (0 == strcmp(def_iterator->symbol, symbol)) 
		{
			fprintf(stderr, "Warning: The constant %s defined on line %d in %s was previously defined on line %d in %s\n",
				symbol, line, defined_in->path, def_iterator->line, def_iterator->defined_in->path);
			
			if (value == NULL || (value[0] == '/' && value[1] == '/'))
#ifndef BLOCK_EMPTY_DEFINES
			{
				fprintf(stderr, "\tWarning: The new value of %s is empty\n\n", symbol);	
				free(def_iterator->value);
				def_iterator->value =NULL;
			}
#else		
			{
				fprintf(stderr, "\tWarning: The new value of %s is empty, doing nothing\n\n", symbol);	
				return 0;
			}
#endif
			else if (0 != strcmp(def_iterator->value, value))
			{
				fprintf(stderr, "\tWarning: The value of %s has been redefined to %s, the prvious value was %s\n\n",
					symbol, value, def_iterator->value);
				free(def_iterator->value);
				def_iterator->value = (char *)strdup(value);
			}

			free(new_def);
			return 0;
		}
	}
	
	/* Create the new define and initalize it. */
	new_def->symbol = (char *)strdup(symbol);
	new_def->value = (value == NULL)? NULL : (char *)strdup(value);
	new_def->line = line;
	new_def->defined_in = defined_in;
	
	veri_defines.defined_constants[veri_defines.current_index] = new_def;
	veri_defines.current_index++;
	
	return 0;
}

/* add_veri_include shall return NULL if it is unable to create a new 
 * veri_include in the lookup table or an entry for that file already exists.
 * Otherwise it wil return a pointer to the new veri_include entry.
 */
veri_include* add_veri_include(char *path, int line, veri_include *included_from) 
{
	int i;
	veri_include *inc_iterator = veri_includes.included_files[0];
	veri_include *new_inc = (veri_include *)malloc(sizeof(veri_include));
	if (new_inc == NULL) 
	{
		perror("new_inc : malloc ");
		return NULL;
	}

	/* Check to see if there's enough space in our lookup table and reallocate if not. */
	if (veri_includes.current_index == veri_includes.current_size) 
	{
		veri_includes.included_files = (veri_include **)realloc(veri_includes.included_files, (size_t)(veri_includes.current_size * 2) * sizeof(veri_include *));
		//In a perfect world there is a check here to make sure realloc succeded
		veri_includes.current_size *= 2; 
	}
	
	/* Scan previous includes to make sure the file wasn't included previously. */
	for (i = 0; i < veri_includes.current_index && i < veri_includes.current_size && inc_iterator != NULL; inc_iterator = veri_includes.included_files[++i]) 
	{
		if (0 == strcmp(path, inc_iterator->path))
		{
			free(new_inc);
			return NULL;
		}
	}
	
	new_inc->path = (char *)strdup(path);
	new_inc->included_from = included_from;
	new_inc->line = line;
	
	veri_includes.included_files[veri_includes.current_index] = new_inc;
	veri_includes.current_index++;
	
	return new_inc;
}

/*
 * Retrieve the value associated, if any, with the given symbol. If the symbol is not present or no
 * value is associated with the symbol then NULL is returned.
 */
char* ret_veri_definedval(char *symbol)
{
	int is_defined = veri_is_defined(symbol);
	if(0 <= is_defined)
	{
		return veri_defines.defined_constants[is_defined]->value;
	}
	return NULL;
	
}

/*
 * Returns a non-negative integer if the symbol has been previously defined.
 */
int veri_is_defined(char * symbol)
{
	int i;
	veri_define *def_iterator = veri_defines.defined_constants[0];
	
	for (i = 0; (i < veri_defines.current_index) && (i < veri_defines.current_size) && (def_iterator != NULL); def_iterator = veri_defines.defined_constants[++i])
	{
		if (0 == strcmp(symbol, def_iterator->symbol))
		{
			return i;
		}
	}
	return -1;
}

/*
 * Return an open file handle
 * if the file is not in the pwd try the paths indicated by char* global_args.verilog_file and/or int current_parse_file
 *
 * Return NULL if unable to find and open the file
 */
FILE* open_source_file(char* filename)
{
	extern global_args_t global_args;
	extern config_t configuration;
	extern int current_parse_file;
	
	FILE* src_file = fopen(filename, "r"); //Look for the file in the PWD
	if (src_file != NULL)
	{
		return src_file;
	}
	
	char* path;
	if (global_args.verilog_file != NULL) //ODIN_II was called with the -V option.
	{
		path = (char *) strdup(global_args.verilog_file);
	}
	else if(global_args.config_file != NULL) //ODIN_II was called with the -c option.
	{
		path = (char *) strdup(configuration.list_of_file_names[current_parse_file]);
	}
	else
	{
		path = NULL;
		fprintf(stderr, "Invalid state in open_source_file.");
	}
	
	char* last_slash = strrchr(path, '/') + 1;
	*last_slash = '\0';
	strcat(path, filename);
	
	src_file = fopen(path, "r");
	if (src_file != NULL)
	{
		fprintf(stderr, "Warning: Unable to find %s in the present working directory, opening %s instead\n", 
				filename, path);
		free(path);
		return src_file;
	}
	
	return NULL;
}

/*
 * Bootstraps our preprocessor
 */
FILE* veri_preproc(FILE *source)
{
	extern global_args_t global_args;
	extern config_t configuration;
	extern int current_parse_file;
	FILE *preproc_producer = NULL;
	
	/* Was going to use filename to prevent duplication but the global var isn't used in the case of a config value */
	char* current_file = (global_args.verilog_file != NULL) ? global_args.verilog_file : configuration.list_of_file_names[current_parse_file];
	veri_include *veri_initial = add_veri_include(current_file, 0, NULL);
	if (veri_initial == NULL)
	{
		fprintf(stderr, "Unable to store include information returning original FILE pointer\n\n");
		return source;
	}

	preproc_producer = tmpfile();
	preproc_producer = freopen(NULL, "r+", preproc_producer);

	if (preproc_producer == NULL) 
	{
		perror("preproc_producer : fdopen - returning original FILE pointer");
		exit(-1);
		return source;
	}
	
	/* to thread or not to thread, that is the question. Wether yac will block when waitin */
	fprintf(stderr, "Preprocessing verilog.\n");

	veri_preproc_bootstraped(source, preproc_producer, veri_initial);
	rewind(preproc_producer);	
	return preproc_producer;
}

/*
 * Returns a new temporary file with the comments removed.
 * Preserves the line numbers by keeping newlines in place.
 */
FILE *remove_comments(FILE *source)
{
	FILE *destination = tmpfile();
	destination = freopen(NULL, "r+", destination);
	rewind(source);

	char line[MaxLine];
	int in_multiline_comment = FALSE;
	while (fgets(line, MaxLine, source))
	{
		int i;
		for (i = 0; i < strlen(line); i++)
		{
			if (!in_multiline_comment)
			{
				// For a single line comment, skip the rest of the line.
				if (line[i] == '/' && line[i+1] == '/')
				{
					break;
				}
				// For a multi-line comment, set the flag and skip over the *.
				else if (line[i] == '/' && line[i+1] == '*')
				{
					i++; // Skip the *.
					in_multiline_comment = TRUE;
				}
				else
				{
					if (line[i] != '\n')
						fputc(line[i], destination);
				}
			}
			else
			{
				// If we're in a multi-line comment, search for the */
				if (line[i] == '*' && line[i+1] == '/')
				{
					i++; // Skip the /
					in_multiline_comment = FALSE;
				}
			}
		}
		fputc('\n', destination);
	}
	rewind(destination);
	return destination;
}

void veri_preproc_bootstraped(FILE *original_source, FILE *preproc_producer, veri_include *current_include)
{
	// Strip the comments from the source file producing a temporary source file.
	FILE *source = remove_comments(original_source);

	int line_number = 1;
	veri_flag_stack *skip = (veri_flag_stack *)calloc(1, sizeof(veri_flag_stack));;
	char line[MaxLine];
	char *token;
	FILE *included_file = NULL;
	veri_include *new_include = NULL;

	while (NULL != fgets(line, MaxLine, source))
	{
		//fprintf(stderr, "%s:%d\t%s", current_include->path,line_number, line);
		char proc_line[MaxLine] ;
		char symbol[MaxLine] ;
		char *value ;
		char *p_proc_line = proc_line ;
		char *last_pch, *pch, *pch_end ;
		// advance past all whitespace
		last_pch = trim(line) ;
		// start searching for backtick
		pch = strchr( last_pch, '`' ) ;
		while ( pch ) {
			// if symbol found, copy everything from end of last_pch to here
			strncpy( p_proc_line, last_pch, pch - last_pch ) ;
			p_proc_line += pch - last_pch ;
			*p_proc_line = '\0' ;
			// find the end of the symbol
			pch_end = pch+1 ;
			while ( ( *pch_end >= '0' && *pch_end <= '9' ) ||
				( *pch_end >= 'A' && *pch_end <= 'Z' ) ||
				( *pch_end >= 'a' && *pch_end <= 'z' ) || 
				*pch_end == '_' )
				pch_end++ ;
			// copy symbol into array
			strncpy( symbol, pch+1, pch_end - (pch+1) ) ;
			*(symbol + (pch_end - (pch+1))) = '\0' ;
			value = ret_veri_definedval( symbol ) ;
			if ( value ) {
				strcpy( p_proc_line, value ) ;
				p_proc_line += strlen( value ) ;
			}
			else {
				// symbol not found, just pass it through
				*p_proc_line++ = '`' ;
				strcpy( p_proc_line, symbol ) ;
				p_proc_line += strlen( symbol ) ;
			}
			last_pch = pch_end ;
			pch = strchr( last_pch+1, '`' ) ;
		}
		pch = strchr( last_pch, '\0' ) ;
		strncpy( p_proc_line, last_pch, pch - last_pch ) ;
		p_proc_line += pch - last_pch ;
		*p_proc_line = '\0' ;

		strcpy( line, proc_line ) ;
		
		//fprintf(stderr, "%s:%d\t%s\n", current_include->path,line_number, line);

		/* Preprocessor directives have a backtick on the first coloumn. */
		if (line[0] == '`') 
		{
			token = trim((char *)strtok(line, " \t"));
			//printf("preproc first token: %s\n", token);
			/* If we encounter an `included directive we want to recurse using included_file and 
			 * new_include in place of source and current_include
			 */
			if (top(skip) < 1 && strcmp(token, "`include") == 0)
			{
				token = trim((char *)strtok(NULL, "\""));
				
				included_file = open_source_file(token);

				/* If we failed to open the included file handle the error */
				if (included_file == NULL)
				{			
					fprintf(stderr, "Warning: Unable to open file %s included on line %d of %s\n", 
						token, line_number, current_include->path);
					perror("included_file : fopen");
					/*return erro or exit ? */
				} 
				else if (NULL != (new_include = add_veri_include(token, line_number, current_include)))
				{
					veri_preproc_bootstraped(included_file, preproc_producer, new_include);
				}
				fclose(included_file);
				/* If last included file has no newline an error could result so we add one. */
				fputc('\n', preproc_producer);
			} 
			/* If we encounter a `define directive we want to add it and its value if any to our
			 * symbol table.
			 */
			else if (top(skip) < 1 && strcmp(token, "`define") == 0)
			{
				char *value = NULL;
				
				/* strtok is destructive to the original string which we need to retain unchanged, this fixes it. */
				fprintf(preproc_producer, "`define %s\n", line + 1 + strlen(line));
				//printf("\tIn define: %s", token + 1 + strlen(token));
				
				token = trim(strtok(NULL, " \t"));
				//printf("token is: %s\n", token);

				// symbol value can potentially be to the end of the line!
				value = trim(strtok(NULL, "\r\n"));
				//printf("value is: %s\n", value);

				if ( value ) {
					// trim it again just in case
					value = trim(value);
				}
				add_veri_define(token, value, line_number, current_include);
			}
			/* If we encounter a `undef preprocessor directive we want to remove the corresponding
			 * symbol from our lookup table.
			 */
			else if (top(skip) < 1 && strcmp(token, "`undef") == 0)
			{
				int is_defined = 0;
				/* strtok is destructive to the original string which we need to retain unchanged, this fixes it. */
				fprintf(preproc_producer, "`undef %s", line + 1 + strlen(line));
				
				token = trim(strtok(NULL, " \t"));
				
				is_defined = veri_is_defined(token);
			
				if(is_defined >= 0)
				{
					clean_veri_define(veri_defines.defined_constants[is_defined]);
					veri_defines.defined_constants[is_defined] = veri_defines.defined_constants[veri_defines.current_index];
					veri_defines.defined_constants[veri_defines.current_index--] = NULL;
				}
			}
			else if (strcmp(token, "`ifdef") == 0)
			{
				// if parent is not skipped
				if ( top(skip) < 1 ) {
					int is_defined = 0;
					
					token = trim(strtok(NULL, " \t"));
					is_defined = veri_is_defined(token);
					if(is_defined < 0) //If we are unable to locate the symbol in the table
					{
						push(skip, 1);
					}
					else
					{
						push(skip, 0);
					}
				} 
				// otherwise inherit skip from parent (use 2)
				else {
					push( skip, 2 ) ;
				}
			}
			else if (strcmp(token, "`ifndef") == 0)
			{
				// if parent is not skipped
				if ( top(skip) < 1 ) {
					int is_defined = 0;
					
					token = trim(strtok(NULL, " \t"));
					is_defined = veri_is_defined(token);
					if(is_defined >= 0) //If we are able to locate the symbol in the table
					{
						push(skip, 1);
					}
					else
					{
						push(skip, 0);
					}
				}
				// otherwise inherit skip from parent (use 2)
				else {
					push( skip, 2 ) ;
				}
			}
			else if (strcmp(token, "`else") == 0)
			{
				// if skip was 0 (prev. ifdef was 1) 
				if(top(skip) < 1)
				{
					// then set to 0
					pop(skip) ;
					push(skip, 1);
				} 
				// only when prev skip was 1 do we set to 0 now
				else if (top(skip) == 1)
				{
					pop(skip) ;
					push(skip, 0);
				}
				// but if it's 2 (parent ifdef is 1)
				else {
					// then do nothing 
				}
			}
			else if (strcmp(token, "`endif") == 0)
			{
				pop(skip);
			}
			/* Leave unhandled preprcessor directives in place. */
			else if (top(skip) < 1)
			{
				fprintf(preproc_producer, "%s %s\n", line, line + 1 + strlen(line));
			}
		}
		else if(top(skip) < 1)
		{
			if (fprintf(preproc_producer, "%s\n", line) < 0)//fputs(line, preproc_producer))
			{
				/* There was an error writing to the stream */
			}
		}
		line_number++;
		token = NULL;
	}
	fclose(source);
	free(skip);
}

/* General Utility methods ------------------------------------------------- */

char* trim(char *string)
{
	int i = 0;
	if (string != NULL)
	{
		// advance past all spaces at the beginning
		while( isspace( *string ) ) string++ ;
		// trim all spaces at the end
		for(i = strlen(string)-1; i >= 0 ; i--)
		{
			if(isspace(string[i]) > 0)
			{
				string[i] = '\0';
			}
			else
				break ;
		}
	}
	return string;
}

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



/* stack methods ------------------------------------------------------------*/

int top(veri_flag_stack *stack)
{
	if(stack != NULL && stack->top != NULL)
	{
		return stack->top->flag;
	}
	return 0;

}

int pop(veri_flag_stack *stack)
{
	if(stack != NULL && stack->top != NULL)
	{
		veri_flag_node *top = stack->top;
		int flag = top->flag;
		
		stack->top = top->next;
		free(top);
	
		return flag;
	}
	return 0;
}
void push(veri_flag_stack *stack, int flag)
{
	if(stack != NULL)
	{
		veri_flag_node *new_node = (veri_flag_node *)malloc(sizeof(veri_flag_node));
		new_node->next = stack->top;
		new_node->flag = flag;
		
		stack->top = new_node;
	}
}

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


