#!/usr/bin/perl

###############################################################################
#
#   Script: NaturalDocs
#
###############################################################################
#
#       Version 1.22
#
#       Copyright  2003-2004 Greg Valure
#
#       http://www.naturaldocs.org
#
#
#   About: License
#
#       Licensed under the GNU General Public License
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#
#       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.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, visit http://www.gnu.org/licenses/gpl.txt
#       or write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
#       Boston, MA  02111-1307  USA.
#
#
#   Topic: Code Conventions
#
#       - Every package function is called with an arrow operator.  It's needed for inheritance in some places, and consistency
#         when it's not.
#
#       - No constant will ever be zero or undef.  Those are reserved so any piece of code can allow a "none of the above" option
#         and not worry about conflicts with an existing value.
#
#
#   Topic: File Format Conventions
#
#       - All integers appear in big-endian format.  So a UInt16 should be handled with a 'n' in pack and unpack, not with a 'S'.
#
#       - AString16's are a big-endian UInt16 followed by that many ASCII characters.  A null-terminator is not stored.
#
#       - If a higher-level type is described in a file format, that means the loading and saving format is handled by that package.
#         For example, if you see <SymbolString> in the format, that means <NaturalDocs::SymbolString->ToBinaryFile()> and
#         <NaturalDocs::SymbolString->FromBinaryFile()> are used to manipulate it, and the underlying format should be treated
#         as opaque.
#

use strict;
use integer;

use 5.005;  # When File::Spec was included by default

use English '-no_match_vars';

use FindBin;
use lib "$FindBin::RealBin/Modules";

sub INIT
    {
    # This function is just here so that when I start the debugger, it doesn't open a new file.  Normally it would jump to an INIT
    # function in some other file since that's the first piece of code to execute.
    };

use NaturalDocs::Constants;
use NaturalDocs::SymbolString;
use NaturalDocs::ReferenceString;
use NaturalDocs::Topics;
use NaturalDocs::Version;
use NaturalDocs::File;
use NaturalDocs::ConfigFile;
use NaturalDocs::NDMarkup;
use NaturalDocs::Languages;
use NaturalDocs::Settings;
use NaturalDocs::Project;
use NaturalDocs::Menu;
use NaturalDocs::SymbolTable;
use NaturalDocs::ClassHierarchy;
use NaturalDocs::Parser;
use NaturalDocs::Builder;
use NaturalDocs::Extensions;



###############################################################################
#
#   Group: Basic Types
#
#   Types used throughout the program.  As Perl is a weakly-typed language unless you box things into objects, these types are
#   for documentation purposes and are not enforced.
#
#
#   Type: FileName
#
#   A string representing the absolute, platform-dependent path to a file.  Relative file paths are no longer in use anywhere in the
#   program.  All path manipulation should be done through <NaturalDocs::File>.
#
#
#   Type: VersionInt
#
#   A comparable integer representing a version number.  Converting them to and from text and binary should be handled by
#   <NaturalDocs::Version>.
#
#
#   Type: SymbolString
#
#   A scalar which encodes a normalized array of identifier strings representing a full or partially-resolved symbol.  All symbols
#   must be retrieved from plain text via <NaturalDocs::SymbolString->FromText()> so that the separation and normalization is
#   always consistent.  Symbols are comparable via string compare functions and are sortable.
#
#
#   Type: ReferenceString
#
#   All the information about a reference that makes it unique encoded into a string.  This includes the <SymbolString> of the
#   reference, the scope <SymbolString> it appears in, the scope <SymbolStrings> it has access to via "using", and the
#   <ReferenceType>.  This is done because if any of those parameters change, it needs to be treated as a completely separate
#   reference.
#



###############################################################################
# Group: Support Functions
# General functions that are used throughout the program, and that don't really fit anywhere else.


#
#   Function: StringCompare
#
#   Compares two strings so that the result is good for proper sorting.  A proper sort orders the characters as
#   follows:
#
#   - End of string.
#   - Whitespace.  Line break-tab-space.
#   - Symbols, which is anything not included in the other entries.
#   - Numbers, 0-9.
#   - Letters, case insensitive except to break ties.
#
#   If you use cmp instead of this function, the result would go by ASCII/Unicode values which would place certain symbols
#   between letters and numbers instead of having them all grouped together.  Also, you would have to choose between case
#   sensitivity or complete case insensitivity, in which ties are broken arbitrarily.
#
#   Returns:
#
#   Like cmp, it returns zero if A and B are equal, a positive value if A is greater than B, and a negative value if A is less than B.
#
sub StringCompare #(a, b)
    {
    my ($a, $b) = @_;

    if (!defined $a)
        {
        if (!defined $b)
            {  return 0;  }
        else
            {  return -1;  };
        }
    elsif (!defined $b)
        {
        return 1;
        };

    my $translatedA = lc($a);
    my $translatedB = lc($b);

    $translatedA =~ tr/\n\r\t 0-9a-z/\x01\x02\x03\x04\xDB-\xFE/;
    $translatedB =~ tr/\n\r\t 0-9a-z/\x01\x02\x03\x04\xDB-\xFE/;

    my $result = $translatedA cmp $translatedB;

    if ($result == 0)
        {
        # Break the tie by comparing their case.  Lowercase before uppercase.

        # If statement just to keep everything theoretically kosher, even though in practice we don't need this.
        if (ord('A') > ord('a'))
            {  return ($a cmp $b);  }
        else
            {  return ($b cmp $a);  };
        }
    else
        {  return $result;  };
    };


#
#   Function: ShortenToMatchStrings
#
#   Compares two arrayrefs and shortens the first array to only contain shared entries.  Assumes all entries are strings.
#
#   Parameters:
#
#       sharedArrayRef - The arrayref that will be shortened to only contain common elements.
#       compareArrayRef - The arrayref to match.
#
sub ShortenToMatchStrings #(sharedArrayRef, compareArrayRef)
    {
    my ($sharedArrayRef, $compareArrayRef) = @_;

    my $index = 0;

    while ($index < scalar @$sharedArrayRef && $index < scalar @$compareArrayRef &&
             $sharedArrayRef->[$index] eq $compareArrayRef->[$index])
        {  $index++;  };

    if ($index < scalar @$sharedArrayRef)
        {  splice(@$sharedArrayRef, $index);  };
    };


#
#   Function: UseDirectory
#
#   Does the equivalent of "use [module]" for every .pm file in a directory and all its subdirectories.  You *must* call this function
#   from within a BEGIN block if you want the INIT functions of the modules to be executed.
#
#   Parameters:
#
#       directory  - A single subdirectory.  Specify this as many times as necessary to make the path from the Natural Docs
#                        directory.
#
#   Example:
#
#       > BEGIN {
#       >    IncludeDirectory('Modules', 'NaturalDocs', 'Extensions');
#       > };
#
#       This code will include every .pm file in [Natural Docs' directory]/Modules/NaturalDocs/Extensions and all its subdirectories.
#
sub UseDirectory #(directory, directory, directory ...)
    {
    my $extraPath = NaturalDocs::File->JoinDirectories(@_);
    my $fullPath = NaturalDocs::File->JoinPaths($FindBin::RealBin, $extraPath, 1);

    my @directories = ( $fullPath );

    while (scalar @directories)
        {
        my $directory = pop @directories;

        opendir DIRECTORYHANDLE, $directory;
        my @entries = readdir DIRECTORYHANDLE;
        closedir DIRECTORYHANDLE;

        @entries = NaturalDocs::File->NoUpwards(@entries);

        foreach my $entry (@entries)
            {
            my $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry);

            # If an entry is a directory, recurse.
            if (-d $fullEntry)
                {
                # Join again with the noFile flag set in case the platform handles them differently.
                push @directories, NaturalDocs::File->JoinPaths($directory, $entry, 1);
                }

            # Otherwise add it if it's a supported extension.  We need to explicitly ignore the menu files because they're text files and
            # their syntax is similar to Natural Docs content.
            elsif ($entry =~ /\.pm$/i)
                {
                require $fullEntry;
                };
            };
        };
    };


#
#   Function: XChomp
#
#   A cross-platform chomp function.  Regular chomp fails when parsing Windows-format line breaks on a Unix platform.  It
#   leaves the /r on, which screws everything up.  This does not.
#
#   Parameters:
#
#       lineRef - A *reference* to the line to chomp.
#
sub XChomp #(lineRef)
    {
    my $lineRef = shift;
    $$lineRef =~ s/[\n\r]+$//;
    };


###############################################################################
#
#   Main Code
#
#   The order in which functions are called here is critically important.  Read the "Usage and Dependencies" sections of all the
#   packages before even thinking about rearranging these.
#


# The Builder and Extension directories are live.  Any .pm files in them are automatically included.

BEGIN {

    UseDirectory( 'Modules', 'NaturalDocs', 'Builder' );
    UseDirectory( 'Modules', 'NaturalDocs', 'Extensions' );

    };


eval {

    # Check that our required packages are okay.

    NaturalDocs::File->CheckCompatibility();


    # Almost everything requires Settings to be initialized.

    NaturalDocs::Settings->Load();


    # Add the supported languages.

    my $cExtensions = [ 'h', 'hpp' ];

    if (!NaturalDocs::Settings->HeadersOnly())
        {  push @$cExtensions, 'c', 'cpp';  };


    # Notes for people unfamiliar with Perl:
    # - Use single values as follows: 'value'
    # - Use multiple values as follows: [  'value1', 'value2', 'value3' ].  You MUST include the brackets.
    # - Specify line breaks as follows: "\n".  You MUST use quotes instead of apostrophes.  You don't have to worry about \r.
    # - Specify backslashes as follows: "\\".  You MUST use quotes instead of apostrophes.
    # - Follow the documentation on the parameters closely.  You don't want to skip one or put them in the wrong order.
    # - Don't worry that some lines use something like PLSQL->New() instead of Simple->New().  This is only necessary
    #   when the language's syntax needs special handling.  Simple->New() should be fine in most cases.

    NaturalDocs::Languages::Simple->New( 'C++', $cExtensions, undef, '//', '/*', '*/', [ ';', '{' ], [ ';', '=' ], undef, '::' );
    NaturalDocs::Languages::Simple->New( 'Java', 'java', undef, '//', '/*', '*/', '{', [ ';', '=' ], undef );
    NaturalDocs::Languages::Simple->New( 'JavaScript', 'js', undef, '//', '/*', '*/', '{', [ ';', '=' ], undef );
    NaturalDocs::Languages::Simple->New( 'Python', 'py', 'python', '#', undef, undef, ':', '=', "\\" );

    # Doesn't detect escapement yet.
    NaturalDocs::Languages::PHP->New( 'PHP', [ 'inc', 'php', 'php3', 'php4', 'phtml' ], 'php',
                                                          [ '//', '#' ], '/*', '*/', [ '{', ';' ], [ ';', '=' ], undef );

    NaturalDocs::Languages::PLSQL->New( 'PL/SQL', 'sql', undef, '--', '/*', '*/', [ ',', ';', ')', 'as', 'AS', 'is', 'IS' ],
                                                              [ ',', ';', ')', ':=', 'default', 'DEFAULT' ], undef );
    NaturalDocs::Languages::Simple->New( 'VB', [ 'vb', 'bas', 'cls', 'frm' ], undef, '\'', undef, undef, "\n", [ "\n", '=' ], '_' );
    NaturalDocs::Languages::Pascal->New( 'Pascal', 'pas', undef, '//', [ '{', '(*' ], [ '}', '*)' ], [ ';' ], [ ';', '=' ], undef );
    NaturalDocs::Languages::Simple->New( 'Assembly', 'asm', undef, ';', undef, undef, undef, "\n", "\\" );
    NaturalDocs::Languages::Ada->New( 'Ada', 'ada', undef, '--', undef, undef, [ 'is', 'IS', ';' ], [ ':=', ';' ], undef );
    NaturalDocs::Languages::Tcl->New( 'Tcl', 'tcl', [ 'tclsh', 'wish' ], '#', undef, undef, [ '{', ';' ], [ ';', "\n" ], "\\", '::' );
    NaturalDocs::Languages::Ruby->New( 'Ruby', 'rb', 'ruby', '#', undef, undef, [ ';', "\n" ], [ ';', '=', "\n" ], "\\" );
    NaturalDocs::Languages::Simple->New( 'Text', 'txt', undef, undef, undef, undef, undef, undef, undef );

    # These have full language support, hence no parameters.
    NaturalDocs::Languages::Perl->New();
    NaturalDocs::Languages::CSharp->New();


    # Migrate from the old file names that were used prior to 1.14.

    NaturalDocs::Project->MigrateOldFiles();


    if (!NaturalDocs::Settings->IsQuiet())
        {  print "Finding files and detecting changes...\n";  };

    NaturalDocs::Project->LoadAndDetectChanges();

    NaturalDocs::SymbolTable->Load();
    NaturalDocs::ClassHierarchy->Load();

    NaturalDocs::SymbolTable->Purge();
    NaturalDocs::ClassHierarchy->Purge();


    # Parse any supported files that have changed.

    my $filesToParse = NaturalDocs::Project->FilesToParse();
    my $amount = scalar keys %$filesToParse;

    if ($amount > 0)
      {
      if (!NaturalDocs::Settings->IsQuiet())
          {  print 'Parsing ' . $amount . ' file' . ($amount > 1 ? 's' : '') . "...\n";  };

      foreach my $file (keys %$filesToParse)
          {  NaturalDocs::Parser->ParseForInformation($file);  };
      };


    # The symbol table is now fully resolved, so we can reduce its memory footprint.

    NaturalDocs::SymbolTable->PurgeResolvingInfo();


    # Load and update the menu file.  We need to do this after parsing so when it is updated, it will detect files where the
    # default menu title has changed and files that have added or deleted Natural Docs content.

    NaturalDocs::Menu->LoadAndUpdate();


    # Build any files that need it.  This needs to be run regardless of whether there are any files to build.  It will handle its own
    # output messages.

    NaturalDocs::Builder->Run();


    # Write the changes back to disk.  The menu must be saved before the project because the project file stores when the
    # menu file was last changed.

    NaturalDocs::Menu->Save();
    NaturalDocs::Project->Save();
    NaturalDocs::SymbolTable->Save();
    NaturalDocs::ClassHierarchy->Save();
    NaturalDocs::Settings->Save();

    if (!NaturalDocs::Settings->IsQuiet())
        {  print "Done.\n";  };

};

if ($EVAL_ERROR)  # Oops.
    {
    my $perlVersion;

    if ($^V)
        {  $perlVersion = sprintf('%vd', $^V);  }
    if (!$perlVersion || substr($perlVersion, 0, 1) eq '%')
        {  $perlVersion = $];  };

    die
         "\n"
         . "Natural Docs encountered the following error and was stopped:\n"
         . "\n"
         . "   " . $@
         . "\n"

         . "Also include the following information when sending an error report:\n"
         . "\n"
         . "   Natural Docs version: " . NaturalDocs::Settings->TextAppVersion() . "\n"
         . "   Perl version: " .$perlVersion . " on " . $OSNAME . "\n"
         . "\n"

         . "You can get help at the following web site:\n"
         . "\n"
         . "   " . NaturalDocs::Settings->AppURL() . "\n"
         . "\n";
    };
