package Install;

#
# Package that provides 'make install' functionality for msvc builds
#
# src/tools/msvc/Install.pm
#
use strict;
use warnings;
use Carp;
use File::Basename;
use File::Copy;
use File::Find ();

use Exporter;
our (@ISA,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT_OK = qw(Install);

sub lcopy
{
    my $src = shift;
    my $target = shift;

    if (-f $target)
    {
        unlink $target || confess "Could not delete $target\n";
    }

    copy($src,$target)
      || confess "Could not copy $src to $target\n";

}

sub Install
{
    $| = 1;

    my $target = shift;
    our $config;
    require "config_default.pl";
    require "config.pl" if (-f "config.pl");

    chdir("../../..") if (-f "../../../configure");
    chdir("../../../..") if (-f "../../../../configure");
    my $conf = "";
    if (-d "debug")
    {
        $conf = "debug";
    }
    if (-d "release")
    {
        $conf = "release";
    }
    die "Could not find debug or release binaries" if ($conf eq "");
    my $majorver = DetermineMajorVersion();
    print "Installing version $majorver for $conf in $target\n";

    EnsureDirectories($target, 'bin','lib','share','share/timezonesets','share/contrib','doc',
        'doc/contrib', 'symbols', 'share/tsearch_data');

    CopySolutionOutput($conf, $target);
    lcopy($target . '/lib/libpq.dll', $target . '/bin/libpq.dll');
    my $sample_files = [];
    File::Find::find(
        {
            wanted =>sub {
                /^.*\.sample\z/s
                  &&push(@$sample_files, $File::Find::name);
              }
        },
        "src"
    );
    CopySetOfFiles('config files', $sample_files, $target . '/share/');
    CopyFiles(
        'Import libraries',
        $target .'/lib/',
        "$conf\\", "postgres\\postgres.lib","libpq\\libpq.lib", "libecpg\\libecpg.lib",
        "libpgport\\libpgport.lib"
    );
    CopySetOfFiles(
        'timezone names',
        [ glob('src\timezone\tznames\*.txt') ],
        $target . '/share/timezonesets/'
    );
    CopyFiles(
        'timezone sets',
        $target . '/share/timezonesets/',
        'src/timezone/tznames/', 'Default','Australia','India'
    );
    CopySetOfFiles('BKI files', [ glob("src\\backend\\catalog\\postgres.*") ],$target .'/share/');
    CopySetOfFiles('SQL files', [ glob("src\\backend\\catalog\\*.sql") ],$target . '/share/');
    CopyFiles(
        'Information schema data',
        $target . '/share/',
        'src/backend/catalog/', 'sql_features.txt'
    );
    GenerateConversionScript($target);
    GenerateTimezoneFiles($target,$conf);
    GenerateTsearchFiles($target);
    CopySetOfFiles(
        'Stopword files',
        [ glob("src\\backend\\snowball\\stopwords\\*.stop") ],
        $target . '/share/tsearch_data/'
    );
    CopySetOfFiles(
        'Dictionaries sample files',
        [ glob("src\\backend\\tsearch\\*_sample.*") ],
        $target . '/share/tsearch_data/'
    );
    CopyContribFiles($config,$target);
    CopyIncludeFiles($target);

    GenerateNLSFiles($target,$config->{nls},$majorver) if ($config->{nls});

    print "Installation complete.\n";
}

sub EnsureDirectories
{
    my $target = shift;
    mkdir $target unless -d ($target);
    while (my $d = shift)
    {
        mkdir $target . '/' . $d unless -d ($target . '/' . $d);
    }
}

sub CopyFiles
{
    my $what = shift;
    my $target = shift;
    my $basedir = shift;

    print "Copying $what";
    while (my $f = shift)
    {
        print ".";
        $f = $basedir . $f;
        die "No file $f\n" if (!-f $f);
        lcopy($f, $target . basename($f));
    }
    print "\n";
}

sub CopySetOfFiles
{
    my $what = shift;
    my $flist = shift;
    my $target = shift;
    print "Copying $what" if $what;
    foreach (@$flist)
    {
        next if /regress/; # Skip temporary install in regression subdir
        next if /ecpg.test/; # Skip temporary install in regression subdir
        my $tgt = $target . basename($_);
        print ".";
        lcopy($_, $tgt) || croak "Could not copy $_: $!\n";
    }
    print "\n";
}

sub CopySolutionOutput
{
    my $conf = shift;
    my $target = shift;
    my $rem = qr{Project\("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"\) = "([^"]+)"};

    my $sln = read_file("pgsql.sln") || croak "Could not open pgsql.sln\n";
    print "Copying build output files...";
    while ($sln =~ $rem)
    {
        my $pf = $1;
        my $dir;
        my $ext;

        $sln =~ s/$rem//;

        my $proj = read_file("$pf.vcproj") || croak "Could not open $pf.vcproj\n";
        if ($proj !~ qr{ConfigurationType="([^"]+)"})
        {
            croak "Could not parse $pf.vcproj\n";
        }
        if ($1 == 1)
        {
            $dir = "bin";
            $ext = "exe";
        }
        elsif ($1 == 2)
        {
            $dir = "lib";
            $ext = "dll";
        }
        else
        {

            # Static lib, such as libpgport, only used internally during build, don't install
            next;
        }
        lcopy("$conf\\$pf\\$pf.$ext","$target\\$dir\\$pf.$ext")
          || croak "Could not copy $pf.$ext\n";
        lcopy("$conf\\$pf\\$pf.pdb","$target\\symbols\\$pf.pdb")
          || croak "Could not copy $pf.pdb\n";
        print ".";
    }
    print "\n";
}

sub GenerateConversionScript
{
    my $target = shift;
    my $sql = "";
    my $F;

    print "Generating conversion proc script...";
    my $mf = read_file('src/backend/utils/mb/conversion_procs/Makefile');
    $mf =~ s{\\\s*[\r\n]+}{}mg;
    $mf =~ /^CONVERSIONS\s*=\s*(.*)$/m
      || die "Could not find CONVERSIONS line in conversions Makefile\n";
    my @pieces = split /\s+/,$1;
    while ($#pieces > 0)
    {
        my $name = shift @pieces;
        my $se = shift @pieces;
        my $de = shift @pieces;
        my $func = shift @pieces;
        my $obj = shift @pieces;
        $sql .= "-- $se --> $de\n";
        $sql .=
"CREATE OR REPLACE FUNCTION $func (INTEGER, INTEGER, CSTRING, INTERNAL, INTEGER) RETURNS VOID AS '\$libdir/$obj', '$func' LANGUAGE C STRICT;\n";
        $sql .=
"COMMENT ON FUNCTION $func(INTEGER, INTEGER, CSTRING, INTERNAL, INTEGER) IS 'internal conversion function for $se to $de';\n";
        $sql .= "DROP CONVERSION pg_catalog.$name;\n";
        $sql .= "CREATE DEFAULT CONVERSION pg_catalog.$name FOR '$se' TO '$de' FROM $func;\n";
        $sql .= "COMMENT ON CONVERSION pg_catalog.$name IS 'conversion for $se to $de';\n";
    }
    open($F,">$target/share/conversion_create.sql")
      || die "Could not write to conversion_create.sql\n";
    print $F $sql;
    close($F);
    print "\n";
}

sub GenerateTimezoneFiles
{
    my $target = shift;
    my $conf = shift;
    my $mf = read_file("src/timezone/Makefile");
    $mf =~ s{\\\s*[\r\n]+}{}mg;
    $mf =~ /^TZDATA\s*:?=\s*(.*)$/m || die "Could not find TZDATA row in timezone makefile\n";
    my @tzfiles = split /\s+/,$1;
    unshift @tzfiles,'';
    print "Generating timezone files...";
    system(
        "$conf\\zic\\zic -d \"$target/share/timezone\" " . join(" src/timezone/data/", @tzfiles));
    print "\n";
}

sub GenerateTsearchFiles
{
    my $target = shift;

    print "Generating tsearch script...";
    my $F;
    my $tmpl = read_file('src/backend/snowball/snowball.sql.in');
    my $mf = read_file('src/backend/snowball/Makefile');
    $mf =~ s{\\\s*[\r\n]+}{}mg;
    $mf =~ /^LANGUAGES\s*=\s*(.*)$/m
      || die "Could not find LANGUAGES line in snowball Makefile\n";
    my @pieces = split /\s+/,$1;
    open($F,">$target/share/snowball_create.sql")
      || die "Could not write snowball_create.sql";
    print $F read_file('src/backend/snowball/snowball_func.sql.in');

    while ($#pieces > 0)
    {
        my $lang = shift @pieces || last;
        my $asclang = shift @pieces || last;
        my $txt = $tmpl;
        my $stop = '';

        if (-s "src/backend/snowball/stopwords/$lang.stop")
        {
            $stop = ", StopWords=$lang";
        }

        $txt =~ s#_LANGNAME_#${lang}#gs;
        $txt =~ s#_DICTNAME_#${lang}_stem#gs;
        $txt =~ s#_CFGNAME_#${lang}#gs;
        $txt =~ s#_ASCDICTNAME_#${asclang}_stem#gs;
        $txt =~ s#_NONASCDICTNAME_#${lang}_stem#gs;
        $txt =~ s#_STOPWORDS_#$stop#gs;
        print $F $txt;
        print ".";
    }
    close($F);
    print "\n";
}

sub CopyContribFiles
{
    my $config = shift;
    my $target = shift;

    print "Copying contrib data files...";
    my $D;
    opendir($D, 'contrib') || croak "Could not opendir on contrib!\n";
    while (my $d = readdir($D))
    {
        next if ($d =~ /^\./);
        next unless (-f "contrib/$d/Makefile");
        next if ($d eq "uuid-ossp"&& !defined($config->{uuid}));
        next if ($d eq "sslinfo" && !defined($config->{openssl}));
        next if ($d eq "xml2" && !defined($config->{xml}));

        my $mf = read_file("contrib/$d/Makefile");
        $mf =~ s{\\s*[\r\n]+}{}mg;
        my $flist = '';
        if ($mf =~ /^DATA_built\s*=\s*(.*)$/m) {$flist .= $1}
        if ($mf =~ /^DATA\s*=\s*(.*)$/m) {$flist .= " $1"}
        $flist =~ s/^\s*//; # Remove leading spaces if we had only DATA_built

        if ($flist ne '')
        {
            $flist = ParseAndCleanRule($flist, $mf);

            # Special case for contrib/spi
            $flist = "autoinc.sql insert_username.sql moddatetime.sql refint.sql timetravel.sql"
              if ($d eq 'spi');
            foreach my $f (split /\s+/,$flist)
            {
                lcopy('contrib/' . $d . '/' . $f,$target . '/share/contrib/' . basename($f))
                  || croak("Could not copy file $f in contrib $d");
                print '.';
            }
        }

        $flist = '';
        if ($mf =~ /^DATA_TSEARCH\s*=\s*(.*)$/m) {$flist .= $1}
        if ($flist ne '')
        {
            $flist = ParseAndCleanRule($flist, $mf);

            foreach my $f (split /\s+/,$flist)
            {
                lcopy('contrib/' . $d . '/' . $f,$target . '/share/tsearch_data/' . basename($f))
                  || croak("Could not copy file $f in contrib $d");
                print '.';
            }
        }

        $flist = '';
        if ($mf =~ /^DOCS\s*=\s*(.*)$/mg) {$flist .= $1}
        if ($flist ne '')
        {
            $flist = ParseAndCleanRule($flist, $mf);

            # Special case for contrib/spi
            $flist =
"autoinc.example insert_username.example moddatetime.example refint.example timetravel.example"
              if ($d eq 'spi');
            foreach my $f (split /\s+/,$flist)
            {
                lcopy('contrib/' . $d . '/' . $f, $target . '/doc/contrib/' . $f)
                  || croak("Could not copy file $f in contrib $d");
                print '.';
            }
        }
    }
    closedir($D);
    print "\n";
}

sub ParseAndCleanRule
{
    my $flist = shift;
    my $mf = shift;

    # Strip out $(addsuffix) rules
    if (index($flist, '$(addsuffix ') >= 0)
    {
        my $pcount = 0;
        my $i;
        for ($i = index($flist, '$(addsuffix ') + 12; $i < length($flist); $i++)
        {
            $pcount++ if (substr($flist, $i, 1) eq '(');
            $pcount-- if (substr($flist, $i, 1) eq ')');
            last if ($pcount < 0);
        }
        $flist = substr($flist, 0, index($flist, '$(addsuffix ')) . substr($flist, $i+1);
    }
    return $flist;
}

sub CopyIncludeFiles
{
    my $target = shift;

    EnsureDirectories($target, 'include', 'include/libpq','include/internal',
        'include/internal/libpq','include/server');

    CopyFiles(
        'Public headers',
        $target . '/include/',
        'src/include/', 'postgres_ext.h', 'pg_config.h', 'pg_config_os.h', 'pg_config_manual.h'
    );
    lcopy('src/include/libpq/libpq-fs.h', $target . '/include/libpq/')
      || croak 'Could not copy libpq-fs.h';

    CopyFiles(
        'Libpq headers',
        $target . '/include/',
        'src/interfaces/libpq/','libpq-fe.h', 'libpq-events.h'
    );
    CopyFiles(
        'Libpq internal headers',
        $target .'/include/internal/',
        'src/interfaces/libpq/', 'libpq-int.h', 'pqexpbuffer.h'
    );

    CopyFiles(
        'Internal headers',
        $target . '/include/internal/',
        'src/include/', 'c.h', 'port.h', 'postgres_fe.h'
    );
    lcopy('src/include/libpq/pqcomm.h', $target . '/include/internal/libpq/')
      || croak 'Could not copy pqcomm.h';

    CopyFiles(
        'Server headers',
        $target . '/include/server/',
        'src/include/', 'pg_config.h', 'pg_config_os.h'
    );
    CopySetOfFiles('',[ glob("src\\include\\*.h") ],$target . '/include/server/');
    my $D;
    opendir($D, 'src/include') || croak "Could not opendir on src/include!\n";

    while (my $d = readdir($D))
    {
        next if ($d =~ /^\./);
        next if ($d eq '.git');
        next if ($d eq 'CVS');
        next unless (-d 'src/include/' . $d);

        EnsureDirectories($target . '/include/server', $d);
        system("xcopy /s /i /q /r /y src\\include\\$d\\*.h \"$target\\include\\server\\$d\\\"")
          && croak("Failed to copy include directory $d\n");
    }
    closedir($D);

    my $mf = read_file('src/interfaces/ecpg/include/Makefile');
    $mf =~ s{\\s*[\r\n]+}{}mg;
    $mf =~ /^ecpg_headers\s*=\s*(.*)$/m || croak "Could not find ecpg_headers line\n";
    CopyFiles(
        'ECPG headers',
        $target . '/include/',
        'src/interfaces/ecpg/include/',
        'ecpg_config.h', split /\s+/,$1
    );
    $mf =~ /^informix_headers\s*=\s*(.*)$/m || croak "Could not find informix_headers line\n";
    EnsureDirectories($target . '/include', 'informix', 'informix/esql');
    CopyFiles(
        'ECPG informix headers',
        $target .'/include/informix/esql/',
        'src/interfaces/ecpg/include/',
        split /\s+/,$1
    );
}

sub GenerateNLSFiles
{
    my $target = shift;
    my $nlspath = shift;
    my $majorver = shift;

    print "Installing NLS files...";
    EnsureDirectories($target, "share/locale");
    my @flist;
    File::Find::find(
        {
            wanted =>sub {
                /^nls\.mk\z/s
                  &&!push(@flist, $File::Find::name);
              }
        },
        "src"
    );
    foreach (@flist)
    {
        my $prgm = DetermineCatalogName($_);
        s/nls.mk/po/;
        my $dir = $_;
        next unless ($dir =~ /([^\/]+)\/po$/);
        foreach (glob("$dir/*.po"))
        {
            my $lang;
            next unless /([^\/]+)\.po/;
            $lang = $1;

            EnsureDirectories($target, "share/locale/$lang", "share/locale/$lang/LC_MESSAGES");
            system(
"\"$nlspath\\bin\\msgfmt\" -o \"$target\\share\\locale\\$lang\\LC_MESSAGES\\$prgm-$majorver.mo\" $_"
            )&& croak("Could not run msgfmt on $dir\\$_");
            print ".";
        }
    }
    print "\n";
}

sub DetermineMajorVersion
{
    my $f = read_file('src/include/pg_config.h') || croak 'Could not open pg_config.h';
    $f =~ /^#define\s+PG_MAJORVERSION\s+"([^"]+)"/m || croak 'Could not determine major version';
    return $1;
}

sub DetermineCatalogName
{
    my $filename = shift;

    my $f = read_file($filename) || croak "Could not open $filename";
    $f =~ /CATALOG_NAME\s*\:?=\s*(\S+)/m || croak "Could not determine catalog name in $filename";
    return $1;
}

sub read_file
{
    my $filename = shift;
    my $F;
    my $t = $/;

    undef $/;
    open($F, $filename) || die "Could not open file $filename\n";
    my $txt = <$F>;
    close($F);
    $/ = $t;

    return $txt;
}

1;
