###############################################################################
#
#   Package: NaturalDocs::Settings
#
###############################################################################
#
#   A package to handle the command line and various other program settings.
#
###############################################################################

# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
# Natural Docs is licensed under the GPL

use Cwd ();

use NaturalDocs::Settings::BuildTarget;

use strict;
use integer;

package NaturalDocs::Settings;


###############################################################################
# Group: Information

=pod begin nd

    Topic: Usage and Dependencies

        - The <Constant Functions> can be called immediately.

        - Prior to initialization, <NaturalDocs::Builder> must have all its output packages registered.

        - To initialize, call <Load()>.  All functions except <InputDirectoryNameOf()> will then be available.

        - <GenerateDirectoryNames()> must be called before <InputDirectoryNameOf()> will work.  Currently it is called by
          <NaturalDocs::Menu->LoadAndUpdate()>.


    Architecture: Internal Overview

        - <Load()> first parses the command line, gathering all the settings and checking for errors.  All <NaturalDocs::Builder>
          packages must be registered before this is called because it needs their command line options.
          <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called right away if -r
          or -ro are used.

        - Output directories are *not* named at this point.  See <Named Directories>.

        - The previous settings from the last time Natural Docs was run are loaded and compared to the current settings.
          <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called if there are
          any differences that warrant it.

        - It then waits for <GenerateDirectoryNames()> to be called by <NaturalDocs::Menu>.  The reason for this is that the
          previous directory names are stored as hints in the menu file, for reasons explained in <Named Directories>.  Once that
          happens all the unnamed directories have names generated for them so everything is named.  The package is completely
          set up.

        - The input directories are stored in an array instead of a hash because the order they were declared in matters.  If two
          people use multiple input directories on separate computers without sharing a menu file, they should at least get consistent
          directory names by declaring them in the same order.


    Architecture: Named Directories

        Ever since Natural Docs introduced multiple input directories in 1.16, they've had to be named.  Since they don't necessarily
        extend from the same root anymore, they can't share an output directory without the risk of file name conflicts.  There was
        an early attempt at giving them actual names, but now they're just numbered from 1.

        Directory names aren't generated right away.  It waits for <Menu.txt> to load because that holds the obfuscated names from
        the last run.  <NaturalDocs::Menu> then calls <GenerateDirectoryNames()> and passes those along as hints.
        <GenerateDirectoryNames()> then applies them to any matches and generates new ones for any remaining.  This is done so
        that output page locations can remain consistent when built on multiple computers, so long as the menu file is shared.  I tend
        to think the menu file is the most likely configuration file to be shared.


    Architecture: Removed Directories

        Directories that were part of the previous run but aren't anymore are still stored in the package.  The primary reason, though
        there may be others, is file purging.  If an input directory is removed, all the output files that were generated from anything
        in it need to be removed.  To find out what the output file name was for a removed source file, it needs to be able to split it
        from it's original input directory and know what that directory was named.  If this didn't happen those output files would be
        orphaned, as was the case prior to 1.32.

=cut



###############################################################################
# Group: Variables


# handle: PREVIOUS_SETTINGS_FILEHANDLE
# The file handle used with <PreviousSettings.nd>.

# array: inputDirectories
# An array of input directories.
my @inputDirectories;

# array: inputDirectoryNames
# An array of the input directory names.  Each name corresponds to the directory of the same index in <inputDirectories>.
my @inputDirectoryNames;

# array: imageDirectories
# An array of image directories.
my @imageDirectories;

# array: imageDirectoryNames
# An array of the image directory names.  Each name corresponds to the directory of the same index in <imageDirectories>.
my @imageDirectoryNames;

# array: relativeImageDirectories
# An array of the relative paths for images.  The asterisks found in the command line are not present.
my @relativeImageDirectories;

# array: excludedInputDirectories
# An array of input directories to exclude.
my @excludedInputDirectories;

# array: removedInputDirectories
# An array of input directories that were once in the command line but are no longer.
my @removedInputDirectories;

# array: removedInputDirectoryNames
# An array of the removed input directories' names.  Each name corresponds to the directory of the same index in
# <removedInputDirectories>.
my @removedInputDirectoryNames;

# array: removedImageDirectories
# An array of image directories that were once in the command line but are no longer.
my @removedImageDirectories;

# array: removedImageDirectoryNames
# An array of the removed image directories' names.  Each name corresponds to the directory of the same index in
# <removedImageDirectories>.
my @removedImageDirectoryNames;

# var: projectDirectory
# The project directory.
my $projectDirectory;

# array: buildTargets
# An array of <NaturalDocs::Settings::BuildTarget>s.
my @buildTargets;

# var: documentedOnly
# Whether undocumented code aspects should be included in the output.
my $documentedOnly;

# int: tabLength
# The number of spaces in tabs.
my $tabLength;

# bool: noAutoGroup
# Whether auto-grouping is turned off.
my $noAutoGroup;

# bool: onlyFileTitles
# Whether source files should always use the file name as the title.
my $onlyFileTitles;

# bool: isQuiet
# Whether the script should be run in quiet mode or not.
my $isQuiet;

# bool: rebuildData
# WHether most data files should be ignored and rebuilt.
my $rebuildData;

# array: styles
# An array of style names to use, most important first.
my @styles;

# var: charset
# The character encoding of the source files, and thus the output.
my $charset;


###############################################################################
# Group: Files


#
#   File: PreviousSettings.nd
#
#   Stores the previous command line settings.
#
#   Format:
#
#       > [BINARY_FORMAT]
#       > [VersionInt: app version]
#
#       The file starts with the standard <BINARY_FORMAT> <VersionInt> header.
#
#       > [UInt8: tab length]
#       > [UInt8: documented only (0 or 1)]
#       > [UInt8: no auto-group (0 or 1)]
#       > [UInt8: only file titles (0 or 1)]
#       > [AString16: charset]
#       >
#       > [UInt8: number of input directories]
#       > [AString16: input directory] [AString16: input directory name] ...
#
#       A count of input directories, then that number of directory/name pairs.
#
#       > [UInt8: number of output targets]
#       > [AString16: output directory] [AString16: output format command line option] ...
#
#       A count of output targets, then that number of directory/format pairs.
#
#
#   Revisions:
#
#       1.4:
#
#           - Added only file titles.
#
#       1.33:
#
#           - Added charset.
#
#       1.3:
#
#           - Removed headers-only, which was a 0/1 UInt8 after tab length.
#           - Change auto-group level (1 = no, 2 = yes, 3 = full only) to no auto-group (0 or 1).
#
#       1.22:
#
#           - Added auto-group level.
#
#       1.2:
#
#           - File was added to the project.  Prior to 1.2, it didn't exist.
#


###############################################################################
# Group: Action Functions

#
#   Function: Load
#
#   Loads and parses all settings from the command line and configuration files.  Will exit if the options are invalid or the syntax
#   reference was requested.
#
sub Load
    {
    my ($self) = @_;

    $self->ParseCommandLine();
    $self->LoadAndComparePreviousSettings();
    };


#
#   Function: Save
#
#   Saves all settings in configuration files to disk.
#
sub Save
    {
    my ($self) = @_;

    $self->SavePreviousSettings();
    };


#
#   Function: GenerateDirectoryNames
#
#   Generates names for each of the input and image directories, which can later be retrieved with <InputDirectoryNameOf()>
#   and <ImageDirectoryNameOf()>.
#
#   Parameters:
#
#       inputHints - A hashref of suggested input directory names, where the keys are the directories and the values are the names.
#                        These take precedence over anything generated.  You should include names for directories that are no longer in
#                        the command line.  This parameter may be undef.
#       imageHints - Same as inputHints, only for the image directories.
#
sub GenerateDirectoryNames #(hashref inputHints, hashref imageHints)
    {
    my ($self, $inputHints, $imageHints) = @_;

    my %usedInputNames;
    my %usedImageNames;


    if (defined $inputHints)
        {
        # First, we have to convert all non-numeric names to numbers, since they may come from a pre-1.32 menu file.  We do it
        # here instead of in NaturalDocs::Menu to keep the naming scheme centralized.

        my @names = values %$inputHints;
        my $hasNonNumeric;

        foreach my $name (@names)
            {
            if ($name !~ /^[0-9]+$/)
                {
                $hasNonNumeric = 1;
                last;
                };
            };


        if ($hasNonNumeric)
            {
            # Hash mapping old names to new names.
            my %conversion;

            # The sequential number to use.  Starts at two because we want 'default' to be one.
            my $currentNumber = 2;

            # If there's only one name, we set it to one no matter what it was set to before.
            if (scalar @names == 1)
                {  $conversion{$names[0]} = 1;  }
            else
                {
                # We sort the list first because we want the end result to be predictable.  This conversion could be happening on many
                # machines, and they may not all specify the input directories in the same order.  They need to all come up with the
                # same result.
                @names = sort @names;

                foreach my $name (@names)
                    {
                    if ($name eq 'default')
                        {  $conversion{$name} = 1;  }
                    else
                        {
                        $conversion{$name} = $currentNumber;
                        $currentNumber++;
                        };
                    };
                };

            # Convert them to the new names.
            foreach my $directory (keys %$inputHints)
                {
                $inputHints->{$directory} = $conversion{ $inputHints->{$directory} };
                };
            };


        # Now we apply all the names from the hints, and save any unused ones as removed directories.

        for (my $i = 0; $i < scalar @inputDirectories; $i++)
            {
            if (exists $inputHints->{$inputDirectories[$i]})
                {
                $inputDirectoryNames[$i] = $inputHints->{$inputDirectories[$i]};
                $usedInputNames{ $inputDirectoryNames[$i] } = 1;
                delete $inputHints->{$inputDirectories[$i]};
                };
            };


        # Any remaining hints are saved as removed directories.

        while (my ($directory, $name) = each %$inputHints)
            {
            push @removedInputDirectories, $directory;
            push @removedInputDirectoryNames, $name;
            };
        };


    if (defined $imageHints)
        {
        # Image directory names were never non-numeric, so there is no conversion.  Apply all the names from the hints.

        for (my $i = 0; $i < scalar @imageDirectories; $i++)
            {
            if (exists $imageHints->{$imageDirectories[$i]})
                {
                $imageDirectoryNames[$i] = $imageHints->{$imageDirectories[$i]};
                $usedImageNames{ $imageDirectoryNames[$i] } = 1;
                delete $imageHints->{$imageDirectories[$i]};
                };
            };


        # Any remaining hints are saved as removed directories.

        while (my ($directory, $name) = each %$imageHints)
            {
            push @removedImageDirectories, $directory;
            push @removedImageDirectoryNames, $name;
            };
        };


    # Now we generate names for anything remaining.

    my $inputCounter = 1;

    for (my $i = 0; $i < scalar @inputDirectories; $i++)
        {
        if (!defined $inputDirectoryNames[$i])
            {
            while (exists $usedInputNames{$inputCounter})
                {  $inputCounter++;  };

            $inputDirectoryNames[$i] = $inputCounter;
            $usedInputNames{$inputCounter} = 1;

            $inputCounter++;
            };
        };


    my $imageCounter = 1;

    for (my $i = 0; $i < scalar @imageDirectories; $i++)
        {
        if (!defined $imageDirectoryNames[$i])
            {
            while (exists $usedImageNames{$imageCounter})
                {  $imageCounter++;  };

            $imageDirectoryNames[$i] = $imageCounter;
            $usedImageNames{$imageCounter} = 1;

            $imageCounter++;
            };
        };
    };



###############################################################################
# Group: Information Functions


#
#   Function: InputDirectories
#
#   Returns an arrayref of input directories.  Do not change.
#
#   This will not return any removed input directories.
#
sub InputDirectories
    {  return \@inputDirectories;  };

#
#   Function: InputDirectoryNameOf
#
#   Returns the generated name of the passed input directory.  <GenerateDirectoryNames()> must be called once before this
#   function is available.
#
#   If a name for a removed input directory is available, it will be returned as well.
#
sub InputDirectoryNameOf #(directory)
    {
    my ($self, $directory) = @_;

    for (my $i = 0; $i < scalar @inputDirectories; $i++)
        {
        if ($directory eq $inputDirectories[$i])
            {  return $inputDirectoryNames[$i];  };
        };

    for (my $i = 0; $i < scalar @removedInputDirectories; $i++)
        {
        if ($directory eq $removedInputDirectories[$i])
            {  return $removedInputDirectoryNames[$i];  };
        };

    return undef;
    };


#
#   Function: SplitFromInputDirectory
#
#   Takes an input file name and returns the array ( inputDirectory, relativePath ).
#
#   If the file cannot be split from an input directory, it will try to do it with the removed input directories.
#
sub SplitFromInputDirectory #(file)
    {
    my ($self, $file) = @_;

    foreach my $directory (@inputDirectories, @removedInputDirectories)
        {
        if (NaturalDocs::File->IsSubPathOf($directory, $file))
            {  return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) );  };
        };

    return ( );
    };


#
#   Function: ImageDirectories
#
#   Returns an arrayref of image directories.  Do not change.
#
#   This will not return any removed image directories.
#
sub ImageDirectories
    {  return \@imageDirectories;  };


#
#   Function: ImageDirectoryNameOf
#
#   Returns the generated name of the passed image or input directory.  <GenerateDirectoryNames()> must be called once before
#   this function is available.
#
#   If a name for a removed input or image directory is available, it will be returned as well.
#
sub ImageDirectoryNameOf #(directory)
    {
    my ($self, $directory) = @_;

    for (my $i = 0; $i < scalar @imageDirectories; $i++)
        {
        if ($directory eq $imageDirectories[$i])
            {  return $imageDirectoryNames[$i];  };
        };

    for (my $i = 0; $i < scalar @removedImageDirectories; $i++)
        {
        if ($directory eq $removedImageDirectories[$i])
            {  return $removedImageDirectoryNames[$i];  };
        };

    return undef;
    };


#
#   Function: SplitFromImageDirectory
#
#   Takes an input image file name and returns the array ( imageDirectory, relativePath ).
#
#   If the file cannot be split from an image directory, it will try to do it with the removed image directories.
#
sub SplitFromImageDirectory #(file)
    {
    my ($self, $file) = @_;

    foreach my $directory (@imageDirectories, @removedImageDirectories)
        {
        if (NaturalDocs::File->IsSubPathOf($directory, $file))
            {  return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) );  };
        };

    return ( );
    };


#
#   Function: RelativeImageDirectories
#
#   Returns an arrayref of relative image directories.  Do not change.
#
sub RelativeImageDirectories
    {  return \@relativeImageDirectories;  };


# Function: ExcludedInputDirectories
# Returns an arrayref of input directories to exclude.  Do not change.
sub ExcludedInputDirectories
    {  return \@excludedInputDirectories;  };


# Function: BuildTargets
# Returns an arrayref of <NaturalDocs::Settings::BuildTarget>s.  Do not change.
sub BuildTargets
    {  return \@buildTargets;  };


#
#   Function: OutputDirectoryOf
#
#   Returns the output directory of a builder object.
#
#   Parameters:
#
#       object - The builder object, whose class is derived from <NaturalDocs::Builder::Base>.
#
#   Returns:
#
#       The builder directory, or undef if the object wasn't found..
#
sub OutputDirectoryOf #(object)
    {
    my ($self, $object) = @_;

    foreach my $buildTarget (@buildTargets)
        {
        if ($buildTarget->Builder() == $object)
            {  return $buildTarget->Directory();  };
        };

    return undef;
    };


# Function: Styles
# Returns an arrayref of the styles associated with the output.
sub Styles
    {  return \@styles;  };

# Function: ProjectDirectory
# Returns the project directory.
sub ProjectDirectory
    {  return $projectDirectory;  };

# Function: ProjectDataDirectory
# Returns the project data directory.
sub ProjectDataDirectory
    {  return NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1);  };

# Function: StyleDirectory
# Returns the main style directory.
sub StyleDirectory
    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Styles', 1);  };

# Function: JavaScriptDirectory
# Returns the main JavaScript directory.
sub JavaScriptDirectory
    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'JavaScript', 1);  };

# Function: ConfigDirectory
# Returns the main configuration directory.
sub ConfigDirectory
    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Config', 1);  };

# Function: DocumentedOnly
# Returns whether undocumented code aspects should be included in the output.
sub DocumentedOnly
    {  return $documentedOnly;  };

# Function: TabLength
# Returns the number of spaces tabs should be expanded to.
sub TabLength
    {  return $tabLength;  };

# Function: NoAutoGroup
# Returns whether auto-grouping is turned off.
sub NoAutoGroup
    {  return $noAutoGroup;  };

# Function: OnlyFileTitles
# Returns whether source files should always use the file name as the title.
sub OnlyFileTitles
    {  return $onlyFileTitles;  };

# Function: IsQuiet
# Returns whether the script should be run in quiet mode or not.
sub IsQuiet
    {  return $isQuiet;  };

# Function: RebuildData
# Returns whether all data files should be ignored and rebuilt.
sub RebuildData
    {  return $rebuildData;  };

# Function: CharSet
# Returns the character set, or undef if none.
sub CharSet
    {  return $charset;  };


###############################################################################
# Group: Constant Functions

#
#   Function: AppVersion
#
#   Returns Natural Docs' version number as an integer.  Use <TextAppVersion()> to get a printable version.
#
sub AppVersion
    {
    my ($self) = @_;
    return NaturalDocs::Version->FromString($self->TextAppVersion());
    };

#
#   Function: TextAppVersion
#
#   Returns Natural Docs' version number as plain text.
#
sub TextAppVersion
    {
    return '1.4';
    };

#
#   Function: AppURL
#
#   Returns a string of the project's current web address.
#
sub AppURL
    {  return 'http://www.naturaldocs.org';  };



###############################################################################
# Group: Support Functions


#
#   Function: ParseCommandLine
#
#   Parses and validates the command line.  Will cause the script to exit if the options ask for the syntax reference or
#   are invalid.
#
sub ParseCommandLine
    {
    my ($self) = @_;

    my %synonyms = ( 'input'    => '-i',
                                  'source' => '-i',
                                  'excludeinput' => '-xi',
                                  'excludesource' => '-xi',
                                  'images' => '-img',
                                  'output'  => '-o',
                                  'project' => '-p',
                                  'documentedonly' => '-do',
                                  'style'    => '-s',
                                  'rebuild' => '-r',
                                  'rebuildoutput' => '-ro',
                                  'tablength' => '-t',
                                  'quiet'    => '-q',
                                  'headersonly' => '-ho',
                                  'help'     => '-h',
                                  'autogroup' => '-ag',
                                  'noautogroup' => '-nag',
                                  'onlyfiletitles' => '-oft',
                                  'onlyfiletitle' => '-oft',
                                  'charset' => '-cs',
                                  'characterset' => '-cs' );


    my @errorMessages;

    my $valueRef;
    my $option;

    my @outputStrings;
    my @imageStrings;


    # Sometimes $valueRef is set to $ignored instead of undef because we don't want certain errors to cause other,
    # unnecessary errors.  For example, if they set the input directory twice, we want to show that error and swallow the
    # specified directory without complaint.  Otherwise it would complain about the directory too as if it were random crap
    # inserted into the command line.
    my $ignored;

    my $index = 0;

    while ($index < scalar @ARGV)
        {
        my $arg = $ARGV[$index];

        if (substr($arg, 0, 1) eq '-')
            {
            $option = lc($arg);

            # Support options like -t2 as well as -t 2.
            if ($option =~ /^([^0-9]+)([0-9]+)$/)
                {
                $option = $1;
                splice(@ARGV, $index + 1, 0, $2);
                };

            # Convert long forms to short.
            if (substr($option, 1, 1) eq '-')
                {
                # Strip all dashes.
                my $newOption = $option;
                $newOption =~ tr/-//d;

                if (exists $synonyms{$newOption})
                    {  $option = $synonyms{$newOption};  }
                }

            if ($option eq '-i')
                {
                push @inputDirectories, undef;
                $valueRef = \$inputDirectories[-1];
                }
            elsif ($option eq '-xi')
                {
                push @excludedInputDirectories, undef;
                $valueRef = \$excludedInputDirectories[-1];
                }
            elsif ($option eq '-img')
                {
                push @imageStrings, undef;
                $valueRef = \$imageStrings[-1];
                }
            elsif ($option eq '-p')
                {
                if (defined $projectDirectory)
                    {
                    push @errorMessages, 'You cannot have more than one project directory.';
                    $valueRef = \$ignored;
                    }
                else
                    {  $valueRef = \$projectDirectory;  };
                }
            elsif ($option eq '-o')
                {
                push @outputStrings, undef;
                $valueRef = \$outputStrings[-1];
                }
            elsif ($option eq '-s')
                {
                $valueRef = \$styles[0];
                }
            elsif ($option eq '-t')
                {
                $valueRef = \$tabLength;
                }
            elsif ($option eq '-cs')
                {
                $valueRef = \$charset;
                }
            elsif ($option eq '-ag')
                {
                push @errorMessages, 'The -ag setting is no longer supported.  You can use -nag (--no-auto-group) to turn off '
                                               . "auto-grouping, but there aren't multiple levels anymore.";
                $valueRef = \$ignored;
                }

            # Options that aren't followed by content.
            else
                {
                $valueRef = undef;

                if ($option eq '-r')
                    {
                    NaturalDocs::Project->ReparseEverything();
                    NaturalDocs::Project->RebuildEverything();
                    $rebuildData = 1;
                    }
                elsif ($option eq '-ro')
                    {
                    NaturalDocs::Project->RebuildEverything();
                    }
                elsif ($option eq '-do')
                    {  $documentedOnly = 1;  }
                elsif ($option eq '-oft')
                    {  $onlyFileTitles = 1;  }
                elsif ($option eq '-q')
                    {  $isQuiet = 1;  }
                elsif ($option eq '-ho')
                    {
                    push @errorMessages, 'The -ho setting is no longer supported.  You can have Natural Docs skip over the source file '
                                                   . 'extensions by editing Languages.txt in your project directory.';
                    }
                elsif ($option eq '-nag')
                    {  $noAutoGroup = 1;  }
                elsif ($option eq '-?' || $option eq '-h')
                    {
                    $self->PrintSyntax();
                    exit;
                    }
                else
                    {  push @errorMessages, 'Unrecognized option ' . $option;  };

                };

            }

        # Is a segment of text, not an option...
        else
            {
            if (defined $valueRef)
                {
                # We want to preserve spaces in paths.
                if (defined $$valueRef)
                    {  $$valueRef .= ' ';  };

                $$valueRef .= $arg;
                }

            else
                {
                push @errorMessages, 'Unrecognized element ' . $arg;
                };
            };

        $index++;
        };


    # Validate the style, if specified.

    if ($styles[0])
        {
        my @stylePieces = split(/ +/, $styles[0]);
        @styles = ( );

        while (scalar @stylePieces)
            {
            if (lc($stylePieces[0]) eq 'custom')
                {
                push @errorMessages, 'The "Custom" style setting is no longer supported.  Copy your custom style sheet to your '
                                               . 'project directory and you can refer to it with -s.';
                shift @stylePieces;
                }
            else
                {
                # People may use styles with spaces in them.  If a style doesn't exist, we need to join the pieces until we find one that
                # does or we run out of pieces.

                my $extras = 0;
                my $success;

                while ($extras < scalar @stylePieces)
                    {
                    my $style;

                    if (!$extras)
                        {  $style = $stylePieces[0];  }
                    else
                        {  $style = join(' ', @stylePieces[0..$extras]);  };

                    my $cssFile = NaturalDocs::File->JoinPaths( $self->StyleDirectory(), $style . '.css' );
                    if (-e $cssFile)
                        {
                        push @styles, $style;
                        splice(@stylePieces, 0, 1 + $extras);
                        $success = 1;
                        last;
                        }
                    else
                        {
                        $cssFile = NaturalDocs::File->JoinPaths( $self->ProjectDirectory(), $style . '.css' );

                        if (-e $cssFile)
                            {
                            push @styles, $style;
                            splice(@stylePieces, 0, 1 + $extras);
                            $success = 1;
                            last;
                            }
                        else
                            {  $extras++;  };
                        };
                    };

                if (!$success)
                    {
                    push @errorMessages, 'The style "' . $stylePieces[0] . '" does not exist.';
                    shift @stylePieces;
                    };
                };
            };
        }
    else
        {  @styles = ( 'Default' );  };


    # Decode and validate the output strings.

    my %outputDirectories;

    foreach my $outputString (@outputStrings)
        {
        my ($format, $directory) = split(/ /, $outputString, 2);

        if (!defined $directory)
            {  push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]';  }
        else
            {
            if (!NaturalDocs::File->PathIsAbsolute($directory))
                {  $directory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $directory, 1);  };

            $directory = NaturalDocs::File->CanonizePath($directory);

            if (! -e $directory || ! -d $directory)
                {
                # They may have forgotten the format portion and the directory name had a space in it.
                if (-e ($format . ' ' . $directory) && -d ($format . ' ' . $directory))
                    {
                    push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]';
                    $format = undef;
                    }
                else
                    {  push @errorMessages, 'The output directory ' . $directory . ' does not exist.';  }
                }
            elsif (exists $outputDirectories{$directory})
                {  push @errorMessages, 'You cannot specify the output directory ' . $directory . ' more than once.';  }
            else
                {  $outputDirectories{$directory} = 1;  };

            if (defined $format)
                {
                my $builderPackage = NaturalDocs::Builder->OutputPackageOf($format);

                if (defined $builderPackage)
                    {
                    push @buildTargets,
                            NaturalDocs::Settings::BuildTarget->New($builderPackage->New(), $directory);
                    }
                else
                    {
                    push @errorMessages, 'The output format ' . $format . ' doesn\'t exist or is not installed.';
                    $valueRef = \$ignored;
                    };
                };
            };
        };

    if (!scalar @buildTargets)
        {  push @errorMessages, 'You did not specify an output directory.';  };


    # Decode and validate the image strings.

    foreach my $imageString (@imageStrings)
        {
        if ($imageString =~ /^ *\*/)
            {
            # The below NaturalDocs::File functions assume everything is canonized.
            $imageString = NaturalDocs::File->CanonizePath($imageString);

            my ($volume, $directoryString) = NaturalDocs::File->SplitPath($imageString, 1);
            my @directories = NaturalDocs::File->SplitDirectories($directoryString);

            shift @directories;

            $directoryString = NaturalDocs::File->JoinDirectories(@directories);
            push @relativeImageDirectories, NaturalDocs::File->JoinPath($volume, $directoryString);
            }
        else
            {
            if (!NaturalDocs::File->PathIsAbsolute($imageString))
                {  $imageString = NaturalDocs::File->JoinPaths(Cwd::cwd(), $imageString, 1);  };

            $imageString = NaturalDocs::File->CanonizePath($imageString);

            if (! -e $imageString || ! -d $imageString)
                {  push @errorMessages, 'The image directory ' . $imageString . ' does not exist.';  };

            push @imageDirectories, $imageString;
            };
        };


    # Make sure the input and project directories are specified, canonized, and exist.

    if (scalar @inputDirectories)
        {
        for (my $i = 0; $i < scalar @inputDirectories; $i++)
            {
            if (!NaturalDocs::File->PathIsAbsolute($inputDirectories[$i]))
                {  $inputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $inputDirectories[$i], 1);  };

            $inputDirectories[$i] = NaturalDocs::File->CanonizePath($inputDirectories[$i]);

            if (! -e $inputDirectories[$i] || ! -d $inputDirectories[$i])
                {  push @errorMessages, 'The input directory ' . $inputDirectories[$i] . ' does not exist.';  };
            };
        }
    else
        {  push @errorMessages, 'You did not specify an input (source) directory.';  };

    if (defined $projectDirectory)
        {
        if (!NaturalDocs::File->PathIsAbsolute($projectDirectory))
            {  $projectDirectory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $projectDirectory, 1);  };

        $projectDirectory = NaturalDocs::File->CanonizePath($projectDirectory);

        if (! -e $projectDirectory || ! -d $projectDirectory)
            {  push @errorMessages, 'The project directory ' . $projectDirectory . ' does not exist.';  };

        # Create the Data subdirectory if it doesn't exist.
        NaturalDocs::File->CreatePath( NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1) );
        }
    else
        {  push @errorMessages, 'You did not specify a project directory.';  };


    # Make sure the excluded input directories are canonized, and add the project and output directories to the list.

    for (my $i = 0; $i < scalar @excludedInputDirectories; $i++)
        {
        if (!NaturalDocs::File->PathIsAbsolute($excludedInputDirectories[$i]))
            {  $excludedInputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $excludedInputDirectories[$i], 1);  };

        $excludedInputDirectories[$i] = NaturalDocs::File->CanonizePath($excludedInputDirectories[$i]);
        };

    push @excludedInputDirectories, $projectDirectory;

    foreach my $buildTarget (@buildTargets)
        {
        push @excludedInputDirectories, $buildTarget->Directory();
        };


    # Determine the tab length, and default to four if not specified.

    if (defined $tabLength)
        {
        if ($tabLength !~ /^[0-9]+$/)
            {  push @errorMessages, 'The tab length must be a number.';  };
        }
    else
        {  $tabLength = 4;  };


    # Strip any quotes off of the charset.
    $charset =~ tr/\"//d;


    # Exit with the error message if there was one.

    if (scalar @errorMessages)
        {
        print join("\n", @errorMessages) . "\nType NaturalDocs -h to see the syntax reference.\n";
        exit;
        };
    };

#
#   Function: PrintSyntax
#
#   Prints the syntax reference.
#
sub PrintSyntax
    {
    my ($self) = @_;

    # Make sure all line lengths are under 80 characters.

    print

    "Natural Docs, version " . $self->TextAppVersion() . "\n"
    . $self->AppURL() . "\n"
    . "This program is licensed under the GPL\n"
    . "--------------------------------------\n"
    . "\n"
    . "Syntax:\n"
    . "\n"
    . "    NaturalDocs -i [input (source) directory]\n"
    . "               (-i [input (source) directory] ...)\n"
    . "                -o [output format] [output directory]\n"
    . "               (-o [output format] [output directory] ...)\n"
    . "                -p [project directory]\n"
    . "                [options]\n"
    . "\n"
    . "Examples:\n"
    . "\n"
    . "    NaturalDocs -i C:\\My Project\\Source -o HTML C:\\My Project\\Docs\n"
    . "                -p C:\\My Project\\Natural Docs\n"
    . "    NaturalDocs -i /src/project -o HTML /doc/project\n"
    . "                -p /etc/naturaldocs/project -s Small -q\n"
    . "\n"
    . "Required Parameters:\n"
    . "\n"
    . " -i [dir]\n--input [dir]\n--source [dir]\n"
    . "     Specifies an input (source) directory.  Required.\n"
    . "     Can be specified multiple times.\n"
    . "\n"
    . " -o [fmt] [dir]\n--output [fmt] [dir]\n"
    . "    Specifies an output format and directory.  Required.\n"
    . "    Can be specified multiple times, but only once per directory.\n"
    . "    Possible output formats:\n";

    $self->PrintOutputFormats('    - ');

    print
    "\n"
    . " -p [dir]\n--project [dir]\n"
    . "    Specifies the project directory.  Required.\n"
    . "    There needs to be a unique project directory for every source directory.\n"
    . "\n"
    . "Optional Parameters:\n"
    . "\n"
    . " -s [style] ([style] [style] ...)\n--style [style] ([style] [style] ...)\n"
    . "    Specifies the CSS style when building HTML output.  If multiple styles are\n"
    . "    specified, they will all be included in the order given.\n"
    . "\n"
    . " -img [image directory]\n--image [image directory]"
    . "    Specifies an image directory.  Can be specified multiple times.\n"
    . "    Start with * to specify a relative directory, as in -img */images.\n"
    . "\n"
    . " -do\n--documented-only\n"
    . "    Specifies only documented code aspects should be included in the output.\n"
    . "\n"
    . " -t [len]\n--tab-length [len]\n"
    . "    Specifies the number of spaces tabs should be expanded to.  This only needs\n"
    . "    to be set if you use tabs in example code and text diagrams.  Defaults to 4.\n"
    . "\n"
    . " -xi [dir]\n--exclude-input [dir]\n--exclude-source [dir]\n"
    . "    Excludes an input (source) directory from the documentation.\n"
    . "    Automatically done for the project and output directories.  Can\n"
    . "    be specified multiple times.\n"
    . "\n"
    . " -nag\n--no-auto-group\n"
    . "    Turns off auto-grouping completely.\n"
    . "\n"
    . " -oft\n--only-file-titles\n"
    . "    Source files will only use the file name as the title.\n"
    . "\n"
    . " -r\n--rebuild\n"
    . "    Rebuilds all output and data files from scratch.\n"
    . "    Does not affect the menu file.\n"
    . "\n"
    . " -ro\n--rebuild-output\n"
    . "    Rebuilds all output files from scratch.\n"
    . "\n"
    . " -q\n--quiet\n"
    . "    Suppresses all non-error output.\n"
    . "\n"
    . " -?\n -h\n--help\n"
    . "    Displays this syntax reference.\n";
    };


#
#   Function: PrintOutputFormats
#
#   Prints all the possible output formats that can be specified with -o.  Each one will be placed on its own line.
#
#   Parameters:
#
#       prefix - Characters to prefix each one with, such as for indentation.
#
sub PrintOutputFormats #(prefix)
    {
    my ($self, $prefix) = @_;

    my $outputPackages = NaturalDocs::Builder::OutputPackages();

    foreach my $outputPackage (@$outputPackages)
        {
        print $prefix . $outputPackage->CommandLineOption() . "\n";
        };
    };


#
#   Function: LoadAndComparePreviousSettings
#
#   Loads <PreviousSettings.nd> and compares the values there with those in the command line.  If differences require it,
#   sets <rebuildData> and/or <rebuildOutput>.
#
sub LoadAndComparePreviousSettings
    {
    my ($self) = @_;

    my $fileIsOkay;

    if (!NaturalDocs::Settings->RebuildData())
        {
        my $version;

        if (NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('PreviousSettings.nd'),
                                                                           NaturalDocs::Version->FromString('1.4') ))
            {  $fileIsOkay = 1;  };
        };

    if (!$fileIsOkay)
        {
        # We need to reparse everything because --documented-only may have changed.
        # We need to rebuild everything because --tab-length may have changed.
        NaturalDocs::Project->ReparseEverything();
        NaturalDocs::Project->RebuildEverything();
        }
    else
        {
        my $raw;

        # [UInt8: tab expansion]
        # [UInt8: documented only (0 or 1)]
        # [UInt8: no auto-group (0 or 1)]
        # [UInt8: only file titles (0 or 1)]
        # [AString16: charset]

        my $prevTabLength = NaturalDocs::BinaryFile->GetUInt8();
        my $prevDocumentedOnly = NaturalDocs::BinaryFile->GetUInt8();
        my $prevNoAutoGroup = NaturalDocs::BinaryFile->GetUInt8();
        my $prevOnlyFileTitles = NaturalDocs::BinaryFile->GetUInt8();
        my $prevCharset = NaturalDocs::BinaryFile->GetAString16();

        if ($prevTabLength != $self->TabLength())
            {
            # We need to rebuild all output because this affects all text diagrams.
            NaturalDocs::Project->RebuildEverything();
            };

        if ($prevDocumentedOnly == 0)
            {  $prevDocumentedOnly = undef;  };
        if ($prevNoAutoGroup == 0)
            {  $prevNoAutoGroup = undef;  };
        if ($prevOnlyFileTitles == 0)
            {  $prevOnlyFileTitles = undef;  };

        if ($prevDocumentedOnly != $self->DocumentedOnly() ||
            $prevNoAutoGroup != $self->NoAutoGroup() ||
            $prevOnlyFileTitles != $self->OnlyFileTitles())
            {
            NaturalDocs::Project->ReparseEverything();
            };

        if ($prevCharset ne $charset)
            {  NaturalDocs::Project->RebuildEverything();  };


        # [UInt8: number of input directories]

        my $inputDirectoryCount = NaturalDocs::BinaryFile->GetUInt8();

        while ($inputDirectoryCount)
            {
            # [AString16: input directory] [AString16: input directory name] ...

            my $inputDirectory = NaturalDocs::BinaryFile->GetAString16();
            my $inputDirectoryName = NaturalDocs::BinaryFile->GetAString16();

            # Not doing anything with this for now.

            $inputDirectoryCount--;
            };


        # [UInt8: number of output targets]

        my $outputTargetCount = NaturalDocs::BinaryFile->GetUInt8();

        # Keys are the directories, values are the command line options.
        my %previousOutputDirectories;

        while ($outputTargetCount)
            {
            # [AString16: output directory] [AString16: output format command line option] ...

            my $outputDirectory = NaturalDocs::BinaryFile->GetAString16();
            my $outputCommand = NaturalDocs::BinaryFile->GetAString16();

            $previousOutputDirectories{$outputDirectory} = $outputCommand;

            $outputTargetCount--;
            };

        # Check if any targets were added to the command line, or if their formats changed.  We don't care if targets were
        # removed.
        my $buildTargets = $self->BuildTargets();

        foreach my $buildTarget (@$buildTargets)
            {
            if (!exists $previousOutputDirectories{$buildTarget->Directory()} ||
                $buildTarget->Builder()->CommandLineOption() ne $previousOutputDirectories{$buildTarget->Directory()})
                {
                NaturalDocs::Project->RebuildEverything();
                last;
                };
            };

        NaturalDocs::BinaryFile->Close();
        };
    };


#
#   Function: SavePreviousSettings
#
#   Saves the settings into <PreviousSettings.nd>.
#
sub SavePreviousSettings
    {
    my ($self) = @_;

    NaturalDocs::BinaryFile->OpenForWriting(  NaturalDocs::Project->DataFile('PreviousSettings.nd') );

    # [UInt8: tab length]
    # [UInt8: documented only (0 or 1)]
    # [UInt8: no auto-group (0 or 1)]
    # [UInt8: only file titles (0 or 1)]
    # [AString16: charset]
    # [UInt8: number of input directories]

    my $inputDirectories = $self->InputDirectories();

    NaturalDocs::BinaryFile->WriteUInt8($self->TabLength());
    NaturalDocs::BinaryFile->WriteUInt8($self->DocumentedOnly() ? 1 : 0);
    NaturalDocs::BinaryFile->WriteUInt8($self->NoAutoGroup() ? 1 : 0);
    NaturalDocs::BinaryFile->WriteUInt8($self->OnlyFileTitles() ? 1 : 0);
    NaturalDocs::BinaryFile->WriteAString16($charset);
    NaturalDocs::BinaryFile->WriteUInt8(scalar @$inputDirectories);

    foreach my $inputDirectory (@$inputDirectories)
        {
        my $inputDirectoryName = $self->InputDirectoryNameOf($inputDirectory);

        # [AString16: input directory] [AString16: input directory name] ...
        NaturalDocs::BinaryFile->WriteAString16($inputDirectory);
        NaturalDocs::BinaryFile->WriteAString16($inputDirectoryName);
        };

    # [UInt8: number of output targets]

    my $buildTargets = $self->BuildTargets();
    NaturalDocs::BinaryFile->WriteUInt8(scalar @$buildTargets);

    foreach my $buildTarget (@$buildTargets)
        {
        # [AString16: output directory] [AString16: output format command line option] ...
        NaturalDocs::BinaryFile->WriteAString16( $buildTarget->Directory() );
        NaturalDocs::BinaryFile->WriteAString16( $buildTarget->Builder()->CommandLineOption() );
        };

    NaturalDocs::BinaryFile->Close();
    };


1;