package GTx::APE::AtomicProcessEngine;

use strict;
use warnings;
use SqlEngine2;
use Carp;
use Class::Throwable;
use Time::HiRes qw(usleep ualarm gettimeofday tv_interval);

use GTx::APE::Process;
use GTx::APE::AtomicProcess;
use GTx::APE::AtomicProcessContainer;
use GTx::APE::ArrayPattern;

use constant LOGIN => 'gt_process';

use constant TRUE  => 1;
use constant FALSE => 0;

$Class::Throwable::Debug = 0;
$SqlEngine2::verbosity   = 0;


use vars qw( $VERBOSE );


{
    my %_attributes = (
		       sqlEngineObj      => undef,
		       topLevelProcessId => undef,
		       userObj           => undef,
		       populateDepth     => undef,
		       subProcessList       => [],
		       topLevelProcess   => undef,
		       );
    
    my %object;
    
    ## Autoloader wanna be. Inserts the sub calls that I want to be
    ## accessible into the symbol table.
    foreach my $attName (keys(%_attributes)) {
	no strict 'refs';
	unless($attName =~ /^sqlEngineObj/) {
	    *{__PACKAGE__ . "::get\u$attName"} = sub { $object{ $_[0] }->{$attName} }
	}
    }


    #################################################################
    ## Constructor method for this object. Returns a new
    ## AtomicProcessEngine object.
    #################################################################
    sub new {
	my ($proto,%args) = @_;

	my $class = ref($proto) || $proto;
	my $self  = bless {}, $class;
	
	%args = $self->_init(%args);

	my @leftover = keys %args;

	confess "Error: the following arguments are unknown - @leftover\n" if @leftover;
	
	## Start the population of process objects.
	$self->_startPopulation();

	return $self;
    }


    ##################################################################
    ## Initialise the objects private attributes. The argument is
    ## deleted as it is found, initialised and set.
    ##################################################################
    sub _init {
	my ($self,%args) = @_;
	
	foreach my $attName (keys(%args)) {
	    if(exists($_attributes{$attName})) { 
		$object{ $self }->{$attName} = delete $args{$attName};
	    }
	}

	$object{ $self }->{sqlEngineObj} = new SqlEngine2();

	## Attribute topLevelProcessId must be set.
	if(!defined $object{ $self }->{topLevelProcessId}) {
	    croak "***** ERROR: a top level process id must be parsed to the constructor - " .
		" this is a valid process.id_process number *****\n";
	}

	## Attribute userObj must be set with a User object.
	croak "ERROR: a User object must be passed as the " .
	    "FlushController objects may require user data to execute correctly."
	    unless $object{ $self }->{userObj} && 
	    ${$object{ $self }->{userObj}}->isa('User');


	## Attribute populateDepth must be undef (unlimited depth) or
	## above 1.
	if(defined $object{ $self }->{populateDepth} && 
	   $object{ $self }->{populateDepth} < 1) {
	    croak "***** ERROR: populateDepth has been set to a number under 1 " . 
		"- must be undef or above 1 *****\n";
	}

	return %args;
    }

    
    ##################################################################
    ## Method to create the top level process using the parsed
    ## id_process. This Process object is then populated down to the
    ## atomic processes.
    ##################################################################
    sub _startPopulation {
	my ($self) = @_;

	## Get this processes data and the data of processes under it.
	my $applicationSystemProcesses = eval { 
	    $object{ $self }->{sqlEngineObj}->getAll('GTx/APE/AtomicProcessEngine::GetProcesses',
						     [ $object{ $self }->{topLevelProcessId},
						       $object{ $self }->{topLevelProcessId} ],
						     SqlEngine2::getDbh(LOGIN)); };
	if($@) { croak $@; }
	
	foreach(@{$applicationSystemProcesses}) {
	    ## TEMP - until we have process data in the db for the other
	    ## Application System processes.
	    #unless($_->[0] == 5007 || $_->[0] == $object{ $self }->{topLevelProcessId}) {
		#_debug("Ignoring process: $_->[0]",0);
		#next;
	    #}

	    _debug("Attempting to process id_process: $_->[0]",0);

	    my $process = new GTx::APE::Process(processId     => $_->[0],
                                                code          => $_->[1],
                                                description   => $_->[2],
                                                executeOrder  => $_->[3],
                                                processTypeId => $_->[4],
                                                underProcess  => $_->[5],
                                                remark        => $_->[6]);
	    
	    my $depth = 1;
	    
	    if($process->getProcessId() == $object{ $self }->{topLevelProcessId}) {
		_debug("Top level process - storing and ignoring!",$depth);
		## Store the top level process.
		$object{ $self }->{topLevelProcess} = $process;
	    }
	    elsif($process->getProcessTypeId() == 60) {
		_debug("Top level process (tlp) has only atomic processes under it - " . 
		       "passing tlp object to _populate",$depth);
		## This is an atomic process which we only get if the
		## topLevelProcess is the parent to a group of atomic
		## processes. We have to pass the topLevelProcess
		## object to the populate method. Depth is irrelevant.
		$self->_populate($self->getTopLevelProcess(),$depth);

		## Push the AtomicProcessContainer object on to the SubProcessList.
		push( @{$object{ $self }->{subProcessList}}, $self->getTopLevelProcess()->getSubProcessList()->[0] );

                ## Reset the top level processes sub process list.
                $self->getTopLevelProcess()->resetProcessList();
		last;
	    }
	    else {
		_debug("Processing id_process: " . $process->getProcessId() . 
		       " description: '" . $process->getDescription() . "' normally.",$depth);
		## Populate top level kids with there child processes
		## and store this process.
		$self->_populate($process,$depth) 
		    if !$object{ $self }->{populateDepth} || $object{ $self }->{populateDepth} > 1;
		
		push( @{$object{ $self }->{subProcessList}}, $process );
	    }
	}
    }
    

    #################################################################
    ## Iterative method that continues to attach child processes to
    ## the parent process object until it finds atomic processes. If
    ## no atomic processes are found then an error is thrown.
    #################################################################
    sub _populate { 
	my ($self,$parentProcessObj,$depth) = @_;

	my $t0 = [gettimeofday] if $VERBOSE;

	my $newProcesses = eval { 
	    $object{ $self }->{sqlEngineObj}->getAll('GTx/APE/AtomicProcessEngine::GetUnderProcesses',
						     [ $parentProcessObj->getProcessId() ],
						     SqlEngine2::getDbh(LOGIN)); };
		
	my $tt = "";
	$tt    = tv_interval($t0) if $VERBOSE;
	
	_debug("Query 'GetUnderProcesses' took: $tt seconds and returned: '" . scalar(@{$newProcesses}) . 
	       "' using id_process: " . $parentProcessObj->getProcessId(),$depth);
	
	if($@) { croak $@; }
	
	if(!$newProcesses) { return; }

	$depth++;
	
	foreach(@{$newProcesses}) 
	{
	    my $isAtomicProcess = FALSE;
	    my $newProcessObj;

	    ## Check if the process type id (process.id_process_type)
	    ## makes this an atomic process.
	    if($_->[4] == 60) {
		_debug("Processing atomic processes under id_process '" . 
		       $parentProcessObj->getProcessId(),$depth);
		## Send these atomic processes to the AtomicProcess creator.
		my $newAtomicProcessList = $self->_getAtomicProcesses($newProcesses,$depth);
		## Add the list of atomic processes to a new AtomicProcessContainer.
		$newProcessObj = 
		    new GTx::APE::AtomicProcessContainer(parentProcessId  =>$parentProcessObj->getProcessId(),
							 atomicProcessList=>$newAtomicProcessList,
							 userObj          =>$object{ $self }->{userObj});
		if ($VERBOSE) {
		    _debug("New AtomicProcessContainer parentProcessId: ".$newProcessObj->getParentProcessId(),$depth);
		    _debug("Added '" . $newProcessObj->getAtomicProcessCount() . 
			   "' AtomicProcess objects to the AtomicProcessContainer.",$depth);
		    _debug("AtomicProcessContainer has created '" . $newProcessObj->getArrayingEntityCount() . 
			   "' new ArrayingEntity objects:",$depth);
		    foreach(@{$newProcessObj->getArrayingEntityList()}) {
			_debug("ArrayingEntity($_): " . $_->getAtomicProcessCount() . " AtomicProcess objects",$depth);
			foreach(@{$_->getSummary()}) {
			    _debug("$_",$depth);
			}
		    }
		}
		$isAtomicProcess = TRUE;
	    } 
	    else 
	    {
		## Create a new process object.
		$newProcessObj = new GTx::APE::Process(processId     => $_->[0],
                                                       code          => $_->[1],
                                                       description   => $_->[2],
                                                       executeOrder  => $_->[3],
                                                       processTypeId => $_->[4],
                                                       underProcess  => $_->[5],
                                                       remark        => $_->[6]);
                
		_debug("--- Processing id_process: $_->[0]",$depth);
		_debug("--- Description: '$_->[2]'",$depth);
		_debug("--- Attaching to id_process " . $parentProcessObj->getProcessId(),$depth);
	    }
	    
	    ## Attach this new process data to its parent process.
	    $parentProcessObj->newSubProcess($newProcessObj);
	    
	    ## Stop population if this is an atomic process (bottom of
	    ## the hierachy).
	    if($isAtomicProcess) { last }
	    
	    ## Check if we have met the population depth or carry on
	    ## populating.
	    if(!$object{ $self }->{populateDepth} || $depth < $object{ $self }->{populateDepth}) {
		$self->_populate($newProcessObj,$depth) ;
	    }else{
		_debug("Stopping due to populateDepth attribute: $depth = " . 
		       $object{ $self }->{populateDepth},$depth);
	    }
	}
    }


    #################################################################
    ## Gathers the data required to create an AtomicProcess object. An
    ## AtomicProcess inherits from a Process object so it also needs
    ## the data to initialise a Process object.
    #################################################################
    sub _getAtomicProcesses {
	my ($self,$processes,$depth) = @_;
	
	my @ids          =();
	my @atProcessList=();
	my %lookup       =();

	
	## Store the id_process nums so they can be used in the query
	## and store all the process data in a hash so that we can
	## match this data against the query return data.
	foreach(@{$processes}) {
	    my $processId = $_->[0];
	    $lookup{ $processId } = $_;
	    push( @ids, $processId );
	}

	my $t0 = [gettimeofday] if $VERBOSE;

	## Get all of the atomic processes data in one chunk.
	my $atomicProcessData = eval {
	    $object{ $self }->{sqlEngineObj}->getAllDynamic('GTx/APE/AtomicProcessEngine::GetAtomicProcesses',
							    [],[@ids],[],
							    SqlEngine2::getDbh(LOGIN));
	};
	
	my $tt = "";
	$tt    = tv_interval($t0) if $VERBOSE;

	_debug("Query 'GetAtomicProcesses' took: $tt seconds and returned: '" . 
	       scalar(@{$atomicProcessData}),$depth);

	if($@ || !defined $atomicProcessData) { confess "Error: can't get atomic process data: $@\n"; }

	my %horde =();
	my $index =0;
	
	foreach my $data (@{$atomicProcessData}) 
	{
	    ## This AtomicProcess id_process num from the db.
	    my $processId = $data->[0];

	    if(!exists($horde{$processId})) { 		
		my $processData = delete $lookup{$processId};
		
		## Match the id_process numbers.
		if($processData->[0] != $processId) {
		    confess "Error: lookup process id '$processData->[0]' doesn't match '$processId'\n";
		}
		
		++$index if defined $atProcessList[$index];

		$atProcessList[$index] = new GTx::APE::AtomicProcess(inConcreteIOId    => $data->[1],
                                                                     inConcreteEntity  => $data->[2],
                                                                     inEntityTypeId    => $data->[3],
								     inEntityRemark    => $data->[4],
                                                                     inPrefix          => $data->[5],  
                                                                     inDescription     => $data->[6],
								     inFormat          => $data->[14],
                                                                     outConcreteIOId   => $data->[7],
                                                                     outConcreteEntity => $data->[8],
                                                                     outEntityTypeId   => $data->[9],
								     outEntityRemark   => $data->[10],
                                                                     outPrefix         => $data->[11],  
                                                                     outDescription    => $data->[12],
								     outFormat         => $data->[22],
                                                                     
                                                                     processId     => $processData->[0],
                                                                     code          => $processData->[1],
                                                                     description   => $processData->[2],
                                                                     executeOrder  => $processData->[3],
                                                                     processTypeId => $processData->[4],
                                                                     underProcess  => $processData->[5],
                                                                     remark        => $processData->[6]
                                                                     );
		
		$horde{$processId} = 1;
	    }

	    
	    $atProcessList[$index]
		->addArrayPattern( new GTx::APE::ArrayPattern(inMapId     => $data->[13],  #$inMapId,
							      inMapFormat => $data->[14],  
                                                              inRow       => $data->[15],  #$inMapRow,
                                                              inColumn    => $data->[16],  #$inMapCol,
                                                              inFormat    => $data->[17],  #$inFormat,
                                                              inQuadrant  => $data->[18],  #$inQuadrant,
                                                              outMapId    => $data->[19],  #$outMapId,
                                                              outRow      => $data->[20],  #$outMapRow,
                                                              outColumn   => $data->[21],  #$outMapCol,
                                                              outFormat   => $data->[22],  #$outFormat,
                                                              outQuadrant => $data->[23]   #$outQuadrant) );
                                                              ));


	    ## Check that the newly created outFormat matches the
	    ## outFormat of the AtomicProcess. 
	    unless($atProcessList[$index]->getOutFormat() == $data->[22]) {
		die "ERROR: new ArrayPattern object with outFormat '$data->[22]' is different to " .
		    "its parent AtomicProcess object with process id '" . 
		    $atProcessList[$index]->getProcessId() . "' and outFormat '" . 
		    $atProcessList[$index]->getOutFormat() . "' - fatal error\n";
	    }


	}
	
	if(keys(%lookup)) { 
	    foreach(keys(%lookup)) { print STDERR " $_ -> $lookup{$_}\n"; }
	    confess "Error: the database is missing process information for the following process id numbers - ";
	    
	}
	
	return \@atProcessList;
    }
    

    #################################################################
    ## Method to print debug messages with a tab for every level of
    ## depth.
    #################################################################
    sub _debug {
	my ($message,$depth) = @_;
	return if !$VERBOSE;
	print "\t" x $depth; 
	print "**** $message\n";
    }

    
    #################################################################
    ## Method to refresh the data held in this object. Builds a new
    ## Process object hierachy tree. Accepts a new topLevelProcessId
    ## number to populate from. This method should make any
    ## AtomicProcessEngine objects re-usable.
    #################################################################
    sub refresh {
	my $self = shift;

	## Catch the top level process id currently being used.
	my $topLevelProcessId = $object{ $self }->{topLevelProcessId};

	## Clean the objects attributes.
	foreach my $key (keys(%_attributes)) {
	    $object{ $self }->{$key} = undef;
	}
	$object{ $self }->{subProcessList} = [];

	## Call _init with.
	$self->_init(topLevelProcessId=> @_ ? $_[0] : $topLevelProcessId); 

	$self->_startPopulation();
    }

    
    ################################################################
    # Method that returns the topLevelProcess objects description.
    # This makes an AtomicProcessEngine polymorphic with Process and
    # AtomicProcess objects.
    ################################################################
    sub getDescription {
	my $self = shift;
	return $object{ $self }->{topLevelProcess}->getDescription();
    }


    #################################################################
    ## Method to destroy this object.
    #################################################################
    sub DESTROY {
	my $self = shift;
	delete $object{ $self };
    }
}

1;
