#!/usr/bin/perl -w

use File::Spec::Functions qw ( :ALL );
use File::stat;
use strict;
use warnings;

my $cfgdir = "config";
my $config_h = shift || "config.h";

# Read in a whole file
#
sub read_file {
  my $file = shift;

  open my $fh, "<$file" or die "Could not open file $file: $!\n";
  local $/;
  my $data = <$fh>;
  close $fh;
  return $data;
}

# Write out a whole file
#
sub write_file {
  my $file = shift;
  my $data = shift;

  open my $fh, ">$file" or die "Could not write $file: $!\n";
  print $fh $data;
  close $fh;
}

# Delete a file
#
sub delete_file {
  my $file = shift;

  unlink $file or die "Could not delete $file: $!\n";
}

# Get a file modification time
#
sub file_mtime {
  my $file = shift;

  my $stat = stat ( $file ) or die "Could not stat $file: $!\n";
  return $stat->mtime;
}

# Read all the .h files in a directory
#
sub read_dir {
  my $dir = shift;

  opendir my $dh, $dir or die "Could not open directory $dir: $!\n";
  my @entries = grep { /\.h$/ } readdir $dh;
  closedir $dh;
  return @entries;
}

# Get the current configuration by reading the configuration file
# fragments
#
sub current_config {
  my $dir = shift;

  my $cfg = {};
  foreach my $file ( read_dir ( $dir ) ) {
    $cfg->{$file} = read_file ( catfile ( $dir, $file ) );
  }
  return $cfg;
}

# Calculate guard name for a header file
#
sub guard {
  my $name = shift;

  $name =~ s/\W/_/g;
  return "CONFIG_".( uc $name );
}

# Calculate preamble for a header file
#
sub preamble {
  my $name = shift;
  my $master = shift;

  my $guard = guard ( $name );
  my $preamble = <<"EOF";
/*
 * This file is automatically generated from $master.  Do not edit this
 * file; edit $master instead.
 *
 */

#ifndef $guard
#define $guard
EOF
  return $preamble;
}

# Calculate postamble for a header file
#
sub postamble {
  my $name = shift;

  my $guard = guard ( $name );
  return "\n#endif /* $guard */\n";
} 

# Get the new configuration by splitting config.h file using the
# @BEGIN/@END tags
#
sub new_config {
  my $file = shift;

  my $cfg = {};
  my $cursor = "";

  open my $fh, "<$file" or die "Could not open $file: $!\n";
  while ( <$fh> ) {
    if ( ( my $newcursor, my $suffix ) = /\@BEGIN\s+(\w+\.h)(.*)$/ ) {
      die "Missing \"\@END $cursor\" before \"\@BEGIN $1\""
	  ." at $file line $.\n" if $cursor;
      $cursor = $newcursor;
      $cfg->{$cursor} = preamble ( $cursor, $file )
	  unless exists $cfg->{$cursor};
      $cfg->{$cursor} .= "\n/*".$suffix."\n";
    } elsif ( ( my $prefix, my $oldcursor ) = /^(.*)\@END\s+(\w+\.h)/ ) {
      die "Missing \"\@BEGIN $oldcursor\" before \"\@END $oldcursor\""
	  ." at $file line $.\n" unless $cursor eq $oldcursor;
      $cfg->{$cursor} .= $prefix."*/\n";
      $cursor = "";
    } else {
      $cfg->{$cursor} .= $_ if $cursor;
    }
  }
  close $fh;
  die "Missing \"\@END $cursor\" in $file\n" if $cursor;

  foreach $cursor ( keys %$cfg ) {
    $cfg->{$cursor} .= postamble ( $cursor );
  }

  return $cfg;
}  

#############################################################################
#
# Main program

# Read in current config file fragments
#
my $current = current_config ( $cfgdir );

# Read in config.h and split it into fragments
#
my $new = new_config ( $config_h );

# Delete any no-longer-wanted config file fragments
#
foreach my $file ( keys %$current ) {
  unlink catfile ( $cfgdir, $file ) unless exists $new->{$file};
}

# Write out any modified fragments, and find the oldest timestamp of
# any unmodified fragments.
#
my $oldest = time ();
foreach my $file ( keys %$new ) {
  if ( $current->{$file} && $new->{$file} eq $current->{$file} ) {
    # Unmodified
    my $time = file_mtime ( catfile ( $cfgdir, $file ) );
    $oldest = $time if $time < $oldest;
  } else {
    write_file ( catfile ( $cfgdir, $file ), $new->{$file} );
  }
}

# If we now have fragments that are older than config.h, set the
# timestamp on config.h to match the oldest fragment, to prevent make
# from always attempting to rebuild the fragments.
#
if ( $oldest < file_mtime ( $config_h ) ) {
  utime time(), $oldest, $config_h or die "Could not touch $config_h: $!\n";
}
