#!/usr/bin/env perl

# Generate dependencies in a form suitable for inclusion into a Makefile.
# The source filenames are provided in a file, one per line.  Directories
# to be searched for the source files and for their dependencies are provided
# in another file, one per line.  Output is written to STDOUT.
#
# For CPP type dependencies (lines beginning with #include), or for Fortran
# include dependencies, the dependency search is recursive.  Only
# dependencies that are found in the specified directories are included.
# So, for example, the standard include file stdio.h would not be included
# as a dependency unless /usr/include were one of the specified directories
# to be searched.
#
# For Fortran module USE dependencies (lines beginning with a case
# insensitive "USE", possibly preceded by whitespace) the Fortran compiler
# must be able to access the .mod file associated with the .o file that
# contains the module.  In order to correctly generate these dependencies
# the following restriction must be observed.
#
# ** All modules that are to be contained in the dependency list must be
# ** contained in one of the source files in the list provided on the command
# ** line.
#
# The reason for this restriction is that the modules have a nominal dependence
# on the .o files. If a module is being used for which the source code is not
# available (e.g., a module from a library), then adding a .o dependency for
# that module is a mistake because make will attempt to build that .o file, and
# will fail if the source code is not available.
#
# Original version: B. Eaton
#                   Climate Modelling Section, NCAR
#                   Feb 2001
#
# ChangeLog:
# -----------------------------------------------------------------------------
# Modifications to Brian Eaton's original to relax the restrictions on 
# source file name matching module name and only one module per source 
# file.  Also added a new "-d depfile" option which allows an additional
# file to be added to every dependence.
#
#
#   Tom Henderson
#   Global Systems Division, NOAA/OAR
#   Mar 2011
# -----------------------------------------------------------------------------
# Several updates:
#
#  - Remove limitation that modules cannot be named "procedure".
#
#  - Allow optional "::" in use statement (Fortran 2003).
#
#  - Instead of having .o files depend on other .o files directly,
#    have them depend indirectly through the .mod files. This allows
#    the compiler to have discretion over whether to update a .mod,
#    and prevents cascading recompilation when it does not.
#
#
#   Sean Santos
#   CESM Software Engineering Group, NCAR
#   Mar 2013
# -----------------------------------------------------------------------------
# More updates:
#
#  - Restore ability to recognize .mod files in the path, if there's no source
#    file that provides the same module.
#
#  - Allow "non_intrinsic" keyword (Fortran 2003).
#
#   Sean Santos
#   CESM Software Engineering Group, NCAR
#   Mar 2013
# -----------------------------------------------------------------------------


use Getopt::Std;
use File::Basename;

# Check for usage request.
@ARGV >= 2                          or usage();

# Process command line.
my %opt = ();
getopts( "t:wd:m:", \%opt )        or usage();
my $filepath_arg = shift()        or usage();
my $srcfile_arg = shift()         or usage();
@ARGV == 0                        or usage();  # Check that all args were processed.

my $obj_dir = "";
if ( defined $opt{'t'} ) { $obj_dir = $opt{'t'}."/"; }

my $additional_file = "";
if ( defined $opt{'d'} ) { $additional_file = $opt{'d'}; }

my $mangle_scheme = "lower";
if ( defined $opt{'m'} ) { $mangle_scheme = $opt{'m'}; }

open(FILEPATH, $filepath_arg) or die "Can't open $filepath_arg: $!\n";
open(SRCFILES, $srcfile_arg) or die "Can't open $srcfile_arg: $!\n";

# Make list of paths to use when looking for files.
# Prepend "." so search starts in current directory.  This default is for
# consistency with the way GNU Make searches for dependencies.
my @file_paths = <FILEPATH>;
close(FILEPATH);
chomp @file_paths;
unshift(@file_paths,'.');
foreach $dir (@file_paths) {  # (could check that directories exist here)
    $dir =~ s!/?\s*$!!;  # remove / and any whitespace at end of directory name
    ($dir) = glob $dir;  # Expand tildes in path names.
}

# Make list of files containing source code.
my @src = <SRCFILES>;
close(SRCFILES);
chomp @src;

my %module_files = ();

# Attempt to parse each file for /^\s*module/ and extract module names 
# for each file.  
my ($f, $name, $path, $suffix, $mod);
my @suffixes = ('\.[fF]90', '\.[fF]','\.F90\.in' );
foreach $f (@src) {
    ($name, $path, $suffix) = fileparse($f, @suffixes);
    # find the file in the list of directorys (in @file_paths)
    my $file_path = find_file($f);
    open(FH, $file_path)  or die "Can't open $file_path: $!\n";
    while ( <FH> ) {
	# Search for module definitions.
	if ( /^\s*MODULE\s+(\w+)\s*(\!.*)?$/i ) {
	    ($mod = $1) =~ tr/A-Z/a-z/;
            if ( defined $module_files{$mod} ) { 
                die "Duplicate definitions of module $mod in $module_files{$mod} and $name: $!\n";
            }
            $module_files{$mod} = $name;
	}
    }
    close( FH );
}

# Now make a list of .mod files in the file_paths.  If a source dependency
# can't be found based on the module_files list above, then maybe a .mod
# module dependency can if the mod file is visible.  
my %trumod_files = ();
my ($dir);
my ($f, $name, $path, $suffix, $mod);
# This might not be clear: we want to mangle a "\" so that it will escape
# the "." in .mod or .MOD
my @suffixes = (mangle_modfile("\\"));
foreach $dir (@file_paths) {
    # Similarly, this gets us $dir/*.mod or $dir/*.MOD
    @filenames = (glob("$dir/".mangle_modfile("*")));
    foreach $f (@filenames) {
       ($name, $path, $suffix) = fileparse($f, @suffixes);
       ($mod = $name) =~ tr/A-Z/a-z/;
       $trumod_files{$mod} = $name;
    }
}

#print STDERR "\%module_files\n";
#while ( ($k,$v) = each %module_files ) {
#    print STDERR "$k => $v\n";
#}

# Find module and include dependencies of the source files.
my ($file_path, $rmods, $rincs);
my %file_modules = ();
my %file_includes = ();
my @check_includes = ();
my %modules_used = ();
foreach $f ( @src ) {

    # Find the file in the seach path (@file_paths).
    unless ($file_path = find_file($f)) {
	if (defined $opt{'w'}) {print STDERR "$f not found\n";}
	next;
    }

    # Find the module and include dependencies.
    ($rmods, $rincs) = find_dependencies( $file_path );

    # Remove redundancies (a file can contain multiple procedures that have
    # the same dependencies).
    $file_modules{$f} = rm_duplicates($rmods);
    $file_includes{$f} = rm_duplicates($rincs);

    # Make a list of all include files.
    push @check_includes, @{$file_includes{$f}};
}

#print STDERR "\%file_modules\n";
#while ( ($k,$v) = each %file_modules ) {
#    print STDERR "$k => @$v\n";
#}
#print STDERR "\%file_includes\n";
#while ( ($k,$v) = each %file_includes ) {
#    print STDERR "$k => @$v\n";
#}
#print STDERR "\@check_includes\n";
#print STDERR "@check_includes\n";

# Find include file dependencies.
my %include_depends = ();
while (@check_includes) {
    $f = shift @check_includes;
    if (defined($include_depends{$f})) { next; }

    # Mark files not in path so they can be removed from the dependency list.
    unless ($file_path = find_file($f)) {
	$include_depends{$f} = -1;
	next;
    }

    # Find include file dependencies.
    ($rmods, $include_depends{$f}) = find_dependencies($file_path);

    # Add included include files to the back of the check_includes list so
    # that their dependencies can be found.
    push @check_includes, @{$include_depends{$f}};

    # Add included modules to the include_depends list.
    if ( @$rmods ) { push @{$include_depends{$f}}, @$rmods;  }
}

#print STDERR "\%include_depends\n";
#while ( ($k,$v) = each %include_depends ) {
#    print STDERR (ref $v ? "$k => @$v\n" : "$k => $v\n");
#}

# Remove include file dependencies that are not in the Filepath.
my $i, $ii;
foreach $f (keys %include_depends) {

    unless (ref $include_depends{$f}) { next; }
    $rincs = $include_depends{$f};
    unless (@$rincs) { next; }
    $ii = 0;
    $num_incs = @$rincs;
    for ($i = 0; $i < $num_incs; ++$i) {
    	if ($include_depends{$$rincs[$ii]} == -1) {
	    splice @$rincs, $ii, 1;
	    next;
	}
    ++$ii;
    }
}

# Substitute the include file dependencies into the %file_includes lists.
foreach $f (keys %file_includes) {
    my @expand_incs = ();

    # Initialize the expanded %file_includes list.
    my $i;
    unless (@{$file_includes{$f}}) { next; }
    foreach $i (@{$file_includes{$f}}) {
	push @expand_incs, $i  unless ($include_depends{$i} == -1);
    }
    unless (@expand_incs) {
	$file_includes{$f} = [];
	next;
    }

    # Expand
    for ($i = 0; $i <= $#expand_incs; ++$i) {
	push @expand_incs, @{ $include_depends{$expand_incs[$i]} };
    }

    $file_includes{$f} = rm_duplicates(\@expand_incs);
}

#print STDERR "expanded \%file_includes\n";
#while ( ($k,$v) = each %file_includes ) {
#    print STDERR "$k => @$v\n";
#}

# Print dependencies to STDOUT.

print "# Declare all module files used to build each object.\n";

foreach $f (sort keys %file_modules) {
    my $file;
    if($f =~ /\.F90\.in$/){
	$f =~ /(.+)\.F90\.in/;
	$file = $1;
    }else{
	$f =~ /(.+)\./;
	$file = $1;
    }
    $target = $obj_dir."$file.o";
    print "$target : @{$file_modules{$f}} @{$file_includes{$f}} $additional_file \n";
}

print "# The following section relates each module to the corresponding file.\n";
$target = mangle_modfile("%");
print "$target : \n";
print "\t\@\:\n";

foreach $mod (sort keys %modules_used) {
    my $mod_fname = $obj_dir.mangle_modfile($mod);
    my $obj_fname = $obj_dir.$module_files{$mod}.".o";
    print "$mod_fname : $obj_fname\n";

}

#--------------------------------------------------------------------------------------

sub find_dependencies {

    # Find dependencies of input file.
    # Use'd Fortran 90 modules are returned in \@mods.
    # Files that are "#include"d by the cpp preprocessor are returned in \@incs.

    # Check for circular dependencies in \@mods.  This type of dependency
    # is a consequence of having multiple modules defined in the same file,
    # and having one of those modules depend on the other.

    my( $file ) = @_;
    my( @mods, @incs );

    open(FH, $file)  or die "Can't open $file: $!\n";

    # Construct the makefile target associated with this file.  This is used to
    # check for circular dependencies.
    my ($name, $path, $suffix, $target);
    my @suffixes = ('\.[fF]90', '\.[fF]','\.F90\.in' );
    ($name, $path, $suffix) = fileparse($file, @suffixes);
    $target = "$name.o";
    my $include; 
    while ( <FH> ) {
	# Search for "#include" and strip filename when found.
	if ( /^#include\s+[<"](.*)[>"]/ ) {
	    $include = $1;
	} 
	# Search for Fortran include dependencies.
	elsif ( /^\s*include\s+['"](.*)['"]/ ) {                   #" for emacs fontlock
	    $include = $1;
	}
	if(defined($include)){
	    if($include =~ /shr_assert.h/){
		push @mods, "$obj_dir".mangle_modfile("shr_assert_mod");
	    }
	    push @incs, $include;
	    undef $include;
	}
	# Search for module dependencies.
	elsif ( /^\s*USE(?:\s+|\s*\:\:\s*|\s*,\s*non_intrinsic\s*\:\:\s*)(\w+)/i ) {
	    # Return dependency in the form of a .mod file
	    ($module = $1) =~ tr/A-Z/a-z/;
	    if ( defined $module_files{$module} ) {
		# Check for circular dependency
		unless ("$module_files{$module}.o" eq $target) {
                    $modules_used{$module} = ();
                    push @mods, "$obj_dir".mangle_modfile($module);
		}
	    }
            # If we already have a .mod file around.
            elsif ( defined $trumod_files{$module} ) { 
                push @mods, "$obj_dir".mangle_modfile($trumod_files{$module});
            }
	}
    }
    close( FH );
    return (\@mods, \@incs);
}

#--------------------------------------------------------------------------------------

sub find_file {

# Search for the specified file in the list of directories in the global
# array @file_paths.  Return the first occurance found, or the null string if
# the file is not found.

    my($file) = @_;
    my($dir, $fname);

    foreach $dir (@file_paths) {
	$fname = "$dir/$file";
	if ( -f  $fname ) { return $fname; }
    }
    return '';  # file not found
}

#--------------------------------------------------------------------------------------

sub rm_duplicates {

# Return a list with duplicates removed.

    my ($in) = @_;       # input arrary reference
    my @out = ();
    my $i;
    my %h = ();
    foreach $i (@$in) {
	$h{$i} = '';
    }
    @out = keys %h;
    return \@out;
}

#--------------------------------------------------------------------------------------

sub mangle_modfile {

# Return the name of the module file corresponding
# to a given module.

    my ($mod) = @_;
    my $fname;

    if ($mangle_scheme eq "lower") {
        ($fname = $mod) =~ tr/A-Z/a-z/;
        $fname .= ".mod";
    } elsif ($mangle_scheme eq "upper") {
        ($fname = $mod) =~ tr/a-z/A-Z/;
        $fname .= ".MOD";
    } else {
        die "Unrecognized mangle_scheme!\n";
    }

    return $fname;

}

#--------------------------------------------------------------------------------------

sub usage {
    ($ProgName = $0) =~ s!.*/!!;            # name of program
    die <<EOF
SYNOPSIS
     $ProgName [-d depfile] [-m mangle_scheme] [-t dir] [-w] Filepath Srcfiles
OPTIONS
     -d depfile
          Additional file to be added to every .o dependence.
     -m mangle_scheme
          Method of mangling Fortran module names into .mod filenames.
          Allowed values are:
              lower - Filename is module_name.mod
              upper - Filename is MODULE_NAME.MOD
          The default is -m lower.
     -t dir
          Target directory.  If this option is set the .o files that are
          targets in the dependency rules have the form dir/file.o.
     -w   Print warnings to STDERR about files or dependencies not found.
ARGUMENTS
     Filepath is the name of a file containing the directories (one per 
     line) to be searched for dependencies.  Srcfiles is the name of a
     file containing the names of files (one per line) for which
     dependencies will be generated.
EOF
}
