#!perl -w
package Moz::Jar;

#
# Module for creating jar files, either using a jar manifest, or
# simply jarring up folders on disk.
#

require 5.004;
require Exporter;

use strict;
use Archive::Zip;
use File::Path;

use Mac::Files;

use Moz::Moz;
use Moz::Milestone;

use vars qw( @ISA @EXPORT );

@ISA        = qw(Exporter);
@EXPORT     = qw(
                    CreateJarFileFromDirectory
                    CreateJarFromManifest
                    WriteOutJarFiles
                    SanityCheckJarOptions
                );


#-------------------------------------------------------------------------------
# Add the contents of a directory to the zip file
#
#-------------------------------------------------------------------------------
sub _addDirToJar($$$$)
{
    my($dir, $jar_root, $zip, $compress) = @_;

    opendir(DIR, $dir) or die "Error: Cannot open dir $dir\n";
    my @files = readdir(DIR);
    closedir DIR;
    
    my $unix_jar_root = $jar_root;
    $unix_jar_root =~ s|:|/|g;       # colon to slash conversion
    
    my $file;
    
    foreach $file (@files)
    {        
        my $filepath = $dir.":".$file;
        
        if (-d $filepath)
        {
            print "Adding files to jar from $filepath\n";            
            _addDirToJar($filepath, $jar_root, $zip, $compress);
        }
        else
        {
            my $member = Archive::Zip::Member->newFromFile($filepath);
            die "Error: Failed to create zip file member $filepath\n" unless $member;
            
            my $unixName = $filepath;
            $unixName =~ s|:|/|g;               # colon to slash conversion
            $unixName =~ s|^$unix_jar_root||;   # relativise
            
            $member->fileName($unixName);
            
            # print "Adding $file as $unixName\n";
            
            if ($compress) {
                $member->desiredCompressionMethod(Archive::Zip::COMPRESSION_DEFLATED);            
            } else {
                $member->desiredCompressionMethod(Archive::Zip::COMPRESSION_STORED);
            }
            
            $zip->addMember($member);
        }
    }
}

#-------------------------------------------------------------------------------
# Add the contents of a directory to the zip file
#
#-------------------------------------------------------------------------------

sub CreateJarFileFromDirectory($$$)
{
    my($srcdir, $jarpath, $compress) = @_;

    my $zip = Archive::Zip->new();

    _addDirToJar($srcdir, $srcdir, $zip, $compress);

    print "Saving zip file...\n";
    my $status = $zip->writeToFileNamed($jarpath);
    if ($status == 0) {
        print "Zipping completed successfully\n";
    } else {
        print "Error saving zip file\n";
    }
    
    # set the file type/creator to something reasonable
    MacPerl::SetFileInfo("ZIP ", "ZIP ", $jarpath);
}


#-------------------------------------------------------------------------------
# printZipContents
# 
#-------------------------------------------------------------------------------
sub printZipContents($)
{
    my($zip) = @_;

    my(@members) = $zip->memberNames();
    
    print "Zip contains:\n";
    
    my($member);
    foreach $member (@members)
    {
        print "  $member\n";
    }
}


#-------------------------------------------------------------------------------
# safeSaveJarFile
# 
# Archive::Zip has a problem where you cannot save a zip file on top of
# an existing zip file that it has open, because it holds references
# into that zip. So we have to save to a temp file, then do a swap.
# 
# Note that the zip will become invalid after this operation.
# If you want to do further operations on it, you'll have to reread it.
#-------------------------------------------------------------------------------
sub safeSaveJarFile($$)
{
    my($zip, $full_dest_path) = @_;
    
    my($temp_file_name) = $full_dest_path."_temp";

    ($zip->writeToFileNamed($temp_file_name) == Archive::Zip::AZ_OK) || die "Error: died writing jar to temp file $temp_file_name\n";
    
    unlink $full_dest_path;
    
    (rename $temp_file_name, $full_dest_path) || die "Error: Failed to rename $temp_file_name\n";
    
    MacPerl::SetFileInfo("ZIP ", "ZIP ", $full_dest_path);
}


#-------------------------------------------------------------------------------
# addToJarFile
# 
# Add a file to a jar file
#
# Parameters:
#   1. Jar ID. Unix path of jar file inside chrome.
#   2. Abs path to jar.mn file (i.e. source)    (mac breaks)
#   3. File source, relative to jar.mn path     (mac breaks)
#   4. Abs path to the resulting .jar file      (mac breaks)
#   5. Relative file path within the jar        (unix breaks)
#   6. Reference to hash of jar files
#
#-------------------------------------------------------------------------------

sub addToJarFile($$$$$$$)
{
    my($jar_id, $jar_man_dir, $file_src, $jar_path, $file_jar_path, $override, $jars) = @_;

    # print "addToJarFile with:\n  $jar_man_dir\n  $file_src\n  $jar_path\n  $file_jar_path\n";

    unless ($jar_path =~ m/(.+:)([^:]+)$/) { die "Error: Bad jar path $jar_path\n"; }
    
    my($target_dir) = $1;
    my($jar_name) = $2;

    $target_dir =~ s/[^:]+$//;
    
    # print "¥ $target_dir $jar_name\n";

    # find the source file
    my($src) = $jar_man_dir.":".$file_src;
    if ((!-e $src) && ($file_src =~ m/.+:([^:]+)$/))    # src does not exist. Fall back to looking for src in jar.mn dir
    {
				    my $tmpl_file = "$src.tmpl";
        if (-e $tmpl_file) {
            my $mfile = $src;

            #
            # A sloppy way of getting $topsrcdir... but not sure how to do it otherwise.
            #
            if ($mfile =~ /:ns:/) {
              $mfile =~ s/:ns:.*$/:ns/;
            } else { 
              $mfile =~ s/:mozilla:.*$/:mozilla/;
            }
            $mfile =~ s/:$//;
            $mfile .= ":config:milestone.txt";

            Moz::Milestone::getOfficialMilestone($mfile);
            Moz::Milestone::build_file($tmpl_file,$src);
        }
        else {
            $file_src = $1;
            $src = $jar_man_dir.":".$file_src;
        
            if (!-e $src) {
                die "Error: Can't find chrome file $src\n";
												}     
   				 }
    }
    
    if ($main::options{chrome_jars})
    {
        my($zip) = $jars->{$jar_id};
        unless ($zip) { die "Error: Can't find Zip entry for $jar_id\n"; }
        
        # print "Adding $file_src to jar file $jar_path at $file_jar_path\n";
        my($member) = Archive::Zip::Member->newFromFile($src);
        unless ($member) { die "Error: Failed to create zip file member $src\n"; }

        $member->fileName($file_jar_path);    
    
        my($compress) = 1;
        if ($compress) {
            $member->desiredCompressionMethod(Archive::Zip::COMPRESSION_DEFLATED);            
            $member->desiredCompressionLevel(Archive::Zip::COMPRESSION_LEVEL_DEFAULT);   # defaults to 6
        } else {
            $member->desiredCompressionMethod(Archive::Zip::COMPRESSION_STORED);
        }

        my($old_member) = $zip->memberNamed($file_jar_path);
        
        if ($override)
        {
            if ($old_member)
            {
                # print "Overriding $file_jar_path in jar file $jar_id\n";
                # need to compare mod dates or use the + here
                $zip->removeMember($old_member);
            }
        
            $zip->addMember($member);
        }
        else
        {
            if ($old_member)
            {
                #compare dates here
                my($member_moddate) = $old_member->lastModTime();
                my($file_moddate) = GetFileModDate($src);
            
                if ($file_moddate > $member_moddate)
                {
                    print "Updating older file $file_jar_path in $jar_id\n";
                    $zip->removeMember($old_member);
                    $zip->addMember($member);
                }
                else
                {
                    print "File $file_jar_path in $jar_id is more recent. Not updating.\n";
                }
            }
            else
            {
                $zip->addMember($member);
            }
        }
    }
    
    if ($main::options{chrome_files})  # we install raw files too
    {
        my($rel_path) = $file_jar_path;
        $rel_path =~ s|/|:|g;       # slash to colons
        
        my($dir_name) = $jar_name;
        $dir_name =~ s/\.jar$//;
        
        my($dst) = $target_dir.$dir_name.":".$rel_path;
                
        # print "Aliassing $src\n to\n$dst\n";        
        if ($override)
        {
            unlink $dst;
        MakeAlias($src, $dst);      # don't check errors, otherwise we fail on replacement
    }
        else
        {
            if (-e $dst)
            {
                #compare dates here
                my($dst_moddate) = GetFileModDate($dst);
                my($file_moddate) = GetFileModDate($src);
            
                if ($file_moddate > $dst_moddate)
                {
                    print "Updating older file $rel_path in $dir_name\n";
                    unlink $dst;
                    MakeAlias($src, $dst);
                }
                else
                {
                    print "File $file_jar_path in $jar_id is more recent. Not updating.\n";
                }
            
            }
            else
            {
                MakeAlias($src, $dst);
            }
        }
        
    }
}


#-------------------------------------------------------------------------------
# setupJarFile
# 
# setup a zip for writing
#-------------------------------------------------------------------------------

sub setupJarFile($$$)
{
    my($jar_id, $dest_path, $jar_hash) = @_;

    # print "Creating jar file $jar_id at $jar_path\n";
    
    my($jar_file) = $jar_id;
    $jar_file =~ s|/|:|g;           # slash to colons
    my($full_jar_path) = full_path_to($dest_path.":".$jar_file);

    if ($main::options{chrome_jars})
    {
        my($zip) = $jar_hash->{$jar_id};
        if (!$zip)      # if we haven't made it already, do so
        {
	        my($zip) = Archive::Zip->new();
	        $jar_hash->{$jar_id} = $zip;

            # does the jar file exist already? If so, read it in
            if (-e $full_jar_path)
            {
                print "Reading in jar file $jar_id\n";
                if ($zip->read($full_jar_path) != Archive::Zip::AZ_OK) { die "Error: Failed to re-read $full_jar_path\n"; }
                
                # printZipContents($zip);
            }

        }
    }
    else
    {
        # installing files.
        # nothing to do. MakeAlias creates dirs as needed.

	    # add this jar to the list
	    $jar_hash->{$jar_id} = 1;
    }
}


#-------------------------------------------------------------------------------
# closeJarFile
# 
# We're done with this jar file _for this jar.mn_. We may add more entries
# to it later, so keep it open in the hash.
#-------------------------------------------------------------------------------
sub closeJarFile($$)
{
    my($jar_path, $jar_hash) = @_;

    # print "Closing jar file $jar_path\n";

    if ($main::options{chrome_jars})
    {
    
    }
    else
    {
        # installing files.
        # nothing to do
    }
}


#-------------------------------------------------------------------------------
# WriteOutJarFiles
# 
# Now we dump out the jars
#-------------------------------------------------------------------------------
sub WriteOutJarFiles($$)
{
    my($chrome_dir, $jars) = @_;

    unless ($main::options{chrome_jars}) { return; }
    
    my($full_chrome_path) = full_path_to($chrome_dir);

    my($key);
    foreach $key (keys %$jars)
    {
        my($zip) = $jars->{$key};
        
        my($rel_path) = $key;
        $rel_path =~ s/\//:/g;
        
        my($output_path) = $full_chrome_path.":".$rel_path;
        
        print "Writing zip file $key to $output_path\n";

        # ensure the target dirs exist
        my($path) = $output_path;
        $path =~ s/[^:]+$//;
        mkpath($path);
        
        # unlink $output_path;        # remove any existing jar
        safeSaveJarFile($zip, $output_path);
        # $zip is invalid after this operation, so nuke it here
        $jars->{$key} = 0;
    }
}


#-------------------------------------------------------------------------------
# registerChromePackage
# 
# Enter a chrome package into the installed-chrome.txt file
#-------------------------------------------------------------------------------
sub registerChromePackage($$$$$$)
{
    my($jar_file, $file_path, $chrome_dir, $jar_hash, $chrome_type, $pkg_name) = @_;

    my($manifest_subdir) = $jar_file;
    $manifest_subdir =~ s/:/\//g;
    
    if (index($manifest_subdir, "-unix") == -1 && index($manifest_subdir, "-win") == -1) {

    my($chrome_entry);
    
    if ($main::options{use_jars}) {
        $chrome_entry = "$chrome_type,install,url,jar:resource:/chrome/$manifest_subdir!/$chrome_type/$pkg_name";
    } else {
        $manifest_subdir =~ s/\.jar$//;
        $chrome_entry = "$chrome_type,install,url,resource:/chrome/$manifest_subdir/$chrome_type/$pkg_name";
    }

    # print "Entering $chrome_entry in installed-chrome.txt\n";
    
    # ensure chrome_dir exists
    mkpath($chrome_dir);
    
    my($inst_chrome) = ${chrome_dir}.":installed-chrome.txt";
    
    if (open(CHROMEFILE, "<$inst_chrome")) {
        while (<CHROMEFILE>) {
            chomp;
            if ($_ eq $chrome_entry) {
                # $chrome_entry already appears in installed-chrome.txt file
                # just update the mod date
                my $now = time;
                utime($now, $now, $inst_chrome) || die "Error: Couldn't touch $inst_chrome";
                print "+++ updating chrome $inst_chrome\n+++\t\t$chrome_entry\n";
                close(CHROMEFILE) || die "Error: can't close $inst_chrome: $!";
                return 0;
            }
        }
        close(CHROMEFILE) || die "Error: can't close $inst_chrome: $!";
    }
    open(CHROMEFILE, ">>${inst_chrome}") || die "Error: Failed to open $inst_chrome\n";
    print(CHROMEFILE "${chrome_entry}\n");
    close(CHROMEFILE) || die "Error: Failed to close $inst_chrome\n";
    print "+++ adding chrome $inst_chrome\n+++\t\t$chrome_entry\n";
  }
}

#-------------------------------------------------------------------------------
# Create or add to a jar file from a jar.mn file.
# Both arguments are relative to the mozilla root dir.
#
#
#-------------------------------------------------------------------------------
sub CreateJarFromManifest($$$)
{
    my($jar_man_path, $dest_path, $jars) = @_;
    
    if ($main::options{chrome_jars}) {
        print "Jarring from $jar_man_path\n";
    }
    if ($main::options{chrome_files}) {
        print "Installing files from $jar_man_path\n";
    }

    $jar_man_path = full_path_to($jar_man_path);
    $dest_path = full_path_to($dest_path);

    # if the jars hash is empty, nuke installed-chrome.txt
    if (! scalar(%$jars))
    {
        print "Nuking installed-chrome.txt\n";
	    my($installed_chrome) = $dest_path.":installed-chrome.txt";
	    # unlink $installed_chrome;
    }
    
    my $jar_man_dir = "";
    my $jar_man_file = "";
    
    if ($jar_man_path =~ /(.+):([^:]+)$/)
    {
        $jar_man_dir = $1;      # no trailing :
        $jar_man_file = $2;
    }

    # Keep a hash of jar files, keyed on relative jar path (e.g. "packages/core.jar")
    # Entries are open Archive::Zips (if zipping), and installed-chrome entries.
    
    my($jar_id) = "";       # Current foo/bar.jar from jar.mn file
    my($jar_file) = "";     # relative path to jar file (from $dest_path), with mac separators
    my($full_jar_path);
    
    open(FILE, "<$jar_man_path") || die "Error: could not open \"$jar_man_path\": $!";
    while (<FILE>)
    {
        my($line) = $_;
        chomp($line);
        
        # print "$line\n";
        
        if ($line =~ /^\s*\#.*$/) {     # skip comments
            next;
        }
        
        if ($line =~/^([\w\d.\-\_\\\/]+)\:\s*$/)                                  # line start jar file entries
        {
            $jar_id = $1;
            $jar_file = $jar_id;
            $jar_file =~ s|/|:|g;           # slash to colons
            $full_jar_path = $dest_path.":".$jar_file;
                        
            setupJarFile($jar_id, $dest_path, $jars);

        }
        elsif ($line =~ /^(\+?)\s+([\w\d.\-\_\\\/]+)\s*(\([\w\d.\-\_\\\/]+\))?$\s*/)     # jar file entry
        {
            my($override) = ($1 eq "+");
            my($file_dest) = $2;
            my($file_src) = $3;
 
            if ($file_src) {  
                $file_src = substr($file_src, 1, -1);      #strip the ()
            } else {
                $file_src = $file_dest;
            }
            
            $file_src =~ s|/|:|g;
            
            if ($jar_file ne "")    # if jar is open, add to jar
            {
                if ($file_dest =~ /([\w\d.\-\_]+)\/([\w\d.\-\_\\\/]+)contents.rdf/)
                {
                    my $chrome_type = $1;
                    my $pkg_name = $2;
                    registerChromePackage($jar_file, $file_dest, $dest_path, $jars, $chrome_type, $pkg_name);
                }
                
                addToJarFile($jar_id, $jar_man_dir, $file_src, $full_jar_path, $file_dest, $override, $jars);
            }
            else
            {
                die "Error: bad jar.mn format at $line\n";
            }
        }
        elsif ($line =~ /^\s*$/ )                                               # blank line
        {
            if ($jar_file ne "")    #if a jar file is open, close it
            {
                closeJarFile($full_jar_path, $jars);
                
                $jar_file = "";
                $full_jar_path = "";
            }
        }
    }
    
    close(FILE);

    if ($jar_file ne "")    #if a jar file is open, close it
    {
        closeJarFile($full_jar_path, $jars);
    }

}

1;
