package APE::InputControllerFactory::ConcreteInputController::DefaultExoApeInputController;

use base 'APE::InputControllerFactory::ConcreteInputController::InputController';

$APE::InputControllerFactory::ConcreteInputController::DefaultExoApeInputController::VERSION = '0.8';

use strict;
use warnings;
use Carp;
use SqlEngine2;
use Class::Throwable;

use constant LOGIN    => 'snp';
use constant GT_LOGIN => 'gt_process';

=head1 NAME

APE::InputControllerFactory::ConcreteInputController::DefaultExoApeInputController
- base class of all concrete input controller objects for the ExoSeq
project.


=head1 INHERITANCE

APE::InputControllerFactory::ConcreteInputController::InputController


=head1 SYNOPSIS


=head1 DESCRIPTION

An input controller that know how to execute lookups for ExoSeq
entities within the SNP database.

=head1 CONSTANTS

=over 5

=item TRUE

Boolean true.

=cut

use constant TRUE  => 1;

=item FALSE

Boolean false.

=cut

use constant FALSE => 0;

=back

Indexes into the arrays returned by rowcol_map lookup methods:

getRowColMapFromCoord()
getRowColMapFromFormat()

=over 5

=cut

use vars qw/$ID_MAP $ROW $COL $FORMAT/;

=item ID_MAP

The rowcol_map.id_map column.

=cut

$ID_MAP = 0;

=item ROW

The rowcol_map.row column.

=cut

$ROW = 1;

=item COL

The rowcol_map.col column.

=cut

$COL = 2;

=item FORMAT

The rowcol_map.format column.

=cut

$FORMAT = 3;

{

=back

=head1 CONSTRUCTOR

=over 5

=item $ic = APE::InputControllerFactory::ConcreteInputController::DefaultExoApeInputController->new(option => value,... ?)

Use this syntax to create a new DefaultExoApeInputController
instance. The following arguments may be passed to the constructor.

=over 5

=item atomicProcessContainer

Required. An APE::AtomicProcessContainer object that represents the
arraying process that this InputController should create and manage
Requirements for.

=item parentProcessId

Required. The process id of the parent process. Normally found by
calling the APE::AtomicProcessContainer::getParentProcessId() method.

=item rules

Required. A hash of rules to be executed after the
createRequirements() method has been called. See the
APE::InputControllerFactory::ConcreteInputController::InputController::execRules()
method. If no rules are being used pass this argument an empty hash
reference.

=back

=back

=cut

    my %_attributes = (sqlEngineObj => undef);

    my %object;

=head2 INSTANTIATION PROCESS

The new method for this object calls the _init() method on $self. If
$self is a derived class then it's _init() method will be called
giving it the chance to remove any arguments it requires from the
%args hash. If there is no _init() method in the derived class then
the _init() method in this class is called.

Derived classes implementing a _init() method should return the %args
hash to the base class:

package Derived::Class;

sub _init {
   my ($self,%args) = @_;
   my $argument = delete $args{classArgument};
   return $self->SUPER::_init(%args);
}


=head1 METHODS

=over 10

=item $ic->_init(%args);

Private method. This method is called by the new method during object
instantiation (see the INSTANTIATION process). The method removes any
of the arguments required by the object from the %args hash and
returns it.

See the INSTANTIATION section for information on how to use this
method in a derived class.

This method creates a new SqlEngine2 object that can be retrieved by
calling this objects getSqlEngineObj() method.

=cut

    sub _init {
        my ($self,%args) = @_;

        foreach my $attName (keys(%args)) {
            if(exists($_attributes{$attName})) {
                $object{ $self }->{$attName} = delete $args{$attName};
            }
        }

        ## Attribute sqlEngineObj must be set. Create a new object if
        ## one isn't passed.
        $object{ $self }->{sqlEngineObj} = new SqlEngine2()
          unless $object{ $self }->{sqlEngineObj};

        $args{description} = "A concrete InputController that provides ".
          "the default functionality for the ExoApe application."
          unless $args{description};

        ## Preload the SQL lib now. This saves time on lookups.
        ($self->getSqlEngineObj())->loadSqlLib
          (["APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController"]);

        return $self->SUPER::_init(%args);
    }

=item $ic->getSqlEngineObj()

Returns this objects SqlEngine2 object for interaction with a
database.

=cut

    foreach my $attName (keys(%_attributes)) {
        no strict 'refs';
        *{__PACKAGE__ . "::get\u$attName"} = sub { $object{ $_[0] }->{$attName} }
    }

=item $ic->process(APE:InputControllerFactory::ConcreteInputController::Requirement)

Processes the passed Requirement object that should have already been
satisfied using its setData() method.

Calls the getData() method, passing in the Requirement object.

If the Requirement::getType() method returns 'Input' then this method
returns before processing begins.

=cut

    sub process {
        my ($self,$requirementObj) = @_;
        return if $requirementObj->getType eq 'Input';
        warn "*** Using base objects process method: ".$requirementObj->getData. "\n";
        $self->getData($requirementObj);
    }

=item $ic->getData(APE::InputControllerFactory::ConcreteInputController::Requirement)

Performs entity type lookups to retrieve data to be used in the
arraying process.

An entities type is defined by its prefix. The prefix of an entity set
within a Requirement is found using the following code:

my ($prefix,$id) = $requirement->getData() =~ /^(\w\w)(\d+)$/;

The lookup SQL statements are held within the
DefaultExoApeInputController.sql_lib library file. Each statement is
named [prefix]_lookup so that a PCR plate with prefix 'PC' with have
an associated lookup SQL statement called 'PC_lookup' in the library
file.

The actual execution of the SQL statement is delegated to the
executeLookup() method.

Special entity types, such as EP and DN are dealt with here. EP
oligoplates may be one of three types (sense, antisense or mix) so
this method finds which type the EP is and executes the correct lookup
for the type. DN entity wells may contain DNA tubes that contain
individuals DNA or they may contain individuals DNA directly, this
method executes two SQL statements for these scenarios. If the first
statement returns data then that data is used, otherwise the second
statement is executed.

An exception is thrown if no data is found after executing the
entities type lookup. It also throws an exception if the number of
rows found by the lookup does not equal the format of the arraying
processes in entity (APE::AtomicProcess::getInFormat() method).

EP entities of type 'mix' contain two oligos per well (sense and
antisense), this method doubles the in entity format returned by the
APE::AtomicProcess::getInFormat() method and checks that against the
data returned by the lookup.

The lookup data is stored in the Requirement object using its
setDataStore() method.

The Requirement object is returned.

=cut

    sub getData {
        my ($self,$requirementObj) = @_;
        ## Split the data. Expects a plate name i.e. EP1234.
        my ($prefix,$id) = $requirementObj->getData() =~ /^(\w\w)(\d+)$/;
        my $sqlName      = $requirementObj->getType() . '_lookup';

        ## Get a list of the attached AtomicProcess objects. Find the plate format
        ## to verify that the lookup data is consistent.
        my $attachedList  = $requirementObj->getAttachedList;
        my $atomicProcess = $attachedList->[0];
        my $inFormat      = $atomicProcess->getInFormat;

        my ($lookupData,$columnNames,$plateType,$plateDesc);

        eval {
            my $inType = $requirementObj->getType();
            ## Deal with horrid EP plates that can be one of three types -
            ## sense, antisense or mix. These types are defined in SNP and
            ## GTR with different id numbers - arrgh!
            if($inType eq 'EP') {
                warn "********* Found EP in DefaultExoApeInputController getData method ************\n";
                unless(($plateType,$plateDesc) = getType($atomicProcess->getInEntityTypeId())) {
                    die "Error: can't find plate type and description ".
                      "using in entity type id: ".
                      $atomicProcess->getInEntityTypeId();
                }
                warn "********* Found plate type '".join(",",@$plateType).
                  "' and plate desc '$plateDesc' for EP '$id' ***********\n";
                $sqlName = 'EPSA_lookup';
                foreach my $type (@$plateType) {
                    ## Mix plates have two things per well (sense and
                    ## antisense).
                    $inFormat *= 2 if $type == 3;
                    ## Execute the SQL.
                    ($lookupData,$columnNames) = $self->executeLookup
                      ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::". $sqlName,
                        [$id,$type]);
                    if(defined $lookupData && scalar(@$lookupData)) {
                        print STDERR "**** PLATE TYPE = $type ****\n";
                        last;
                    }
                }
            }

            elsif($inType eq 'DN') {
                print "******* Executing DN lookups *******\n";
                ## Some DN plates contain DNA tubes, others contain
                ## individuals. Execute each SQL statement and see
                ## which one returns data.
                foreach my $lsqlName ($sqlName,"DN_IND_lookup") {
                    ($lookupData,$columnNames) = $self->executeLookup
                      ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::".$lsqlName,
                        [$id]);
                    if(scalar @$lookupData) {
                        warn "DN lookup for in entity id '$id' completed using '$lsqlName' SQL statement";
                        last;
                    }
                }
            }

            ## Everything other than EP and DN plates gets dealt with
            ## here. The SQL statement name has already been assigned
            ## and is relevant to the plate type i.e. DNA plate
            ## lookups are DN_lookup.
            else {
                ## Execute the SQL.
                ($lookupData,$columnNames) = $self->executeLookup
                  ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::". $sqlName,
                    [$id]);
            }
          };

        ## Problems occurred when attempting to execute the SQL
        ## statement. This is most likely to be due to the plate type
        ## not being recognised i.e. no SQL statement defined for the
        ## prefix.
        if($@) {
            die "ERROR: can't execute sql '$sqlName' - I probably " .
              "don't know how to deal with a '$prefix' - you should " .
              "attach a specialised InputController: $@";
        }

        ## The SQL statement executed successfully but no data was
        ## found for this plate. Report the error.
        if(scalar(@{$lookupData})==0) {
            die "Error: nothing found using '$prefix$id'";
        }

        ## Check that the number of rows returned equals the
        ## AtomicProcess inFormat attribute.
        unless(scalar(@{$lookupData}) == $inFormat) {
            die "Error: '$prefix$id' appears to be a partial plate - ".
              "required format is '$inFormat' " .
              "format found is '".scalar(@{$lookupData})."'";
        }

        ## Set the lookup data in the Requirement.
        $requirementObj->setDataStore($lookupData);
        return $requirementObj;
    }

=item $ic->executeLookup($queryName,$args)

Executes SQL statement $queryName using the SqlEngine2 object passing
it $args, which should be an array reference. Completes an addition
lookup to find the columns returned by $queryName SQL statement.

Returns $lookupData which is an array ref of array refs containing
data from the lookup. $columnNames an array reference of the column
names returned by the statement.

=cut

    sub executeLookup {
        my ($self,$queryName,$args) = @_;
        my $sqlEngine  = $self->getSqlEngineObj();
        my $dbh        = SqlEngine2::getDbh(LOGIN);
        my $lookupData = $sqlEngine
          ->getAll($queryName,$args,$dbh);
        my $columnNames = $sqlEngine->
          getSthInfo($queryName,"NAME_lc",$dbh,1);
        return($lookupData,$columnNames);
    }

=item getType($entityTypeId)

Static method to find the SNP id and description of an EP plate.  This
method returns two things:

$plateType - array ref containing valid plate types to use in lookups
             in the SNP database.

$plateDesc - a description of the plate type.

The $entityTypeId is a number that represents an entity type. In the
case of ExoApe this number is from the entity_type.id_entity_type@gtr
database table. This number can be found by calling the
APE::AtomicProcess::getInEntityTypeId() or
APE::AtomicProcess::getOutEntityTypeId() methods. These objects can be
automatically populated using the APE::AtomicProcessEngine class.

=cut

    sub getType {
        my $entityTypeId = shift;
        croak "Error: this method is static." if ref($entityTypeId);
        croak "Error: method requires an entity type id" unless $entityTypeId;
        my ($plateType,$plateDesc);
        SWITCH: for($entityTypeId) {
            /20/ && do { $plateType = [1,10]; $plateDesc = "sense";     last; }; ## 10
            /21/ && do { $plateType = [2,11]; $plateDesc = "antisense"; last; }; ## 11
            /22/ && do { $plateType = [3,13]; $plateDesc = "mixed";     last; }; ## 13
            $plateType = undef;
        }
        return($plateType,$plateDesc);
    }

=item $ic->getSessionId()

Retrieves a new session id number from the SNP database and returns
it.

=cut

    sub getSessionId {
        my $self = shift;
        my $sessionId =
          eval { $self->getSqlEngineObj()->getRow
              ('ExoSeq::next_session',
                [],SqlEngine2::getDbh(LOGIN)); };
        if($@) { croak $@;          }
        else   { return $sessionId; }
    }

=item $ic->getRowColMapFromCoord($coord,$pformat)

Retrieves the id_map number (and associated information) associated
with $coord in a plate of format $pformat. The $coord cordinate is in
the form of [row_letter][column_number], for example 'a1' or 'b4'. The
$pformat refers to the format of the plate. This is required to
isolate the unique id_map for the specific plate format i.e. $coord
'a1' will have a different id_map number on entities of $pformat 96
and 384 wells.

The lookup information generated by this method is always cached, so
that subsequent calls using the same $coord and $pformat will not
required a lookup.

Returns an anonymous array:

 0 - id_map (constant: ID_MAP)
 1 - row    (constant: ROW)
 2 - col    (constant: COL)
 3 - format (constant: FORMAT)

See CONSTANTS section for information on the above constant indexes
into the returned array.

=cut

    sub getRowColMapFromCoord {
        my ($self,$coord,$pformat) = @_;
        croak "Error: method requires a valid coordinate and format"
          unless $coord && $pformat;
        ## Return cached results.
        return $object{ $self }->{_rowColMapCoord}{$pformat}{$coord}
          if exists $object{ $self }->{_rowColMapCoord}{$pformat}{$coord};
        my $sqlEngine = $self->getSqlEngineObj;
        croak "Error: failed to get SqlEngine2 object" unless $sqlEngine;
        my $data = eval {
            $sqlEngine->
              getAll("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::".
                  "RowColMapCoord",[$coord,$pformat],
                SqlEngine2::getDbh(LOGIN)); };
        if($@ || !$data) {
            croak "ERROR: can't get rowcol_map lookup data using - ".
              "'$coord' '$pformat': $@\n";
        }
        my $idMap  = $data->[0][$ID_MAP];
        my $row    = $data->[0][$ROW];
        my $col    = $data->[0][$COL];
        my $format = $data->[0][$FORMAT];
        ## Cache the row.
        $object{ $self }->{_rowColMapCoord}{$format}{$coord} = [$idMap,$row,$col,$format];
        return $object{ $self }->{_rowColMapCoord}{$format}{$coord};
    }

=item $ic->getRowColMapFromFormat($format)

Retrieves the id_map numbers (and associated information) associated
with a plate of format $format from the database.

Returns an anonymous hash that allows access using an id_map,
coordinate or format:

 my $rcm = getRowColMapFromFormat(96);

 $rcm->{idMap}{1};    # Array ref retrieved using id_map key.
 $rcm->{coord}{'a1'}; # Array ref retrieved using coordinate key.
 $rcm->{format};      # Array ref of all well map data.

=cut

    sub getRowColMapFromFormat {
        my ($self,$format) = @_;
        croak "Error: method required a valid format" unless $format;
        my $sqlEngine = $self->getSqlEngineObj;
        croak "Error: failed to get SqlEngine2 object" unless $sqlEngine;
        ## Return cached results.
        return $object{ $self }->{_rowColMapFormat}{$format}
          if exists $object{ $self }->{_rowColMapFormat}{$format};
        my $data =
          $sqlEngine->getAll
          ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::".
              "RowColMapFormat",[$format],
            SqlEngine2::getDbh(LOGIN));
        croak "Error: failed to find row col map information for format '$format' - ".
          "check the format exists" if scalar(@$data) == 0;
        foreach(@$data) {
            my $idMap  = $_->[$ID_MAP];
            my $mapRow = $_->[$ROW];
            my $mapCol = $_->[$COL];
            my $format = $_->[$FORMAT];
            $object{ $self }->{_rowColMapFormat}{$format}{idMap}{$idMap} = $_;
            $object{ $self }->{_rowColMapFormat}{$format}{coord}{$mapRow.$mapCol} = $_;
            push(@{$object{ $self }->{_rowColMapFormat}{$format}{format}},$_);
        }
        return $object{ $self }->{_rowColMapFormat}{$format};
    }

=item $ic->getChildren($prefix,$id)

Returns a list of entities that where created using the passed entity
(prefix,id). This method completes a lookup on the exo_process table
and returns the results in an array of arrays with each inner array
representing a child entity.

 [ [id_process, child_prefix, child_id],[id_process, child_prefix, child_id] ]

The above example contains two children of the passed entity.

=cut

    sub getChildren {
        my ($self,$prefix,$id) = @_;
        my $sqlEngine = $self->getSqlEngineObj;
        my $children  =
          $sqlEngine->getAll
          ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::" .
              "GetChildren",[$prefix,$id],
            SqlEngine2::getDbh(LOGIN));
        return $children;
    }

=item $ic->getParents($prefix,$id)

Return a list of entities that where used to create the passed entity
(prefix,id). This method completes a lookup on the exo_process table
and returns the results in an array of arrays with each inner array
representing a parent entity.

[ [id_process, parent_prefix, parent_id],[id_process, parent_prefix, parent_id] ]

The above example contains two parent entities that where used to
create the passed entity.

=cut

    sub getParents {
        my ($self,$prefix,$id) = @_;
        my $sqlEngine = $self->getSqlEngineObj;
        my $parents  =
          $sqlEngine->getAll
          ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::" .
              "GetParents",[$prefix,$id],
            SqlEngine2::getDbh(LOGIN));
        return $parents;
    }

=item getTractEntity(SqlEngine2,$prefix,$id)

Static method that returns the id_process and description of the tract
that the passed entity (depicted by $prefix and $id) was created
under.

Performs a lookup to find the id_process that was used to create the
passed entity. Then calls the getTractProcess() method to find the
root process for the id_process of the entity.

Returns an anonymous array:

 0 - id_process of entity root process
 1 - description of entity root process

=cut

    sub getTractEntity {
        my ($sqlEngine,$prefix,$id) = @_;
        my $processId = $sqlEngine->getRow
          ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::" .
              "FindEntityProcessId",
            [$prefix,$id],SqlEngine2::getDbh(GT_LOGIN));
        return getTractProcess($sqlEngine,$processId);
    }

=item getTractProcess(SqlEngine2,$processId)

Static method that returns details about the root process for the
passed id_process.

Returns an anonymous array:

 0 - root id_process of passed id_process
 1 - description of root process

=cut

    sub getTractProcess {
        my ($sqlEngine,$processId) = @_;
        my $tract = $sqlEngine->getAll
          ("APE/InputControllerFactory/ConcreteInputController/DefaultExoApeInputController::" .
              "FindRootTractProcess",
            [$processId],
            SqlEngine::getDbh(LOGIN));
        return $tract;
    }

=item DESTROY

Method to destroy this object. Make sure you call this method if you
derive from it.

=cut

    sub DESTROY {
        my $self = shift;
        delete $object{ $self };
    }
}

=back

=head1 ALSO SEE

APE::InputControllerFactory::ConcreteInputController::InputController,
APE::InputControllerFactory::ConcreteInputController::Requirement,
APE::AtomicProcessEngine

=head1 AUTHOR

Simon Holroyd <holroyd@sanger.ac.uk>

=cut

1;
