/*
GanttProject is an opensource project management tool.
Copyright (C) 2004-2011 GanttProject Team

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

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

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

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import com.google.common.collect.ImmutableList;

import biz.ganttproject.core.calendar.AlwaysWorkingTimeCalendarImpl;
import biz.ganttproject.core.calendar.GPCalendar.DayMask;
import biz.ganttproject.core.calendar.GPCalendar.DayType;
import biz.ganttproject.core.calendar.GPCalendarCalc;
import biz.ganttproject.core.calendar.GPCalendarCalc.MoveDirection;
import biz.ganttproject.core.chart.render.ShapePaint;
import biz.ganttproject.core.time.CalendarFactory;
import biz.ganttproject.core.time.GanttCalendar;
import biz.ganttproject.core.time.TimeDuration;
import biz.ganttproject.core.time.TimeDurationImpl;
import biz.ganttproject.core.time.impl.GPTimeUnitStack;
import net.sourceforge.ganttproject.GPLogger;
import net.sourceforge.ganttproject.chart.MilestoneTaskFakeActivity;
import net.sourceforge.ganttproject.document.AbstractURLDocument;
import net.sourceforge.ganttproject.document.Document;
import net.sourceforge.ganttproject.task.Task.Cost;
import net.sourceforge.ganttproject.task.algorithm.AlgorithmCollection;
import net.sourceforge.ganttproject.task.algorithm.AlgorithmException;
import net.sourceforge.ganttproject.task.algorithm.CostAlgorithmImpl;
import net.sourceforge.ganttproject.task.algorithm.ShiftTaskTreeAlgorithm;
import net.sourceforge.ganttproject.task.dependency.TaskDependencyException;
import net.sourceforge.ganttproject.task.dependency.TaskDependencySlice;
import net.sourceforge.ganttproject.task.dependency.TaskDependencySliceAsDependant;
import net.sourceforge.ganttproject.task.dependency.TaskDependencySliceAsDependee;
import net.sourceforge.ganttproject.task.dependency.TaskDependencySliceImpl;
import net.sourceforge.ganttproject.task.hierarchy.TaskHierarchyItem;
import net.sourceforge.ganttproject.util.collect.Pair;

/**
 * @author bard
 */
public class TaskImpl implements Task {
  private final int myID;

  private final TaskManagerImpl myManager;

  private String myName;

  private String myWebLink = "";

  private boolean isMilestone;

  boolean isProjectTask;

  private Priority myPriority;

  private GanttCalendar myStart;

  private GanttCalendar myEnd;

  private GanttCalendar myThird;

  private int myThirdDateConstraint;

  private int myCompletionPercentage;

  private TimeDuration myLength;

  private final List<TaskActivity> myActivities = new ArrayList<TaskActivity>();

  private boolean bExpand;

  // private final TaskDependencyCollection myDependencies = new
  // TaskDependencyCollectionImpl();
  private final ResourceAssignmentCollectionImpl myAssignments;

  private final TaskDependencySlice myDependencySlice;

  private final TaskDependencySlice myDependencySliceAsDependant;

  private final TaskDependencySlice myDependencySliceAsDependee;

  private boolean myEventsEnabled;

  private final TaskHierarchyItem myTaskHierarchyItem;

  private ShapePaint myShape;

  private Color myColor;

  private String myNotes;

  private MutatorImpl myMutator;

  private final CustomColumnsValues customValues;

  private boolean critical;

  private List<TaskActivity> myMilestoneActivity;

  private final CostImpl myCost = new CostImpl();

  private boolean isUnplugged = false;

  public final static int NONE = 0;

  public final static int EARLIESTBEGIN = 1;

  private static final GPCalendarCalc RESTLESS_CALENDAR = new AlwaysWorkingTimeCalendarImpl();

  private static final TimeDuration EMPTY_DURATION = new TimeDurationImpl(GPTimeUnitStack.DAY, 0);

  protected TaskImpl(TaskManagerImpl taskManager, int taskID) {
    myManager = taskManager;
    myID = taskID;

    myAssignments = new ResourceAssignmentCollectionImpl(this, myManager.getConfig().getResourceManager());
    myDependencySlice = new TaskDependencySliceImpl(this, myManager.getDependencyCollection(), TaskDependencySlice.COMPLETE_SLICE_FXN);
    myDependencySliceAsDependant = new TaskDependencySliceAsDependant(this, myManager.getDependencyCollection());
    myDependencySliceAsDependee = new TaskDependencySliceAsDependee(this, myManager.getDependencyCollection());
    myPriority = DEFAULT_PRIORITY;
    myTaskHierarchyItem = myManager.getHierarchyManager().createItem(this);
    myNotes = "";
    bExpand = true;
    myColor = null;

    customValues = new CustomColumnsValues(myManager.getCustomPropertyManager());
  }

  protected TaskImpl(TaskImpl copy, boolean isUnplugged) {
    this.isUnplugged = isUnplugged;
    myManager = copy.myManager;
    // Use a new (unique) ID for the cloned task
    myID = myManager.getAndIncrementId();

    if (!isUnplugged) {
      myTaskHierarchyItem = myManager.getHierarchyManager().createItem(this);
    } else {
      myTaskHierarchyItem = copy.myTaskHierarchyItem;
    }
    myAssignments = new ResourceAssignmentCollectionImpl(this, myManager.getConfig().getResourceManager());
    myAssignments.importData(copy.getAssignmentCollection());
    myName = copy.myName;
    myWebLink = copy.myWebLink;
    isMilestone = copy.isMilestone;
    isProjectTask = copy.isProjectTask;
    myPriority = copy.myPriority;
    myStart = copy.myStart;
    myEnd = copy.myEnd;
    myThird = copy.myThird;
    myThirdDateConstraint = copy.myThirdDateConstraint;
    myCompletionPercentage = copy.myCompletionPercentage;
    myLength = copy.myLength;
    myShape = copy.myShape;
    myColor = copy.myColor;
    myNotes = copy.myNotes;
    bExpand = copy.bExpand;
    myCost.setValue(copy.myCost);

    myDependencySlice = new TaskDependencySliceImpl(this, myManager.getDependencyCollection(), TaskDependencySlice.COMPLETE_SLICE_FXN);
    myDependencySliceAsDependant = new TaskDependencySliceAsDependant(this, myManager.getDependencyCollection());
    myDependencySliceAsDependee = new TaskDependencySliceAsDependee(this, myManager.getDependencyCollection());

    customValues = (CustomColumnsValues) copy.getCustomValues().clone();

    recalculateActivities();
  }

  @Override
  public Task unpluggedClone() {
    TaskImpl result = new TaskImpl(this, true) {
      @Override
      public boolean isSupertask() {
        return false;
      }
    };
    return result;
  }

  class MutatorException extends RuntimeException {
    public MutatorException(String msg) {
      super(msg);
    }
  }

  @Override
  public TaskMutator createMutator() {
    if (myMutator != null) {
      return myMutator;
    }
    myMutator = new MutatorImpl();
    return myMutator;
  }

  @Override
  public TaskMutator createMutatorFixingDuration() {
    if (myMutator != null) {
      throw new MutatorException("Two mutators have been requested for task=" + getName());
    }
    myMutator = new MutatorImpl() {
      @Override
      public void setStart(GanttCalendar start) {
        super.setStart(start);
        TaskImpl.this.myEnd = null;
      }
    };
    return myMutator;
  }

  // main properties
  @Override
  public int getTaskID() {
    return myID;
  }

  @Override
  public String getName() {
    return myName;
  }

  public String getWebLink() {
    return myWebLink;
  }

  @Override
  public List<Document> getAttachments() {
    if (getWebLink() != null && !"".equals(getWebLink())) {
      return Collections.singletonList((Document) new AbstractURLDocument() {
        @Override
        public boolean canRead() {
          return true;
        }

        @Override
        public IStatus canWrite() {
          return Status.CANCEL_STATUS;
        }

        @Override
        public String getFileName() {
          return null;
        }

        @Override
        public InputStream getInputStream() throws IOException {
          return null;
        }

        @Override
        public OutputStream getOutputStream() throws IOException {
          return null;
        }

        @Override
        public String getPath() {
          return null;
        }

        @Override
        public URI getURI() {
          try {
            return new URI(new URL(getWebLink()).toString());
          } catch (URISyntaxException e) {
            // Do nothing
          } catch (MalformedURLException e) {
            File f = new File(getWebLink());
            if (f.exists()) {
              return f.toURI();
            }
          }
          try {
            URL context = myManager.getProjectDocument();
            if (context == null) {
              return null;
            }
            URL relative = new URL(context, getWebLink());
            return new URI(URLEncoder.encode(relative.toString(), "utf-8"));
          } catch (URISyntaxException e) {
            // Do nothing
          } catch (MalformedURLException e) {
            // Do nothing
          } catch (UnsupportedEncodingException e) {
            // Do nothing
          }
          return null;
        }

        @Override
        public boolean isLocal() {
          return false;
        }

        @Override
        public boolean isValidForMRU() {
          return false;
        }

        @Override
        public void write() throws IOException {
        }
      });
    }
    return Collections.emptyList();
  }

  @Override
  public boolean isMilestone() {
    return isMilestone && Boolean.TRUE == myManager.isZeroMilestones();
  }

  public boolean isLegacyMilestone() {
    return isMilestone;
  }
  @Override
  public Priority getPriority() {
    return myPriority;
  }

  @Override
  public GanttCalendar getStart() {
    if (myMutator != null && myMutator.myIsolationLevel == TaskMutator.READ_UNCOMMITED) {
      return myMutator.getStart();
    }
    return myStart;
  }

  @Override
  public GanttCalendar getEnd() {
    GanttCalendar result = null;
    if (myMutator != null && myMutator.myIsolationLevel == TaskMutator.READ_UNCOMMITED) {
      result = myMutator.getEnd();
    }
    if (result == null) {
      if (myEnd == null) {
        myEnd = calculateEnd();
      }
      result = myEnd;
    }
    return result;
  }

  @Override
  public GanttCalendar getDisplayEnd() {
    GanttCalendar modelEnd = getEnd();
    if (modelEnd.equals(getStart())) {
      boolean allMilestones = true;
      Task[] deepNestedTasks = getManager().getTaskHierarchy().getDeepNestedTasks(this);
      for (Task t : deepNestedTasks) {
        if (!t.isSupertask() && !t.isMilestone()) {
          allMilestones = false;
          break;
        }
      }
      if (!allMilestones) {
        GPLogger.getLogger(Task.class).warning(String.format(
            "This is probably a bug. Task #%d (%s) has end date=%s equal to start date." +
            "It could be possible if all child tasks were milestones, however they are not. Child tasks: %s",
            getTaskID(), getName(), modelEnd, Arrays.asList(deepNestedTasks)));
      }
      return modelEnd;
    }
    return isMilestone ? modelEnd : modelEnd.getDisplayValue();
  }

  GanttCalendar calculateEnd() {
    GanttCalendar result = getStart().clone();
    Date newEnd = shiftDate(result.getTime(), getDuration());
    result.setTime(newEnd);
    return result;
  }

  @Override
  public GanttCalendar getThird() {
    if (myMutator != null && myMutator.myIsolationLevel == TaskMutator.READ_UNCOMMITED) {
      return myMutator.getThird();
    }
    return myThird;
  }

  @Override
  public int getThirdDateConstraint() {
    return myThirdDateConstraint;
  }

  @Override
  public List<TaskActivity> getActivities() {
    if (isMilestone) {
      return myMilestoneActivity;
    }
    List<TaskActivity> activities = myMutator == null ? null : myMutator.getActivities();
    if (activities == null) {
      activities = myActivities;
    }
    return activities;
  }

  @Override
  public TimeDuration getDuration() {
    if (isMilestone()) {
      return EMPTY_DURATION;
    }
    return (myMutator != null && myMutator.myIsolationLevel == TaskMutator.READ_UNCOMMITED) ? myMutator.getDuration()
        : myLength;
  }

  @Override
  public int getCompletionPercentage() {
    return (myMutator != null && myMutator.myIsolationLevel == TaskMutator.READ_UNCOMMITED) ? myMutator.getCompletionPercentage()
        : myCompletionPercentage;
  }

  @Override
  public boolean getExpand() {
    return bExpand;
  }

  @Override
  public ShapePaint getShape() {
    return myShape;
  }

  @Override
  public Color getColor() {
    Color result = myColor;
    if (result == null) {
      if (isMilestone() || myManager.getTaskHierarchy().hasNestedTasks(this)) {
        result = Color.BLACK;
      } else {
        result = myManager.getConfig().getDefaultColor();
      }
    }
    return result;
  }

  @Override
  public String getNotes() {
    return myNotes;
  }

  @Override
  public ResourceAssignment[] getAssignments() {
    return myAssignments.getAssignments();
  }

  @Override
  public ResourceAssignmentCollection getAssignmentCollection() {
    return myAssignments;
  }

  @Override
  public Task getSupertask() {
    TaskHierarchyItem container = myTaskHierarchyItem.getContainerItem();
    return container.getTask();
  }

  @Override
  public Task[] getNestedTasks() {
    TaskHierarchyItem[] nestedItems = myTaskHierarchyItem.getNestedItems();
    Task[] result = new Task[nestedItems.length];
    for (int i = 0; i < nestedItems.length; i++) {
      result[i] = nestedItems[i].getTask();
    }
    return result;
  }

  @Override
  public void move(Task targetSupertask) {
    TaskImpl supertaskImpl = (TaskImpl) targetSupertask;
    TaskHierarchyItem targetItem = supertaskImpl.myTaskHierarchyItem;
    myTaskHierarchyItem.delete();
    targetItem.addNestedItem(myTaskHierarchyItem);
    myManager.onTaskMoved(this);
  }

  @Override
  public void delete() {
    getDependencies().clear();
    getAssignmentCollection().clear();
  }

  @Override
  public TaskDependencySlice getDependencies() {
    return myDependencySlice;
  }

  @Override
  public TaskDependencySlice getDependenciesAsDependant() {
    return myDependencySliceAsDependant;
  }

  @Override
  public TaskDependencySlice getDependenciesAsDependee() {
    return myDependencySliceAsDependee;
  }

  @Override
  public TaskManager getManager() {
    return myManager;
  }

  private static interface EventSender {
    void enable();

    void fireEvent();
  }

  private class ProgressEventSender implements EventSender {
    private boolean myEnabled;

    @Override
    public void fireEvent() {
      if (myEnabled) {
        myManager.fireTaskProgressChanged(TaskImpl.this);
      }
      myEnabled = false;
    }

    @Override
    public void enable() {
      myEnabled = true;
    }
  }

  private class PropertiesEventSender implements EventSender {
    private boolean myEnabled;

    @Override
    public void fireEvent() {
      if (myEnabled) {
        myManager.fireTaskPropertiesChanged(TaskImpl.this);
      }
      myEnabled = false;
    }

    @Override
    public void enable() {
      myEnabled = true;
    }
  }

  private static class FieldChange {
    Object myFieldValue;
    Object myOldValue;

    EventSender myEventSender;

    void setValue(Object newValue) {
      myFieldValue = newValue;
      myEventSender.enable();
    }

    public void setOldValue(Object oldValue) {
      myOldValue = oldValue;
    }
  }

  private class MutatorImpl implements TaskMutator {
    private EventSender myPropertiesEventSender = new PropertiesEventSender();

    private EventSender myProgressEventSender = new ProgressEventSender();

    private FieldChange myCompletionPercentageChange;

    private FieldChange myStartChange;

    private FieldChange myEndChange;

    private FieldChange myThirdChange;

    private FieldChange myDurationChange;

    private List<TaskActivity> myActivities;

    private Pair<FieldChange, FieldChange> myShiftChange;

    private final List<Runnable> myCommands = new ArrayList<Runnable>();

    private int myIsolationLevel;

    public final Exception myException = new Exception();
    @Override
    public void commit() {
      try {
        if (myStartChange != null) {
          GanttCalendar start = getStart();
          TaskImpl.this.setStart(start);
        }
        if (myDurationChange != null) {
          TimeDuration duration = getDuration();
          TaskImpl.this.setDuration(duration);
          myEndChange = null;
        }
        if (myCompletionPercentageChange != null) {
          int newValue = getCompletionPercentage();
          TaskImpl.this.setCompletionPercentage(newValue);
        }
        if (myEndChange != null) {
          GanttCalendar end = getEnd();
          if (end.getTime().compareTo(TaskImpl.this.getStart().getTime()) > 0) {
            TaskImpl.this.setEnd(end);
          }
        }
        if (myThirdChange != null) {
          GanttCalendar third = getThird();
          TaskImpl.this.setThirdDate(third);
        }
        for (Runnable command : myCommands) {
          command.run();
        }
        myCommands.clear();
        myPropertiesEventSender.fireEvent();
        myProgressEventSender.fireEvent();
      } finally {
        TaskImpl.this.myMutator = null;
      }
      if (myStartChange != null && TaskImpl.this.isSupertask()) {
        TaskImpl.this.adjustNestedTasks();
      }
      if ((myStartChange != null || myEndChange != null || myDurationChange != null || myShiftChange != null) && areEventsEnabled()) {
        GanttCalendar oldStart;
        if (myStartChange != null) {
          oldStart = (GanttCalendar) myStartChange.myOldValue;
        } else if (myShiftChange != null) {
          oldStart = (GanttCalendar) myShiftChange.first().myOldValue;
        } else {
          oldStart = TaskImpl.this.getStart();
        }
        GanttCalendar oldEnd;
        if (myEndChange != null) {
          oldEnd = (GanttCalendar) myEndChange.myOldValue;
        } else if (myShiftChange != null){
          oldEnd = (GanttCalendar) myShiftChange.second().myOldValue;
        } else {
          oldEnd = TaskImpl.this.getEnd();
        }
        myManager.fireTaskScheduleChanged(TaskImpl.this, oldStart, oldEnd);
      }
    }

    public GanttCalendar getThird() {
      return myThirdChange == null ? TaskImpl.this.myThird : (GanttCalendar) myThirdChange.myFieldValue;
    }

    public List<TaskActivity> getActivities() {
      if (myActivities == null && (myStartChange != null) || (myDurationChange != null)) {
        myActivities = new ArrayList<TaskActivity>();
        TaskImpl.recalculateActivities(myManager.getConfig().getCalendar(), TaskImpl.this, myActivities,
            getStart().getTime(), TaskImpl.this.getEnd().getTime());
      }
      return myActivities;
    }

    @Override
    public void setName(final String name) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setName(name);
        }
      });
    }

    @Override
    public void setProjectTask(final boolean projectTask) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setProjectTask(projectTask);
        }
      });
    }

    @Override
    public void setMilestone(final boolean milestone) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setMilestone(milestone);
        }
      });
    }

    @Override
    public void setPriority(final Priority priority) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setPriority(priority);
        }
      });
    }

    @Override
    public void setStart(final GanttCalendar start) {
      assert start != null;
      GanttCalendar currentStart = getStart();
      if (currentStart != null && start.equals(currentStart)) {
        return;
      }
      if (myStartChange == null) {
        myStartChange = new FieldChange();
        myStartChange.myEventSender = myPropertiesEventSender;
      }
      myStartChange.setOldValue(TaskImpl.this.myStart);
      myStartChange.setValue(start);
      myActivities = null;
    }

    @Override
    public void setEnd(final GanttCalendar end) {
      if (myEndChange == null) {
        myEndChange = new FieldChange();
        myEndChange.myEventSender = myPropertiesEventSender;
      }
      myEndChange.setOldValue(TaskImpl.this.myEnd);
      myEndChange.setValue(end);
      myActivities = null;
    }

    @Override
    public void setThird(final GanttCalendar third, final int thirdDateConstraint) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setThirdDateConstraint(thirdDateConstraint);
        }
      });
      if (myThirdChange == null) {
        myThirdChange = new FieldChange();
        myThirdChange.myEventSender = myPropertiesEventSender;
      }
      myThirdChange.setValue(third);
      myActivities = null;
    }

    @Override
    public void setDuration(final TimeDuration length) {
      // If duration of task was set to 0 or less do not change it
      if (length.getLength() <= 0) {
        return;
      }

      if (myDurationChange == null) {
        myDurationChange = new FieldChange();
        myDurationChange.myEventSender = myPropertiesEventSender;
        myDurationChange.setValue(length);
      } else {
        TimeDuration currentLength = (TimeDuration) myDurationChange.myFieldValue;
        if (currentLength.getLength() - length.getLength() == 0) {
          return;
        }
      }

      myDurationChange.setValue(length);
      Date shifted = TaskImpl.this.shiftDate(getStart().getTime(), length);
      GanttCalendar newEnd = CalendarFactory.createGanttCalendar(shifted);
      setEnd(newEnd);
      myActivities = null;
    }

    @Override
    public void setExpand(final boolean expand) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setExpand(expand);
        }
      });
    }

    @Override
    public void setCompletionPercentage(final int percentage) {
      if (myCompletionPercentageChange == null) {
        myCompletionPercentageChange = new FieldChange();
        myCompletionPercentageChange.myEventSender = myProgressEventSender;
      }
      myCompletionPercentageChange.setValue(new Integer(percentage));
    }

    @Override
    public void setCritical(final boolean critical) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setCritical(critical);
        }
      });
    }

    @Override
    public void setShape(final ShapePaint shape) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setShape(shape);
        }
      });
    }

    @Override
    public void setColor(final Color color) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setColor(color);
        }
      });
    }

    @Override
    public void setWebLink(final String webLink) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setWebLink(webLink);
        }
      });
    }

    @Override
    public void setNotes(final String notes) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.setNotes(notes);
        }
      });
    }

    @Override
    public void addNotes(final String notes) {
      myCommands.add(new Runnable() {
        @Override
        public void run() {
          TaskImpl.this.addNotes(notes);
        }
      });
    }

    @Override
    public int getCompletionPercentage() {
      return myCompletionPercentageChange == null ? TaskImpl.this.myCompletionPercentage
          : ((Integer) myCompletionPercentageChange.myFieldValue).intValue();
    }

    GanttCalendar getStart() {
      return myStartChange == null ? TaskImpl.this.myStart : (GanttCalendar) myStartChange.myFieldValue;
    }

    GanttCalendar getEnd() {
      return myEndChange == null ? null : (GanttCalendar) myEndChange.myFieldValue;
    }

    TimeDuration getDuration() {
      return myDurationChange == null ? TaskImpl.this.myLength : (TimeDuration) myDurationChange.myFieldValue;
    }

    @Override
    public void shift(float unitCount) {
      Task result = getPrecomputedShift(unitCount);
      if (result == null) {
        result = TaskImpl.this.shift(unitCount);
        cachePrecomputedShift(result, unitCount);
      }

      setStart(result.getStart());
      setDuration(result.getDuration());
      setEnd(result.getEnd());
    }

    @Override
    public void shift(TimeDuration shift) {
      if (myShiftChange == null) {
        myShiftChange = Pair.create(new FieldChange(), new FieldChange());
        myShiftChange.first().setOldValue(TaskImpl.this.myStart);
        myShiftChange.second().setOldValue(TaskImpl.this.myEnd);
      }
      ShiftTaskTreeAlgorithm shiftAlgorithm = new ShiftTaskTreeAlgorithm(myManager, null);
      try {
        shiftAlgorithm.run(TaskImpl.this, shift, ShiftTaskTreeAlgorithm.DEEP);
      } catch (AlgorithmException e) {
        GPLogger.log(e);
      }
    }

    @Override
    public void setIsolationLevel(int level) {
      myIsolationLevel = level;
    }

    private void cachePrecomputedShift(Task result, float unitCount) {
      // TODO Implement cache
    }

    private Task getPrecomputedShift(float unitCount) {
      // TODO Use cache to grab value
      return null;
    }

    @Override
    public void setTaskInfo(TaskInfo taskInfo) {
      myTaskInfo = taskInfo;
    }
  }

  @Override
  public void setName(String name) {
    myName = (name == null ? null : name.trim());
  }

  @Override
  public void setWebLink(String webLink) {
    myWebLink = webLink;
  }

  @Override
  public void setMilestone(boolean milestone) {
    isMilestone = milestone;
    if (milestone) {
      setEnd(null);
    }
  }

  @Override
  public void setPriority(Priority priority) {
    myPriority = priority;
  }

  @Override
  public void setStart(GanttCalendar start) {
    Date closestWorkingStart = myManager.findClosestWorkingTime(start.getTime());
    start.setTime(closestWorkingStart);
    myStart = start;
    recalculateActivities();
    adjustNestedTasks();
  }

  private void adjustNestedTasks() {
    assert myManager != null;
    try {
      AlgorithmCollection algorithmCollection = myManager.getAlgorithmCollection();
      if (algorithmCollection != null) {
        algorithmCollection.getAdjustTaskBoundsAlgorithm().adjustNestedTasks(this);
      }
    } catch (TaskDependencyException e) {
      if (!GPLogger.log(e)) {
        e.printStackTrace(System.err);
      }
    }
  }

  @Override
  public boolean isSupertask() {
    return myManager.getTaskHierarchy().hasNestedTasks(this);
  }

  @Override
  public void setEnd(GanttCalendar end) {
    myEnd = end;
    recalculateActivities();
  }

  @Override
  public void setThirdDate(GanttCalendar third) {
    myThird = third;
    // recalculateActivities();
  }

  @Override
  public void setThirdDateConstraint(int thirdDateConstraint) {
    myThirdDateConstraint = thirdDateConstraint;
    // recalculateActivities();
  }

  @Override
  public void shift(TimeDuration shift) {
    float unitCount = shift.getLength(myLength.getTimeUnit());
    if (unitCount != 0f) {
      Task resultTask = shift(unitCount);
      GanttCalendar oldStart = myStart;
      GanttCalendar oldEnd = myEnd;
      myStart = resultTask.getStart();
      myLength = resultTask.getDuration();
      myEnd = resultTask.getEnd();
      if (areEventsEnabled()) {
        myManager.fireTaskScheduleChanged(this, oldStart, oldEnd);
      }
      recalculateActivities();
    }
  }

  public Task shift(float unitCount) {
    Task clone = unpluggedClone();
    if (unitCount != 0) {
      Date newStart;
      if (unitCount > 0) {
        TimeDuration length = myManager.createLength(myLength.getTimeUnit(), unitCount);
        // clone.setDuration(length);
        newStart = RESTLESS_CALENDAR.shiftDate(myStart.getTime(), length);
        if (0 == (getManager().getCalendar().getDayMask(newStart) & DayMask.WORKING)) {
          newStart = getManager().getCalendar().findClosest(newStart, myLength.getTimeUnit(), MoveDirection.FORWARD, DayType.WORKING);
        }
      } else {
        newStart = RESTLESS_CALENDAR.shiftDate(clone.getStart().getTime(),
            getManager().createLength(clone.getDuration().getTimeUnit(), (long) unitCount));
        if (0 == (getManager().getCalendar().getDayMask(newStart) & DayMask.WORKING)) {
          newStart = getManager().getCalendar().findClosest(newStart, myLength.getTimeUnit(), MoveDirection.BACKWARD, DayType.WORKING);
        }
      }
      clone.setStart(CalendarFactory.createGanttCalendar(newStart));
      clone.setDuration(myLength);
    }
    return clone;
  }

  @Override
  public void setDuration(TimeDuration length) {
    assert length.getLength() >= 0 : "An attempt to set length=" + length + " to task=" + this;

    myLength = length;
    myEnd = null;
    recalculateActivities();
  }

  private Date shiftDate(Date input, TimeDuration duration) {
    return myManager.getConfig().getCalendar().shiftDate(input, duration);
  }

  @Override
  public TimeDuration translateDuration(TimeDuration duration) {
    return myManager.createLength(myLength.getTimeUnit(), translateDurationValue(duration));
  }

  private float translateDurationValue(TimeDuration duration) {
    if (myLength.getTimeUnit().equals(duration.getTimeUnit())) {
      return duration.getValue();
    }
    if (myLength.getTimeUnit().isConstructedFrom(duration.getTimeUnit())) {
      return duration.getValue() / myLength.getTimeUnit().getAtomCount(duration.getTimeUnit());
    }
    if (duration.getTimeUnit().isConstructedFrom(myLength.getTimeUnit())) {
      return duration.getValue() * duration.getTimeUnit().getAtomCount(myLength.getTimeUnit());
    }
    throw new RuntimeException("Can't translate duration=" + duration + " into units=" + myLength.getTimeUnit());
  }

  private void recalculateActivities() {
    if (myLength == null || myManager == null) {
      return;
    }
    if (isMilestone) {
      myMilestoneActivity = ImmutableList.<TaskActivity>of(new MilestoneTaskFakeActivity(this));
      return;
    }

    final Date startDate = myStart.getTime();
    final Date endDate = getEnd().getTime();

    myActivities.clear();
    if (startDate.equals(endDate)) {
      myActivities.add(new MilestoneTaskFakeActivity(this));
      return;
    }

    recalculateActivities(myManager.getConfig().getCalendar(), this, myActivities, startDate, endDate);
    int length = 0;
    for (TaskActivity activity : myActivities) {
      if (activity.getIntensity() > 0) {
        length += activity.getDuration().getLength(getDuration().getTimeUnit());
      }
    }
    myLength = getManager().createLength(myLength.getTimeUnit(), length);
  }

  private static void recalculateActivities(GPCalendarCalc calendar, Task task, List<TaskActivity> output, Date startDate,
      Date endDate) {
    TaskActivitiesAlgorithm alg = new TaskActivitiesAlgorithm(calendar);
    alg.recalculateActivities(task, output, startDate, endDate);
  }

  @Override
  public void setCompletionPercentage(int percentage) {
    if (percentage != myCompletionPercentage) {
      myCompletionPercentage = percentage;
      EventSender progressEventSender = new ProgressEventSender();
      progressEventSender.enable();
      progressEventSender.fireEvent();
    }
  }

  @Override
  public void setShape(ShapePaint shape) {
    myShape = shape;
  }

  @Override
  public void setColor(Color color) {
    myColor = color;
  }

  @Override
  public void setNotes(String notes) {
    myNotes = notes;
  }

  @Override
  public void setExpand(boolean expand) {
    bExpand = expand;
  }

  @Override
  public void addNotes(String notes) {
    myNotes += notes;
  }

  protected void enableEvents(boolean enabled) {
    myEventsEnabled = enabled;
  }

  protected boolean areEventsEnabled() {
    return myEventsEnabled && myManager.areEventsEnabled();
  }

  /**
   * Determines whether a special shape is defined for this task.
   *
   * @return true, if this task has its own shape defined.
   */
  public boolean shapeDefined() {
    return (myShape != null);
  }

  /**
   * Determines whether a special color is defined for this task.
   *
   * @return true, if this task has its own color defined.
   */
  public boolean colorDefined() {
    return (myColor != null);
  }

  @Override
  public String toString() {
    return getName();
  }

  public boolean isUnplugged() {
    return this.isUnplugged;
  }

  /** @return The CustomColumnValues. */
  @Override
  public CustomColumnsValues getCustomValues() {
    return customValues;
  }

  @Override
  public void setCritical(boolean critical) {
    this.critical = critical;
  }

  @Override
  public boolean isCritical() {
    return this.critical;
  }

  // TODO: implementation of this method has no correlation with algorithms
  // recalculating schedules,
  // doesn't affect subtasks and supertasks. It is necessary to call this
  // method explicitly from other
  // parts of code to be sure that constraint fulfills
  @Override
  public void applyThirdDateConstraint() {
//    if (getThird() != null)
//      switch (getThirdDateConstraint()) {
//      case EARLIESTBEGIN:
//        if (getThird().after(getStart())) {
//          shift(myManager.getTimeUnitStack().createDuration(getDuration().getTimeUnit(), getStart().getTime(), getThird().getTime()));
//        }
//        break;
//      }
  }

  private TaskInfo myTaskInfo;

  @Override
  public TaskInfo getTaskInfo() {
    return myTaskInfo;
  }

  @Override
  public void setTaskInfo(TaskInfo taskInfo) {
    myTaskInfo = taskInfo;
  }

  @Override
  public boolean isProjectTask() {
    return isProjectTask;
  }

  @Override
  public void setProjectTask(boolean projectTask) {
    isProjectTask = projectTask;
  }

  private class CostImpl implements Cost {
    private BigDecimal myValue = BigDecimal.ZERO;
    private boolean isCalculated = true;

    @Override
    public BigDecimal getValue() {
      return (isCalculated) ? getCalculatedValue() : getManualValue();
    }

    @Override
    public BigDecimal getManualValue() {
      return myValue;
    }

    @Override
    public BigDecimal getCalculatedValue() {
      return new CostAlgorithmImpl().getCalculatedCost(TaskImpl.this);
    }

    @Override
    public void setValue(BigDecimal value) {
      myValue = value;
    }

    public void setValue(Cost copy) {
      myValue = copy.getValue();
      isCalculated = copy.isCalculated();
    }

    @Override
    public boolean isCalculated() {
      return isCalculated;
    }

    @Override
    public void setCalculated(boolean calculated) {
      isCalculated = calculated;
    }
  }

  @Override
  public Cost getCost() {
    return myCost;
  }
}
