#!/usr/bin/env perl 
use XML::LibXML;
use IO::File;
use Data::Dumper;
use Getopt::Long;
use POSIX qw(strftime);
use Cwd qw(abs_path);
use File::Basename;
#==========================================================================
# Globals
#==========================================================================
my %opts;
my $scriptdir = dirname(abs_path(__FILE__));
my $testlist = "$scriptdir/testlist.xml";
my $banner = "==========================================================================";

{
	#==========================================================================
	# Simple attribute class to facilitate easier test parsing..
	#==========================================================================
	package CESMTest;

	sub new 
	{
		my ($class, %params) = @_;
		
		my $self = {
			compset => $params{'compset'} || undef, 	
			grid    => $params{'grid'} || undef, 	
			testname => $params{'testname'} || undef, 	
			machine  => $params{'machine'} || undef, 	
			compiler  => $params{'compiler'} || undef, 	
			testmods  => $params{'testmods'} || undef, 	
			comment  => $params{'comment'} || undef, 	
		};
		bless $self, $class;
		return $self;
	}
}

#==========================================================================
# Show the usage. 
#==========================================================================
sub usage
{

my $usgstatement;
$usgstatement = <<EOF;
SYNOPSIS

    manage_xml_entries -addlist -file new_test_list -category test_category
    
    manage_xml_entries -synclist -file modified_test_list -category test_category 
                        [-compset|-grid|-test|-machine|-compiler|-testmods]
   
    manage_xml_entries -removetests [-compset|-grid|-test|-machine|-compiler|-testmods]

    manage_xml_entries -list compsets|grids|tests|machines|compilers

    manage_xml_entries -query [-compset|-grid|-test|-machine|-compiler|-testmods]


DESCRIPTION

    Adds, removes, and 'syncs' modified testlists with the main xml test file.  

TEST FILE FORMAT: 

    Testname.Grid.Compset.Machine_Compiler[.Testmods] # Test comments may be placed here. 

USAGE, OPTIONS, AND EXAMPLES
   
    There are three main modes of operation: -addlist, -synclist, and -removetests.
    Usage for each of the modes are described below.  

    -addlist:
 
    This mode is intended for adding new tests to the testlist. The script will parse your text 
    list, and add the new test to the appropriate compset, grid, and test entry.  If duplicates are found, 
    they will be silently ignored, even if they contain a different comment.  Also, please not that since the
    CESM text testlist never contained a test category, the test category is required to add tests. 

    Example:
    To clone and add a new set of prebeta tests for a machine, one would do the following: 
    use ./manage_xml_entries -query -outputlist and the appropriate flags, and save the output. 

    Modify the test list, change the machine, compiler, and add comments if necessary.  

    run manage_xml_entries -addlist -file newfile.txt -category prebeta.   


    -synclist: 
 
    This mode is intended to let one dump an existing test list, 'aux_clm_short' for example, 
    make changes as required, and then re-import those changes back into the test list. The script
    will delete tests that match any of the following options: -compset, -grid, -test, -machine, 
    -compiler, and -testmods.  This is currently the easiest way I can see to make mass edits possible, 
    and as a result, you *MUST* use the same options used to output the test list when you use the -synclist
    option. Also, since the category was never part of the test list, the testlist can only be mass-edited 
    by each category.  
 
    Example:  
    To modify the yellowstone PGI prebeta test list for example, one would do the following: 
	manage_xml_entries -query -outputlist -machine yellowstone -compiler pgi -category prebeta > yellowstone.txt
 
    Make any modifications necessary to yellowstone.txt

    manage_xml_entries -synclist -machine yellowstone -compiler pgi -category prebeta -file yellowstone.txt

    This command will delete all tests that match yellowstone, the pgi compiler, and the prebeta category. 
    Then all the potentially modified tests in yellowstone.txt will be added back in.   

    -removetests:
 
    This mode is intended delete any tests found matching the specified arguments.
    Want to delete all the aux_clm tests?  
    manage_xml_entries -removetests -category aux_clm
   
    Want to delete all the BG1850CN tests?  
    manage_xml_entries -removetests -compset BG1850CN
	
    want to delte the yellowstone aux_science tests using only the gnu compiler? 
    manage_xml_entries -removetests -category aux_science -machine yellowstone -compiler gnu

    Want to remove only tests with the 'clm/decStart' testmod? 
    manage_xml_entries -removetests -testmods "clm/decStart"

    -list <name>             name can be [compsets,grids,compilers,machines,categories,tests]

	list the available compsets, grids, compilers, machines, categories, or tests. 

    -query:

	Query the testlist by compset, grid, test, compiler, machine, testmod, or any combination 
    thereof. 

    A note on the PE count specifiers:
    T, S, M, L, X are not constant.  first, they are only defined for a few
    compsets/resolution combinations.  second, they depend on compset
    and resolution.   you can imagine a T31_g37 range being 100 cores
    to 1000 cores from T to X.  but ne240_t12 would probably be 500 at T
    and 100,000 at X, for instance.  the 1, 1x1, 16x4, etc specify the tasks
    and threads for each component directly.

    A note about the "points mode" specifier for tests, ie SMS_RL*:
    "L" is a land point "O" is an ocean point A and B are two points on the land
    and ocean that CLM provided. so LA, LB, OA, and OB are 4 distinct single test
    points, 2 over land, 2 over ocean.
EOF

	print $usgstatement;
	exit(1);

}


#==========================================================================
# Get the options, check the options. 
#==========================================================================
sub getOptions
{
	usage() if(@ARGV < 1);
	GetOptions(
		"h|help"   => \$opts{'help'},
		"f|file=s" => \$opts{'file'},
		"a|addlist" => \$opts{'addlist'},
		"s|synclist" => \$opts{'synclist'},
		"r|removetests" => \$opts{'removetests'},
        "q|query"       => \$opts{'query'},
        'l|list=s'       => \$opts{'list'},
        "outputlist"    => \$opts{'outputlist'},
        "outputxml"    => \$opts{'outputxml'},
		"compset=s"    => \$opts{'compset'},
		"grid=s"    => \$opts{'grid'},
		"test=s"    => \$opts{'test'},
		"machine=s"    => \$opts{'machine'},
		"compiler=s"    => \$opts{'compiler'},
		"category=s"    => \$opts{'category'},
		"testmods=s"    => \$opts{'testmods'},
	);
	usage() if $opts{'help'};
	if(defined $opts{'addlist'} && ! defined $opts{'file'})
	{
		print "To add test lists, you must supply a test list via the -file option\n";
		exit(1);
	}
	if(defined $opts{'addlist'} && ! defined $opts{'category'})
	{
		print "The -category option is required to add tests\n";
		exit(1);
	}
	if(defined $opts{'synclist'} && ! defined $opts{'file'})
	{
		print "To sync changes to a test list, you must supply a test list via the -file option\n";
		exit(1);
	}
	if(defined $opts{'synclist'} && ( ! defined $opts{'compset'} && ! defined $opts{'grid'} && ! defined $opts{'test'}
               && ! defined $opts{'category'} && ! defined $opts{'machine'} && ! defined $opts{'compiler'} && ! defined $opts{'testmods'}))
	{
		print "To sync changes to an existing test list, \n";
		print "one or more of the following options must be supplied:\n";
		print "compset, grid, test, machine, compiler, or testmods\n";
		exit(1);
	}
	if(defined $opts{'removetests'} &&  ! defined $opts{'compset'} &&  ! defined $opts{'grid'} && ! defined $opts{'test'}
               && ! defined $opts{'category'} && ! defined $opts{'machine'} && ! defined $opts{'compiler'} && ! defined $opts{'testmods'})
	{
		print "To delete tests from the xml file, \n";
	    print "one or more of the following options must be supplied:\n";
		print "compset, grid, test, machine, compiler, or testmods\n";
		exit(1);
	}
	if(defined $opts{'list'} && $opts{'list'} !~ /(compsets|grids|compilers|machines|categories|tests)/)
	{
		print "the -list option must be set to one of the following values: \n";
		print "compsets, grids, compilers, machines, categories, or tests\n";
	}

}


#==========================================================================
# Read the testlist.xml file, return the XML::LibXML::Document
#==========================================================================
sub readXML
{
	my $xmlfile = shift;
	my $parser = XML::LibXML->new( no_blanks => 1);
	my $testxml = $parser->parse_file($testlist);
	return $testxml;
}

#==========================================================================
# Write the new testlist xml file.  
#==========================================================================
sub writeXML
{
	my ($testxml) = shift;
	my $dtformat = strftime "%d%b%Y-%H%M%S",  localtime;
	my $newfilename = "testlist-$dtformat.xml";
	print "now writing the new test list to $newfilename\n";
	print "Please carefully review and/or diff the new file against the\n";
	print "original, and if you are satisfied with the changes, move \n";
	print "$newfilename to testlist.xml\n";
	open my $NEWTESTXML, ">", "./$newfilename" or die $?;
	my $tststring = $testxml->toString(1);
	print $NEWTESTXML $tststring;
	close $NEWTESTXML;
}

#==========================================================================
# Parse the text test list.  
#==========================================================================
sub parseTextList
{
	my ($testfile) = @_;

	open my $TSTFILE, "<", $testfile or die "can't open $txtfile";
	my @lines = <$TSTFILE>;
	close $TSTFILE;

	# We're building a list of CESMTest objects
	my @tests;
	my $testsfound = 0;
	
	foreach my $line(@lines)
	{
		my $comment = undef;
		chomp $line;	
		# skip blank lines and # commments at the beginning of lines. 
		next if $line =~ m/^$/;
		next if $line =~ m/^#/;

		# if we find a comment, split by #, and strip the whitespace. 
		if($line =~ m/#/)
		{
			($line, $comment) = split('\#', $line, 2);
			# no single quotes in comments, it messes up the XPATH xml searching
			# when parsing test lists. 
			if($comment =~ /'/)
			{
				print "Sorry, quotes aren't allowed in comments. The offending line was:\n";
				print "$line$comment\n";
				exit(1);
			}
			$comment =~ s/^\s+//;
			$comment =~ s/\s+$//;
			$line =~ s/^\s+//;
			$line =~ s/\s+$//;
		}
		
		my ($testname, $grid, $compset, $machcomp, $testmods) = split('\.', $line);	
		# if the split results in an undefined testname, grid, compset, machcomp, machine, or
		# compiler, complain and exit. 
		my $parse_error = 0;
		if(! defined $testname || ! defined $grid || ! defined $compset || ! defined $machcomp)
		{
			$parse_error = 1;
		}
	
		my ($machine, $compiler) = split('_', $machcomp);
		if(! defined $machine || ! defined $compiler)
		{
			$parse_error = 1;
		}
		if($parse_error)
		{
			print "Formatting error found in the following line:\n";
			print "$line\n";
			print "Please review your test list and correct any errors\n";
			return undef;
		}
		
		if(defined $testmods)
		{
			$testmods =~ s/ //g;
			$testmods =~  s/-/\//g;
		}
		my $tst = new CESMTest(compset => $compset, grid => $grid, testname => $testname, 
					          machine => $machine, compiler => $compiler); 
		
		if(defined $testmods)
		{
			$tst->{testmods} = $testmods;
		}
		if(defined $comment)
		{
			$tst->{comment} = $comment;
		}
		
		push(@tests, $tst);
		$testsfound++;
	}
	print "found $testsfound tests in $testfile\n";

	return \@tests;
}

#==========================================================================
# Add a new set of tests.  Go through the list of CESMTests, silently ignore 
# any that happen to already be there, and add the tests that don't yet exist 
# to a new machine element.  Then walk back up the tree of tests, grids, and compsets, 
# adding any that do not yet exist. 
#==========================================================================
sub addXMLTests
{
	my ($tests, $testxml, $testtype) = @_;
	my $acounter = 0;
	foreach my $test(@$tests)
	{
		# get the relevant values. 
		my $compset = $test->{compset};
		my $grid = $test->{grid};
		my $testname = $test->{testname};
		my $machine = $test->{machine};
		my $compiler = $test->{compiler};
		my $testmods = $test->{testmods};
		my $comment = $test->{comment};

		# Search the xml for matchng tests using Xpath queries. 
		my @xmltestnodes;
		if(defined $test->{comment} && defined $test->{testmods})
		{
			@xmltestnodes =
            $testxml->findnodes("/testlist/compset[\@name=\'$compset\']/grid[\@name=\'$grid\']/test[\@name=\'$testname\']/machine[text()=\'$machine\' and \@compiler=\'$compiler\' and \@testtype=\'$$testtype\' and \@testmods=\'$testmods\' and \@comment=\'$comment\']");
		}
		elsif(! defined $test->{comment} &&  defined $test->{testmods})
		{
			@xmltestnodes =
            $testxml->findnodes("/testlist/compset[\@name=\'$compset\']/grid[\@name=\'$grid\']/test[\@name=\'$testname\']/machine[text()=\'$machine\' and \@compiler=\'$compiler\' and \@testtype=\'$$testtype\' and \@testmods=\'$testmods\' and not(\@comment) ]");

		}
		elsif( defined $test->{comment} &&  !defined $test->{testmods})
		{
			@xmltestnodes =
            $testxml->findnodes("/testlist/compset[\@name=\'$compset\']/grid[\@name=\'$grid\']/test[\@name=\'$testname\']/machine[text()=\'$machine\' and \@compiler=\'$compiler\' and \@testtype=\'$$testtype\' and \@comment=\'$comment\' and not(\@testmods) ]");

		}
		elsif( ! defined $test->{comment} && ! defined $test->{testmods})
		{
			@xmltestnodes =
            $testxml->findnodes("/testlist/compset[\@name=\'$compset\']/grid[\@name=\'$grid\']/test[\@name=\'$testname\']/machine[text()=\'$machine\' and \@compiler=\'$compiler\' and \@testtype=\'$$testtype\' and not(\@testmods) and not(\@comment)]");

		}
		# if we find matching tests in the xml, skip adding the test. 
		next if @xmltestnodes;

		# If we're here, we need to add a new machne element with the machine name
		# and relevant attributes. 
		my $newmachnode = $testxml->createElement('machine');
		$newmachnode->appendText($machine);
		$newmachnode->setAttribute('compiler', $compiler);
		$newmachnode->setAttribute('testtype', $$testtype);
		$newmachnode->setAttribute('testmods', $testmods) if defined $testmods;
		$newmachnode->setAttribute('comment', $comment) if defined $comment;

		# Now we search for matching compset, grid, and test nodes again using Xpath queries.  
		my $compsetnode;
		my $gridnode;
		my $testnode;
		my @testnodes = $testxml->findnodes("/testlist/compset[\@name=\'$compset\']/grid[\@name=\'$grid\']/test[\@name=\'$testname\']");
		my @gridnodes = $testxml->findnodes("/testlist/compset[\@name=\'$compset\']/grid[\@name=\'$grid\']");
		my @compsetnodes = $testxml->findnodes("/testlist/compset[\@name=\'$compset\']");
		
		# if a matching test node is found, set the test node to the found test node. Otherwise add 
		# a new one. 
		if(@testnodes)
		{
			$testnode = $testnodes[0];
		}
		else
		{
			$testnode = $testxml->createElement('test');
			$testnode->setAttribute('name', $testname);
		}
		$testnode->addChild($newmachnode);
		$acounter++;
		
		# if a matching grid node is found, set the test node to the found test node. Otherwise add 
		# a new one. 
		if(@gridnodes)
		{
			$gridnode = $gridnodes[0];
		}
		else
		{
			$gridnode = $testxml->createElement('grid');
			$gridnode->setAttribute('name', $grid);
		}
		# if a matching compset node is found, set the test node to the found test node. Otherwise add 
		# a new one. 
		if(@compsetnodes)
		{
			$compsetnode = $compsetnodes[0];
		}
		else
		{
			$compsetnode = $testxml->createElement('compset');
			$compsetnode->setAttribute('name', $compset);
		}

		# add node children if necessary..
		$testnode->addChild($newmachnode);
		$gridnode->addChild($testnode) if !@testnodes;		
		$compsetnode->addChild($gridnode) if !@gridnodes;		

		my $root = $testxml->findnodes('./testlist')->get_node(0);
		$root->addChild($compsetnode) if ! @compsetnodes;
	}

	$testxml = sortXML($testxml);
	print "added $acounter tests\n";
	return $testxml;
	

}

#==========================================================================
# Sort the xml entries.  Drill down to the machine element, sort the machine elements
# by machine name, testtype, then compiler.  Then sort the tests, then the grids,
# finally the compsets.
#==========================================================================
sub sortXML
{
	my ($testxml) = shift;
	foreach my $compset($testxml->findnodes('/testlist/compset'))
	{
		foreach my $grid($compset->findnodes('./grid'))
		{
			foreach my $test($grid->findnodes('./test'))
			{
				#sort the machines nodes by machine, then by test type, then by
				#compiler. 
				my @machnodes = $test->findnodes('./machine');
				my @sortedMachNodes = sort {
					$a->textContent() cmp $b->textContent() ||
					$a->getAttribute('testtype') cmp $b->getAttribute('testtype') ||
					$a->getAttribute('compiler') cmp $b->getAttribute('compiler') 
				} @machnodes;
				# remove the unsorted nodes, and re-add the sorted nodes..
				$test->removeChildNodes();
				map {  $test->addChild($_) } @sortedMachNodes;
			}
			# sort the test nodes by name, remove the unsorted test nodes, 
			# re-add the sorted test nodes. 
			my @testnodes = $grid->findnodes('./test');
			my @sortedTestNodes = sort {
				$a->getAttribute('name') cmp $b->getAttribute('name')
			} @testnodes;
			$grid->removeChildNodes();
			map { $grid->addChild($_) } @sortedTestNodes;
		}
        # sort the grid nodes by name, remove the unsorted, then 
        # add the sorted. 
		my @gridnodes = $compset->findnodes('./grid');
		my @sortedGridNodes = sort {
			$a->getAttribute('name') cmp $b->getAttribute('name')
		} @gridnodes;
		$compset->removeChildNodes();
		map { $compset->addChild($_) } @sortedGridNodes;
	}

	# sort the compset nodes. 
	my @compsetnodes = $testxml->findnodes('/testlist/compset');
	my @sortedCompsetNodes = sort {
		$a->getAttribute('name') cmp $b->getAttribute('name')
	} @compsetnodes;

	
	# get the root element, remove the unsorted compset nodes, 
	# add the sorted compset nodes. 
	my $root = $testxml->getDocumentElement();
	$root->removeChildNodes();
	map { $root->addChild($_) } @sortedCompsetNodes;

	return $testxml;
	
}

#==========================================================================
# Remove xml tests if they match the compset, grid, testname, machine, compiler
# test category , or testmods argument, and return the xml object. 
#==========================================================================
sub removeXMLTests
{
	my ($testxml, $compset, $grid, $test, $machine, $compiler, $testtype, $testmods) = @_;
	# drill down into the machine nodes.  Move onto the next element
	# if anything doesn't match the compset, grid, testname...
	my $rcounter = 0;
	foreach my $compsetnode($testxml->findnodes('/testlist/compset'))
	{
		my $xcompsetname = $compsetnode->getAttribute('name');
		if(defined $$compset)
		{
			next unless ($xcompsetname eq $$compset);
		}
		foreach my $gridnode($compsetnode->findnodes('./grid'))
		{
			my $xgridname = $gridnode->getAttribute('name');
			if(defined $$grid)
			{
				next unless ($xgridname eq $$grid);
			}
			foreach my $testnode($gridnode->findnodes('./test'))
			{
				my $xtestname = $testnode->getAttribute('name');
				if(defined $$test)
				{
					next unless ($xtestname eq $$test);
				}
				# get the machine node content. Skip anything that doesn't
				# match the machine name, compiler, test category, or testmods.
				foreach my $machnode($testnode->findnodes('./machine'))
				{
					my $xmachinename = $machnode->textContent();
					my $xcompilername = $machnode->getAttribute('compiler');
					my $xtesttypename = $machnode->getAttribute('testtype');
					my $xtestmodsname = $machnode->getAttribute('testmods');
					
					if(defined $$machine)
					{
						next unless $xmachinename eq $$machine;
					}
					if(defined $$testtype)
					{
						next unless $xtesttypename eq $$testtype;
					}
					if(defined $$compiler)
					{
						next unless $xcompilername eq $$compiler;
					}
					if(defined $$testmods)
					{
						next unless $xtestmodsname eq $$testmods;	
					}

					# Remove the node if everything matched..
					$testnode->removeChild($machnode);
					$rcounter++;
				}
				# If the current test entry doesn't have any children, 
				# remove it.
				if(! $testnode->nonBlankChildNodes())
				{
					my $parent = $testnode->parentNode();
					$parent->removeChild($testnode);
				}
				
			}
			# If the current grid node doesn't have any children, 
			# remove it. 
			if(! $gridnode->nonBlankChildNodes())
			{
				my $parent = $gridnode->parentNode();
				$parent->removeChild($gridnode);
			}
		}
		# If the current compset node doesn't have any children, 
		# remove it from the root. 
		if(! $compsetnode->nonBlankChildNodes())
		{
				my $parent = $compsetnode->parentNode();
				$parent->removeChild($compsetnode);
		}
	}
	print "removed $rcounter tests\n";
	return $testxml;
}

#==========================================================================
# Query the relevant file for the available compsets, grids, tests, machines,
# and compilers. 
#==========================================================================
sub list
{
	my $listopt = $opts{'list'};
	my $testxml = readXML();
	
	my @list;
	my %uniqs;
	if($listopt =~ /compsets/)
	{
		print "$banner\nAvailable compsets\n$banner\n";
		foreach my $elem($testxml->findnodes('//compset'))
		{
			my $val = $elem->getAttribute('name');
			push(@list, $val);
		}
        map { $uniqs{$_} = 1} @list;
        @list = keys %uniqs;
        map { print "  $_ \n"} sort @list;
	}
	if($listopt =~ /grids/)
	{
		print "$banner\nAvailable grids\n$banner\n";
		foreach my $elem($testxml->findnodes('//grid'))
		{
			my $val = $elem->getAttribute('name');
			push(@list, $val);
		}
        map { $uniqs{$_} = 1} @list;
        @list = keys %uniqs;
	    map { print "  $_ \n"} sort @list;
	}
	if($listopt =~ /machines/)
	{
		print "$banner\nAvailable machines\n$banner\n";
		foreach my $elem($testxml->findnodes('//machine'))
		{
			my $val = $elem->textContent;
			push(@list, $val);
		}
        map { $uniqs{$_} = 1} @list;
        @list = keys %uniqs;
	    map { print "  $_ \n"} sort @list;
	}
	if($listopt =~ /categories/)
	{
		print "$banner\nAvailable test categories\n$banner\n";
		foreach my $elem($testxml->findnodes('//machine'))
		{
			my $val = $elem->getAttribute('testtype');
			push(@list, $val);
		}
        map { $uniqs{$_} = 1} @list;
        @list = keys %uniqs;
	    map { print "  $_ \n"} sort @list;
	}
	if($listopt =~ /tests/)
	{
		print "$banner\nAvailable Tests\n$banner\n";
		my $testconfig = XML::LibXML->new()->parse_file("$scriptdir/../Testcases/config_tests.xml");
		my $testroot = $testconfig->getDocumentElement();
		foreach my $elem($testroot->findnodes('//ccsmtest'))
		{
			my $name = $elem->getAttribute('NAME');
			my $desc = $elem->getAttribute('DESC');
			my $val  = "$name ($desc)";
			push(@list, $val);
		}
        map { $uniqs{$_} = 1} @list;
        @list = keys %uniqs;
	    map { print "  $_ \n"} sort @list;
    	print << 'EOF';

   The following modifiers can be used in the test name
    _CG  = gregorian calendar
    _D   = debug
    _E   = esmf interfaces
    _IOP*= PnetCDF IO test where * is
           A(atm), C(cpl), G(glc) , I(ice),
           L(clm), O(ocn), W(wav) or blank (all components)
    _L*  = set run length y, m, d, h, s, n(nsteps) plus integer (ie _Lm6 for 6 months)
    _M*  = set the mpilib to *, where * is default, mpi-serial, mpich, etc
    _N*  = set NINST_ env value to *, where * is an integer
    _P*  = set pecount to *, where * are specific values which include
           T,S,M,L,X,1,1x1,16,16x1,4x4, etc
    _R*  = PTS_MODE test case, valid values are LA, LB, OA, OB
EOF
    print     " \n";
	}
	if($listopt =~ /compilers/)
	{
		print "$banner\nAvailable compilers\n$banner\n";
		foreach my $elem($testxml->findnodes('//machine'))
		{
			my $compiler = $elem->getAttribute('compiler');
			push(@list, $compiler);
		}
		map { $uniqs{$_} = 1} @list;
		@list = keys %uniqs;
		map { print "  $_\n"} sort @list;
	}
	
}

#==========================================================================
# query the test xml file by compset, grid, test, machine, compiler, 
# test category, testmods, or any combination thereof. Just use the testxml
# object read in from testlist.xml, remove any xml elements that don't match, 
# and return the testxml object with only the nodes matching the query. 
#==========================================================================
sub queryXMLTests
{
    my ($testxml, $compset, $grid, $test, $machine, $compiler, $testtype, $testmods) = @_;
    # drill down into the machine nodes.  Move onto the next element
    # if anything doesn't match the compset, grid, testname...
	my $root = $testxml->getDocumentElement();
	
	foreach my $compsetnode($testxml->findnodes('/testlist/compset'))
	{
		my $xcompsetname = $compsetnode->getAttribute('name');
		if(defined $$compset && $$compset ne $xcompsetname)
		{
			$root->removeChild($compsetnode);
			next;
		}
		foreach my $gridnode($compsetnode->findnodes('./grid'))
		{
			my $xgridname = $gridnode->getAttribute('name');
			if(defined $$grid && $$grid ne $xgridname)
			{
				$compsetnode->removeChild($gridnode);
				next;
			}
			foreach my $testnode($gridnode->findnodes('./test'))
			{
				my $xtestname = $testnode->getAttribute('name');
				if(defined $$test && $$test ne $xtestname)
				{
					$gridnode->removeChild($testnode);
					next;	
				}
				foreach my $machnode($testnode->findnodes('./machine'))
				{
					my $xmachinename = $machnode->textContent();
					my $xcompilername = $machnode->getAttribute('compiler');
					my $xtesttypename = $machnode->getAttribute('testtype');
					my $xtestmodsname = $machnode->getAttribute('testmods');
				
					if(defined $$machine && $$machine ne $xmachinename)
					{
						$testnode->removeChild($machnode);
						next;
					}
					if(defined $$compiler && $$compiler ne $xcompilername)
					{
						$testnode->removeChild($machnode);
						next;
					}
					if(defined $$testtype && $$testtype ne $xtesttypename)
					{
						$testnode->removeChild($machnode);
						next;
					}
					if(defined $$testmods && $$testmods ne $xtestmodsname)
					{
						$testnode->removeChild($machnode);
						next;
					}
				}
				if(! $testnode->nonBlankChildNodes())
				{
					$gridnode->removeChild($testnode);
				}
			}
			if(! $gridnode->nonBlankChildNodes())
			{
				$compsetnode->removeChild($gridnode);
			}
		}
		if(! $compsetnode->nonBlankChildNodes())
		{
			$root->removeChild($compsetnode);
		}
		
	}
	return $testxml;
}

#==========================================================================
# Query subroutine called from main. Read the xml file, query the object, 
# and print the user's choice of output. 
#==========================================================================
sub query
{
	my $testxml = readXML();
	$testxml = queryXMLTests($testxml, \$opts{'compset'}, \$opts{'grid'}, \$opts{'test'}, \$opts{'machine'}, \$opts{'compiler'},
                    \$opts{'category'}, \$opts{'testmods'});
    #print $testxml->toString(1);
	$testxml = sortXML($testxml);
	if($opts{'outputxml'})
	{
		print $testxml->toString(1);
	}
	elsif($opts{'outputlist'})
	{
		textOutput($testxml);
	}
	else
	{
		formattedOutput($testxml);
	}
}

#==========================================================================
# Print out the queried test list in the old-style test output. 
#==========================================================================
sub textOutput
{
	my $testxml = shift;
	#print $testxml->toString(1);
	my @output;
	foreach my $compsetnode($testxml->findnodes('./testlist/compset'))
	{
		foreach my $gridnode($compsetnode->findnodes('./grid'))
		{
			foreach my $testnode($gridnode->findnodes('./test'))
			{
				foreach my $machnode($testnode->findnodes('./machine'))
				{
					my $compset = $compsetnode->getAttribute('name');
					my $grid = $gridnode->getAttribute('name');
					my $test = $testnode->getAttribute('name');
					my $machine = $machnode->textContent;
					my $compiler = $machnode->getAttribute('compiler');
					my $testtype = $machnode->getAttribute('testtype');
					my $testmods = $machnode->getAttribute('testmods');
					my $comment = $machnode->getAttribute('comment');

				  	my $line = "$test\.$grid\.$compset\.$machine\_$compiler";
				  	if(defined $testmods)
				  	{
				  	    $testmods =~ s/ //g;
				  	    $testmods =~  s/\//-/g;
				  	    $line .= "\.$testmods";
				  	}
				  	if(defined $comment)
				  	{
				  		$line .= " # $comment";
				  	}
					push(@output, $line);
				}
			}
		}
	}
	# add a header, and the options used to create the text list. 
	my $dtformat = strftime "%d%b%Y-%H%M%S",  localtime;
	my $header = "# Test list created $dtformat with the following options: ";
	$opt_output = "# ";
	$opt_output .= "-compset $opts{'compset'} " if defined $opts{'compset'};
	$opt_output .= "-grid $opts{'grid'} " if defined $opts{'grid'};
	$opt_output .= "-test $opts{'test'} " if defined $opts{'test'};
	$opt_output .= "-compiler $opts{'compiler'} " if defined $opts{'compiler'};
	$opt_output .= "-machine $opts{'machine'} " if defined $opts{'machine'};
	$opt_output .= "-category  $opts{'category'} " if defined $opts{'category'};
	$opt_output .= "-testmods  $opts{'testmods'} " if defined $opts{'testmods'};
	unshift(@output, $opt_output);
	unshift(@output, $header);
	
	#map { print "$_\n" } sort @output;
	map { print "$_\n" } @output;
}

#==========================================================================
# print out the queried test list in a (hopefully) nicely formatted fashion. 
#==========================================================================
sub formattedOutput
{
	my $testxml = shift;
	my @output;
    my $header =  sprintf("   %-50s %-20s %-20s %-20s  %-10s  %-20s %-20s",
         "Compset", "TestName", "Grid", "Machine_compiler", "Test Category", "TestMods (optional)", "Comment(Optional)");

    my %configcompsets;
	my $compsetxml = XML::LibXML->new()->parse_file("$scriptdir/../Case.template/config_compsets.xml");
	foreach my $cfgcompset($compsetxml->findnodes('//COMPSET'))
	{
		my $alias = $cfgcompset->getAttribute('alias');
		my $sname = $cfgcompset->getAttribute('sname');
		$configcompsets{$alias} = $sname;
	}
	
	foreach my $compsetnode($testxml->findnodes('./testlist/compset'))
	{
		foreach my $gridnode($compsetnode->findnodes('./grid'))
		{
			foreach my $testnode($gridnode->findnodes('./test'))
			{
				foreach my $machnode($testnode->findnodes('./machine'))
				{
					my $compset = $compsetnode->getAttribute('name');
					my $fcompset = "$compset ($configcompsets{$compset})";
					my $grid = $gridnode->getAttribute('name');
					my $test = $testnode->getAttribute('name');
					my $machine = $machnode->textContent;
					my $compiler = $machnode->getAttribute('compiler');
					my $machine_compiler = $machine . "_" . $compiler;
					my $testtype = $machnode->getAttribute('testtype');
					my $testmods = $machnode->getAttribute('testmods');
					my $comment = $machnode->getAttribute('comment');

					#next if($line =~ /^$/);
				    $line = sprintf("   %-50s %-20s %-20s %-20s  %-13s  %-20s %-20s", 
                    	$fcompset, $test, $grid, $machine_compiler, $testtype, $testmods, $comment);
					push(@output, $line);
				}
			}
		}
	}
	
	my @sortedoutput = sort @output;
	unshift(@sortedoutput, $header);
	map { print "$_\n" } @sortedoutput;

}

#==========================================================================
# Add the tests.  Parse the text file, read the xml, add the xml tests. 
# Write the new xml file. 
#==========================================================================
sub addTests
{
	my $tests = parseTextList($opts{'file'});
    # parseTextList will return undef if there is a parsing problem...
	exit(1) if(! defined $tests);
	my $testxml = readXML();
	$testxml = addXMLTests($tests, $testxml, \$opts{'category'});
	writeXML($testxml);
}

#==========================================================================
# Sync an existing test list. Read testlist.xml, remove the tests matching the 
# options (must be the same options used to dump the test list, parse the changed 
# text test list, add the changes back to the xml object, and write a new file. 
#==========================================================================
sub syncTests
{
	my $tests = parseTextList($opts{'file'});
    # parseTextList will return undef if there is a parsing problem...
	exit(1) if(! defined $tests);
	my $testxml = readXML();
	$testxml = removeXMLTests($testxml, \$opts{'compset'}, \$opts{'grid'}, \$opts{'test'}, \$opts{'machine'}, \$opts{'compiler'},
                    \$opts{'category'}, \$opts{'testmods'});
	$testxml = addXMLTests($tests, $testxml, \$opts{'category'});
	writeXML($testxml);

}

#==========================================================================
# Remove tests from testlist.xml.  Tests can be removed using compset, grid, test name, 
# machine, compiler, testmods, or any combination thereof. 
#==========================================================================
sub removeTests
{
	my $testxml = readXML();
	$testxml = removeXMLTests($testxml, \$opts{'compset'}, \$opts{'grid'}, \$opts{'test'}, \$opts{'machine'}, \$opts{'compiler'},
                    \$opts{'category'}, \$opts{'testmods'});
	$testxml = sortXML($testxml);
	writeXML($testxml);
}

sub main
{
	getOptions();
	if(defined $opts{'removetests'})
	{
		print "removing tests..\n";
		removeTests();
	}
	elsif(defined $opts{'addlist'})
	{
		print "adding tests...\n";
		addTests(\$opts{'file'}, \$opts{'category'});
	}
	elsif(defined $opts{'synclist'})
	{
		print "syncing changes to test list..\n";
		syncTests();
	}
	elsif(defined $opts{'list'})
	{
		list();
	}
	elsif(defined $opts{'query'})
	{
		query();
	}
}

main(@ARGV) unless caller;
