#!/usr/bin/perl

use strict;
use Getopt::Long;
use File::Basename;
use File::Spec::Unix;
use File::Path qw(make_path remove_tree);
use Config;
use Switch;
use POSIX qw(strftime);
use LWP::UserAgent;
use LWP::Protocol::https;

#### GLOBAL CONSTANTS
my $TMP_DIR_KEY = "tmpDir";
my $PROPERTIES_FILE = $ENV{"HOME"} . "/.d1_mergeDiffProject.properties";
my $REPOSITORY_URL = "https://repository.dataone.org/software/cicore/";
my $dev_build_control = "d1_dev_build_control/current_dev_control.properties";
my $beta_build_control = "d1_beta_build_control/current_beta_control.properties";
my $stable_build_control = "d1_stable_build_control/current_release_control.properties";

##### GLOBAL variables
my $graphic_diff;
my %configProperties;

##### two copies in two different directories on your system.
##### for example you have checked out the trunk and a branch on 
##### your computer and you wish to find all the files that are missing
##### or have been altered.  You would issue a command similar
##### to the one below
##### 
##### mergeDiffProjects.pl --trunk /home/waltz/Projects/AllSoftware/d1_common_java --branch /home/waltz/Projects/branches/D1_COMMON_JAVA_v1.1
#####
##### If you need to compare all the projects from trunk and branch, you will
##### specify tha --all switch and then provide the path to the checked out
##### trunk.  The directory holding trunk must have all the directories, and
##### most importantly the trunk subdir ./d1_dev_build_control and ./d1_beta_build_control
##### The program will investigate the properties files in both subdirectory and
##### come up with a series of projects to diff
##### All of the branches must be checked out to the --branch directory 
##### with the naming convention as denoted in the current_beta_control.properties file
#####
##### mergeDiffProjects.pl --all --trunk /home/waltz/Projects/AllSoftware --branch /home/waltz/Projects/branches
#####
##### The visual diff program used on linux environments is meld.
##### For mac, it is gvimdiff.  If a better visual diff tool is available
##### Then change the code below to point to the correct visual diff
#####

if ( $Config{osname} =~ /linux/) 
  {
  `meld --version`;
  die ("install meld or add meld to the PATH of your shell") if ($? != 0);

  $graphic_diff = 'meld';
  }
else
  {
  `gvimdiff --version`;
  die ("install gvimdiff or add meld to the PATH of your shell") if ($? != 0);
  $graphic_diff = 'gvimdiff';
  }

sub usage 
  {
  my $message = $_[0];
  if (defined $message && length $message) 
    {
    $message .= "\n" unless $message =~ /\n$/;
    }

  my $command = $0;
  $command =~ s#^.*/##;

  print STDERR (
       $message,
       "usage: $command [--help]  --branch|--tag --component componentName | --all\n" ,
       "\tcomponentName must be derived from the keys in a control.properties file\n",
       "\tinstead of componentName, use of all will compare every component listed in the control.properties file\n\n",
       "\twith a --tag switch $command will compare dataone components as listed in\n",
       "\t$beta_build_control\n",
       "\tto the tag location as listed in\n",
       "\t$stable_build_control\n",
       "\twith a --branch switch $command will compare dataone dev components as listed in\n",
       "\t$dev_build_control\n",
       "\tto the branch location as listed in\n",
       "\t$beta_build_control\n",
   );

  die("\n")
  }
   
   
   
 sub readProperties
  {
  my $properties = shift (@_);
  open (PROPERTIES,"<",  "$PROPERTIES_FILE") or die ("Unable to open $PROPERTIES_FILE");
  while(defined( my $line = <PROPERTIES>) )
    {
    chomp($line);
    next if ($line =~ /^ *#/);
    my @lines = split (/ *= */, $line);
    $properties->{$lines[0]} = $lines[1];
     
    }
  close (PROPERTIES);
  }
   
sub writeProperties
  {
  my $properties = shift (@_);
  open (PROPERTIES, ">", "$PROPERTIES_FILE") or die ("Unable to open $PROPERTIES_FILE");
  foreach my $key (keys %{$properties})
    {
    # print "$key";
    print PROPERTIES "$key = " . $properties->{$key} . "\n";
    }
  close (PROPERTIES);
  }
      
sub diffFiles
  {
  die("compareBranches requires 2 arguments, only " . scalar(@_) . " provided") if (scalar(@_) != 2);
  my ($originFile, $targetFile) = @_;
  my $diffReturn;
  return if ($originFile =~ /\.png$/);
  open (DIFF_DATA, "diff -Bb $originFile $targetFile |")   or die "Couldn't execute program: $!";
  while ( defined( my $line = <DIFF_DATA> )  ) 
    {
    chomp($line);
    $diffReturn .= $line;
    }
  close DIFF_DATA;
  if ( defined($diffReturn) && (length($diffReturn) > 0 ) )
    {
    my $rtn = `$graphic_diff  $originFile $targetFile`;
    }
  return;
  }

sub getHttpsRequest
  {
  my ($url) = @_;
  my $rtn;
  my $ua = LWP::UserAgent->new;
  $ua->agent("d1ComponentRelease/1.0");
  my $data;
  # Create a request
  my $request = HTTP::Request->new(GET => $url);
  
  # Pass request to the user agent and get a response back
  my $response = $ua->request($request);
  
  # Check the outcome of the response
  if ($response->is_success) 
    {
    $rtn = $response->content;
    }
  else 
    {
      warn( $response->status_line . "\n");
    }
  return $rtn;
  }

sub setControlledProjects
  {
  die("getControlledProjects requires two arguments- " . scalar(@_) . " provided") if (scalar(@_) != 2);
  my ($controlUrl, $controlHash)   = @_;

  my $data = getHttpsRequest($controlUrl);
  die ("unable to retrieve $controlUrl\n") unless (defined($data));
  foreach (split(/\n/,$data)) 
    {
    my $line = $_;
    chomp($line);
    $line =~ s/^\s+//;
    $line =~ s/\s+$//;

    unless ( !(defined($line)) || ($line =~ /^#/) || (length($line) <= 0) )
      {
      my @keyValue = split("=", $line);
      $controlHash->{$keyValue[0]} = $keyValue[1];
      }
    }
  }

sub compareDirectoryCopies
  {
  die("compareDirectoryCopies requires two arguments- " . scalar(@_) . " provided") if (scalar(@_) != 2);
  my ($originProjectPath, $targetProjectPath) = @_;
  my @diffFiles;
  my @originFiles;
  my @targetFiles;
  ##### read in all the files in origin, that we are concerned about
  ##### excluded are files name .project, .classpath (eclipse files), .class, and nbactions.xml
  ##### and the directories .settings (netbeans) and .svn
  #####  tmp/.lock 
  open (ORIGIN_DATA, "find $originProjectPath -name tmp -a -type d -prune -o -name out -a -type d -prune -o -name target -a -type d -prune -o -name nbactions.xml -a -type f -prune -o -name *.jar -a -type f -prune -o -name *.war -a -type f -prune -o -name .project -a -type f -prune -o -name .classpath -a -type f -prune -o -name .settings -a -type d -prune -o -name .svn -a -type d -prune -o -name .class -a -prune  -o  -type f  -print |")   or die "Couldn't execute program: $!";
  while ( defined( my $line = <ORIGIN_DATA> )  ) 
    {
    chomp($line);
    unless ($line =~ /~$/)
      {
      push (@originFiles, $line);
      my $diffFile = $line;
      $diffFile =~ s/$originProjectPath\///g;
      if (-e "$targetProjectPath/$diffFile")  
        {
        push (@diffFiles, $diffFile);
        }
      else
        {
        print "\n*****************************************************\n";
        print   "MISSING FILE $diffFile \nin target: $targetProjectPath";
        print "\n*****************************************************\n";
        my $answer;
        do 
          {
          print "Do you wish to add the file into the target?(y/n)\n";
          $answer =  <STDIN>;
          chomp ($answer);
          print "User typed $answer\n";
          } 
        until ($answer =~ /^(?:y|n)$/);
        if ($answer eq "y") 
          {
          ### need to find the starting point of the  missing file
          ### if parent directories are missing, then svn add
          ### should be run from the topmost missing sub directory
          ### of the project
          my $addPath = 0;
           my $startPath  = dirname ("$targetProjectPath/$diffFile");  
          $addPath = 1 unless (-e $startPath);  
          while (!(-e $startPath) && (length($startPath) > 1))
            {
            my @startPathDirs = File::Spec::Unix->splitdir( $startPath );
            pop(@startPathDirs);
            $startPath = File::Spec::Unix->catdir(@startPathDirs);
            }
          if ($addPath)
            {
             my $fullPath  = dirname ("$targetProjectPath/$diffFile");  
            my $rtn = `mkdir --parents $fullPath`;
            my $rtn =`cp $originProjectPath/$diffFile $targetProjectPath/$diffFile`;
            print "copied file to $targetProjectPath/$diffFile . Results: $rtn";
            $rtn =`svn add --force $startPath`;  
            print "added $startPath to subversion. Results: $rtn";
            }
          else 
            {
            my $rtn =`cp $originProjectPath/$diffFile $targetProjectPath/$diffFile`;
            print "copied file to $targetProjectPath/$diffFile . Results: $rtn";
            $rtn =`svn add $targetProjectPath/$diffFile`;  
            print "added $targetProjectPath/$diffFile to subversion. Results: $rtn";
            }  
          }
        }
      }
    }
  close ORIGIN_DATA;
  ##### read in all the files in the target, that we are concerned about
  ##### excluded are files name .project, .classpath (eclipse files), .class and nbactions.xml
  ##### and the directories .settings (netbeans) and .svn
  #####  tmp/.lock 
  open (TARGET_DATA, "find $targetProjectPath -name tmp -a -type d -prune -o -name out -a -type d -prune -o -name target -a -type d -prune -o -name nbactions.xml -a -type f -prune -o -name *.jar -a -type f -prune -o -name *.war -a -type f -prune -o -name .project -a -type f -prune -o -name .classpath -a -type f -prune -o -name .settings -a -type d -prune -o -name .svn -a -type d -prune -o -name .class -a -prune  -o  -type f  -print |")   or die "Couldn't execute program: $!";
  while ( defined( my $line = <TARGET_DATA> )  ) 
    {
    chomp($line);
    push (@targetFiles, $line) unless $line =~ /~$/;
    }
  close TARGET_DATA;
  
  ##### make certain all the target files are in the origin
  foreach my $targetFile (@targetFiles)
    {
    $targetFile =~ s/$targetProjectPath\///g;
    unless (-e "$originProjectPath/$targetFile")
      {
      print(REPORT "MISSING FILE in $originProjectPath/$targetFile\n");
      }
    }

  foreach my $diffFile (@diffFiles)
    {
    diffFiles("$originProjectPath/$diffFile", "$targetProjectPath/$diffFile");
    }
  }

sub checkoutAndCompareProjects
  {
  my ($component_name, $origin_rpath, $target_rpath, $tmp_dir) = @_;
  my $originProjectURL = $REPOSITORY_URL . $origin_rpath;
  my $originProjectPath = $configProperties{$TMP_DIR_KEY} . "/origin/" . $component_name;
  my $rtn = `svn checkout $originProjectURL $originProjectPath`;
  $rtn = `mvn clean $originProjectPath`;

  my $targetProjectURL = $REPOSITORY_URL . $target_rpath;
  my $targetProjectPath = $configProperties{$TMP_DIR_KEY} . "/target/" . $component_name;
  $rtn = `svn checkout $targetProjectURL $targetProjectPath`;
  $rtn = `mvn clean $targetProjectPath`;
  compareDirectoryCopies($originProjectPath, $targetProjectPath);
  my $confirm;
  print "\n****************************************************************\n";
  print "\nDo you wish to commit the changes in $targetProjectURL? (Y/N)\n";
  print "> ";
  chomp($confirm = <STDIN>);
  if ($confirm eq "Y")
    {
    $rtn = `svn commit -m"merging differences for $component_name" $targetProjectPath`;
    }
  remove_tree( $configProperties{$TMP_DIR_KEY} . "/origin", $configProperties{$TMP_DIR_KEY} . "/target");
  }
  
 ##### i like defining the main subroutine, especially if there is the possibility of
 ##### other subroutines being a part of this namespace
 main:  {
   my $help;
   my $mesg;
   my %controlledProjects;
   my $all;
   my %Options;
   my $tmp_dir;
   my %originProjects;
   my $originPropertyURL;
   my %targetProjects;
   my $targetPropertyURL;

   my $ok = GetOptions(\%Options, 
       "component=s", 
       "tag",
       "branch",
       "all",
     'help') or usage("Invalid commmand line options.");
    ### trunc report  
    open (REPORT, "> mergeDiffProjects.report"); 
    ##### Loop continuesly and ask for command input
    if ( ! (-e $PROPERTIES_FILE )) 
     {
     
     if ( ! (-e "/tmp" && -d "/tmp"))
       {
       my $tmp_dir;
       print "Enter full path of where to store temporary files? (such as " . $ENV{"HOME"} . "/tmp)\n";
       print "> ";
       chomp($tmp_dir = <STDIN>);
       
       make_path($tmp_dir) || die ("unable to create the directory $tmp_dir") if (! (-e $tmp_dir && -d $tmp_dir));
       %configProperties = ($TMP_DIR_KEY => $tmp_dir);
       
       } 
     else 
       {
       %configProperties = ($TMP_DIR_KEY => "/tmp");
       print REPORT "TMP_DIR_KEY defaulting to '/tmp')\n";
       }
     writeProperties(\%configProperties);
     } 
   else 
     {
     ##### read the properties file to determine where the tmp directory is
     print REPORT "reading configProperties\n";
     readProperties(\%configProperties);
     }


  usage("Have some Help") if ((defined $Options{help}) || !$ok);
  usage("Do you know what you are doing?") unless ((defined($Options{all}) || exists($Options{component})) && ( defined($Options{tag}) || defined($Options{branch}) ));

  if (defined($Options{tag}))
    {
    $originPropertyURL = "$REPOSITORY_URL/trunk/$beta_build_control";
    $targetPropertyURL = "$REPOSITORY_URL/trunk/$stable_build_control";
    } 
  elsif (defined($Options{branch}))
    {
    $originPropertyURL = "$REPOSITORY_URL/trunk/$dev_build_control";
    $targetPropertyURL = "$REPOSITORY_URL/trunk/$beta_build_control";
    }
  else
    {
    die("should never have gotten here after the usage check\n");
    }
  foreach my $key (keys %configProperties)
    {
    print  "$key = " . $configProperties{$key} . "\n";
    }
   

  ##### if using the all designation, you must have the trunk checked out to a single directory
  ##### and you must checkout all the targetes to a single directory as well
  ##### pass in those full paths to the appropriate arguments
  setControlledProjects($originPropertyURL, \%originProjects);
  setControlledProjects($targetPropertyURL, \%targetProjects);
  print REPORT "Origin Projects\n";
  print REPORT "key\t\toriginProjects\n";
  foreach my $key (keys(%originProjects))
    {
    print "$key $originProjects{$key}\n";
    print REPORT "$key\t\t$originProjects{$key}\n";
  }
  print REPORT "\nTarget Projects\n";
  print REPORT "key\t\ttargetProjects\n";
  foreach my $key (keys(%targetProjects))
    {
    print "$key $targetProjects{$key}\n";
    print REPORT "$key\t\t$targetProjects{$key}\n";
  }
  if (defined($Options{all})) 
    {
    foreach my $component (keys(%originProjects))
      {
      if (exists($targetProjects{$component}) && defined($targetProjects{$component})) 
        {

          checkoutAndCompareProjects($component, $originProjects{$component}, $targetProjects{$component}, $tmp_dir);
        }
      else 
        {
        warn("$component does not have an entry in the target\n");
        }
      }
    }
  else 
    {
     my $component = uc($Options{component});
     if (! (exists($originProjects{$component}) && exists($targetProjects{$component})))
       {
       my $deb_component = "${component}_DEB";
       die ("$component does not exists in control properties file") if (! (exists($originProjects{$deb_component}) && exists($targetProjects{$deb_component})));
       $component = $deb_component;
       }

     checkoutAndCompareProjects($component, $originProjects{$component}, $targetProjects{$component}, $tmp_dir);
     }  
   close (REPORT);

   } # end the main subroutine
  exit(0);
