/*
 * Created on 05.07.2003
 *
 */
package net.sourceforge.ganttproject.task;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

import org.jdesktop.swingx.treetable.MutableTreeTableNode;

import biz.ganttproject.core.calendar.AlwaysWorkingTimeCalendarImpl;
import biz.ganttproject.core.calendar.GPCalendarCalc;
import biz.ganttproject.core.calendar.GPCalendarListener;
import biz.ganttproject.core.chart.scene.BarChartActivity;
import biz.ganttproject.core.chart.scene.gantt.ChartBoundsAlgorithm;
import biz.ganttproject.core.chart.scene.gantt.ChartBoundsAlgorithm.Result;
import biz.ganttproject.core.option.DefaultEnumerationOption;
import biz.ganttproject.core.option.DefaultStringOption;
import biz.ganttproject.core.option.EnumerationOption;
import biz.ganttproject.core.option.StringOption;
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.TimeUnit;
import biz.ganttproject.core.time.TimeUnitStack;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;

import net.sourceforge.ganttproject.CustomPropertyDefinition;
import net.sourceforge.ganttproject.CustomPropertyListener;
import net.sourceforge.ganttproject.CustomPropertyManager;
import net.sourceforge.ganttproject.GPLogger;
import net.sourceforge.ganttproject.GanttTask;
import net.sourceforge.ganttproject.ProjectEventListener;
import net.sourceforge.ganttproject.gui.NotificationChannel;
import net.sourceforge.ganttproject.gui.NotificationItem;
import net.sourceforge.ganttproject.gui.NotificationManager;
import net.sourceforge.ganttproject.gui.options.model.GP1XOptionConverter;
import net.sourceforge.ganttproject.language.GanttLanguage;
import net.sourceforge.ganttproject.resource.HumanResource;
import net.sourceforge.ganttproject.resource.HumanResourceManager;
import net.sourceforge.ganttproject.task.algorithm.AdjustTaskBoundsAlgorithm;
import net.sourceforge.ganttproject.task.algorithm.AlgorithmCollection;
import net.sourceforge.ganttproject.task.algorithm.CriticalPathAlgorithm;
import net.sourceforge.ganttproject.task.algorithm.CriticalPathAlgorithmImpl;
import net.sourceforge.ganttproject.task.algorithm.DependencyGraph;
import net.sourceforge.ganttproject.task.algorithm.FindPossibleDependeesAlgorithm;
import net.sourceforge.ganttproject.task.algorithm.FindPossibleDependeesAlgorithmImpl;
import net.sourceforge.ganttproject.task.algorithm.RecalculateTaskCompletionPercentageAlgorithm;
import net.sourceforge.ganttproject.task.algorithm.RecalculateTaskScheduleAlgorithm;
import net.sourceforge.ganttproject.task.algorithm.SchedulerImpl;
import net.sourceforge.ganttproject.task.dependency.EventDispatcher;
import net.sourceforge.ganttproject.task.dependency.TaskDependency;
import net.sourceforge.ganttproject.task.dependency.TaskDependency.Hardness;
import net.sourceforge.ganttproject.task.dependency.TaskDependencyCollection;
import net.sourceforge.ganttproject.task.dependency.TaskDependencyCollectionImpl;
import net.sourceforge.ganttproject.task.dependency.TaskDependencyConstraint;
import net.sourceforge.ganttproject.task.dependency.TaskDependencyException;
import net.sourceforge.ganttproject.task.dependency.constraint.FinishFinishConstraintImpl;
import net.sourceforge.ganttproject.task.dependency.constraint.FinishStartConstraintImpl;
import net.sourceforge.ganttproject.task.dependency.constraint.StartFinishConstraintImpl;
import net.sourceforge.ganttproject.task.dependency.constraint.StartStartConstraintImpl;
import net.sourceforge.ganttproject.task.event.TaskDependencyEvent;
import net.sourceforge.ganttproject.task.event.TaskHierarchyEvent;
import net.sourceforge.ganttproject.task.event.TaskListener;
import net.sourceforge.ganttproject.task.event.TaskPropertyEvent;
import net.sourceforge.ganttproject.task.event.TaskScheduleEvent;
import net.sourceforge.ganttproject.task.hierarchy.TaskHierarchyManagerImpl;
import net.sourceforge.ganttproject.util.collect.Pair;

/**
 * @author bard
 */
public class TaskManagerImpl implements TaskManager {
  private static final GPCalendarCalc RESTLESS_CALENDAR = new AlwaysWorkingTimeCalendarImpl();

  private final TaskHierarchyManagerImpl myHierarchyManager;

  private final TaskDependencyCollectionImpl myDependencyCollection;

  private final AlgorithmCollection myAlgorithmCollection;

  private final List<TaskListener> myListeners = new ArrayList<TaskListener>();

  private AtomicInteger myMaxID = new AtomicInteger(0);

  private final Task myRoot;

  private final TaskManagerConfig myConfig;

  private final TaskNamePrefixOption myTaskNamePrefixOption = new TaskNamePrefixOption();

  private final StringOption myTaskCopyNamePrefixOption = new DefaultStringOption("taskCopyNamePrefix", GanttLanguage.getInstance().getText("task.copy.prefix"));

  private final EnumerationOption myDependencyHardnessOption = new DefaultEnumerationOption<Object>(
      "dependencyDefaultHardness", new String[] { "Strong", "Rubber" }) {
    {
      resetValue("Strong", true);
    }
  };

  private final TaskContainmentHierarchyFacade.Factory myFacadeFactory;

  private final Supplier<TaskContainmentHierarchyFacade> myHierarchySupplier = new Supplier<TaskContainmentHierarchyFacade>() {
    @Override
    public TaskContainmentHierarchyFacade get() {
      return getTaskHierarchy();
    }
  };
  private final DependencyGraph myDependencyGraph = new DependencyGraph(myHierarchySupplier, new DependencyGraph.Logger() {
    @Override
    public void logDependencyLoop(String title, String message) {
      if (myConfig.getNotificationManager() != null) {
        myConfig.getNotificationManager().addNotifications(NotificationChannel.WARNING,
            ImmutableList.of(new NotificationItem("Dependency loop detected", message, NotificationManager.DEFAULT_HYPERLINK_LISTENER)));
      }
      GPLogger.log(title + "\n" + message);
    }
  });

  private final SchedulerImpl myScheduler = new SchedulerImpl(myDependencyGraph, myHierarchySupplier);

  private boolean areEventsEnabled = true;

  private static class TaskMap {
    private final Map<Integer, Task> myId2task = new HashMap<Integer, Task>();
    private TaskDocumentOrderComparator myComparator;
    private boolean isModified = true;
    private Task[] myArray;
    private final TaskManagerImpl myManager;

    TaskMap(TaskManagerImpl taskManager) {
      myComparator = new TaskDocumentOrderComparator(taskManager);
      myManager = taskManager;
    }

    void addTask(Task task) {
      myId2task.put(new Integer(task.getTaskID()), task);
      isModified = true;
    }

    Task getTask(int id) {
      return myId2task.get(new Integer(id));
    }

    public Task[] getTasks() {
      if (isModified) {
        myArray = myId2task.values().toArray(new Task[myId2task.size()]);
        Arrays.sort(myArray, myComparator);
        isModified = false;
      }
      return myArray;
    }

    public void clear() {
      myId2task.clear();
      isModified = true;
    }

    public void removeTask(Task task) {
      myId2task.remove(new Integer(task.getTaskID()));
      Task[] nestedTasks = myManager.getTaskHierarchy().getNestedTasks(task);
      for (int i = 0; i < nestedTasks.length; i++) {
        removeTask(nestedTasks[i]);
      }
      isModified = true;
    }

    public int size() {
      return myId2task.size();
    }

    public boolean isEmpty() {
      return myId2task.isEmpty();
    }

    void setDirty() {
      isModified = true;
    }
  }

  private final TaskMap myTaskMap = new TaskMap(this);

  private final CustomPropertyListenerImpl myCustomPropertyListener;

  private final CustomColumnsManager myCustomColumnsManager;

  private Boolean isZeroMilestones = true;

  TaskManagerImpl(TaskContainmentHierarchyFacade.Factory containmentFacadeFactory, TaskManagerConfig config) {
    myCustomPropertyListener = new CustomPropertyListenerImpl(this);
    myCustomColumnsManager = new CustomColumnsManager();
    myCustomColumnsManager.addListener(getCustomPropertyListener());

    myConfig = config;
    myHierarchyManager = new TaskHierarchyManagerImpl();
    EventDispatcher dispatcher = new EventDispatcher() {
      @Override
      public void fireDependencyAdded(TaskDependency dep) {
        TaskManagerImpl.this.fireDependencyAdded(dep);
      }
      @Override
      public void fireDependencyRemoved(TaskDependency dep) {
        TaskManagerImpl.this.fireDependencyRemoved(dep);
      }
      @Override
      public void fireDependencyChanged(TaskDependency dep) {
        TaskManagerImpl.this.fireDependencyChanged(dep);
      }
    };
    myDependencyCollection = new TaskDependencyCollectionImpl(containmentFacadeFactory, dispatcher) {
      @Override
      protected TaskContainmentHierarchyFacade getTaskHierarchy() {
        return TaskManagerImpl.this.getTaskHierarchy();
      }

      @Override
      protected Hardness getDefaultHardness() {
        String optionValue = getDependencyHardnessOption().getValue();
        return optionValue == null ? super.getDefaultHardness() : TaskDependency.Hardness.parse(optionValue);
      }

    };
    myFacadeFactory = containmentFacadeFactory == null ? new FacadeFactoryImpl() : containmentFacadeFactory;
    // clear();
    myRoot = createRootTask();

    FindPossibleDependeesAlgorithm alg1 = new FindPossibleDependeesAlgorithmImpl() {
      @Override
      protected TaskContainmentHierarchyFacade createContainmentFacade() {
        return TaskManagerImpl.this.getTaskHierarchy();
      }

    };
    AdjustTaskBoundsAlgorithm alg3 = new AdjustTaskBoundsAlgorithm() {
      @Override
      protected TaskContainmentHierarchyFacade createContainmentFacade() {
        return TaskManagerImpl.this.getTaskHierarchy();
      }
    };
    RecalculateTaskScheduleAlgorithm alg2 = new RecalculateTaskScheduleAlgorithm(alg3) {
      @Override
      protected TaskContainmentHierarchyFacade createContainmentFacade() {
        return TaskManagerImpl.this.getTaskHierarchy();
      }
    };
    RecalculateTaskCompletionPercentageAlgorithm alg4 = new RecalculateTaskCompletionPercentageAlgorithm() {
      @Override
      protected TaskContainmentHierarchyFacade createContainmentFacade() {
        return TaskManagerImpl.this.getTaskHierarchy();
      }
    };
    ChartBoundsAlgorithm alg5 = new ChartBoundsAlgorithm();
    CriticalPathAlgorithm alg6 = new CriticalPathAlgorithmImpl(this, getCalendar());
    myAlgorithmCollection = new AlgorithmCollection(this, alg1, alg2, alg3, alg4, alg5, alg6, myScheduler);
    addTaskListener(myScheduler.getTaskModelListener());
  }

  private CustomPropertyListener getCustomPropertyListener() {
    return myCustomPropertyListener;
  }

  @Override
  public GanttTask getTask(int taskId) {
    return (GanttTask) myTaskMap.getTask(taskId);
  }

  @Override
  public Task getRootTask() {
    return myRoot;
  }

  @Override
  public Task[] getTasks() {
    return myTaskMap.getTasks();
  }

  private Task createRootTask() {
    Calendar c = CalendarFactory.newCalendar();
    Date today = c.getTime();
    Task root = new GanttTask(null, CalendarFactory.createGanttCalendar(today), 1, this, -1);
    root.setStart(CalendarFactory.createGanttCalendar(today));
    root.setDuration(createLength(getConfig().getTimeUnitStack().getDefaultTimeUnit(), 1));
    root.setExpand(true);
    root.setName("root");
    return root;
  }

  private void projectClosed() {
    myDependencyGraph.clear();
    myTaskMap.clear();
    myMaxID.set(0);
    myDependencyCollection.clear();
    // createRootTask();
    fireTaskModelReset();
  }

  private void projectOpened() {
    processCriticalPath(getRootTask());
    myAlgorithmCollection.getRecalculateTaskCompletionPercentageAlgorithm().run(getRootTask());
  }

  @Override
  public void deleteTask(Task tasktoRemove) {
    Task container = getTaskHierarchy().getContainer(tasktoRemove);
    myTaskMap.removeTask(tasktoRemove);
    tasktoRemove.delete();
    fireTaskRemoved(container, tasktoRemove);
  }

  @Override
  public GanttTask createTask() {
    return (GanttTask) newTaskBuilder().build();
  }

  @Override
  public GanttTask createTask(int id) {
    return (GanttTask) newTaskBuilder().withId(id).build();
  }

  @Override
  public TaskBuilder newTaskBuilder() {
    return new TaskBuilder() {
      @Override
      public Task build() {

        if (myId == null || myTaskMap.getTask(myId) != null) {
          myId = getAndIncrementId();
        }

        TaskImpl task = myPrototype == null ?
            new GanttTask("", CalendarFactory.createGanttCalendar(), 1, TaskManagerImpl.this, myId) : new GanttTask((TaskImpl)myPrototype);

        String name = myName == null ? getTaskNamePrefixOption().getValue() + "_" + task.getTaskID() : myName;
        task.setName(name);

        if (myStartDate != null) {
          GanttCalendar cal = CalendarFactory.createGanttCalendar(myStartDate);
          task.setStart(cal);
        }
        TimeDuration duration;
        if (myDuration != null) {
          duration = myDuration;
        } else if (myPrototype != null) {
          duration = myPrototype.getDuration();
        } else {
          duration = (myEndDate == null)
              ? createLength(getTimeUnitStack().getDefaultTimeUnit(), 1.0f)
                  : createLength(getTimeUnitStack().getDefaultTimeUnit(), myStartDate, myEndDate);
        }
        task.setDuration(duration);

        if (myColor != null) {
          task.setColor(myColor);
        }
        if (myPriority != null) {
          task.setPriority(myPriority);
        }
        if (isExpanded != null) {
          task.setExpand(isExpanded);
        }
        if (myNotes != null) {
          task.setNotes(myNotes);
        }
        if (myWebLink != null) {
          task.setWebLink(myWebLink);
        }
        if (myCompletion != null) {
          task.setCompletionPercentage(myCompletion);
        }
        if (myCost != null) {
          task.getCost().setCalculated(false);
          task.getCost().setValue(myCost);
        }
        registerTask(task);


        if (myPrevSibling != null && myPrevSibling != getRootTask()) {
          int position = getTaskHierarchy().getTaskIndex(myPrevSibling) + 1;
          Task parentTask = getTaskHierarchy().getContainer(myPrevSibling);
          getTaskHierarchy().move(task, parentTask, position);
        } else {
          Task parentTask = myParent == null ? getRootTask() : myParent;
          getTaskHierarchy().move(task, parentTask);
        }

        if (isLegacyMilestone) {
          task.setMilestone(isLegacyMilestone);
        }
        fireTaskAdded(task);
        return task;
      }
    };
  }

  protected TimeUnitStack getTimeUnitStack() {
    return getConfig().getTimeUnitStack();
  }

  int getAndIncrementId() {
    return myMaxID.getAndIncrement();
  }

  @Override
  public void registerTask(Task task) {
    int taskID = task.getTaskID();
    assert myTaskMap.getTask(taskID) == null : "There is a task that already has the ID " + taskID;
    myTaskMap.addTask(task);
    myMaxID.set(Math.max(taskID + 1, myMaxID.get()));
    myDependencyGraph.addTask(task);
  }

  boolean isRegistered(TaskImpl task) {
    return myTaskMap.getTask(task.getTaskID()) != null;
  }

  @Override
  public int getTaskCount() {
    return myTaskMap.size();
  }

  private static Iterable<BarChartActivity<?>> tasksToActivities(Task[] tasks) {
    return Iterables.transform(Arrays.asList(tasks), new Function<Task, BarChartActivity<?>>() {
      @Override
      public BarChartActivity<?> apply(final Task task) {
        return new BarChartActivity<Task>() {
          @Override
          public Date getStart() {
            return task.getStart().getTime();
          }
          @Override
          public Date getEnd() {
            return task.getEnd().getTime();
          }
          @Override
          public TimeDuration getDuration() {
            return task.getDuration();
          }
          @Override
          public Task getOwner() {
            return task;
          }
        };
      }
    });
  }

  @Override
  public TimeDuration getProjectLength() {
    if (myTaskMap.isEmpty()) {
      return createLength(getConfig().getTimeUnitStack().getDefaultTimeUnit(), 0);
    }
    Result result = getAlgorithmCollection().getProjectBoundsAlgorithm().getBounds(tasksToActivities(myTaskMap.getTasks()));
    return createLength(getConfig().getTimeUnitStack().getDefaultTimeUnit(), result.lowerBound, result.upperBound);
  }

  @Override
  public Date getProjectStart() {
    if (myTaskMap.isEmpty()) {
      return myRoot.getStart().getTime();
    }
    Result result = getAlgorithmCollection().getProjectBoundsAlgorithm().getBounds(tasksToActivities(myTaskMap.getTasks()));
    return result.lowerBound;
  }

  @Override
  public Date getProjectEnd() {
    if (myTaskMap.isEmpty()) {
      return myRoot.getStart().getTime();
    }
    Result result = getAlgorithmCollection().getProjectBoundsAlgorithm().getBounds(tasksToActivities(myTaskMap.getTasks()));
    return result.upperBound;
  }

  @Override
  public int getProjectCompletion() {
    return myRoot.getCompletionPercentage();
  }

  @Override
  public String encode(TimeDuration taskLength) {
    StringBuffer result = new StringBuffer(String.valueOf(taskLength.getLength()));
    result.append(myConfig.getTimeUnitStack().encode(taskLength.getTimeUnit()));
    return result.toString();
  }

  @Override
  public TimeDuration createLength(String lengthAsString) throws DurationParsingException {
    int state = 0;
    StringBuffer valueBuffer = new StringBuffer();
    Integer currentValue = null;
    TimeDuration currentLength = null;
    lengthAsString += " ";
    for (int i = 0; i < lengthAsString.length(); i++) {
      char nextChar = lengthAsString.charAt(i);
      if (Character.isDigit(nextChar)) {
        switch (state) {
        case 0:
          if (currentValue != null) {
            throw new DurationParsingException();
          }
          state = 1;
          valueBuffer.setLength(0);
        case 1:
          valueBuffer.append(nextChar);
          break;
        case 2:
          TimeUnit timeUnit = findTimeUnit(valueBuffer.toString());
          if (timeUnit == null) {
            throw new DurationParsingException(valueBuffer.toString());
          }
          assert currentValue != null;
          TimeDuration localResult = createLength(timeUnit, currentValue.floatValue());
          if (currentLength == null) {
            currentLength = localResult;
          } else {
            if (currentLength.getTimeUnit().isConstructedFrom(timeUnit)) {
              float recalculatedLength = currentLength.getLength(timeUnit);
              currentLength = createLength(timeUnit, localResult.getValue() + recalculatedLength);
            } else {
              throw new DurationParsingException();
            }
          }
          state = 1;
          currentValue = null;
          valueBuffer.setLength(0);
          valueBuffer.append(nextChar);
          break;
        }
      } else if (Character.isWhitespace(nextChar)) {
        switch (state) {
        case 0:
          break;
        case 1:
          currentValue = Integer.valueOf(valueBuffer.toString());
          state = 0;
          break;
        case 2:
          TimeUnit timeUnit = findTimeUnit(valueBuffer.toString());
          if (timeUnit == null) {
            throw new DurationParsingException(valueBuffer.toString());
          }
          assert currentValue != null;
          TimeDuration localResult = createLength(timeUnit, currentValue.floatValue());
          if (currentLength == null) {
            currentLength = localResult;
          } else {
            if (currentLength.getTimeUnit().isConstructedFrom(timeUnit)) {
              float recalculatedLength = currentLength.getLength(timeUnit);
              currentLength = createLength(timeUnit, localResult.getValue() + recalculatedLength);
            } else {
              throw new DurationParsingException();
            }
          }
          state = 0;
          currentValue = null;
          break;
        }
      } else {
        switch (state) {
        case 1:
          currentValue = Integer.valueOf(valueBuffer.toString());
        case 0:
          if (currentValue == null) {
            throw new DurationParsingException("Failed to parse value=" + lengthAsString);
          }
          state = 2;
          valueBuffer.setLength(0);
        case 2:
          valueBuffer.append(nextChar);
          break;
        }
      }
    }
    if (currentValue != null) {
      currentValue = Integer.valueOf(valueBuffer.toString());
      TimeUnit dayUnit = findTimeUnit("d");
      currentLength = createLength(dayUnit, currentValue.floatValue());
    }
    return currentLength;
  }

  private TimeUnit findTimeUnit(String code) {
    return myConfig.getTimeUnitStack().findTimeUnit(code);
  }

  @Override
  public TimeDuration createLength(TimeUnit unit, float length) {
    return new TimeDurationImpl(unit, length);
  }

  @Override
  public TimeDuration createLength(long count) {
    return new TimeDurationImpl(getConfig().getTimeUnitStack().getDefaultTimeUnit(), count);
  }

  @Override
  public TimeDuration createLength(TimeUnit timeUnit, Date startDate, Date endDate) {
    return getConfig().getTimeUnitStack().createDuration(timeUnit, startDate, endDate);
  }

  @Override
  public Date shift(Date original, TimeDuration duration) {
    GPCalendarCalc calendar = RESTLESS_CALENDAR;
    return calendar.shiftDate(original, duration);
  }

  @Override
  public TaskDependencyCollection getDependencyCollection() {
    return myDependencyCollection;
  }

  @Override
  public AlgorithmCollection getAlgorithmCollection() {
    return myAlgorithmCollection;
  }

  public TaskHierarchyManagerImpl getHierarchyManager() {
    return myHierarchyManager;
  }

  @Override
  public TaskDependencyConstraint createConstraint(final TaskDependencyConstraint.Type type) {
    TaskDependencyConstraint result;
    switch (type) {
    case finishstart:
      result = new FinishStartConstraintImpl();
      break;
    case finishfinish:
      result = new FinishFinishConstraintImpl();
      break;
    case startfinish:
      result = new StartFinishConstraintImpl();
      break;
    case startstart:
      result = new StartStartConstraintImpl();
      break;
    default:
      throw new IllegalArgumentException("Unknown constraint type=" + type);
    }
    return result;
  }

  @Override
  public void addTaskListener(TaskListener listener) {
    myListeners.add(listener);
  }

  @Override
  public GPCalendarCalc getCalendar() {
    return getConfig().getCalendar();
  }

  public ProjectEventListener getProjectListener() {
    return new ProjectEventListener.Stub() {
      @Override
      public void projectClosed() {
        TaskManagerImpl.this.projectClosed();
      }

      @Override
      public void projectOpened() {
        TaskManagerImpl.this.projectOpened();
      }
    };
  }

  public GPCalendarListener getCalendarListener() {
    return new GPCalendarListener() {
      @Override
      public void onCalendarChange() {
        for (Task t : getTasks()) {
          t.setEnd(null);
        }
        myScheduler.run();
      }
    };
  }
  public void fireTaskProgressChanged(Task changedTask) {
    if (areEventsEnabled) {
      TaskPropertyEvent e = new TaskPropertyEvent(changedTask);
      for (int i = 0; i < myListeners.size(); i++) {
        TaskListener next = myListeners.get(i);
        next.taskProgressChanged(e);
      }
    }
  }

  void fireTaskScheduleChanged(Task changedTask, GanttCalendar oldStartDate, GanttCalendar oldFinishDate) {
    myScheduler.run();
    if (areEventsEnabled) {
      TaskScheduleEvent e = new TaskScheduleEvent(changedTask, oldStartDate, oldFinishDate, changedTask.getStart(),
          changedTask.getEnd());
      // List copy = new ArrayList(myListeners);
      // myListeners.clear();
      for (int i = 0; i < myListeners.size(); i++) {
        TaskListener next = myListeners.get(i);
        next.taskScheduleChanged(e);
      }
    }
  }

  private void fireDependencyAdded(TaskDependency newDependency) {
    myDependencyGraph.addDependency(newDependency);
    if (areEventsEnabled) {
      TaskDependencyEvent e = new TaskDependencyEvent(getDependencyCollection(), newDependency);
      for (int i = 0; i < myListeners.size(); i++) {
        TaskListener next = myListeners.get(i);
        next.dependencyAdded(e);
      }
    }
  }

  private void fireDependencyRemoved(TaskDependency dep) {
    myDependencyGraph.removeDependency(dep);
    TaskDependencyEvent e = new TaskDependencyEvent(getDependencyCollection(), dep);
    for (int i = 0; i < myListeners.size(); i++) {
      TaskListener next = myListeners.get(i);
      next.dependencyRemoved(e);
    }
  }

  private void fireDependencyChanged(TaskDependency dep) {
    TaskDependencyEvent e = new TaskDependencyEvent(getDependencyCollection(), dep);
    for (int i = 0; i < myListeners.size(); i++) {
      TaskListener next = myListeners.get(i);
      next.dependencyChanged(e);
    }
  }

  private void fireTaskAdded(Task task) {
    if (areEventsEnabled) {
      TaskHierarchyEvent e = new TaskHierarchyEvent(this, task, null, getTaskHierarchy().getContainer(task));
      for (int i = 0; i < myListeners.size(); i++) {
        TaskListener next = myListeners.get(i);
        next.taskAdded(e);
      }
    }
  }

  private void fireTaskRemoved(Task container, Task task) {
    myDependencyGraph.removeTask(task);
    if (areEventsEnabled) {
      TaskHierarchyEvent e = new TaskHierarchyEvent(this, task, container, null);
      for (TaskListener l : myListeners) {
        l.taskRemoved(e);
      }
    }
  }
  void fireTaskPropertiesChanged(Task task) {
    if (areEventsEnabled) {
      TaskPropertyEvent e = new TaskPropertyEvent(task);
      for (int i = 0; i < myListeners.size(); i++) {
        TaskListener next = myListeners.get(i);
        next.taskPropertiesChanged(e);
      }
    }
  }

  private void fireTaskModelReset() {
    if (areEventsEnabled) {
      for (int i = 0; i < myListeners.size(); i++) {
        TaskListener next = myListeners.get(i);
        next.taskModelReset();
      }
    }
  }

  public TaskManagerConfig getConfig() {
    return myConfig;
  }

  private final class FacadeImpl implements TaskContainmentHierarchyFacade {
    // private final Task myRoot;

    private List<Task> myPathBuffer = new ArrayList<Task>();

    // public FacadeImpl(Task root) {
    // myRoot = root;
    // }

    @Override
    public Task[] getNestedTasks(Task container) {
      return container.getNestedTasks();
    }

    @Override
    public Task[] getDeepNestedTasks(Task container) {
      ArrayList<Task> result = new ArrayList<Task>();
      addDeepNestedTasks(container, result);
      return result.toArray(new Task[result.size()]);
    }

    private void addDeepNestedTasks(Task container, ArrayList<Task> result) {
      Task[] nested = container.getNestedTasks();
      result.addAll(Arrays.asList(nested));
      for (int i = 0; i < nested.length; i++) {
        addDeepNestedTasks(nested[i], result);
      }
    }

    @Override
    public boolean hasNestedTasks(Task container) {
      return container.getNestedTasks().length > 0;
    }

    @Override
    public Task getRootTask() {
      return TaskManagerImpl.this.getRootTask();
    }

    @Override
    public Task getContainer(Task nestedTask) {
      return nestedTask.getSupertask();
    }

    @Override
    public Task getPreviousSibling(Task nestedTask) {
      int pos = getTaskIndex(nestedTask);
      return pos == 0 ? null : nestedTask.getSupertask().getNestedTasks()[pos - 1];
    }

    @Override
    public Task getNextSibling(Task nestedTask) {
      throw new UnsupportedOperationException();
    }

    @Override
    public int getTaskIndex(Task nestedTask) {
      Task container = nestedTask.getSupertask();
      if (container == null) {
        return 0;
      }
      return Arrays.asList(container.getNestedTasks()).indexOf(nestedTask);
    }

    @Override
    public boolean areUnrelated(Task first, Task second) {
      if (first.equals(second)) {
        return false;
      }
      myPathBuffer.clear();
      for (Task container = getContainer(first); container != null; container = getContainer(container)) {
        myPathBuffer.add(container);
      }
      if (myPathBuffer.contains(second)) {
        return false;
      }
      myPathBuffer.clear();
      for (Task container = getContainer(second); container != null; container = getContainer(container)) {
        myPathBuffer.add(container);
      }
      if (myPathBuffer.contains(first)) {
        return false;
      }
      return true;
    }

    @Override
    public void move(Task whatMove, Task whereMove) {
      whatMove.move(whereMove);
    }

    @Override
    public void move(Task whatMove, Task whereMove, int index) {
      whatMove.move(whereMove);
    }

    @Override
    public int getDepth(Task task) {
      int depth = 0;
      while (task != myRoot) {
        task = task.getSupertask();
        depth++;
      }
      return depth;
    }

    @Override
    public int compareDocumentOrder(Task task1, Task task2) {
      if (task1 == task2) {
        return 0;
      }
      List<Task> buffer1 = new ArrayList<Task>();
      for (Task container = task1; container != null; container = getContainer(container)) {
        buffer1.add(0, container);
      }
      List<Task> buffer2 = new ArrayList<Task>();
      for (Task container = task2; container != null; container = getContainer(container)) {
        buffer2.add(0, container);
      }
      if (buffer1.get(0) != getRootTask() && buffer2.get(0) == getRootTask()) {
        return -1;
      }
      if (buffer1.get(0) == getRootTask() && buffer2.get(0) != getRootTask()) {
        return 1;
      }

      int i = 0;
      Task commonRoot = null;
      while (true) {
        if (i == buffer1.size()) {
          return -1;
        }
        if (i == buffer2.size()) {
          return 1;
        }
        Task root1 = buffer1.get(i);
        Task root2 = buffer2.get(i);
        if (root1 != root2) {
          assert commonRoot != null : "Failure comparing task=" + task1 + " and task=" + task2 + "\n. Path1=" + buffer1
              + "\nPath2=" + buffer2;
          Task[] nestedTasks = commonRoot.getNestedTasks();
          for (int j = 0; j < nestedTasks.length; j++) {
            if (nestedTasks[j] == root1) {
              return -1;
            }
            if (nestedTasks[j] == root2) {
              return 1;
            }
          }
          throw new IllegalStateException("We should not be here");
        }
        i++;
        commonRoot = root1;
      }
    }

    @Override
    public boolean contains(Task task) {
      throw new UnsupportedOperationException();
    }

    @Override
    public List<Task> getTasksInDocumentOrder() {
      List<Task> result = Lists.newArrayList();
      Deque<Task> deque = new LinkedList<Task>();
      deque.addFirst(getRootTask());
      while (!deque.isEmpty()) {
        Task head = deque.poll();
        result.addAll(0, Arrays.asList(head.getNestedTasks()));
      }
      return result;
    }


    @Override
    public void breadthFirstSearch(Task root, Predicate<Pair<Task, Task>> predicate) {
      Preconditions.checkNotNull(root);
      Queue<Task> queue = Queues.newArrayDeque();
      if (predicate.apply(Pair.create((Task) null, root))) {
        queue.add(root);
      }
      while (!queue.isEmpty()) {
        Task head = queue.poll();
        for (Task child : head.getNestedTasks()) {
          if (predicate.apply(Pair.create(head, child))) {
            queue.add(child);
          }
        }
      }
    }

    @Override
    public List<Task> breadthFirstSearch(Task root, final boolean includeRoot) {
      final Task _root = (root == null) ? getRootTask() : root;
      final List<Task> result = Lists.newArrayList();
      breadthFirstSearch(_root, new Predicate<Pair<Task,Task>>() {
        public boolean apply(Pair<Task, Task> parent_child) {
          if (includeRoot || parent_child.first() != null) {
            result.add(parent_child.second());
          }
          return true;
        }
      });
      return result;
    }

    @Override
    public List<Integer> getOutlinePath(Task task) {
      throw new UnsupportedOperationException();
    }
  }

  private class FacadeFactoryImpl implements TaskContainmentHierarchyFacade.Factory {
    // private final Task myRoot;
    //
    // FacadeFactoryImpl(Task root) {
    // myRoot = root;
    // }

    @Override
    public TaskContainmentHierarchyFacade createFacade() {
      return new FacadeImpl();
    }
  }

  @Override
  public TaskContainmentHierarchyFacade getTaskHierarchy() {
    // if (myTaskContainment==null) {
    return myFacadeFactory.createFacade();
    // }
    // return myTaskContainment;
  }

  @Override
  public TaskManager emptyClone() {
    TaskManagerImpl result = new TaskManagerImpl(null, myConfig);
    result.myDependencyHardnessOption.setValue(this.myDependencyHardnessOption.getValue());
    return result;
  }

  @Override
  public Map<Task, Task> importData(TaskManager taskManager,
      Map<CustomPropertyDefinition, CustomPropertyDefinition> customPropertyMapping) {
    Task importRoot = taskManager.getRootTask();
    Map<Task, Task> original2imported = new LinkedHashMap<Task, Task>();
    importData(importRoot, getRootTask(), customPropertyMapping, original2imported);
    TaskDependency[] deps = taskManager.getDependencyCollection().getDependencies();
    for (int i = 0; i < deps.length; i++) {
      Task nextDependant = deps[i].getDependant();
      Task nextDependee = deps[i].getDependee();
      Task importedDependant = original2imported.get(nextDependant);
      Task importedDependee = original2imported.get(nextDependee);
      try {
        TaskDependency dependency = getDependencyCollection().createDependency(importedDependant, importedDependee,
            new FinishStartConstraintImpl());
        dependency.setConstraint(deps[i].getConstraint());
        dependency.setDifference(deps[i].getDifference());
        dependency.setHardness(deps[i].getHardness());
      } catch (TaskDependencyException e) {
        if (!GPLogger.log(e)) {
          e.printStackTrace(System.err);
        }
      }
    }
    return original2imported;
  }

  private void importData(Task importRoot, Task root,
      Map<CustomPropertyDefinition, CustomPropertyDefinition> customPropertyMapping, Map<Task, Task> original2imported) {
    Task[] nested = importRoot.getManager().getTaskHierarchy().getNestedTasks(importRoot);
    for (int i = 0; i < nested.length; i++) {
      TaskManager.TaskBuilder builder = newTaskBuilder();
      GanttTask that = (GanttTask) nested[i];
      if (getTask(that.getTaskID()) == null) {
        builder = builder.withId(that.getTaskID());
      }
      Task nextImported = builder.withName(that.getName()).withStartDate(that.getStart().getTime())
        .withDuration(that.getDuration())
        .withColor(that.getColor()).withNotes(that.getNotes()).withWebLink(that.getWebLink()).withParent(root).build();

      nextImported.setShape(nested[i].getShape());
      nextImported.setCompletionPercentage(nested[i].getCompletionPercentage());
      nextImported.setTaskInfo(nested[i].getTaskInfo());
      nextImported.setExpand(nested[i].getExpand());
      nextImported.setMilestone(nested[i].isMilestone());
      nextImported.getCost().setValue(that.getCost());
      if (nested[i].getThird() != null) {
        nextImported.setThirdDate(nested[i].getThird().clone());
        nextImported.setThirdDateConstraint(nested[i].getThirdDateConstraint());
      }

      CustomColumnsValues customValues = nested[i].getCustomValues();
      for (CustomPropertyDefinition thatDef : importRoot.getManager().getCustomPropertyManager().getDefinitions()) {
        CustomPropertyDefinition thisDef = customPropertyMapping.get(thatDef);
        Object value = customValues.getValue(thatDef);
        if (value != null) {
          try {
            nextImported.getCustomValues().setValue(thisDef, value);
          } catch (CustomColumnsException e) {
            if (!GPLogger.log(e)) {
              e.printStackTrace(System.err);
            }
          }
        }
      }
      original2imported.put(nested[i], nextImported);
      importData(nested[i], nextImported, customPropertyMapping, original2imported);
    }
  }

  public Date findClosestWorkingTime(Date time) {
    return getCalendar().findClosestWorkingTime(time);
  }

  @Override
  public void processCriticalPath(Task root) {
    try {
      myAlgorithmCollection.getRecalculateTaskScheduleAlgorithm().run();
    } catch (TaskDependencyException e) {
      if (!GPLogger.log(e)) {
        e.printStackTrace(System.err);
      }
    }
    Task[] tasks = myAlgorithmCollection.getCriticalPathAlgorithm().getCriticalTasks();
    resetCriticalPath();
    for (int i = 0; i < tasks.length; i++) {
      tasks[i].setCritical(true);
    }
  }

  private void resetCriticalPath() {
    Task[] allTasks = getTasks();
    for (int i = 0; i < allTasks.length; i++) {
      allTasks[i].setCritical(false);
    }
  }

  @Override
  public void importAssignments(TaskManager importedTaskManager, HumanResourceManager hrManager,
      Map<Task, Task> original2importedTask, Map<HumanResource, HumanResource> original2importedResource) {
    Task[] tasks = importedTaskManager.getTasks();
    for (int i = 0; i < tasks.length; i++) {
      ResourceAssignment[] assignments = tasks[i].getAssignments();
      for (int j = 0; j < assignments.length; j++) {
        Task task = getTask(original2importedTask.get(tasks[i]).getTaskID());
        ResourceAssignment assignment = task.getAssignmentCollection().addAssignment(
            original2importedResource.get(assignments[j].getResource()));
        assignment.setLoad(assignments[j].getLoad());
        assignment.setCoordinator(assignments[j].isCoordinator());
      }
    }
  }

  void onTaskMoved(TaskImpl task) {
    if (!isRegistered(task)) {
      registerTask(task);
    }
    myDependencyGraph.move(task, getTaskHierarchy().getContainer(task));
    myTaskMap.setDirty();
  }

  public void setEventsEnabled(boolean enabled) {
    areEventsEnabled = enabled;
  }

  boolean areEventsEnabled() {
    return areEventsEnabled;
  }

  @Override
  public CustomPropertyManager getCustomPropertyManager() {
    return myCustomColumnsManager;
  }

  public URL getProjectDocument() {
    return myConfig.getProjectDocumentURL();
  }

  private static class TaskNamePrefixOption extends DefaultStringOption implements GP1XOptionConverter {
    public TaskNamePrefixOption() {
      super("taskNamePrefix");
      resetValue(GanttLanguage.getInstance().getText("defaultTaskPrefix"), true);
    }

    @Override
    public String getTagName() {
      return "task-name";
    }

    @Override
    public String getAttributeName() {
      return "prefix";
    }

    @Override
    public void loadValue(String legacyValue) {
      resetValue(legacyValue, true);
    }
  }

  @Override
  public StringOption getTaskNamePrefixOption() {
    return myTaskNamePrefixOption;
  }

  @Override
  public StringOption getTaskCopyNamePrefixOption() {
    return myTaskCopyNamePrefixOption;
  }

  @Override
  public EnumerationOption getDependencyHardnessOption() {
    return myDependencyHardnessOption;
  }

  @Override
  public void setZeroMilestones(Boolean b) {
    isZeroMilestones = b;
    if (Boolean.TRUE == isZeroMilestones) {
      List<Task> milestones = Lists.newArrayList();
      for (Task t : getTasks()) {
        if (t.isMilestone()) {
          t.setMilestone(true);
          milestones.add(t);
        }
      }
      getAlgorithmCollection().getAdjustTaskBoundsAlgorithm().run(milestones);
      try {
        getAlgorithmCollection().getRecalculateTaskScheduleAlgorithm().run(milestones);
      } catch (TaskDependencyException e) {
        GPLogger.log(e);
      }
    }
  }

  @Override
  public Boolean isZeroMilestones() {
    return isZeroMilestones;
  }

  @Override
  public DependencyGraph getDependencyGraph() {
    return myDependencyGraph;
  }
}
