###############################################################################
#
#   Package: NaturalDocs::ImageReferenceTable
#
###############################################################################
#
#   A <NaturalDocs::SourceDB>-based package that manages all the image references appearing in source files.
#
###############################################################################

# This file is part of Natural Docs, which is Copyright © 2003-2010 Greg Valure
# Natural Docs is licensed under version 3 of the GNU Affero General Public License (AGPL)
# Refer to License.txt for the complete details

use strict;
use integer;

use NaturalDocs::ImageReferenceTable::String;
use NaturalDocs::ImageReferenceTable::Reference;


package NaturalDocs::ImageReferenceTable;

use base 'NaturalDocs::SourceDB::Extension';


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

#
#   Topic: Usage
#
#       - <NaturalDocs::Project> and <NaturalDocs::SourceDB> must be initialized before this package can be used.
#
#       - Call <Register()> before using.
#
#
#   Topic: Programming Notes
#
#       When working on this code, remember that there are three things it has to juggle.
#
#       - The information in <NaturalDocs::SourceDB>.
#       - Image file references in <NaturalDocs::Project>.
#       - Source file rebuilding on changes.
#
#       Managing the actual image files will be handled between <NaturalDocs::Project> and the <NaturalDocs::Builder>
#       sub-packages.
#
#
#   Topic: Implementation
#
#       Managing image references is simpler than managing the references in <NaturalDocs::SymbolTable>.  In SymbolTable,
#       you have to worry about reference targets popping into and out of existence.  A link may go to a file that hasn't been
#       reparsed yet and the target may no longer exist.  We have to deal with that when we know it, which may be after the
#       reference's file was parsed.  Also, a new definition may appear that serves as a better interpretation of a link than its
#       current target, and again we may only know that after the reference's file has been parsed already.  So we have to deal
#       with scores and potential symbols and each symbol knowing exactly what links to it and so forth.
#
#       Not so with image references.  All possible targets (all possible image files) are known by <NaturalDocs::Project> early
#       on and will remain consistent throughout execution.  So because of that, we can get away with only storing reference
#       counts with each image and determining exactly where a reference points to as we find them.
#
#       Reference counts are stored with the image file information in <NaturalDocs::Project>.  However, it is not loaded and
#       saved to disk by it.  Rather, it is regenerated by this package when it loads <ImageReferenceTable.nd>.
#       NaturalDocs::Project only stores the last modification time (so it can add files to the build list if they've changed) and
#       whether it had any references at all on the last run (so it knows whether it should care if they've changed.)
#       ImageReferenceTable.nd stores each reference's target, width, and height.  Whether their interpretations have changed is
#       dealt with in the <Load()> function, again since the list of targets (image files) is constant.
#
#       The package is based on <NaturalDocs::SourceDB>, so read it's documentation for more information on how it works.
#


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


#
#   var: extensionID
#   The <ExtensionID> granted by <NaturalDocs::SourceDB>.
#
my $extensionID;



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


#
#   File: ImageReferenceTable.nd
#
#   The data file which stores all the image references from the last run of Natural Docs.
#
#   Format:
#
#       > [Standard Binary Header]
#
#       It starts with the standard binary header from <NaturalDocs::BinaryFile>.
#
#       > [Image Reference String or undef]
#       > [AString16: target file]
#       > [UInt16: target width or 0]
#       > [UInt16: target height or 0]
#
#       For each <ImageReferenceString>, it's target, width, and height are stored.  The target is needed so we can tell if it
#       changed from the last run, and the dimensions are needed because if the target hasn't changed but the file's dimensions
#       have, the source files need to be rebuilt.
#
#       <ImageReferenceStrings> are encoded by <NaturalDocs::ImageReferenceTable::String>.
#
#       > [AString16: definition file or undef] ...
#
#       Then comes a series of AString16s for all the files that define the reference until it hits an undef.
#
#       This whole series is repeated for each <ImageReferenceString> until it hits an undef.
#
#	Revisions:
#
#		1.4:
#
#			- The file was added to Natural Docs.
#



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


#
#   Function: Register
#   Registers the package with <NaturalDocs::SourceDB>.
#
sub Register
    {
    my $self = shift;
    $extensionID = NaturalDocs::SourceDB->RegisterExtension($self, 0);
    };


#
#   Function: Load
#
#   Loads the data from <ImageReferenceTable.nd>.  Returns whether it was successful.
#
sub Load # => bool
    {
    my $self = shift;

    if (NaturalDocs::Settings->RebuildData())
        {  return 0;  };

    # The file format hasn't changed since it was introduced.
    if (!NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('ImageReferenceTable.nd') ))
        {  return 0;  };


    # [Image Reference String or undef]
    while (my $referenceString = NaturalDocs::ImageReferenceTable::String->FromBinaryFile())
        {
        NaturalDocs::SourceDB->AddItem($extensionID, $referenceString,
                                                           NaturalDocs::ImageReferenceTable::Reference->New());

        # [AString16: target file]
        # [UInt16: target width or 0]
        # [UInt16: target height or 0]

        my $targetFile = NaturalDocs::BinaryFile->GetAString16();
        my $width = NaturalDocs::BinaryFile->GetUInt16();
        my $height = NaturalDocs::BinaryFile->GetUInt16();

        my $newTargetFile = $self->SetReferenceTarget($referenceString);
        my $newWidth;
        my $newHeight;

        if ($newTargetFile)
            {
            NaturalDocs::Project->AddImageFileReference($newTargetFile);
            ($newWidth, $newHeight) = NaturalDocs::Project->ImageFileDimensions($newTargetFile);
            };

        my $rebuildDefinitions = ($newTargetFile ne $targetFile || $newWidth != $width || $newHeight != $height);


        # [AString16: definition file or undef] ...
        while (my $definitionFile = NaturalDocs::BinaryFile->GetAString16())
            {
            NaturalDocs::SourceDB->AddDefinition($extensionID, $referenceString, $definitionFile);

            if ($rebuildDefinitions)
                {  NaturalDocs::Project->RebuildFile($definitionFile);  };
            };
        };


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


#
#   Function: Save
#
#   Saves the data to <ImageReferenceTable.nd>.
#
sub Save
    {
    my $self = shift;

    my $references = NaturalDocs::SourceDB->GetAllItemsHashRef($extensionID);

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

    while (my ($referenceString, $referenceObject) = each %$references)
        {
        # [Image Reference String or undef]
        # [AString16: target file]
        # [UInt16: target width or 0]
        # [UInt16: target height or 0]

        NaturalDocs::ImageReferenceTable::String->ToBinaryFile($referenceString);

        my $target = $referenceObject->Target();
        my ($width, $height);

        if ($target)
            {  ($width, $height) = NaturalDocs::Project->ImageFileDimensions($target);  };

        NaturalDocs::BinaryFile->WriteAString16( $referenceObject->Target() );
        NaturalDocs::BinaryFile->WriteUInt16( ($width || 0) );
        NaturalDocs::BinaryFile->WriteUInt16( ($height || 0) );

        # [AString16: definition file or undef] ...

        my $definitions = $referenceObject->GetAllDefinitionsHashRef();

        foreach my $definition (keys %$definitions)
            {  NaturalDocs::BinaryFile->WriteAString16($definition);  };

        NaturalDocs::BinaryFile->WriteAString16(undef);
        };

    NaturalDocs::ImageReferenceTable::String->ToBinaryFile(undef);

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


#
#   Function: AddReference
#
#   Adds a new image reference.
#
sub AddReference #(FileName file, string referenceText)
    {
    my ($self, $file, $referenceText) = @_;

    my $referenceString = NaturalDocs::ImageReferenceTable::String->Make($file, $referenceText);

    if (!NaturalDocs::SourceDB->HasItem($extensionID, $referenceString))
        {
        my $referenceObject = NaturalDocs::ImageReferenceTable::Reference->New();
        NaturalDocs::SourceDB->AddItem($extensionID, $referenceString, $referenceObject);

        my $target = $self->SetReferenceTarget($referenceString);
        if ($target)
            {  NaturalDocs::Project->AddImageFileReference($target);  };
        };

    NaturalDocs::SourceDB->AddDefinition($extensionID, $referenceString, $file);
    };


#
#   Function: OnDeletedDefinition
#
#   Called for each definition deleted by <NaturalDocs::SourceDB>.  This is called *after* the definition has been deleted from
#   the database, so don't expect to be able to read it.
#
sub OnDeletedDefinition #(ImageReferenceString referenceString, FileName file, bool wasLastDefinition)
    {
    my ($self, $referenceString, $file, $wasLastDefinition) = @_;

    if ($wasLastDefinition)
        {
        my $referenceObject = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString);
        my $target = $referenceObject->Target();

        if ($target)
            {  NaturalDocs::Project->DeleteImageFileReference($target);  };

        NaturalDocs::SourceDB->DeleteItem($extensionID, $referenceString);
        };
    };


#
#   Function: GetReferenceTarget
#
#   Returns the image file the reference resolves to, or undef if none.
#
#   Parameters:
#
#       sourceFile - The source <FileName> the reference appears in.
#       text - The reference text.
#
sub GetReferenceTarget #(FileName sourceFile, string text) => FileName
    {
    my ($self, $sourceFile, $text) = @_;

    my $referenceString = NaturalDocs::ImageReferenceTable::String->Make($sourceFile, $text);
    my $reference = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString);

    if (!defined $reference)
        {  return undef;  }
    else
        {  return $reference->Target();  };
    };


#
#   Function: SetReferenceTarget
#
#   Determines the best target for the passed <ImageReferenceString> and sets it on the
#   <NaturalDocs::ImageReferenceTable::Reference> object.  Returns the new target <FileName>.  Does *not* add any source
#   files to the bulid list.
#
sub SetReferenceTarget #(ImageReferenceString referenceString) => FileName
    {
    my ($self, $referenceString) = @_;

    my $referenceObject = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString);
    my ($sourcePath, $text) = NaturalDocs::ImageReferenceTable::String->InformationOf($referenceString);


    # Try the path relative to the source file first.

    my $target;

    my $imageFile = NaturalDocs::File->JoinPaths($sourcePath, $text);
    my $exists = NaturalDocs::Project->ImageFileExists($imageFile);


    # Then try relative image directories.

    if (!$exists)
        {
        my $relativeImageDirectories = NaturalDocs::Settings->RelativeImageDirectories();

        for (my $i = 0; $i < scalar @$relativeImageDirectories && !$exists; $i++)
            {
            $imageFile = NaturalDocs::File->JoinPaths($sourcePath, $relativeImageDirectories->[$i], 1);
            $imageFile = NaturalDocs::File->JoinPaths($imageFile, $text);

            $exists = NaturalDocs::Project->ImageFileExists($imageFile);
            };
        };


    # Then try absolute image directories.

    if (!$exists)
        {
        my $imageDirectories = NaturalDocs::Settings->ImageDirectories();

        for (my $i = 0; $i < scalar @$imageDirectories && !$exists; $i++)
            {
            $imageFile = NaturalDocs::File->JoinPaths($imageDirectories->[$i], $text);
            $exists = NaturalDocs::Project->ImageFileExists($imageFile);
            };
        };


    if ($exists)
        {  $target = NaturalDocs::Project->ImageFileCapitalization($imageFile);  };
    #else leave it as undef.

    $referenceObject->SetTarget($target);
    return $target;
    };


1;
