package gov.va.vinci.dart.biz;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import gov.va.vinci.dart.common.ValidationHelper;
import gov.va.vinci.dart.common.exception.ObjectNotFoundException;
import gov.va.vinci.dart.common.exception.ValidationException;
import gov.va.vinci.dart.dms.biz.Document;
import gov.va.vinci.dart.service.DartObjectFactory;
import gov.va.vinci.dart.wf2.WorkflowException;
import gov.va.vinci.dart.wf2.WorkflowResolver;

@Entity
@Table(name = "Request", schema = "hib")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "requesttype", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue("1")
public abstract class Request extends BusinessObject {

    /** The log. */
    private static Log log = LogFactory.getLog(Request.class);

    /** The Constant DATA_ACCESS. */
    public static final String DATA_ACCESS = "Research Data Access";

    /** The Constant OPERATIONS_DATA_ACCESS. */
    public static final String OPERATIONS_DATA_ACCESS = "Operations Data Access";

    /** The Constant PREPARATORY_TO_RESEARCH_ACCESS. */
    public static final String PREPARATORY_TO_RESEARCH_ACCESS = "Preparatory to Research Access";

    /** The status. */
    @Column(name = "state")
    private int status;

    /** The tracking number. */
    @Column(name = "trackingnumber")
    protected String trackingNumber;

    /** The amendment. */
    @Column(name = "amendment", columnDefinition = "BIT", length = 1)
    protected boolean amendment;

    /** The head id. */
    // the id of the first request in a chain of related changes. (0 if there is no previous request)
    @Column(name = "headid")
    protected int headId;

    /** The previous id. */
    // id of the previous request if a change has occurred
    @Column(name = "previousid")
    private int previousId;

    /** The type. */
    @Column(name = "type")
    protected String type;

    /** The request type. */
    @Column(name = "requesttype", insertable = false, updatable = false)
    protected int requestType;

    /** The current. */
    // true if this request is the most current of those that have been modified.
    @Column(name = "currentflag", columnDefinition = "BIT", length = 1)
    protected boolean current;

    /** The irb number. */
    @Column(name = "irbnumber")
    protected String irbNumber;

    /** The irb expiration. */
    @Temporal(TemporalType.DATE)
    @Column(name = "irbexpiration")
    protected Date irbExpiration;

    /** The workflow id. */
    @Column(name = "workflowid")
    protected int workflowId; // workflow type

    /** The workflow state. */
    @Column(name = "workflowstate")
    protected int workflowState;

    /** The workflow mask. */
    @Column(name = "workflowmask")
    protected long workflowMask;

    /** The requestor. */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "requestorid")
    protected Person requestor;

    /** The activity. */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "activityid")
    protected Activity activity;

    /** The participants. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    Set<Participant> participants;

    /** The reviews. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    protected Set<Review> reviews;

    /** The workflows. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    protected Set<RequestWorkflow> workflows;

    /** The documents. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    protected Set<Document> documents;

    /** The events. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    @OrderBy("createdOn DESC")
    protected List<Event> events;

    /** The narratives. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    protected List<Narrative> narratives;

    /** The primary location. */
    // the primary location for the activity
    @ManyToOne
    @JoinColumn(name = "primarysiteid")
    protected Location primaryLocation;

    /** The sites. */
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "requestlocation", schema = "hib", joinColumns = { @JoinColumn(
            name = "requestid", referencedColumnName = "ID") }, inverseJoinColumns = { @JoinColumn(
            name = "locationid", referencedColumnName = "ID") })
    protected Set<Location> sites;

    /** The online datas. */
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "requestonlinedata", schema = "hib", joinColumns = { @JoinColumn(
            name = "requestid", referencedColumnName = "ID") }, inverseJoinColumns = { @JoinColumn(
            name = "onlinedataid", referencedColumnName = "ID") })
    protected Set<OnlineData> onlineDatas;

    /** The comments. */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "request")
    protected Set<Comment> comments;

    /** The submitted on. */
    @Column(name = "submittedon")
    protected Date submittedOn;
    
    @Transient
    protected boolean processWithdraw = false;



    Request() {
    }

    /**
     * Find by id.
     *
     * @param requestId
     *            the request id
     * @return the request
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public static Request findById(final int requestId) throws ObjectNotFoundException {
        return DartObjectFactory.getInstance().getRequestDAO().findById(requestId);
    }


    /**
     * List by tracking number.
     *
     * @param trackingNumber
     *            the tracking number
     * @return the list
     */
    public static List<RequestSummary> listByTrackingNumber(final String trackingNumber) {
        return DartObjectFactory.getInstance().getRequestDAO().listByTrackingNumber(trackingNumber);
    }

    /**
     * Find by tracking number.
     *
     * @param trackingNumber
     *            the tracking number
     * @return the request
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public static Request findByTrackingNumber(final String trackingNumber) throws ObjectNotFoundException {
        return DartObjectFactory.getInstance().getRequestDAO().findByTrackingNumber(trackingNumber);
    }

    /**
     * Gets the requestor.
     *
     * @return the requestor
     */
    public Person getRequestor() {
        return requestor;
    }

    // public String getType() {
    // return type;
    // }

    /**
     * Gets the request type.
     *
     * @return the request type
     */
    public int getRequestType() {
        return requestType;
    }
    
    public String getRequestTypeString(){
        if (this.requestType == 5){
            return PREPARATORY_TO_RESEARCH_ACCESS;
        } else if (this.requestType == 2){
            return DATA_ACCESS;
        }
        else{
            return OPERATIONS_DATA_ACCESS;
        }
    }

    /**
     * Gets the data sources.
     *
     * @return the data sources
     */
    // overridden in the derived classes
    public Set<DataSource> getDataSources() {
        return (new HashSet<DataSource>());
    }

    /**
     * List all but initiated.
     *
     * @return the list
     */
    public static List<RequestSummary> listAllButInitiated() {
        return DartObjectFactory.getInstance().getRequestDAO().listAllButInitiated();
    }

    /**
     * List all but initiated without contact name.
     *
     * Only reference is in TestRquest - test two currently - marked Deprecated for now.
     * @return the list
     */
    @Deprecated
    public static List<RequestSummary> listAllButInitiatedWithoutContactName() {
        return DartObjectFactory.getInstance().getRequestDAO().listAllButInitiatedWithoutContactName();
    }

    /**
     * List all request summary.
     *
     * @return the list
     */
    public static List<RequestSummary> listAllRequestSummary() {
        return DartObjectFactory.getInstance().getRequestDAO().listAllRequestSummary();
    }

    /**
     * List all request summary by participant.
     *
     * @param participantId
     *            the participant id
     * @return the list
     */
    public static List<RequestSummary> listAllRequestSummaryByParticipant(final String participantId) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllRequestSummaryByParticipant(participantId);
    }

    /**
     * List all request summary without contact name.
     *
     * Only referenced by TestRequest - marking Deprecated for now.
     * @return the list
     */
    @Deprecated
    public static List<RequestSummary> listAllRequestSummaryWithoutContactName() {
        return DartObjectFactory.getInstance().getRequestDAO().listAllRequestSummaryWithoutContactName();
    }

    /**
     * List all user reviewable.
     *
     * @param userId
     *            the user id
     * @return the list
     */
    @Deprecated
    public static List<RequestSummary> listAllUserReviewable(final int userId) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllUserReviewable(userId);
    }

    /**
     * List all group reviewable.
     *
     * @param groupName
     *            the group name
     * @return the list
     */
    public static List<RequestSummary> listAllGroupReviewable(final String groupName) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllGroupReviewable(groupName);
    }

    /**
     * List all request by head id.
     *
     * @param headId
     *            the head id
     * @return the list
     */
    /*------------------------------------------------------------------------------------------
     * --------------------------------------------------------------*/
    public static List<Request> listAllRequestByHeadId(final int headId) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllRequestByHeadId(headId);
    }

    /**
     * Retrieves the list of previous requests in the chain, sorted by most recently created first.
     * 
     * Returns null if no Requests are retrieved.
     *
     * @param headId
     *            the head id
     * @param createdOn
     *            the created on
     * @return the list
     */
    public static List<Request> listAllPreviousRequests(final int headId, final Date createdOn) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllPreviousRequests(headId, createdOn);
    }

    /**
     * Retrieves the list of previous requests in the chain, sorted by most recent tracking number first.
     * 
     * Returns null if no Requests are retrieved.
     *
     * @param headId the head id
     * @param trackingNumberStr the tracking number str
     * @return the list
     */
    public static List<Request> listAllPreviousRequests(final int headId, final String trackingNumberStr) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllPreviousRequests(headId, trackingNumberStr);
    }

    /**
     * Retrieves the list of previous request IDs (request ID only) in the chain, sorted by most recent tracking number first.
     * 
     * Returns null if no request IDs are retrieved.
     *
     * @param headId the head id
     * @param trackingNumberStr the tracking number str
     * @return the list
     */
    public static List<Integer> listAllPreviousRequestIds(final int headId, final String trackingNumberStr) {
        return DartObjectFactory.getInstance().getRequestDAO().listAllPreviousRequestIds(headId, trackingNumberStr);
    }

    // public static Request getPreviousRequestByStatus(final int headId, final String trackingNumberStr, final RequestStatus
    // status) {
    // return DartObjectFactory.getInstance().getRequestDAO().getPreviousRequestByStatus(headId, trackingNumberStr, status);
    // }

    // public static Request getPreviousApprovedRequest(final int headId, final String trackingNumberStr) {
    // return DartObjectFactory.getInstance().getRequestDAO().getPreviousApprovedRequest(headId, trackingNumberStr);
    // }

    // public static Request getRequestWithPreviousApprovedWorkflow(final int headId, final String trackingNumberStr, final int
    // workflowTypeId) {
    // return DartObjectFactory.getInstance().getRequestDAO().getRequestWithPreviousApprovedWorkflow(headId, trackingNumberStr,
    // workflowTypeId);
    // }

    // public static RequestWorkflow getPreviousApprovedWorkflowByType(final int headId, final String trackingNumberStr, final
    // int workflowTypeId) {
    // return DartObjectFactory.getInstance().getRequestDAO().getPreviousApprovedWorkflowByType(headId, trackingNumberStr,
    // workflowTypeId);
    // }

    /**
     * List request location document summary by reviewer.
     *
     * @param requestId
     *            the request id
     * @param reviewer
     *            the reviewer
     * @return the list
     */
    public static List<RequestLocationDocumentSummary> listRequestLocationDocumentSummaryByReviewer(final int requestId,
            final String reviewer) {
        return DartObjectFactory.getInstance().getRequestDAO()
                .listRequestLocationDocumentSummaryByReviewer(requestId, reviewer);
    }

    /**
     * List request participant document summary by reviewer.
     *
     * @param requestId
     *            the request id
     * @param reviewer
     *            the reviewer
     * @return the list
     */
    public static List<RequestParticipantDocumentSummary> listRequestParticipantDocumentSummaryByReviewer(final int requestId,
            final String reviewer) {
        return DartObjectFactory.getInstance().getRequestDAO()
                .listRequestParticipantDocumentSummaryByReviewer(requestId, reviewer);
    }

    /**
     * Constructor.
     *
     * @param createdBy            userID that created the request.
     * @throws ValidationException the validation exception
     */
    Request(final String createdBy) throws ValidationException {
        trackingNumber = generateTrackingNumber("D");
        setStatus(RequestStatus.INITIATED, createdBy);

        EventType.initialize();
        Group.initialize();

        Event.create(EventType.INITIATE_REQUEST, this, createdBy);
    }

    /**
     * Generate a tracking number for the request.
     *
     * @param requestType            Either 'D' for data request, or 'A' for amendment.
     * @return the string
     */
    protected String generateTrackingNumber(final String requestType) {
        Formatter fmt = new Formatter();
        String result = null;

        try {
            int cnt = DartObjectFactory.getInstance().getRequestDAO().countRequestsInMonth();

            // nah, this isn't magic at all
            fmt.format("%1$tY-%1$tm-%2$03d-%3$s", new Date(), cnt + 1, requestType);

            result = fmt.toString();
        } finally {
            fmt.close();
        }
        return result;
    }

    /**
     * Generate an amendment tracking number.
     *
     * @param baseRequest the base request
     * @return the string
     */
    protected String generateAmendmentTrackingNumber(final Request baseRequest) {
        Formatter fmt = null;
        String result = null;

        try {
            // since this is called *AFTER* the amendment has been inserted in the database, cnt will start at 1
            int cnt = DartObjectFactory.getInstance().getRequestDAO().countRequestAmendments(baseRequest.getId());

            String[] TrackNum = baseRequest.getTrackingNumber().split("\\(");

            // make sure that the tracking number increases (verify that it's after the last tracking number)

            boolean newTrackingNumber = false;
            while (!newTrackingNumber) {
                fmt = new Formatter();

                if (TrackNum.length > 1) {
                    fmt.format("%1$s-A%2$02d (%3$s", TrackNum[0].trim(), cnt, TrackNum[1]);
                } else {
                    fmt.format("%1$s-A%2$02d", baseRequest.getTrackingNumber(), cnt);
                }
                // nah, this isn't magic at all
                // fmt.format("%1$s-A%2$02d", baseRequest.getTrackingNumber(), cnt);

                result = fmt.toString();

                // TODO: might want to do a "like" match on the base tracking number:
                // Request.listByTrackingNumber
                try {
                    Request request = findByTrackingNumber(result);
                    if (request == null)
                        newTrackingNumber = true;
                } catch (ObjectNotFoundException e) {
                    newTrackingNumber = true; // this request doesn't exist yet
                }

                cnt++;
                fmt.close();
            }// end while
        } finally {
            if (fmt != null) {
                fmt.close();
            }
        }

        return result;
    }

    /**
     * Create an amendment from a request. This object will be the amendment of the given request.
     *
     * @param headRequest
     *            the original request in the amendment chain.
     * @return the request
     */
    protected Request createAmendment(final Request headRequest) {

        this.amendment = true;
        this.headId = headRequest.getId();
        this.current = true;

        return this;
    }

    /**
     * set the status of the request to INITIATED.
     *
     * @param userId
     *            the user id
     * @throws ValidationException
     *             the validation exception
     */
    protected void initiate(final String userId) throws ValidationException {
        setStatus(RequestStatus.INITIATED, userId);
    }

    /**
     * Close the request.
     *
     * @param userId
     *            the user id
     * @throws ValidationException
     *             the validation exception
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public void close(final String userId) throws ValidationException, ObjectNotFoundException {

        if (RequestStatus.CLOSED.getId() == getStatus().getId()) {
            return;
        }

        setStatus(RequestStatus.CLOSED, userId);

        EventType.initialize();
        Event.create(EventType.CLOSE_REQUEST, this, userId); // no group for this event
    }

    /**
     * Request the submitter add a change to the request.
     *
     * @param userId
     *            the user id
     * @throws ValidationException
     *             the validation exception
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public void requestChange(final String userId) throws ValidationException, ObjectNotFoundException {

        if (RequestStatus.CHANGE_REQUESTED.getId() == getStatus().getId()) { // top-level status
            return;
        }

        setStatus(RequestStatus.CHANGE_REQUESTED, userId);
    }

    /**
     * Approve the request.
     *
     * @param workflow
     *            the workflow
     * @param userId
     *            the user id
     * @throws ValidationException
     *             the validation exception
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public void approve(final RequestWorkflow workflow, final String userId) throws ValidationException,
            ObjectNotFoundException {

        if (RequestStatus.APPROVED.getId() == getStatus().getId()) { // old NDS request
            return;
        }

        if (RequestStatus.CLOSED.getId() == getStatus().getId() || RequestStatus.DENIED.getId() == getStatus().getId()) {
            throw new ValidationException("Cannot approve request in current state.");
        }

        // cannot approve request with a rejected review.
        if (!allReviewsApproved(workflow)) {
            throw new ValidationException("Cannot approve request with a denied review.");
        }

        // TODO lock the current version numbers of documents associated with this request.

        setStatus(RequestStatus.APPROVED, userId);
    }

    /**
     * Deny the request.
     *
     * @param userId
     *            the user id
     * @throws ValidationException
     *             the validation exception
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public void reject(final String userId) throws ValidationException, ObjectNotFoundException {

        if (RequestStatus.DENIED.getId() == getStatus().getId()) { // old NDS request
            return;
        }

        if (RequestStatus.CLOSED.getId() == getStatus().getId() || RequestStatus.APPROVED.getId() == getStatus().getId()) {
            throw new ValidationException("Cannot reject request in current state.");
        }

        setStatus(RequestStatus.DENIED, userId);
    }

    /**
     * Submit the request for review.
     *
     * @param userId
     *            the user id
     * @throws Exception
     *             the exception
     */
    public void submit(final String userId) throws Exception {

        setStatus(RequestStatus.SUBMITTED, userId);
        this.submittedOn = new Date();
    }

    /**
     * Mark the request as completed.
     *
     * @param userId
     *            the user id
     * @throws ValidationException
     *             the validation exception
     * @throws ObjectNotFoundException
     *             the object not found exception
     */
    public void complete(final String userId) throws ValidationException, ObjectNotFoundException {

        if (RequestStatus.REQUEST_COMPLETED.getId() == getStatus().getId()) {
            return;
        }

        // request is not completed until all workflows are completed
        if (allWorkflowsCompleted() == false) {
            throw new ValidationException("Cannot mark request as completed until all workflows have completed.");
        }

        // TODO lock the current version numbers of documents associated with this request.

        setStatus(RequestStatus.REQUEST_COMPLETED, userId);
    }

    /**
     * Returns true if the specified person is the request creator or a participant.
     *
     * @param person
     *            the person
     * @return true, if successful
     */
    public boolean hasCreatorOrParticipant(final Person person) {

        if (person != null) {

            String creatorName = this.getCreatedBy();
            if (creatorName != null && person.getName() != null 
                    && creatorName.trim().equalsIgnoreCase(person.getName().trim())) {
                return true;
            }// end if

            for (Participant participant : this.getParticipants()) {
                if (participant != null) {
                    if (participant.getPerson() != null && participant.getPerson().getId() == person.getId()) {
                        return true;
                    }// end if
                }// end if
            }// end for
        }// end if

        return false;
    }


    /**
     * Create an amendment narrative that explains the reason for amending the request.
     *
     * @param description the description
     * @param userLoginId the user login id
     * @param text the text
     * @throws ValidationException the validation exception
     */
    public void createAmendmentNarrative(final String description, final String userLoginId, final String text)
            throws ValidationException {
        Narrative newNarrative = Narrative.create(description, this, userLoginId, text);
        getNarratives().add(newNarrative);

        // TODO perhaps there is only one amendment narrative allowed on a request amendment.
    }

    /**
     * Get all amendment narratives associated with the request.
     *
     * @return the narratives
     */
    public List<Narrative> getNarratives() {
        if (narratives == null) {
            narratives = new ArrayList<Narrative>();
        }

        return narratives;
    }

    /**
     * Sets the narratives.
     *
     * @param narratives the new narratives
     */
    // TESTING ONLY
    public void setNarratives(List<Narrative> narratives) {
        this.narratives = narratives;
    }

    /**
     * Checks if is amendment.
     *
     * @return true, if is amendment
     */
    public boolean isAmendment() {
        return amendment;
    }

    /**
     * Checks if is current.
     *
     * @return true, if is current
     */
    public boolean isCurrent() {
        return current;
    }

    // public RequestStatus getStatus( final RequestWorkflow workflow ) throws ObjectNotFoundException {
    // if( workflow == null ) {
    // return RequestStatus.findById(status); //request-level status
    // }
    //
    // return workflow.getRequestStatus(); //status for the specific workflow
    // }

    /**
     * Gets the status.
     *
     * @return the status
     * @throws ObjectNotFoundException the object not found exception
     */
    public RequestStatus getStatus() throws ObjectNotFoundException {
        return RequestStatus.findById(status); // request-level status
    }

    /**
     * Get the elaborate status of the request. If the request is undergoing review, return a percentage of the completed
     * reviews.
     *
     * @param workflow the workflow
     * @return the elaborate status
     * @throws ObjectNotFoundException the object not found exception
     */
    public String getElaborateStatus(final RequestWorkflow workflow) throws ObjectNotFoundException {

        //
        // if the request is not in-process, get the simple status
        int currentStatus = status; // top-level status
        if (workflow != null) {
            currentStatus = workflow.getRequestStatus().getId(); // workflow status
        }

        if (RequestStatus.SUBMITTED.getId() == currentStatus) { // if this workflow is in-process, get the percentage completed

            try {
                WorkflowResolver workflowResolver = DartObjectFactory.getInstance().getWorkflowResolver();
                if (workflowResolver != null) {

                    if (workflow != null) {
                        return (workflowResolver.resolve(workflow).calculateReviewCompletion(workflow, this)); // child workflow
                    } else {
                        return (workflowResolver.resolve(this).calculateReviewCompletion(workflow, this)); // top-level workflow
                    }

                }// end if
            } catch (WorkflowException e) {
                log.error("WorkflowException when retrieving the elaborate status: " + e.getMessage());
                e.printStackTrace();
            }
        }// end if

        //
        // if the request is not in-process, get the simple status
        return RequestStatus.findById(currentStatus).getName();
    }

    /**
     * Sets the status.
     *
     * @param status the status
     * @param userId the user id
     */
    private void setStatus(final RequestStatus status, final String userId) {
        this.status = status.getId();
        updatedOn = new Date();
        updatedBy = userId;
    }

    /**
     * Gets the head id.
     *
     * @return the head id
     */
    public int getHeadId() {
        return headId;
    }

    /**
     * Gets the tracking number.
     *
     * @return the tracking number
     */
    public String getTrackingNumber() {
        return trackingNumber;
    }

    /**
     * Gets the events.
     *
     * @return the events
     */
    public List<Event> getEvents() {
        return events;
    }

    /**
     * Sets the events.
     *
     * @param events the new events
     */
    // TESTING ONLY
    public void setEvents(List<Event> events) {
        this.events = events;
    }

    /**
     * Gets the irb number.
     *
     * @return the irb number
     */
    public String getIrbNumber() {
        return irbNumber;
    }

    /**
     * Sets the irb number.
     *
     * @param irbNumber the new irb number
     */
    public void setIrbNumber(final String irbNumber) {
        this.irbNumber = irbNumber;
    }

    /**
     * Gets the irb expiration.
     *
     * @return the irb expiration
     */
    public Date getIrbExpiration() {
        return irbExpiration;
    }

    /**
     * Sets the irb expiration.
     *
     * @param irbExpiration the new irb expiration
     */
    public void setIrbExpiration(final Date irbExpiration) {
        this.irbExpiration = irbExpiration;
    }

    public Set<Document> getDocuments() {
        return documents;
    }

    /**
     * Sets the documents.
     *
     * @param documents the new documents
     */
    // TESTING ONLY
    public void setDocuments(Set<Document> documents) {
        this.documents = documents;
    }

    /**
     * Gets the online datas.
     *
     * @return the online datas
     */
    public Set<OnlineData> getOnlineDatas() {
        return onlineDatas;
    }

    /**
     * Sets the online datas.
     *
     * @param onlineDatas the new online datas
     */
    // TESTING ONLY
    public void setOnlineDatas(Set<OnlineData> onlineDatas) {
        this.onlineDatas = onlineDatas;
    }

    /**
     * Update name.
     *
     * @param userId the user id
     * @param newText the new text
     * @throws ValidationException the validation exception
     */
    public void updateName(final String userId, final String newText) throws ValidationException {
        ValidationHelper.required("User Id", userId);
        ValidationHelper.required("Name", newText);
        ValidationHelper.validateSize("Name", newText, 1, 1024);

        name = newText;
        updatedBy = userId;
        updatedOn = new Date();
    }

    /**
     * Returns the list of intermediate reviews (non-NDS reviews).
     * 
     * If the workflow passed in is NOT null, returns all reviews attached to this request that are associated with this
     * workflow. If the workflow passed in is null, returns all reviews attached to this request, regardless of which workflow
     * they are associated with.
     *
     * @param workflow the workflow
     * @return the reviews
     */
    public Set<Review> getReviews(RequestWorkflow workflow) {
        if (workflow == null) {
            return getAllReviews();
        }

        // TODO: might want to ignore closed workflows when retrieving the reviews (might never get here if the workflows are
        // closed, though): final boolean ignoreClosedWorkflows

        Set<Review> reviewsForThisWorkflow = new HashSet<Review>();
        for (Review rev : getAllReviews()) {
            if (rev != null) {
                if (rev.workflow != null && rev.workflow.getId() == workflow.getId()) { // review belongs to this workflow
                    reviewsForThisWorkflow.add(rev);
                }
            }
        }// end for

        return reviewsForThisWorkflow;
    }

    // public Set<Review> getReviewsByWorkflowType( int workflowTypeId ) {
    //
    // WorkflowType.initialize();
    //
    // Set<Review> reviewsForThisWorkflow = new HashSet<Review>();
    // for( Review rev : getAllReviews() ) {
    // if( rev != null && rev.workflow != null ) {
    // if( rev.workflow.getWorkflowTemplate() != null && rev.workflow.getWorkflowTemplate().getWorkflowTypeId() ==
    // workflowTypeId ) {
    // reviewsForThisWorkflow.add(rev);
    //
    // } else if( rev.workflow == null && workflowTypeId == WorkflowType.WF_NDS.getId() ) { //support the old NDS-only reviews
    // reviewsForThisWorkflow.add(rev);
    // }
    // }
    // }//end for
    //
    // return reviewsForThisWorkflow;
    // }

    // returns the list of ALL intermediate reviews
    public Set<Review> getAllReviews() {
        // TODO: might want to ignore closed workflows when retrieving the reviews (might never get here if the workflows are
        // closed, though): final boolean ignoreClosedWorkflows
        return reviews;
    }

    // TESTING ONLY
    public void setReviews(final Set<Review> reviews) {
        this.reviews = reviews;
    }

    public void addReview(final Review review) {
        reviews.add(review);
    }

    public void removeReviewList(final List<Review> reviewList, final boolean deleteReview) {
        if (reviewList == null) {
            return;
        }

        this.reviews.removeAll(reviewList);

        if (deleteReview) {
            for (Review review : reviewList) {
                review.delete();
            }
        }// end if
    }

    /**
     * find a review assigned to a specific group for this request.
     *
     * @param workflow the workflow
     * @param group the group
     * @return the review
     */
    public Review findReviewAssignedToGroup(final RequestWorkflow workflow, final Group group) {
        Review review = null;

        // find the review assigned to the specific group
        for (Review rev : getReviews(workflow)) {
            if (rev.getReviewer().getId() == group.getId()) {
                review = rev;
                break;
            }
        }

        return review;
    }

    /**
     * Returns a set containing the reviews for the specified workflow (based on the workflow ID) Returns an empty set if there
     * are no reviews for this workflow.
     *
     * @param workflow the workflow
     * @return the reviews for workflow
     */
    public Set<Review> getReviewsForWorkflow(final RequestWorkflow workflow) {
        Set<Review> reviewSet = new HashSet<Review>();

        for (Review rev : getReviews(workflow)) {
            if (rev.getWorkflow() != null && rev.getWorkflow().getId() == workflow.getId()) {
                reviewSet.add(rev);
            }
        }

        return reviewSet;
    }

    public void addWorkflow(final RequestWorkflow workflow) {
        workflows.add(workflow);
    }

    public Set<RequestWorkflow> getWorkflows(final boolean ignoreClosedWorkflows) {
        Set<RequestWorkflow> openWorkflows = new HashSet<RequestWorkflow>();

        if (ignoreClosedWorkflows) { // only return the workflows that are not closed (removed the associated data sources)

            if (workflows != null && workflows.size() > 0) {
                for (RequestWorkflow currWorkflow : workflows) {

                    try {
                        if (currWorkflow != null && currWorkflow.getRequestStatus().getId() != RequestStatus.CLOSED.getId()) {
                            openWorkflows.add(currWorkflow);
                        }
                    } catch (ObjectNotFoundException e) {
                        log.error("Error retrieving the workflow status: " + e.getMessage());
                        e.printStackTrace();
                    }

                }// end for
            }// end if

        } else { // keep ALL workflows, even the closed ones

            openWorkflows.addAll(workflows);

        }

        return openWorkflows;
    }

    public Set<WorkflowTemplate> getWorkflowTemplates(final boolean ignoreClosedWorkflows) {
        Set<WorkflowTemplate> openWorkflowTemplates = new HashSet<WorkflowTemplate>();

        Set<RequestWorkflow> openWorkflows = getWorkflows(ignoreClosedWorkflows);
        if (openWorkflows != null) {

            for (RequestWorkflow currWorkflow : openWorkflows) {

                WorkflowTemplate currTemplate = currWorkflow.getWorkflowTemplate();
                if (currTemplate != null) {
                    openWorkflowTemplates.add(currTemplate);
                }// end if

            }// end for

        }// end if

        return openWorkflowTemplates;
    }

    /**
     * Returns true if all workflows attached to this request are in their final state.
     * 
     * Ignores workflows that were removed when all of their data sources were removed.
     *
     * @return true, if successful
     */
    public boolean allWorkflowsCompleted() {
        boolean allDone = true;

        for (RequestWorkflow workflow : getWorkflows(true)) { // check the current workflows (have NOT been closed by having
                                                              // their data sources removed)

            if (workflow.isCompleted() == false) {
                allDone = false;
                break;
            }// end if

        }// end for

        return allDone;
    }

    /**
     * Returns true if there is at least one other in-process workflow attached to this request.
     * 
     * Ignores workflows that were removed when all of their data sources were removed.
     *
     * @param workflow the workflow
     * @return true, if successful
     */
    public boolean anotherWorkflowInProcess(RequestWorkflow workflow) {
        boolean workflowInProcess = false;

        if (workflow != null) {
            for (RequestWorkflow currWorkflow : getWorkflows(true)) { // check the current workflows (have NOT been closed by
                                                                      // having their data sources removed)

                if (currWorkflow != null && currWorkflow.equals(workflow) == false) { // don't compare this workflow to itself

                    if (currWorkflow.isCompleted() == false) {
                        workflowInProcess = true;
                        break;
                    }// end if
                }// end if

            }// end for
        }// end if

        return workflowInProcess;
    }

    /**
     * Returns the set of Data Sources that have already been approved for this request.
     * 
     * Ignores SAS Grid (currently stored as a data source)
     *
     * @return the selected and approved data sources
     */
    public Set<DataSource> getSelectedAndApprovedDataSources() {
        Set<DataSource> approvedDataSources = new HashSet<DataSource>();
        Set<DataSource> approvedDataSourcesExceptSAS = new HashSet<DataSource>();

        // old data: if the top-level request state is approved, return the entire list of Data Sources attached to this request
        // new data (multi-workflow): step through each workflow attached to this request
        // for each workflow attached to this request
        // if this workflow has been approved
        // get the Data Sources that correspond to this workflow
        // get the WorkflowTemplate for this workflow: workflow.getWorkflowTemplate()
        // get the DataSources for this WorkflowTemplate: template.getDataSources()
        // add the Data Sources that correspond to this workflow to the overall set of approved Data Sources (keep only unique
        // Data Sources)
        // return the overall set of approved Data Sources

        Set<DataSource> selectedDataSources = getDataSources(); // get the currently selected data sources

        Set<RequestWorkflow> workflowSet = getWorkflows(true);
        if (workflowSet != null && workflowSet.size() > 0) { // new data (multi-workflow): step through each workflow attached
                                                             // to this request

            for (RequestWorkflow currWorkflow : workflowSet) {

                if (currWorkflow != null && currWorkflow.isApproved()) { // this workflow has already been approved

                    WorkflowTemplate currTemplate = currWorkflow.getWorkflowTemplate(); // get the template for this workflow
                    if (currTemplate != null) {

                        // get the Data Sources for this workflow template
                        Set<DataSource> currDataSourceSet = currTemplate.getDataSources();
                        if (currDataSourceSet != null && currDataSourceSet.size() > 0) {

                            // determine if any of these workflow Data Sources were selected
                            for (DataSource ds : currDataSourceSet) {
                                if (ds != null && selectedDataSources.contains(ds) == true) { // this Data Source was selected
                                                                                              // and approved
                                    approvedDataSources.add(ds); // add the selected Data Sources for this workflow to the set
                                                                 // of approved Data Sources
                                }// end if
                            }

                        }// end if
                    }// end if

                }// end if

            }// end for

        } else { // old data: if the top-level request state is approved, return the entire list of Data Sources attached to
                 // this request

            try {

                if (getStatus().getId() == RequestStatus.APPROVED.getId()) { // this entire request has been approved (old data)
                    // return( getDataSources() ); //return the entire list of Data Sources attached to this request
                    approvedDataSources.addAll(getDataSources()); // return the entire list of Data Sources attached to this
                                                                  // request
                }

            } catch (ObjectNotFoundException e) {
                log.error("Exception retrieving the approved Data Sources: " + e.getMessage());
                e.printStackTrace();
            }
        }// end else

        // Allow SAS Grid to be removed, because it's not really a data source.
        if (approvedDataSources != null) {
            for (DataSource currDataSource : approvedDataSources) {
                if (currDataSource.getId() != 1035) { // ignore SAS Grid
                    approvedDataSourcesExceptSAS.add(currDataSource);
                }
            }// end for
        }// end if

        // return approvedDataSources;
        return approvedDataSourcesExceptSAS;
    }

    // /**
    // * Returns the set of Data Sources that have already been approved for this workflow (not necessarily for the entire
    // request).
    // *
    // * @return
    // */
    // public Set<DataSource> getSelectedAndApprovedDataSourcesForWorkflow( final RequestWorkflow workflow ) {
    // Set<DataSource> approvedDataSources = new HashSet<DataSource>();
    //
    //
    // Set<DataSource> selectedDataSources = getDataSources(); //get the currently selected data sources
    //
    //
    // if( workflow != null && workflow.isApproved() ) { //this workflow has already been approved
    //
    // WorkflowTemplate currTemplate = workflow.getWorkflowTemplate(); //get the template for this workflow
    // if( currTemplate != null ) {
    //
    // //get the Data Sources for this workflow template
    // Set<DataSource> currDataSourceSet = currTemplate.getDataSources();
    // if( currDataSourceSet != null && currDataSourceSet.size() > 0 ) {
    //
    // //determine if any of these workflow Data Sources were selected
    // for( DataSource ds : currDataSourceSet ) {
    // if( ds != null && selectedDataSources.contains(ds) == true ) { //this Data Source was selected and approved
    // approvedDataSources.add(ds); //add the selected Data Sources for this workflow to the set of approved Data Sources
    // }//end if
    // }
    //
    // }//end if
    // }//end if
    //
    // }//end if
    //
    //
    // return approvedDataSources;
    // }

    /**
     * Returns the set of Data Sources that are not currently editable (the corresponding workflow is not currently editable).
     *
     * @return the locked data sources
     */
    public Set<DataSource> getLockedDataSources() {
        Set<DataSource> lockedDataSources = new HashSet<DataSource>();

        try {

            //
            // Determine which workflows are currently editable: editable workflows can have their data sources be editable.
            // Otherwise, lock the data sources.
            // old request -> no workflows, continue to function as usual (is the request itself editable?)
            // new request -> before the request is submitted, allow changes to the selected data sources
            // new request -> after submit, use the status of the workflows to determine if the data sources are locked
            Set<RequestWorkflow> workflows = getWorkflows(true);
            if (workflows != null) {
                for (RequestWorkflow currWorkflow : workflows) {
                    if (currWorkflow != null) {

                        if (isEditable(currWorkflow) == false) { // this workflow is NOT editable, so shouldn't modify the
                                                                 // selected data sources that correspond to this workflow

                            WorkflowTemplate template = currWorkflow.getWorkflowTemplate();
                            if (template != null) {

                                // get the data sources for this workflow template
                                Set<DataSource> workflowDataSources = template.getDataSources();
                                if (workflowDataSources != null) { // && workflowDataSources.isEmpty() == false ) {

                                    lockedDataSources.addAll(workflowDataSources);

                                }// end if
                            }// end if
                        }// end if -- workflow is editable

                    }// end if
                }// end for
            }// end if

        } catch (ObjectNotFoundException e) {
            log.error("Error retrieving the request status: " + e.getMessage());
            e.printStackTrace();
        }// end if

        return lockedDataSources;
    }

    /**
     * which states are required by the workflow.
     *
     * @return the workflow mask
     */
    public long getWorkflowMask() {
        return workflowMask;
    }

    /**
     * Sets the workflow mask.
     *
     * @param mask the new workflow mask
     */
    public void setWorkflowMask(final long mask) {
        this.workflowMask = mask;
    }

    public void setWorkflowState(final int state) {
        this.workflowState = state;
    }

    /**
     * what state is this request in in the workflow.
     *
     * @return the workflow state
     */
    public int getWorkflowState() {
        return workflowState;
    }

    /**
     * which workflow is this request following (workflow type).
     *
     * @return the workflow type id
     */
    public int getWorkflowTypeId() {
        return workflowId;
    }

    /**
     * which workflow is this request following (workflow type).
     *
     * @param id the new workflow type id
     */
    public void setWorkflowTypeId(final int id) {
        this.workflowId = id;
    }

    public boolean isProcessWithdraw() {
        return processWithdraw;
    }

    public void setProcessWithdraw(boolean processWithdraw) {
        this.processWithdraw = processWithdraw;
    }

    /**
     * Retrieve a specific review on the request.
     *
     * @param reviewId the review id
     * @return the review
     * @throws ObjectNotFoundException the object not found exception
     */
    public Review getReview(final int reviewId) throws ObjectNotFoundException {

        for (Review rev : getAllReviews()) {
            if (rev != null && rev.getId() == reviewId) {
                return rev;
            }
        }

        throw new ObjectNotFoundException("Review " + reviewId + " not found.");
    }

    /**
     * Gets the submitted on.
     *
     * @return the submitted on
     */
    public Date getSubmittedOn() {
        return submittedOn;
    }

    /**
     * Gets the activity.
     *
     * @return the activity
     */
    public Activity getActivity() {
        return activity;
    }

    /**
     * Gets the primary location.
     *
     * @return the primary location
     */
    public Location getPrimaryLocation() {
        return primaryLocation;
    }

    /**
     * Sets the primary location.
     *
     * @param primaryLocation the new primary location
     */
    public void setPrimaryLocation(final Location primaryLocation) {
        this.primaryLocation = primaryLocation;
    }

    /**
     * Gets the sites.
     *
     * @return the sites
     */
    public Set<Location> getSites() {
        return sites;
    }

    /**
     * Sets the sites.
     *
     * @param sites the new sites
     */
    // TESTING ONLY
    public void setSites(Set<Location> sites) {
        this.sites = sites;
    }

    /**
     * Gets the participants.
     *
     * @return the participants
     */
    public Set<Participant> getParticipants() {
        return participants;
    }

    /**
     * Sets the test participants.
     *
     * @param participants the new test participants
     */
    // ONLY FOR TEST
    public void setTestParticipants(final Set<Participant> participants) {
        this.participants = participants;
    }

    /**
     * Find a specific participant in the request.
     *
     * @param person the person
     * @param location the location
     * @return the participant
     * @throws ObjectNotFoundException the object not found exception
     */
    public Participant findParticipant(final Person person, final Location location) throws ObjectNotFoundException {
        if (person == null || location == null) {
            throw new ObjectNotFoundException("Participant not found");
        }

        for (Participant part : participants) {

            // don't use Person.equals if the person class instance might be a proxy generated by Hibernate. equals() will fail.
            if (part.getPerson().getId() == person.getId() && part.getLocation().getId() == location.getId()) {
                return part;
            }
        }

        throw new ObjectNotFoundException("Participant not found");
    }

    /**
     * Set the list of participants on the request. This only manages the list of participants. It does not modify the
     * participant objects themselves.
     *
     * @param participantList the new participants
     * @throws ValidationException the validation exception
     */
    public void setParticipants(final Collection<Participant> participantList) throws ValidationException {
        ArrayList<Participant> orphans = new ArrayList<Participant>();
        ArrayList<Participant> newParticipants = new ArrayList<Participant>();
        orphans.addAll(getParticipants());

        for (Participant request : participantList) {
            if (orphans.contains(request)) {
                orphans.remove(request);
            } else {
                newParticipants.add(request);
            }
        }

        getParticipants().removeAll(orphans);
        getParticipants().addAll(newParticipants);
    }

    /**
     * Returns the fullName of the principal investigator (at the primary site). If the request has a principal investigator at
     * the primary site -> return the fullname of that participant Else if the request has a principal investigator, return the
     * fullname of the 1st principal investigator (alphabetical order) Else if the request has a primary site, return the
     * fullname of the 1st participant at that site (alphabetical order) Else return the fullname of the 1st participant at the
     * 1st site (alphabetical order) Else return an empty string.
     * 
     * OperationalRequest now allows only one participant (and one site) -- if OperationalRequest changes back to allowing
     * multiple sites and multiple participants, this function will need to be modified.
     *
     * @return the principal investigator
     */
    @SuppressWarnings("unchecked")
    public String getPrincipalInvestigator() {

        Set<Participant> participantSet = getParticipants();

        //
        // if participantSet has not been populated (like at Activity creation time) return empty string
        if (participantSet == null) {
            return "";
        }

        List<Participant> participantList = new ArrayList<Participant>(participantSet);
        Collections.sort(participantList); // sort by participant fullname

        Set<Location> locationSet = getSites();

        //
        // if locationSet has not been populated (like at Activity creation time) return empty string
        if (locationSet == null) {
            return "";
        }

        List<Location> locationList = new ArrayList<Location>(locationSet);
        Collections.sort(locationList); // sort by location name

        //
        // find the principalInvestigator at the primarySite
        for (Participant part : participantSet) {
            if (part.getPrincipalInvestigator() != null && part.getPrincipalInvestigator().booleanValue() == true) {
                if (getPrimaryLocation() != null && part.getLocation() != null
                        && part.getLocation().getId() == getPrimaryLocation().getId()) {
                    return part.getPerson().getFullName();
                }
            }
        }

        //
        // if we have not yet found the principal investigator, handle the old DART business rules and old DART data
        //
        //
        // find the 1st principal investigator, based on Person.fullname
        for (Participant part : participantList) {
            if (part.getPrincipalInvestigator() != null && part.getPrincipalInvestigator().booleanValue() == true) {
                return part.getPerson().getFullName();
            }
        }

        //
        // find the primarySite -> return the 1st participant at that location (based on Person.fullname)
        for (Participant part : participantList) {
            if (getPrimaryLocation() != null && part.getLocation() != null
                    && part.getLocation().getId() == getPrimaryLocation().getId()) {
                return part.getPerson().getFullName();
            }
        }

        //
        // find the first participant at the first location
        if (locationList.size() > 0) {
            Location firstLocation = locationList.get(0);

            for (Participant part : participantList) {
                if (part.getLocation() != null && part.getLocation().getId() == firstLocation.getId()) {
                    return part.getPerson().getFullName();
                }
            }
        }// end if

        return "";
    }

    /**
     * Gets the comments.
     *
     * @return the comments
     */
    // TODO do you want to wrap an ImmutableSet around these containers as a return value?
    public Set<Comment> getComments() {
        return comments;
    }

    /**
     * Sets the comments.
     *
     * @param comments the new comments
     */
    // TESTING ONLY
    public void setComments(Set<Comment> comments) {
        this.comments = comments;
    }

    /**
     * Test if all reviews on the request are approved.
     *
     * @param workflow the workflow
     * @return true, if successful
     */
    public boolean allReviewsApproved(final RequestWorkflow workflow) {
        boolean allDone = true;
        
        for (Review rev : getReviews(workflow)) {
            if (rev.isApproved() == false && !rev.isWithdrawn()) {
                allDone = false;
                break;
            }
        }

        return allDone;
    }

    /**
     * return true if all reviews are either approved or rejected or withdrawn.
     *
     * @param workflow the workflow
     * @return true, if successful
     */
    public boolean allReviewsCompleted(final RequestWorkflow workflow) {
        boolean allDone = true;

        for (Review rev : getReviews(workflow)) {
            if (!rev.isApproved() && !rev.isRejected()  && !rev.isWithdrawn()) {
                allDone = false;
                break;
            }
        }

        return allDone;
    }

    /**
     * Returns true if any intermediate review has been denied. Otherwise, returns false.
     *
     * @param workflow the workflow
     * @return true, if successful
     */
    public boolean anyReviewDenied(final RequestWorkflow workflow) {

        for (Review rev : getReviews(workflow)) {
            if (rev.isRejected() == true) {
                return true;
            }
        }

        return false;
    }
    
    /**
     *  Returns true if any intermediate review is in a change request status. Otherwise, returns false.
     *
     * @param workflow the workflow
     * @return true, if successful
     */
    public boolean anyReviewChangeRequested(final RequestWorkflow workflow) {

        for (Review rev : getReviews(workflow)) {
            if (!rev.isWithdrawn() && rev.isChangeRequested()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns true if the specified review (based on group) has not yet been completed. Returns false if the review has been
     * completed or if this review group cannot be found in the request's set of reviews.
     * 
     * If the workflow object exists, retrieves the review for this group and this workflow. If the workflow object is null,
     * retrieves any review for this group that is attached to this request.
     *
     * @param workflow the workflow
     * @param group the group
     * @return true, if is review open
     */
    public boolean isReviewOpen(final RequestWorkflow workflow, final Group group) {

        if (group != null) {

            Review rev = findReviewAssignedToGroup(workflow, group); // get the review for this group
            if (rev != null) {

                if (rev.isApproved() || rev.isRejected() || rev.isWithdrawn()) {
                    return false; // review has been completed
                } else {
                    return true; // review has not yet been completed
                }
            }
        }

        return false; // failed to find this review group
    }

    /**
     * Test if the request is in the submitted or pending change request state.
     *
     * @param workflow the workflow
     * @return true, if is submitted or changed
     * @throws ObjectNotFoundException the object not found exception
     */
    public boolean isSubmittedOrChanged(final RequestWorkflow workflow) throws ObjectNotFoundException {

        int requestStatusId = getStatus().getId(); // use the top-level request status
        if (workflow != null) {
            requestStatusId = workflow.getRequestStatus().getId(); // use the specific workflow status
        }

        if (RequestStatus.SUBMITTED.getId() == requestStatusId || RequestStatus.CHANGE_REQUESTED.getId() == requestStatusId) {
            return true;
        }

        return false;
    }

    /**
     * Test if the request is in the initiated or pending change request state.
     *
     * @param workflow the workflow
     * @return true, if is initiated or changed
     * @throws ObjectNotFoundException the object not found exception
     */
    public boolean isInitiatedOrChanged(final RequestWorkflow workflow) throws ObjectNotFoundException {

        int requestStatusId = getStatus().getId(); // use the top-level request status
        if (workflow != null) {
            requestStatusId = workflow.getRequestStatus().getId(); // use the specific workflow status
        }

        if (RequestStatus.INITIATED.getId() == requestStatusId || RequestStatus.CHANGE_REQUESTED.getId() == requestStatusId) {
            return true;
        }

        return false;
    }

    /**
     * Test if the request is editable.
     *
     * @param workflow the workflow
     * @return true, if is editable
     * @throws ObjectNotFoundException the object not found exception
     */
    public boolean isEditable(final RequestWorkflow workflow) throws ObjectNotFoundException {

        // only allow editing of requests in the Change Request or Initiated state
        return (isInitiatedOrChanged(workflow));
    }

    /**
     * Returns true if the request is in a final state: REQUEST_COMPLETED APPROVED, DENIED CLOSED.
     *
     * @return true, if is completed status
     * @throws ObjectNotFoundException the object not found exception
     */
    public boolean isCompletedStatus() throws ObjectNotFoundException {

        int requestStatusId = getStatus().getId(); // use the top-level request status

        if (RequestStatus.REQUEST_COMPLETED.getId() == requestStatusId || // new data
                RequestStatus.APPROVED.getId() == requestStatusId || RequestStatus.DENIED.getId() == requestStatusId || // old
                                                                                                                        // data
                RequestStatus.CLOSED.getId() == requestStatusId) {
            return true;
        }

        return false;
    }

    /**
     * Test if all documents required for a request have been uploaded.
     *
     * @return true, if successful
     */
    public boolean allDocumentsReady() {
        // validate that all required documents have been uploaded.
        for (Document doc : getDocuments()) {
            if (doc.getContent() == null || doc.getContent().getRepositoryPath() == null) {
                return false;
            }
        }

        return true;
    }

    /**
     * Copy documents.
     *
     * @param destination the destination
     * @throws ValidationException the validation exception
     * @throws ObjectNotFoundException the object not found exception
     */
    protected void copyDocuments(final Request destination) throws ValidationException, ObjectNotFoundException {
        // why isn't this automatically generated by Hibernate?
        // because it's lazy loaded - read the documentation.
        if (destination.documents == null) {
            destination.documents = new HashSet<Document>();
        }

        // the list of documents from the original request that have been copied into the amendment.
        HashSet<Document> copiedDocuments = new HashSet<Document>();
        HashSet<Document> newDocuments = new HashSet<Document>();

        // walk through the required participant documents
        for (RequestParticipantDocument rpd : RequestParticipantDocument.listByRequestId(this.getId())) {
            Participant participant = Participant.findById(rpd.getParticipantId());
            if (participant != null) {

                // find the equivalent participant in the amended request
                Participant amendmentParticipant =
                        Participant.find(destination.getId(), participant.getPerson().getId(), participant.getLocation()
                                .getId());

                // get the current document (Currently only copying the most recent document to the amendment, not the entire
                // document history.)
                Document oldDocument = Document.getCurrentOrMostRecentDocument(rpd.getDocumentId());
                if (oldDocument != null) {
                    copiedDocuments.add(oldDocument);

                    Document newDocument = oldDocument.copy(destination, createdBy, false, false); // don't increment the
                                                                                                   // document version for
                                                                                                   // amendment
                    newDocuments.add(newDocument);

                    RequestParticipantDocument.create(destination, amendmentParticipant, newDocument);
                }// end if
            }// end if -- participant
        }

        // walk through the required location documents
        for (RequestLocationDocument rld : RequestLocationDocument.listByRequestId(this.getId())) {
            Location location = Location.findById(rld.getLocationId());

            // get the current document
            Document oldDocument = Document.getCurrentOrMostRecentDocument(rld.getDocumentId());
            if (oldDocument != null) {
                copiedDocuments.add(oldDocument);

                Document newDocument = oldDocument.copy(destination, createdBy, false, false); // don't increment the document
                                                                                               // version for amendment
                newDocuments.add(newDocument);

                RequestLocationDocument.create(destination, location, newDocument);
            }// end if
        }

        // walk through the required admin participant documents
        for (RequestAdminParticipantDocument rapd : RequestAdminParticipantDocument.listByRequestId(this.getId())) {
            Participant participant = Participant.findById(rapd.getParticipantId());
            if (participant != null) {

                // find the equivalent participant in the amended request
                Participant amendmentParticipant =
                        Participant.find(destination.getId(), participant.getPerson().getId(), participant.getLocation()
                                .getId());

                Group reviewer = Group.findById(rapd.getGroupId());

                // get the current document
                Document oldDocument = Document.getCurrentOrMostRecentDocument(rapd.getDocumentId());
                if (oldDocument != null) {
                    copiedDocuments.add(oldDocument);

                    Document newDocument = oldDocument.copy(destination, createdBy, false, false); // don't increment the
                                                                                                   // document version for
                                                                                                   // amendment
                    newDocuments.add(newDocument);

                    RequestAdminParticipantDocument.create(destination, amendmentParticipant, newDocument, reviewer);
                }// end if
            }// end if -- participant
        }

        // walk through the required admin location documents
        for (RequestAdminLocationDocument rald : RequestAdminLocationDocument.listByRequestId(this.getId())) {
            Location location = Location.findById(rald.getLocationId());
            Group reviewer = Group.findById(rald.getGroupId());

            // get the current document
            Document oldDocument = Document.getCurrentOrMostRecentDocument(rald.getDocumentId());
            if (oldDocument != null) {
                copiedDocuments.add(oldDocument);

                Document newDocument = oldDocument.copy(destination, createdBy, false, false); // don't increment the document
                                                                                               // version for amendment
                newDocuments.add(newDocument);

                RequestAdminLocationDocument.create(destination, location, newDocument, reviewer);
            }// end if
        }

        destination.documents.addAll(newDocuments);
    }

    // /**
    // * Copy the selected review groups to the new Request. (Does NOT copy the review decision.)
    // * @param destination
    // * @throws ValidationException
    // */
    // protected void copyReviewGroups(final Request destination) throws ValidationException, ObjectNotFoundException {
    // // why isn't this automatically generated by Hibernate?
    // // because it's lazy loaded - read the documentation.
    // if (destination.reviews == null) {
    // destination.reviews = new HashSet<Review>();
    // }
    //
    // //walk through the reviews attached to the original request
    // for( Review rev : getReviews() ) {
    //
    // //copy the review group info into the amended request
    // int groupId = rev.getReviewer().getId();
    // ReviewTemplate template = ReviewTemplate.findByGroupId( groupId );
    //
    // Review newReview = Review.create(destination, template, rev.getReviewer(), createdBy);
    // destination.reviews.add( newReview );
    // }
    // }

    /**
     * Removes the all location documents.
     */
    public void removeAllLocationDocuments() {

        for (Location loc : getSites()) {
            removeLocationDocuments(loc);
        }
    }

    /**
     * Removes the all participant documents.
     */
    public void removeAllParticipantDocuments() {

        for (Participant part : getParticipants()) {
            removeParticipantDocuments(part);
        }
    }

    /**
     * Removes the location documents.
     *
     * @param loc the loc
     */
    public void removeLocationDocuments(final Location loc) {
        if (loc == null) {
            return;
        }

        // List<RequestLocationDocument> rldList = RequestLocationDocument.listByRequestId(this.getId());
        List<RequestLocationDocument> rldList = RequestLocationDocument.listByRequestAndLocationId(this.getId(), loc.getId());
        // get the documents for this location

        for (int i = 0; i < rldList.size(); i++) {
            RequestLocationDocument rld = rldList.get(i);
            if (rld.getLocationId() == loc.getId()) {
                Document doc = Document.findById(rld.getDocumentId());
                if (doc != null) {
                    this.documents.remove(doc);
                    doc.delete();
                }// end if

                // TODO: might just want to set this RequestLocationDocument to inactive instead of deleting it?
                rld.delete();
            }
        }
    }

    public void removeParticipantDocuments(final Participant part) {
        if (part == null) {
            return;
        }

        // List<RequestParticipantDocument> rpdList = RequestParticipantDocument.listByRequestId(this.getId());
        List<RequestParticipantDocument> rpdList =
                RequestParticipantDocument.listByRequestAndParticipantId(this.getId(), part.getId()); // get the documents for
                                                                                                      // this participant

        for (int i = 0; i < rpdList.size(); i++) {
            RequestParticipantDocument rpd = rpdList.get(i);
            if (rpd.getParticipantId() == part.getId()) {
                Document doc = Document.findById(rpd.getDocumentId());
                if (doc != null) {
                    this.documents.remove(doc);
                    doc.delete();
                }// end if

                // TODO: might just want to set this RequestParticipantDocument to inactive instead of deleting it?
                rpd.delete();
            }
        }
    }

    // // walk through existing Admin documents, document templates, participants and data sources and figure out what
    // // Admin documents should be attached to the request.
    // //
    // // Ignores the documents that are required for the requestor. Retrieves only the Admin documents.
    // public void createAdminDocuments(final String createdBy) throws CheckedException {
    //
    // log.debug("creating Admin documents from templates for request id = " + getId());
    //
    // DocumentRuleEvaluatorHelper.evaluateAdminDocs(this, createdBy);
    // }

    /**
     * Returns a set of (intermediate review) groups that requested a change.
     *
     * @param workflow the workflow
     * @return the changes requested groups
     */
    public Set<Group> getChangesRequestedGroups(final RequestWorkflow workflow) {

        Set<Group> changeRequestGroups = new HashSet<Group>();

        // get the groups that requested a change within a specific worklow
        for (Review review : getReviews(workflow)) {

            if (review.isChangeRequested()) { // this group requested changes

                Group group = review.getReviewer();
                if (group != null) {
                    changeRequestGroups.add(group);
                }
            }
        }

        return changeRequestGroups;
    }

    /**
     * Returns the NDS review status (based on existing events for the NDS reviews).
     *
     * @param workflow the workflow
     * @param group the group
     * @param isInitNDS the is init nds
     * @return the NDS review status
     */
    public String getNDSReviewStatus(final RequestWorkflow workflow, final Group group, final boolean isInitNDS) {

        if (group != null) {

            EventType.initialize();

            //
            // approved? (final event)
            List<Event> approveEventListForGroup =
                    Event.listByEventTypeAndRequestIdAndGroupOrder(EventType.APPROVE_REVIEW.getId(), getId(), group.getId(),
                            isInitNDS);
            if (approveEventListForGroup != null && approveEventListForGroup.size() > 0) {
                return RequestStatus.APPROVED.getName();
            }// end if

            //
            // denied? (final event)
            List<Event> denyEventListForGroup =
                    Event.listByEventTypeAndRequestIdAndGroupOrder(EventType.DENY_REVIEW.getId(), getId(), group.getId(),
                            isInitNDS);
            if (denyEventListForGroup != null && denyEventListForGroup.size() > 0) {
                return RequestStatus.DENIED.getName();
            }// end if

            //
            // change request (middle event)
            List<Event> changeEventListForGroup =
                    Event.listByEventTypeAndRequestIdAndGroupOrder(EventType.CHANGE_REQUEST.getId(), getId(), group.getId(),
                            isInitNDS);
            if (changeEventListForGroup != null && changeEventListForGroup.size() > 0) {

                // verify the status of the request (if the request has been re-submitted, it is no longer in the change
                // requested state)
                try {
                    int requestStatusId = getStatus().getId(); // use the top-level request status
                    if (workflow != null) {
                        requestStatusId = workflow.getRequestStatus().getId(); // use the specific workflow status
                    }

                    if (RequestStatus.CHANGE_REQUESTED.getId() == requestStatusId)
                        return RequestStatus.CHANGE_REQUESTED.getName();

                } catch (ObjectNotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }// end if

            //
            // waiting for review? (initial event)
            // if (DartRequest.class.isAssignableFrom(this.getClass())) {

            if (isInitNDS) { // initial NDS review

                try {
                    boolean waitingForInitialNDSReview = false;

                    WorkflowResolver workflowResolver = DartObjectFactory.getInstance().getWorkflowResolver();
                    if (workflowResolver != null) {

                        if (workflow != null) {
                            waitingForInitialNDSReview =
                                    workflowResolver.resolve(workflow).isReadyForInitialReview(workflow, this); 
                            // child  workflow
                        } else {
                            waitingForInitialNDSReview = 
                                    workflowResolver.resolve(this).isReadyForInitialReview(workflow, this); 
                            // top-level  workflow
                        }

                        if (waitingForInitialNDSReview == true) {
                            return Review.WAITING_FOR_REVIEW;
                        }

                    }// end if
                } catch (WorkflowException e) {
                    log.error("WorkflowException when retrieving the NDS review status: " + e.getMessage());
                    e.printStackTrace();
                }

            } else { // final NDS review

                try {
                    boolean waitingForFinalNDSReview = false;

                    WorkflowResolver workflowResolver = DartObjectFactory.getInstance().getWorkflowResolver();
                    if (workflowResolver != null) {

                        if (workflow != null) {
                            waitingForFinalNDSReview = 
                                    workflowResolver.resolve(workflow).isReadyForFinalReview(workflow, this); 
                            // child  workflow
                        } else {
                            waitingForFinalNDSReview = 
                                    workflowResolver.resolve(this).isReadyForFinalReview(workflow, this); 
                            // top-level  workflow
                        }

                        if (waitingForFinalNDSReview == true) {
                            return Review.WAITING_FOR_REVIEW;
                        }

                    }// end if
                } catch (WorkflowException e) {
                    log.error("WorkflowException when retrieving the NDS review status: " + e.getMessage());
                    e.printStackTrace();
                }

            }

            // }
            // else if (OperationalRequest.class.isAssignableFrom(this.getClass())) {
            //
            // if( isInitNDS ) { //initial NDS review
            //
            // if( ((OperationalRequest)this).isReadyForInitialReview() ) {
            // return Review.WAITING_FOR_REVIEW;
            // }
            //
            // } else { //final NDS review
            //
            // if( ((OperationalRequest)this).isReadyForFinalReview(workflow) ) {
            // return Review.WAITING_FOR_REVIEW;
            // }
            // }
            // }

        }// end if

        return ""; // the request has not yet been submitted
    }

    public static List<Event> getNDSFinalEvents(final int requestId) {

        Group.initialize();
        EventType.initialize();

        //
        // Final NDS approved? (final event)
        List<Event> approveEventListForGroup =
                Event.listByEventTypeAndRequestIdAndGroupOrder(EventType.APPROVE_REVIEW.getId(), requestId, Group.NDS.getId(),
                        false);
        if (approveEventListForGroup != null && approveEventListForGroup.size() > 0) {
            return approveEventListForGroup;
        }// end if

        //
        // Final NDS denied? (final event)
        List<Event> denyEventListForGroup =
                Event.listByEventTypeAndRequestIdAndGroupOrder(EventType.DENY_REVIEW.getId(), requestId, Group.NDS.getId(),
                        false);
        if (denyEventListForGroup != null && denyEventListForGroup.size() > 0) {
            return denyEventListForGroup;
        }// end if

        return null;
    }

    @SuppressWarnings("unchecked")
    public static Date getFinalEventDate(final int requestId) {

        List<Event> finalEventList = getNDSFinalEvents(requestId);
        if (finalEventList != null && finalEventList.size() > 0) {

            // sort the list! (earliest event first)
            Collections.sort(finalEventList, Event.getComparator());

            int lastIndex = finalEventList.size() - 1;
            Event lastEvent = finalEventList.get(lastIndex);

            if (lastEvent != null) {
                return lastEvent.getCreatedOn(); // date of this event
            }// end if
        }// end if

        return (new Date()); // no final event, so use the current Date
    }

    // necessary to use (List<Request>).contains()
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        // if ( (Request.class.isAssignableFrom(obj.getClass())) == false &&
        // (obj.getClass().isAssignableFrom(Request.class)) == false ) { //class mis-match
        if ((Request.class.isAssignableFrom(obj.getClass())) == false) {
            return false;
        }

        Request rs2 = (Request) obj;
        return rs2.getId() == this.getId();
    }

    /**
     * Get a comparator for sorting a list of requests by date ascending.
     *
     * @return the comparator
     */
    public static Comparator<Request> getComparator() {
        return new Comparator<Request>() {

            @Override
            public int compare(Request o1, Request o2) {
                if (o1 == null && o2 == null) {
                    return 0;
                }

                if (o1.getCreatedOn() == null) {
                    if (o2.getCreatedOn() == null) {
                        return 0;
                    } else {
                        return -1;
                    }
                } else {
                    if (o2.getCreatedOn() == null) {
                        return 1;
                    }
                }

                return o1.getCreatedOn().compareTo(o2.getCreatedOn());
            }
        };
    }

    /**
     * Get a comparator for sorting a list of requests by trackingNumber ascending.
     *
     * @return the tracking number comparator
     */
    public static Comparator<Request> getTrackingNumberComparator() {
        return new Comparator<Request>() {

            @Override
            public int compare(Request o1, Request o2) {
                if (o1 == null && o2 == null) {
                    return 0;
                }

                if (o1.trackingNumber == null) {
                    if (o2.trackingNumber == null) {
                        return 0;
                    } else {
                        return -1;
                    }
                } else {
                    if (o2.trackingNumber == null) {
                        return 1;
                    }
                }

                return o1.trackingNumber.trim().compareTo(o2.trackingNumber.trim());
            }
        };
    }

    /**
     * Get a comparator for sorting a list of requests by trackingNumber, descending.
     *
     * @return the desc tracking number comparator
     */
    public static Comparator<Request> getDescTrackingNumberComparator() {
        return new Comparator<Request>() {

            @Override
            public int compare(Request o1, Request o2) {
                if (o1 == null && o2 == null) {
                    return 0;
                }

                if (o1.trackingNumber == null) {
                    if (o2.trackingNumber == null) {
                        return 0;
                    } else {
                        return 1; // descending sort
                    }
                } else {
                    if (o2.trackingNumber == null) {
                        return -1; // descending sort
                    }
                }

                return o2.trackingNumber.trim().compareTo(o1.trackingNumber.trim()); // descending sort
            }
        };
    }
}
