#!/usr/bin/perl
package PhraseDS;

###########################################################################
#
# Script Name: PhraseDS.pm
# Programmer:  Kane Tse (KT)
# Creation:    Jul/2001
# Rev./Ver.:   Sep/2001 v
#
# Purpose:     This is a perl-object designed to encapsulate all functions
#                related to manipulating phrases within dictyBase
#
# Project:     dictyBase / Summary Paragraphs / Oracle Conversion
#
# Notes:       It is the hope that this module will eliminate the need for
#                users or programmers to use SQL statements to manipulate
#                the multiple tables related to phrases.
#
#>             All accessor methods of this module do not retrieve directly
#>               from the database, but rather from the values associated
#>               with the current instance of PhraseDS.  Thus, if you 
#>               load the phrase from the database, and then subsequently
#>               alter phrase information in this object, then the value
#>               displayed will differ from the value in the database.
#>             Using the update() method will synchronize the changes in this
#>               object into the database.
#
# Dependencies: /usr/local/dicty/www_dictybase/db/lib/common/Login.pm
#               /usr/local/dicty/www_dictybase/db/lib/dictyBase/Objects/Paragraph.pm
#               /usr/local/dicty/www_dictybase/db/lib/dictyBase/Objects/Phrase.pm
#               /usr/local/dicty/www_dictybase/db/lib/dictyBase/Objects/Coll_para.pm
#               /usr/local/dicty/www_dictybase/db/lib/dictyBase/Objects/Locus.pm
#               /usr/local/dicty/www_dictybase/db/lib/dictyBase/ParagraphDS.pm
#
# Assumptions: Assumes the user has some familiarity the way in which Gene
#                Summary Paragraphs are organized in dictyBase.
#
# Other Related Files:
#              Scripts that are known to rely upon this module  --
#               --> Paragraph Curation [#1]
#               --> Paragraph display on the Locus page [#2]
#               --> Paragraph UI Module (used by #1 and #2, above)
#
#              Related scripts
#               --> Table of Gene Summary Paragraphs display
#
###########################################################################
use strict;
use DBI;
use lib "/usr/local/dicty/www_dictybase/db/lib/common";
use Login qw (ConnectToDatabase);
use lib "/usr/local/dicty/www_dictybase/db/lib/dictyBase/Objects";
use Phrase;
use Phrase_link;
use Phrase_category;
use Reflink;
use Reference; # used to validate Reference numbers
use Locus; # used to validate Locus numbers
use Go; # used to validate GO ids

#######################################################################
#################### global variables #################################
#######################################################################

my $dbh;

my %validPhraseCategory; # a list of valid phrase categories in dictyBase
my %goPrimaryID;
my %dictyBaseLoadedGOIDs;
my $schema = "prod";

# my $objectType = 0;
# 0 = normal paragraph (already in DB)
# 1 = new paragraph (not in DB)
# 2 = delete paragraph (already in DB, to be deleted)

#>#####################################################################
#> PhraseDS->new() Constructor
#> FUNCTION:   Creates a new instance of the PhraseDS for a single
#>                phrase.
#> PARAMETERS: All parameters are passed as hash values.
#>             Multiple parameters are accepted.  For example, for 1,
#>               specify either: (1a) or (1b.1 and 1b.2)
#>             1a dbh - A database handle, already connected to the
#>                      database
#>             1b.1 database - string "sdev" or "dictyBase", indicating which 
#>                             database to connect to
#>             1b.2 user - string username of the user wishing to connect
#>                       to the database
#>             2 phrase_no - the PARAGRAPH_NO of the paragraph to be
#>                              retrieved
#> RETURNS:    A PhraseDS object.
#> NOTES:
#######################################################################
sub new {      ############ constructor ###############################
#######################################################################
    my ($self, %args) = @_;

    $self = {};
    bless $self;

    if ($args{'dbh'}) { 
        $dbh = $args{'dbh'};
    }
    elsif ($args{'database'}) {
        $self->{'_database'} = $args{'database'};
        $dbh = &ConnectToDatabase($args{'database'});
    }
    else {
        die "No database name or database handle is passed to this object.";
    }

    if ($args{'phrase_no'}) {
	my $searchTerm = $args{'phrase_no'};
	$self->getPhraseFromDB($searchTerm);
    }
    else {
	# create a new phrase object
	$self->{'objectType'} = 1;

	$self->{'phrase_link'} = {};
	$self->{'ref_link'} = {};
	$self->{'phrase_category'} = {};

	$self->{'oldPhrase_link'} = {};
	$self->{'oldRef_link'} = {};
	$self->{'oldPhrase_category'} = {};
    }

    # load a list of valid phrase_categories from the PHRASE table.
    $self->loadPhraseCategories();

    $self->loadPrimaryGOids();

    return $self;
}

#>#####################################################################
#> getPhraseFromDB()
#> FUNCTION:   Loads a phrase from the database
#> PARAMETERS: $phraseNo - PHRASE_NO of the phrase to retreive from the
#>               database.
#> RETURNS:    1 - always a 1.
#> NOTES:
#######################################################################
sub getPhraseFromDB {
#######################################################################
    my ($self, $phraseNo) = @_;

    # retrieve data from CGM_DDB.PHRASE
    my $phrase_row = new Phrase(dbh=>$dbh, 
				phrase_no=>$phraseNo);

    $self->{'phrase_row'} = $phrase_row;

    $self->{'phrase_no'} = $phrase_row->phrase_no;
    $self->{'phrase_text'} = $phrase_row->phrase_text;
    $self->{'phrase_order'} = $phrase_row->phrase_order;
    $self->{'paragraph_no'} = $phrase_row->paragraph_no;

    # retrieve data from CGM_DDB.PHRASE_LINK
    my %phraseLink;
    my %oldPhraseLink;
    my $phrase_link_ref = $phrase_row->phraseLinkRef();
    foreach my $rowref (@$phrase_link_ref) {
	(undef, my $tab_name, my $primary_key, undef) = @$rowref;
	$phraseLink{"$tab_name:$primary_key"} = $rowref;
	$oldPhraseLink{"$tab_name:$primary_key"} = $rowref;
    }
    $self->{'phrase_link'} = \%phraseLink;
    $self->{'oldPhrase_link'} = \%phraseLink;

    # retrieve data from CGM_DDB.PHRASE_CATEGORY

    my %phrase_category;
    my %oldPhrase_category;
    my $phrase_category_ref = $phrase_row->phraseCategoryRef();
    foreach my $rowref (@$phrase_category_ref) {
	(my $phrase_category, undef) = @$rowref;
	$phrase_category{$phrase_category} = 1;
	$oldPhrase_category{$phrase_category} = 1;
    }
    $self->{'phrase_category'} = \%phrase_category;
    $self->{'oldPhrase_category'} = \%oldPhrase_category;

    # retrieve data from CGM_DDB.REFLINK
    my %ref_link;
    my %oldRef_link;
    my $ref_link_ref = $phrase_row->reflinkRef();
    foreach my $rowref (@$ref_link_ref) {
	(undef, my $reference_no, undef) = @$rowref;

	if (defined $reference_no) {
	    $ref_link{$reference_no} = @$ref_link_ref;
	    $oldRef_link{$reference_no} = @$ref_link_ref;
	}
    }
    $self->{'ref_link'} = \%ref_link;
    $self->{'oldRef_link'} = \%oldRef_link;

    return 1;
} # end new()

#>#####################################################################
#> delete()
#> FUNCTION:   Marks a phrase for deletion.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      For the phrase to actually be deleted from the database,
#>               the update() for this phrase needs to be called.
#######################################################################
sub delete {
#######################################################################
    my ($self) = @_;

    $self->{'objectType'} = 2;
    return;
} # end delete()

#>#####################################################################
#> update()
#> FUNCTION:   This function updates the database with new and updated
#>               phrase values.
#> PARAMETERS: (None)
#> RETURNS:    0 - always 0.
#> NOTES:      This method will call validate() to ensure that the phrase
#>               is valid, before committing it into the database.
#######################################################################
sub update {
#######################################################################
    my ($self) = @_;

    my $phrase_row;

    my $errorMessage = $self->validate();
    if ($errorMessage ne 0) {
	my $phraseNo = "(None)";

	if (defined $self->{'phrase_no'}) {
	    $phraseNo = $self->{'phrase_no'}
	}

	die ("A problem with this phrase (PHRASE_NO = $phraseNo) was "
	     . "encountered.\n"
	     . "$errorMessage"
	     . "The update of PHRASE_NO = $phraseNo has been aborted.\n");
    }

    if ($self->{'objectType'} == 1) {
	Phrase->Insert
	     (dbh=>$dbh,
	      binds=>{phrase_text=>$self->{'phrase_text'},
		      phrase_order=>$self->{'phrase_order'},
		      paragraph_no=>$self->{'paragraph_no'}
		     }
	     );
	my $sthGetNewPhraseNo 
	     = $dbh->prepare(  "SELECT CGM_DDB.phraseno_seq.currval "
				    . "FROM   dual");

	$sthGetNewPhraseNo->execute();

	my $new_phrase_no = $sthGetNewPhraseNo->fetch()->[0];

	$sthGetNewPhraseNo->finish();

	$phrase_row = Phrase->new(dbh=>$dbh,
				  phrase_no=>$new_phrase_no);

	$self->{'phrase_row'} = $phrase_row;
	$self->{'phrase_no'} = $phrase_row->phrase_no;

    }
    $phrase_row = $self->{'phrase_row'};

    if (($self->{'objectType'} == 0) || ($self->{'objectType'} == 1)) {
	# assumes that the current phrase object
	#   already has a phrase_no associated  with it

	# look for changes in CGM_DDB.PHRASE table
	my $updates = 0;

	if ($phrase_row->phrase_text ne $self->{'phrase_text'}) {
	    $phrase_row->updatephrase_text($self->{'phrase_text'});
	    $updates = 1;
	}
	if ($phrase_row->phrase_order ne $self->{'phrase_order'}) {
	    $phrase_row->updatephrase_order($self->{'phrase_order'});
	    $updates = 1;
	}
	if ($phrase_row->paragraph_no ne $self->{'paragraph_no'}) {
	    $phrase_row->updateparagraph_no($self->{'paragraph_no'});
	    $updates = 1;
	}

	if ($updates == 1) {
	    $self->{'phrase_row'}->enterUpdates();
	}

	# look for changes in the CGM_DDB.PHRASE_LINK table
	my $phrase_linkRef = $self->{'phrase_link'};
	my %phrase_link = %$phrase_linkRef;
	my $oldPhrase_linkRef = $self->{'oldPhrase_link'};
	my %oldPhrase_link = %$oldPhrase_linkRef;

	foreach my $phrase_link_key (keys %phrase_link) {
	    if (!defined $oldPhrase_link{$phrase_link_key}) { # new phrase_link
		my $newPhraseLinkRef = $phrase_link{$phrase_link_key};
		my @new_PL = @$newPhraseLinkRef;

		Phrase_link->Insert
		     (dbh=>$dbh,
		      binds=>{phrase_no=>$phrase_row->phrase_no,
			      tab_name=>$new_PL[1],
			      primary_key=>$new_PL[2],
			      primary_key_col=>$new_PL[3]
		      }
		     );
	    }
	}

	foreach my $phrase_link_key (keys %oldPhrase_link) {
	    if (!defined $phrase_link{$phrase_link_key}) { # old phrase_link
		my $oldPhraseLinkRef = $oldPhrase_link{$phrase_link_key};
		my @row = @$oldPhraseLinkRef;
		(my $phrase_link_no, undef) = @row;

		my $phrase_link
		     = Phrase_link->new(dbh=>$dbh,
					phrase_link_no=>$phrase_link_no);
		$phrase_link->delete();
	    }
	}

	# look for changes in CGM_DDB.PHRASE_CATEGORY table
	my $phrase_categoryRef = $self->{'phrase_category'};
	my %phrase_category = %$phrase_categoryRef;
	my $oldPhrase_categoryRef = $self->{'oldPhrase_category'};
	my %oldPhrase_category = %$oldPhrase_categoryRef;

	foreach my $phrase_category (keys %phrase_category) {
	    if (!defined $oldPhrase_category{$phrase_category}) {
		Phrase_category->Insert
		     (dbh=>$dbh,
		      binds=>{phrase_no=>$phrase_row->phrase_no,
			      phrase_category=>$phrase_category})
		 }
	}
	foreach my $phrase_category (keys %oldPhrase_category) {
	    if (!defined $phrase_category{$phrase_category}) {
		my $phrase_category
		     = Phrase_category->new(dbh=>$dbh,
					    phrase_no=>$phrase_row->phrase_no,
					    phrase_category=>$phrase_category);
		$phrase_category->delete();

	    }
	}

	# look for changes in CGM_DDB.REF_LINK table
	my $ref_linkRef = $self->{'ref_link'};
	my %ref_link = %$ref_linkRef;
	my $oldRef_linkRef = $self->{'oldRef_link'};
	my %oldRef_link = %$oldRef_linkRef;

	foreach my $reference_no (keys %ref_link) {
	    if (!defined $oldRef_link{$reference_no}) {

		Reflink->Insert
		     (dbh=>$dbh,
		      literals=>{tab_name=>'\'PHRASE\'',
				 primary_key_col=>'\'PHRASE_NO\''
				},
		      binds=>{reference_no=>$reference_no,
			      primary_key=>$phrase_row->phrase_no
			     }
		     );
	    }
	}
	foreach my $reference_no (keys %oldRef_link) {
	    if (!defined $ref_link{$reference_no}) {
		my $ref_link
		     = Reflink->new(dbh=>$dbh,
				    reference_no=>$reference_no,
				    tab_name=>'PHRASE',
				    primary_key=>$self->{'phrase_row'}->phrase_no,
				    primary_key_col=>'PHRASE_NO'
				   );
		$ref_link->delete();
	    }
	}

	# resynchronize old and new database values
	$self->getPhraseFromDB($self->{'phrase_no'});

    }
    else { # objectType == 2, thus, delete the object.
	# delete related entries in the CGM_DDB.REFLINK to prevent orphans

	my $ref_linkRef = $self->{'ref_link'};
	my %ref_link = %$ref_linkRef;

	foreach my $reference_no (keys %ref_link) {
	    my $ref_link =
		 Reflink->new(dbh=>$dbh,
			      reference_no=>$reference_no,
			      tab_name=>'PHRASE',
			      primary_key=>$self->{'phrase_row'}->phrase_no,
			      primary_key_col=>'PHRASE_NO');
	    $ref_link->delete();
	}

	# orphans in other tables (CGM_DDB.PHRASE_CATEGORY, CGM_DDB.PHRASE_LINK) are
	#   handled by the database when the row in CGM_DDB.PHRASE is deleted
	$phrase_row->delete();
    }
    return 0;
} # end update()

#>#####################################################################
#> getPhraseNo()
#> FUNCTION:   Retrieves the PHRASE_NO of the current phrase
#> PARAMETERS: (None)
#> RETURNS:    int - the PHRASE_NO of the current phrase.
#> NOTES:      
#######################################################################
sub getPhraseNo {
#######################################################################
    my ($self) = @_;

    return $self->{'phrase_no'};
} # end getPhraseNo()

#>#####################################################################
#> setPhraseNo()
#> FUNCTION:   Sets the PHRASE_NO of the current phrase
#> PARAMETERS: (None)
#> RETURNS:    int - the desired PHRASE_NO of the current phrase.
#> NOTES:      USE THIS METHOD WITH EXTREME CAUTION.
#######################################################################
sub setPhraseNo {
#######################################################################
    my ($self, $new_phrase_no) = @_;

    $self->{'phrase_no'} = $new_phrase_no;
    return $self->{'phrase_no'};
}

#>#####################################################################
#> getPhraseOrder()
#> FUNCTION:   Retrieves the PHRASE_ORDER of the current phrase
#> PARAMETERS: (None)
#> RETURNS:    int - the PHRASE_ORDER of the current phrase.
#> NOTES:      Note that phrase order values are zero-indexed
#######################################################################
sub getPhraseOrder {
#######################################################################
    my ($self) = @_;
    return $self->{'phrase_order'};
}

#>#####################################################################
#> setPhraseOrder()
#> FUNCTION:   Sets the PHRASE_ORDER of the current phrase
#> PARAMETERS: $new_phrase_order - the PHRASE_ORDER of the current phrase.
#> RETURNS:    (None)
#> NOTES:      Note that phrase order values are zero-indexed
#######################################################################
sub setPhraseOrder {
#######################################################################
    my ($self, $new_phrase_order) = @_;
    $self->{'phrase_order'} = $new_phrase_order;
    return;
}

#>#####################################################################
#> getPhraseText()
#> FUNCTION:   Sets the PHRASE_TEXT of the current phrase
#> PARAMETERS: (None)
#> RETURNS:    string - the PHRASE_TEXT of the current phrase.
#> NOTES:      
#######################################################################
sub getPhraseText {
#######################################################################
    my ($self) = @_;

    return $self->{'phrase_text'};
}

#>#####################################################################
#> setPhraseText()
#> FUNCTION:   Sets the PHRASE_TEXT of the current phrase
#> PARAMETERS: $new_phrase_text - the desired PHRASE_TEXT of the current
#>                phrase.
#> RETURNS:    (None)
#> NOTES:      
#######################################################################
sub setPhraseText {
#######################################################################
    my ($self, $new_phrase_text) = @_;

    my $old_phrase_text = $self->{'phrase_text'};
    $self->{'phrase_text'} = $new_phrase_text;

    eval {
	{
	    my $oldPCRef = $self->{'phrase_category'};
	    $self->clearAll();
	    $self->{'phrase_category'} = $oldPCRef;
	}

	foreach my $word (split(/ /, $new_phrase_text)) {
	    # (secondary loci) locus name/no
	    if ($word =~ /\<locus02:(\d+)\>/) {
		$self->setSecondaryLoci($1);
	    }

	    # go ids
	    if ($word =~ /\<go:(\d+)\>/) {
		my $goid = $goPrimaryID{$1};

		if (!defined $goid) {
		    $goid = $1;
		}

		$self->setGOTerm($goid);
	    }

	    # references
	    if ($word =~ /\<ref_no:(\d+)\>/) {
		$self->setReference($1);
	    }

	    # pubmed references
	    if ($word =~ /\<ref_pm:(\d+)\>/) {
		$self->setPMReference($1);
	    }

	}
    };
    if ($@) {
	my $errorMessage = $@;
	$self->setPhraseText($old_phrase_text);
	die $errorMessage;
    }

    return;
} # end setPhraseText()

#>#####################################################################
#> getParagraphNo()
#> FUNCTION:   Retrieves the PARAGRAPH_NO of the paragraph to which this
#>               phrase belongs.
#> PARAMETERS: (None)
#> RETURNS:    int - the PARAGRAPH_NO of the associated paragraph.
#> NOTES:      
#######################################################################
sub getParagraphNo {
#######################################################################
    my ($self) = @_;

    return $self->{'paragraph_no'};
} # end setParagraphNo()

#>#####################################################################
#> setParagraphNo()
#> FUNCTION:   Associates the current phrase with a paragraph, as given
#>               by its PARAGRAPH_NO value
#> PARAMETERS: $new_paragraph_no - the PARAGRAPH_NO of the new paragraph
#> RETURNS:    (None)
#> NOTES:      
#######################################################################
sub setParagraphNo {
#######################################################################
    my ($self, $new_paragraph_no) = @_;

    if ($new_paragraph_no !~ /^(\d+)$/) {
	die "Tried to assign a non-numeric value \"$new_paragraph_no\""
	     . " to CGM_DDB.PHRASE.PARAGRAPH_NO.\n";
    }

    $self->{'paragraph_no'} = $new_paragraph_no;
    return;
} # end setParagraphNo

#>#####################################################################
#> getParagraph()
#> FUNCTION:   Retrieves the ParagraphDS object for the paragraph which
#>               is currently assigned to this phrase.
#> PARAMETERS: (None)
#> RETURNS:    ParagraphDS object of the paragraph assigned to this phrase.
#> NOTES:      
#######################################################################
sub getParagraph {
#######################################################################
    my ($self) = @_;

    return ParagraphDS->new($dbh, paragraphNo=>$self->{'paragraph_no'});
} # end getParagraph()

#>#####################################################################
#> getPhraseLinks()
#> FUNCTION:   Retrieves all rows from the PHRASE_LINK table that are
#>               associated with the current phrase.
#> PARAMETERS: (None)
#> RETURNS:    hash ref of PHRASE_LINK rows - key is {$tab_name:$primary_key}
#>               - value of the hash is 
#>                   (PHRASE_LINK_NO, TAB_NAME, PRIMARY_KEY, PRIMARY_KEY_COL)
#> NOTES:      
#######################################################################
sub getPhraseLinks {
#######################################################################
    my ($self) = @_;

    my $phraseLinkRef = $self->{'phrase_link'};

    return $phraseLinkRef;
} # end getPhraseLinks()

#>#####################################################################
#> setPhraseLinks()
#> FUNCTION:   Creates a row in the PHRASE_LINK table to associate another
#>               table with the current PHRASE
#> PARAMETERS: $phraseLinkRow - string formatted as follows:
#>               "$PHRASE_NO:$TAB_NAME:$PRIMARY_KEY:$PRIMARY_KEY_COL"
#>               ... representing the row to be inserted into PHRASE_LINK
#> RETURNS:    (None)
#> NOTES:      As with all other items of this table, nothing will be committed
#>               into the database until the update() method is called.
#######################################################################
sub setPhraseLinks {
#######################################################################
    my ($self, $phraseLinkRow) = @_;
    # $phraseLinkRow should be an array in this format:
    #   'phrase_no:tab_name:primary_key:primary_key_col'

    (undef, my $tab_name, my $primary_key, undef) = @$phraseLinkRow;

    my $phraseLinkRef = $self->{'phrase_link'};
    my %phraseLink = %$phraseLinkRef;

    $phraseLink{"$tab_name:$primary_key"} = $phraseLinkRow;

    $self->{'phrase_link'} = \%phraseLink;

    return;
} # end setPhraseLinks()

#>#####################################################################
#> setPhraseLinks()
#> FUNCTION:   Deletes a row from the PHRASE_LINK table associated
#>               with the current PHRASE
#> PARAMETERS: $phraseLinkRow - string formatted as follows:
#>               "$PHRASE_NO:$TAB_NAME:$PRIMARY_KEY:$PRIMARY_KEY_COL"
#>               ... which should be the exact value to remove from PHRASE_LINK
#> RETURNS:    (None)
#> NOTES:      As with all other items of this table, nothing will be committed
#>               into the database until the update() method is called.
#######################################################################
sub unsetPhraseLink {
#######################################################################
    my ($self, $phraseLinkRow) = @_;
    # $phraseLinkRow should be an array in this format:
    #   'phrase_no:tab_name:primary_key:primary_key_col'

    (undef, my $tab_name, my $primary_key, undef) = @$phraseLinkRow;

    my $phrase_linkRef = $self->{'phrase_link'};
    my %phrase_link = %$phrase_linkRef;

    if (!defined $phrase_link{"$tab_name:$primary_key"}) {
	# phrase_link row to delete/unlink does not exist
    }
    else {
	undef $phrase_link{"$tab_name:$primary_key"};
    }

    $self->{'phrase_link'} = \%phrase_link;
    return;
} # end unsetPhraseLink()

#>#####################################################################
#> getSecondaryLoci()
#> FUNCTION:   Retrieves all rows from the PHRASE_LINK table that are
#>               associate the current phrase with the LOCUS table.
#> PARAMETERS: (None)
#> RETURNS:    array ref - containing LOCUS_NO values of secondary loci.
#> NOTES:      This is a specialized wrapper for getPhraseLink() for 
#>               retrieving secondary loci from that table
#######################################################################
sub getSecondaryLoci {
#######################################################################
    my ($self) = @_;
    my @secondaryLociList;

    my $phrase_linkRef = $self->{'phrase_link'};
    my %phrase_link = %$phrase_linkRef;
    foreach my $key (keys %phrase_link) {
	my ($tab_name, $locus_no) = split(/:/, $key);
	if ($tab_name eq "LOCUS") {
	    push(@secondaryLociList, $locus_no);
	}
    }
    return \@secondaryLociList;
} # end getSecondaryLoci()

#>#####################################################################
#> isSecondaryLoci()
#> FUNCTION:   Determines whether or not a given LOCUS_NO is a secondary
#>               locus for the current phrase.
#> PARAMETERS: $locus_no - the LOCUS_NO value of the potential secondary locus
#> RETURNS:    0 - if $locus_no is not a secondary locus of this phrase
#>             1 - if $locus_no is a secondary locus of this phrase
#> NOTES:      This is a specialized wrapper for getPhraseLink() for 
#>               retrieving secondary loci from that table
#######################################################################
sub isSecondaryLoci {
#######################################################################
    my ($self, $locus_no) = @_;
    my $isASecondaryLoci = 0;

    my $secondaryLociRef = $self->getSecondaryLoci();

    foreach my $curr_locus_no (@$secondaryLociRef) {
	if ($curr_locus_no == $locus_no) {
	    $isASecondaryLoci = 1;
	    last;
	}
    }
    return $isASecondaryLoci;
} # end isSecondaryLoci;

#>#####################################################################
#> setSecondaryLoci()
#> FUNCTION:   Sets a specified LOCUS_NO as a secondary locus for the current
#>               phrase.
#> PARAMETERS: $locus_no - the LOCUS_NO of the secondary locus to be set
#> RETURNS:    (None)
#> NOTES:      This is a specialized wrapper for setPhraseLink() for 
#>               retrieving secondary loci from that table
#>             This method will die with an error if the LOCUS_NO is not
#>               found in the LOCUS table.
#######################################################################
sub setSecondaryLoci {
#######################################################################
    my ($self, $locus_no) = @_;
    if (!defined $self->{'phrase_no'}) {
	$self->{'phrase_no'} = -1;
    }

    my $locusObj = Locus->new(dbh=>$dbh,
			      locus_no=>$locus_no);

    my @locus_record;
    if (defined $locusObj) {
	@locus_record 
	     = ( $self->{'phrase_no'}, 'LOCUS', $locus_no, 'LOCUS_NO' );
    }
    else {
	die "Locus no [$locus_no] was not found in the LOCUS table.\n";
    }
    $self->setPhraseLinks(\@locus_record);
    return;
} # end setSecondaryLoci()

#>#####################################################################
#> unsetSecondaryLoci()
#> FUNCTION:   Removes a specified LOCUS_NO as a secondary locus for the
#>               current phrase.
#> PARAMETERS: $locus_no - the LOCUS_NO of the secondary locus to be set
#> RETURNS:    (None)
#> NOTES:      This is a specialized wrapper for unsetPhraseLink() for 
#>               retrieving secondary loci from that table
#>             This method does not actually check whether or not LOCUS_NO 
#>               is a secondary locus of the current phrase.
#######################################################################
sub unsetSecondaryLoci {
#######################################################################
    my ($self, $locus_no) = @_;
    my @locus_record = ( undef, "LOCUS" , $locus_no, undef );
    $self->unsetPhraseLink(\@locus_record);
    return;
} # end unsetSecondaryLoci()

#>#####################################################################
#> getGOTerms()
#> FUNCTION:   Retrieves all rows from the PHRASE_LINK table that are
#>               associate the current phrase with the GO table.
#> PARAMETERS: (None)
#> RETURNS:    array ref - containing GO_ID values of GO terms.
#> NOTES:      This is a specialized wrapper for getPhraseLink() for 
#>               retrieving GO terms from that table
#######################################################################
sub getGOTerms {
#######################################################################
    my ($self) = @_;
    my @goTermsList;

    my $phrase_linkRef = $self->{'phrase_link'};
    my %phrase_link = %$phrase_linkRef;
    foreach my $key (keys %phrase_link) {
	my ($tab_name, $go_id) = split(/:/, $key);
	if ($tab_name eq "GO") {
	    push(@goTermsList, $go_id);
	}
    }
    return \@goTermsList;
} # end getGOTerms()

#>#####################################################################
#> isGOTerm()
#> FUNCTION:   Determines whether or not a given GO_ID is a
#>               GO Term for the current phrase.
#> PARAMETERS: $go_id - the GO_ID value of the potential GO Term
#> RETURNS:    0 - if $go_id is not a GO Term used in this phrase
#>             1 - if $go_id is a GO Term used in this phrase
#> NOTES:      This is a specialized wrapper for getPhraseLink() for 
#>               retrieving GO Terms from that table
#>             This method does not check to ensure that the $go_id value
#>               is a valid GO_ID in dictyBase.
#######################################################################
sub isGOTerm {
#######################################################################
    my ($self, $go_id) = @_;
    my $isGOid = 0;
    $go_id = $goPrimaryID{$go_id};
    my $goTermRef = $self->getGOTerms();
    foreach my $curr_go_id (@$goTermRef) {
	if ($curr_go_id == $go_id) {
	    $isGOid = 1;
	    last;
	}
    }
    return $isGOid;
} # isGOTerm()

#>#####################################################################
#> setGOTerm()
#> FUNCTION:   Sets a specified GO_ID as a GO Term used by the current
#>               phrase.
#> PARAMETERS: $go_id - the GO_ID of the GO Term to be set
#> RETURNS:    (None)
#> NOTES:      This is a specialized wrapper for setPhraseLink() for 
#>               retrieving GO Terms from that table
#>             This method will die with an error if the GO_ID is not
#>               found in the GO table, it will also use the primary GO
#>               ID for the given GO term, if valid.
#######################################################################
sub setGOTerm {
#######################################################################
    my ($self, $go_id) = @_;
    if (!defined $self->{'phrase_no'}) {
	$self->{'phrase_no'} = -1;
    }
    $go_id = defined $goPrimaryID{$go_id} ? $goPrimaryID{$go_id} : $go_id;

    # check to see if this is a valid GO id before attempting to add it to
    #   the database
    my $goObj = Go->new(dbh=>$dbh,
			goid=>$go_id);

    my @go_record;
    if (defined $goObj) {
	@go_record = ( $self->{'phrase_no'}, "GO", $go_id, "GOID" );
    }
    else {
	die "GO ID [$go_id] is not defined in the database.\n";
    }

    $self->setPhraseLinks(\@go_record);

    return;
} # end setGOTerm()

#>#####################################################################
#> unsetGOTerm()
#> FUNCTION:   Removes a specified GO_ID as a GO Term used by the current
#>               phrase.
#> PARAMETERS: $go_id - the GO_ID of the GO Term to be unset
#> RETURNS:    (None)
#> NOTES:      This is a specialized wrapper for unsetPhraseLink() for 
#>               retrieving GO Terms from that table
#>             This method does not check to see if $go_id is currently
#>               assigned to the current phrase or not.
#######################################################################
sub unsetGOTerm {
#######################################################################
    my ($self, $go_id) = @_;
    $go_id = $goPrimaryID{$go_id};
    my @go_record = ( undef, "GO" , $go_id, "GOID" );
    $self->unsetPhraseLink(\@go_record);
    return;
} # end unsetGOTerm()

#>#####################################################################
#> getPhraseCategories()
#> FUNCTION:   Retreives all PHRASE_CATEGORY values for the current phrase
#>               from the PHRASE_CATEGORY table.
#> PARAMETERS: (None)
#> RETURNS:    hash ref - A hash reference of all phrase categories assigned
#>               to the current phrase, where the key is the phrase category
#>               and the value is 1 if the key/phrase category is actually
#>               assigned to the current phrase.
#> NOTES:      
#######################################################################
sub getPhraseCategories {
#######################################################################
    my ($self) = @_;

    my $phrase_categoriesRef = $self->{'phrase_category'};

    return $phrase_categoriesRef;
} # end getPhraseCategories()

#>#####################################################################
#> isPhraseCategory()
#> FUNCTION:   Determines whether or not the phrase is assigned to a 
#>               particular phrase category.
#> PARAMETERS: $phrase_category - string phrase_category to be checked
#> RETURNS:    0 - if this phrase is not assigned to $phrase_category
#>             1 - if this phrase is not assigned to $phrase_category
#> NOTES:      This method does not test if $phrase_category is a
#>               valid phrase category or not.
#######################################################################
sub isPhraseCategory {
#######################################################################
    my ($self, $phrase_category) = @_;
    my $isPhraseCategory = 0;

    my $phrase_categoriesRef = $self->{'phrase_category'};
    my %phrase_category = %$phrase_categoriesRef;

    $phrase_category =~ tr/[a-z]/[A-Z]/;
    if (defined $phrase_category{$validPhraseCategory{$phrase_category}}) {
	$isPhraseCategory = 1;
    }
    return $isPhraseCategory;
} # end isPhraseCategory()

#>#####################################################################
#> setPhraseCategory()
#> FUNCTION:   Assigns a specified phrase category for the current
#>               phrase.
#> PARAMETERS: $phrase_category - the desired phrase category of the phrase
#> RETURNS:    (None)
#> NOTES:      This method will check to ensure that $phrase_category is a
#>               valid phrase category as listed in the CODE table.
#######################################################################
sub setPhraseCategory {
#######################################################################
    my ($self, $phrase_category) = @_;
    # check CGM_DDB.CODE for a list of valid phrase_categories

    my $phrase_categoriesRef = $self->{'phrase_category'};
    my %phrase_category = %$phrase_categoriesRef;

    $phrase_category =~ tr/[a-z]/[A-Z]/;
    if (defined $validPhraseCategory{$phrase_category}) {
	my $pc = $validPhraseCategory{$phrase_category};

	$phrase_category{$pc} = 1;
    }
    else {
	# $phrase_category is not a valid phrase_category because it
	#   doesn't exist in CGM_DDB.CODE
    }
    $self->{'phrase_category'} = \%phrase_category;

    return;
} # end setPhraseCategory()

#>#####################################################################
#> setPhraseCategory()
#> FUNCTION:   Unassigns a specified phrase category for the current
#>               phrase.
#> PARAMETERS: $phrase_category - the desired phrase category of the phrase
#>               to be removed.
#> RETURNS:    (None)
#> NOTES:      This method will check to ensure that $phrase_category is a
#>               valid phrase category as listed in the CODE table.
#######################################################################
sub unsetPhraseCategory {
#######################################################################
    my ($self, $phrase_category) = @_;

    my $phrase_categoriesRef = $self->{'phrase_category'};
    my %phrase_category = %$phrase_categoriesRef;

    $phrase_category =~ tr/[a-z]/[A-Z]/;
    if (defined $validPhraseCategory{$phrase_category}) {

	my $pc = $validPhraseCategory{$phrase_category};

	if (defined $phrase_category{$pc}) {
	    delete $phrase_category{$pc};
	}
	else {
	    # this phrase does not have this phrase as one of its categories.
	}
    }
    else {
	# $phrase_category is not a valid phrase_category because it
	#   doesn't exist in CGM_DDB.CODE
    }
    $self->{'phrase_category'} = \%phrase_category;

    return;
} # end unsetPhraseCategory()

#>#####################################################################
#> loadPhraseCategories()
#> FUNCTION:   Loads a list of valid phrase categories from the CGM_DDB.CODE table
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      The list of valide phrase categories will be stored in a
#>               hash table, for subsequent later use.
#######################################################################
sub loadPhraseCategories {
#######################################################################
    my ($self) = @_;

    if (scalar keys %validPhraseCategory != 0) {
	return;
    }

    my $sthGetPhraseCategories
	 = $dbh->prepare(
	      "SELECT code_value
	       FROM   $schema.code
	       WHERE  tab_name = 'PHRASE_CATEGORY'
	       AND    col_name = 'PHRASE_CATEGORY'
              ");
    $sthGetPhraseCategories->execute();

    while (my $array_ref = $sthGetPhraseCategories->fetchrow_arrayref) {
	my $phrase_category = $array_ref->[0];
	my $phrase_categoryUC = $array_ref->[0];
	$phrase_categoryUC =~ tr/[a-z]/[A-Z]/;
	$validPhraseCategory{$phrase_categoryUC} = $phrase_category;
    }
} # end getLoadPhraseCategories

#>#####################################################################
#> getReferences()
#> FUNCTION:   Retrieves a list of all references 
#>               associated with the current phrase with the REFLINK table.
#> PARAMETERS: (None)
#> RETURNS:    hash ref - containing rows from the REFLINK table in the
#>               following format:
#>               (REFLINK_NO, REFERENCE_NO, DATE_CREATED, CREATED_BY)
#> NOTES:      
#######################################################################
sub getReferences {
#######################################################################
    my ($self) = @_;

    my $ref_linkRef = $self->{'ref_link'};
    my %ref_link = %$ref_linkRef;

    return $ref_linkRef;
} # end getReferences()

#>#####################################################################
#> getOrderedReferences()
#> FUNCTION:   Retrieves a list of all references ordered the same way that
#>               they are referened in the PHRASE_TEXT.
#> PARAMETERS: (None)
#> RETURNS:    array ref - containing rows from the REFLINK table in the
#>               following format:
#>               (REFLINK_NO, REFERENCE_NO, DATE_CREATED, CREATED_BY)
#> NOTES:      This is handy when displayed the list of references in the
#>               same order as seen in the phrase.  This method will ensure
#>               that a reference is not displayed twice, even if it is
#>               mentioned twice in the PHRASE_TEXT
#######################################################################
sub getOrderedReferences {
#######################################################################
    # this method retrieves references from a phrase in the same order
    #   that they appear in the phrase_text

    my ($self) = @_;

    my @references = ();
    my $phrase_text = $self->{'phrase_text'};

    # this ensures that no reference is appears in @references more then
    #   once
    my %references;

    foreach my $word (split(/ /, $phrase_text)) {
	if (($word =~ /ref_no:(\d+)/) && (!defined $references{$1})) {
	    push(@references, $1);

	    # flag this reference so that is it not added again in the future
	    $references{$1} = 1;
	}
	elsif (($word =~ /ref_pm:(\d+)/)) {

	    my $pubmed_id = $1;
	    my $refObj = Reference->new(dbh=>$dbh,
					pubmed=>$pubmed_id);

	    if ((defined $refObj)
		&& (!defined $references{$refObj->reference_no()})
	       ) {
		push(@references, $refObj->reference_no());
	    }
	}
    }

    return \@references;
} # end getOrderedReferences()

#>#####################################################################
#> isReference()
#> FUNCTION:   Given a REFERENCE_NO value, it will determine whether or not
#>               the reference is cited in the current PHRASE_TEXT or not.
#> PARAMETERS: $reference_no - REFERENCE_NO value of reference to check.
#> RETURNS:    0 - $reference_no is not a reference used by this phrase.
#>             1 - $reference_no is a reference used by this phrase.
#> NOTES:      
#######################################################################
sub isReference {
#######################################################################
    my ($self, $reference_no) = @_;
    my $isReference = 0;

    my $ref_linkRef = $self->{'ref_link'};
    my %ref_link = %$ref_linkRef;

    if (defined $ref_link{$reference_no}) {
	$isReference = 1;
    }
    return $isReference;
} # end isReference()

#>#####################################################################
#> setPMReference()
#> FUNCTION:   Given a PUBMED_ID, will set associate that reference with
#>               the current phrase.
#> PARAMETERS: $pubmed_id - PUBMED_ID value of reference to add.
#> RETURNS:    
#> NOTES:      This method will die if the pubmed id given is not
#>               loaded into dictyBase.  The reference is actually stored by
#>               its REFERENCE_NO value.
#>             References will not be added more than once to the phrase.
#######################################################################
sub setPMReference {
#######################################################################
    my ($self, $pubmed_id) = @_;

    my $reference = Reference->new(dbh=>$dbh,
				   pubmed=>$pubmed_id);
    if (defined $reference) {
	$self->setReference($reference->reference_no());
    }
    else {
	die ("Unable to locate Pubmed Id = $pubmed_id in "
	     . "dictyBase.  Perhaps this reference has not been loaded into "
	     . "the database.\n");
    }

} # end setPMReference()

#>#####################################################################
#> setReference()
#> FUNCTION:   Given a REFERENCE_NO, will set associate that reference with
#>               the current phrase.
#> PARAMETERS: $reference_no - REFERENCE_NO value of reference to add.
#> RETURNS:    
#> NOTES:      This method will die if the reference_no given is not
#>               loaded into dictyBase.  The reference is actually stored by
#>               its REFERENCE_NO value.
#>             References will not be added more than once to the phrase.
#######################################################################
sub setReference {
#######################################################################
    my ($self, $reference_no) = @_;

    my $ref_linkRef = $self->{'ref_link'};
    my %ref_link = %$ref_linkRef;

    if (!defined $ref_link{$reference_no}) {

	my $reference = Reference->new(dbh=>$dbh,
				       reference_no=>$reference_no
				       );
	if (defined $reference) {
	    $ref_link{$reference_no} = 1;
	}
	else {
	    delete $ref_link{$reference_no};
	    die "Unable to locate reference: $reference_no in the database.";
	}
    }

    $self->{'ref_link'} = \%ref_link;
    return;
} # end setReference()

#>#####################################################################
#> unsetReference()
#> FUNCTION:   Given a REFERENCE_NO, will set un-associate that reference with
#>               the current phrase.
#> PARAMETERS: $reference_no - REFERENCE_NO value of reference to unset.
#> RETURNS:    
#> NOTES:      This method will not check if $reference_no is actaully
#>               assigned to this phrase or not before removing it.  However,
#>               if it is not assigned, that's no problem.
#>             The reference itself will not be deleted from dictyBase.
#######################################################################
sub unsetReference {
#######################################################################
    my ($self, $reference_no) = @_;

    my $ref_linkRef = $self->{'ref_link'};
    my %ref_link = %$ref_linkRef;

    if (defined $ref_link{$reference_no}) {
	delete $ref_link{$reference_no};
    }
    else {
	# this phrase does not have this phrase as one of its categories.
    }

    $self->{'ref_link'} = \%ref_link;

    return;
} # end unsetReference()

#>#####################################################################
#> clearAll()
#> FUNCTION:   For internal use only.  Clears all hash values used to
#>               store data in database tables, prior to reloading the phrase
#>               from the database.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      
#######################################################################
sub clearAll {
#######################################################################
    my ($self, $go_id) = @_;

    $self->{'phrase_link'} = {};
    $self->{'phrase_category'} = {};
    $self->{'ref_link'} = {};
}

#>#####################################################################
#> validate()
#> FUNCTION:   Used to check that the phrase has valid values before attempting
#>               to alter the database version of this phrase.
#>               from the database.
#> PARAMETERS: (None)
#> RETURNS:    0 - if everything is a-OK
#>             string - containing an error message of what is wrong with
#>               the phrase object.
#> NOTES:      
#######################################################################
sub validate {
#######################################################################
    my ($self) = @_;

    my $errorMsg = "";
    my $errorCount = 0;

    my $phrase_text = $self->getPhraseText();
    if ((!defined $phrase_text) || (length($phrase_text) == 0)) {
	$errorCount++;
	$errorMsg .= "Zero-length or no PHRASE_TEXT found.\n";
    }

    my $phrase_order = $self->getPhraseOrder();
    if ((!defined $phrase_order) || (length($phrase_order) == 0)) {
	$errorCount++;
	$errorMsg .= "Zero-length or no PHRASE_ORDER found.\n";
    }

    my $paragraph_no = $self->getParagraphNo();
    if ((!defined $paragraph_no) || (length($paragraph_no) == 0)) {
	$errorCount++;
	$errorMsg .= "Zero-length or no PARAGRAPH_NO found.\n";
    }

    my $phraseCategoryRef = $self->getPhraseCategories();
    if (!defined $phraseCategoryRef) {
	$errorCount++;
	$errorMsg .= "No PHRASE_CATEGORY assigned to this phrase. (1)\n";
    }
    else {
	my %phraseCategories = %$phraseCategoryRef;

	if (scalar keys %phraseCategories == 0) {
	    $errorCount++;
	    $errorMsg .= "No PHRASE_CATEGORY assigned to this phrase. (2)\n";
	}
    }

    my $returnCode = 0;
    if ($errorCount > 0) {
	$returnCode 
	     =  "The following errors were discovered with this phrase:\n";
	$returnCode .= $errorMsg;
    }
    return $returnCode;
}

#>#####################################################################
#> loadPrimaryGOids()
#> FUNCTION:   Loads a list of primary GO ids from the database to ensure
#>               that all GO IDs used are valid.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      Due to speed issues, this method has been turned off.
#######################################################################
sub loadPrimaryGOids {
#######################################################################
    if (scalar keys %goPrimaryID != 0) { return; }

    my $slow_method = "OFF";

    if ($slow_method eq "ON") {

	# retrieve a list of GO IDs in dictyBase
	my $sth = $dbh->prepare("SELECT  goid".
				       "FROM    $schema.go");
	$sth->execute();

	while (my $array_ref = $sth->fetchrow_arrayref) {
	    my $goid = $array_ref->[0];
	    $dictyBaseLoadedGOIDs{$goid} = 1;
	}

	my $goPathname = "/share/ftp/go/ontology";
	my $goComponentFilename = "$goPathname/component.ontology";
	my $goFunctionFilename = "$goPathname/function.ontology";
	my $goProcessFilename = "$goPathname/process.ontology";

	my @filenames = ($goComponentFilename, 
			 $goFunctionFilename, 
			 $goProcessFilename);

	foreach my $currFilename (@filenames) {
	    open (IN, $currFilename) 
		 || die "Can't open file ($currFilename): $!\n";

       line: while (<IN>) {
		if (/^\!/) { next; }
		chomp($_);

		my $primaryGOID = 0;

		foreach my $word (split(/\s+/, $_)) {

		    if ($word =~ /[\<\%]/) {

			if ($primaryGOID != 0) {
			    next line;
			}

			$primaryGOID = 0;
			next;
		    }

		    if (($primaryGOID == 0) 
			&& ($word =~ /GO:(\d+)/)) {
			$primaryGOID = $1;
			my $zpPrimaryGOID = $1;
			# strip off leading zeroes
			$primaryGOID =~ s/^0*//;
			if ((defined $goPrimaryID{$zpPrimaryGOID})
			    && 
			    ($goPrimaryID{$zpPrimaryGOID} != $primaryGOID)) {
			}
			$goPrimaryID{$zpPrimaryGOID} = $primaryGOID;
			next;
		    }

		    if ($word =~ /GO:(\d+)/) {
			$goPrimaryID{$1} = $primaryGOID;
		    }
		}
	    }
	    close (IN);
	}

	foreach my $zpGo_id (keys %goPrimaryID) {
	    my $primary_goid = $goPrimaryID{$zpGo_id};

	    my $go_id = $zpGo_id;
	    $go_id =~ s/^0*//;

	    if (defined $dictyBaseLoadedGOIDs{$primary_goid}) {
		$goPrimaryID{$go_id} = $primary_goid;
	    }
	    elsif (defined $dictyBaseLoadedGOIDs{$go_id}) {
		$goPrimaryID{$go_id} = $go_id;
		$goPrimaryID{$zpGo_id} = $go_id;
		
		$goPrimaryID{$primary_goid} = $go_id;
		my $zpPGoid = substr(7, "0000000".$primary_goid);
		$goPrimaryID{$zpPGoid} = $go_id;
	    }
	    else {
		$goPrimaryID{$go_id} = "$primary_goid";
	    }
	    # print "\{$go_id\} = $primary_goid\n";
	}
    }
    else {
	    
    }
}

#######################################################################
1;
#######################################################################
