#
# 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::windows::update;

use installer::converter;
use installer::exiter;
use installer::files;
use installer::globals;
use installer::pathanalyzer;
use installer::systemactions;

#################################################################################
# Extracting all tables from an msi database
#################################################################################

sub extract_all_tables_from_msidatabase
{
    my ($fulldatabasepath, $workdir) = @_;

    my $msidb = "msidb.exe";    # Has to be in the path
    my $infoline = "";
    my $systemcall = "";
    my $returnvalue = "";
    my $extraslash = "";        # Has to be set for non-ActiveState perl

    # Export of all tables by using "*"

    if ( $^O =~ /cygwin/i ) {
        # msidb.exe really wants backslashes. (And double escaping because system() expands the string.)
        $fulldatabasepath =~ s/\//\\\\/g;
        $workdir =~ s/\//\\\\/g;
        $extraslash = "\\";
    }
    if ( $^O =~ /linux/i) {
        $extraslash = "\\";
    }

    $systemcall = $msidb . " -d " . $fulldatabasepath . " -f " . $workdir . " -e " . $extraslash . "*";
    $returnvalue = system($systemcall);

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

    if ($returnvalue)
    {
        $infoline = "ERROR: Could not execute $systemcall !\n";
        push( @installer::globals::logfileinfo, $infoline);
        installer::exiter::exit_program("ERROR: Could not exclude tables from msi database: $fulldatabasepath !", "extract_all_tables_from_msidatabase");
    }
    else
    {
        $infoline = "Success: Executed $systemcall successfully!\n";
        push( @installer::globals::logfileinfo, $infoline);
    }
}

#################################################################################
# Collecting the keys from the first line of the idt file
#################################################################################

sub collect_all_keys
{
    my ($line) = @_;

    my @allkeys = ();
    my $rownumber = 0;
    my $onekey = "";

    while ( $line =~ /^\s*(\S+?)\t(.*)$/ )
    {
        $onekey = $1;
        $line = $2;
        $rownumber++;
        push(@allkeys, $onekey);
    }

    # and the last key

    $onekey = $line;
    $onekey =~ s/^\s*//g;
    $onekey =~ s/\s*$//g;

    $rownumber++;
    push(@allkeys, $onekey);

    return (\@allkeys, $rownumber);
}

#################################################################################
# Analyzing the content of one line of an idt file
#################################################################################

sub get_oneline_hash
{
    my ($line, $allkeys, $rownumber) = @_;

    my $counter = 0;
    my %linehash = ();

    $line =~ s/^\s*//;
    $line =~ s/\s*$//;

    my $value = "";
    my $onekey = "";

    while ( $line =~ /^(.*?)\t(.*)$/ )
    {
        $value = $1;
        $line = $2;
        $onekey = ${$allkeys}[$counter];
        $linehash{$onekey} = $value;
        $counter++;
    }

    # the last column

    $value = $line;
    $onekey = ${$allkeys}[$counter];

    $linehash{$onekey} = $value;

    return \%linehash;
}

#################################################################################
# Analyzing the content of an idt file
#################################################################################

sub analyze_idt_file
{
    my ($filecontent) = @_;

    my %table = ();
    # keys are written in first line
    my ($allkeys, $rownumber) = collect_all_keys(${$filecontent}[0]);

    for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
    {
        if (( $i == 0 ) || ( $i == 1 ) || ( $i == 2 )) { next; }

        my $onelinehash = get_oneline_hash(${$filecontent}[$i], $allkeys, $rownumber);
        my $linekey = $i - 2;  # ! : The linenumber is the unique key !? Always decrease by two, because of removed first three lines.
        $table{$linekey} = $onelinehash;
    }

    return \%table;
}

#################################################################################
# Reading all idt files in a specified directory
#################################################################################

sub read_all_tables_from_msidatabase
{
    my ($workdir) = @_;

    my %database = ();

    my $ext = "idt";

    my $allidtfiles = installer::systemactions::find_file_with_file_extension($ext, $workdir);

    for ( my $i = 0; $i <= $#{$allidtfiles}; $i++ )
    {
        my $onefilename = ${$allidtfiles}[$i];
        my $longonefilename = $workdir . $installer::globals::separator . $onefilename;
        if ( ! -f $longonefilename ) { installer::exiter::exit_program("ERROR: Could not find idt file: $longonefilename!", "read_all_tables_from_msidatabase"); }
        my $filecontent = installer::files::read_file($longonefilename);
        my $idtcontent = analyze_idt_file($filecontent);
        if ($onefilename eq "Directory.idt") {
            collect_directories($filecontent);
        }
        my $key = $onefilename;
        $key =~ s/\.idt\s*$//;
        $database{$key} = $idtcontent;
    }

    return \%database;
}

#################################################################################
# Checking, if this is the correct database.
#################################################################################

sub correct_database
{
    my ($product, $pro, $langs, $languagestringref) = @_;

    my $correct_database = 0;

    # Comparing $product with $installer::globals::product and
    # $pro with $installer::globals::pro and
    # $langs with $languagestringref

    my $product_is_good = 0;

    my $localproduct = $installer::globals::product;
    if ( $installer::globals::languagepack ) { $localproduct = $localproduct . "LanguagePack"; }
    elsif ( $installer::globals::helppack ) { $localproduct = $localproduct . "HelpPack"; }

    if ( $product eq $localproduct ) { $product_is_good = 1; }

    if ( $product_is_good )
    {
        my $pro_is_good = 0;

        if ((( $pro eq "pro" ) && ( $installer::globals::pro )) || (( $pro eq "nonpro" ) && ( ! $installer::globals::pro ))) { $pro_is_good = 1; }

        if ( $pro_is_good )
        {
            my $langlisthash = installer::converter::convert_stringlist_into_hash(\$langs, ",");
            my $langstringhash = installer::converter::convert_stringlist_into_hash($languagestringref, "_");

            my $not_included = 0;
            foreach my $onelang ( keys %{$langlisthash} )
            {
                if ( ! exists($langstringhash->{$onelang}) )
                {
                    $not_included = 1;
                    last;
                }
            }

            if ( ! $not_included )
            {
                foreach my $onelanguage ( keys %{$langstringhash} )
                {
                    if ( ! exists($langlisthash->{$onelanguage}) )
                    {
                        $not_included = 1;
                        last;
                    }
                }

                if ( ! $not_included ) { $correct_database = 1; }
            }
        }
    }

    return $correct_database;
}

#################################################################################
# Searching for the path to the reference database for this special product.
#################################################################################

sub get_databasename_from_list
{
    my ($filecontent, $languagestringref, $filename) = @_;

    my $databasepath = "";

    for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
    {
        my $line = ${$filecontent}[$i];
        if ( $line =~ /^\s*$/ ) { next; } # empty line
        if ( $line =~ /^\s*\#/ ) { next; } # comment line

        if ( $line =~ /^\s*(.+?)\s*\t+\s*(.+?)\s*\t+\s*(.+?)\s*\t+\s*(.+?)\s*$/ )
        {
            my $product = $1;
            my $pro = $2;
            my $langs = $3;
            my $path = $4;

            if (( $pro ne "pro" ) && ( $pro ne "nonpro" )) { installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename. Only \"pro\" or \"nonpro\" allowed in column 1! Line: \"$line\"", "get_databasename_from_list"); }

            if ( correct_database($product, $pro, $langs, $languagestringref) )
            {
                $databasepath = $path;
                last;
            }
        }
        else
        {
            installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename! Line: \"$line\"", "get_databasename_from_list");
        }
    }

    return $databasepath;
}

#################################################################################
# Reading an existing database completely
#################################################################################

sub readdatabase
{
    my ($allvariables, $languagestringref, $includepatharrayref) = @_;

    my $database = "";
    my $infoline = "";

    if ( ! $allvariables->{'UPDATE_DATABASE_LISTNAME'} ) { installer::exiter::exit_program("ERROR: If \"UPDATE_DATABASE\" is set, \"UPDATE_DATABASE_LISTNAME\" is required.", "Main"); }
    my $listfilename = $allvariables->{'UPDATE_DATABASE_LISTNAME'};

    # Searching the list in the include paths
    my $listname = installer::scriptitems::get_sourcepath_from_filename_and_includepath(\$listfilename, $includepatharrayref, 1);
    if ( $$listname eq "" ) { installer::exiter::exit_program("ERROR: List file not found: $listfilename !", "readdatabase"); }
    my $completelistname = $$listname;

    # Reading list file
    my $listfile = installer::files::read_file($completelistname);

    # Get name and path of reference database
    my $databasename = get_databasename_from_list($listfile, $languagestringref, $completelistname);

    # If the correct database was not found, this is not necessarily an error. But in this case, this is not an update packaging process!
    if (( $databasename ) && ( $databasename ne "" )) # This is an update packaging process!
    {
        $installer::globals::updatedatabase = 1;
        installer::logger::print_message( "... update process, using database $databasename ...\n" );
        $infoline = "\nDatabase found in $completelistname: \"$databasename\"\n\n";
        # Saving in global variable
        $installer::globals::updatedatabasepath = $databasename;
    }
    else
    {
        $infoline = "\nNo database found in $completelistname. This is no update process!\n\n";
    }
    push( @installer::globals::logfileinfo, $infoline);

    if ( $installer::globals::updatedatabase )
    {
        if ( ! -f $databasename ) { installer::exiter::exit_program("ERROR: Could not find reference database: $databasename!", "readdatabase"); }

        my $msifilename = $databasename;
        installer::pathanalyzer::make_absolute_filename_to_relative_filename(\$msifilename);

        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase start");

        # create directory for unpacking
        my $databasedir = installer::systemactions::create_directories("database", $languagestringref);

        # copy database
        my $fulldatabasepath = $databasedir . $installer::globals::separator . $msifilename;
        installer::systemactions::copy_one_file($databasename, $fulldatabasepath);

        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase: before extracting tables");

        # extract all tables from database
        extract_all_tables_from_msidatabase($fulldatabasepath, $databasedir);

        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase: before reading tables");

        # read all tables
        $database = read_all_tables_from_msidatabase($databasedir);

        # Test output:

        #   foreach my $key1 ( keys %{$database} )
        #   {
        #       print "Test1: $key1\n";
        #       foreach my $key2 ( keys %{$database->{$key1}} )
        #       {
        #           print "\tTest2: $key2\n";
        #           foreach my $key3 ( keys %{$database->{$key1}->{$key2}} )
        #           {
        #               print "\t\tTest3: $key3: $database->{$key1}->{$key2}->{$key3}\n";
        #           }
        #       }
        #   }

        # Example: File table

        # my $filetable = $database->{'File'};
        # foreach my $linenumber ( keys  %{$filetable} )
        # {
        #   print "Test Filenumber: $linenumber\n";
        #   foreach my $key ( keys %{$filetable->{$linenumber}} )
        #   {
        #       print "\t\tTest: $key: $filetable->{$linenumber}->{$key}\n";
        #   }
        # }

        # Example: Searching for ProductCode in table Property

        # my $column1 = "Property";
        # my $column2 = "Value";
        # my $searchkey = "ProductCode";
        # my $propertytable = $database->{'Property'};
        # foreach my $linenumber ( keys  %{$propertytable} )
        # {
        #   if ( $propertytable->{$linenumber}->{$column1} eq $searchkey )
        #   {
        #       print("Test: $searchkey : $propertytable->{$linenumber}->{$column2}\n");
        #   }
        # }

        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase end");
    }

    return $database;
}

#########################################################################
# Reading the file "Directory.idt".
#########################################################################

sub collect_directories
{
    my ($filecontent) = @_;

    for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
    {
        if ( $i <= 2 ) { next; }                        # ignoring first three lines
        if ( ${$filecontent}[$i] =~ /^\s*$/ ) { next; } # ignoring empty lines
        # Format: Directory Directory_Parent    DefaultDir
        if ( ${$filecontent}[$i] =~ /^\s*(.+?)\t(.*?)\t(.*?)\s*$/ )
        {
            $installer::globals::merge_directory_hash{$1} = 1;
        }
        else
        {
            my $linecount = $i + 1;
            installer::exiter::exit_program("ERROR: Unknown line format in table \"$idtfilename\" (line $linecount) !", "collect_directories");
        }
    }
}

#################################################################################
# Files can be included in merge modules. This is also important for update.
#################################################################################

sub readmergedatabase
{
    my ( $mergemodules, $languagestringref, $includepatharrayref ) = @_;

    installer::logger::include_timestamp_into_logfile("Performance Info: readmergedatabase start");

    my $mergemoduledir = installer::systemactions::create_directories("mergedatabase", $languagestringref);

    my %allmergefiles = ();

    foreach my $mergemodule ( @{$mergemodules} )
    {
        my $filename = $mergemodule->{'Name'};
        my $mergefile = $ENV{'MSM_PATH'} . $filename;

        if ( ! -f $mergefile ) { installer::exiter::exit_program("ERROR: msm file not found: $filename !", "readmergedatabase"); }
        my $completesource = $mergefile;

        my $mergegid = $mergemodule->{'gid'};
        my $workdir = $mergemoduledir . $installer::globals::separator . $mergegid;
        if ( ! -d $workdir ) { installer::systemactions::create_directory($workdir); }

        my $completedest = $workdir . $installer::globals::separator . $filename;
        installer::systemactions::copy_one_file($completesource, $completedest);
        if ( ! -f $completedest ) { installer::exiter::exit_program("ERROR: msm file not found: $completedest !", "readmergedatabase"); }

        # extract all tables from database
        extract_all_tables_from_msidatabase($completedest, $workdir);

        # read all tables
        my $onemergefile = read_all_tables_from_msidatabase($workdir);

        $allmergefiles{$mergegid} = $onemergefile;
    }

    foreach my $mergefilegid ( keys %allmergefiles )
    {
        my $onemergefile = $allmergefiles{$mergefilegid};
        my $filetable = $onemergefile->{'File'};

        foreach my $linenumber ( keys %{$filetable} )
        {
            # Collecting all files from merge modules in global hash
            $installer::globals::mergemodulefiles{$filetable->{$linenumber}->{'File'}} = 1;
        }
    }

    installer::logger::include_timestamp_into_logfile("Performance Info: readmergedatabase end");
}

#################################################################################
# Creating several useful hashes from old database
#################################################################################

sub create_database_hashes
{
    my ( $database ) = @_;

    # 1. Hash ( Component -> UniqueFileName ), required in File table.
    # Read from File table.

    my %uniquefilename = ();
    my %allupdatesequences = ();
    my %allupdatecomponents = ();
    my %allupdatefileorder = ();
    my %allupdatecomponentorder = ();
    my %revuniquefilename = ();
    my %revshortfilename = ();
    my %shortdirname = ();
    my %componentid = ();
    my %componentidkeypath = ();
    my %alloldproperties = ();
    my %allupdatelastsequences = ();
    my %allupdatediskids = ();

    my $filetable = $database->{'File'};

    foreach my $linenumber ( keys  %{$filetable} )
    {
        my $comp = $filetable->{$linenumber}->{'Component_'};
        my $uniquename = $filetable->{$linenumber}->{'File'};
        my $filename = $filetable->{$linenumber}->{'FileName'};
        my $sequence = $filetable->{$linenumber}->{'Sequence'};

        my $shortname = "";
        if ( $filename =~ /^\s*(.*?)\|\s*(.*?)\s*$/ )
        {
            $shortname = $1;
            $filename = $2;
        }

        # unique is the combination of $component and $filename
        my $key = "$comp/$filename";

        if ( exists($uniquefilename{$key}) ) { installer::exiter::exit_program("ERROR: Component/FileName \"$key\" is not unique in table \"File\" !", "create_database_hashes"); }

        my $value = $uniquename;
        if ( $shortname ne "" ) { $value = "$uniquename;$shortname"; }
        $uniquefilename{$key} = $value; # saving the unique keys and short names in hash

        # Saving reverse keys too
        $revuniquefilename{$uniquename} = $key;
        if ( $shortname ne "" ) { $revshortfilename{$shortname} = $key; }

        # Saving Sequences for unique names (and also components)
        $allupdatesequences{$uniquename} = $sequence;
        $allupdatecomponents{$uniquename} = $comp;

        # Saving unique names and components for sequences
        $allupdatefileorder{$sequence} = $uniquename;
        $allupdatecomponentorder{$sequence} = $comp;
    }

    # 2. Hash, required in Directory table.

    my $dirtable = $database->{'Directory'};

    foreach my $linenumber ( keys  %{$dirtable} )
    {
        my $dir = $dirtable->{$linenumber}->{'Directory'}; # this is a unique name
        my $defaultdir = $dirtable->{$linenumber}->{'DefaultDir'};

        my $shortname = "";
        if ( $defaultdir =~ /^\s*(.*?)\|\s*(.*?)\s*$/ )
        {
            $shortname = $1;
            $shortdirname{$dir} = $shortname;   # collecting only the short names
        }
    }

    # 3. Hash, collecting info from Component table.
    # ComponentID and KeyPath have to be reused.

    my $comptable = $database->{'Component'};

    foreach my $linenumber ( keys  %{$comptable} )
    {
        my $comp = $comptable->{$linenumber}->{'Component'};
        my $compid = $comptable->{$linenumber}->{'ComponentId'};
        my $keypath = $comptable->{$linenumber}->{'KeyPath'};

        $componentid{$comp} = $compid;
        $componentidkeypath{$comp} = $keypath;
    }

    # 4. Hash, property table, required for ProductCode and Installlocation.

    my $proptable = $database->{'Property'};

    foreach my $linenumber ( keys  %{$proptable} )
    {
        my $prop = $proptable->{$linenumber}->{'Property'};
        my $value = $proptable->{$linenumber}->{'Value'};

        $alloldproperties{$prop} = $value;
    }

    # 5. Media table, getting last sequence

    my $mediatable = $database->{'Media'};
    $installer::globals::updatelastsequence = 0;

    foreach my $linenumber ( keys  %{$mediatable} )
    {
        my $cabname = $mediatable->{$linenumber}->{'Cabinet'};
        my $lastsequence = $mediatable->{$linenumber}->{'LastSequence'};
        my $diskid = $mediatable->{$linenumber}->{'DiskId'};
        $allupdatelastsequences{$cabname} = $lastsequence;
        $allupdatediskids{$cabname} = $diskid;

        if ( $lastsequence > $installer::globals::updatelastsequence ) { $installer::globals::updatelastsequence = $lastsequence; }
    }

    $installer::globals::updatesequencecounter = $installer::globals::updatelastsequence;

    return (\%uniquefilename, \%revuniquefilename, \%revshortfilename, \%allupdatesequences, \%allupdatecomponents, \%allupdatefileorder, \%allupdatecomponentorder, \%shortdirname, \%componentid, \%componentidkeypath, \%alloldproperties, \%allupdatelastsequences, \%allupdatediskids);
}


1;
