#!/usr/bin/perl -w

=head1 NAME

MakeDox - Generates Doxygen documentation for vs.net solution (.sln) files

=head1 SYNOPSIS
MakeDox Perl script usage:
    MakeDox {solution name} [output directory]
    where: solution is the name of the .sln file containing the
                    projects to be documented
           output directory is the name of the target directory
                            to contain all the generated documentation
                            and other output files.

=head1 DESCRIPTION

This script reads the specified .sln (VS.NET solution file) and runs
Doxygen to create documentation for each project contained therein:
1: Parses the specified solution (.sln) file and extracts all it's\
   member project.
2: Sorts the projects by name, case-insensitive.
3: Builds a .html file containing links to all the directories to be
   generated by Doxygen.
4: Examines the timestamps of the previous Doxygen configuration files
   along with the timestamps of all the source code filenames to
   determine whether there is any need to run Doxygen on the project
5: If any source file is newer than the Doxygen configuration file, a
   new configuration file is created, and Doxygen is launched with it.

Outputs: 

=head1 OPTIONS
This script expects one or two parameters:
1: The name of the solution file to parse
2: (optional) The name of the output directory

=head1 REQUIRES
Installed software: Perl (ActiveState used for this script)
                    (from http://www.activestate.com/Products/Download/Register.plex?id=ActivePerl)
                    MSXSL.exe (from www.Microsoft.com)
                    Doxygen (from http://www.stack.nl/~dimitri/doxygen/)
                    GraphViz for Doxygen-generated diagrams
                    (from http://www.research.att.com/sw/tools/graphviz/)
Install all software into the default locations except for MSXSL.exe,
which should be in "C:/Program Files/Microsoft/MSXSL/msxsl.exe" 

=head1 COPYRIGHT
Copyright (c) 2003 MusicMatch, Inc.  All rights reserved.

=head1 AUTHORS
Greg Engelstad (with assistance from Tony Foiani)
MusicMatch <http://www.musicmatch.com>

=cut

# ======================================================================

# pragmata
use strict;

# standard modules
use Cwd;
use POSIX          qw( strftime );
use File::stat;
use File::Basename qw( basename );

# Windows specific
use Win32;

### ======================================================================================
### Subroutine prototypes
sub ShowUsage;
sub ByIgnoreCase;
sub EncodeForHTML                ( $                      );
sub AdjustCase                   ( $  $                   );
sub DetermineScriptDirectory     ( $                      );
sub IsAbsolutePath               ( $                      );
sub IsDot                        ( $                      );
sub IsAlpha                      ( $                      );
sub IsAlnum                      ( $                      );
sub IsMixedCase                  ( $                      );
sub ContainsAlphanum             ( $                      );
sub IsCppOrH                     ( $                      );
sub TrimFilename                 ( $                      );
sub StripFileExtension           ( $                      );
sub SeparatePathAndFilename      ( $                      );
sub ExtractPathPortion           ( $                      );
sub PrepFilenameForDOS           ( $                      );
sub BuildAbsolutePath            ( $  $                   );
sub NormalizeAndValidateIncludes ( $  @                   );
sub IsDoxFileOlderThanInput      ( $  $  $  $  $ \@ \@ \@ );
sub WriteHTMLPrefix              (                        );
sub WriteHTMLEntry               ( $  $                   );
sub WriteHTMLPostfix             (                        );
sub WriteDoxEntries              ( $  $  @                );
sub WriteDoxPrefixOutput         ( $  $  $                );
sub WriteDoxInsertOutput         ( $                      );
sub WriteDoxPostfixOutput        ( $                      );

# Array definitions
my @all_projects;
my @html_projects;

### Frequently-used strings
# a regular expression that matches zero or more non-double-quote
# characters inside of double quotes, and saves the guts.
my $DQ = qr/"([^\"]*)"/;

my $SCRIPT_NAME     = basename $0;
my $SCRIPT_VERSION  = '2003-02-12-gje';

# User-configurable global constant strings
my $HTML_PAGE_TITLE = "Project Documentation";

### ======================================================================================
### Main program

my $SCRIPT_DIR = DetermineScriptDirectory ($0);

my $MSXSL_EXE       = Win32::GetShortPathName( "${SCRIPT_DIR}\\BinTools\\msxsl.exe" );
my $DOXYGEN_EXE     = Win32::GetShortPathName( "${SCRIPT_DIR}\\BinTools\\doxygen.exe" );

my $DOXYGEN_FILES_XSL    = Win32::GetShortPathName( BuildAbsolutePath( "$SCRIPT_DIR", "DoxygenFiles.xsl" ) );
my $DOXYGEN_INCLUDES_XSL = Win32::GetShortPathName( BuildAbsolutePath( "$SCRIPT_DIR", "DoxygenIncludes.xsl" ) );

my $SOLUTION_DIR;
my $SOLUTION_FILENAME;
my $SOLUTION_NAME;

if (@ARGV < 2)
{
    ShowUsage;
    die "Insufficient command-line arguments.";
}

my $INPUT_FILE_NAME = PrepFilenameForDOS ($ARGV[0]);
my $OUTPUT_DIR_NAME;

if (@ARGV > 1)
{
    $OUTPUT_DIR_NAME = PrepFilenameForDOS ($ARGV[1]);
}
if (!IsAbsolutePath ($OUTPUT_DIR_NAME))
{
    $OUTPUT_DIR_NAME = BuildAbsolutePath ("$SCRIPT_DIR", "$OUTPUT_DIR_NAME");
}

### Ensure output dir exists
mkdir($OUTPUT_DIR_NAME);

### Make it a short path name so that we can correctly handle paths with spaces
$OUTPUT_DIR_NAME = Win32::GetShortPathName($OUTPUT_DIR_NAME);

if (IsAbsolutePath ($INPUT_FILE_NAME))
{
    SeparatePathAndFilename ($INPUT_FILE_NAME);
}
else
{
    my $composite_dir = BuildAbsolutePath ("$SCRIPT_DIR", "$INPUT_FILE_NAME");
    SeparatePathAndFilename ($composite_dir);
}

$SOLUTION_NAME = AdjustCase (StripFileExtension ($SOLUTION_FILENAME), 1);

### Build compsite strings
my $DOXYGEN_LOG  = BuildAbsolutePath ("$OUTPUT_DIR_NAME", "DoxygenLog.txt");
my $HTML_OUTPUT  = BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$SOLUTION_NAME" . "Index.html");
my $BAD_FILE_LOG = BuildAbsolutePath ("$OUTPUT_DIR_NAME", "BadFileLog.txt");

### Open main files
open my $FILE_input, "$INPUT_FILE_NAME"
  or die "Can't open '$INPUT_FILE_NAME': $1";

open my $FILE_html_out, "> $HTML_OUTPUT"
  or die "couldn't open '$HTML_OUTPUT': $!";

open my $FILE_doxygen_log, "> $DOXYGEN_LOG"
  or die "couldn't open '$DOXYGEN_LOG': $!";

open my $FILE_bad_file_log, "> $BAD_FILE_LOG"
  or die "couldn't open '$BAD_FILE_LOG': $!";

print "*** Doxygen output directory: $OUTPUT_DIR_NAME\n";
print "*** Solution file:            $INPUT_FILE_NAME\n";
print "*** HTML output:              $HTML_OUTPUT\n";
print "*** Doxygen Log:              $DOXYGEN_LOG\n";
print "*** Bad File Log:             $BAD_FILE_LOG\n\n\n";

print $FILE_doxygen_log "*** Doxygen output directory: $OUTPUT_DIR_NAME\n";
print $FILE_doxygen_log "*** Solution file:            $INPUT_FILE_NAME\n";
print $FILE_doxygen_log "*** HTML output:              $HTML_OUTPUT\n";
print $FILE_doxygen_log "*** Doxygen Log:              $DOXYGEN_LOG\n";
print $FILE_doxygen_log "*** Bad File Log:             $BAD_FILE_LOG\n\n\n";

### Collect all the project names and paths
while ( my $line = <$FILE_input> )
{
    # skip all lines that don't start with "Project"
    if ($line =~ /^Project/)
    {
        if ( $line =~ / ^ Project\( $DQ \) # parent uuid ($1)
                          \s+ = \s+
                          $DQ, \s+         # child name  ($2)
                          $DQ, \s+         # child path  ($3)
                          $DQ  \s*         # child uuid  ($4)
                        $ /x )
        {
            my ( $project_name, $project_path ) = ( $2, $3);
            push (@all_projects, "$project_name;$project_path");
        }
        else
        {
            warn "unable to parse line $.: $line";
        }
    }
}
close $FILE_input;

### Sort project array
@all_projects = sort ByIgnoreCase (@all_projects);
my $project_count = @all_projects;

### Create and run the Doxygen config files for each project
for (my $idx = 0; $idx < $project_count; $idx++)
{
    chdir $SOLUTION_DIR;

    my ($project_name, $project_path) = split (/[;]/, $all_projects[$idx]);
    $project_name = PrepFilenameForDOS ($project_name);
    $project_path = PrepFilenameForDOS ($project_path);

    unless (IsAbsolutePath ($project_path))
    {
        $project_path = BuildAbsolutePath ("$SOLUTION_DIR", "$project_path");
    }

    my ( $project_dir, $project_base ) = ( $project_path =~ m/^(.*)\\(.*)$/ );
	
    ### Run first XSL script to obtain input filenames
    my @input_files  = qx ($MSXSL_EXE $project_path $DOXYGEN_FILES_XSL -xw);
    my $input_file_count = @input_files;
    if ($input_file_count > 0)
    {
        my @valid_project_files;

        ### Add only projects that actually have files to html output
        push (@html_projects, $project_name);

        ### Run second XSL script to obtain include path line
        my $includes = qx ($MSXSL_EXE $project_path $DOXYGEN_INCLUDES_XSL -xw);
    
        ### Parse include path into individual directories, and validate them
        my @include_dirs = split (/[,;]/, $includes);
        @include_dirs = NormalizeAndValidateIncludes ($project_path, @include_dirs);

        ### Determine whether a new set of documentation files is needed
        my $out_dox_file = BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name.dox");
        print "Examining project $project_name dependencies ...\n";
        print $FILE_doxygen_log "Examining project $project_name dependencies ...\n";

        if (IsDoxFileOlderThanInput ($FILE_bad_file_log,
                                     $FILE_doxygen_log,
                                     $project_name,
                                     $out_dox_file,
                                     $project_dir,
                                     @input_files,
                                     @include_dirs,
                                     @valid_project_files))
        {
            my $valid_files_cnt = @valid_project_files;

            ### Announce the start of a Doxygen run
            my $start_time = strftime '%Y-%m-%d %H:%M:%S', localtime (time);
            my $elapsed_begin = time;
            print "\n<<< Start of cycle for $project_name @ $start_time >>>\n";
            print $FILE_doxygen_log "\n<<< Start of cycle for $project_name @ $start_time >>>\n";

            ### Open new Doxygen configuration file
            open my $out, "> $out_dox_file"
              or die "couldn't open '$out_dox_file': $!";

            ### Write prefix to dox file with initial parameters
            WriteDoxPrefixOutput ($out, $out_dox_file, $project_name);

            ### Copy input filenames to dox file
            my @valid_input_files;
            foreach my $file (@valid_project_files)
            {
                $file =~ s/\A\s+//;
                $file =~ s/\s+\z//;
                my $len = length ($file);
                if ($len > 0)
                {
                    my $ext_pos = index ($file, ".h");
                    unless ($ext_pos > 1)
                    {
                        $ext_pos = index ($file, ".cpp");
                    }

                    if ($ext_pos > 0)
                    {
                        my $space_pos = index ($file, " ");
                        if ($space_pos > 0)
                        {
                            $file = "\"$file\"";
                        }
                        else
                        {
                            $file = " $file";
                        }
                        push (@valid_input_files, $file);
                    }
                }
            }

            WriteDoxEntries ("INPUT                  = ", $out, @valid_input_files);
            WriteDoxInsertOutput ($out);

            ### Copy include directories to dox file
            WriteDoxEntries ("INCLUDE_PATH           = ", $out, @include_dirs);

            ### Write postfix to dox file and close it
            WriteDoxPostfixOutput ($out);
            close $out;

            ### Change to project directory
            chdir $project_dir;

            ### Create output directory
            mkdir (BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name"));

            ### Run Doxygen
            my $log_filename = BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name.log");
            my $err_filename = BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name.err");
            system ("$DOXYGEN_EXE $out_dox_file >\"$log_filename\" 2>\"$err_filename\"");

            ### Test for Doxygen failure
            my $log_file_size = 0;
            if (open my $FILE_test, BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name.log"))
            {
                $log_file_size = stat($FILE_test)->size;
                close $FILE_test;
                if ($log_file_size == 0)
                {
                    print "*** Doxygen failed to run for project: $project_name ***\n";
                    print $FILE_doxygen_log "*** Doxygen failed to run for project: $project_name ***\n";
                }
            }
            else
            {
                print "*** Doxygen failed to run for project: $project_name ***\n";
                print $FILE_doxygen_log "*** Doxygen failed to run for project: $project_name ***\n";
            }

            ### Test for Doxygen warnings
            if ($log_file_size > 0 &&
                open my $FILE_test, BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name.err"))
            {
                my $err_file_size = stat($FILE_test)->size;
                close $FILE_test;
                if ($err_file_size > 0)
                {
                    print "*** Doxygen emitted warnings for project: $project_name ***\n";
                    print $FILE_doxygen_log "*** Doxygen emitted warnings for project: $project_name ***\n";
                }
                else
                {
                    print "No Doxygen warnings for project: $project_name\n";
                    print $FILE_doxygen_log "No Doxygen warnings for project: $project_name\n";
                    my $delete_command = "del " . BuildAbsolutePath ("$OUTPUT_DIR_NAME", "$project_name.err");
                    system ($delete_command);
                }
            }

            ### Restore current directory
            chdir $SCRIPT_DIR;

            my $elapsed_end = time;
            my $time_elapsed = $elapsed_end - $elapsed_begin;
            my $end_time = strftime '%Y-%m-%d %H:%M:%S', localtime ($elapsed_end);
            print "<<< End of cycle for $project_name @ $end_time (${time_elapsed} seconds) >>>\n\n";
            print $FILE_doxygen_log "<<< End of cycle for $project_name @ $end_time (${time_elapsed} seconds) >>>\n\n";
        }
    }
    else
    {
        print "*** Project $project_name has no source files.\n\n";
        print $FILE_doxygen_log "*** Project $project_name has no source files.\n\n";
    }
}

### Create the "front door" HTML file
my $html_project_count = @html_projects;
my $last_column_1      = $html_project_count / 3;
my $last_column_2      = $last_column_1      * 2;
if (($html_project_count % 3) > 0)
{
    $last_column_1++;
    if (($html_project_count % 3) > 0)
    {
        $last_column_2++;
    }
}

WriteHTMLPrefix;
for (my $prj_idx = 0; $prj_idx < $html_project_count; $prj_idx++)
{
    WriteHTMLEntry ($html_projects[$prj_idx], $OUTPUT_DIR_NAME);
    if ($prj_idx == (int ($last_column_1) - 1) ||
        $prj_idx == (int ($last_column_2) - 1))
    {
        print $FILE_html_out "  </td>\n";
        print $FILE_html_out "\n";
        print $FILE_html_out "  <td>\n";
    }
}

WriteHTMLPostfix ();
close $FILE_html_out;

### Close files
close $FILE_input;
close $FILE_html_out;
close $FILE_doxygen_log;
close $FILE_bad_file_log;

###=======================================================================================
### Subroutines defined

# Displays usage information on the console.
sub ShowUsage
{
    print "MakeDox Perl script usage:\n";
    print "    MakeDox {solution name} {output directory}\n";
    print "    where: solution (required) is the name of the .sln file containing the\n";
    print "                    projects to be documented\n";
    print "           output (required) directory is the name of the target directory\n";
    print "                            to contain all the generated documentation\n";
    print "                            and other output files.\n";
}

# Used by the sort function for performing
# a case-insensitive sort.
sub ByIgnoreCase
{
    if (uc($a) lt uc($b))
    {
        return -1;
    }
    if (uc($a) gt uc($b))
    {
        return 1;
    }
    return 0;
}

# Replaces special characters with their corresponding
# pre-defined entity terms.
sub EncodeForHTML ( $ )
{
    my $src = shift;

    $src =~ s/&/&amp;/g;
    $src =~ s/</&lt;/g;
    $src =~ s/>/&gt;/g;
    $src =~ s/"/&quot;/g;
    $src =~ s/'/&#27;/g;

    return $src;
}

# Changes a string to all lowercase or uppercase characters
# unless the string is already mixed case; then the string
# is returned unchanged to preserve case.
sub AdjustCase ( $ $ ) # 2nd param = 0: all lowercase, 1: all uppercase
{
    my ($input_str, $shift_mode) = @_;

    if ($shift_mode != 0 &&
        $shift_mode != 1)
    {
        return $input_str;
    }
    if (IsMixedCase ($input_str))
    {
        if ($shift_mode == 0)
        {
            return lc ($input_str);
        }
        if ($shift_mode == 1)
        {
            return uc ($input_str);
        }
    }
    return $input_str;
}

# Examines the contents of ARGV[0] and if it is a relative
# path, prepends it with the current working directory.
sub DetermineScriptDirectory ( $ )
{
	my ($script_filename) = @_;
	my $script_Path = ExtractPathPortion ($script_filename);
	
	if (IsAbsolutePath ($script_Path))
	{
		return $script_Path;
	}
	else
	{
		return BuildAbsolutePath (getcwd, $script_Path);
	}
}

# Returns true if the input string is an absolute
# filename path, otherwise false.
sub IsAbsolutePath ( $ )
{
    my ($test_str) = @_;

    if (substr ($test_str, 0, 1) eq "\\" ||
        substr ($test_str, 0, 1) eq "/")
    {
        return "true";
    }

    return (IsAlpha (substr ($test_str, 0, 1)) &&
            substr ($test_str, 1, 1) eq ":"    &&
           (substr ($test_str, 2, 1) eq "\\"   ||
            substr ($test_str, 2, 1) eq "/"));
}

# Returns true if the character passed in is a period (.).
sub IsDot ( $ )
{
    my ($test_string) = @_;

    return (length ($test_string) == 1 &&
            substr ($test_string, 0, 1) eq ".");
}

# Returned true if the single character passed in is alphabetic.
sub IsAlpha ( $ )
{
    my ($test_str) = @_;

    my $lower_case = lc ($test_str);

    return (length ($test_str) == 1 &&
            $lower_case ge "a"      && 
            $lower_case le "z")
}

# Returned true if the single character passed in is alphanumeric.
sub IsAlnum ( $ )
{
    my ($test_str) = @_;

    my $lower_case = lc ($test_str);

    return (length ($test_str) == 1 &&
            ($lower_case ge "a"     && 
             $lower_case le "z")    ||
            ($lower_case ge "0"     &&
             $lower_case le "9"));
}

# Returns true if the string passed in contains a mixture
# of lowercase and uppercase alphabetic characters.
sub IsMixedCase ( $ )
{
    my ($input_str) = @_;

    my @input_chr = split (//, $input_str);
    my $has_upper = 0;
    my $has_lower = 0;
    if (@input_chr)
    {
        foreach my $test_chr (@input_chr)
        {
            if ($test_chr eq lc ($test_chr))
            {
                $has_lower = 1;
            }
            if ($test_chr eq uc ($test_chr))
            {
                $has_upper = 1;
            }
            if ($has_lower == 1 &&
                $has_upper == 1)
            {
                return "true";
            }
        }
    }
    return 0;
}

# Returns true if the string passed in contains at least
# one alphanumeric character.
sub ContainsAlphanum ( $ )
{
    my ($test_string) = @_;

    my $str_len = length ($test_string);
    for (my $idx = 0; $idx < $str_len; $idx++)
    {
        if (IsAlnum (substr ($test_string, $idx, 1)))
        {
            return "true";
        }
    }
}

# Returns true if the supplied string ends with either a
# ".h" or ".cpp", without regard to case.
sub IsCppOrH ( $ )
{
    my ($test_string) = @_;

    my $str_len = length ($test_string);

    return (($str_len > 2 && lc (substr ($test_string, $str_len - 2, 2)) eq ".h") ||
            ($str_len > 4 && lc (substr ($test_string, $str_len - 4, 4)) eq ".cpp"));
}

# Strips all leading and trailing characters from the string
# passed in that are neither alphanumeric nor a period (.).
sub TrimFilename ( $ )
{
    my ($test_string) = @_;

    my $str_len = length ($test_string);

    if ($str_len <= 0)
    {
        return "";
    }

    my $first_idx;
    for ($first_idx = 0; $first_idx < $str_len; $first_idx++)
    {
        my $chr = substr ($test_string, $first_idx, 1);
        if (IsDot   ($chr) ||
            IsAlnum ($chr))
        {
            last;
        }
    }

    my $last_idx;
    for ($last_idx = $str_len - 1; $last_idx > $first_idx; $last_idx--)
    {
        my $chr = substr ($test_string, $last_idx, 1);
        if (IsDot   ($chr) ||
            IsAlnum ($chr))
        {
            last;
        }
    }

    if ($last_idx >= $first_idx)
    {
        return substr ($test_string, $first_idx, $last_idx - $first_idx + 1);
    }
}

sub StripFileExtension ( $ )
{
    my ($input_filename) = @_;

    my $str_len = length ($input_filename);
    if ($str_len == 0)
    {
        return $input_filename;
    }

    my $last_dot_pos = rindex ($input_filename, ".");
    if ($last_dot_pos < 0)
    {
        return $input_filename;
    }

    return substr ($input_filename, 0, $str_len - ($str_len - $last_dot_pos));
}

# Examines the input string and looks for the rightmost backslash
# or slash character, then copies everything to the left of that
# position to the global string $SOLUTION_DIR, and everything to
# the right of it to the global string $SOLUTION_FILENAME.
sub SeparatePathAndFilename ( $ )
{
    my ($input_str) = @_;

    my $str_len = length ($input_str);
    if ($str_len == 0)
    {
        return 0;
    }

    my $backslash_idx = rindex ($input_str, "\\");
    my $slash_idx     = rindex ($input_str, "/");

    if ($backslash_idx > 0)
    {
        $SOLUTION_DIR      = substr ($input_str, 0, $backslash_idx);
        $SOLUTION_FILENAME = substr ($input_str, $backslash_idx + 1, $str_len - $backslash_idx - 1);
    }

    if ($slash_idx > 0)
    {
        $SOLUTION_DIR      = substr ($input_str, 0, $slash_idx);
        $SOLUTION_FILENAME = substr ($input_str, $slash_idx + 1, $str_len - $slash_idx - 1);
    }
    
    ### Make it a short path name so that we can correctly handle paths with spaces
    $SOLUTION_DIR = Win32::GetShortPathName($SOLUTION_DIR);
}

# Examines the input string and looks for the rightmost backslash or
# slash character, then returns everything to the left of that.
sub ExtractPathPortion ( $ )
{
    my ($input_str) = @_;

    my $str_len = length ($input_str);
    if ($str_len == 0)
    {
        return 0;
    }

    my $backslash_idx = rindex ($input_str, "\\");
    my $slash_idx     = rindex ($input_str, "/");

    if ($backslash_idx > 0)
    {
        return substr ($input_str, 0, $backslash_idx);
    }

    if ($slash_idx > 0)
    {
        return substr ($input_str, 0, $slash_idx);
    }
}

# Replace all foreward slash characters with backslashes.
sub PrepFilenameForDOS ( $ )
{
    my ($input_str) = @_;
    $input_str =~ s/\//\\/g;
    return $input_str;
}

# Replace all occurences of double backslashes except for the
# first 2 characters, to preserve network paths.
sub BuildAbsolutePath ( $ $ )
{
	my ($root_portion, $relative_portion) = @_;
	
    my $complete_path = PrepFilenameForDOS ("$root_portion\\$relative_portion");
    my $is_network_path = (substr ($complete_path, 0, 2) eq "\\\\");
    
    $complete_path =~ s/\\\\/\\/g;
    if ($is_network_path)
    {
		$complete_path = "\\$complete_path";
    }

    return $complete_path;
}

# Examines all directory paths in the string array passed in.  First
# prepeneds the path with that contained in the first argument to
# make it an absolute path unless it already is an absolute path.
# Then it test the absolute path for validity.  If the path
# corresponds to a directory on the system, the path is added to the
# string array retuned by the function.
sub NormalizeAndValidateIncludes ( $ @ )
{
    my ($project_path_file, @all_include_dirs) = @_;

    my ( $project_path, $project_file ) = ( $project_path_file =~ m/^(.*)\\(.*)$/ );
    unless (IsAbsolutePath ($project_path))
    {
        $project_path = BuildAbsolutePath ("$SOLUTION_DIR", "$project_path");
    }

    my @valid_includes;
    foreach my $inc_dir (@all_include_dirs)
    {
        my $TEST_DIR;
        my $test_path;
        if (IsAbsolutePath ($inc_dir))
        {
            $test_path = $inc_dir;
        }
        else
        {
            $test_path = BuildAbsolutePath ("$project_path", "$inc_dir");
        }

        if (opendir ($TEST_DIR, "$test_path"))
        {
            push (@valid_includes, "$test_path");
        }
        else
        {
            print $FILE_bad_file_log "*** Bad include path (project: $project_file): >>$test_path<<\n";
        }
    }

    return @valid_includes;
}

# Returns true if the timestamp of the specified .dox file is older
# than any of the named input files.  Each of the named input files
# is searched for using in all the source directories until it is
# found.  The function also attempts to locate each file using each
# include path until it is found.  If it is found it is added to
# the array of known-valid filenames with a full path.
sub IsDoxFileOlderThanInput ( $ $ $ $ $  \@ \@ \@)
{
    my ($FILE_bad_file_log,
        $FILE_doxygen_log,
        $project_name,
        $dox_filename,
        $project_base_dir,
        $project_files,
        $project_includes,
        $valid_project_files ) = @_;

    my $latest_timestamp = 0;
    my $dox_date = -1;
    my @test_directories = @$project_includes;

    if (open my $FILE_dox, "$dox_filename")
    {
        $dox_date = stat($FILE_dox)->mtime;
        close $FILE_dox;
    }
    else
    {
        print "Doxygen configuration file: $dox_filename not found\n";
        print $FILE_doxygen_log "--> Doxygen configuration file: $dox_filename not found\n";
        $dox_date = -1;
    }

    my $TEST_DIR;
    if (opendir ($TEST_DIR, "$project_base_dir"))
    {
        unshift (@test_directories, $project_base_dir);
    }

    foreach my $project_file (@$project_files)
    {
        my $trimmed_filename;
        if (ContainsAlphanum ($project_file))
        {
            $trimmed_filename = TrimFilename ($project_file);
            if (IsCppOrH ($trimmed_filename))
            {
                my $project_include;
                foreach $project_include (@test_directories)
                {
                    my $complete_filename = BuildAbsolutePath ("$project_include", "$trimmed_filename");
                    if (open my $FILE_input, $complete_filename)
                    {
                        my $date_value = stat($FILE_input)->mtime;
                        close $FILE_input;
# Optimization disabled to enable validation of all input files
#                        if ($date_value > $dox_date)
#                        {
#                            return "true";
#                        }

                        if ($date_value > $latest_timestamp)
                        {
                            $latest_timestamp = $date_value;
                        }

                        push (@$valid_project_files, $complete_filename);
                        last;
                    }
                    print $FILE_bad_file_log "*** Bad filename (project: $project_name): >>" . BuildAbsolutePath ("$project_include", "$trimmed_filename") . "<<\n";
                }
            }
        }
    }

    my $val_fil_cnt = @$valid_project_files;
    return ($latest_timestamp == -1 || $latest_timestamp > $dox_date);
}

# Writes the portion of the HTML file that precedes the list of the
# project names and the links to their index.html files.
sub WriteHTMLPrefix ()
{
    my $run_time = strftime '%Y-%m-%d %H:%M:%S', localtime (time);
    print $FILE_html_out <<DOXX;
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<html>
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html;charset=iso-8859-1\" />
<title>$SOLUTION_NAME Projects</title>
</head>

<body>
<!-- Generated by $SCRIPT_NAME (version: $SCRIPT_VERSION) @ $run_time -->

DOXX

    print $FILE_html_out "<hr /><h1>$SOLUTION_NAME $HTML_PAGE_TITLE ($run_time)</h1>";

    print $FILE_html_out <<DOXX;

<table border="0" cellpadding="5" cellspacing="50">
 <tr>

  <td>
DOXX
}

# Writes a project name and a link to its index.html file.
sub WriteHTMLEntry ( $ $ )
{
    my ($project_name, $output_dir) = @_;
    my $project_name_enc = EncodeForHTML ($project_name);
    my $output_dir_enc   = EncodeForHTML ($output_dir);
    print $FILE_html_out "   <a class=\"$project_name_enc\" href=\"$project_name_enc\\index.html" . 
                         "\">$project_name_enc</a><br>\n";
}

# Writes the portion of the HTML file that follows the list of the
# project names and the links to their index.html files.
sub WriteHTMLPostfix ()
{
    print $FILE_html_out <<DOXX;

  </td>

 </tr>
</table>

</body>
</html>

<table border="0" cellpadding="5" cellspacing="30">
 <tr>

  <td>
DOXX
}

# Writes the supplied string array out to either the "INPUT" or
# "INCLUDE_PATH" section of the Doxygen configuration file.
sub WriteDoxEntries ( $ $ @ )
{
    my ($prefix, $out, @raw_entries) = @_;
    my $indent = " " x length $prefix;
    print $out ( $prefix, join( " \\\n" . $indent, @raw_entries ));
}

# Writes the section of the Doxygen configuration file that precedes
# the "INPUT" section (as generated by Doxygen).
sub WriteDoxPrefixOutput ( $ $ $ )
{
    my ($out, $out_dox_file, $project_name) = @_;
    my $output_directory = BuildAbsolutePath ("$OUTPUT_DIR_NAME" , "$project_name");
    print $out <<DOX;
# Doxyfile 1.3-rc2

#---------------------------------------------------------------------------
# General configuration options
#---------------------------------------------------------------------------
DOX

    print $out qq|PROJECT_NAME           = "$SOLUTION_NAME: $project_name"\n|;
    print $out "PROJECT_NUMBER         = \n";
    print $out "OUTPUT_DIRECTORY       = \"$output_directory\"\n";

    print $out <<DOX;
OUTPUT_LANGUAGE        = English
EXTRACT_ALL            = YES
EXTRACT_PRIVATE        = YES
EXTRACT_STATIC         = YES
EXTRACT_LOCAL_CLASSES  = YES
HIDE_UNDOC_MEMBERS     = NO
HIDE_UNDOC_CLASSES     = NO
HIDE_FRIEND_COMPOUNDS  = NO
HIDE_IN_BODY_DOCS      = NO
BRIEF_MEMBER_DESC      = YES
REPEAT_BRIEF           = YES
ALWAYS_DETAILED_SEC    = NO
INLINE_INHERITED_MEMB  = NO
FULL_PATH_NAMES        = NO
STRIP_FROM_PATH        = 
INTERNAL_DOCS          = NO
CASE_SENSE_NAMES       = YES
SHORT_NAMES            = NO
HIDE_SCOPE_NAMES       = NO
VERBATIM_HEADERS       = YES
SHOW_INCLUDE_FILES     = YES
JAVADOC_AUTOBRIEF      = YES
MULTILINE_CPP_IS_BRIEF = NO
DETAILS_AT_TOP         = NO
INHERIT_DOCS           = YES
INLINE_INFO            = YES
SORT_MEMBER_DOCS       = YES
DISTRIBUTE_GROUP_DOC   = NO
TAB_SIZE               = 8
GENERATE_TODOLIST      = YES
GENERATE_TESTLIST      = YES
GENERATE_BUGLIST       = YES
GENERATE_DEPRECATEDLIST= YES
ALIASES                = 
ENABLED_SECTIONS       = 
MAX_INITIALIZER_LINES  = 30
OPTIMIZE_OUTPUT_FOR_C  = NO
OPTIMIZE_OUTPUT_JAVA   = NO
SHOW_USED_FILES        = YES
#---------------------------------------------------------------------------
# configuration options related to warning and progress messages
#---------------------------------------------------------------------------
QUIET                  = NO
WARNINGS               = YES
WARN_IF_UNDOCUMENTED   = YES
WARN_IF_DOC_ERROR      = YES
WARN_FORMAT            = "\$file:\$line: \$text"
WARN_LOGFILE           = 
#---------------------------------------------------------------------------
# configuration options related to the input files
#---------------------------------------------------------------------------
DOX
}

# Writes the section of the Doxygen configuration file bracketed by the
# "INPUT" and "INCLUDE_PATH" sections (as generated by Doxygen).
sub WriteDoxInsertOutput ( $ )
{
    my ($out) = @_;
    print $out <<DOX;

FILE_PATTERNS          = 
RECURSIVE              = NO
EXCLUDE                = 
EXCLUDE_SYMLINKS       = NO
EXCLUDE_PATTERNS       = *.dll \\
                         *.exp \\
                         *.htm \\
                         *.idb \\
                         *.ilk \\
                         *.lib \\
                         *.obj \\
                         *.pch \\
                         *.pdb \\
                         *.tlh \\
                         *.tli
EXAMPLE_PATH           = 
EXAMPLE_PATTERNS       = 
EXAMPLE_RECURSIVE      = NO
IMAGE_PATH             = 
INPUT_FILTER           = 
FILTER_SOURCE_FILES    = NO
#---------------------------------------------------------------------------
# configuration options related to source browsing
#---------------------------------------------------------------------------
SOURCE_BROWSER         = YES
INLINE_SOURCES         = NO
STRIP_CODE_COMMENTS    = YES
REFERENCED_BY_RELATION = YES
REFERENCES_RELATION    = YES
#---------------------------------------------------------------------------
# configuration options related to the alphabetical class index
#---------------------------------------------------------------------------
ALPHABETICAL_INDEX     = YES
COLS_IN_ALPHA_INDEX    = 5
IGNORE_PREFIX          = 
#---------------------------------------------------------------------------
# configuration options related to the HTML output
#---------------------------------------------------------------------------
GENERATE_HTML          = YES
HTML_OUTPUT            = .
HTML_FILE_EXTENSION    = .html
HTML_HEADER            = 
HTML_FOOTER            = 
HTML_STYLESHEET        = 
HTML_ALIGN_MEMBERS     = YES
GENERATE_HTMLHELP      = NO
CHM_FILE               = 
HHC_LOCATION           = 
GENERATE_CHI           = NO
BINARY_TOC             = NO
TOC_EXPAND             = NO
DISABLE_INDEX          = NO
ENUM_VALUES_PER_LINE   = 4
GENERATE_TREEVIEW      = YES
TREEVIEW_WIDTH         = 250
#---------------------------------------------------------------------------
# configuration options related to the LaTeX output
#---------------------------------------------------------------------------
GENERATE_LATEX         = NO
LATEX_OUTPUT           = latex
LATEX_CMD_NAME         = latex
MAKEINDEX_CMD_NAME     = makeindex
COMPACT_LATEX          = NO
PAPER_TYPE             = a4wide
EXTRA_PACKAGES         = 
LATEX_HEADER           = 
PDF_HYPERLINKS         = NO
USE_PDFLATEX           = NO
LATEX_BATCHMODE        = NO
#---------------------------------------------------------------------------
# configuration options related to the RTF output
#---------------------------------------------------------------------------
GENERATE_RTF           = NO
RTF_OUTPUT             = rtf
COMPACT_RTF            = NO
RTF_HYPERLINKS         = NO
RTF_STYLESHEET_FILE    = 
RTF_EXTENSIONS_FILE    = 
#---------------------------------------------------------------------------
# configuration options related to the man page output
#---------------------------------------------------------------------------
GENERATE_MAN           = NO
MAN_OUTPUT             = man
MAN_EXTENSION          = .3
MAN_LINKS              = NO
#---------------------------------------------------------------------------
# configuration options related to the XML output
#---------------------------------------------------------------------------
GENERATE_XML           = NO
XML_SCHEMA             = 
XML_DTD                = 
#---------------------------------------------------------------------------
# configuration options for the AutoGen Definitions output
#---------------------------------------------------------------------------
GENERATE_AUTOGEN_DEF   = NO
#---------------------------------------------------------------------------
# configuration options related to the Perl module output
#---------------------------------------------------------------------------
GENERATE_PERLMOD       = NO
PERLMOD_LATEX          = NO
PERLMOD_PRETTY         = YES
PERLMOD_MAKEVAR_PREFIX = 
#---------------------------------------------------------------------------
# Configuration options related to the preprocessor   
#---------------------------------------------------------------------------
ENABLE_PREPROCESSING   = YES
MACRO_EXPANSION        = YES
EXPAND_ONLY_PREDEF     = NO
SEARCH_INCLUDES        = YES
DOX
}

# Writes the section of the Doxygen configuration file that follows
# the "INCLUDE_PATH" section (as generated by Doxygen).
sub WriteDoxPostfixOutput ( $ )
{
    my ($out) = @_;
    print $out <<DOX;

INCLUDE_FILE_PATTERNS  = 
PREDEFINED             = __cplusplus \\
                         FAR= \\
                         "STDMETHOD(a)=HRESULT a" \\
                         WIN32
EXPAND_AS_DEFINED      = 
SKIP_FUNCTION_MACROS   = YES
#---------------------------------------------------------------------------
# Configuration::addtions related to external references   
#---------------------------------------------------------------------------
TAGFILES               = 
GENERATE_TAGFILE       = 
ALLEXTERNALS           = NO
EXTERNAL_GROUPS        = YES
PERL_PATH              = /usr/bin/perl
#---------------------------------------------------------------------------
# Configuration options related to the dot tool   
#---------------------------------------------------------------------------
CLASS_DIAGRAMS         = YES
HIDE_UNDOC_RELATIONS   = YES
HAVE_DOT               = NO
CLASS_GRAPH            = YES
COLLABORATION_GRAPH    = YES
TEMPLATE_RELATIONS     = YES
INCLUDE_GRAPH          = YES
INCLUDED_BY_GRAPH      = YES
GRAPHICAL_HIERARCHY    = YES
DOT_IMAGE_FORMAT       = png
DOT_PATH               = 
DOTFILE_DIRS           = 
MAX_DOT_GRAPH_WIDTH    = 1024
MAX_DOT_GRAPH_HEIGHT   = 1024
GENERATE_LEGEND        = YES
DOT_CLEANUP            = YES
#---------------------------------------------------------------------------
# Configuration::addtions related to the search engine   
#---------------------------------------------------------------------------
SEARCHENGINE           = NO
DOX
}
