package Bio::PrimerDesigner::primer3;

# $Id: primer3.pm 9 2008-11-06 22:48:20Z kyclark $

=head1 NAME 

Bio::PrimerDesigner::primer3 - An class for accessing primer3

=head1 SYNOPSIS

  use Bio::PrimerDesigner::primer3;

=head1 METHODS

Methods are called using the simplifed alias for each primer3 result
or the raw primer3 BoulderIO key.  Use the raw_output method to view the
raw output.

=cut

use strict;
use warnings;
use File::Spec::Functions 'catfile';
use File::Temp 'tempfile';
use Bio::PrimerDesigner::Remote;
use Bio::PrimerDesigner::Result;
use Readonly;

Readonly our 
    $VERSION => sprintf "%s", q$Revision: 24 $ =~ /(\d+)/;

use base 'Class::Base';


# -------------------------------------------------------------------
sub binary_name {

=pod

=head2 binary_name

Defines the binary's name on the system.

=cut

    my $self = shift;
    return 'primer3';
}

# -------------------------------------------------------------------
sub check_params {

=pod

=head2 check_params

Make sure we have required primer3 arguments.

=cut

    my ( $self, $args ) = @_;

    if ( 
        defined $args->{'PRIMER_LEFT_INPUT'} && 
        defined $args->{'PRIMER_RIGHT_INPUT'} 
    ) {
        return $self->error(
            "Number of sets must be set to 1 for defined primers"
        ) if $args->{'PRIMER_NUM_RETURN'} > 1;

        return $self->error("Sequence input is missing")
            unless defined $args->{'SEQUENCE'};
    }
    else {
        return $self->error("Required design paramaters are missing") unless
            defined $args->{'PRIMER_SEQUENCE_ID'} &&
            defined $args->{'SEQUENCE'} &&
            defined $args->{'PRIMER_PRODUCT_SIZE_RANGE'};
    } 

    return 1;
}

# -------------------------------------------------------------------
sub design {

=pod

=head2 design

Build the primer3 config file, run primer3, then parse the results.
Expects to be passed a hash of primer3 input options.  Returns an object
that can be used to call result methods.

=cut

    my ( $self, $method, $loc, $args ) = @_;

    return $self->error('No arguments for design method' ) unless $args;
    
    #
    # Unalias incoming parameters if required.
    #
    my %aliases = $self->list_aliases;
    my %lookup  = reverse %aliases;
    while ( my ( $k, $v ) = each %$args ) {
        next if exists $aliases{ $k };
        my $alias = $lookup{ $k } or return $self->error("No alias for '$k'");
        delete $args->{ $k };
        $args->{ $alias } = $v;
    }

    #
    # Check that everything required is present.
    #
    $self->check_params( $args ) or return $self->error;

    #
    # Send request to designer.
    #
    my @data = $self->request( $method, $loc, $args );
    
    #
    # abort on empty or undefined data array
    #
    return '' unless @data && @data > 1; 

    my $output = '';
    my $count  = 1;
    my $result = Bio::PrimerDesigner::Result->new;

    for ( @data ) {
        $output .= $_;# unless /^SEQUENCE=/;

        # save raw output into the results hash
        my ($key, $value) = /(.+)=(.+)/;
        $result->{$count}->{$key} = $value if $key && $value;

        # save aliased output
        $result->{$count}->{'qual'}       = $1 
	    if /R_PAIR_PENALT\S+=(\S+)/ || /R_PAIR_QUAL\S+=(\S+)/;
        $result->{$count}->{'left'}       = $1 
	    if /R_LEFT\S+SEQUENC\S+=(\S+)/;
        $result->{$count}->{'right'}      = $1 
	    if /R_RIGHT\S+SEQUENC\S+=(\S+)/;
        $result->{$count}->{'startleft'}  = $1 
	    if /R_LEFT_?\d*=(\d+),\d+/;
        $result->{$count}->{'startright'} = $1 
	    if /R_RIGHT_?\d*=(\d+),\d+/;
        $result->{$count}->{'lqual'}      = $1 
	    if /R_LEFT\S*_PENALT\S+=(\S+)/ || /R_LEFT\S*_QUAL\S+=(\S+)/;
        $result->{$count}->{'rqual'}      = $1 
	    if /R_RIGHT\S*_PENALT\S+=(\S+)/ || /R_RIGHT\S*_QUAL\S+=(\S+)/;
        $result->{$count}->{'leftgc'}     = int $1
            if /R_LEFT\S+GC_PERCEN\S+=(\S+)/;
        $result->{$count}->{'rightgc'}    = int $1
            if /R_RIGHT\S+GC_PERCEN\S+=(\S+)/;
        $result->{$count}->{'lselfany'}   = int $1
            if /R_LEFT\S+SELF_AN\S+=(\S+)/;
        $result->{$count}->{'rselfany'}   = int $1
            if /R_RIGHT\S+SELF_AN\S+=(\S+)/;
        $result->{$count}->{'lselfend'}   = int $1
            if /R_LEFT\S+SELF_EN\S+=(\S+)/;
        $result->{$count}->{'rselfend'}   = int $1
            if /R_RIGHT\S+SELF_EN\S+=(\S+)/;
        $result->{$count}->{'lendstab'}   = int $1
            if /R_LEFT\S+END_STABILIT\S+=(\S+)/;
        $result->{$count}->{'rendstab'}   = int $1
            if /R_RIGHT\S+END_STABILIT\S+=(\S+)/;
        $result->{$count}->{'pairendcomp'}= int $1
            if /R_PAIR\S+COMPL_EN\S+=(\S+)/;
        $result->{$count}->{'pairanycomp'}= int $1
            if /R_PAIR\S+COMPL_AN\S+=(\S+)/;
        $result->{$count}->{'hyb_oligo'}= lc $1
        if /PRIMER_INTERNAL_OLIGO_SEQUENC\S+=(\S+)/;
    
        #
        # round up Primer Tm's
        #
        $result->{$count}->{'hyb_tm'}= int (0.5 + $1)
            if /PRIMER_INTERNAL_OLIGO\S+TM=(\d+)/;  

        $result->{$count}->{'tmleft'}  = int (0.5 + $1)
            if (/^PRIMER_LEFT.*_TM=(\S+)/);

        $result->{$count}->{'tmright'} = int (0.5 + $1)
            if (/^PRIMER_RIGHT.*_TM=(\S+)/);

        #
        # product size key means that we are at the end of each primer set
        #
        $result->{$count}->{'prod'} = $1 and $count++
          if /^PRIMER_PRODUCT_SIZ\S+=(\S+)/ && !/RANGE/;
        
        #
        # abort if we encounter a primer3 error message
        #
        if (/PRIMER_ERROR/) {
            $self->error("Some sort of primer3 error:\n$output");
            return '';
        }
    }

    #
    # save the raw primer3 output (except for input sequence -- too big)
    #
    $result->{1}->{'raw_output'} = $output;
    
    return $result;
}


# -------------------------------------------------------------------
sub request {

=pod

=head2 request

Figures out where the primer3 binary resides and accesses it with a
list of parameters for designing primers.

=cut

    my ( $self, $method, $loc, $args ) = @_;
    $method ||= 'remote';

    my $config = '';
    while ( my ( $key, $value ) = each %$args ) {
        $config .= "$key=$value\n";
    }

    my @data = ();

    if ( $method eq 'remote' ) {
        my $cgi  = Bio::PrimerDesigner::Remote->new
            or return $self->error('could not make remote object');
        my $url  = $loc;
        $args->{'program'} = $self->binary_name;
        @data   = $cgi->CGI_request( $url, $args );
    }
    else { # "local"
        my $path        =  $loc;
        my $binary_name =  $self->binary_name or return;
        my $binary_path =  catfile( $path, $binary_name );
        return $self->error("Can't execute local binary '$binary_path'")
            unless -x $binary_path;

        my ( $tmp_fh, $tmp_file ) = tempfile;
        print $tmp_fh $config, "=\n";
        close $tmp_fh;
        
        # 
        # send the instructions to primer3 and get results
        #
        open RESULT_FILE, "$binary_path < $tmp_file |";
        @data = <RESULT_FILE>;
        close RESULT_FILE;
        unlink $tmp_file;
    }
    
    if ( $self->check_results( $method, @data ) ) {
        return @data;
    }
    else {
        return '';
    }
}     

# -------------------------------------------------------------------
sub check_results {

=pod

=head2 check_results

Verify the validity of the design results.

=cut

    my $self    = shift;
    my $method  = shift;
    my $results = join '', grep {defined} @_;

    my $thing   = $method eq 'remote' ? 'URL' : 'binary'; 
    my $problem = "Possible problem with the primer3 $thing";
    
    if ( $results =~ /SEQUENCE=/m ) {
        return 1;
    }
    else {
        return $self->error("Primer design failure:\n", $problem);
    }
}

# -------------------------------------------------------------------
sub list_aliases {

=pod

=head2 list_aliases

Prints a list of shorthand aliases for the primer3 BoulderIO
input format.  The full input/ouput options and the aliases can be
used interchangeably.

=cut

    my $self = shift;
    
    return (
        PRIMER_SEQUENCE_ID             => 'id',
        SEQUENCE                       => 'seq',
        INCLUDED_REGION                => 'inc',
        TARGET                         => 'target',
        EXCLUDED_REGION                => 'excluded',
        PRIMER_COMMENT                 => 'comment',
        PRIMER_SEQUENCE_QUALITY        => 'quality',
        PRIMER_LEFT_INPUT              => 'leftin',
        PRIMER_RIGHT_INPUT             => 'rightin',
        PRIMER_START_CODON_POSITION    => 'start_cod_pos',
        PRIMER_PICK_ANYWAY             => 'pickanyway',
        PRIMER_MISPRIMING_LIBRARY      => 'misprimelib',
        PRIMER_MAX_MISPRIMING          => 'maxmisprime',
        PRIMER_PAIR_MAX_MISPRIMING     => 'pairmaxmisprime',
        PRIMER_PRODUCT_MAX_TM          => 'prodmaxtm',
        PRIMER_PRODUCT_MIN_TM          => 'prodmintm',
        PRIMER_EXPLAIN_FLAG            => 'explain',
        PRIMER_PRODUCT_SIZE_RANGE      => 'sizerange',
        PRIMER_GC_CLAMP                => 'gcclamp',
        PRIMER_OPT_SIZE                => 'optpsize',
        PRIMER_INTERNAL_OLIGO_OPT_SIZE => 'hyb_opt_size',
        PRIMER_MIN_SIZE                => 'minpsize',
        PRIMER_MAX_SIZE                => 'maxpsize',
        PRIMER_OPT_TM                  => 'opttm',
        PRIMER_MIN_TM                  => 'mintm',
        PRIMER_MAX_TM                  => 'maxtm',
        PRIMER_MAX_DIFF_TM             => 'maxtmdiff',
        PRIMER_MIN_GC                  => 'mingc',
        PRIMER_OPT_GC_PERCENT          => 'optgc',
        PRIMER_MAX_GC                  => 'maxgc',
        PRIMER_SALT_CONC               => 'saltconc',
        PRIMER_DNA_CONC                => 'dnaconc',
        PRIMER_NUM_NS_ACCEPTED         => 'maxN',
        PRIMER_SELF_ANY                => 'selfany',
        PRIMER_SELF_END                => 'selfend',
        PRIMER_DEFAULT_PRODUCT         => 'sizerangelist',
        PRIMER_MAX_POLY_X              => 'maxpolyX',
        PRIMER_LIBERAL_BASE            => 'liberal',
        PRIMER_NUM_RETURN              => 'num',
        PRIMER_FIRST_BASE_INDEX        => '1stbaseindex',
        PRIMER_MAX_END_STABILITY       => 'maxendstab',
        PRIMER_PRODUCT_OPT_TM          => 'optprodtm',
        PRIMER_PRODUCT_OPT_SIZE        => 'optprodsize',
        PRIMER_WT_TM_GT                => 'wt_tm_gt',
        PRIMER_WT_TM_LT                => 'wt_tm_lt',
        PRIMER_WT_SIZE_LT              => 'wt_size_lt',
        PRIMER_WT_SIZE_GT              => 'wt_size_gt',
        PRIMER_WT_GC_PERCENT_LT        => 'wt_gc_lt',
        PRIMER_WT_GC_PERCENT_GT        => 'wt_gc_gt',
        PRIMER_WT_COMPL_ANY            => 'wt_comp_any',
        PRIMER_WT_COMPL_END            => 'wt_comp_end',
        PRIMER_WT_NUM_NS               => 'wt_numN',
        PRIMER_WT_REP_SIM              => 'wt_rep_sim',
        PRIMER_WT_SEQ_QUAL             => 'wt_seq_qual',
        PRIMER_WT_END_QUAL             => 'wt_end_qual',
        PRIMER_WT_END_STABILITY        => 'wt_end_stab',
        PRIMER_PAIR_WT_PR_PENALTY      => 'wt_pr_penalty',
        PRIMER_PAIR_WT_DIFF_TM         => 'wt_pr_tmdiff',
        PRIMER_PAIR_WT_COMPL_ANY       => 'wt_pr_comp_any',
        PRIMER_PAIR_WT_COMPL_END       => 'wt_pr_comp_end',
        PRIMER_PAIR_WT_PRODUCT_TM_LT   => 'wt_prodtm_lt',
        PRIMER_PAIR_WT_PRODUCT_TM_GT   => 'wt_prodtm_gt',
        PRIMER_PAIR_WT_PRODUCT_SIZE_GT => 'wt_prodsize_gt',
        PRIMER_PAIR_WT_PRODUCT_SIZE_LT => 'wt_prodsize_lt',
        PRIMER_PAIR_WT_REP_SIM         => 'wt_repsim',
        PRIMER_PICK_INTERNAL_OLIGO     => 'hyb_oligo',
        PRIMER_LEFT_EXPLAIN            => 'left_explain',
        PRIMER_RIGHT_EXPLAIN           => 'right_explain',
        PRIMER_PAIR_EXPLAIN            => 'pair_explain',
        PRIMER_INTERNAL_OLIGO_EXPLAIN  => 'hyb_explain',
        PRIMER_INTERNAL_OLIGO_MIN_SIZE => 'hyb_min_size',
        PRIMER_INTERNAL_OLIGO_MAX_SIZE => 'hyb_max_size',
    );
}

# -------------------------------------------------------------------
sub list_params {

=pod

=head2 list_params

Returns a list of primer3 configuration options.  primer3 will use
reasonable default options for most parameters.

=cut


    my $self = shift;

    return (
        'PRIMER_SEQUENCE_ID (string, optional)',
        'SEQUENCE (nucleotide sequence, REQUIRED)',
        'INCLUDED_REGION (interval, optional)',
        'TARGET (interval list, default empty)',
        'EXCLUDED_REGION (interval list, default empty)',
        'PRIMER_COMMENT (string, optional)',
        'PRIMER_SEQUENCE_QUALITY (quality list, default empty)',
        'PRIMER_LEFT_INPUT (nucleotide sequence, default empty)',
        'PRIMER_RIGHT_INPUT (nucleotide sequence, default empty)',
        'PRIMER_START_CODON_POSITION (int, default -1000000)',
        'PRIMER_PICK_ANYWAY (boolean, default 0)',
        'PRIMER_MISPRIMING_LIBRARY (string, optional)',
        'PRIMER_MAX_MISPRIMING (decimal,9999.99, default 12.00)',
        'PRIMER_PAIR_MAX_MISPRIMING (decimal,9999.99, default 24.00)',
        'PRIMER_PRODUCT_MAX_TM (float, default 1000000.0)',
        'PRIMER_PRODUCT_MIN_TM (float, default -1000000.0)',
        'PRIMER_EXPLAIN_FLAG (boolean, default 0)',
        'PRIMER_PRODUCT_SIZE_RANGE (size range list, default 100-300)',
        'PRIMER_GC_CLAMP (int, default 0)',
        'PRIMER_OPT_SIZE (int, default 20)',
        'PRIMER_MIN_SIZE (int, default 18)',
        'PRIMER_MAX_SIZE (int, default 27)',
        'PRIMER_OPT_TM (float, default 60.0C)',
        'PRIMER_MIN_TM (float, default 57.0C)',
        'PRIMER_MAX_TM (float, default 63.0C)',
        'PRIMER_MAX_DIFF_TM (float, default 100.0C)',
        'PRIMER_MIN_GC (float, default 20.0%)',
        'PRIMER_OPT_GC_PERCENT (float, default 50.0%)',
        'PRIMER_MAX_GC (float, default 80.0%)',
        'PRIMER_SALT_CONC (float, default 50.0 mM)',
        'PRIMER_DNA_CONC (float, default 50.0 nM)',
        'PRIMER_NUM_NS_ACCEPTED (int, default 0)',
        'PRIMER_SELF_ANY (decimal,9999.99, default 8.00)',
        'PRIMER_SELF_END (decimal 9999.99, default 3.00)',
        'PRIMER_DEFAULT_PRODUCT (size range list, default 100-300)',
        'PRIMER_MAX_POLY_X (int, default 5)',
        'PRIMER_LIBERAL_BASE (boolean, default 0)',
        'PRIMER_NUM_RETURN (int, default 5)',
        'PRIMER_FIRST_BASE_INDEX (int, default 0)',
        'PRIMER_MAX_END_STABILITY (float 999.9999, default 100.0)',
        'PRIMER_PRODUCT_OPT_TM (float, default 0.0)',
        'PRIMER_PRODUCT_OPT_SIZE (int, default 0)',
        '',
        '** PENALTY WEIGHTS **',
        '',
        'PRIMER_WT_TM_GT (float, default 1.0)',
        'PRIMER_WT_TM_LT (float, default 1.0)',
        'PRIMER_WT_SIZE_LT (float, default 1.0)',
        'PRIMER_WT_SIZE_GT (float, default 1.0)',
        'PRIMER_WT_GC_PERCENT_LT (float, default 1.0)',
        'PRIMER_WT_GC_PERCENT_GT (float, default 1.0)',
        'PRIMER_WT_COMPL_ANY (float, default 0.0)',
        'PRIMER_WT_COMPL_END (float, default 0.0)',
        'PRIMER_WT_NUM_NS (float, default 0.0)',
        'PRIMER_WT_REP_SIM (float, default 0.0)',
        'PRIMER_WT_SEQ_QUAL (float, default 0.0)',
        'PRIMER_WT_END_QUAL (float, default 0.0)',
        'PRIMER_WT_END_STABILITY (float, default 0.0)',
        'PRIMER_PAIR_WT_PR_PENALTY (float, default 1.0)',
        'PRIMER_PAIR_WT_DIFF_TM (float, default 0.0)',
        'PRIMER_PAIR_WT_COMPL_ANY (float, default 0.0)',
        'PRIMER_PAIR_WT_COMPL_END (float, default 0.0)',
        'PRIMER_PAIR_WT_PRODUCT_TM_LT (float, default 0.0)',
        'PRIMER_PAIR_WT_PRODUCT_TM_GT (float, default 0.0)',
        'PRIMER_PAIR_WT_PRODUCT_SIZE_GT (float, default 0.0)',
        'PRIMER_PAIR_WT_PRODUCT_SIZE_LT (float, default 0.0)',
        'PRIMER_PAIR_WT_REP_SIM (float, default 0.0)',
    );
}

# -------------------------------------------------------------------
sub example {

=pod

=head2 example

Runs a sample remote primer design job.  Returns an
Bio::PrimerDesigner::Result object.

=cut

    my $self       = shift;
    my $dna        = $self->_example_dna;
    my $length     =  length $dna;
    my $result     =  $self->design(
        'remote',
        'dev.wormbase.org/db/seq/primer_designer.cgi',
        { 
            num        => 1,
            seq        => $dna,
            sizerange  => '100-200',
            target     => '150,10',
            excluded   => '1,30 400,' . ($length - 401),
            id         => 'test_seq'
        }
    ) or return $self->error("Can't get remote server call to work");
    
    return $result; 
}

# -------------------------------------------------------------------
sub _example_dna {

=pod

=head2 _example_dna

Returns an example DNA sequence.

=cut

  my $self = shift;
  return 'cagagttaaagagaaaactgataattttttttccatctttctcctcacttgtgaataaac' .
         'taaacgcatttctgtggacgttccaagtgtaatatgagagttgttttcatttggaaatgc' .
         'gggaatatattgaatcttccattagatgttcaggaatatataaatacgttgtctgctctg' .
         'aaaattcacacggaaaatctaaaaattgtcaaattatagatttcattctcaaatgactat' .
         'ataacattttatttttgcaatttcttttcaattaggaaacatttcaaaaagctacgttgt' .
         'ttttcacattcaaaatgattactgtcggtgcgttcattttccgagtttttccaatttcac' .
         'gcttgctcttcttcgtaaaaaactcgtaatttagaaattgtgtctagatcaaaaaaaaaa' .
         'ttttctgagcaatcctgaatcaggcatgctctctaaacaactctcagatatctgagatat' .
         'gggaagcaaattttgagaccttactagttataaaaatcattaaaaatcaacgccgacagt' .
         'ttctcacagaaacttaaaccgaaaaatcccaacgaagacttcagctcttttttctttgaa';
}

1;

# -------------------------------------------------------------------

=pod

=head1 AUTHOR

Copyright (C) 2003-2008 Sheldon McKay E<lt>mckays@cshl.eduE<gt>,
Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.

=head1 LICENSE

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 3 or any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA.

=head1 SEE ALSO

Bio::PrimerDesigner::epcr.

=cut
