#!/usr/bin/perl
package ParagraphDS;

###########################################################################
#
# Script Name: ParagraphDS.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 paragraphs within dictyBase.
#
# Project:     dictyBase / Summary Paragraphs / Oracle Conversaion
#
# 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 paragraphs.
#
#>             All accessor methods of this module do not retrieve directly
#>               from the database, but rather from the values associated
#>               with the current instance of ParagraphDS.  Thus, if you 
#>               load the paragraph from the database, and then subsequently
#>               alter paragraph 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/PhraseDS.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";
use PhraseDS;
use lib "/usr/local/dicty/www_dictybase/db/lib/dictyBase/Objects";
use Paragraph;
use Coll_para;
use Locus;
use Phrase;

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

my $dbh;
#>#####################################################################
#> ParagraphDS->new() Constructor
#> FUNCTION:   Creates a new instance of the ParagraphDS for a single
#>                paragraph.
#> 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
#>             2a locusNo - the LOCUS_NO of whose paragraph is to be
#>                          retrieved
#>             2b locusName - the LOCUS_NAME whose paragraph is to be
#>                            retrieved
#>             2c paragraphNo - the PARAGRAPH_NO of the paragraph to be
#>                              retrieved
#> RETURNS:    A ParagraphDS object.
#> NOTES:
#######################################################################
sub new {      ############ constructor ###############################
#######################################################################
    my ($self, %args) = @_;

    $self = {};
    bless $self;

    if ($args{'dbh'}) { 
        $dbh = $args{'dbh'};
    }
    elsif ($args{'database'}) {
        $self->{'_database'} = $args{'database'};
	my $user = $args{'user'};

	my ($dbuser, $dbpasswd) = &getUsernamePassword($user,
                                                       $self->{'_database'});

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

    # initialize instance variable references
    my @newArray;
    my $newHash;
    my %newHash;
    @newArray = (); $self->{'phrases'} = \@newArray;
    %newHash = (); $self->{'colleagues'} = \%newHash;
    %newHash = (); $self->{'primaryLoci'} = \%newHash;
    %newHash = (); $self->{'references'} = \%newHash;

    @newArray = (); $self->{'oldPhrases'} = \@newArray;
    %newHash = (); $self->{'oldColleagues'} = \%newHash;
    %newHash = (); $self->{'oldPrimaryLoci'} = \%newHash;
    %newHash = (); $self->{'oldReferences'} = \%newHash;

    if ($args{'locusNo'}
	|| $args{'locusName'}
	|| $args{'paragraphNo'}) {

	my $searchTerm;
	if ($args{'locusNo'}) {
	    $searchTerm = $args{'locusNo'};
	    $self->getParagraphByLocusNoFromDB($searchTerm);
	}
	elsif ($args{'locusName'}) {
	    $searchTerm = $args{'locusName'};
	    $self->getParagraphByLocusNameFromDB($searchTerm);
	}
	elsif ($args{'paragraphNo'}) {
	    $searchTerm = $args{'paragraphNo'};
	    $self->getParagraphByParagraphNoFromDB($searchTerm);
	}

	my $paragraph_row = $self->{'paragraph_row'};

	$self->{'paragraph_no'} = $paragraph_row->paragraph_no;
	$self->{'paragraph_text'} = $paragraph_row->paragraph_text;
	$self->{'date_written'} = $paragraph_row->date_written;
	$self->{'written_by'} = $paragraph_row->written_by;

	$self->{'loadTbl_collPara'} = 0;
	$self->{'loadTbl_locus'} = 0;
	$self->{'loadTbl_phrase'} = 0;

	if (!defined $self->{'paragraph_row'}) {
	    die ("Paragraph.pm::new Unable to locate paragraph using search "
		 ."term = " . $searchTerm);
	}
    }
    else {
	# create a new, fresh, paragraph object
	$self->{'objectType'} = 1;
    }

    $self->{'loadTbl_collPara'} = 0;
    $self->{'loadTbl_locus'} = 0;
    $self->{'loadTbl_phrase'} = 0;

    return $self;
} # end new();

#>#####################################################################
#> dbh()
#> FUNCTION:   Returns the database handle used in creating this
#>                ParagraphDS object
#> PARAMETERS: (None)
#> RETURNS:    A database handle.
#> NOTES:
#######################################################################
sub dbh { return $dbh; }

#>#####################################################################
#> getParagraphByLocusNoFromDB()
#> FUNCTION:   Retrieves a paragraph from the database, given a LOCUS_NO
#>                value.
#> PARAMETERS: $locusNo - the LOCUS_NO of the locus whose paragraph to
#>                        be retrieved
#> RETURNS:    1, always.  If not paragraph is associated with the current
#>               paragraph, then the $self->{'paragraph_row'} will be
#>               undefined.  It is up to the caller to test this before
#>               using this object further
#> NOTES:
#######################################################################
sub getParagraphByLocusNoFromDB {
#######################################################################
    my ($self, $locusNo) = @_;

    my $paragraph_row = Paragraph->new('dbh'=>$dbh,
				       locus_no=>$locusNo);
    $self->{'paragraph_row'} = $paragraph_row;

    return 1;
} # end getParagraphByLocusNoFromDB()

#>#####################################################################
#> delete()
#> FUNCTION:   Mark the current ParagraphDS object and the paragraph
#>                associated with this object to be deleted from the
#>                database.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      Call the update() method to delete this paragraph from
#>               from the database.
#######################################################################
sub delete {
#######################################################################
    my ($self) = @_;

    $self->{'objectType'} = 2;

    return;
} # end delete()

#>#####################################################################
#> getParagraphByLocusNameFromDB()
#> FUNCTION:   Retrieves a paragraph from the database, given a LOCUS_NAME
#>                value.
#> PARAMETERS: $locusName - the LOCUS_NAME of the locus whose paragraph to
#>                        be retrieved
#> RETURNS:    1, always.  If not paragraph is associated with the current
#>               paragraph, then the $self->{'paragraph_row'} will be
#>               undefined.  It is up to the caller to test this before
#>               using this object further
#> NOTES:
#######################################################################
sub getParagraphByLocusNameFromDB {
#######################################################################
    my ($self, $locusName) = @_;

    my $paragraph_row = Paragraph->new('dbh'=>$dbh, 
				       locus_name=>$locusName);
    $self->{'paragraph_row'} = $paragraph_row;

    return 1;
} # end getParagraphByLocusNameFromDB()

#>#####################################################################
#> getParagraphByParagraphNoFromDB()
#> FUNCTION:   Retrieves a paragraph from the database, given a PARAGRAPH_NO
#>                value.
#> PARAMETERS: $paragraphNo - the PARAGRAPH_NO of the paragraph to
#>                        be retrieved
#> RETURNS:    1, always.  If not paragraph is associated with the current
#>               paragraph, then the $self->{'paragraph_row'} will be
#>               undefined.  It is up to the caller to test this before
#>               using this object further
#> NOTES:
#######################################################################
sub getParagraphByParagraphNoFromDB {
#######################################################################
    my ($self, $paragraphNo) = @_;

    my $paragraph_row = Paragraph->new('dbh'=>$dbh, 
				       paragraph_no=>$paragraphNo);
    $self->{'paragraph_row'} = $paragraph_row;

    return 1;
} # end getParagraphByParagraphNoFromDB()

#>#####################################################################
#> update()
#> FUNCTION:   Updates the database with the current ParagraphDS object,
#>                including updates, insertions and deletions from the
#>                database, if necessary.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      This method will call validate() to ensure that data in
#>                is valid, before committing to the database.
#######################################################################
sub update {
#######################################################################
    my ($self) = @_;

    my $paragraph_row;

    if (($self->{'objectType'} == 0) || ($self->{'objectType'} == 1)) {
	my $errorMessage = $self->validate();

	if ($errorMessage ne 0) {
	    my $paragraphNo = "(None)";

	    if (defined $self->{'paragraph_no'}) {
		$paragraphNo = $self->{'paragraph_no'}
	    }

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

	# test the length of the paragrapbh_text string
	if (length $self->{'paragraph_text'} > 4000) {
	    die "Problem: This paragraph is too long.\n";
	}
    }

    # create a new paragraph object, if this is a new paragraph
    if ($self->{'objectType'} == 1) {
	$self->updateParagraphText();

	# do not insert this paragraph if it already exists in the database
	$paragraph_row
	     = Paragraph->new('dbh'=>$dbh,
			      paragraph_text=>$self->{'paragraph_text'});

	if (defined $paragraph_row) {
	    die ("A paragraph with this text already exists in the database.");
	}

	# insert the paragraph into the table
	Paragraph->Insert
	     ('dbh'=>$dbh,
	      binds=>{paragraph_text=>$self->{'paragraph_text'},
		      date_written=>$self->{'date_written'},
		      written_by=>$self->{'written_by'}
		     });

	# attempt to determine the paragraph_no of the recently inserted
	#   paragraph
	$paragraph_row
	     = Paragraph->new('dbh'=>$dbh,
			      paragraph_text=>$self->{'paragraph_text'});

	$self->{'paragraph_no'} = $paragraph_row->paragraph_no;
	$self->{'paragraph_row'} = $paragraph_row;
    }
    $paragraph_row = $self->{'paragraph_row'};

    if (($self->{'objectType'} == 0) || ($self->{'objectType'} == 1)) {
	# this is an update, and assumes that the current paragraph object
	#   already has a paragraph_no associated with it

	my $updates = 0;
	if ($paragraph_row->paragraph_text
	    ne $self->{'paragraph_text'}) {
	    $paragraph_row->updatePARAGRAPH_TEXT($self->{'paragraph_text'});
	    $updates = 1;
	}
	if ($paragraph_row->date_written
	    ne $self->{'date_written'}) {
	    $paragraph_row->updateDATE_WRITTEN($self->{'date_written'});
	    $updates = 1;
	}
	if ($paragraph_row->written_by ne $self->{'written_by'}) {
	    $paragraph_row->updateWRITTEN_BY($self->{'written_by'});
	    $updates = 1;
	}

	if ($updates == 1) {
	    eval {
		$paragraph_row->enterUpdates();
	    };
	    if ($@) {
		warn "Error: $@\n";
	    }
	    else {
		# updates were processed without problems
	    }
	}

	# look for changes in @colleagues
	if ($self->{'loadTbl_collPara'} == 1) {
	    my $colleaguesRef = $self->{'colleagues'};
	    my %colleagues = %$colleaguesRef;
	    my $oldColleaguesRef = $self->{'oldColleagues'};
	    my %oldColleagues = %$oldColleaguesRef;

	    foreach my $newColleague (keys %colleagues) {
		if (!defined $oldColleagues{$newColleague}) {
		    Coll_para->Insert
			 ('dbh'=>$dbh,
			  binds=>{colleague_no=>$newColleague,
				  paragraph_no=>$paragraph_row->paragraph_no}
			 );
		}
	    }
	    foreach my $oldColleague (keys %oldColleagues) {
		if (!defined $colleagues{$oldColleague}) {
		    my $coll_para
			 = Coll_para->new('dbh'=>$dbh,
					  colleague_no=>$oldColleague,
					  paragraph_no=>$self->{'paragraph_no'}
					 );
		    $coll_para->delete();
		}
	    }
	}

	# look for changes in @primaryLoci
	if ($self->{'loadTbl_locus'} == 1) {

	    my $primaryLociRef = $self->{'primaryLoci'};
	    my %primaryLoci = %$primaryLociRef;
	    my $oldPrimaryLociRef = $self->{'oldPrimaryLoci'};
	    my %oldPrimaryLoci = %$oldPrimaryLociRef;

	    foreach my $newPrimaryLocus (keys %primaryLoci) {
		if (!defined $oldPrimaryLoci{$newPrimaryLocus}) {

		    my $paragraph_no = $self->{'paragraph_no'};

		    # associate loci with current paragraph
		    my $locus = Locus->new('dbh'=>$dbh,
					   locus_no=>$newPrimaryLocus);

		    $locus->updatePARAGRAPH_NO($paragraph_row->paragraph_no);
		    $locus->enterUpdates;
		}
	    }
	    foreach my $oldPrimaryLocus (keys %oldPrimaryLoci) {
		if (!defined $primaryLoci{$oldPrimaryLocus}) {

		    # dis-associate loci with current paragraph
		    my $locus = Locus->new('dbh'=>$dbh,
					   locus_no=>$oldPrimaryLocus);
		    $locus->updatePARAGRAPH_NO("");
		    $locus->enterUpdates;

		}
	    }
	}

	# look for changes in @phrases
	my $phraseRef = $self->{'phrases'};
	my @phrases = @$phraseRef;
	my $oldPhrasesRef = $self->{'oldPhrases'};
	my @oldPhrases = @$oldPhrasesRef;

	my %updatedPhrases;

	if ($self->{'loadTbl_phrase'} == 1) {
	    foreach my $newPhrase (@phrases) {
		$newPhrase->setParagraphNo($self->{'paragraph_no'});
		$newPhrase->update();
		$updatedPhrases{$newPhrase->getPhraseNo} = 1;
	    }
	    foreach my $oldPhrase (@oldPhrases) {
		if (!defined $updatedPhrases{$oldPhrase->getPhraseNo}) {
		    $oldPhrase->setParagraphNo($self->{'paragraph_no'});
		    $oldPhrase->delete();
		    $oldPhrase->update();
		}
	    }
	}
    }
    elsif ($self->{'objectType'} == 2) {

	# DELETE the paragraph

	# delete from CGM_DDB.PHRASE (calling this method will also remove
	#                          associated CGM_DDB.REFLINK rows)
	my $phraseRef = $self->getPhrases();
	my @phrases = @$phraseRef;

	foreach my $phrase (@phrases) {
	    $self->deletePhrase($phrase->getPhraseNo());
	    $phrase->update();
	}

	# delete from CGM_DDB.LOCUS (actually, just clear the PARAGRAPH_NO field)
	my $primaryLociRef = $self->getPrimaryLoci();
	my %primaryLoci = %$primaryLociRef;

	foreach my $primaryLocus (keys %primaryLoci) {
	    # dis-associate loci with current paragraph
	    my $locusObj = Locus->new('dbh'=>$dbh,
				   locus_no=>$primaryLocus);

	    $locusObj->updatePARAGRAPH_NO("");
	    $locusObj->enterUpdates();

	}

	# delete from CGM_DDB.COLL_PARA (automatically done by database)
	# delete from CGM_DDB.COLL (automatically done by database)

	# delete from CGM_DDB.PARAGRAPH
	$paragraph_row->delete();
    }

    # This next line resets all the instance variables of this class
    $self->getParagraphByParagraphNoFromDB($self->{'paragraph_no'});
} # end update()

#>#####################################################################
#> getParagraphNo()
#> FUNCTION:   Retrieves the paragraphNo of the current paragraph
#> PARAMETERS: (None)
#> RETURNS:    int - the PARAGRAPH_NO of the current paragraph.
#> NOTES:      
#######################################################################
sub getParagraphNo {
#######################################################################
    my ($self) = @_;

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

#>#####################################################################
#> getParagraphText()
#> FUNCTION:   Retrieves the paragraphText of the current paragraph
#> PARAMETERS: (None)
#> RETURNS:    string - text of the paragraph
#> NOTES:      
#######################################################################
sub getParagraphText {
#######################################################################
    my ($self) = @_;

    return $self->{'paragraph_text'};
} # end getParagraphText()

#>#####################################################################
#> getReferences()
#> FUNCTION:   Retrieves all references associated with phrases of the
#>               current paragraph.
#> PARAMETERS: (None)
#> RETURNS:    An array reference - a list of non-duplicate references
#>               used by phrases of this paragraph
#> NOTES:      
#######################################################################
sub getReferences {
#######################################################################
    my ($self) = @_;

    my @referenceList = ();

    my %referenceList;

    foreach my $word (split(/[^a-zA-Z0-9_:]+/, $self->{'paragraph_text'})) {
	if (($word =~ /ref_no:(\d+)/) 
	    && (!defined $referenceList{"RF:$1"})) {
	    push (@referenceList, "RF:$1");
	    $referenceList{"RF:$1"} = 1;
	}

	if (($word =~ /ref_pm:(\d+)/)
	    && (!defined $referenceList{"PM:$1"})) {
	    push (@referenceList, "PM:$1");
	    $referenceList{"PM:$1"} = 1;
	}
    }

    return \@referenceList;
} # end getReferences()

#>#####################################################################
#> deletePhrase()
#> FUNCTION:   Deletes a single phrase from the current paragraph
#> PARAMETERS: $phrase_no - the PHRASE_NO (not PHRASE_ORDER) of the
#>               phrase to be deleted
#> RETURNS:    (None)
#> NOTES:      The phrase will not be deleted from the database until
#>               its update() method is called... this paragraph object's
#>               update() method will call all phrases' update() methods.
#>             This method also deals with renumbering the PHRASE_ORDERs
#>               of the remaining phrases.
#######################################################################
sub deletePhrase {
#######################################################################
    my ($self, $phrase_no) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	loadPhraseTableData();
    }

    my @remainingPhrases = ();

    # retrieve all current phrases associated with this paragraph
    #   (the deletePhrase() method assumes that $self->{'phrases'} are not
    #    ordered in any way)
    my $phraseRef = $self->{'phrases'};
    my @phrases = @$phraseRef;

    # iterate through all current phrases
    foreach my $curr_phrase (@phrases) {

	# locate the phrase to be deleted
	if ($curr_phrase->getPhraseNo() == $phrase_no) {
	    $curr_phrase->delete(); # mark that phrase for deletion
	}
	else {
	    # these phrases are not to be deleted, to push them onto an array
	    push(@remainingPhrases, $curr_phrase);
	}
    }

    # renumber the phrases, eliminating the numerical gap created by the
    #   deleted phrase
    my %orderedPhrases;

    # create a hash of all the remaining phrases, so that we can retrieve them
    #   in numerical order by their phraseOrder value
    foreach my $phrase (@remainingPhrases) {
	$orderedPhrases{$phrase->getPhraseOrder}
	     = $phrase;
    }

    my $new_phrase_order_no = 0;
    # my %reorderedPhrases;
    @phrases = (); # this array will store the phrases in the correct order

    foreach my $old_phrase_order_no (sort {$a <=> $b} keys %orderedPhrases) {
	my $curr_phrase = $orderedPhrases{$old_phrase_order_no};
#	$reorderedPhrases{$new_phrase_order_no} 
#	     = $curr_phrase;
	$curr_phrase->setPhraseOrder($new_phrase_order_no);
	push(@phrases, $curr_phrase);
	$new_phrase_order_no++;
    }

    $self->{'phrases'} = \@phrases;
    $self->updateParagraphText();

    return;
} # deletePhrase()

#>#####################################################################
#> updateParagraphText()
#> FUNCTION:   Updates the PARAGRAPH_TEXT field of the paragraph object
#> PARAMETERS: paragraph_text [hash] - string containing the new text
#>               of the paragraph.
#> RETURNS:    (None)
#> NOTES:      The paragraph text should be created from the phrase texts
#>               of each phrase (inserted in order).
#>             This method is provided for internal use of this object
#>               only.
#######################################################################
sub updateParagraphText {
#######################################################################
    my ($self) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    $self->{'paragraph_text'} = "";

    my $phraseRef = $self->{'phrases'};
    my @phrases = @$phraseRef;
    foreach my $phrase (@phrases) {
	$self->{'paragraph_text'} .= $phrase->getPhraseText() . " ";
    }

    $self->{'paragraph_text'} =~ s/ $//;

    return;
} # end updateParagraphText()

#>#####################################################################
#> loadPhraseTableData()
#> FUNCTION:   Loads data from the phrase table in dictyBase into the 
#>               current ParagraphDS.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      This method only loads data if the data has not been loaded
#>               from the PHRASE table recently.  It has a flag that tracks
#>               whether or not the data has been loaded yet.
#######################################################################
sub loadPhraseTableData {
#######################################################################
    my ($self) = @_;

    $self->{'loadTbl_phrase'} = 1;

    my @phrases = ();
    my @oldPhrases = ();

    if (!defined $self->{'paragraph_no'}) {
    }
    else {
	my $phraseRef = $self->{'paragraph_row'}->phraseRef();
	foreach my $rowRef (@$phraseRef) {
	    my ($phrase_no) = @$rowRef;
	    my $phrase = PhraseDS->new('dbh'=>$dbh,
				     phrase_no=>$phrase_no);
	    push(@phrases, $phrase);
	    my $oldPhrase = PhraseDS->new('dbh'=>$dbh,
					phrase_no=>$phrase_no);
	    push(@oldPhrases, $oldPhrase);
	}
    }

    $self->{'phrases'} = \@phrases;
    $self->{'oldPhrases'} = \@oldPhrases;
    return;
} # end loadPhraseTableData()

#>#####################################################################
#> loadLocusTableData()
#> FUNCTION:   Loads data from the locus table in dictyBase into the 
#>               current ParagraphDS.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      This method only loads data if the data has not been loaded
#>               from the LOCUS table recently.  It has a flag that tracks
#>               whether or not the data has been loaded yet.
#######################################################################
sub loadLocusTableData {
#######################################################################
    my ($self) = @_;

    $self->{'loadTbl_locus'} = 1;

    my %primaryLoci;
    my %oldPrimaryLoci;

    if (!defined $self->{'paragraph_no'}) {
    }
    else {
	my $locusRef = $self->{'paragraph_row'}->locusRef();
	foreach my $rowRef (@$locusRef) {
	    my ($locusNo, $locusName) = @$rowRef;
	    $primaryLoci{$locusNo} = $locusName;
	    $oldPrimaryLoci{$locusNo} = $locusName;
	}
    }

    $self->{'primaryLoci'} = \%primaryLoci;
    $self->{'oldPrimaryLoci'} = \%oldPrimaryLoci;
    return;
} # end loadLocusTableData()

#>#####################################################################
#> loadCollParaTableData()
#> FUNCTION:   Loads data from the COLL_PARA table in dictyBase into the 
#>               current ParagraphDS.
#> PARAMETERS: (None)
#> RETURNS:    (None)
#> NOTES:      This method only loads data if the data has not been loaded
#>               from the LOCUS table recently.  It has a flag that tracks
#>               whether or not the data has been loaded yet.
#######################################################################
sub loadCollParaTableData {
#######################################################################
    my ($self) = @_;

    $self->{'loadTbl_collPara'} = 1;

    my %colleagues;
    my %oldColleagues;

    if (!defined $self->{'paragraph_no'}) {
    }
    else {
	my $colleagueRef = $self->{'paragraph_row'}->colleagueRef();
	foreach my $rowRef (@$colleagueRef) {
	    (my $colleagueNo, undef) = @$rowRef;
	    $colleagues{$colleagueNo} = 1;
	    $oldColleagues{$colleagueNo} = 1;
	}
    }

    $self->{'colleagues'} = \%colleagues;
    $self->{'oldColleagues'} = \%oldColleagues;
    return;
} # end loadCollParaTableData()

#>#####################################################################
#> insertPhrase()
#> FUNCTION:   Inserts a new phrase into the paragraph, reorganizing all
#>               relevant links, phrase order numbers, etc.
#> PARAMETERS: 1 new_phrase_text [hash] - a string containing the text of the
#>               new phrase
#>             2 new_phrase_order [hash] - the new phrase order number of
#>               the phrase to be inserted.
#>             3 new_phrase_object [hash] - the new phrase object to be
#>               inserted into the database.  This is a PhraseDS object.
#> RETURNS:    The new phrase object
#> NOTES:      When calling this subroutine with parameter 1, parameter 2
#>               is mandatory.  If calling this subroutine with parameter 3
#>               and parameter 2 is not given, it will take new_phrase_order
#>               from the value specified by 
#>               new_phrase_object->getPhraseOrder()
#######################################################################
sub insertPhrase {
#######################################################################
    my ($self, %args) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    my $new_phrase_object;
    my $new_phrase_order;

    my $new_phrase_text = $args{'new_phrase_text'};
    if (defined $new_phrase_text) {
	$new_phrase_object = PhraseDS->new('dbh'=>$dbh);
	$new_phrase_object->setPhraseText($new_phrase_text);
    }
    elsif (defined $args{'new_phrase_object'}) {
	$new_phrase_object = $args{'new_phrase_object'};

	if (!defined $args{'new_phrase_order'}) {
	    $new_phrase_order = $new_phrase_object->getPhraseOrder();
	}
    }
    else {
	$new_phrase_object = PhraseDS->new('dbh'=>$dbh);
    }


    $new_phrase_order = $args{'new_phrase_order'};

    if ($new_phrase_order > $self->getPhraseCount()) {
	$new_phrase_order = $self->getPhraseCount();
    }

    $new_phrase_object->setPhraseOrder($new_phrase_order);

    if (defined $self->{'paragraph_no'}) {
	$new_phrase_object->setParagraphNo($self->{'paragraph_no'});
    }

    # renumber the phrases, adding the new phrase at the appropriate position
    my $new_phrase_order_no = 0;
    my $new_phrase_inserted = 0;
    my @reorderedPhrases;

    my $phrasesRef = $self->getPhrases();
    my @phrases = @$phrasesRef;

    foreach my $old_phrase (@phrases) {
	if ($new_phrase_order_no eq $new_phrase_order) {
	    push(@reorderedPhrases, $new_phrase_object);
	    $new_phrase_order_no++;
	    $new_phrase_inserted = 1;
	}
	$old_phrase->setPhraseOrder($new_phrase_order_no);
	push(@reorderedPhrases, $old_phrase);
	$new_phrase_order_no++;
    }

    if ($new_phrase_inserted == 0) {
	push(@reorderedPhrases, $new_phrase_object);
	$new_phrase_object->setPhraseOrder($new_phrase_order_no);
	$new_phrase_order_no++;
	$new_phrase_inserted = 1;
    }

    @phrases = @reorderedPhrases;
    $self->{'phrases'} = \@phrases;

    $self->updateParagraphText();

    return $new_phrase_object;
} # end insertPhrase()

#>#####################################################################
#> updatePhrase()
#> FUNCTION:   Updates a phrase within the current paragraph.
#> PARAMETERS: $updated_phrase_object - the new phrase object
#>             $phrase_order_no - the phrase order number where the updated
#>               phrase is to be placed.
#> RETURNS:    (None)
#> NOTES:      This method replaces a phrase at position $phrase_order_no
#>               with $updated_phrase_object.
#######################################################################
sub updatePhrase {
#######################################################################
    my ($self, $updated_phrase_object, $phrase_order_no) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    if ($phrase_order_no !~ /^[\+\-]*(\d+)$/) {
	# error
	die "$phrase_order_no passed to replacePhrase() is not a number.\n";
    }

    if ($phrase_order_no > $self->getPhraseCount()) {
	die "Cannot select phrase " . $phrase_order_no
	     . ". There are only " . ($self->getPhraseCount()+1) . " phrases.";
    }

    if ($phrase_order_no < 0) {
	die "Cannot select phrase " . $phrase_order_no;
    }

    my $phraseRef = $self->{'phrases'};
    my @phrases = @$phraseRef;
    $phrases[$phrase_order_no] = $updated_phrase_object;
    $self->{'phrases'} = \@phrases;

    $self->updateParagraphText();

    return;
} # end updatePhrase()

#>#####################################################################
#> movePhrase()
#> FUNCTION:   Moves a phrase within a paragraph.
#> PARAMETERS: $curr_phrase_order - the current phrase order number of the
#>               phrase to be moved.
#>             $new_phrase_order - the new phrase order number of the phrase
#>               to be moved.
#> RETURNS:    (None)
#> NOTES:      This method will automatically update phrase order numbers as
#>               well as the PARAGRAPH_TEXT
#>             Phrase order numbers are zero-indexed.
#######################################################################
sub movePhrase {
#######################################################################
    my ($self, $curr_phrase_order, $new_phrase_order) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    # boolean flag to indicate if the phrase has been moved into its new
    #  position or not
    my $phrase_moved = 0;

    if ($curr_phrase_order == $new_phrase_order) {
	return;
    }

    if (($curr_phrase_order < 0) || ($new_phrase_order < 0)) {
	# this module is unable to move phrases to/from negative phrase
	#  order values

	return;
    }

    if ($new_phrase_order > $self->getPhraseCount()) {
	# if $new_phrase_order is greater than the number of phrases in the
	#   paragraph, then assume that the phrase is to be placed at then
	#   *end* of the paragraph, and renumber accordingly
	$new_phrase_order = $self->getPhraseCount();
    }

    my @removedPhrases = ();
    my $phrase_to_be_moved;

    my $phrase_order = 0;
    my $phrasesRef = $self->{'phrases'};
    my @phrases = @$phrasesRef;
    foreach my $phrase (@phrases) {
	if ($phrase_order == $curr_phrase_order) {
	    $phrase_to_be_moved = $phrase;
	}
	else {
	    push(@removedPhrases, $phrase);
	}
	$phrase_order++;
    }

    @phrases = ();

    $phrase_order = 0;
    foreach my $phrase (@removedPhrases) {
	if ($phrase_order == $new_phrase_order) {
	    push(@phrases, $phrase_to_be_moved);
	    $phrase_to_be_moved->setPhraseOrder($phrase_order);
	    $phrase_order++;
	    $phrase_moved = 1;
	}

	$phrase->setPhraseOrder($phrase_order);
	push(@phrases, $phrase);
	$phrase_order++;
    }
    if ($phrase_moved == 0) {
	$phrase_to_be_moved->setPhraseOrder($phrase_order);
	push(@phrases, $phrase_to_be_moved);
	$phrase_order++;
	$phrase_moved = 1;
    }

    $self->{'phrases'} = \@phrases;
    $self->updateParagraphText();

    return;
} # end movePhrase()

#>#####################################################################
#> getPhrase()
#> FUNCTION:   Retrieves a single phrase object from the paragraph
#> PARAMETERS: $phrase_order - the phrase order number of the
#>               phrase to be retrieved.
#> RETURNS:    PhraseDS object
#> NOTES:      Phrase order numbers are zero-indexed.
#######################################################################
sub getPhrase {
#######################################################################
    my ($self, $phrase_order) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    my $requestedPhraseObj;
    my $phrasesRef = $self->{'phrases'};
    my @phrases = @$phrasesRef;
    if ($phrase_order < @phrases) {
	foreach my $phrase (@phrases) {
	    if ($phrase->getPhraseOrder() == $phrase_order) {
		$requestedPhraseObj = $phrase;
		last;
	    }
	}
    }
    else {
	# phrase_order > the number of phrases in the paragraph
	die ("There are only " . (scalar @phrases) 
	     . " phrases in this paragraph; and phrase "
	     . $phrase_order . " was requested.");
    }

    return $requestedPhraseObj;
} # end getPhrase()

#>#####################################################################
#> getPhraseText()
#> FUNCTION:   Retrieves the phrase text of a single phrase from the paragraph
#> PARAMETERS: $phrase_order - the phrase order number of the
#>               phrase whose text is to be retrieved.
#> RETURNS:    string - the phrase text
#> NOTES:      Phrase order numbers are zero-indexed.
#######################################################################
sub getPhraseText {
#######################################################################
    my ($self, $phrase_order) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    my $phraseText;
    my $phrasesRef = $self->{'phrases'};
    my @phrases = @$phrasesRef;
    if ($phrase_order < @phrases) {
	$phraseText = $phrases[$phrase_order]->getPhraseText();
    }
    else {
	# phrase number does not exist!
    }

    return $phraseText;
} # end getPhraseText()

#>#####################################################################
#> getPhraseText()
#> FUNCTION:   Retrieves the phrase text of a single phrase from the paragraph
#> PARAMETERS: $phrase_order - the phrase order number of the
#>               phrase whose text is to be retrieved.
#> RETURNS:    string - the phrase text
#> NOTES:      Phrase order numbers are zero-indexed.
#######################################################################
sub getPhrases {
#######################################################################
    my ($self) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    my $phrasesRef = $self->orderPhrases();
    return $phrasesRef;
} # end getPhrases()

#>#####################################################################
#> getPhraseCount()
#> FUNCTION:   Counts the number of phrases in the current paragraph
#> PARAMETERS: (None)
#> RETURNS:    number of phrases in the current paragraph.
#> NOTES:      
#######################################################################
sub getPhraseCount {
#######################################################################
    my ($self) = @_;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }
    my $phrasesRef = $self->getPhrases();
    my @phrases = @$phrasesRef;
    return scalar @phrases;
} # end getPhraseCount()

#>#####################################################################
#> orderPhrases()
#> FUNCTION:   This method puts the phrase_objects (stored in 
#>               $self->{'phrases'}) in order by their phrase_order
#> PARAMETERS: (None)
#> RETURNS:    array reference - all phrase objects of this paragraph in order
#> NOTES:      
#######################################################################
sub orderPhrases {
#######################################################################
    my ($self) = @_;

    my $phrasesRef = $self->{'phrases'};
    my @phrases = @$phrasesRef;

    my @orderedPhrases = ();
    foreach my $phraseObj (@phrases) {
	$orderedPhrases[$phraseObj->getPhraseOrder()] = $phraseObj;
    }

    @phrases = ();
    for (my $index = 0; $index < scalar @orderedPhrases; $index++) {
	if (defined $orderedPhrases[$index]) {
	    push(@phrases, $orderedPhrases[$index]);
	}
    }
    $self->{'phrases'} = \@phrases;

    return \@phrases;
} # end orderPhrases()

#>#####################################################################
#> getPhraseTexts()
#> FUNCTION:   this method iterates through each phrase and returns an 
#>               array containing ($phrase_no, $phrase_text) for each 
#>               phrase.  This array is ordered by the phrase_order value
#>                of each phrase.
#> PARAMETERS: (None)
#> RETURNS:    array reference - containing ($phrase_no, $phrase_text) entry
#>               for each phrase in the paragraph.  The array is ordered by
#>               the phrase_order_number of each phrase.
#> NOTES:      
#######################################################################
sub getPhraseTexts{
#######################################################################
    my ($self) = @_;

    my $phraseRef = $self->getPhrases();
    my @phrases = @$phraseRef;

    my @returnArray;

    foreach my $phraseObj (@phrases) {
	my @phraseRow = ($phraseObj->getPhraseNo(), 
			 $phraseObj->getPhraseText(),
			 $phraseObj->getPhraseCategories());
	push(@returnArray, \@phraseRow);
    }

    return \@returnArray;;
} # end getPhraseTexts()

#>#####################################################################
#> getGOTerms()
#> FUNCTION:   Retrieves the GO terms used by all phrases of this paragraph.
#> PARAMETERS: (None)
#> RETURNS:    hash reference - the key of the hash is the GO ID of the GO
#>               term.  The value of the hash key is always 1.
#> NOTES:      The list of GO terms does not contain any duplicates.
#######################################################################
sub getGOTerms {
#######################################################################
    my ($self) = @_;

    my %GOTerms;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    my $phrasesRef = $self->{'phrases'};
    my @phrases = @$phrasesRef;
    foreach my $phrase (@phrases) {
	my $goTermRef = $phrase->getGOTerms();
	foreach my $goID (@$goTermRef) {
	    $GOTerms{$goID} = 1;
	}
    }

    return \%GOTerms;
} # end getGOTerms()

#>#####################################################################
#> getSecondaryLoci()
#> FUNCTION:   Retrieves all secondary loci associated the phrases of
#>               the paragraph.
#> PARAMETERS: (None)
#> RETURNS:    hash reference - the key of the hash is the LOCUS_NO of the
#>               secondary loci.  The value of the hash key is always 1.
#> NOTES:      The list of secondary loci does not contain any duplicates.
#######################################################################
sub getSecondaryLoci {
#######################################################################
    my ($self) = @_;

    my %secondaryLoci;

    if ($self->{'loadTbl_phrase'} == 0) {
	$self->loadPhraseTableData();
    }

    my $phrasesRef = $self->{'phrases'};
    my @phrases = @$phrasesRef;
    foreach my $phrase (@phrases) {
	my $secondaryLociRef = $phrase->getSecondaryLoci();
	foreach my $locus_no (@$secondaryLociRef) {
	    $secondaryLoci{$locus_no} = 1;
	}
    }

    return \%secondaryLoci;
} # end getSecondaryLoci()

#>#####################################################################
#> getPrimaryLoci()
#> FUNCTION:   Retrieves all primary loci associated with a paragraph.
#> PARAMETERS: (None)
#> RETURNS:    hash reference - the key of the hash is the LOCUS_NO of the
#>               primary locus term.  The value of the hash key is always 1.
#> NOTES:      
#######################################################################
sub getPrimaryLoci {
#######################################################################
    my ($self) = @_;

    if ($self->{'loadTbl_locus'} == 0) {
	$self->loadLocusTableData();
    }

    return $self->{'primaryLoci'};
} # end getPrimaryLoci()

#>#####################################################################
#> setPrimaryLoci()
#> FUNCTION:   Sets the LOCUS.PARAGRAPH_NO of the specified locus to
#>               be the PARAGRAPH_NO of the current paragraph
#> PARAMETERS: $locus_no - the LOCUS_NO of the locus to be set as the
#>               primary locus.
#> RETURNS:    (None)
#> NOTES:      This method overwrites the LOCUS.PARAGRAPH_NO value,
#>               regardless of what was there before.  However, changes
#>               will not be writtem to the database until update() is called.
#######################################################################
sub setPrimaryLoci {
#######################################################################
    my ($self, $locus_no) = @_;

    if ($self->{'loadTbl_locus'} == 0) {
	$self->loadLocusTableData();
    }

    my $primaryLociRef = $self->{'primaryLoci'};
    my %primaryLoci = %$primaryLociRef;
    $primaryLoci{$locus_no} = 1;
    $self->{'primaryLoci'} = \%primaryLoci;

    # look for occurrences where this locus_no is mentioned as a secondary
    #   locus, and rename it to a primary locus
    my $phrasesRef = $self->getPhrases();
    my @phrases = @$phrasesRef;

    my @modifiedPhrases = ();
    foreach my $phrase (@phrases) {
	my $phraseText = $phrase->getPhraseText();
	if ($phraseText =~ m/\<locus02:$locus_no\>/) {
	    $phraseText =~ s/\<locus02:$locus_no/\<locus01:$locus_no/g;
	    $phrase->setPhraseText($phraseText);
	    $phrase->unsetSecondaryLoci($locus_no);
	}
	else {
	}
	push(@modifiedPhrases, $phrase);
    }
    $self->{'phrases'} = \@phrases;

    return;
} # end setPrimaryLoci

#>#####################################################################
#> unsetPrimaryLoci()
#> FUNCTION:   Sets the LOCUS.PARAGRAPH_NO of the specified locus to
#>               be an empty value
#> PARAMETERS: $locus_no - the LOCUS_NO of the locus to be unset as a
#>               primary locus.
#> RETURNS:    (None)
#> NOTES:      This method overwrites the LOCUS.PARAGRAPH_NO value,
#>               regardless of what was there before.  However, changes
#>               will not be written to the database until update() is called.
#######################################################################
sub unsetPrimaryLoci {
#######################################################################
    my ($self, $locus_no) = @_;

    if ($self->{'loadTbl_locus'} == 0) {
	$self->loadLocusTableData();
    }

    my $primaryLociRef = $self->{'primaryLoci'};
    my %primaryLoci = %$primaryLociRef;
    if (defined $primaryLoci{$locus_no}) {
	delete $primaryLoci{$locus_no};
    }
    $self->{'primaryLoci'} = \%primaryLoci;

    # look for occurrences where this locus_no is mentioned as a primary
    #   locus, and rename it to a secondary locus
    my $phrasesRef = $self->getPhrases();
    my @phrases = @$phrasesRef;

    my @modifiedPhrases = ();
    foreach my $phrase (@phrases) {
	my $phraseText = $phrase->getPhraseText();
	if ($phraseText =~ m/\<locus01:$locus_no\>/) {
	    $phraseText =~ s/\<locus01:$locus_no/\<locus02:$locus_no/g;
	    $phrase->setPhraseText($phraseText);
	}
	push(@modifiedPhrases, $phrase);
    }
    $self->{'phrases'} = \@phrases;

    return;
} # end unsetPrimaryLocus

#>#####################################################################
#> isPrimaryLocus()
#> FUNCTION:   Given a primary locus, determines it is a primary locus
#>               for the current paragraph or not.
#> PARAMETERS: $locus_no - the LOCUS_NO of the locus to be checked if
#>               it lists this paragraph as its paragraph or not.
#> RETURNS:    1 if this locus is associated with this paragraph.
#>             0 otherwise
#> NOTES:      
#######################################################################
sub isPrimaryLocus {
#######################################################################
    my ($self, $locus_no) = @_;

    if ($self->{'loadTbl_locus'} == 0) {
	$self->loadLocusTableData();
    }

    my $primaryLociRef = $self->{'primaryLoci'};
    my %primaryLoci = %$primaryLociRef;
    if (defined $primaryLoci{$locus_no}) {
	return 1;
    }
    else {
	return 0;
    }
} # end isPrimaryLocus()

#>#####################################################################
#> getColleagues()
#> FUNCTION:   Retrieves a list of colleagues associated with this paragraph
#> PARAMETERS: (None)
#> RETURNS:    hash reference - the key of the hash is the COLLEAGUE_NO of the
#>               colleague.  The value of the hash key is always 1.
#> NOTES:      
#######################################################################
sub getColleagues {
#######################################################################
    my ($self) = @_;

    if ($self->{'loadTbl_collPara'} == 0) {
	$self->loadCollParaTableData();
    }

    return $self->{'colleagues'};
} # end getColleagues()

#>#####################################################################
#> setPrimaryLoci()
#> FUNCTION:   Creates an entry in the COLL_PARA table that associates
#>               the specified colleague with the current locus.
#> PARAMETERS: $colleague_no - the COLLEAGUE_NO of the colleague to be'
#>               associated with this paragraph.
#> RETURNS:    (None)
#> NOTES:      However, changes will not be writtem to the database until
#>               update() is called.
#######################################################################
sub setColleague {
#######################################################################
    my ($self, $colleague_no) = @_;

    if ($self->{'loadTbl_collPara'} == 0) {
	$self->loadCollParaTableData();
    }

    my $colleaguesRef = $self->{'colleagues'};
    my %colleagues = %$colleaguesRef;
    $colleagues{$colleague_no} = 1;
    $self->{'colleagues'} = \%colleagues;
    return;
} # end setColleague()

#>#####################################################################
#> unsetColleague()
#> FUNCTION:   Removes a vlue from COLL_PARA that associates the specified
#>               colleague with the current paragraph.
#> PARAMETERS: $colleague_no - the COLLEAGUE_NO of the colleague to be
#>               de-associated (dissociated?) with this paragraph
#> RETURNS:    (None)
#> NOTES:      If a colleague_no is passed to this method that is not currently
#>               associated with this paragraph, nothing will happen.  
#>             Changes will not be written to the database until update() 
#>               is called.
#######################################################################
sub unsetColleague {
#######################################################################
    my ($self, $colleague_no) = @_;

    if ($self->{'loadTbl_collPara'} == 0) {
	$self->loadCollParaTableData();
    }

    my $colleaguesRef = $self->{'colleagues'};
    my %colleagues = %$colleaguesRef;
    if (defined $colleagues{$colleague_no}) {
	delete $colleagues{$colleague_no};
    }
    $self->{'colleagues'} = \%colleagues;
    return;
} # end unsetColleague()

#>#####################################################################
#> isColleague()
#> FUNCTION:   Given a COLLEAGUE_NO value, determines if the current colleague
#>               is associated with the current paragraph via COLL_PARA table.
#> PARAMETERS: $colleague_no - the COLLEAGUE_NO of the colleague to be
#>               checked if it is associated with this paragraph or not.
#> RETURNS:    1 if this colleague is associated with this paragraph.
#>             0 otherwise
#> NOTES:
#######################################################################
sub isColleague {
#######################################################################
    my ($self, $colleague_no) = @_;

    if ($self->{'loadTbl_collPara'} == 0) {
	$self->loadCollParaTableData();
    }

    my $colleaguesRef = $self->{'colleagues'};
    my %colleagues = %$colleaguesRef;
    if (defined $colleagues{$colleague_no}) {
	return 1;
    }
    else {
	return 0;
    }
} # end isColleague()

#>#####################################################################
#> getDateWritten()
#> FUNCTION:   Retrieves the DATE_WRITTEN field from CGM_DDB.PARAGRAPH
#> PARAMETERS: (None)
#> RETURNS:    string representing the date this entry was written.
#>               primary locus term.  The value of the hash key is always 1.
#> NOTES:      
#######################################################################
sub getDateWritten {
#######################################################################
    my ($self) = @_;

    return $self->{'date_written'};
} # end getDateWritten()

#>#####################################################################
#> setDateWritten()
#> FUNCTION:   Set the DATE_WRITTEN field from CGM_DDB.PARAGRAPH
#> PARAMETERS: $new_date_written - the date on which the paragraph was
#>               was written, in YYYY-MM-DD format.
#> RETURNS:    (None)
#> NOTES:      The script will check to ensure that the date is in
#>               YYYY-MM-DD format before it is committed to the database
#>             This method assumes that the year value is between 1950 and
#>               2050.
#######################################################################
sub setDateWritten {
#######################################################################
    my ($self, $new_date_written) = @_;

    my $dateFormatError = 0;
    if ($new_date_written =~ /^(\d{4})-(\d{2})-(\d{2})$/) {

	my $year = $1;
	my $month = $2;
	my $day = $3;

	if (! (($year > 1950) && ($year < 2050))) {
	    $dateFormatError = 1;
	}

	if (! (($month > 0) && ($month <= 12))) {
	    $dateFormatError = 1;
	}

	if (! (($day > 0) && ($day <= 31))) {
	    $dateFormatError = 1;
	}
    }
    else {
	$dateFormatError = 1;
    }

    if ($dateFormatError == 0) {
	$self->{'date_written'} = $new_date_written;
    }

    return;
} # end setDateWritten()

#>#####################################################################
#> getWrittenBy()
#> FUNCTION:   Gets the WRITTEN_BY field from CGM_DDB.PARAGRAPH
#> PARAMETERS: (None)
#> RETURNS:    string - with the Oracle username of the person who wrote
#>               the current paragraph.
#> NOTES:      
#######################################################################
sub getWrittenBy {
#######################################################################
    my ($self) = @_;

    return $self->{'written_by'};
} # end getWrittenBy()

#>#####################################################################
#> setWrittenBy()
#> FUNCTION:   Set the WRITTEN_BY field from CGM_DDB.PARAGRAPH
#> PARAMETERS: $new_written_by - the Oracle username of the original author
#>               of paragraph
#> RETURNS:    (None)
#> NOTES:      This code does not check the validity of  Oracle usernames
#>               submitted into the database
#######################################################################
sub setWrittenBy {
#######################################################################
    my ($self, $new_written_by) = @_;

    $self->{'written_by'} = $new_written_by;

    return;
} # setWrittenBy()

#>#####################################################################
#> getLastModifiedDate()
#> FUNCTION:   Gets the last modified date of this entry, the latter
#>               of DATE_WRITTEN and DATE_CREATED, depending on their dates.
#> PARAMETERS: (None)
#> RETURNS:    string - date of last modification, in YYYY-MM-DD format.
#> NOTES:      
#######################################################################
sub getLastModifiedDate {
#######################################################################
    my ($self) = @_;

    my $lastModificationDate = "";

    my $lastModifiedRef;
    if (defined $self->{'paragraph_row'}) {
	$lastModifiedRef = $self->{'paragraph_row'}->updatesRef();
    }
    else {
	# this case occurs when dealing with new paragraph objects that 
	#   have not yet been committed into the database
	my @nullArray = ();
	$lastModifiedRef = \@nullArray;
    }

    my @lastModified = @$lastModifiedRef;

    if (scalar @lastModified != 0) {
	(my $lastModificationDateRef, undef)
	     = @$lastModifiedRef[scalar @lastModified-1];

	(undef, $lastModificationDate, undef) = @$lastModificationDateRef;
    }
    else {
	$lastModificationDate = $self->{'date_written'};
    }
    return $lastModificationDate;
} # end getLastModifiedDate()

#>#####################################################################
#> getLastModifiedBy()
#> FUNCTION:   Gets the last modified person of this entry, the latter
#>               of WRITTEN_BY and CREATED_BY, depending on their dates.
#> PARAMETERS: (None)
#> RETURNS:    string - with the Oracle username of the person who wrote
#>               the current paragraph.
#> NOTES:      
#######################################################################
sub getLastModifiedBy {
#######################################################################
    my ($self) = @_;

    my $lastModificationBy = "";

    my $lastModifiedRef;
    if (defined $self->{'paragraph_row'}) {
	$lastModifiedRef = $self->{'paragraph_row'}->updatesRef();
    }
    else {
	# this case occurs when dealing with new paragraph objects that 
	#   have not yet been committed into the database
	my @nullArray = ();
	$lastModifiedRef = \@nullArray;
    }


    my @lastModified = @$lastModifiedRef;

    # take the later of: (a) date_created or (b) last_modified date
    if (scalar @lastModified != 0) {
	(my $lastModificationByRef, undef)
	     = @$lastModifiedRef[scalar @lastModified-1];

	($lastModificationBy, undef) = @$lastModificationByRef;
    }
    else {
	$lastModificationBy = $self->{'written_by'};
    }
    return $lastModificationBy;
} # end getLastModifiedBy()

#>#####################################################################
#> getLastModifiedBy()
#> FUNCTION:   Gets the last modified person of this entry, the latter
#>               of WRITTEN_BY and CREATED_BY, depending on their dates.
#> PARAMETERS: (None)
#> RETURNS:    string - with the Oracle username of the person who wrote
#>               the current paragraph.
#> NOTES:      
#######################################################################
sub getModificationHistory {
#######################################################################
    my ($self) = @_;

    return $self->{'paragraph_row'}->updatesRef();
} # end getModificationHistory()

#>#####################################################################
#> validate()
#> FUNCTION:   validates the current paragraph to ensure that everything
#>               is a-OK before attempting to write it into the database.
#> PARAMETERS: (None)
#> RETURNS:    0 if everything is okay with the paragraph.
#>             string - error messages if there are any problems with
#>               the paragraph, describing what the error messages are
#> NOTES:      This method will recursively call the PhraseDS.pm::validate()
#>               method for each phrase in this paragraph to ensure that
#>               it is a valid phrase.
#######################################################################
sub validate {
#######################################################################
    my ($self) = @_;

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

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

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

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

    if (0) {  # this check is being disabled, as orphaned paragraphs are 
	      # now being permitted in the database.

	      # Note: there is a curator tool which can locate and
	      #       delete orphaned paragraphs
	my $primaryLociRef = $self->getPrimaryLoci();
	if (!defined $primaryLociRef) {
	    $errorCount++;
	    $errorMsg .= "No primary loci assigned to this paragraph. (1)\n";
	}
	else {
	    my %primaryLoci = %$primaryLociRef;

	    if (scalar keys %primaryLoci == 0) {
		$errorCount++;
		$errorMsg .= "No primary loci assigned to this paragraph. (2)\n";
	    }
	}
    }

    my $phrasesRef = $self->getPhrases();
    if (!defined $phrasesRef) {
	$errorCount++;
	$errorMsg .= "No phrases assigned to this paragraph. (1)\n";
    }
    else {
	my @phrases = @$phrasesRef;

	if (scalar @phrases == 0) {
	    $errorCount++;
	    $errorMsg .= "No phrases assigned to this paragraph. (2)\n";
	}
	else {
	}
    }

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

    return $returnCode;
} # end validate()

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







































