/*
The contents of this file are subject to the Common Public Attribution License
Version 1.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.projity.com/license . The License is based on the Mozilla Public
License Version 1.1 but Sections 14 and 15 have been added to cover use of
software over a computer network and provide for limited attribution for the
Original Developer. In addition, Exhibit A has been modified to be consistent
with Exhibit B.

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License. The
Original Code is OpenProj. The Original Developer is the Initial Developer and
is Projity, Inc. All portions of the code written by Projity are Copyright (c)
2006, 2007. All Rights Reserved. Contributors Projity, Inc.

Alternatively, the contents of this file may be used under the terms of the
Projity End-User License Agreeement (the Projity License), in which case the
provisions of the Projity License are applicable instead of those above. If you
wish to allow use of your version of this file only under the terms of the
Projity License and not to allow others to use your version of this file under
the CPAL, indicate your decision by deleting the provisions above and replace
them with the notice and other provisions required by the Projity  License. If
you do not delete the provisions above, a recipient may use your version of this
file under either the CPAL or the Projity License.

[NOTE: The text of this license may differ slightly from the text of the notices
in Exhibits A and B of the license at http://www.projity.com/license. You should
use the latest text at http://www.projity.com/license for your modifications.
You may not remove this license text from the source files.]

Attribution Information: Attribution Copyright Notice: Copyright � 2006, 2007
Projity, Inc. Attribution Phrase (not exceeding 10 words): Powered by OpenProj,
an open source solution from Projity. Attribution URL: http://www.projity.com
Graphic Image as provided in the Covered Code as file:  openproj_logo.png with
alternatives listed on http://www.projity.com/logo

Display of Attribution Information is required in Larger Works which are defined
in the CPAL as a work which combines Covered Code or portions thereof with code
not governed by the terms of the CPAL. However, in addition to the other notice
obligations, all copies of the Covered Code in Executable and Source Code form
distributed must, as a form of attribution of the original author, include on
each user interface screen the "OpenProj" logo visible to all users.  The
OpenProj logo should be located horizontally aligned with the menu bar and left
justified on the top left of the screen adjacent to the File menu.  The logo
must be at least 100 x 25 pixels.  When users click on the "OpenProj" logo it
must direct them back to http://www.projity.com.
*/

package com.projity.pm.assignment;



import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;

import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;

import com.projity.algorithm.CollectionIntervalGenerator;
import com.projity.algorithm.DoubleValue;
import com.projity.algorithm.InstantIntervalGenerator;
import com.projity.algorithm.IntervalGeneratorSet;
import com.projity.algorithm.Merge;
import com.projity.algorithm.Query;
import com.projity.algorithm.RangeIntervalGenerator;
import com.projity.algorithm.ReverseQuery;
import com.projity.algorithm.SelectFrom;
import com.projity.algorithm.TimeIteratorGenerator;
import com.projity.algorithm.buffer.CalculatedValues;
import com.projity.association.Association;
import com.projity.association.InvalidAssociationException;
import com.projity.configuration.Configuration;
import com.projity.datatype.CanSupplyRateUnit;
import com.projity.datatype.Duration;
import com.projity.datatype.DurationFormat;
import com.projity.datatype.ImageLink;
import com.projity.datatype.Rate;
import com.projity.datatype.RateFormat;
import com.projity.datatype.TimeUnit;
import com.projity.document.Document;
import com.projity.field.Field;
import com.projity.field.FieldContext;
import com.projity.functor.IntervalConsumer;
import com.projity.functor.ObjectVisitor;
import com.projity.options.AdvancedOption;
import com.projity.pm.assignment.contour.AbstractContour;
import com.projity.pm.assignment.contour.AbstractContourBucket;
import com.projity.pm.assignment.contour.ContourBucketIntervalGenerator;
import com.projity.pm.assignment.contour.ContourFactory;
import com.projity.pm.assignment.contour.PersonalContour;
import com.projity.pm.assignment.functor.AssignmentFieldClosureCollection;
import com.projity.pm.assignment.functor.AssignmentFieldFunctor;
import com.projity.pm.assignment.functor.CalculatedValuesFunctor;
import com.projity.pm.assignment.functor.CostFunctor;
import com.projity.pm.assignment.functor.DateAtValueFunctor;
import com.projity.pm.assignment.functor.FixedCostFunctor;
import com.projity.pm.assignment.functor.PercentAllocFunctor;
import com.projity.pm.assignment.functor.PersonalContourMaker;
import com.projity.pm.assignment.functor.PrintValueFunctor;
import com.projity.pm.assignment.functor.ResourceAvailabilityFunctor;
import com.projity.pm.assignment.functor.ValueAtInstant;
import com.projity.pm.assignment.functor.WorkComparator;
import com.projity.pm.assignment.functor.WorkFunctor;
import com.projity.pm.assignment.functor.ZeroFunctor;
import com.projity.pm.assignment.timesheet.AssignmentWorkflowState;
import com.projity.pm.assignment.timesheet.TimesheetHelper;
import com.projity.pm.assignment.timesheet.TimesheetStatus;
import com.projity.pm.assignment.timesheet.UpdatesFromTimesheet;
import com.projity.pm.calendar.WorkCalendar;
import com.projity.pm.costing.Accrual;
import com.projity.pm.costing.EarnedValueCalculator;
import com.projity.pm.costing.EarnedValueFields;
import com.projity.pm.costing.EarnedValueValues;
import com.projity.pm.costing.HasCostRateIndex;
import com.projity.pm.criticalpath.TaskSchedule;
import com.projity.pm.key.HasKey;
import com.projity.pm.key.HasKeyImpl;
import com.projity.pm.resource.Resource;
import com.projity.pm.resource.ResourceImpl;
import com.projity.pm.scheduling.BarClosure;
import com.projity.pm.scheduling.ConstraintType;
import com.projity.pm.scheduling.Delayable;
import com.projity.pm.scheduling.Schedule;
import com.projity.pm.scheduling.ScheduleInterval;
import com.projity.pm.scheduling.ScheduleUtil;
import com.projity.pm.scheduling.SchedulingType;
import com.projity.pm.snapshot.Snapshottable;
import com.projity.pm.task.BelongsToDocument;
import com.projity.pm.task.NormalTask;
import com.projity.pm.task.Project;
import com.projity.pm.task.Task;
import com.projity.pm.time.HasStartAndEnd;
import com.projity.pm.time.ImmutableInterval;
import com.projity.pm.time.Interval;
import com.projity.pm.time.MutableInterval;
import com.projity.server.data.DataObject;
import com.projity.strings.Messages;
import com.projity.util.Alert;
import com.projity.util.Environment;
/**
 * Class representing resource assignments
 * @stereotype thing
 */
public final class Assignment implements Schedule, Association, Allocation, Delayable, HasTimeDistributedData, TimeDistributedFields, EarnedValueValues, EarnedValueFields, AssignmentSpecificFields, HasKey, BelongsToDocument, HasRequestDemandType,UpdatesFromTimesheet, CanSupplyRateUnit, HasCostRateIndex, Cloneable, DataObject  {
	static final long serialVersionUID = 56779404923241L;
	AssignmentDetail detail = null;
	private transient HasKeyImpl hasKey = new HasKeyImpl(true,this); //local ids only for assignments

	private static Field unitsFieldInstance = null;

	private transient Date cachedStart = null;
	private transient Date cachedEnd = null;
	private transient int timesheetStatus = TimesheetStatus.NO_DATA;
	private transient long lastTimesheetUpdate = 0;
	private transient int workflowState = AssignmentWorkflowState.NEW;
	private transient boolean timesheetAssignment = false;


	public static Field getUnitsField() {
		if (unitsFieldInstance == null)
			unitsFieldInstance = Configuration.getFieldFromId("Field.assignmentUnits");
		return unitsFieldInstance;
	}
	private static Field rateFieldInstance = null;
	public static Field getRateField() {
		if (rateFieldInstance == null)
			rateFieldInstance = Configuration.getFieldFromId("Field.rate");
		return rateFieldInstance;
	}

	private static Field requestDemandTypeInstance = null;
	public static Field getRequestDemandTypeField() {
		if (requestDemandTypeInstance == null)
			requestDemandTypeInstance = Configuration.getFieldFromId("Field.requestDemandType");
		return requestDemandTypeInstance;
	}


	public static Assignment getInstance(Task task, Resource resource, double units, int requestDemandType) {
		return new Assignment(task, resource, units, requestDemandType);
	}
	public static Assignment getInstance(Task task,	Resource resource,	double units, long delay) {
		return new Assignment(task, resource, units, delay);
	}
	/**
	 * Construct an assignment.  The arguments are those that are presented in the assign resource dialog
	 * @param task
	 * @param resource
	 * @param units
	 * @param requestDemandType  -Normally empty
	 */
	private Assignment(Task task,
					Resource resource,
					double units,
					int requestDemandType) {
		detail = new AssignmentDetail(task,resource,units,requestDemandType,0);
	}


	public Assignment(Assignment from) {
		detail = from.detail;
	}
	public Assignment(AssignmentDetail detail) {
		this.detail = detail;
	}

	public boolean isReadOnly() {
		return getTask().isReadOnly();
	}
	public boolean isExternal() {
		return getTask().isExternal();
	}

	public static Predicate instanceofPredicate() {
		return new Predicate() {
			public boolean evaluate(Object arg0) {
				return arg0 instanceof Assignment;
			}};
	}

/**
 * Copy the properies from another assignment.  This also includes the contour. However, it does NOT
 * include the resource or the units.  This is called in the context of replacing an assignment.
 * @param from Assignment to copy from
 */
	public void usePropertiesOf(Assignment from) {
		boolean compatibleTypes = isLabor() == from.isLabor();
		double units = getUnits();
		Rate r = getRate();
		Resource resource = getResource();
		detail = (AssignmentDetail) from.detail.clone();
	    detail.replaceResourceAndUnits(units,resource);

		if (!compatibleTypes) // don't want to set units if not compatible
			detail.setRate(r);
	}

	public void setStart(long start) {
		detail.setStart(start);
	}

	public int getRequestDemandType() {
		return detail.getRequestDemandType();
	}

	public void setRequestDemandType(int requestDemandType) {
		newDetail().setRequestDemandType(requestDemandType);
	}

	public AbstractContourBucket[] getContour(Object type) {
		return detail.getContour(type);
	}

/**
 * Constructor adapted to mpx
 * @param task
 * @param resource
 * @param units
 * @param delay
 */
	private Assignment(Task task,
			Resource resource,
			double units,
			long delay) {
		detail = new AssignmentDetail(task,resource,units,RequestDemandType.NONE,delay);
	}

	/**
	 * Accessor for units (value of work)
	 * @return units
	 */
	public final double getUnits() {
		return detail.getUnits();
	}

	public final double getLaborUnits() {
		if (isLabor())
			return getUnits();
		return 0.0D;
	}


	public void setOvertimeWork(long overtimeWork) {
		newDetail().setOvertimeWork(overtimeWork);
	}

/**
 * For use in sharing with external systems such as salesforce. Uniquely identifies object
 * @return
 */
	public String toExternalId() {
		return getResourceId() + "." + getTaskId();
	}

	public String toString() {
		return getTask() + "/" + getResource(); //super.toString();
//		return "[start] " + new java.util.Date(getStart())
//		      + "\n[end] " +  new java.util.Date(getEnd())
//			  +"\n[units] " + getUnits()// in hours
//			  +"\n[work] " + getWork() / (1000*60*60); // in hours
	}

	/**
	 * @return Returns the contour.
	 */
	public AbstractContour getWorkContour() {
		return detail.getWorkContour();
	}

	/**
	 * @param contour The contour to set.
	 * TODO get rid of this
	 */
	public void debugSetWorkContour(AbstractContour contour) {
		newDetail().setWorkContour(contour);
	}

	/**
	 * Accessor for the assignment's delay
	 * @return delay
	 */
	public long getDelay(){
		return detail.getDelay();
	}

	public void setDelay(long delay) {
		newDetail().setDelay(delay);
	}

	public long getLevelingDelay(){
		return detail.getLevelingDelay();
	}

	public void setLevelingDelay(long levelingDelay){
		newDetail().setLevelingDelay(levelingDelay);
	}


	/**
	 * @return Returns the duration.
	 */
	public long getDurationMillis() {
		return detail.getDuration();
	}

	private void setDurationMillis(long durationMillis) {
		newDetail().setDuration(durationMillis);
	}

	public void setDuration(long duration) {
		adjustRemainingDuration(duration - getActualDuration(),false);
	}
	/**
	 * @param duration The duration to set.
	 */
	public void adjustRemainingDuration(long newRemainingDuration, boolean doChildren) {
		long old = getRemainingDuration();
		newRemainingDuration = Duration.millis(newRemainingDuration); // don't use time unit
		if (getUnits() == 0) // take care of degenerate case
			newRemainingDuration = 0;
		newDetail().adjustRemainingDuration(newRemainingDuration);


//TODO commented out may 8 2007
//		if (getTaskSchedulingType() == SchedulingType.FIXED_WORK && newRemainingDuration != 0) {
//			double multiplier = ((double)old) / newRemainingDuration;
//			if (multiplier > 0) {
//				detail.setWorkContour(detail.getWorkContour().adjustUnits(multiplier, getActualDuration()));
//				detail.setUnits(getUnits() * multiplier);
//			}
//		}



	}

	public void adjustRemainingDurationIfWorkingAtTaskEnd(long newRemainingDuration) {
		if (getEnd() >= getTask().getEnd() || !isInitialized())
			adjustRemainingDuration(newRemainingDuration,false);
	}
	public void adjustRemainingWork(double multiplier, boolean doChildren) {
		if (getPercentComplete() > 0.0D) // if actuals, then remaining may have different contour
			makeContourPersonal(); //fix?
		newDetail().adjustRemainingWork(multiplier);
	}

	/**
	 * @param units The units to set.
	 */
	public void adjustRemainingUnits(double newRemainingUnits, double oldRemainingUnits, boolean doChildren, boolean conserveTotalUnits) {
		if (!isTemporal()) // for absolute quantities, don't change
			return;
		oldRemainingUnits = getRemainingUnits();
		NormalTask task =(NormalTask)getTask();
	//	task.adjustUnitsDelta(newRemainingUnits - oldRemainingUnits);
		if (doChildren) {
			task.getSchedulingRule().adjustRemainingUnits(this, newRemainingUnits,
				oldRemainingUnits, false, false);
		} else { // just treat the assignment
			if (getPercentComplete() > 0.0D) // if actuals, then remaining may have different contour
				makeContourPersonal();

			newDetail().adjustRemainingUnits(newRemainingUnits);
		}
	}

	public void setWork(long work, FieldContext context) {
		work = Duration.millis(work);
		if (isLabor() && work < 60000) {
			work *= Duration.timeUnitFactor(TimeUnit.HOURS);
			System.out.println("modifying invalid work to make it hours");
		}
		long remainingWork = work - getActualWork(null);
		WorkCalendar cal = getEffectiveWorkCalendar();
		if (!FieldContext.hasInterval(context)) {
			long currentRemainingWork = Duration.millis(getWork(context)) - Duration.millis(getActualWork(context));
			if (currentRemainingWork == 0 && remainingWork == 0)
				adjustRemainingUnits(0, 0, true, false); //TODO is this ok?
			else {
				if (getTaskSchedulingType() == SchedulingType.FIXED_UNITS)
					adjustRemainingDuration(remainingWork, false);
				else if (getTaskSchedulingType() == SchedulingType.FIXED_DURATION && currentRemainingWork > 0)
					adjustRemainingWork( ((double)remainingWork) / currentRemainingWork, true);
//					newDetail().setWorkContour(getWorkContour().adjustUnits((((double)remainingWork) / currentRemainingWork), getActualDuration()));

				else
					newDetail().adjustRemainingDuration(remainingWork); // just set duration

			}
		} else {
			long start = cal.adjustInsideCalendar(context.getStart(),false);
			long end = cal.adjustInsideCalendar(context.getEnd(),true);
			if (end > start)
				setWorkInterval(start, end, work);
			if (getRate().getValue() == 0.0D) {// special case if units was 0
				double rate = ((double)work) / cal.compare(end,start,false); // use rate for the period as the assignment's rate
				this.forceUnits(rate);
			}
		}
	}

	private boolean addWorkingTimeIfRequired(long work,FieldContext context, boolean actual) {
		WorkCalendar cal = getEffectiveWorkCalendar();
		// adjust start and end inside the calendar
		long start = cal.adjustInsideCalendar(context.getStart(),false);
		long end = cal.adjustInsideCalendar(context.getEnd(),true);
		if (work != 0 && !verifyBounds(start,end))
			return false;

		// if adding during non calendar time
		if (end <= start && work != 0) {
			start = context.getStart();
			end = context.getEnd();
	//		example 10 day task -	MTWTFSSMTWTF
	//		initial bar:        	=====  =====
	//		calendar time add    	====== ====   here I make a saturday working, so the task finishes sooner
	//        shift the saturday   	=====  =====  shifting the saturday puts the task back as it was
	//		set the interval     	====== =====  now I set the saturday value

//			System.out.println("before adding" + getWorkContour());
			addCalendarTime(start, end);
			cal = getEffectiveWorkCalendar();
			if (actual && getTask().getActualStart() == 0) {
				long taskStart = cal.adjustInsideCalendar(start, false);
				((NormalTask)getTask()).setActualStartNoEvent(taskStart);
			}
			long addedDuration = cal.compare(end,start, false);
//			System.out.println("before shift" + getWorkContour());
			shift(start,end, addedDuration); // shift the remaining contour to the right by the duration of the inserted time
//			System.out.println("after shift" + getWorkContour());

			getTask().recalculateLater(this); // task needs to be recalculated

			//		SwingUtilities.invokeLater(new Runnable());

		}
		return true;
	}

	public void makeFlatIfPossible() {
		if (getWorkContour().isPersonal()) {
			detail.recalculateDuration(); // set duration from contour
			detail.setWorkContour(((PersonalContour)getWorkContour()).convertToFlatIfPossible());
		}

	}

	public void setActualWork(long actualWork, FieldContext fieldContext) {
		long workValue = Duration.millis(actualWork);

		if (FieldContext.hasInterval(fieldContext)) {

			boolean isComplete = getPercentComplete() == 1.0D;
			long oldWork = work();


			if (!addWorkingTimeIfRequired(actualWork,fieldContext, true))
				return;

			//if the task isn't yet started and setting work, move to start date
			if (getTask().getActualStart() == 0 && actualWork != 0) {
				long s = getEffectiveWorkCalendar().adjustInsideCalendar(fieldContext.getStart(), false);
				getTask().setActualStart(s); //TODO handle if setting non calendar time
			}



			long stop = getStop();

			setWork(actualWork,fieldContext);		//when entering time phased actuals, first thing is adjust the work contour

//			if (!isComplete)
//				((NormalTask)getTask()).getSchedulingRule().adjustRemainingWork(this, newRemainingWork, remainingWork, false);


			// need to 0 out an time between stop and the intervals start, if any
			if (fieldContext.getStart() > stop) {
				FieldContext empty = new FieldContext();
				empty.setInterval(new ImmutableInterval(stop == 0 ? getStart() : stop,fieldContext.getStart()));
				setWork(0L,empty);
			}
			if (workValue > 0) {

				 if (fieldContext.getEnd() > stop)  // if will set stop later because new work is after stop
					 setStop(fieldContext.getEnd());
				 else
					 setStop(stop); // use current stop - for case when setting actuals before current stop
			} else {
				if (fieldContext.getStart() < stop && fieldContext.getEnd() >= stop ) {//if overlapping current stop
					setStop(fieldContext.getStart());
				} else {
					if (stop != 0)
						setStop(stop); // make sure stop stays where it is
				}
			}
//			if (!isComplete)
//				((NormalTask)getTask()).getSchedulingRule().adjustRemainingWork(this, newRemainingWork, remainingWork, false);

		//	adjust remaining work
			long remainingWork = oldWork - getActualWork(null);


			//				adjustRemainingWork( ((double)remainingWork) / currentRemainingWork, true);

			if (!isComplete) {
				((NormalTask)getTask()).getSchedulingRule().adjustRemainingWork(this,  remainingWork, false);
			}
			getTask().recalculateLater(this); // task needs to be recalculated
		} else {
			if (workValue ==0) {
				setPercentComplete(0);
				return;
			}
			long  date = ReverseQuery.getDateAtValue(WORK, this, workValue, false);
			setStop(date);
		}

	}


	public boolean isInitialized() {
		return getProject().isInitialized();
	}

	/**
	 * Allow setting of working duration. MSProject displays working duration (excludes non-work intervals) except when
	 * task type is fixed duration, in which case it shows duration with non-work intervals
	 * @param newWorkingDuration
	 */
	public void adjustWorkingDuration(long newWorkingDuration) {
		newDetail().adjustWorkingDuration(newWorkingDuration);
	}


	public WorkCalendar getEffectiveWorkCalendar() {
		return detail.getEffectiveWorkCalendar();
	}
	/**
	 * @return Returns the task.
	 */
	public Task getTask() {
		return detail.getTask();
	}

	public String getTaskName() {
		return getTask().getName();
	}

	public String getTaskId() {
		return ""+getTask().getId();
	}

	public String getResourceName() {
		return getResource().getName();
	}

	public String getResourceId() {
		return ""+getResource().getId();
	}

	public long getStart() {
		return detail.getStart();
	}

	public long getFinish() {
		return detail.getFinish();
	}

/**
 * Offset a date by its duration or remaining duration
 * @param date
 * @param ahead - true if we want to add duraiton, false otherwise
 * @param remainingOnly - true if use remaining duration, otherwise all duration
 * @param useSooner - Used for end/beginning of day issues
 * @return new date
 */

	private long computeStart(long startDate, long dependencyDate) {
		long delay = this.calcTotalDelay();
		long s = startDate;
		if (getPercentComplete() == 0) { // if no completion, use task dependency date if needed
			if (dependencyDate > s)
				s = dependencyDate;
		}
		if (delay > 0)
			s = getEffectiveWorkCalendar().add(s,delay,true);
		//TODO check if above should use task calendar or assignment calendar
		return s;
	}



	public long calcOffsetFrom(long startDate, long dependencyDate, boolean ahead, boolean remainingOnly, boolean useSooner) {
		long start;
		if (ahead)
			start = computeStart(startDate,dependencyDate);
		else
			start = startDate;
		if (getPercentComplete() > 0)
			start=getEffectiveWorkCalendar().add(start,getActualDuration(),useSooner);


		long duration = remainingOnly ? detail.getRemainingDuration() : detail.getDuration();// + this.detail.getSplitDuration();
//TODO integrate split - still needed?

		long amount = (ahead ? duration : -duration);
		return  getEffectiveWorkCalendar().add(start,amount, useSooner);
	}


	boolean isInRange(long start, long finish) {
		long s = getStart();
		return (finish > s && start < getEffectiveWorkCalendar().add(s,detail.getDuration(),true));
	}

	/**
	 * @return Returns the resource.
	 */
	public Resource getResource() {
		return detail.getResource();
	}


	public Closure forResource(Closure visitor) {
		return new ObjectVisitor(visitor) {
			protected final Object getObject(Object caller) {
				return ((Assignment)caller).getResource();
			}
		};
	}

	public Closure forTask(Closure visitor) {
		return new ObjectVisitor(visitor) {
			protected final Object getObject(Object caller) {
				return ((Assignment)caller).getTask();
			}
		};
	}

	/**
	 * @return
	 */
	public long calcWork() {
		if (!isLabor())
			return 0;
		return detail.calcWork();
	}

	/**
	 * @return
	 */
	public long calcTotalDelay() {
		return detail.calcTotalDelay();
	}

	/**
	 * @return Returns the detail.
	 */
	public AssignmentDetail getDetail() {
		return detail;
	}



	/**
	 * Gets an instance of a generator that acts on the cost contour
	 * @return instance of generator
	 */
	public ContourBucketIntervalGenerator contourGeneratorInstance(Object type)   {
		return contourGeneratorInstance(type,getStart());

	}

	/**
	 * Gets an instance of a generator that acts on the cost contour
	 * @return instance of generator
	 */
	public ContourBucketIntervalGenerator contourGeneratorInstance(Object type, long start)   {
		return ContourBucketIntervalGenerator.getInstance(this,type);//getEffectiveWorkCalendar(), detail.getContour(type), detail.getDurationMillis(), getStart());
		//TODO actual cost treatment if entered manually
	}

	/**
	 * Fills in a SelectFrom clause with "select work from contour"
	 * @param clause to fill in
	 * @return work field functor
	 */
	AssignmentFieldFunctor work(SelectFrom clause) {
		ContourBucketIntervalGenerator contour = contourGeneratorInstance(WORK);
		WorkFunctor workF = WorkFunctor.getInstance(this, contour.getWorkCalendar(), contour, detail.calcOvertimeUnits());
		clause.select(workF).from(contour);
		return workF;
	}



//	public AssignmentFieldFunctor work(SelectFrom clause, AssignmentFieldFunctor using) {
//		ContourBucketIntervalGenerator contour = using.getContourBucketIntervalGenerator();
//		WorkFunctor workF = WorkFunctor.getInstance(this, contour.getWorkCalendar(), contour, detail.calcOvertimeUnits());
//		clause.select(workF);
//		return workF;
//	}

	private AssignmentFieldFunctor percentAlloc(SelectFrom clause, boolean threshold) {
		ContourBucketIntervalGenerator contour = contourGeneratorInstance(WORK);
		PercentAllocFunctor functor = PercentAllocFunctor.getInstance(this, contour.getWorkCalendar(), contour,threshold);
		clause.select(functor).from(contour);
		return functor;
	}

	private AssignmentFieldFunctor fixedCost(SelectFrom clause) {
		ContourBucketIntervalGenerator contour = contourGeneratorInstance(COST);
		FixedCostFunctor functor = FixedCostFunctor.getInstance(this);
		clause.select(functor).from(contour);
		return functor;
	}

	private AssignmentFieldFunctor availability(SelectFrom clause) {
		//TODO this is untested, as histogram is currently broken 8/12/04 hk
		ResourceAvailabilityFunctor functor = ResourceAvailabilityFunctor.getInstance(this);
		CollectionIntervalGenerator availability = CollectionIntervalGenerator.getInstance(((ResourceImpl)detail.getResource()).getAvailabilityTable().getList());
		clause.select(functor)
				.from(availability); //use range?
		return functor;
	}

	/** This version does not depend on the assignment
	 *
	 * @param clause
	 * @param resource
	 * @return
	 */
	private static AssignmentFieldFunctor resourceAvailability(SelectFrom clause, Resource resource) {
		//TODO this is untested, as histogram is currently broken 8/12/04 hk
		ResourceAvailabilityFunctor functor = ResourceAvailabilityFunctor.getInstance(resource);
		CollectionIntervalGenerator availability = CollectionIntervalGenerator.getInstance(resource.getAvailabilityTable().getList());
		clause.select(functor)
				.from(availability); //use range?
		return functor;
	}

//	PercentAllocFunctor(Assignment assignment, WorkCalendar workCalendar, ContourBucketIntervalGenerator contourBucketIntervalGenerator) {
	/**
	 * Convenience function to fill in the SelectFrom clause and return the cost functor.
	 * One particularity of this function is that if the cost has been saved in the cost contour, then the cost contour is used.
	 * This is the usual case in baselines.
	 * If the cost contour is not personal, then the cost is calculated from costRate and work; however, the resource accrual settings
	 * determine how the data is grouped: If accrual is not PRORATED, then the entire cost is calculated, and the result is then put
	 * in a special ConstantCost functor which will return the value if and only if the current group by range encloses the value.
	 * @param clause: SelectFrom clause to populate
	 * @return The functor to use to calculate cost
	 */
	private AssignmentFieldFunctor cost(SelectFrom clause, boolean all) {
		if (detail.getCostContour().isPersonal()) { // in the case where a cost contour has already been saved, use it
			ContourBucketIntervalGenerator costContour = contourGeneratorInstance(COST);
			WorkFunctor workF = WorkFunctor.getInstance(this, getEffectiveWorkCalendar(), costContour, detail.calcOvertimeUnits()); // note that is is a work contour
			clause.select(workF).from(costContour);
			return workF;
		} else {
			boolean prorated = isProratedCost();
			// if prorated, or if calculating a total, then treat as prorated
			if (all || prorated) {
				ContourBucketIntervalGenerator workContour = contourGeneratorInstance(WORK);
				CollectionIntervalGenerator costRate = CollectionIntervalGenerator.getInstance(detail.getResource().getCostRateTable(detail.getCostRateIndex()).getList());
				clause.from(costRate).from(workContour);
				// Note that the getStart() parameter implies cost per use is applied at start
				CostFunctor costF = CostFunctor.getInstance(this, getEffectiveWorkCalendar(), workContour, detail.calcOvertimeUnits(), costRate, getStart(),prorated);
				clause.select(costF);
				return costF;
			} else { // accrue start or end
				long triggerDate = (detail.getResource().getAccrueAt() == Accrual.START) ? getStart() : getFinish();// use start or end
				AssignmentFieldFunctor constantCost = ValueAtInstant.getInstance(triggerDate, calcAll(COST));
				clause.select(constantCost).from(InstantIntervalGenerator.getInstance(triggerDate)); // just one instant
				return constantCost;
			}
		}
	}

	public boolean isProratedCost() {
		return detail.getResource().getAccrueAt() == Accrual.PRORATED;
	}

	public AssignmentFieldFunctor getDataSelect(Object type, SelectFrom clause, boolean all) {
		if (type == PERCENT_ALLOC) {
			return percentAlloc(clause,false);
		} else if (type == AVAILABILITY) {
			return availability(clause);
		} else if (type == COST) {
			return cost(clause,all);
		} else if (type == OVERALLOCATED) {
			return work(clause);
		} else if (type == WORK || type == THIS_PROJECT) {
			return work(clause);
		} else if (type == ACTUAL_WORK) {
			clause.whereInRange(detail.getStart(),getStop());
			return work(clause);
		} else if (type == REMAINING_WORK) {
			clause.whereInRange(getResume(),detail.getFinish());
			return work(clause);
		} else if (type == ACTUAL_COST) {
			clause.whereInRange(detail.getStart(),getStop());
			return cost(clause,all);
		} else if (type == FIXED_COST) {
			clause.whereInRange(detail.getStart(),getEnd());
			return fixedCost(clause);
		} else if (type == ACTUAL_FIXED_COST) {
			clause.whereInRange(detail.getStart(),getStop());
			return fixedCost(clause);
		} else if (type == REMAINING_COST) {
			clause.whereInRange(getResume(),detail.getFinish());
			return cost(clause,all);
		} else if (type == BASELINE_COST) {
			return baselineData(COST,clause);
		} else if (type == BASELINE_WORK) {
			return baselineData(WORK,clause);
		} else if (type == ACWP) {
			clause.whereInRange(detail.effectiveBaselineStart(),getCompletedOrStatusDate());
			return cost(clause,all);
		} else if (type == BCWP) {
			clause.whereInRange(detail.getStart(),getStatusDate());
			AssignmentFieldFunctor costF = cost(clause,all);
			costF.setMultiplier(efficiency());
			return costF;
		} else if (type == BCWS) {
			clause.whereInRange(detail.effectiveBaselineStart(),getStatusDate());
			return baselineData(COST,clause);
		}  else if (type instanceof Field) { // treat all baselines
			Field field = (Field)type;
			if (field.isIndexed()) {
				Integer index = new Integer(field.getIndex());
				if (field.isWork())
					return baselineData(WORK,clause,index);
				else
					return baselineData(COST,clause,index);
			}
		}

		return null;
	}


	public void calcDataBetween(Object type, long start, long end) {
		SelectFrom clause = SelectFrom.getInstance().whereInRange(start,end); // automatically also adds a generator to limit range
		AssignmentFieldFunctor dataFunctor = getDataSelect(type,clause,false);

		RangeIntervalGenerator dailyInRange = RangeIntervalGenerator.getInstance(start, end, 1000*60*60*24);
		PrintValueFunctor print = PrintValueFunctor.getInstance(dataFunctor);
		Query.getInstance().selectFrom(clause)
		.groupBy(dailyInRange)
		.action(print)
		.execute();
	}

	/**
	 * Calculate the total cost
	 * @return
	 */
	public double calcAll(Object type) {
		SelectFrom clause = SelectFrom.getInstance();
		AssignmentFieldFunctor dataFunctor = getDataSelect(type,clause,true);
		Query.getInstance().selectFrom(clause)
		.execute();
		return dataFunctor.getValue();
	}

//	public void forEach(Object type, Closure actionVisitor) {
//		SelectFrom clause = SelectFrom.getInstance();
//		AssignmentFieldFunctor dataFunctor = getDataSelect(type,clause,false);
//		Query.getInstance().selectFrom(clause)
//		.action(actionVisitor)
//		.execute();
//	}

	public void buildReverseQuery(ReverseQuery reverseQuery) {
		SelectFrom clause = SelectFrom.getInstance();
		reverseQuery.addField(getDataSelect(reverseQuery.getType(),clause,false));
		reverseQuery.addGroupBy(IntervalGeneratorSet.extractUnshared(clause.getFromIntervalGenerators()));
		reverseQuery.addSelectFrom(clause);
	}



/**
 * calls back on for all "gantt bar" intervals having matching rates.  Matching is determined by a comparator.
 * @param visitor : callback
 * @param mergeWorking : if true, then matching occurs if the periods hava non-zero work, otherwise the work
 * must be an exact match.
 */
	public void forEachWorkingInterval(Closure visitor, boolean mergeWorking, WorkCalendar workCalendar) {
		Comparator comparator = (mergeWorking ? null : WorkComparator.getInstance()); 	// if a value of null is used , then the bars will be grouped based on time only
		Merge merge =  Merge.getInstance(visitor, comparator);
		Query.getInstance().groupBy(contourGeneratorInstance(WORK))
							.action(merge)
							.execute();
	}

	private transient static BarClosure barClosureInstance = new BarClosure();

	public void consumeIntervals(IntervalConsumer consumer) {
		barClosureInstance.initialize(consumer,this);
		boolean inProgress = getPercentComplete() > 0.0D;
		MutableInterval bounds = null;
		if (inProgress) {
			bounds = new MutableInterval(getStart(),getStop());
			barClosureInstance.setBounds(bounds);
		}
		forEachWorkingInterval(barClosureInstance,true,getEffectiveWorkCalendar());
		if (inProgress) {
//			barClosureInstance.initCount();
			bounds.setStart(bounds.getEnd()); // shift for second half
			bounds.setEnd(getEnd());

			forEachWorkingInterval(barClosureInstance,true,getEffectiveWorkCalendar());
		}
		barClosureInstance.setBounds(null);
	}

	public void moveInterval(Object eventSource, long start, long end, ScheduleInterval oldInterval, boolean isChild) {
		long startShiftDuration = getEffectiveWorkCalendar().compare(start,oldInterval.getStart(),false);
		long endShiftDuration = getEffectiveWorkCalendar().compare(end,oldInterval.getEnd(),false);
		boolean shifting = startShiftDuration != 0L && endShiftDuration != 0L;
		long stop = getStop(); // Store old completion
		if (shifting) {
			shift(oldInterval.getStart(),oldInterval.getEnd(),startShiftDuration);
		} else {
			if (endShiftDuration != 0)
				extend(oldInterval.getStart(),oldInterval.getEnd(),endShiftDuration);
			else
				extendBefore(oldInterval.getStart(),oldInterval.getEnd(),startShiftDuration);
		}

		setStop(stop); // put back old completion
		//Need to update schedule if the assignment bar was moved
		if (!isChild) {
			getTask().updateCachedDuration();
			getTask().recalculate(eventSource);
		}

//		//Undo
//		UndoableEditSupport undoableEditSupport=getProject().getUndoController().getEditSupport();
//		if (undoableEditSupport!=null&&!(eventSource instanceof UndoableEdit)){
//			undoableEditSupport.postEdit(new ScheduleEdit(this,new ScheduleInterval(start,end),oldInterval,isChild,eventSource));
//		}

	}

	/**
	 * This function will calculate the exact date/time when the value is achieved
	 * @param type
	 * @param value
	 * @return
	 */
	public long getDateAtValue(Object type, double value) {
		SelectFrom clause = SelectFrom.getInstance();
		AssignmentFieldFunctor dataFunctor = (type==COST) ? cost(clause, true) : work(clause);

		DateAtValueFunctor dateAtValue = DateAtValueFunctor.getInstance(value, AssignmentFieldClosureCollection.getInstance(dataFunctor));
		clause.select(dateAtValue); // override existing select

		Query.getInstance().selectFrom(clause)
		.execute();
		return dateAtValue.getDate();
	}

	public long getDateAtWorkFraction(double workFraction) {
//		System.out.println("all work is " + calcAll(WORK) / (1000*60*60*8));
		return getDateAtValue(WORK,calcAll(WORK) * workFraction);
	}


	private AssignmentFieldFunctor baselineData(Object type, SelectFrom baselineSelectFrom) {
		Assignment baselineAssignment = detail.getBaselineAssignment();
		AssignmentFieldFunctor baselineFunctor;
		if (baselineAssignment == null) {
			baselineFunctor = ZeroFunctor.getInstance();
			baselineSelectFrom.select(baselineFunctor).from(RangeIntervalGenerator.empty());
		} else {
			baselineFunctor = baselineAssignment.getDataSelect(type,baselineSelectFrom, false);
		}
		return baselineFunctor;
	}

	public Assignment getBaselineAssignment(Object baseline, boolean createIfDoesntExist) {
		return detail.getBaselineAssignment(baseline, createIfDoesntExist);
	}

	private AssignmentFieldFunctor baselineData(Object type, SelectFrom baselineSelectFrom, Object baseline) {
		Assignment baselineAssignment = detail.getBaselineAssignment(baseline, false);
		AssignmentFieldFunctor baselineFunctor;
		if (baselineAssignment == null) {
			baselineFunctor = ZeroFunctor.getInstance();
			baselineSelectFrom.select(baselineFunctor).from(RangeIntervalGenerator.empty());
		} else {
			baselineFunctor = baselineAssignment.getDataSelect(type,baselineSelectFrom, false);
		}
		return baselineFunctor;
	}

	private long getStatusDate() {
		return getProject().getStatusDate();
	}
	private long getCompletedOrStatusDate() {
		return Math.min(getStatusDate(),detail.getStop());
	}


	public double getCost() {
		return calcAll(COST);
	}

	/**
	 * Set the total delay for the assignment.  Priority is given to changing the assignment delay over the
	 * leveling delay.
	 * @param newDelay
	 */
	void setTotalDelay(long newDelay) {

		/*
		 *   DDDDDDDLLLLLL
		 *   NNNNNNNNNN
		 */

//		long delay = getDelay();
//		long levelingDelay = getLevelingDelay();
//		long oldTotalDelay = calcTotalDelay();
//		if (newDelay == oldTotalDelay)
//			return;
//
//		if (newDelay <= delay) {
//			delay = newDelay;
//			levelingDelay = 0;
//		}
//		else {
//			delay = 0;
//			levelingDelay =
//		}
//		if (newDelay < oldTotalDelay) {
//			delay -= newDelay;
//			if (delay < 0) {
//				levelingDelay += delay;
//				delay = 0;
//			}
//		} else {
//			delay += (newDelay - oldTotalDelay);
//		}
//		setDelay(delay);
//		setLevelingDelay(levelingDelay);

		setDelay(newDelay);
		setLevelingDelay(0);
	}

	/**
	 * This treats just a single interval to merge in
	 * @param type - WORK or COST. can also be a time distributed field
	 * @param start - start of interval
	 * @param end - end of interval
	 * @param value - value of interval
	 */
	public void setInterval(Object type, long start, long end, double value) {
		//TODO treat cost values - they are currently ignored
		if (type == ACTUAL_WORK || type == WORK || type == REMAINING_WORK) {
			if (value != 0 && !verifyBounds(start,end))
				return;
			if (Environment.isImporting()) {
				WorkCalendar cal = getEffectiveWorkCalendar();
				start = cal.adjustInsideCalendar(start,false);
				end = cal.adjustInsideCalendar(end,true);
			}

			setWorkInterval(start,end,value);
			if (type == ACTUAL_WORK && end > getStop() && value > 0.0D) // bug fix - actuals were lost
				setStop(end);
		} else {
			if (TimeDistributedHelper.isWork(type)) {
				Object baseline = TimeDistributedHelper.baselineForData(type);
//System.out.println("baseline " + baseline + " " + new Date(start) + " " + new Date(end) + " " + value);

				Assignment baselineAssignment = getBaselineAssignment(baseline, true); // get or create baseline assignment

				// update the task schedule too
				TaskSchedule schedule = baselineAssignment.getTaskScheduleOfAssignment();
				long baselineStart = schedule.getStart();
				long baselineFinish = schedule.getFinish();
				if (baselineStart == 0 || baselineStart > start)
					schedule.setStart(start);
				if (baselineFinish == 0 || baselineFinish < end)
					schedule.setEnd(end);

				baselineAssignment.setWorkInterval(start,end,value);
			}
		}
	}
	TaskSchedule getTaskScheduleOfAssignment() {
		return detail.getTaskSchedule();
	}

	public boolean verifyBounds(long start, long end) {
		if (Environment.isImporting()) // always accept time distributed edits when importing
			return true;
		if (getProject().getStart() > start) // if setting before project start
			return Alert.okCancel(Messages.getString("Message.allowDistrbutedStartBeforeProjectStart"));
		else if (start < getTask().getStart())
			return Alert.okCancel(Messages.getString("Message.allowDistrbutedStartBeforeTaskStart"));
		else
			return true;

	}
	/**
	 * This treats just a single interval to merge in
	 * @param type - WORK or COST
	 * @param start - start of interval
	 * @param end - end of interval
	 * @param value - value of interval (an wmount of work (duration), not units)
	 */
	private void setWorkInterval(long start, long end, double value) {
		moveDelayToContour();
		long assignmentStart = getStart(); // the start including any delay
		WorkCalendar cal = getEffectiveWorkCalendar();

		if (end > getFinish()) { //  add end if necessary
			if (value == 0) {
				this.setEnd(start);
				return; // don't extend if value is 0
			}
			makeContourPersonal();
			long extraDuration = cal.compare(end,getFinish(),false);
			setDurationMillis(getDurationMillis() + extraDuration);
			AbstractContour contour = PersonalContour.addEmptyBucket(getWorkContour(), extraDuration,true);
			newDetail().setWorkContour(contour);

			// adjust task end too
			if (end > getTask().getEnd())
				getTask().setEnd(end);
		} else if (start < assignmentStart) { // if before assignment start
//			if (value == 0)
//				return; // don't extend if value is 0
			long taskStart = getTask().getStart();
			if (start < taskStart)  {
				long shiftAmount = cal.compare(taskStart,start, false);
				setDurationMillis(getDurationMillis() + shiftAmount);
				if (!getWorkContour().isPersonal())
					makeContourPersonal();
				newDetail().setWorkContour(PersonalContour.addEmptyBucket(getWorkContour(), shiftAmount,false)); // add empty space before
				getTask().setScheduleConstraintAndUpdate(ConstraintType.SNLT, start); // this can set start before project duration.
				taskStart = getTask().getStart();
			}
			assignmentStart = taskStart;
		}

		long startDuration = cal.compare(start,assignmentStart,false);
		long endDuration = cal.compare(end,assignmentStart,false);
		if (startDuration == endDuration)
			return;

		if (!getWorkContour().isPersonal())
			makeContourPersonal();

		value /= (endDuration - startDuration);
		PersonalContour newContour = ((PersonalContour)getWorkContour()).setInterval(startDuration,endDuration,value);

		extractDelayFromContour(newContour); // move delay back into delay field from contour
		newDetail().setWorkContour(newContour);
		newDetail().recalculateDuration();
		getTask().updateCachedDuration();
	}

	/**
	 * Move remaining work to the date given.  It will either be moved later or sooner.  Also, if at the date, there
	 * is a gap in the work, the gap is reduced so that the work begins on the date
	 * @param date
	 */
	public void moveRemainingToDate(long date) {
		long resume = getResume();
		long end = getEnd();
		long shiftDuration = getEffectiveWorkCalendar().compare(date,resume,false); // get shift time
		shift(resume,end,shiftDuration);

		long duration = getEffectiveWorkCalendar().compare(date,getStart(),false); // get offset from start
		if (duration > 0)
			newDetail().removeEmptyBucketAtDuration(duration);
	}

	public void shift(long start, long end, long shiftDuration) {
		//get bounds.
		WorkCalendar cal = getEffectiveWorkCalendar();

		if (getTask().inProgress() && cal.compare(getStop(),start,false) > 0) { // if stop greater than start, can't change it
		//	System.out.println("Not shifting - stop = " + new Date(getStop()) + "start " + new Date(start));
			return;
		}
		long taskStart = getTask().getStart();
		start = Math.max(start,getStop()); //only shift remaining
		start = Math.max(start,taskStart); // dont shift before task start of course
		long startOffset = cal.compare(start,taskStart,false);
		long endOffset = cal.compare(end,taskStart,false);

		if (getResume() != 0 && start >= getResume()) {
			long splitDuration = detail.getSplitDuration();
			startOffset -= splitDuration;
			endOffset -= splitDuration;
		}

		if (startOffset >=endOffset) // this prevents moving a completed interval
			return;
		boolean firstBar = (cal.compare(getStart(),start,false) == 0);
		MutableInterval range = getRangeThatIntervalCanBeMoved(start,startOffset,endOffset);
		if (firstBar)
			range.setStart(range.getStart() - calcTotalDelay());
		if (shiftDuration > 0)
			shiftDuration = Math.min(shiftDuration,range.getEnd() - endOffset); // don't allow to shift more than possible
		else {
			shiftDuration = Math.max(shiftDuration,range.getStart() /*+ (firstBar ? -calcTotalDelay() : 0)*/ - startOffset); // don't allow to shift more than possible
		}
		if (firstBar) {
			setTotalDelay(calcTotalDelay() + shiftDuration);
		} else {
			newDetail().shift(startOffset,endOffset,shiftDuration);
		}
	}

	private MutableInterval getRangeThatIntervalCanBeMoved(long start, long startOffset, long endOffset) {
		MutableInterval range = getRangeThatIntervalCanBeMoved(startOffset,endOffset);
		if (getEffectiveWorkCalendar().compare(getStart(),start,false) == 0)
			range.setStart(range.getStart() - calcTotalDelay());
		return range;

	}
	public MutableInterval getRangeThatIntervalCanBeMoved(long startOffset, long endOffset) {
		return detail.getRangeThatIntervalCanBeMoved(startOffset,endOffset);
	}

	public void extend(long start, long end, long extendDuration) {
		if (end < getStop())
			return;
		WorkCalendar cal = getEffectiveWorkCalendar();
		long startOffset = cal.compare(start,getStart(),false);
		long endOffset = cal.compare(end,getStart(),false);

//		if (start >= getResume()) {
//			long splitDuration = detail.getSplitDuration();
//			startOffset -= splitDuration;
//			endOffset -= splitDuration;
//		}


		Interval range = getRangeThatIntervalCanBeMoved(start,startOffset,endOffset);
		if (extendDuration > 0)
			extendDuration = Math.min(extendDuration,range.getEnd() - endOffset); // don't allow to shift more than possible
		else
			extendDuration = Math.max(extendDuration,startOffset - endOffset);
		newDetail().extend(endOffset,extendDuration);
	}




	/* (non-Javadoc)
	 * @see com.projity.pm.scheduling.Schedule#split(java.lang.Object, long, long)
	 */
	public void split(Object eventSource, long from, long to) {
		from = Math.max(from,getResume());
		if (from >= to)
			return;
		long duration = getEffectiveWorkCalendar().compare(to,from,false); // calculate shift duration
		shift(from,getEnd(), duration); // shift the remaining contour to the right by the duration
	}

	public void extendBefore(long start, long end, long extendDuration) {
		if (start < getStop())
			return;
		WorkCalendar cal = getEffectiveWorkCalendar();

		boolean firstBar = (cal.compare(getStart(),start,false) == 0);
		long startOffset = cal.compare(start,getStart(),false);
		long endOffset = cal.compare(end,getStart(),false);
		Interval range = getRangeThatIntervalCanBeMoved(start,startOffset,startOffset);


		if (extendDuration > 0)
			extendDuration = Math.min(extendDuration,endOffset - startOffset);
		else
			extendDuration = Math.max(extendDuration,range.getStart()  - startOffset); // don't allow to shift more than possible

		if (firstBar)
			setTotalDelay(calcTotalDelay() + extendDuration);
		newDetail().extendBefore(startOffset,extendDuration);

	}



	public void addCalendarTime(long start, long end) {
		newDetail().addCalendarTime(start,end);
	}


	/* (non-Javadoc)
	 * @see com.projity.pm.assignment.AssignmentSpecificFields#getWorkContourId()
	 */
	public int getWorkContourType() {
		return getWorkContour().getType();
	}


	/* (non-Javadoc)
	 * @see com.projity.pm.assignment.AssignmentSpecificFields#setWorkContourId(int)
	 */
	public void setWorkContourType(int workContourType) {
		newDetail().setWorkContour(ContourFactory.getInstance(workContourType));
		// TODO need to actually transform contour correctly when changing

	}
	public final double getRemainingUnits() {
		if (!isLabor())
			return 1.0D;
		long duration = getRemainingDuration();
		if (duration == 0.0)
			return getWorkContour().getLastBucketUnits();
//			return 1.0D; // degeneratate case
		long work = getRemainingWork();
		if (work == 0) // degenerate case with no work yet
			return 1.0;
		return ((double) work) / duration;

	}

	public final double getRemainingLaborUnits() {
		if (isLabor())
			return getRemainingUnits();
		return 0.0D;
	}

	/* (non-Javadoc)
	 * @see com.projity.util.Association#getLeft()
	 */
	public Object getLeft() {
		return getTask();
	}


	/* (non-Javadoc)
	 * @see com.projity.util.Association#getRight()
	 */
	public Object getRight() {
		return getResource();
	}


	/* (non-Javadoc)
	 * @see com.projity.util.Association#testValid()
	 */
	public void testValid(boolean allowDuplicate) throws InvalidAssociationException {
	}


	/* (non-Javadoc)
	 * @see com.projity.util.Association#copyPrincipalFieldsFrom(com.projity.util.Association)
	 */
	public void copyPrincipalFieldsFrom(Association from) {
		Assignment fromAssignment = (Assignment)from;
		double units = fromAssignment.getUnits();
		if (fromAssignment.isLabor()) {
			adjustRemainingUnits(fromAssignment.getUnits(), 0, true, false);
			detail.setUnits(units);
		} else {
			detail.rate = fromAssignment.detail.getRate();
			detail.setDuration(getDuration()); // reset duration
		}

		getTask().updateCachedDuration(); // needed for checking if milestone

	}


	public void forceUnits(double units) {
		newDetail().setUnits(units);
	}
	public void doAddService(Object eventSource) {
		AssignmentService.getInstance().connect(this,eventSource);
	}

	public void doRemoveService(Object eventSource) {
		AssignmentService.getInstance().remove(this,eventSource,true);
	}

	/* (non-Javadoc)
	 * @see com.projity.association.Association#doUpdateService(java.lang.Object)
	 */
	public void doUpdateService(Object eventSource) {
		getUnitsField().fireEvent(this,this,null); // need to update

	}
	public  boolean isDefault() {
		return getTask() == NormalTask.getUnassignedInstance()
		|| getResource() == ResourceImpl.getUnassignedInstance();
	}
	public boolean isReadOnlyUnits(FieldContext fieldContext) {
		return false;
	}


	//Costs and earned value
	public double acwp(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_DOUBLE;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(Math.max(start,detail.effectiveBaselineStart()),Math.min(end,getCompletedOrStatusDate()));
		query.selectFrom(clause)
			.action(cost(clause,false))
			.execute();
		return ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public double bcws(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_DOUBLE;

		if (AdvancedOption.getInstance().isEarnedValueFieldsCumulative())
			start = getStart(); // start from the beginning of the task and ignore the range start

		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(Math.max(start,detail.effectiveBaselineStart()),Math.min(end,getStatusDate()));
		query.selectFrom(clause)
			.action(baselineData(COST,clause))
			.execute();
		return ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public double efficiency() {
		Assignment baselineAssignment = detail.getBaselineAssignment();
		if (baselineAssignment == null)
			return 0.0D;

		long baselineWork = baselineAssignment.work();
		if (baselineWork == 0)
			return 0.0;
		long work = work();
		if (work == 0.0D)
			return 1.0D;
		return ((double)baselineWork)/work;

	}
	//[(Actual % of completion / Expected % of completion) of an activity for a given period] * Actual cost of activity
	public double bcwp(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_DOUBLE;
		end = Math.min(end,getStatusDate());
		if (end == 0)
			return 0.0D;
		if (AdvancedOption.getInstance().isEarnedValueFieldsCumulative())
			start = getStart(); // start from the beginning of the task and ignore the range start
		double cost = actualCost(start,end);
		if (cost == 0)
			return 0;
		return efficiency() * cost;
//
//		Query query = Query.getInstance();
//		long boundaryStart = detail.getStart(); // always use assignment start and never start
//		long boundaryEnd = Math.min(getStatusDate(),baselineAssignment.getEffectiveWorkCalendar().add(boundaryStart, (long) (baselineAssignment.getDuration() * getPercentComplete()),false));
//		SelectFrom clause = SelectFrom.getInstance().whereInRange(boundaryStart,Math.min(end,boundaryEnd));
//		query.selectFrom(clause)
//			.action(baselineData(COST,clause))
//			.execute();
//		return ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public double bac(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_DOUBLE;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(start,end);
		query.selectFrom(clause)
			.action(baselineData(COST,clause))
			.execute();
		return ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public double cost(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_DOUBLE;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(start,end);
		query.selectFrom(clause)
			.action(cost(clause,false))
			.execute();
		return ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public long work(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_LONG;
//		if (!isLabor()) // TODO this right?
//			return 0;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(start,end);
		query.selectFrom(clause)
			.action(work(clause))
			.execute();
		return (long) ((DoubleValue)query.getActionVisitor()).getValue();

	}
	public double actualCost(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_DOUBLE;

		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(Math.max(start,detail.getStart()),Math.min(end,getStop()));
		query.selectFrom(clause)
			.action(cost(clause,false))
			.execute();
		return ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public long actualWork(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_LONG;
		if (!isLabor()) // TODO this right?
			return 0;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(Math.max(start,detail.getStart()),Math.min(end,getStop()));
		query.selectFrom(clause)
			.action(work(clause))
			.execute();
		return (long) ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public long remainingWork(long start, long end) {
		if (!isInRange(start,end))
			return NO_VALUE_LONG;
		if (!isLabor()) // TODO this right?
			return 0;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(Math.max(start,detail.getStop()),Math.min(end,getEnd()));
		query.selectFrom(clause)
			.action(work(clause))
			.execute();
		return (long) ((DoubleValue)query.getActionVisitor()).getValue();
	}

	public double baselineCost(long start, long end) {
//		if (!isInRange(start,end))
//			return NO_VALUE_DOUBLE;

		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(start,end);
		query.selectFrom(clause)
			.action(baselineData(COST,clause))
			.execute();
		return ((DoubleValue)query.getActionVisitor()).getValue();
	}


	public long baselineWork(long start, long end) {
//		if (!isInRange(start,end))
//			return NO_VALUE_LONG;
		if (!isLabor()) // TODO this right?
			return 0;
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance().whereInRange(start,end);
		query.selectFrom(clause)
			.action(baselineData(WORK,clause))
			.execute();
		return (long) ((DoubleValue)query.getActionVisitor()).getValue();
	}


	private boolean isFieldHidden(FieldContext fieldContext) {
		return fieldContext != null && !isInRange(fieldContext.getStart(),fieldContext.getEnd());
	}

	private boolean isEarnedValueFieldHidden(FieldContext fieldContext) {
		if (isFieldHidden(fieldContext))
			return true;
		return getStatusDate() < fieldContext.getStart();
	}

	private boolean isBaselineFieldHidden(int numBaseline,FieldContext fieldContext) {
		 Assignment baselineAssignment = getBaselineAssignment(new Integer(numBaseline), false);
		 if (baselineAssignment == null)
			 return true;

		 if (fieldContext == null) // the baseline exists, but no time range
			 return false;
		 return (fieldContext.getStart() >= baselineAssignment.getFinish() || fieldContext.getEnd() <= baselineAssignment.getStart());
	}

	/***************************************************************************************
	 * Time Distributed Fields
	 **************************************************************************************/


	public boolean fieldHideCost(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideWork(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideActualCost(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideActualWork(FieldContext fieldContext) {
		return fieldHideWork(fieldContext);
	}
	public boolean fieldHideBaselineCost(int numBaseline, FieldContext fieldContext) {
		return isBaselineFieldHidden(numBaseline,fieldContext);
	}
	public boolean fieldHideBaselineWork(int numBaseline, FieldContext fieldContext) {
		return isBaselineFieldHidden(numBaseline,fieldContext);
	}
	public boolean fieldHideAcwp(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideBac(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideBcwp(FieldContext fieldContext) {
		return isEarnedValueFieldHidden(fieldContext);
	}
	public boolean fieldHideBcws(FieldContext fieldContext) {
		return isEarnedValueFieldHidden(fieldContext);
	}
	public boolean fieldHideCv(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideSv(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideEac(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideVac(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideCpi(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideSpi(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideCvPercent(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideSvPercent(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}
	public boolean fieldHideTcpi(FieldContext fieldContext) {
		return isFieldHidden(fieldContext);
	}


	public double getCost(FieldContext fieldContext) {
		return cost(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public long work() {
		return work(FieldContext.defaultStart,FieldContext.defaultEnd);
	}
	public long getWork(FieldContext fieldContext) {
		long w = work(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
		if (!isLabor()) {
//			System.out.println("work before setting non temporal" + DurationFormat.format(w));
			w = Duration.setAsNonTemporal(w);
		}
		return w;
	}
	public double getActualCost(FieldContext fieldContext) {
		return actualCost(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public long getActualWork(FieldContext fieldContext) {
		return actualWork(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public long getRemainingWork(FieldContext fieldContext) {
		return remainingWork(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}

	public long getRemainingWork() {
		return getRemainingWork(null);
	}

	/* (non-Javadoc)
	 * @see com.projity.pm.assignment.TimeDistributedFields#getBaselineCost(int, com.projity.field.FieldContext)
	 */
	public double getBaselineCost(int numBaseline, FieldContext fieldContext) {
		return baselineCost(FieldContext.start(fieldContext),FieldContext.end(fieldContext));	}
	/* (non-Javadoc)
	 * @see com.projity.pm.assignment.TimeDistributedFields#getBaselineWork(int, com.projity.field.FieldContext)
	 */
	public long getBaselineWork(int numBaseline, FieldContext fieldContext) {
		return baselineWork(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
/***************************************************************************************
 * Earned Value Fields
 **************************************************************************************/
	public double getAcwp(FieldContext fieldContext) {
		return acwp(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getBac(FieldContext fieldContext) {
		return bac(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getBcwp(FieldContext fieldContext) {
		return bcwp(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getBcws(FieldContext fieldContext) {
		return bcws(FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getCv(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().cv(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getSv(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().sv(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getEac(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().eac(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getVac(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().vac(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getCpi(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().cpi(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getSpi(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().spi(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getCsi(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().csi(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getCvPercent(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().cvPercent(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getSvPercent(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().svPercent(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}
	public double getTcpi(FieldContext fieldContext) {
		return EarnedValueCalculator.getInstance().tcpi(this,FieldContext.start(fieldContext),FieldContext.end(fieldContext));
	}

	/**
	 * @return
	 */
	public Date getCreated() {
		return hasKey.getCreated();
	}
	/**
	 * @return
	 */
	public long getId() {
		return hasKey.getId();
	}
	/**
	 * @return
	 */
	public String getName() {
		return "" + getLeft() + " " + getRight();
	}
	/**
	 * @param context
	 * @return
	 */
	public String getName(FieldContext context) {
		if (context==null) return "???"; //fix
		if (context.isLeftAssociation())
			return getRight().toString();
		else
			return getLeft().toString();
	}
	/**
	 * @return
	 */
	public long getUniqueId() {
		return hasKey.getUniqueId();
	}
//	public void setNew(boolean isNew) {
//		hasKey.setNew(isNew);
//	}
	/**
	 * @param created
	 */
	public void setCreated(Date created) {
		hasKey.setCreated(created);
	}
	/**
	 * @param id
	 */
	public void setId(long id) {
		hasKey.setId(id);
	}
	/**
	 * @param name
	 */
	public void setName(String name) {
		hasKey.setName(name);
	}
	/**
	 * @param id
	 */
	public void setUniqueId(long id) {
		hasKey.setUniqueId(id);
	}
	public Document getDocument() {
		return getProject();
	}
	public Document getDocument(boolean leftObject) {
		return (leftObject)?getTask().getDocument():getResource().getDocument();
	}

	public Query workQuery() {
		Query query = Query.getInstance();
		SelectFrom clause = SelectFrom.getInstance();
		query.selectFrom(clause)
			.action(work(clause));
		return query;
	}

	public void calcDataBetween(Object type, HasStartAndEnd generator, CalculatedValues values) {
		SelectFrom clause = SelectFrom.getInstance();
		AssignmentFieldFunctor dataFunctor = getDataSelect(type,clause,false);
		calcDataBetween(dataFunctor,clause,generator,values);
	}

	public static void calcResourceAvailabilityBetween(Resource resource, HasStartAndEnd generator, CalculatedValues values) {
		SelectFrom clause = SelectFrom.getInstance();
		AssignmentFieldFunctor dataFunctor = resourceAvailability(clause,resource);
		calcDataBetween(dataFunctor,clause,generator,values);
	}

	public static void calcDataBetween(AssignmentFieldFunctor dataFunctor, SelectFrom clause, HasStartAndEnd generator, CalculatedValues values) {
		if (generator != null)
			clause.whereInRange(generator.getStart(),generator.getEnd()); // automatically also adds a generator to limit range

		CalculatedValuesFunctor visitor = CalculatedValuesFunctor.getInstance(dataFunctor,values, (TimeIteratorGenerator)generator);

		Query query = Query.getInstance();
		query.selectFrom(clause);
		if (generator != null && generator instanceof TimeIteratorGenerator) {
			query.groupBy((TimeIteratorGenerator)generator)
				 .action(visitor);
		} else {
			clause.select(visitor); // replaces other one
		}
		query.execute();
	}

	public long getResourceAvailability() {
		return detail.getResourceAvailability();
	}


	public Collection childrenToRollup() {
		return null;
	}


	/* (non-Javadoc)
	 * @see com.projity.pm.assignment.Allocation#getMostLoadedAssignmentUnits()
	 */
	public double getMostLoadedAssignmentUnits() {
		return getUnits();
	}

	/**
	 * @return
	 */

	public void makeContourPersonal() {
//		if (getWorkContour().isPersonal())
//			return;
		Object type = WORK;
		ContourBucketIntervalGenerator contourGenerator = contourGeneratorInstance(type); //contour
		PersonalContourMaker contourBuilder = PersonalContourMaker.getInstance(this, contourGenerator);
		Query.getInstance()
			.selectFrom(SelectFrom.getInstance()
						.select(contourBuilder)
						.from(contourGenerator)
						.all())
			.execute();

		newDetail().setContour(type,contourBuilder.getList());
		detail.recalculateDuration();
	}


/**
 * Clone the detail and set it
 * @return cloned assignment detail
 */	private AssignmentDetail newDetail() {
 		//TODO store off detail for undo
//System.out.println("before clone " + new Date(detail.getStop()));
		detail = (AssignmentDetail) detail.clone();
		if (getTask() != null)
			getTask().setDirty(true);
		setDirty(true);
//System.out.println("after clone " + new Date(detail.getStop()));
		return detail;
	}


	public long getElapsedDuration() {
		return detail.getElapsedDuration();
	}
	public long getDuration() {
		return detail.getDuration();
	}
	public double getPercentComplete() {
		return detail.getPercentComplete();
	}
	public void setPercentComplete(double percentComplete) {
		newDetail().setPercentComplete(percentComplete);
	}
	public long getEnd() {
		return detail.getEnd();
	}
	public void setEnd(long end) {
		detail.setEnd(end);
	}
	public long getActualStart() {
		return detail.getActualStart();
	}
	public void setActualStart(long actualStart) {
		newDetail().setActualStart(actualStart);
	}
	public void setRemainingDuration(long remainingDuration) {
		newDetail().setRemainingDuration(remainingDuration);
	}
	public long getActualFinish() {
		return detail.getActualFinish();
	}
	public void setActualFinish(long actualFinish) {
		newDetail().setActualFinish(actualFinish);
	}
	public long getResume() {
		return detail.getResume();
	}
	public long getActualDuration() {
		return detail.getActualDuration();
	}
	public void setActualDuration(long actualDuration) {
		newDetail().setActualDuration(actualDuration);
	}

	public long getRemainingDuration() {
		return detail.getRemainingDuration();
	}

	public void setResume(long resume) {
		newDetail().setResume(resume);
	}
	public long getStop() {
		return detail.getStop();
	}
	public void setStop(long stop) {
		if (stop < getStart()) // make sure in assignment's range
			stop = getStart();
		else if (stop > getEnd())
			stop = getEnd();
		long currentStop = getStop();
		if (currentStop == stop)
			return;
		if (stop < currentStop) // if uncompleting
			newDetail().removeFillerAfter(stop);
		if (currentStop > 0 && getDependencyStart() > currentStop && getDependencyStart() < stop) {// if setting stop incorporates split due to dependency
			makeContourPersonal();

		}
		newDetail().setStop(stop);
	}
	public void clearDuration() {
		newDetail().clearDuration();
	}


	public long getDependencyStart() {
		return detail.getDependencyStart();
	}
	public void setDependencyStart(long dependencyStart) {
		detail.setDependencyStart(dependencyStart); //TODO is it ok to modify schedule directly?  Should be if it is transient
	}

	//for gantt bar formula
	//probably to add in a common interface with task
	public boolean isNormal() {
		return false;
	}
	public boolean isCritical() {
		return false;
	}
	public boolean isSummary() {
		return false;
	}
	public boolean isMilestone() {
		return false;
	}
	public boolean isAssignment() {
		return true;
	}

	public void setTaskSchedule(TaskSchedule taskSchedule) {
		newDetail().setTaskSchedule(taskSchedule);
	}

	public boolean inProgress() {
		double percentComplete = getPercentComplete();
		return (percentComplete > 0.0D
				&& percentComplete < 1.0D);
	}

	public boolean isComplete() {
		return getPercentComplete() == 1.0D;
	}
	public boolean isUnstarted() {
		return getPercentComplete() == 0.0D;
	}
	public boolean isMine() {
		return getResource().isMe();
	}

	private void writeObject(ObjectOutputStream s) throws IOException {
	    s.defaultWriteObject();
	    hasKey.serialize(s);
	}
	private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException  {
	    s.defaultReadObject();
	    hasKey=HasKeyImpl.deserialize(s,this);
//	    barClosureInstance = new BarClosure();
	}

	public Object clone(){
		try {
			Assignment a=(Assignment)super.clone();
			a.hasKey=new HasKeyImpl(true,a);
			a.setName(getName());
//			barClosureInstance = new BarClosure();
			a.detail=(AssignmentDetail)detail.clone();
			return a;
		} catch (CloneNotSupportedException e) {
			throw new InternalError();
		}
	}
	public Object cloneWithTask(Task task){
		Assignment a=(Assignment)clone();
		a.detail.setTask(task);
		return a;
	}
	public Object cloneWithResource(Resource resource){
		Assignment a=(Assignment)clone();
		a.detail.setResource(resource);
		return a;
	}
	public Object cloneWithResourceAndTask(Resource resource,Task task){
		Assignment a=(Assignment)clone();
		a.detail.setResource(resource);
		a.detail.setTask(task);
		return a;
	}

	public void convertToBaselineAssignment(boolean useDefaultCalendar) {
		detail.convertToBaselineAssignment(useDefaultCalendar);
	}


	/**
	 * See if assignment duration is empty
	 * @return
	 */
    public boolean hasDuration() {
    	return detail.hasDuration();
    }

    public void setRemainingWork(long remainingWork, FieldContext fieldContext) {
		setActualWork(getWork(fieldContext) - Duration.millis(remainingWork), fieldContext);
	}
	public boolean isReadOnlyWork(FieldContext fieldContext) {

		if (fieldContext == null) {
			return isMaterial(); //// material resource assignments cannot have their total work modified
		}
		// see if there is some calendar time
		return !isActiveBetween(fieldContext.getStart(),fieldContext.getEnd());
	}

	public boolean isActiveBetween(long start, long end) {
		return getEffectiveWorkCalendar().compare(end,start,false) > 0;

	}
	public boolean isReadOnlyActualWork(FieldContext fieldContext) {
		return false;
	}
	public boolean isReadOnlyRemainingWork(FieldContext fieldContext) {
		return isReadOnlyWork(fieldContext);
	}


	public double getFixedCost(FieldContext fieldContext) {
		return 0;
	}
	public double getActualFixedCost(FieldContext fieldContext) {
		return 0;
	}
	public boolean fieldHideActualFixedCost(FieldContext fieldContext) {
		return true;
	}

	public double fixedCost(long start, long end) {
		return ((Task)getTask()).fixedCost(start,end);
	}

	public double actualFixedCost(long start, long end) {
		return ((Task)getTask()).actualFixedCost(start,end);
	}


	/* (non-Javadoc)
	 * @see com.projity.pm.assignment.TimeDistributedFields#setFixedCost(double, com.projity.field.FieldContext)
	 */
	public void setFixedCost(double fixedCost, FieldContext fieldContext) {
	}


	public boolean isReadOnlyFixedCost(FieldContext fieldContext) {
		return true;
	}

	public boolean isLabor() {
		return getResource().isLabor();
	}
	public boolean isTemporal() {
		return detail.isTemporal();
	}

	/**
	 * @param timeUnit
	 */
	public void setRateUnit(int timeUnit) {
		detail.setRateUnit(timeUnit);
		getTask().updateCachedDuration(); // needed for checking if milestone - happens when changing units in dialog
	}

	public final Rate getRate() {
		return detail.getRate();
	}
	private int getTaskSchedulingType() {
		return ((NormalTask)getTask()).getSchedulingType();
	}

	public String valuesString() {
		return "Duration=" + DurationFormat.format(getDuration())
		+ " Work=" + DurationFormat.format(getWork(null))
		+ " Units=" + getUnits() + " contour=" + getWorkContour().toString(getDuration());

	}
	public final void setRate(Rate rate) {

		if (rate.isNonTemporal() == getRate().isNonTemporal()) { // normal case.  If a material resource is modified from temporal to non (or vice versa), just set the rate
			double old = getRate().getValue();
			if (rate.getValue() == old) // if no change
				return;
			long oldRemaining = getRemainingDuration();
			newDetail();
			double multiplier = rate.getValue() / old;
			if (getPercentComplete() > 0)
				makeContourPersonal();
			detail.adjustRemainingUnits(rate.getValue());
			if (getTaskSchedulingType() != SchedulingType.FIXED_DURATION)
				detail.adjustRemainingDuration((long) (oldRemaining / multiplier));
		}
		detail.setRate(rate);

		getTask().updateCachedDuration(); // needed for checking if milestone

	}
//	public boolean isNew() {
//		return hasKey.isNew();
//	}
	public double getRemainingCost(FieldContext fieldContext) {
		return getCost(fieldContext) - getActualCost(fieldContext);
	}

	public void invalidateAssignmentCalendar() {
		detail.invalidateAssignmentCalendar();
	}

	public boolean isSubproject() {
		return false;
	}

	public boolean isJustModified(){
		Task task=getTask();
		if (task==null) return false;
		else return task.isJustModified();
	}
	public boolean isInvalidIntersectionCalendar() {
		return detail.isInvalidIntersectionCalendar();
	}

	private void moveDelayToContour() {
		// Remove delay and add at beginning of contour
		long oldDelay = calcTotalDelay();
		if (oldDelay == 0)
			return;
		makeContourPersonal();
		setTotalDelay(0);
		setDurationMillis(getDurationMillis() + oldDelay);
		AbstractContour contour = PersonalContour.addEmptyBucket(getWorkContour(), oldDelay,false); // add empty space before
		newDetail().setWorkContour(contour);
	}
	private void extractDelayFromContour(PersonalContour newContour) {
		long delay = newContour.extractDelay(); // the case when adding 0 units at start
		if (delay > 0) {
			newDetail();
			detail.setDelay(delay);
//			detail.recalculateDuration();
		}
	}


	public void setComplete(boolean complete) {
		ScheduleUtil.setComplete(this,complete);
	}


	public String getProjectName() {
		return getOwningProject().getName();
	}


	public Project getProject() {
		return getTask().getProject();
	}


	public Project getOwningProject() {
		Project p = getTask().getOwningProject();
		if (p == null)
			p = getProject();
		return p;
	}


	public final int getTimesheetStatus() {
		Assignment ts = getTimesheetAssignment();
		if (ts != null)
			return ts.timesheetStatus;
		return timesheetStatus;
	}
	public final void setTimesheetStatus(int timesheetStatus) {
		this.timesheetStatus = timesheetStatus;
	}
	public final long getLastTimesheetUpdate() {
		return lastTimesheetUpdate;
	}
	public final void setLastTimesheetUpdate(long lastTimesheetUpdate) {
		this.lastTimesheetUpdate = lastTimesheetUpdate;;
	}
	public boolean isPendingTimesheetUpdate() {
		return (getTimesheetStatus() == TimesheetStatus.VALIDATED);
	}
	public String getTimesheetStatusName() {
		return TimesheetHelper.getTimesheetStatusName(getTimesheetStatus());
	}

	public final boolean isTimesheetAssignment() {
		return timesheetAssignment;
	}
	public final void setTimesheetAssignment(boolean timesheetAssignment) {
		this.timesheetAssignment = timesheetAssignment;
	}

	public Assignment getTimesheetAssignment() {
		if (isTimesheetAssignment())
			return this;
		return detail.getBaselineAssignment(Snapshottable.TIMESHEET, false);
	}

	public long getTimesheetStart() {
		return getCachedStart().getTime();
	}

	public long getTimesheetFinish() {
		return getCachedEnd().getTime();
	}

	public boolean isTimesheetEditable() {
		return getTimesheetStatus() != TimesheetStatus.INTEGRATED;
	}

	public boolean isTimesheetEntered() {
		return getTimesheetStatus() != TimesheetStatus.ENTERED;
	}
	public boolean isTimesheetValidated() {
		return getTimesheetStatus() != TimesheetStatus.VALIDATED;
	}
	public boolean isTimesheetRejected() {
		return getTimesheetStatus() != TimesheetStatus.REJECTED;
	}

	public boolean copyFieldsFromTimesheet(Collection fieldArray) {
		Assignment ts = getTimesheetAssignment();
		if (ts == null)
			return false;
		if (ts.getTimesheetStatus() != TimesheetStatus.VALIDATED) // only incorporate validated data
			return false;
		Field.copyData(fieldArray,this,ts);
		return true;
	}

	public boolean applyTimesheet(Collection fieldArray, long timesheetUpdateDate) {
		boolean updated = copyFieldsFromTimesheet(fieldArray);
		if (updated) {
			setTimesheetStatus(TimesheetStatus.INTEGRATED);
			this.lastTimesheetUpdate = timesheetUpdateDate;
			Assignment ts = getTimesheetAssignment();
			ts.setTimesheetStatus(TimesheetStatus.INTEGRATED);
			ts.lastTimesheetUpdate = timesheetUpdateDate;

		}
		return updated;
	}
	public String getTimesheetStatusStyle() { // used for display style in web
		return TimesheetHelper.getTimesheetStatusStyle(getTimesheetStatus());
	}

	private transient boolean dirty=true;
	public boolean isDirty() {
		return dirty;
	}
	public void setDirty(boolean dirty) {
		//System.out.println("Assignment _setDirty("+dirty+"): "+getName());
		this.dirty = dirty;
		if (dirty){
			Task task=getTask();
			if (task!=null) task.setDirty(true);
		}
	}


	public final Date getCachedEnd() {
		return cachedEnd;
	}


	public final void setCachedEnd(Date savedEnd) {
		this.cachedEnd = savedEnd;
	}


	public final Date getCachedStart() {
		return cachedStart;
	}


	public final void setCachedStart(Date savedStart) {
		this.cachedStart = savedStart;
	}


	public final int getWorkflowState() {
		return workflowState;
	}


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

	public void setTaskAndResource(Task task, Resource resource) {
		detail.setTask(task);
		detail.setResource(resource);
	}

	public long getDeadline() { // needed for indicators
		return 0;
	}


	public final long getEarliestStop() {
		return detail.getEarliestStop();
	}

	public final long getCompletedThrough() {
		return detail.getCompletedThrough();
	}


	public void setCompletedThrough(long completedThrough) {
		setStop(completedThrough);
	}
	public void replace(Object newOne, boolean leftObject) {
		if (leftObject)
			newDetail().setTask((Task) newOne);
		else
			newDetail().setResource((Resource)newOne);
	}
	public long getFinishOffset() {
		return EarnedValueCalculator.getInstance().getFinishOffset(this);
	}

	public long getStartOffset() {
		return EarnedValueCalculator.getInstance().getStartOffset(this);
	}


	public String getTimeUnitLabel() {
		return getResource().getTimeUnitLabel();
	}


	public boolean isMaterial() {
		return getResource().isMaterial();
	}

	public RateFormat getRateFormat() {
		return getResource().getRateFormat();
	}


	public ImageLink getBudgetStatusIndicator() {
		return EarnedValueCalculator.getInstance().getBudgetStatusIndicator(getCpi(null));
	}

	public ImageLink getScheduleStatusIndicator() {
		return EarnedValueCalculator.getInstance().getBudgetStatusIndicator(getSpi(null));
	}


	public Object backupDetail() {
		return detail.backupDetail();
	}

	//use when updating only this assignment
	public void restoreDetail(Object source,Object detail,boolean isChild) {
		restoreDetail(detail);
		getTask().recalculate(source);
		getTask().updateCachedDuration();
	}

	public void restoreDetail(Object detail) {
		this.detail=(AssignmentDetail)detail;
	}

	public String getDelegatedToName() {
		return getTask().getDelegatedToName();
	}



	public boolean isLocal() {
		return true;
	}

	public void setLocal(boolean local) {
	}

	public void renumber(){
		hasKey.renumber();
	}
    public int getCostRateIndex() {
    	return detail.getCostRateIndex();
    }

    public void setCostRateIndex(int val) {
    	newDetail().setCostRateIndex(val);
    }

    public String getUniqueIdString() {
    	return getTask().getUniqueId() + "." + getResource().getUniqueId();
    }


}