#!/usr/bin/perl

use Getopt::Long;
use Cwd 'realpath';
use File::Spec;

################################################################################
#
# Find place of SDK
#
($vol,$dir) = File::Spec->splitpath(realpath($0));
$sdk = File::Spec->catpath($vol, $dir, "..");
($sdk && (-d "$sdk")) || die("Cannot find location of SDK");

################################################################################
#
# TODO: ARM/THUMB tag on a per-object basis for multi-object sources
#
# Generates makefile rules for ARM code
#
# -d    Directory to work on
#
# Looks for a file SOURCES in the given (or current) directory
# Builds a Makefile that compiles all of the sources
#

my $host;
my $dir;

GetOptions("host|h"		=> \$host,     # When present, generate rules for compiling a host binary
	   "dir|d=s"		=> \$dir);     # Indicates directory to scan for */SOURCES

($dir ne "") || die("Directory missing\n");

################################################################################

if ($host) {
    $objdir = "host";
} else {
    $objdir = "obj";
}

################################################################################
################################################################################
#####									   #####
##### Subroutines							   #####
#####									   #####
################################################################################
################################################################################

#
# Read and parse a sources file
#
sub scanfile($) {
    my $sourcesfilename = shift;
    my $chkitem;
    my @lines;

    my $continues = 0;
    # print STDERR "Scanning \"$sourcesfilename\"\n";

    @lines = `arm-elf-cpp -undef -x assembler-with-cpp -nostdinc $sourcesfilename`;

    foreach (@lines) {
	chomp();
 
	next if /^[ \t]*#.*$/;	# Discard comments
	next if /^[ \t]*$/;	# Discard empty lines

	my $line = $_;

	$line =~ s/^[ \t]*//;		# Remove leading whitespace
	$line =~ s/[ \t]*$//;		# Remove trailing whitespace

	# print STDERR "line=$line\n";

	# See if line is an include statement
	if ($line =~ /^include[ \t]+\"(.*)\"$/) {

	    my $incname = $1;
	    if ($incname =~ /^\//) {
		# Keep absolute path
	    } else {
		if ($sourcesfilename =~ /(.*\/)/) {
		    $incname = $1 . $incname;
		}
	    }

	    # print STDERR "Including \"$incname\"\n";
	    scanfile($incname);
	    # print STDERR "Continuing on \"$sourcesfilename\"\n";
	    next;
	}

	# See if line is an assignment
	if ($line =~ /^([a-zA-Z_][a-zA-Z0-9_]*)[ \t]*(\+?)=[ \t]*(.*)/) {
	    my $var  = $1;
	    my $plus = $2;
	    my $val  = $3;
	    # Note to self: This is clumsy
	    if ($var eq "LIBS") {
		if ($plus ne "+") { $libs = ""; }
		if ($libs ne "")  { $libs = "$libs $val"; } else { $libs = $val; }
	    } elsif ($var eq "AFLAGS") {
		if ($plus ne "+") { $usraflags = ""; }
		$usraflags = "$usraflags $val";
	    } elsif ($var eq "CFLAGS") {
		if ($plus ne "+") { $usrcflags = ""; }
		$usrcflags = "$usrcflags $val";
	    } elsif ($var eq "CXXFLAGS") {
		if ($plus ne "+") { $usrcxxflags = ""; }
		$usrcxxflags = "$usrcxxflags $val";
	    } elsif ($var eq "INCLUDES") {
		if ($plus ne "+") { $usrincludes = $val; }
		$usrincludes = "$usrincludes $val";
	    } elsif ($var eq "INSTALL") {
		($plus eq "") || die("Cannot append to \$INSTALL");
		$install = $val;
	    } elsif ($var eq "LIB") {
		($plus eq "") || die("Cannot append to \$LIB");
		$lib = $val;
	    } else {
		die("Cannot assign \"$var\"");
	    }
	    next;
	}

	# See if line starts with a label
	if ($line =~ /^[ \t]*([ľa-zA-Z0-9_\-\.\/]+):/) {
	    ($label eq "") || die "New label $1 found while processing $label.\n";
	    $label = $1;
	    $line =~ s/^.*:[ \t]*//;	# Remove the label
	    # (!@list{$label}) || die "Label $label seen twice.\n"; # Removed -- allowed now
	}

	# See if line ends with continuation mark
	if ($line =~ /\\$/) {
	    $line =~ s/\\$//;
	    $continues = 1;
	} else {
	    $continues = 0;
	}

	# Split line into items
	@items = split(/[ \t]+/, $line);
	$len = @items;
	next if ($len = 0);

	# print STDERR "label=\"$label\"\n";

	if ($label eq "") {

	    #
	    # Items with no label go into global section, if they are source files (.s .S .c .cpp)
	    #

	    foreach $chkitem (@items) {
		my $chkfn = $chkitem;
		$chkfn =~ s/;.*//;
		if (($chkfn =~ /\.s$/) ||
		    ($chkfn =~ /\.S$/) ||
		    ($chkfn =~ /\.c$/) ||
		    ($chkfn =~ /\.cpp$/)) {
		    my $obfn = $chkfn;
		    $obfn =~ s/\.\w+$/.o/;
		    $list{$chkitem} = "$obfn";
		} else {
		    die "Items \"$line\" contain unsupported file types\n";
		}
	    }

	} else {

	    #
	    # Items with label go into a section named after the label
	    #

	    my $label_path = "";
	    my $slash = rindex($label, '/');
	    if ($slash >= 0) {
		$label_path = substr($label, 0, $slash+1);
	    }

	    $olditems = $list{$label};
	    foreach $chkitem (@items) {
		if (index(":$olditems", ":$chkitem:")>=0) {
		    die("$chkitem (found in $sourcesfilename) already in list for $label");
		}
	    }

	    $list{$label} = $list{$label} . $label_path . join(":$label_path", @items) . ":";

	    # print STDERR "$label contains $list{$label}\n";

	    if (!$continues) { $label = ""; }
	}
    }

}

################################################################################
# Make string uppercase, replace - and . by _

sub uppify($) {
    my $fnord = shift;

    $fnord = uc($fnord);
    $fnord =~ s/\.\.\///g; # Remove ../
    $fnord =~ s/[-\.]/_/g; # Replace characters not allowed in an identifier

    return "$fnord";
}

################################################################################
# Generate the Makefile variable name that will hold the list of object files

sub varname {
    my $rawlabel = shift;
    return $prfx . '_' . uppify($rawlabel) . "_OBJS";
}

################################################################################
#
# Make complete hierarchy of directories
#
sub makedirhier($) {
    my $path = shift;
    my (@subdirs) = split(/[\/\\]/, $path);

    my $dir_to_create = '';

    foreach my $subdir (@subdirs) {
        if ($dir_to_create eq '') {
            $dir_to_create = '/' if $subdir eq '';
        } else {
            $dir_to_create .= '/' if $dir_to_create !~ /\/$/;
        }

        $dir_to_create .= $subdir;

        next if -d $dir_to_create;

        unless (mkdir $dir_to_create) {
	    if ($!{EEXIST}) {
		next if -d $dir_to_create;
	    }
	    die("Could not create directory $dir_to_create: $!\n");
        }
    }
}

################################################################################
################################################################################
#####									   #####
##### Main code								   #####
#####									   #####
################################################################################
################################################################################

$usrcflags = "";   # Extra C flags found in SOURCES
$usrcxxflags = ""; # Extra c++ flags found in SOURCES
$usraflags = "";   # Extra assembler flags found in SOURCES
$usrincludes = ""; # Extra includes found in SOURCES
$libs = "";        # Libraries we depend on
$install = "";     # Where to put the binary
$lib = "";         # We are building a library, not an executable

if (-e "$dir/include") {
    $includes = "\$(INCLUDES) -I\"$dir/include\"";
} else {
    $includes = "\$(INCLUDES)";
}

################################################################################
# Read and parse sources file

$label = "";
scanfile("$dir/SOURCES");
@keys = sort(keys(%list));

if ($lib) {
    $prfx = "LIB_" . uppify($dir);
} else {
    $prfx = "APP_" . uppify($dir);
}

if ($lib && $install) {
    die("Shamelessly refusing to install library $lib to $install.");
}

################################################################################
#
# Try to find LIBS, so the binary can properly depend on them.
#
$libmissing = "";
$libdeps = "";

sub dolibdep($) {
    my $lib = shift;

    if ($lib =~ /^-l/) {

	if (!$host) {

	    my $tmp = substr($lib, 2);
	    my $dep = "$sdk/lib/lib$tmp.a";

	    if (-f $dep) {
		$libdeps = "$libdeps $dep";
	    } else {
		$libmissing = "$libmissing $lib";
	    }

	}

    } else {

	$libdeps = "$libdeps $lib";

    }

}


if ($lib eq "") {

    foreach $lib (split(/[ \t]+/, $libs)) { dolibdep($lib); }

    if ($libmissing eq "") {
	$libs = $libdeps;
    }

    dolibdep("-lc");

    if ($libmissing ne "") {
	print STDERR "\n\n";
	print STDERR "$dir: CANNOT GENERATE DEPENDENCY FOR THE FOLLOWING LIBS:\n";
	print STDERR "$libmissing\n";
	print STDERR "\n\n";
    }
}

################################################################################
#
# Generate Makefile dependencies
#

print "-include $dir/Makefile.local\n";
print "-include $dir/$objdir/Makefile.depend\n";
print "\n";

################################################################################
#
# Generate Makefile variables with object file lists
#

$allobs = "";
$allsrc = "";
$has_cplusplus = 0;

foreach $label (@keys) {

    # Used for dependencies
    my $srcfn = $label;
    $srcfn =~ s/;.*//;
    $allsrc = "$allsrc $dir/$srcfn";

    # Make sure it's got it's object dir
    my $slash = rindex($label, '/');
    if ($slash >= 0) {
	makedirhier("$dir/$objdir/" . substr($label, 0, $slash));
    }

    # The list of object files this source file builds into
    my $objs = $list{$label};

    if (index($objs, ":") < 0) {

	#
	# Source compiles into a single object file
	#

	$allobs = "$allobs $dir/$objdir/$objs";

    } else {

	# The name of the makefile variable that holds the list of objects
	my $varname = varname($label);

	#
	# Source compiles into multiple object files
	#

	# printf STDERR "$label >>>$objs<<<\n";

	# An array with all object file names
	my @files = sort(split(":", $objs));

	#
	# Generate a list of object files and have make put them into a variable
	#
	print "# From $label\n";
	print "$varname = ";
	foreach $file (@files) {
	    print "$dir/$objdir/$file ";
	}
	print "\n\n";

	$allobs = "$allobs " . ' $(' . $varname . ')';
    }
}

print "# Alltogether now\n";
print $prfx . "_ALL_OBJS =$allobs \$(" . $prfx . "_EXTRA_OBJS)\n";
print $prfx . "_ALL_SRCS =$allsrc\n\n";

################################################################################
#
# Generate build rules for each object file
#
foreach $label (@keys) {
    my $defs;
    my $cmd;
    my $objs;
    my $source;
    my $tag;

    # The name of the makefile variable that holds the list of objects
    $varname = varname($label);

    # The list of object files that this source file builds into
    $objs = $list{$label};

    # Source filename may have a tag attached
    $source = $label;
    $source =~ s/;.*//;
    $tag    = substr($label, length($source)+1);

    # print STDERR "source=$source objs=$objs tag=$tag\n";

    if (index($objs, ":") < 0) {

	#
	# Source compiles into a single file
	#
	print "$dir/$objdir/$objs: $dir/$source\n";
	$defs = "";

    } else {

	#
	# Source compiles into multiple files
	#
	print "\$($varname): $dir/$objdir/\%.o: $dir/$source\n";
	$defs = ' -DL$(notdir $(basename $@))';
    }

    if ($source =~ /\.s$/) {
	$cmd = '$(AS)' . $usraflags;
    } elsif ($source =~ /\.S$/) {
	$cmd = '$(AS)' . $usraflags;
    } elsif ($source =~ /\.c$/) {
	$cmd = '$(CC)' . $usrcflags;
    } elsif ($source =~ /\.cpp$/) {
	$cmd = '$(CXX)' . $usrcxxflags;
	$has_cplusplus = 1;
    } else {
	die "Don't know how to compile $source\n";
    }

    # A tag may explicitly specify ARM/THUMB mode or select specific compiler flags
    my @tags = split(",", $tag);
    foreach $tag (@tags) {
	if ($tag eq "THUMB") {
	    $defs = $defs . " -mthumb -UARM -DTHUMB";
	} elsif ($tag eq "ARM") {
	    $defs = $defs . " -mno-thumb -UTHUMB -DARM";
	} elsif ($tag =~ /^O/) {
	    $defs = $defs . " -$tag";
	} elsif ($tag =~ /^-/) {
	    $defs = $defs . " $tag";
	}
    }

    print "\t" . '@echo "[1;30m$< -> $@[0m"' . "\n";
    print "\t\@$cmd$defs$usrincludes" . ' ' . $includes . ' -c $< -o $@' . "\n\n";
}

################################################################################
# Generate build rules for versioning

if (-e "$dir/Version.txt") {
    $version_o = "$dir/$objdir/version.o";

    print "$dir/$objdir/version.c $dir/$objdir/version.txt: $dir/Version.txt \$($prfx\_ALL_OBJS) " .
			"$libdeps \$(C4SDK)/scripts/c4maketimestamp.pl\n";
    print "\t" . '@echo "[1;30m$< -> $@[0m"' . "\n";
    print "\t\@\$(C4SDK)/scripts/c4maketimestamp.pl -v $dir/Version.txt -o $dir/$objdir/version.c\n";
    print "\t\@\$(C4SDK)/scripts/c4maketimestamp.pl -v $dir/Version.txt -p $dir/$objdir/version.txt\n";
    print "\n";

    print "$version_o: $dir/$objdir/version.c\n";
    print "\t" . '@echo "[1;30m$< -> $@[0m"' . "\n";
    print "\t\@\$(CC)$usrincludes $includes -c \$< -o \$@\n";
    print "\n";

    print "version::\n";
    print "\trm -f \"$dir/$objdir/version.c\"\n";
    print "\t\@echo \"Version will be remade on next build.\"\n";
    print "\n";

} else {
    $version_o = "";
}

################################################################################
# Generate build rules for the application or the library

if ($lib) {

    $binary = "$dir/$objdir/$lib";

    print "$binary: \$($prfx\_ALL_OBJS) $version_o\n";

    print "\t" . '@rm -f $@' . "\n";
    print "\t" . '@echo [31m$(AR) rcs $@ ...[0m' . "\n";
    print "\t\@\$(AR) rcs \$\@ \$($prfx\_ALL_OBJS) $version_o\n";
    print "\n";

} else {

    $app = $dir;
    $app =~ s/.*\///;

    $binary = "$dir/$objdir/$app";

    if ($has_cplusplus) {
	$cmd = "\$(CXX)";
    } else {
	$cmd = "\$(CC)";
    }

    if ($host) {
	$lnkargs = "";
    } else {
	$lnkargs = "-Wl,-elf2flt";
    }

    print "$binary: \$($prfx\_ALL_OBJS) $version_o $libdeps\n";
    print "\t\@echo \"[31mLinking \$\@ ...[0m\"\n";
    print "\t\@$cmd \$($prfx\_ALL_OBJS) $version_o $libs $lnkargs -o \$\@\n";

    print "\n";
}

################################################################################
# Generate install rule
#
# If application has a install destination configured, use this.
# Else build a rule that checks if application has been manually put to
# /bin or /sbin and install it only if this is the case.
#
if ($lib eq "") {

    print "install:: $binary\n";

    if ($install ne "") {
	if (substr($install, 0, 1) ne "/") {
	    $install = "\$(ROOT)/" . $install;
	} else {
	    $install = "\$(ROOT)" . $install;
	}

	print "\t\@echo \"[31mInstall $app to $install[0m\"\n";
	print "\t\@install -m 0755 $dir/$objdir/$app $install\n";

    } else {

	print "\t\@if [ -e \$(ROOT)/bin/$app  ]; then ";
	print "echo \"[31mInstall $app to \$(ROOT)/bin[0m\";  ";
	print "install -m 755 $binary \$(ROOT)/bin;  fi\n";

	print "\t\@if [ -e \$(ROOT)/sbin/$app ]; then ";
	print "echo \"[31mInstall $app to \$(ROOT)/sbin[0m\"; ";
	print "install -m 755 $binary \$(ROOT)/sbin; fi\n";

    }

    print "\n";
}

################################################################################
# Generate clean rule

print "clean::\n";
print "\trm -f $dir/$objdir/*.elf $dir/$objdir/*.gdb $binary \n";
print "\trm -f `find $dir/$objdir -name \"*.[oa]\" | grep -v \"\\\\.svn\"`\n";
print "\n";

print "mrproper::\n";
print "\trm -f `find $dir/$objdir -type f | grep -v \"\\\\.svn\"`\n";
print "\n";

################################################################################
#
# Generate depend rules for each object file
#
# This grinds every single source-file through the preprocessor, as there's
# no other way to specify a separate file path for each object file.
#
print "$dir/$objdir/Makefile.depend: $dir/SOURCES $dir/$objdir/Makefile.inc\n";
print "\t\@echo $dir/$objdir/Makefile.depend\n";
print "\t\@\>$dir/$objdir/Makefile.depend\n";

foreach $label (@keys) {
    my $defs;
    my $cmd;
    my $objs;
    my $source;
    my $tag;

    # The name of the makefile variable that holds the list of objects
    $varname = varname($label);

    # The list of object files that this source file builds into
    $objs = $list{$label};

    # Source filename may have a tag attached
    $source = $label;
    $source =~ s/;.*//;
    $tag    = substr($label, length($source)+1);

    # print STDERR "source=$source objs=$objs tag=$tag\n";

    if ($source =~ /\.s$/) {
	$cmd = '$(AS)';
    } elsif ($source =~ /\.S$/) {
	$cmd = '$(AS)';
    } elsif ($source =~ /\.c$/) {
	$cmd = '$(CC)';
    } elsif ($source =~ /\.cpp$/) {
	$cmd = '$(CXX)';
    } else {
	die "Don't know how to compile $source\n";
    }

    if (index($objs, ":") < 0) {

	#
	# Source compiles into a single file
	#
	print "\t\@$cmd$usrincludes $includes -D_MAKEDEPEND -MT $dir/$objdir/$objs -M $dir/$source " .
	    ">>$dir/$objdir/Makefile.depend\n";

    } else {

	#
	# Source compiles into multiple files
	#

	my $ob;
	my @obs = split(":", $objs);
	foreach $ob (@obs) {
	    print "\t\@$cmd -D_MAKEDEPEND$usrincludes $includes -MT $dir/$objdir/$ob -M $dir/$source " .
		">>$dir/$objdir/Makefile.depend\n";
	}
    }

}

print "\n";

################################################################################
