#!/usr/local/bin/perl5

###############################################################################
###############################################################################
##
##    Written by Adam Swift (c) 1995 by Friday Software and Consulting
##                           All rights reserved.
##
##	  This notice may not be removed from this source code.
##
##    This program is included in the MiscKit by permission from the author
##    and its use is governed by the MiscKit license, found in the file
##    "LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
##    for a list of all applicable permissions and restrictions.
##
##    Because AutoDoc is licensed free of charge, there is no warranty 
##    for the program.  Copyright holder, Friday Software and Consulting, 
##    is providing this program "as is" and this program is distributed in 
##    the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
##    even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
##    PARTICULAR PURPOSE.
##
###############################################################################
###############################################################################

##
## Set the required packages for normal execution 
##
require 5.000;

##
## Set this perl file's descriptive variables
##
$file_name		   = 'autodoc';
$file_release	   = '1.8.4';

$file_mlist	= '<autodoc@friday.com>';
$file_copyright	   = 'Friday Software and Consulting, 1995';
$file_author	   = 'Adam Swift <aswift@friday.com>';
@file_contributors = 
  ('Bill Bumgarner <bbum@friday.com>',
   'Todd Anthony Nathan <todd@icebox.com>',
   'Kim Shrier <kim@media.com>',
   'Craig Kelley <ckelley@capaccess.org>');

$file_version	   = '$Revision: 1.5 $'; #'
$file_version	   =~ s!(\$\w+: | \$)!!g; 
$file_id		   = '$Id: autodoc,v 1.5 1995/10/20 22:10:41 aswift Exp $'; #'
$file_id		   =~ s!(\$\w+: | \$)!!g;


###############################################################################
#
# Purpose:	This script extracts comments and other data from Objective-C 
# 		source and header files.  It processes this input to produce 
#		Rich Text Formatted documentation, designed to look like the 
#		NEXTSTEP developer documentation for Objective-C objects.
#
# HISTORY: START
# $Log: autodoc,v $
# Revision 1.5  1995/10/20  22:10:41  aswift
# Completed checkin using DevMan Logs
#
# Revision 1.4  1995/10/20  16:45:28  aswift
# Continuing to try DevMan Log history
#
# Release 1.8.4
# - fixed "up to date documentation" testing 
# - added missing small newlines between the methods and their documentation
# - moved tab locations in the Defined Types stuff to improve doc apperance
# - long method lines are now broken into multiple lines
# - added new paragraph styles to the GenerateRTF module
#
# Release 1.8.3
# - completely documented GenerateRTF.pm Autodoc module
# - abstracted DumpDocs to remove rtf dependancies
# - fixed case insensitive argument parsing to differentiate -D from -d (bbum)
# - cleaned up usage of 'require' and 'use' to remove redundancy (bbum)
# - added support for documenting what protocols an object conforms to.
# - added support for documenting protocols. (Craig Kelley)
# 
# Release 1.8.2
# - added version control to the support modules, and their version is now
#	reported when the -version option is used
# - removed some warning messages in file_expandpath
# - fixed bug preventing doc creation with .[hm] files with no object def.
# - fixed resource naming when files are specified with no project dir
# - fixed boldifying values of typedef-type structures
# - allowed multiple levels of indention to follow curly braces in typedef's
#
# Release 1.8.1 - MiscKit Release 1.6.1
# - fixed documentation extraction failure detection
# - added support for AD_PMLIBDIR to specify perl module lib directory
# - added more log debugging to Autodoc .pm files
# - moved LogDebug::Log.pm into Autodoc::LogDebug.pm
# - moved FileSupport::Misc.pm into Autodoc::FileSupport.pm
#
# Release 1.8
# - added -nosingles option to exclude unpaired '.h' and '.m' files from docs
# - added support for functions, typedefs, defines, macros, and globals
# - moved all autodoc doc creation into Autodoc::DumpDocs
# - moved all autodoc source reading into Autodoc::ReadSource
# - moved autodoc comment parsing into Autodoc::ParseComment
# - moved source file scanning code into Autodoc::ScanFile module
# - added timestamping capability for documentation
# - can now set copyright string with environment var: AD_COPYRIGHT
# - moved debug logging into LogDebug::Log
# - moved filepath expansion into FileSupport::Misc
#
# Release 1.7 - contributions by b.bum
# - added support for GetOpts::Long package; GetOpts::Long can deal w/single 
#   character argnames as long as they aren't ambiguous.
# - removed all warnings produced by perl -w
# - modified scan_line to discard stars from the beginning of ADC lines
# - added -help option;  shows help and exits.
# - made a Source directory and moved code files into (as per the README)
# - look for '## bbum';  i fixed a bunch of a stuff that produced
#   warnings when the rtf documentation did not yet exist.
# - fixed -force so that it actually works.
# - added NULL to list of bold words
# - any instance variable on a line w/the word "INTERNAL" will be skipped.
# - fixed method parameter matching to allow whitespace between the : and arg.
#   (Kim Shrier)
# - fixed argument italicising in method docs to parse out whitespace between 
#   the : and arg (Kim Shrier)
#
# Release 1.6
# - added \ escaping for % and # characters to force display 
#
# Release 1.5.1
# - fixed the "Category Description" bug.  Was putting out "Class Description"
#   (Todd Nathan)
#
# Release 1.5 - MiscKit Release
# - added option to force overwriting up-to-date documentation files
# - added time check for source and doc files to prevent unnecessary work
# - added support for copying .rtf and .rtfd files to the doc dir
# - added support for specifying a copyright owner on the command line
# - fixed ivar parsing so it doesn't skip the line with the starting brace
#
# Release 1.4
# - added minimal category support.
# - changed filename to autodoc (from AutoDoc)
# - added support for relative path naming for documentation and project dirs.
# - fixed bug which aborted doc creation if no newline was found on @end line
# 
# Release 1.3
# - Added minimal support for lists of items via _{xxx desc} format
# - Fixed matching problems at beginning and ends of text strings
# - Added "id" to the list of bold words.
# - Filtered out @public/@private/@protected modifiers in ivars.
# - Fixed incorrect struct detection in ivars with curly braces in a following
#   AutoDoc comment.
#
# Release 1.2
# - Started tracking changes
#
# HISTORY: END
###############################################################################
#
# GOBAL VARIABLES THAT HOLD OBJECT INFORMATION:
#
#  @objects             The array of objects to generate docs for
#
# GLOBAL VARIABLES THAT HOLD EXECUTION MODE INFORMATION
#
#  $proj_dir		The object source project directory (input)
#  $dest_dir		The object documentation destination directory (output)
#  $cwd                 The current working directory autodoc was executed from
#  $opt_silent		The console logging mode, 0 = not silent, 1 = silent
#  $opt_tree            The documentation file organization, 0 = flat, 1 = tree
#  $opt_force           True if doc creation should be forced when up-to-date
#  $opt_rtf             True if rtf and rtfd files should be copied 
#  $opt_nosingles       True if unpaired '.[hm]' files are excluded from docs 
# 
# GLOBAL VARIABLES THAT AFFECT DATA EXTRACTION
#
#  $obj_h               The path and file name of the object header file
#  $obj_m               The path and file name of the object source file
#
# GLOBAL VARIABLES THAT AFFECT DOCUMENTATION DUMP
#
#  $obj_r               The path and file name of the object documentation file
#
# GLOBAL VARIABLES THAT SUPPORT SPECIAL BEHAVIORS
#  $rtffiles
#
##############################################################################

##############################################################################
#
# PROGRAM INITIALIZATION AND ARGUMENT PROCESSING
#
#    Read in the command line arguments and configure the execution mode 
#  variables appropriately.  Read in the files to operate on, and begin 
#  processesing source files.
#
##############################################################################

##################
# READ ARGUMENTS #
##################
## load GetOpt::Long, fix @INC, and check arguments.
BEGIN {
  use Getopt::Long;
  $Getopt::Long::ignorecase = 0;

  $show_usage = "unrecognized or invalid argument"
    unless &GetOptions('lib=s@',
					   'project=s',
					   'destination=s',
					   'copyright=s',
					   'version',
					   'rtf',
					   'force',
					   'timestamp',
					   'nosingles',
					   'tree',
					   'silent',
					   'Debug:i',
					   'help'
					  );
  
  unshift (@INC, @opt_lib)
    if @opt_lib;
  unshift (@INC, $ENV{'AD_PMLIBDIR'})
    if defined($ENV{'AD_PMLIBDIR'});
}

#  $show_usage if GetOptions failed
if ($show_usage) {
  usage($show_usage);
}

##########################
# Load Required Packages #
##########################
use Cwd;
use Autodoc::FileSupport;
use Autodoc::LogDebug;
use Autodoc::ScanFile;
use Autodoc::ParseComments;
use Autodoc::GenerateRTF;
use Autodoc::ReadSource;
use Autodoc::DumpDocs;

######################
# INITIALIZE GLOBALS #
######################
$proj_dir    = "";		# no project directory
$dest_dir    = "";		# no destination directory
$cwd	     = getcwd();

dblog (2, "Current working dir: $cwd\n");

################################
# Process Command Line Options #
################################
# if -help, show usage and exit.
if ($opt_help) {
  $opt_help += 0; # shut up perl -w
  &usage;
}

# if -version appeared on the command line, show version and exit
if ($opt_version) {
  $opt_version += 0; # shut up perl -w
  &show_version;
  exit;
}

# -silent causes autodoc to run silently
if ($opt_silent) {
  dblog (0, "Operating silently.");
  $opt_silent = 1;
}

# -rtf will copy rtf files
if ($opt_rtf){
  dblog (0, "Copying documentation files found in source.");
  $opt_rtf = 1;
}

# force document creation
if ($opt_force) {
    dblog (0, "Forcing documentation creation.");
    $opt_force = 1;
}

# build document tree
if ($opt_tree) {
  dblog (0, "Building documentation tree.");
  $opt_tree = 1;
}

# timestamp documentation
if ($opt_timestamp) {
  dblog (0, "Including timestamp in documentation.");
  set_usetimestamp($opt_timestamp);
}

# exclude unpaired source files from documentation
if ($opt_nosingles) {
  dblog (0, "Excluding unmatched source files from documentation");
  $opt_nosingles += 0;
}

# set copyright notice
if ($opt_copyright) {
  set_copyrightowner($opt_copyright);
  dblog (0, "Setting copyright owner: ", copyrightowner());
} else {
  if (grep (m!AD_COPYRIGHT!, keys (%ENV))) {
    set_copyrightowner($ENV{'AD_COPYRIGHT'});
    dblog (0, "Setting copyright owner: ", copyrightowner());
  }
}

# set project directory
if ($opt_project) {
  $proj_dir = &file_expandpath ($opt_project);
  
  # if there isn't a trailing "/", append it
  $proj_dir = "$proj_dir/" if (!($proj_dir =~ m!/$!));
  dblog (0, "Project directory: $proj_dir");
}

# destination directory specified
if ($opt_destination) {
  $dest_dir = &file_expandpath ($opt_destination);
  
  # if there isn't a trailing "/", append it
  $dest_dir = "$dest_dir/" if (!($dest_dir =~ m/\/$/));
  dblog (0, "Destination directory: $dest_dir");
}

# set debugging level
if ($opt_Debug) {
  &set_dblog_debuglevel($opt_Debug);
  dblog (0, "Debug level: $opt_Debug.");
}

foreach (@ARGV) {
  # strip of the .h or .m from the object source name
  s/\.[hm]$//;
  
  $path = &file_expandpath ($_);
  
  # if it's root name isn't already in the list, then add it.
  if (!(grep (/$path/, @objects))) {
	push (@objects, $path);
  } else {
	dblog (1, "Source file specified twice: $path.");
  }
}

&check_inputfiles;


###############################################################################
# 
# MAIN PROGRAM OPERATION LOOP
# 
###############################################################################
MAINLOOP:						# Main operating loop
  foreach $currobject (@objects) {
	
	# prepare the object (h,m,rtf) file paths
	&set_obj_filepaths ($currobject); 
	
	# check for a .mdoc file (used for documenting protocols)
	if ((!(-e $obj_m))) {
	  $obj_mdoc = "${obj_m}doc";
	  if ((-e $obj_mdoc)) {
	    $obj_m = $obj_mdoc;
	  }
	}
	
	$obj_m = ""
	  if ((!$opt_nosingles) && (!(-e $obj_m)));
	
	# check whether the doc file exists and is up to date
	if ((-r $obj_h) && (($obj_m eq "") || (-r $obj_m))) {
	  
	  $docs_expired = 0;
	  $docs_expired = 1
		if (file_findnewest($obj_h, $obj_m, $obj_r) ne $obj_r);
	  
	  unless ($opt_force || $docs_expired || !(-r $obj_r)) {
		# Documentation file is up to date, skip this object
		print "autodoc documentation up to date for $currobject.\n" 
		  if (!$opt_silent);
		next MAINLOOP;
	  } 
	} else {
	  warn ("Data extraction failed for $currobject, ",
			"could not read source files.\n");
	  next MAINLOOP;
	}
	
	# read object info and try to generate doc's
	if (&read_objectinfo ($currobject, $obj_h, $obj_m)) {
	  
	  # Make any directories necessary for the docs...
	  if (($obj_r =~ m/\//) && (!&make_docdir ($obj_r))) {
		warn ("Could not write to documentation directory for: ",
		      "$obj_r\n");
		return 0;
	  } else {
		if (dump_documentation ($currobject, $obj_r, $opt_silent)) {
		  &dblog (0, "Completed processing for object:\t",
				  $currobject);
		} else {
		  warn ("Documentation file dump failed for ",
				"$currobject.\n");
		}
	  }
	} else {
	  if ($obj_m ne "") {
		warn ("Data extraction failed for $currobject, ",
		      "no documentation file generated.\n");
	  } else {
		dblog (0, "Data extraction failed for $currobject, ",
		       "no documentation file generated.\n");
	  }
	}
  }
# End MAINLOOP

COPYLOOP:						# Copy loop for rtf files
  foreach $rtffile (@rtffiles) {
	
	&set_rtf_filepaths ($rtffile);
	
	# Check if the source file is readable
	if (!(-r $rtf_src)) {
	  warn ("Documentation copy failed for: $rtffile, ",
			"Could not read file.\n");
	  next COPYLOOP;
	}
	
	$docs_expired = 1
	  if (file_findnewest($rtf_src, $rtf_dest) eq $rtf_src);
	
	# Check if it is up to date
	unless ($opt_force || $docs_expired || !(-r $rtf_dest)) {
	  print "autodoc documentation up to date for $rtffile.\n" 
		if (!$opt_silent);
	  next COPYLOOP;
	}
	
	# Make any directories necessary...
	if ($rtf_dest =~ m/\//) {
	  if (!&make_docdir ($rtf_dest)) {
		warn ("Could not write to documentation directory for: ",
		      "$rtf_dest\n");
		next COPYLOOP;
	  }
	}
	print "autodoc copying $rtffile to $rtf_dest." 
	  if (!$opt_silent);
	#	open (CP, "cd $cwd; cp -r $rtf_src $rtf_dest |");  Why?
	system "cd $cwd; cp -r $rtf_src $rtf_dest";
  }
# End COPYLOOP



###############################################################################
# 
# EXECUTION CONTROL AND GENERAL SUPPORT SUBROUTINES
#
#    Execution mode configuration from command line switches, and 
#  preprocessing of input files and paths.  
#
###############################################################################

#
# Create all of the directories necessary for the documentation file specified
#
sub make_docdir
  {
    local ($docfile, $docdir, $partdir, @alldirs, $dir, $lastdir);
    $docfile = $_[0];
	
    $docdir = $docfile;
    $docdir =~ s/\/[^\/]+$//;
	
    if (-e $docdir) {
	  if (-d $docdir) {
	    if (-w $docdir) {
		  return 1;				# the directory exists, and is writable
	    } else {
		  warn ("Cannot write to directory: $docdir");
	    }
	    warn ("Directory would replace file: $docdir");
	  }
	  return 0;
    } 
    dblog (1, "Need to create documentation directory: $docdir");
	
    if ($docdir =~ m/^[^\/\.]/) {
	  $docdir = "./$docdir";
    }
	
    @alldirs = split (/\//, $docdir);
    
  MKDIRS:
    foreach $dir (@alldirs) {
	  if (!$lastdir) {
	    if (!$dir) {
		  $lastdir = "/";
		  next MKDIRS;
	    }
	    $lastdir = "$dir";
	    $partdir = "$dir";
	    next MKDIRS;
	  } 
	  
	  $partdir .= "/$dir";
	  if (!(-e $partdir)) {
	    if (-d $lastdir) {
		  if (-w $lastdir) {
		    print "autodoc creating $partdir ... " if (!$opt_silent);
		    if (mkdir ("$partdir", oct (755))) {
			  print "done\n" if (!$opt_silent);
			  dblog (2, "Made directory $partdir, with mode 755");
		    } else {
			  print "failed!\n" if (!$opt_silent);
			  warn ("Could not make directory: $partdir");
			  return 0;
		    } 
		  } else {
		    warn ("Cannot write to directory: $lastdir  ",
				  "Can not create directory: $partdir");
		    return 0;
		  }
	    } else {
		  warn ("Directory would replace file: $lastdir  ",
				"Can not create directory: $partdir");
		  return 0;
	    }
	  }
	  $lastdir = "$partdir";
    }

    return 1;
  }



#
# Check if we have valid object files or a project directory, create the 
# list of object source files if none are specified.
#
sub check_inputfiles
  {
    dblog(2, "Checking input files");
	
    # check if the project directory is valid
    if ($proj_dir ne "") {
	  if (!(-r $proj_dir)) {
	    die ("The project directory {$proj_dir} specified "
			 ."is not readable.\nDied");
	  }
	  if (!(-d $proj_dir)) {
	    die ("The project directory specified is not a directory.\nDied");
	  }
    }
	
    if (scalar(@objects) == 0) {
	  if ($proj_dir ne "") {
	    &find_objectfiles;
	  }
    }
	
    dblog (1, "Auto-documenting objects: (@objects)");
	
    if (($proj_dir ne "") && ($opt_rtf)) {
	  &find_rtffiles;
    }
	
    dblog (1, "Copying documentation files: (@rtffiles)");
	
    if ((scalar(@objects) == 0) && ($proj_dir eq "")) {
	  &usage ("No source files or project directory specified.");
    }

}


#
# Use find to recursively scan the project directory for object source files
# then file @objects with all of the matched pairs of .h and .m files from 
# the project directory
#
sub find_objectfiles
  {
    local (@projfiles, $pfile, $find_proj_dir);
	
    dblog (0, "Scanning project directory for object source files.");
	
    $find_proj_dir = $proj_dir;	# This is lame ... I'm just removing 
    $find_proj_dir =~ s/\/$//;	# the trailing '/' from the file path
	
    open (FIND, "cd $cwd; find $find_proj_dir -name '*.[hm]' -print |");
    while (<FIND>) {
	  chop;
	  s/$proj_dir//;
	  s/.[hm]$//;
	  push (@projfiles, $_);
    }
    @projfiles = sort @projfiles;
	
    while (@projfiles) {
	  $pfile = shift (@projfiles);
	  if (defined($pfile)) {
	    if (defined($projfiles[0]) && ($pfile eq $projfiles[0])) { 
		  # there must have been both a .h and a .m with the same root 
		  # since the only file names in @projfiles end in .h or .m
		  # the test to see if it is an object file is based on this.
		  push (@objects, $pfile);
		  shift (@projfiles);
	    } else {
		  # there is only one file with the .h or .m extension, if
		  # the option to disallow singles is not set, we add it 
		  push (@objects, $pfile)
		    if (!$opt_nosingles);
	    }
	  }
    }
  }


#
# Use find to recursively scan the project directory for 'rtf' and 'rtfd'
# files.  Store the files to the array rtffiles
#
sub find_rtffiles
  {
    local ($pfile, $find_proj_dir);

    dblog (0, "Scanning project directory for rtf/rtfd files.");

    $find_proj_dir = $proj_dir;	# This is lame ... I'm just removing 
    $find_proj_dir =~ s/\/$//;	# the trailing '/' from the file path
	
    open (FIND, "cd $cwd; find $find_proj_dir \\( -name '*.rtf' -o -name '*.rtfd' \\) -print |");
    while (<FIND>) {
	  chop;
	  s/$proj_dir//;
	  push (@rtffiles, $_);
    }
    @rtffiles = sort @rtffiles;
	
  }


#
# generate the $obj_h, $obj_m and $obj_rtf file path variables.
#
sub set_obj_filepaths
  {
    local ($obj_root) = $_[0];
	
    $obj_h = "${proj_dir}${obj_root}.h";
    $obj_m = "${proj_dir}${obj_root}.m";
	
    if ($opt_tree) {
	  $obj_r = $obj_root;		# if the obj_root is in a subdirectory
	  # remove any file extension on the 
	  # subdirectory name.
	  $obj_r =~ s/\.[^\.]+\//\//g;
	} else {
	  if ($obj_root =~ m!/([^/]+)$!) {
		  $obj_r = $1;
		} else {
		  $obj_r = $obj_root;
		}
    }
    $obj_r = "$dest_dir$obj_r.rtf";
    dblog(2, "Destination file set to: $obj_r");
}

#
# Generate the $rtf_src and $rtf_dest file path variables
#
sub set_rtf_filepaths
  {
    local ($rtf_root) = $_[0];
	
    # prepare the rtf source and dest file path
    $rtf_src = "${proj_dir}${rtf_root}";
	
    if ($opt_tree) {
	  $rtf_dest = $rtf_root; 
	  
	  # If the rtf_root is in a subdirectory
	  # remove any file extension on the subdirectory name.
	  $rtf_dest =~ s/\.[^\.]+\//\//g;
    } else {
	  if ($rtf_root =~ m!/([^/]+)$!) {
		$rtf_dest =~ $1;
	  } else {
		$rtf_dest = $rtf_root;
	  }
    }
    $rtf_dest = "$dest_dir$rtf_dest";
  }

sub usage {
  $error_fmt = shift;
    
  select(STDERR);
    
  printf "$file_name: $error_fmt\n", @_
	if $error_fmt;

  print <<_ENDOFUSAGE_;
Usage: $file_name [-force] [-rtf] [-tree] [-nosingles] [-timestamp] 
		   [-help] [-version] [-silent] [-Debug [#]]
		   [-copyright "Copyright String"] [-destination "dest path"]
		   [-project "project directory"] [ sourcefiles ...]

   -force            Force document creation for up to date files.
   -rtf              Copy rtf and rtfd files fount in project tree.
   -tree	     Create a tree of documentation files.
   -nosingles        Exclude unpaired ".h" and ".m" files from documentation
   -timestamp	     Insert a timestamp into the documentation
   -help             Show this help and exit.	   
   -version	     Show AutoDoc version/copyright and exit.
   -silent	     Operate silently;  do not generate log entries.
   -Debug [#]        Set debugging level to #; 0 (default) is least, 5 is most.
   -copyright "..."  Set copyrightowner in the copyright line to "...".
   -destination ".." Create documentation files in directory "..".
   -project "..."    Look for source files in the directory "...".
_ENDOFUSAGE_
   exit(1);
}

#
# Show version and copyright information
#
sub show_version
{
    local (@module_versions);

    @module_versions = (Autodoc::FileSupport::module_versionstamp(),
						Autodoc::LogDebug::module_versionstamp(),
						Autodoc::ScanFile::module_versionstamp(),
						Autodoc::ReadSource::module_versionstamp(),
						Autodoc::ParseComments::module_versionstamp(),
						Autodoc::DumpDocs::module_versionstamp(),
						Autodoc::GenerateRTF::module_versionstamp());
	
    print "This is $file_name Release $file_release (rev-$file_version)\n";
    print "Module versions:\n\t", join ("\n\t", @module_versions), "\n";
    print "Written by $file_author\n";
    print "Copyright $file_copyright - All Rights Reserved\n";
    print "Please send bugs and suggestions to:$file_mlist\n";
    print "Contributions by:\n\t";
    print join("\n\t", @file_contributors), "\n"
	  if scalar(@file_contributors);
    exit(0);
}
