#
# 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 .
#

package installer::packagelist;

use installer::converter;
use installer::exiter;
use installer::globals;
use installer::remover;
use installer::scriptitems;

########################################
# Check existence of module
########################################

sub check_module_existence
{
    my ($onegid, $moduleslist) = @_;

    my $foundgid = 0;

    for ( my $i = 0; $i <= $#{$moduleslist}; $i++ )
    {
        my $gid = ${$moduleslist}[$i]->{'gid'};

        if ( $gid eq $onegid )
        {
            $foundgid = 1;
            last;
        }
    }

    return $foundgid;
}

###################################################
# Analyzing the gids, defined in the packagelist
###################################################

sub analyze_list
{
    my ($packagelist, $moduleslist) = @_;

    @allpackages = ();

    my $moduleshash = get_module_hash($moduleslist);

    for ( my $i = 0; $i <= $#{$packagelist}; $i++ )
    {
        my $onepackage = ${$packagelist}[$i];

        my $onegid = $onepackage->{'module'};

        installer::remover::remove_leading_and_ending_whitespaces(\$onegid);

        my $moduleexists = check_module_existence($onegid, $moduleslist);

        if ( ! $moduleexists ) { next; }

        my @allmodules = ();

        push(@allmodules, $onegid);

        get_children_with_hash($moduleshash, $onegid, \@allmodules);

        $onepackage->{'allmodules'} = \@allmodules;

        push(@allpackages, $onepackage);
    }

    return \@allpackages;
}

###################################################
# Creating a hash, that contains the module gids
# as keys and the parentids as values
###################################################

sub get_module_hash
{
    my ($moduleslist) = @_;

    my %modulehash = ();

    for ( my $i = 0; $i <= $#{$moduleslist}; $i++ )
    {
        my $gid = ${$moduleslist}[$i]->{'gid'};
        # Containing only modules with parent. Root modules can be ignored.
        if ( ${$moduleslist}[$i]->{'ParentID'} ) { $modulehash{$gid} = ${$moduleslist}[$i]->{'ParentID'}; }
    }

    return \%modulehash;
}

########################################################
# Recursively defined procedure to order
# modules and directories
########################################################

sub get_children_with_hash
{
    my ($modulehash, $parentgid, $newitemorder) = @_;

    foreach my $gid ( keys %{$modulehash} )
    {
        my $parent = $modulehash->{$gid};

        if ( $parent eq $parentgid )
        {
            push(@{$newitemorder}, $gid);
            my $parent = $gid;
            get_children_with_hash($modulehash, $parent, $newitemorder);    # recursive!
        }
    }
}

########################################################
# Recursively defined procedure to order
# modules and directories
########################################################

sub get_children
{
    my ($allitems, $startparent, $newitemorder) = @_;

    for ( my $i = 0; $i <= $#{$allitems}; $i++ )
    {
        my $gid = ${$allitems}[$i]->{'gid'};
        my $parent = "";
        if ( ${$allitems}[$i]->{'ParentID'} ) { $parent = ${$allitems}[$i]->{'ParentID'}; }

        if ( $parent eq $startparent )
        {
            push(@{$newitemorder}, $gid);
            my $parent = $gid;
            get_children($allitems, $parent, $newitemorder);    # recursive!
        }
    }
}

#####################################################################
# All modules below a defined gid_Module_A are collected now for
# each modules defined in the packagelist. Now the modules have
# to be removed, that are part of more than one package.
#####################################################################

sub remove_multiple_modules_packages
{
    my ($allpackagemodules) = @_;

    # iterating over all packages

    for ( my $i = 0; $i <= $#{$allpackagemodules}; $i++ )
    {
        my $onepackage = ${$allpackagemodules}[$i];
        my $allmodules = $onepackage->{'allmodules'};

        # Comparing each package, with all following packages. If a
        # gid for the module is part of more than one package, it is
        # removed if the number of modules in the package is greater
        # in the current package than in the compare package.

        # Taking all modules from package $i

        my $packagecount = $#{$allmodules};

        my @optimizedpackage = ();

        # iterating over all modules of this package

        for ( my $j = 0; $j <= $#{$allmodules}; $j++ )
        {
            my $onemodule = ${$allmodules}[$j]; # this is the module, that shall be removed or not

            my $put_module_into_new_package = 1;

            # iterating over all other packages

            for ( my $k = 0; $k <= $#{$allpackagemodules}; $k++ )
            {
                if ( $k == $i ) { next; }   # not comparing equal module

                if (! $put_module_into_new_package) { next; } # do not compare, if already found

                my $comparepackage = ${$allpackagemodules}[$k];
                my $allcomparemodules = $comparepackage->{'allmodules'};

                my $comparepackagecount = $#{$allcomparemodules};

                # modules will only be removed from packages, that have more modules
                # than the compare package

                if ( $packagecount < $comparepackagecount ) { next; }  # nothing to do, take next package

                # iterating over all modules of this package

                for ( my $m = 0; $m <= $#{$allcomparemodules}; $m++ )
                {
                    my $onecomparemodule = ${$allcomparemodules}[$m];

                    if ( $onemodule eq $onecomparemodule )  # this $onemodule has to be removed
                    {
                        $put_module_into_new_package = 0;
                    }
                }
            }

            if ( $put_module_into_new_package )
            {
                push(@optimizedpackage, $onemodule)
            }
        }

        $onepackage->{'allmodules'} = \@optimizedpackage;
    }
}

#####################################################################
# Analyzing all files if they belong to a special package.
# A package is described by a list of modules.
#####################################################################

sub find_files_for_package
{
    my ($filelist, $onepackage) = @_;

    my @newfilelist;
    my $lastmodules = '';
    my $lastincludefile = 0;

    my %packagemodules = map {$_ => 1} @{$onepackage->{'allmodules'}};

    for my $onefile (@$filelist)
    {
        my $modulesstring = $onefile->{'modules'};   # comma separated modules list

        # check if the modules string is the same as in the last file
        if ($modulesstring eq $lastmodules)
        {
            push(@newfilelist, $onefile) if $lastincludefile;
            next;
        }

        my $moduleslist = installer::converter::convert_stringlist_into_array(\$modulesstring, ",");

        my $includefile = 0;

        # iterating over all modules of this file
        for my $filemodule (@$moduleslist) {
            installer::remover::remove_leading_and_ending_whitespaces(\$filemodule);
            if ($packagemodules{$filemodule}) {
                $includefile = 1;
                last;
            }
        }

        push(@newfilelist, $onefile) if $includefile;

        # cache last result for this modules list
        $lastmodules = $modulesstring;
        $lastincludefile = $includefile;

    }
    return \@newfilelist;
}

#####################################################################
# Analyzing all links if they belong to a special package.
# A package is described by a list of modules.
# A link is inserted into the package, if the corresponding
# file is also inserted.
#####################################################################

sub find_links_for_package
{
    my ($linklist, $filelist) = @_;

    # First looking for all links with a FileID.
    # Then looking for all links with a ShortcutID.

    my @newlinklist = ();

    for ( my $i = 0; $i <= $#{$linklist}; $i++ )
    {
        my $includelink = 0;

        my $onelink = ${$linklist}[$i];

        my $fileid = "";
        if ( $onelink->{'FileID'} ) { $fileid = $onelink->{'FileID'}; }

        if ( $fileid eq "" ) { next; }   # A link with a ShortcutID

        for ( my $j = 0; $j <= $#{$filelist}; $j++ )     # iterating over file list
        {
            my $onefile = ${$filelist}[$j];
            my $gid = $onefile->{'gid'};

            if ( $gid eq $fileid )
            {
                $includelink = 1;
                last;
            }
        }

        if ( $includelink )
        {
            push(@newlinklist, $onelink);
        }
    }

    # iterating over the new list, because of all links with a ShortcutID

    for ( my $i = 0; $i <= $#{$linklist}; $i++ )
    {
        my $includelink = 0;

        my $onelink = ${$linklist}[$i];

        my $shortcutid = "";
        if ( $onelink->{'ShortcutID'} ) { $shortcutid = $onelink->{'ShortcutID'}; }

        if ( $shortcutid eq "" ) { next; }   # A link with a ShortcutID

        for ( my $j = 0; $j <= $#newlinklist; $j++ )     # iterating over newly created link list
        {
            my $onefilelink = $newlinklist[$j];
            my $gid = $onefilelink->{'gid'};

            if ( $gid eq $shortcutid )
            {
                $includelink = 1;
                last;
            }
        }

        if ( $includelink )
        {
            push(@newlinklist, $onelink);
        }
    }

    return \@newlinklist;
}

#####################################################################
# Analyzing all directories if they belong to a special package.
# A package is described by a list of modules.
# Directories are included into the package, if they are needed
# by a file or a link included into the package.
# Attention: A directory with the flag CREATE, is only included
# into the root module:
# ($packagename eq $installer::globals::rootmodulegid)
#####################################################################

sub find_dirs_for_package
{
    my ($dirlist, $onepackage) = @_;

    my @newdirlist = ();

    for ( my $i = 0; $i <= $#{$dirlist}; $i++ )
    {
        my $onedir = ${$dirlist}[$i];
        my $modulesstring = $onedir->{'modules'};    # comma separated modules list
        my $moduleslist = installer::converter::convert_stringlist_into_array(\$modulesstring, ",");

        my $includedir = 0;

        # iterating over all modules of this dir

        for ( my $j = 0; $j <= $#{$moduleslist}; $j++ )
        {
            if ( $includedir ) { last; }
            my $dirmodule = ${$moduleslist}[$j];
            installer::remover::remove_leading_and_ending_whitespaces(\$dirmodule);

            # iterating over all modules of the package

            my $packagemodules = $onepackage->{'allmodules'};

            for ( my $k = 0; $k <= $#{$packagemodules}; $k++ )
            {
                my $packagemodule = ${$packagemodules}[$k];

                if ( $dirmodule eq $packagemodule )
                {
                    $includedir = 1;
                    last;
                }
            }
        }

        if ( $includedir )
        {
            push(@newdirlist, $onedir);
        }
    }

    return \@newdirlist;
}

#####################################################################
# Resolving all variables in the packagename.
#####################################################################

sub resolve_packagevariables
{
    my ($packagenameref, $variableshashref, $make_lowercase) = @_;

    my $key;

    # Special handling for dictionaries
    if ( $$packagenameref =~ /-dict-/ )
    {
        if (exists($variableshashref->{'DICTIONARYUNIXPRODUCTNAME'}) ) { $$packagenameref =~ s/\%UNIXPRODUCTNAME/$variableshashref->{'DICTIONARYUNIXPRODUCTNAME'}/g; }
        if (exists($variableshashref->{'DICTIONARYBRANDPACKAGEVERSION'}) ) { $$packagenameref =~ s/\%BRANDPACKAGEVERSION/$variableshashref->{'DICTIONARYBRANDPACKAGEVERSION'}/g; }
    }

    foreach $key (keys %{$variableshashref})
    {
        my $value = $variableshashref->{$key};
        if ( $make_lowercase ) { $value = lc($value); }
        $$packagenameref =~ s/\%$key/$value/g;
    }
}

#####################################################################
# Resolving all variables in the packagename.
#####################################################################

sub resolve_packagevariables2
{
    my ($packagenameref, $variableshashref, $make_lowercase, $isdict ) = @_;

    my $key;

    # Special handling for dictionaries
    if ( $isdict )
    {
        if (exists($variableshashref->{'DICTIONARYUNIXPRODUCTNAME'}) ) { $$packagenameref =~ s/\%UNIXPRODUCTNAME/$variableshashref->{'DICTIONARYUNIXPRODUCTNAME'}/g; }
        if (exists($variableshashref->{'DICTIONARYBRANDPACKAGEVERSION'}) ) { $$packagenameref =~ s/\%BRANDPACKAGEVERSION/$variableshashref->{'DICTIONARYBRANDPACKAGEVERSION'}/g; }
    }

    foreach $key (keys %{$variableshashref})
    {
        my $value = $variableshashref->{$key};
        if ( $make_lowercase ) { $value = lc($value); }
        $$packagenameref =~ s/\%$key/$value/g;
    }
}

#####################################################################
# New packages system.
#####################################################################

##################################################################
# Controlling the content of the packagelist
# 1. Items in @installer::globals::packagelistitems must exist
# 2. If a shellscript file is defined, it must exist
##################################################################

sub check_packagelist
{
    my ($packages) = @_;

    if ( ! ( $#{$packages} > -1 )) { installer::exiter::exit_program("ERROR: No packages defined!", "check_packagelist"); }

    for ( my $i = 0; $i <= $#{$packages}; $i++ )
    {
        my $onepackage = ${$packages}[$i];

        my $element;

        # checking all items that must be defined

        foreach $element (@installer::globals::packagelistitems)
        {
            if ( ! exists($onepackage->{$element}) )
            {
                installer::exiter::exit_program("ERROR in package list: No value for $element !", "check_packagelist");
            }
        }

        # checking the existence of the script file, if defined

        if ( $onepackage->{'script'} )
        {
            my $scriptfile = $onepackage->{'script'};
            my $gid =  $onepackage->{'module'};
            my $fileref = installer::scriptitems::get_sourcepath_from_filename_and_includepath(\$scriptfile, "" , 0);

            if ( $$fileref eq "" ) { installer::exiter::exit_program("ERROR: Could not find script file $scriptfile for module $gid!", "check_packagelist"); }

            my $infoline = "$gid: Using script file: \"$$fileref\"!\n";
            push( @installer::globals::logfileinfo, $infoline);

            $onepackage->{'script'} = $$fileref;
        }
    }
}

#####################################################################
# Reading pack info for one module from packinfo file.
#####################################################################

sub get_packinfo
{
    my ($gid, $filename, $packages, $onelanguage, $islanguagemodule) = @_;

    my $packagelist = installer::files::read_file($filename);

    my @allpackages = ();

    for ( my $i = 0; $i <= $#{$packagelist}; $i++ )
    {
        my $line = ${$packagelist}[$i];

        if ( $line =~ /^\s*\#/ ) { next; }  # this is a comment line

        if ( $line =~ /^\s*Start\s*$/i )    # a new package definition
        {
            my %onepackage = ();

            my $counter = $i + 1;

            while (!( ${$packagelist}[$counter] =~ /^\s*End\s*$/i ))
            {
                if ( ${$packagelist}[$counter] =~ /^\s*(\S+)\s*\=\s*\"(.*)\"/ )
                {
                    my $key = $1;
                    my $value = $2;
                    $onepackage{$key} = $value;
                }

                $counter++;
            }

            $onepackage{'islanguagemodule'} = $islanguagemodule;
            if ( $islanguagemodule )
            {
                $saveonelanguage = $onelanguage;
                $saveonelanguage =~ s/_/-/g;
                $onepackage{'language'} = $saveonelanguage;
            }

            push(@allpackages, \%onepackage);
        }
    }

    # looking for the packinfo with the correct gid

    my $foundgid = 0;
    my $onepackage;
    foreach $onepackage (@allpackages)
    {
        # Adding the language to the module gid for LanguagePacks !
        # Making the module gid language specific: gid_Module_Root -> gir_Module_Root_pt_BR (as defined in scp2)
        if ( $onelanguage ne "" ) { $onepackage->{'module'} = $onepackage->{'module'} . "_$onelanguage"; }

        if ( $onepackage->{'module'} eq $gid )
        {
            # Resolving the language identifier
            my $onekey;
            foreach $onekey ( keys %{$onepackage} )
            {
                # Some keys require "-" instead of "_" for example in "en-US". All package names do not use underlines.
                my $locallang = $onelanguage;
                if (( $onekey eq "solarispackagename" ) ||
                   ( $onekey eq "solarisrequires" ) ||
                   ( $onekey eq "packagename" ) ||
                   ( $onekey eq "requires" )) { $locallang =~ s/_/-/g; } # avoiding illegal package abbreviation
                $onepackage->{$onekey} =~ s/\%LANGUAGESTRING/$locallang/g;
            }

            # Saving the language for the package
            my $lang = $onelanguage;
            $lang =~ s/_/-/g;
            $onepackage->{'specificlanguage'} = $lang;

            push(@{$packages}, $onepackage);
            $foundgid = 1;
            last;
        }
    }

    if ( ! $foundgid )
    {
        installer::exiter::exit_program("ERROR: Could not find package info for module $gid in file \"$filename\"!", "get_packinfo");
    }
}

#####################################################################
# Collecting all packages from scp project.
#####################################################################

sub collectpackages
{
    my ( $allmodules, $languagesarrayref ) = @_;

    installer::logger::include_header_into_logfile("Collecting packages:");

    my @packages = ();
    my %gid_analyzed = ();

    my $onemodule;
    foreach $onemodule ( @{$allmodules} )
    {
        if ( $onemodule->{'PackageInfo'} )   # this is a package module!
        {
            my $modulegid = $onemodule->{'gid'};

            my $styles = "";
            if ( $onemodule->{'Styles'} ) { $styles = $onemodule->{'Styles'}; }

            # checking modules with style LANGUAGEMODULE
            my $islanguagemodule = 0;
            my $onelanguage = "";
            if ( $styles =~ /\bLANGUAGEMODULE\b/ )
            {
                $islanguagemodule = 1;
                $onelanguage = $onemodule->{'Language'}; # already checked, that it is set.
                $onelanguage =~ s/-/_/g; # pt-BR -> pt_BR in scp
            }

            # Modules in different languages are listed more than once in multilingual installation sets
            if ( exists($gid_analyzed{$modulegid}) ) { next; }
            $gid_analyzed{$modulegid} = 1;

            my $packinfofile = $onemodule->{'PackageInfo'};

            # The file with package information has to be found in path list
            my $fileref = installer::scriptitems::get_sourcepath_from_filename_and_includepath(\$packinfofile, "" , 0);

            if ( $$fileref eq "" ) { installer::exiter::exit_program("ERROR: Could not find file $packinfofile for module $modulegid!", "collectpackages"); }

            my $infoline = "$modulegid: Using packinfo: \"$$fileref\"!\n";
            push( @installer::globals::logfileinfo, $infoline);

            get_packinfo($modulegid, $$fileref, \@packages, $onelanguage, $islanguagemodule);
        }
    }

    return \@packages;
}

#####################################################################
# Printing packages content for debugging purposes
#####################################################################

sub log_packages_content
{
    my ($packages) = @_;

    if ( ! ( $#{$packages} > -1 )) { installer::exiter::exit_program("ERROR: No packages defined!", "print_content"); }

    installer::logger::include_header_into_logfile("Logging packages content:");

    my $infoline = "";

    for ( my $i = 0; $i <= $#{$packages}; $i++ )
    {
        my $onepackage = ${$packages}[$i];

        # checking all items that must be defined

        $infoline = "Package $onepackage->{'module'}\n";
        push(@installer::globals::logfileinfo, $infoline);

        my $key;
        foreach $key (sort keys %{$onepackage})
        {
            if ( $key =~ /^\s*\;/ ) { next; }

            if ( $key eq "allmodules" )
            {
                $infoline = "\t$key:\n";
                push(@installer::globals::logfileinfo, $infoline);
                my $onemodule;
                foreach $onemodule ( @{$onepackage->{$key}} )
                {
                    $infoline = "\t\t$onemodule\n";
                    push(@installer::globals::logfileinfo, $infoline);
                }
            }
            else
            {
                $infoline = "\t$key: $onepackage->{$key}\n";
                push(@installer::globals::logfileinfo, $infoline);
            }
        }

        $infoline = "\n";
        push(@installer::globals::logfileinfo, $infoline);

    }
}

#####################################################################
# Creating assignments from modules to destination paths.
# This is required for logging in fileinfo file. Otherwise
# the complete destination file would not be known in file list.
# Saved in %installer::globals::moduledestination
#####################################################################

sub create_module_destination_hash
{
    my ($packages, $allvariables) = @_;

    for ( my $i = 0; $i <= $#{$packages}; $i++ )
    {
        my $onepackage = ${$packages}[$i];

        my $defaultdestination = $onepackage->{'destpath'};
        resolve_packagevariables(\$defaultdestination, $allvariables, 1);
        if ( $^O =~ /darwin/i ) { $defaultdestination =~ s/\/opt\//\/Applications\//; }

        foreach my $onemodule ( @{$onepackage->{'allmodules'}} )
        {
            $installer::globals::moduledestination{$onemodule} = $defaultdestination;
        }
    }
}

#####################################################################
# Adding the default paths into the files collector for Unixes.
# This is necessary to know the complete destination path in
# fileinfo log file.
#####################################################################

sub add_defaultpaths_into_filescollector
{
    my ($allfiles) = @_;

    for ( my $i = 0; $i <= $#{$allfiles}; $i++ )
    {
        my $onefile = ${$allfiles}[$i];

        if ( ! $onefile->{'destination'} ) { installer::exiter::exit_program("ERROR: No destination found at file $onefile->{'gid'}!", "add_defaultpaths_into_filescollector"); }
        my $destination = $onefile->{'destination'};

        if ( ! $onefile->{'modules'} ) { installer::exiter::exit_program("ERROR: No modules found at file $onefile->{'gid'}!", "add_defaultpaths_into_filescollector"); }
        my $module = $onefile->{'modules'};
        # If modules contains a list of modules, only taking the first one.
        if ( $module =~ /^\s*(.*?)\,/ ) { $module = $1; }

        if ( ! exists($installer::globals::moduledestination{$module}) ) { installer::exiter::exit_program("ERROR: No default destination path found for module $module!", "add_defaultpaths_into_filescollector"); }
        my $defaultpath = $installer::globals::moduledestination{$module};
        $defaultpath =~ s/\/\s*$//; # removing ending slashes
        my $fulldestpath = $defaultpath . $installer::globals::separator . $destination;

        $onefile->{'fulldestpath'} = $fulldestpath;
    }
}

#####################################################################
# Creating list of cabinet files from packages
#####################################################################

sub prepare_cabinet_files
{
    my ($packages, $allvariables) = @_;

    if ( ! ( $#{$packages} > -1 )) { installer::exiter::exit_program("ERROR: No packages defined!", "print_content"); }

    installer::logger::include_header_into_logfile("Preparing cabinet files:");

    my $infoline = "";

    for ( my $i = 0; $i <= $#{$packages}; $i++ )
    {
        my $onepackage = ${$packages}[$i];

        my $cabinetfile = "$onepackage->{'packagename'}\.cab";

        resolve_packagevariables(\$cabinetfile, $allvariables, 0);

        $installer::globals::allcabinets{$cabinetfile} = 1;

        # checking all items that must be defined

        $infoline = "Package $onepackage->{'module'}\n";
        push(@installer::globals::logfileinfo, $infoline);

        # Assigning the cab file to the module and also to all corresponding sub modules

        my $onemodule;
        foreach $onemodule ( @{$onepackage->{'allmodules'}} )
        {
            if ( ! exists($installer::globals::allcabinetassigns{$onemodule}) )
            {
                $installer::globals::allcabinetassigns{$onemodule} = $cabinetfile;
            }
            else
            {
                my $infoline = "Warning: Already existing assignment: $onemodule : $installer::globals::allcabinetassigns{$onemodule}\n";
                push(@installer::globals::logfileinfo, $infoline);
                $infoline = "Ignoring further assignment: $onemodule : $cabinetfile\n";
                push(@installer::globals::logfileinfo, $infoline);
            }
        }
    }
}

#####################################################################
# Logging assignments of cabinet files
#####################################################################

sub log_cabinet_assignments
{
    installer::logger::include_header_into_logfile("Logging cabinet files:");

    my $infoline = "List of cabinet files:\n";
    push(@installer::globals::logfileinfo, $infoline);

    my $key;
    foreach $key ( sort keys %installer::globals::allcabinets ) { push(@installer::globals::logfileinfo, "\t$key\n"); }

    $infoline = "\nList of assignments from modules to cabinet files:\n";
    push(@installer::globals::logfileinfo, $infoline);

    foreach $key ( sort keys %installer::globals::allcabinetassigns ) { push(@installer::globals::logfileinfo, "\t$key : $installer::globals::allcabinetassigns{$key}\n"); }
}

1;
