#!/usr/bin/perl

## @file vcs-installer.pl
#
# @par DESCRIPTION
# Performs installation of the MOPS Testing System along with MOPS modules to
# be tested from source code in a VCS. VCS versions comprise the Testing State
# that is stored in binary form using the serialized data module Storable.
#
# @par USAGE
#
# cvs-installer.pl [switches]
#
#     --test_instance_name    A unique name used to create a directory and name databases during testing.
#     --database              Write testing results to database.
#     --noninteractive        Don't pause.
#     --testing_path          Path to testing code.
#
## @todo Have all modules, man pages, and other Perl accessory information be
# installed to consistent paths.
#
## @note A temporary build directory is created where the testing system is installed
# along with the MOPS modules being tested.
#
# @def testing_state Versions of the modules for the testing system.
# @def cvs_versions Versions of the modules for MOPS.
#
## @todo Separate VCS checkout from unit testing.

use strict;
our $VERSION = sprintf "%d", q$Revision: $ =~ /(\d+)/g;
our $g_REMOVE_BUILD_DIR=0; # Flag indicating build directory should be removed.

## @todo Remove these constants from this file.
our $g_UNIT_TEST_CONFIG_PATH="/usr/local/MOPS_DEV/unit-test-config";
our $g_TESTING_SYSTEM_CONFIG_PATH="/usr/local/MOPS_DEV/testing-config";
our $g_CVS_PERL_ROOT="src/perl5";
our $g_BUILD_WORK_DIR;

# Pass serialized data structures.
our $g_STORABLE_PATH="/tmp";
our $g_TESTING_STATE_FILENAME;
our $g_CVS_VERSIONS_FILENAME;

use Storable;
use PS::MOPS::Test::Utility;
use PS::MOPS::Test::Globals qw($g_CVS_VERSIONS $g_TESTING_STATE $g_TEST_RUN_ID $g_PERL5LIB);
$g_TEST_RUN_ID=-1;
use Error qw(try finally);
use Cwd;

use Getopt::Long;
use Config;

our $opt_DATABASE=0;
our $opt_NONINTERACTIVE=0;
our $opt_TEST_INSTANCE_NAME=0;
our $opt_TESTING_PATH=0;

# Options set from the command-line.
GetOptions('test_instance_name=s'=>\$opt_TEST_INSTANCE_NAME,
           'database'=>\$opt_DATABASE,
           'noninteractive'=>\$opt_NONINTERACTIVE,
           'testing_path'=>\$opt_TESTING_PATH);

our $g_WRITE_TO_DATABASE=1 if $opt_DATABASE;

# Set names of serialized data structures.
$g_TESTING_STATE_FILENAME="testing_state-$opt_TEST_INSTANCE_NAME.stor";
$g_CVS_VERSIONS_FILENAME="vcs_versions-$opt_TEST_INSTANCE_NAME.stor";

# Create a Makefile from Makefile.PL or Build from Build.PL.
#
# type=Perl module installation type.
sub _createPerlMakefile
{
    my(%arg)=@_;
    validate(\%arg,type=>1);
    my $cmd_create_makefile;

    # NOTE: The following configuration options allow the current PSMOPS
    # modules in MakeMaker and Module::Build styles to be installed in
    # synchronized locations.

    if($arg{type}eq"mm"){ # MakeMaker.
        # This performs a site installation.
        # TODO: Man pages are getting installed to $g_BUILD_WORK_DIR/share/man/man{n}.
        #       Can they be installed in $g_BUILD_WORK_DIR/lib/share/man/man{n} to match
        #       the existing structure?
        $cmd_create_makefile="perl Makefile.PL PREFIX=$g_BUILD_WORK_DIR LIB=$g_BUILD_WORK_DIR/lib/perl5";
    }
    elsif($arg{type}eq"mb"){ # Module Build.
        $cmd_create_makefile="perl Build.PL --install_path lib=$g_BUILD_WORK_DIR/lib/perl5 --install_path bindoc=$g_BUILD_WORK_DIR/share/man/man1 --install_path libdoc=$g_BUILD_WORK_DIR/share/man/man3 --install_path arch=$g_BUILD_WORK_DIR/lib/perl5/$Config{archname}";
    }
    else{
        throw Error::Simple "ERROR: Type $arg{type} doesn't match.";
    }
    if(!runSystemCommand(cmd=>$cmd_create_makefile)) {
        throw Error::Simple "ERROR: System command $cmd_create_makefile failed.";
    }
}

# Install a Perl module.
#
# type=Perl module installation type.
sub _installPerlModule
{
    my(%arg)=@_;
    validate(\%arg,type=>1);
    my $cmd_install;
    if($arg{type}eq"mm"){ # MakeMaker.
        $cmd_install="make install";
    }
    elsif($arg{type}eq"mb"){ # Module Build.
        $cmd_install="./Build install";
    }
    else{
        throw Error::Simple "ERROR: Type $arg{type} doesn't match.";
    }
    if(!runSystemCommand(cmd=>$cmd_install)) {
        throw Error::Simple "ERROR: System command $cmd_install failed.";
    }
}

## @fn private _retrieveVCSVersion_VCSTime()
# Retrieve a version and time for a module from a VCS.
#
# Prerequisite: Working directory is the directory in which the file, for
# which the version is desired, is located.
#
# TODO: module = Fully qualified name of Perl module of the form AA::BB::CC::Module.
# TODO: This routine immediate returns when the version is found. This might be able to
#       to be written more efficiently.
#
# @param filename Filename of module.
# @return Hash reference.
sub _retrieveVCSVersion_VCSTime
{
    my(%arg)=@_;
    validate(\%arg,filename=>1);
    my %rv; # Return value.
    my $cwd=getcwd();

    print "cwd = $cwd\n";

    my $found=0;

    my $cmd_get_revision="svn log -q -r COMMITTED";
    my $revision=`$cmd_get_revision`;

    # Discard first line.
    my @lines=split("\n",$revision);
    my @revision_info=split(/\|/,$lines[1]);
    if($revision_info[0] =~ s/\s+//g){} # Remove whitespace.

    print "line = $lines[1]\n";
    print "revision = $revision_info[0]\n";
    print "time = $revision_info[2]\n";

    $rv{vcs_version}="$revision_info[0]";
    $rv{vcs_time}="$revision_info[2]";
    return \%rv;

    print STDERR "WARNING: VCS information not found for $arg{filename}.\n";
    return "NA";
}

# Return the module filename less the extension of a module given an FQ module name.
# PM extension is not added here.
# module = Fully qualified module name.
sub _makeModuleFilename
{
    my(%arg)=@_;
    validate(\%arg,module=>1);
    my @parts=split(/\:\:/,$arg{module});
    my $name=$parts[scalar(@parts-1)]; # Get the filename portion of the FQ module name.
    if(defined($name)){return "$name";}
    return 0;
}

# Return an array containing the individual subdirectory names to be descended.
# @todo Handle architecture dependent path separation character.
sub _makeModulePathArray
{
    my(%arg)=@_;
    validate(\%arg,module_dir=>1);
    my @parts=split(/\//,$arg{module_dir});
    my @path=();
    if(scalar(@parts)==1){ # No path separators. This is for the case of a single path name of form
                           # AA-BB-CC. Parse the directories based on the dash-separated name.
        @parts=split(/\-/,$arg{module_dir});
        for(my $i=0;$i<scalar(@parts)-1;$i++){
            push(@path,$parts[$i]);
        }
    }
    else { # Path is of type PS/MOPS/Module
        for(my $i=0;$i<scalar(@parts);$i++){
            push(@path,$parts[$i]);
        }
    }
    if(scalar(@path)>=1){return \@path;}
    return 0;
}

# Given a dash-separate module name, convert to a ::-separated module name.
sub _makeFQModuleName
{
    my(%arg)=@_;
    validate(\%arg,module=>1);
    # Substitute :: for dashes.
    if($arg{module}=~s/\-/\:\:/g){
        return $arg{module};
    }
    else { return 0; }
}

# Make a working directory wd in the test installation directory.
#
# Checkout from a VCS into the working directory.

# @todo Set build directory based on Test Instance name.

my $build_prefix="/usr/local/MOPS_DEV/psmops-build-tmp";
my $build_time=getFormattedTime();
$build_time=$opt_TEST_INSTANCE_NAME;
$g_BUILD_WORK_DIR="$build_prefix/build_" . $build_time; # Temporary directory in which entire CVS checkout
                                                        # and installation exists.

print "===============================================================================\n";
print "Starting testing using directory:\n";
print "$g_BUILD_WORK_DIR\n";
print "===============================================================================\n";
if(!$opt_NONINTERACTIVE){
    print "Enter to continue.\n"; my $input=<>;
}

## @todo Remove coding of filenames here.

# Read testing module configuration.
my $testing_modules_ref=readCSVFile(filename=>"$g_TESTING_SYSTEM_CONFIG_PATH/psmops-testing-system-modules.csv");
my $num_testing_modules=scalar(@{$testing_modules_ref});

# Read Perl module configuration.

my $perl_modules_ref=readCSVFile(filename=>"$g_UNIT_TEST_CONFIG_PATH/perl-modules-to-install.csv");
my $num_perl_modules=scalar(@{$perl_modules_ref});
my @cvs_versions=();
my @testing_state=();

try {
    print "Performing VCS checkout...\n";

    # Create the work directory for the unique Testing Instance.
    if(!makeDirectory(fullpath=>$g_BUILD_WORK_DIR)) {
        throw Error::Simple "ERROR: Failed to make directory.";
    }

    # Create a src directory in the build directory.
    if(!makeDirectory(fullpath=>"$g_BUILD_WORK_DIR/src")) {
        throw Error::Simple "ERROR: Failed to make directory.";
    }

    if(!chdir("$g_BUILD_WORK_DIR/src")){
        throw Error::Simple "ERROR: Failed to chdir.";
    }

    # Do a version control system  checkout of all Perl modules.
    my $cmd_vcs_checkout="svn checkout svn+ssh://svnuser\@mopsdc.ifa.hawaii.edu/svn/src/trunk/perl5";
    if(!runSystemCommand(cmd=>$cmd_vcs_checkout)) {
        throw Error::Simple "ERROR: System command $cmd_vcs_checkout failed.";
    }

# Install the testing system to the temporary build directory.

    print "Installing Testing System...\n";
    # Change directory to perl5 root.
    my $perl5_root="$g_BUILD_WORK_DIR/src/perl5";
    if(!chdir($perl5_root)){
        throw Error::Simple "ERROR: Failed to change into perl5 directory $perl5_root";
    }

    # Run psmops-testing-installer.pl from the files extracted from the VCS.

#    my $cmd_installer="/usr/local/MOPS_DEV/testing-bin/psmops-testing-installer.pl --base_lib_path=$g_BUILD_WORK_DIR --noninteractive";
#    if(!runSystemCommand(cmd=>$cmd_installer)) {
#        throw Error::Simple "ERROR: System command $cmd_installer failed.";
#    }

    my $cwd=getcwd;
    print "cwd = $cwd\n";

    my $unit_testing_dir=$opt_TESTING_PATH;

    # Retrieve VCS versions for Testing Modules.
    for(my $i=0;$i<$num_testing_modules;$i++){
        my $module_dir=@{$testing_modules_ref}[$i]->[0];
        print "module_dir=$module_dir\n";

        my $fqname=_makeFQModuleName(module=>$module_dir);

#        print "Changing directory to $unit_testing_dir...\n";

#        if(!chdir("$g_BUILD_WORK_DIR/$g_CVS_PERL_ROOT/$module_dir")){
#            throw Error::Simple "ERROR: Failed to change into module directory $module_dir";
#        }

#        my $module_file=_makeModuleFilename(module=>$fqname);
#        if(!$module_file){
#            throw Error::Simple "ERROR: Unable to determine module filename for @{$perl_modules_ref}[$i]->[1].";
#        }
        $testing_state[$i]->{name}=$fqname;
#        my $cvs_info=_retrieveVCSVersion_VCSTime(filename=>$module_file);
#        $testing_state[$i]->{version}=%{$cvs_info}->{vcs_version};
        $testing_state[$i]->{version}="NA";
#        $testing_state[$i]->{time}=convertToUnixTime(time=>%{$cvs_info}->{vcs_time});
        $testing_state[$i]->{time}="NA";
    }
    $g_TESTING_STATE=\@testing_state;
    store \@testing_state, "$g_STORABLE_PATH/$g_TESTING_STATE_FILENAME";

    print "Installing MOPS Perl modules...\n";
    # Retrieve CVS version for Perl modules. Create Makefiles for Perl modules. Install Perl modules.
    for(my $i=0;$i<$num_perl_modules;$i++){
        my $module_dir=@{$perl_modules_ref}[$i]->[2];
        print "Changing directory to $module_dir...\n";
        if(!chdir("$g_BUILD_WORK_DIR/$g_CVS_PERL_ROOT/$module_dir")){
            throw Error::Simple "ERROR: Failed to change into module directory $module_dir.";
        }

        my $module_file=_makeModuleFilename(module=>@{$perl_modules_ref}[$i]->[1]);
        if(!$module_file){
            throw Error::Simple "ERROR: Unable to determine module filename for @{$perl_modules_ref}[$i]->[1].";
        }
        _createPerlMakefile(type=>@{$perl_modules_ref}[$i]->[0]);
        _installPerlModule(type=>@{$perl_modules_ref}[$i]->[0]);

        # Given a AA::BB::CC module name, descend into the directory structure until the
        # the level of which the the module file resides. This comes after module installation so
        # the module directory structure can be descended.
        my $found_module_file=0;
        print STDERR "module_file=$module_file\n";
        if(!chdir("lib")){
            # Look for module in current directory.
            if(-e "$module_file.pm"){
                $found_module_file=1;
            }
            else {
                throw Error::Simple "ERROR: Failed to change into directory lib. Cwd=" . getcwd();
            }
        }
        if(!$found_module_file){
            my $dirs=_makeModulePathArray(module_dir=>$module_dir);
            foreach my $dir (@{$dirs}){
                if(!chdir($dir)){
                    throw Error::Simple "ERROR: Failed to change into directory $dir.";
                }
            }
        }

        $cvs_versions[$i]->{name}=@{$perl_modules_ref}[$i]->[1];
        my $cvs_info=_retrieveVCSVersion_VCSTime(filename=>$module_file);
        $cvs_versions[$i]->{version}=%{$cvs_info}->{vcs_version};
        $cvs_versions[$i]->{time}=convertToUnixTime(time=>%{$cvs_info}->{vcs_time});
    }

    # Save serialized version of @cvs_versions.
    store \@cvs_versions, "$g_STORABLE_PATH/$g_CVS_VERSIONS_FILENAME";

    if(!$opt_NONINTERACTIVE){
        print "Enter to continue.\n"; my $input=<>;
    }
}
finally { # Remove temporary build files.
    if($g_REMOVE_BUILD_DIR){
        print "Removing temporary build files...\n";
        # Remove temporary build directory.
        if(!removeDirectory(fullpath=>$g_BUILD_WORK_DIR)){
            throw Error::Simple "ERROR: Failed to remove directory $g_BUILD_WORK_DIR.";
        }
    }
}; # Important semicolon.

print "\nBuild and test complete.\n";
