#!/usr/bin/perl

my @SOURCE_DIRS = (
	".",
	"crbond",
	"fourier",
	"output"
	);
my @SOURCE_FILTERS = (
	".cpp"
	);
my @PROJECT_FILE_FILTERS = (
	".cpp",
	".h"
	);
my $OBJ_DIR="../obj";
my $OUTPUT_DIR="../bin";
my $IHM_CACHE_DIR="ihm_cache";
my $OUTPUT_FILE="ScatterMatrix";

my @INCLUDE_DIRS = (
	".",
	"..",
	"../mtl_unix",
	"../confuse-2.6/src",
	"../../../root/include"
	);
my $CPPFLAGS="-c -O3 -DPLATFORM_LINUX -pthread";
my @LD_STATIC_LIBS = (
	"../../../root/lib/libconfuse.a",
	"../../../root/lib/libfftw3.a"
	);
my $LDFLAGS="-L/usr/lib -lblas -llapack -pthread";

my $OBJ_EXT=".o";
my $CPP="g++";
my $CC="gcc";
my $LD="g++";
my $F77="g77";

############# README
#
# ihm.ph - I Hate Makefiles
#
# This is a tool to automagically compilea set of sources. You merely
# specify the parameters above, and this script will figure out
# dependencies and modification times for you, and rebuild
# intelligently.
#
# Assumptions:
#   * All source files have different names, even if they are in
#     different directories.
#   * The only file dependencies come from #include's.
#   * Modification times can be used reliably.
#   * Any and all files matching the extension filters is a part of the
#     project.
#   * Dependencies not in the project directories or not matching the
#     filters never get changed. If they do, a rebuild is necessary.
#
# Version history:
#   2008-06-07 vkl Created.

############# Utility Functions

sub file_basename{
	my ($filename) = @_;
	$filename =~ s/^.*\///;
	return $filename;
}
sub mkdir_p{
	my ($dirname) = @_;
	my $dh;
	if(!opendir($dh, $OBJ_DIR)){ mkdir($OBJ_DIR); }else{ closedir($dh); }
}
sub echo_system{
	my ($cmdstr) = @_;
	print $cmdstr, "\n";
	system($cmdstr);
	return ($? >> 8);
}

############# Project file list

# Find all files that should be managed by the project
sub get_project_file_list{
	my @project_file_list;
	for $source_dir (@SOURCE_DIRS){
		for $filter (@PROJECT_FILE_FILTERS){
#			my $dh;
#			opendir $dh, $source_dir;
#			push @project_file_list, grep {/^[^\.]\S+$filter/} readdir($dh);
#			closedir $dh;
			
			my $file_list = `ls -1 $source_dir/*$filter`;
			push @project_file_list, split(/\s/, $file_list);
		}
	}
	return \@project_file_list;
}
sub write_project_file_list{
	my ($project_file_list) = @_;
	my $fp;
	open $fp, ">$IHM_CACHE_DIR/project_files.txt";
	for(@$project_file_list){
		print $fp "$_\n";
	}
	print $fp "# EOF\n";
	close $fp;
}
sub is_in_project_file_list{
	my ($project_file_list, $filename_without_dir) = @_;
	for(@$project_file_list){
		if(file_basename($_) eq $filename_without_dir){
			return $_;
		}
	}
	return '';
}

############# Source dependencies

sub file_includes{
	my ($filename) = @_;
	my @includes;
	my $fp;
	open $fp, $filename;
	while(<$fp>){
		if(/#\s*include\s+"(.+)"/){
			push @includes, $1;
		}elsif(/#\s*include\s+<(.+)>/){
			push @includes, $1;
		}
	}
	close $fp;
	return \@includes;
}


# start with explored = empty, unexplored = file
# function(file, explored, unexplored)
#   explored   += file
#   unexplored -= file
#   new_deps = deps of file not in explored
#   unexplored += new_deps
#   foreach(dep in unexplored){
#     function(dep, explored, unexplored)
#   }
#
sub find_file_deps_r{
	my ($filename, $explored, $unexplored, $project_file_list) = @_;
	
	#print "1 Looking at $filename, explored: ", keys(%$explored), " unexplored: ", keys(%$unexplored), "\n";
	
	delete $$unexplored{$filename};
	$$explored{$filename} = '';
	
	#print "2 Looking at $filename, explored: ", keys(%$explored), " unexplored: ", keys(%$unexplored), "\n";
	
	my $list_of_includes = file_includes($filename);
	#print "3 Found includes: ", @$list_of_includes, "\n";
	foreach(@$list_of_includes){
		$included_file_full_path = is_in_project_file_list($project_file_list, $_);
		if($included_file_full_path){
			if(!exists($$unexplored{$included_file_full_path}) && !exists($$explored{$included_file_full_path})){
				$$unexplored{$included_file_full_path} = '';
				#print "4 Adding includes: ", keys(%$unexplored), "\n";
			}
		}
	}
	foreach(keys(%$unexplored)){
		find_file_deps_r($_, $explored, $unexplored, $project_file_list);
	}
}

sub find_file_deps{
	my ($filename, $project_file_list) = @_;
	my %explored;
	my %unexplored;
	$unexplored{$filename} = '';
	find_file_deps_r($filename, \%explored, \%unexplored, $project_file_list);
	return keys(%explored);
}

sub get_source_deps{
	my ($project_file_list) = @_;
	my %source_dep_hash;
	for $DIR (@SOURCE_DIRS){
		for $filter (@SOURCE_FILTERS){
			my $str=`ls -1 $DIR/*$filter`;
			@SOURCE_DIR_FILES = split /\s/, $str;
#			my $dh;
#			opendir $dh, $DIR;
#			my @SOURCE_DIR_FILES = grep {/^[^\.]\S+$filter/} readdir($dh);
#			closedir $dh;
			
			for $source_file (@SOURCE_DIR_FILES){
				my @SOURCE_FILE_DEPS = find_file_deps($source_file, $project_file_list);
				$source_dep_hash{$source_file} = \@SOURCE_FILE_DEPS;
			}
		}
	}
	return \%source_dep_hash;
}
sub write_source_dep_hash{
	my ($source_dep_hash) = @_;
	my $fp;
	open $fp, ">$IHM_CACHE_DIR/source_deps.txt";
	for(sort keys(%$source_dep_hash)){
		print $fp "$_: ", join(' ', sort(@{${$source_dep_hash}{$_}})), "\n";
	}
	print $fp "# EOF\n";
	close $fp;
}

############# File modification time

# returns a pair (exists?, mtime)
# exists is 0 or 1 (indicates if mtime could be found or not)
sub get_file_mtime{
	my ($filename) = @_;
	my @ret;
	
	my $fh;
	if(open($fh, "$filename")){
		@stat_entries = stat($fh);
		close $fh;
		if(() != @stat_entries){
			$ret[0] = 1;
			$ret[1] = $stat_entries[9];
		}else{
			$ret[0] = 0;
			$ret[1] = time();
		}
	}else{
		$ret[0] = 0;
		$ret[1] = time();
	}
	
	return @ret;
}

# returns a pair (exists?, mtime)
# exists is 0 or 1 (indicates if all mtimes could be found or not)
# This function really should never return 0 in the first slot
sub get_latest_dep_mtime{
	my ($filename, $source_dep_hash) = @_;
	my @ret = (1, 0);
	
	if(!exists(${$source_dep_hash}{$filename})){
		$ret[0] = 0;
		$ret[1] = time();
	}else{
		for $dep_file (@{${$source_dep_hash}{$filename}}){
			my @cur_dep_mtime = get_file_mtime($dep_file);
			if(0 == $cur_dep_mtime[0]){
				$ret[0] = 0;
				$ret[1] = time();
				last;
			}else{
				if($ret[1] < $cur_dep_mtime[1]){
					$ret[1] = $cur_dep_mtime[1];
				}
			}
		}
	}
	
	return @ret;
}

############# Building/linking files

sub build_file{
	my ($source_file, $obj_file) = @_;
	my $cmdstr = "$CPP $source_file -o $obj_file $CPPFLAGS " . join(' ', map("-I$_", @INCLUDE_DIRS));
	#print "CPP $source_file\n";
	my $ret = echo_system($cmdstr);
	if(0 != $ret){
		exit $ret;
	}
}
		
sub link_files{
	my ($obj_file_list) = @_;
	my $cmdstr = "$LD " . join(' ', @$obj_file_list) . " " . join(' ', @LD_STATIC_LIBS) . " $LDFLAGS -o $OUTPUT_DIR/$OUTPUT_FILE";
	#print "LD  $OUTPUT_DIR/$OUTPUT_FILE\n";
	my $ret = echo_system($cmdstr);
	if(0 != $ret){
		exit $ret;
	}
}

############# Core routine

if($ARGV[0] eq 'clean'){
	echo_system("rm -f $OBJ_DIR/*$OBJ_EXT");
	echo_system("rm -f $OUTPUT_DIR/$OUTPUT_FILE");
	exit(0);
}

mkdir_p($OBJ_DIR);
mkdir_p($OUTPUT_DIR);
mkdir_p($IHM_CACHE_DIR);
	
my $project_file_list = get_project_file_list();
#write_project_file_list($project_file_list);

my $source_dep_hash = get_source_deps($project_file_list);
#write_source_dep_hash($source_dep_hash);

# Naive algorithm: for each source file, rebuild it if any of its deps have modification time after its obj

$needed_rebuild = 0;
my @obj_file_list;
for $source_file (keys(%$source_dep_hash)){
	my $cur_file_needs_rebuild = 0;

	my $obj_file = "$OBJ_DIR/".file_basename($source_file);
	$obj_file =~ s/.[^\.]+$/$OBJ_EXT/;
	
	my ($obj_file_exists, $obj_mtime) = get_file_mtime($obj_file);
	my ($all_deps_exist, $latest_dep_mtime) = get_latest_dep_mtime($source_file, $source_dep_hash);
	if(!$all_deps_exist){
		die("Not all dependency files could be stat'd for mtime.\n");
	}
	
	if(!($obj_file_exists) || ($latest_dep_mtime > $obj_mtime)){
		build_file($source_file, $obj_file);
		$needed_rebuild = 1;
	}
	
	push @obj_file_list, $obj_file;
}

if($needed_rebuild){
	link_files(\@obj_file_list);
}else{
	if((stat($0))[9] > (stat("$OUTPUT_DIR/$OUTPUT_FILE"))[9]){
		link_files(\@obj_file_list);
	}
}
