:
eval 'exec perl -wS $0 ${1+"$@"}'
    if 0;
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This file incorporates work covered by the following license notice:
#
#   Licensed to the Apache Software Foundation (ASF) under one or more
#   contributor license agreements. See the NOTICE file distributed
#   with this work for additional information regarding copyright
#   ownership. The ASF licenses this file to you under the Apache
#   License, Version 2.0 (the "License"); you may not use this file
#   except in compliance with the License. You may obtain a copy of
#   the License at http://www.apache.org/licenses/LICENSE-2.0 .
#

#
# packimages.pl - pack images into archives
#

use strict;
use Getopt::Long;
use File::Find;
use File::Basename;
require File::Temp;
use File::Temp ();
use Archive::Zip qw(:ERROR_CODES :CONSTANTS);

#### globals ####

my $img_global = '%GLOBALRES%';  # 'global' image prefix
my $img_module = '%MODULE%';  # 'module' image prefix

my $out_file;                # path to output archive
my $tmp_out_file;            # path to temporary output file
my $global_path;             # path to global images directory
my $module_path;             # path to module images directory
my $sort_file;               # path to file containing sorting data
my @custom_path;             # path to custom images directory
my @imagelist_path;          # paths to directories containing the image lists
my $verbose;                 # be verbose
my $extra_verbose;           # be extra verbose
my $do_rebuild = 0;          # is rebuilding zipfile required?

my @custom_list;
#### script id #####

( my $script_name = $0 ) =~ s/^.*\b(\w+)\.pl$/$1/;

print "$script_name -- version: 1.17\n" if $verbose;

#### main #####

parse_options();
my $image_lists_ref = get_image_lists();
my %image_lists_hash;
foreach ( @{$image_lists_ref} ) {
    $image_lists_hash{$_}="";
}
$do_rebuild = is_file_newer(\%image_lists_hash) if $do_rebuild == 0;
my ($global_hash_ref, $module_hash_ref, $custom_hash_ref) = iterate_image_lists($image_lists_ref);
# custom_hash filled from filesystem lookup
find_custom($custom_hash_ref);

# build a consolidated set of links
my %links;
read_links(\%links, $global_path);
for my $path (@custom_path) {
    read_links(\%links, $path);
}
check_links(\%links);

# rebuild if links.txt has been modified
for my $path (@custom_path) {
    my $links_file = $path."/links.txt";
    if ((-e $links_file ) && ( -e $out_file )){
        if ((stat($out_file))[9] < (stat($links_file))[9]){
            $do_rebuild = 1;
            print_message("$links_file has been modified.") if $verbose;
        }
    }
}

my $zip_hash_ref = create_zip_list($global_hash_ref, $module_hash_ref, $custom_hash_ref);
remove_links_from_zip_list($zip_hash_ref, \%links);

$do_rebuild = is_file_newer($zip_hash_ref) if $do_rebuild == 0;
if ( $do_rebuild == 1 ) {
    create_zip_archive($zip_hash_ref, \%links);
    replace_file($tmp_out_file, $out_file);
    print_message("packing  $out_file finished.") if $verbose;
} else {
    print_message("$out_file up to date. nothing to do.") if $verbose;
}

exit(0);

#### subroutines ####

sub parse_options
{
    my $opt_help;
    my $p = Getopt::Long::Parser->new();
    my @custom_path_list;
    my $custom_path_extended;
    my $success =$p->getoptions(
                             '-h' => \$opt_help,
                             '-o=s' => \$out_file,
                             '-g=s' => \$global_path,
                 '-s=s' => \$sort_file,
                             '-m=s' => \$module_path,
                             '-c=s' => \@custom_path_list,
                             '-e=s' => \$custom_path_extended,
                             '-l=s' => \@imagelist_path,
                             '-v'   => \$verbose,
                             '-vv'  => \$extra_verbose
                            );
    push @custom_path_list, $custom_path_extended if ($custom_path_extended);
    if ( $opt_help || !$success || !$out_file || !$global_path
        || !$module_path || !@custom_path_list || !@imagelist_path )
    {
        usage();
        exit(1);
    }
    #define intermediate output file
    $tmp_out_file="$out_file"."$$".".tmp";
    # Sanity checks.

    # Check if out_file can be written.
    my $out_dir = dirname($out_file);

    # Check paths.
    foreach ($out_dir, $global_path, $module_path, @imagelist_path) {
        print_error("no such directory: '$_'", 2) if ! -d $_;
        print_error("can't search directory: '$_'", 2) if ! -x $_;
    }
    print_error("directory is not writable: '$out_dir'", 2) if ! -w $out_dir;

    # Use just the working paths
    @custom_path = ();
    foreach (@custom_path_list) {
        if ( ! -d $_ ) {
            print_warning("skipping non-existing directory: '$_'", 2);
        }
        elsif ( ! -x $_ ) {
            print_error("can't search directory: '$_'", 2);
        }
        else {
            push @custom_path, $_;
        }
    }
}

sub get_image_lists
{
    my @image_lists;
    my $glob_imagelist_path;

    foreach ( @imagelist_path ) {
        $glob_imagelist_path = $_;
        # cygwin perl
        chomp( $glob_imagelist_path = qx{cygpath -u "$glob_imagelist_path"} ) if "$^O" eq "cygwin";
        push @image_lists, glob("$glob_imagelist_path/*.ilst");
    }
    if ( !@image_lists ) {
        print_error("can't find any image lists in '@imagelist_path'", 3);
    }

    return wantarray ? @image_lists : \@image_lists;
}

sub iterate_image_lists
{
    my $image_lists_ref = shift;

    my %global_hash;
    my %module_hash;
    my %custom_hash;

    foreach my $i ( @{$image_lists_ref} ) {
        parse_image_list($i, \%global_hash, \%module_hash, \%custom_hash);
    }

    return (\%global_hash, \%module_hash, \%custom_hash);
}

sub parse_image_list
{
    my $image_list      = shift;
    my $global_hash_ref = shift;
    my $module_hash_ref = shift;
    my $custom_hash_ref = shift;

    print_message("parsing '$image_list' ...") if $verbose;
    my $linecount = 0;
    open(IMAGE_LIST, "< $image_list") or die "ERROR: can't open $image_list: $!";
    while ( <IMAGE_LIST> ) {
        $linecount++;
        next if /^\s*#/;
        next if /^\s*$/;
        # clean up trailing whitespace
        tr/\r\n//d;
        s/\s+$//;
        # clean up backslashes and double slashes
        tr{\\}{/}s;
        tr{/}{}s;
        # hack "res" back into globals
        if ( /^\Q$img_global\E\/(.*)$/o ) {
            $global_hash_ref->{"res/".$1}++;
            next;
        }
        if ( /^\Q$img_module\E\/(.*)$/o ) {
            $module_hash_ref->{$1}++;
            next;
        }
        # parse failed if we reach this point, bail out
        close(IMAGE_LIST);
        print_error("can't parse line $linecount from file '$image_list'", 4);
    }
    close(IMAGE_LIST);

    return ($global_hash_ref, $module_hash_ref, $custom_hash_ref);
}

sub find_custom
{
    my $custom_hash_ref = shift;
    my $keep_back;
    for my $path (@custom_path) {
    find({ wanted => \&wanted, no_chdir => 0 }, $path);
    foreach ( @custom_list ) {
        if ( /^\Q$path\E\/(.*)$/ ) {
        $keep_back=$1;
        if (!defined $custom_hash_ref->{$keep_back}) {
            $custom_hash_ref->{$keep_back} = $path;
        }
        }
    }
    }
}

sub wanted
{
    my $file = $_;

    if ( $file =~ /.*\.png$/ && -f $file ) {
        push @custom_list, $File::Find::name;
    }
}

sub create_zip_list
{
    my $global_hash_ref = shift;
    my $module_hash_ref = shift;
    my $custom_hash_ref = shift;

    my %zip_hash;
    my @warn_list;

    print_message("assemble image list ...") if $verbose;
    foreach ( keys %{$global_hash_ref} ) {
        # check if in 'global' and in 'module' list and add to warn list
        if ( exists $module_hash_ref->{$_} ) {
            push(@warn_list, $_);
            next;
        }
        if ( exists $custom_hash_ref->{$_} ) {
            $zip_hash{$_} = $custom_hash_ref->{$_};
            next;
        }
        # it's neither in 'module' nor 'custom', record it in zip hash
        $zip_hash{$_} = $global_path;
    }
    foreach ( keys %{$module_hash_ref} ) {
        if ( exists $custom_hash_ref->{$_} ) {
            $zip_hash{$_} = $custom_hash_ref->{$_};
            next;
        }
        # it's not in 'custom', record it in zip hash
        $zip_hash{$_} = $module_path;
    }

    if ( @warn_list ) {
        foreach ( @warn_list ) {
            print_warning("$_ is duplicated in 'global' and 'module' list");
        }
    }

    return \%zip_hash
}

sub is_file_newer
{
    my $test_hash_ref = shift;
    my $reference_stamp = 0;

    print_message("checking timestamps ...") if $verbose;
    if ( -e $out_file ) {
        $reference_stamp = (stat($out_file))[9];
        print_message("found $out_file with $reference_stamp ...") if $verbose;
    }
    return 1 if $reference_stamp == 0;

    foreach ( sort keys %{$test_hash_ref} ) {
        my $path = $test_hash_ref->{$_};
        $path .= "/" if "$path" ne "";
        $path .= "$_";
        print_message("checking '$path' ...") if $extra_verbose;
        my $mtime = (stat($path))[9];
        return 1 if $reference_stamp < $mtime;
    }
    return 0;
}

sub optimize_zip_layout($)
{
    my $zip_hash_ref = shift;

    if (!defined $sort_file) {
    print_message("no sort file - sorting alphabetically ...") if $verbose;
    return sort keys %{$zip_hash_ref};
    }
    print_message("sorting from $sort_file ...") if $verbose;

    my $orderh;
    my %included;
    my @sorted;
    open ($orderh, $sort_file) || die "Can't open $sort_file: $!";
    while (<$orderh>) {
    /^\#.*/ && next; # comments
    s/[\r\n]*$//;
    /^\s*$/ && next;
    my $file = $_;
    if (!defined $zip_hash_ref->{$file}) {
        print "unknown file '$file'\n" if ($extra_verbose);
    } else {
        push @sorted, $file;
        $included{$file} = 1;
    }
    }
    close ($orderh);

    for my $img (sort keys %{$zip_hash_ref}) {
    push @sorted, $img if (!$included{$img});
    }

    print_message("done sort ...") if $verbose;

    return @sorted;
}

sub create_zip_archive
{
    my $zip_hash_ref = shift;
    my $links_hash_ref = shift;

    print_message("creating image archive ...") if $verbose;
    my $zip = Archive::Zip->new();

    my $linktmp;
    if (keys %{$links_hash_ref}) {
        $linktmp = write_links($links_hash_ref);
        my $member = $zip->addFile($linktmp->filename, "links.txt", COMPRESSION_DEFLATED);
        if (!$member) {
            print_error("failed to add links file: $!", 5);
        }
    }

# FIXME: test - $member = addfile ... $member->desiredCompressionMethod( COMPRESSION_STORED );
# any measurable performance win/loss ?
    foreach ( optimize_zip_layout($zip_hash_ref) ) {
        my $path = $zip_hash_ref->{$_} . "/$_";
        print_message("zipping '$path' ...") if $extra_verbose;
        if ( -e $path) {
            my $member = $zip->addFile($path, $_, COMPRESSION_STORED);
            if ( !$member ) {
                print_error("can't add file '$path' to image zip archive: $!", 5);
            }
        } else {
                print_warning("file '$path' not found");
        }
    }
    my $status = $zip->writeToFileNamed($tmp_out_file);
    if ( $status != AZ_OK ) {
        print_error("write image zip archive '$tmp_out_file' failed. Reason: $status", 6);
    }
    return;
}

sub replace_file
{
    my $source_file = shift;
    my $dest_file = shift;
    my $result = 0;

    $result = unlink($dest_file) if -f $dest_file;
    if ( $result != 1 && -f $dest_file ) {
        unlink $source_file;
        print_error("couldn't remove '$dest_file'",1);
    }  else {
        if ( !rename($source_file, $dest_file)) {
            unlink $source_file;
            print_error("couldn't rename '$source_file'",1);
        }
    }
    return;
}

sub usage
{
    print STDERR "Usage: packimages.pl [-h] -o out_file -g g_path -m m_path -c c_path -l imagelist_path\n";
    print STDERR "Creates archive of images\n";
    print STDERR "Options:\n";
    print STDERR "    -h                 print this help\n";
    print STDERR "    -o out_file        path to output archive\n";
    print STDERR "    -g g_path          path to global images directory\n";
    print STDERR "    -m m_path          path to module images directory\n";
    print STDERR "    -c c_path          path to custom images directory\n";
    print STDERR "    -s sort_file       path to image sort order file\n";
    print STDERR "    -l imagelist_path  path to directory containing image lists (may appear mutiple times)\n";
    print STDERR "    -v                 verbose\n";
    print STDERR "    -vv                very verbose\n";
}

sub print_message
{
    my $message     = shift;

    print "$script_name: ";
    print "$message\n";
    return;
}

sub print_warning
{
    my $message     = shift;

    print STDERR "$script_name: ";
    print STDERR "WARNING $message\n";
    return;
}

sub print_error
{
    my $message     = shift;
    my $error_code  = shift;

    print STDERR "$script_name: ";
    print STDERR "ERROR: $message\n";

    if ( $error_code ) {
        print STDERR "\nFAILURE: $script_name aborted.\n";
        exit($error_code);
    }
    return;
}

sub read_links($$)
{
    my $links = shift;
    my $path = shift;

    my $fname = "$path/links.txt";
    if (!-f "$fname") {
        return;
    }

    my $fh;
    open ($fh, $fname) || die "Can't open: $fname: $!";
    # Syntax of links file:
    # # comment
    # missing-image image-to-load-instead
    while (<$fh>) {
        my $line = $_;
        $line =~ s/\r//g;   # DOS line-feeds
        $line =~ s/\#.*$//; # kill comments
        $line =~ m/^\s*$/ && next; # blank lines
        if ($line =~ m/^([^\s]+)\s+(.*)$/) {
            my ($missing, $replace) = ($1, $2);
            # enter into hash, and overwrite previous layer if necessary
            $links->{$1} = $2;
        } else {
            die "Malformed links line: '$line'\n";
        }
    }
    close ($fh);
}

# write out the links to a tmp file
sub write_links($)
{
    my $links = shift;
    my $tmp = File::Temp->new( TEMPLATE => "linksXXXXXXX" );
    $tmp || die "can't create tmp: $!";
    for my $missing (sort keys %{$links}) {
        my $line = $missing . " " . $links->{$missing} . "\n";
        print $tmp $line;
    }
    binmode $tmp; # force flush
    return $tmp;
}

# Ensure that no link points to another link
sub check_links($)
{
    my $links = shift;

    for my $link (keys %{$links}) {
        my $value = $links->{$link};
        if (defined $links->{$value}) {
            die "Link to another link: $link -> $value -> " . $links->{$value};
        }
    }
}

# remove any files from our zip list that are linked
sub remove_links_from_zip_list($$)
{
    my $zip_hash_ref = shift;
    my $links = shift;
    for my $link (keys %{$links}) {
        if (defined $zip_hash_ref->{$link}) {
            delete $zip_hash_ref->{$link};
        }
    }
}
