@Override
 public void setShowExecutionPath(boolean showTraceTail) {
   this.showExecutionPath = showTraceTail;
   Configuration.getInstance().setExecutionPath(showTraceTail);
   if (isDynamicsLoaded() && currentIndex != UNSELECTED) {
     if (showTraceTail) {
       // add all trace elements until the current one exclusively
       addTraceElementsByIndex(0, currentIndex - 1);
     } else {
       // remove all trace elements except the current one
       removeTraceElementsByIndex(0, currentIndex - 1);
     }
   }
 }
 @Override
 public void setReplayEnabled(boolean replayEnabled) {
   this.replayEnabled = replayEnabled;
   Configuration.getInstance().setReplay(replayEnabled);
 }
 @Override
 public void setMovieIntervall(long intervalInMs) {
   this.movieInterval = intervalInMs;
   Configuration.getInstance().setMovieInterval(intervalInMs);
 }
 @Override
 public void setPlayMode(PlayMode mode) {
   this.playMode = mode;
   Configuration.getInstance().setPlayMode(playMode);
 }
 @Override
 public void setTimeUnit(long timeInNs) {
   checkTimeInNsParameter(timeInNs);
   this.currentTimeUnitInNs = timeInNs;
   Configuration.getInstance().setTimeUnit(timeInNs);
 }
public class TraceController extends AbstractTraceController {

  private static final java.util.logging.Logger logger =
      java.util.logging.Logger.getLogger(TraceController.class.getName());
  private static final int UNSELECTED = -1;

  /** Static inner class. Synchronously instantiated by first access. */
  private static final class InstanceHolder {
    static final TraceController INSTANCE = new TraceController();
  }

  /** Private constructor. */
  private TraceController() {}

  /** Returns the only instance of this class. */
  public static TraceController getInstance() {
    return InstanceHolder.INSTANCE;
  }

  // TODO remove this comparator because the sub classes of ITraceEvent are
  // already comparable
  private class ComparatorTin implements Comparator<ITraceElement> {
    @Override
    public int compare(ITraceElement o1, ITraceElement o2) {
      return new Long(o1.getTin()).compareTo(o2.getTin());
    }
  }

  /** the comparison key used by binary search */
  //	private DClassInstance key;
  private final Comparator<? super ITraceElement> compaTin = new ComparatorTin();

  /** all trace elements sorted by <code>tin</code> in ascending order. */
  private List<ITraceElement> LINE_TIME;

  private IFeatureModel featureModel;
  private ISourceModel sourceModel;
  private ISceneController sceneCtrl;

  private List<ITraceElement> traceElements;
  private TraceNavigation navStatus;
  private PlayMode playMode = Configuration.getInstance().getPlayMode();
  /** the next task is executed every each <code>movie interval</code> (in ms) */
  private long movieInterval = Configuration.getInstance().getMovieInterval();

  private long currentTraceId;
  /**
   * Indicates the current trace element.<br>
   * <i>(Range: -1 to number of trace elements -1)</i>
   */
  private int currentIndex;

  private long currentTimeUnitInNs = Configuration.getInstance().getTimeUnit();
  private long currentPointInTime;

  private Timer timer;
  private TimerTask taskRealBack, taskRealForward;
  private TimerTask taskEventBack, taskEventForward;

  private volatile boolean replayEnabled = Configuration.getInstance().isReplay();
  private volatile boolean showExecutionPath = Configuration.getInstance().isExecutionPath();

  public void init(
      IFeatureModel featureModel, SourceModel sourceModel, ISceneController sceneCtrl) {
    // TODO move logger properties into file
    // logger.setLevel(Level.OFF);
    this.featureModel = featureModel;
    this.sceneCtrl = sceneCtrl;
    this.sourceModel = sourceModel;
    // createTimerTasks();
    sceneCtrl.addListener(this);
    //		this.key = DynamicsFactoryImpl.eINSTANCE.createDClassInstance();
  }

  private void createTimerTasks() {
    taskRealBack =
        new TimerTask() {

          @Override
          public void run() {
            if (hasPreviousTime()) {
              backOneTimeUnit();
            } else if (replayEnabled) {
              jumpToPointInTime(getEndTin());
            } else {
              stopMovie();
            }
          }
        };
    taskRealForward =
        new TimerTask() {
          @Override
          public void run() {
            if (hasNextTime()) {
              forwardOneTimeUnit();
            } else if (replayEnabled) {
              jumpToPointInTime(0);
            } else {
              stopMovie();
            }
          }
        };
    taskEventBack =
        new TimerTask() {
          @Override
          public void run() {
            if (hasPreviousEvent()) {
              backOneEvent();
            } else if (replayEnabled) {
              jumpToPointInTime(getEndTin());
            } else {
              stopMovie();
            }
          }
        };
    taskEventForward =
        new TimerTask() {
          @Override
          public void run() {
            if (hasNextEvent()) {
              forwardOneEvent();
            } else if (replayEnabled) {
              jumpToPointInTime(0);
            } else {
              stopMovie();
            }
          }
        };
  }

  @Override
  public void setTraceNavigation(TraceNavigation traceNavigation) {
    if (navStatus == traceNavigation) {
      return;
    }
    this.navStatus = traceNavigation;

    if (isDynamicsLoaded()) {
      loadNavigationMode();
    }
  }

  @Override
  public synchronized void forwardOneEvent() {
    int tempIndex = currentIndex + 1;
    updateTime(traceElements.get(tempIndex).getTin());
    updateIndex(tempIndex);
    addTraceElementsByIndex(tempIndex, tempIndex);
    if (!showExecutionPath && currentIndex > 0) {
      // trace tail should be invisible, so remove last
      removeTraceElementsByIndex(currentIndex - 1, currentIndex - 1);
    }
  }

  @Override
  public synchronized void backOneEvent() {
    int tempIndex = currentIndex;
    if (0 == tempIndex) {
      updateTime(0);
    } else {
      updateTime(traceElements.get(tempIndex - 1).getTin());
    }
    updateIndex(tempIndex - 1);
    removeTraceElementsByIndex(tempIndex, tempIndex);
    if (!showExecutionPath && currentIndex != UNSELECTED) {
      // trace tail was removed, so add the last again
      addTraceElementsByIndex(currentIndex, currentIndex);
    }
  }

  @Override
  public synchronized void forwardOneTimeUnit() {
    long to = currentPointInTime + currentTimeUnitInNs;
    int oldIndex = currentIndex;
    //		updateTime(to);
    addTraceElementsByTime(oldIndex, to);
    if (!showExecutionPath && currentIndex != oldIndex) {
      if (oldIndex < 0) {
        oldIndex = 0;
      }
      removeTraceElementsByIndex(oldIndex, currentIndex - 1);
    }
  }

  @Override
  public synchronized void backOneTimeUnit() {
    final long from = currentPointInTime - currentTimeUnitInNs;
    final int oldIndex = currentIndex;
    //		updateTime(from);
    removeTraceElementsByTime(from, oldIndex);
    if (!showExecutionPath && currentIndex != oldIndex && currentIndex != UNSELECTED) {
      addTraceElementsByIndex(currentIndex, currentIndex);
    }
  }

  @Override
  public synchronized void jumpToEvent(long traceId, int eoi) {
    logger.info("JUMP eoi = " + eoi);
    int from_eoi;
    int to_eoi;
    int tempIndex = currentIndex;
    int index = retrieveIndexForEOI(eoi);

    updateTimeByIndex(index);
    updateIndex(index);

    if (tempIndex < index) {
      from_eoi = tempIndex + 1;
      to_eoi = index;
      addTraceElementsByIndex(from_eoi, to_eoi);
    } else {
      from_eoi = index + 1;
      to_eoi = tempIndex;
      removeTraceElementsByIndex(from_eoi, to_eoi);
    }
  }

  @Override
  public synchronized void jumpToPointInTime(long timeInNs) {
    checkTimeInNsParameter(timeInNs);

    if (timeInNs == currentPointInTime) {
      return;
    }

    long lastPiT = currentPointInTime;
    int oldIndex = currentIndex;

    //		updateIndex(UNSELECTED);
    //		updateTime(timeInNs);

    if (lastPiT < timeInNs) {
      addTraceElementsByTime(oldIndex, timeInNs);
    } else {
      removeTraceElementsByTime(timeInNs, oldIndex);
    }
  }

  @Override
  public long getTimeUnit() {
    return this.currentTimeUnitInNs;
  }

  @Override
  public void setTimeUnit(long timeInNs) {
    checkTimeInNsParameter(timeInNs);
    this.currentTimeUnitInNs = timeInNs;
    Configuration.getInstance().setTimeUnit(timeInNs);
  }

  @Override
  public void stopMovie() {
    timer.cancel();
    timer = null; // gc & set timer state to 'stopped'
    notifyListeners(ObsMovieStopped.class, null);
  }

  @Override
  public boolean hasNextEvent() {
    return (isDynamicsLoaded() && currentIndex < traceElements.size() - 1);
  }

  @Override
  public boolean hasNextTime() {
    return (isDynamicsLoaded() && currentPointInTime < getEndTin());
  }

  @Override
  public boolean isDynamicsLoaded() {
    return null != LINE_TIME;
  }

  @Override
  public boolean hasPreviousEvent() {
    return (isDynamicsLoaded() && currentIndex > UNSELECTED);
  }

  @Override
  public boolean hasPreviousTime() {
    return (isDynamicsLoaded() && getBeginTin() < currentPointInTime);
  }

  @Override
  public boolean isPlaying() {
    return (isTimerScheduling());
  }

  @Override
  public boolean canPlay() {
    return isDynamicsLoaded() && hasNextEvent();
  }

  // ----------------------------------------------------------------

  private long getBeginTin() {
    return traceElements.get(0).getTin();
  }

  private long getEndTin() {
    return traceElements.get(traceElements.size() - 1).getTin();
  }

  /**
   * Converts the given <code>eoi</code> to the corresponding index of the <code>traceElements
   * </code> list.
   *
   * @param eoi
   * @return
   */
  private int retrieveIndexForEOI(int eoi) {
    int index = -1;
    // TODO improve performance
    for (int i = 0; i < traceElements.size(); i++) {
      ITraceElement te = traceElements.get(i);
      if (te.getEoi() == eoi) {
        index = i;
        break;
      }
    }
    if (index < 0) {
      throw new IllegalArgumentException();
    }
    return index;
  }

  /**
   * Sets the <code>currentPointInTime</code> to the given <code>newTime</code> and keeps it within
   * the valid range (begin - end).
   *
   * @param newTime
   */
  private void updateTime(long newTime) {
    currentPointInTime = newTime;
    forceTimeRange();
    // logger.info("time = " + currentPointInTime);
  }

  private void updateTimeByIndex(int index) {
    long newTime = traceElements.get(index).getTin();
    updateTime(newTime);
  }

  /** Keeps the current point in time within the valid range (begin - end). */
  private void forceTimeRange() {
    if (currentPointInTime < 0) { // getBegin()
      currentPointInTime = getBeginTin();
    } else if (currentPointInTime > getEndTin()) {
      currentPointInTime = getEndTin();
    }
  }

  /**
   * Returns true, if the timer is active, i.e. scheduling; otherwise false.
   *
   * @return
   */
  private boolean isTimerScheduling() {
    return null != timer;
  }

  private void checkTimeInNsParameter(long timeInNs) {
    if (timeInNs < 0) {
      throw new IllegalArgumentException(
          "timeInNs may not be negative. Current value: " + timeInNs);
    }
  }

  /**
   * Adds all trace elements from <code>fromTime</code> to <code>toTime</code> (incl.).
   *
   * @param fromTime
   * @param toTime
   */
  private void addTraceElementsByTime(int fromIndex, long toTime) {
    // key.setTin(fromTime);
    // int index = Collections.binarySearch(traceElements, key, compaTin);
    // if (index < 0) {
    // index = -index - 1;
    // }

    //		updateIndex(UNSELECTED);
    updateTime(toTime);

    for (int i = fromIndex + 1; i < traceElements.size(); i++) {
      ITraceElement te = traceElements.get(i);
      if (te.getTin() > toTime) { // incl.
        break;
      }
      updateIndex(i);
      addTraceElement(te);
    }
  }

  /**
   * Removes all trace elements from <code>toTime</code> downto <code>fromTime</code> (excl.).
   *
   * @param fromTime
   * @param fromIndex
   */
  private void removeTraceElementsByTime(long fromTime, int fromIndex) {
    //		updateIndex(UNSELECTED);
    updateTime(fromTime);

    for (int removeIndex = fromIndex; removeIndex >= 0; removeIndex--) {
      ITraceElement te = traceElements.get(removeIndex);
      if (te.getTin() <= fromTime) { // excl.
        break;
      }
      updateIndex(removeIndex - 1);
      removeTraceElement(te);
    }
  }

  private synchronized void addTraceElementsByIndex(int fromIndex, int toIndex) {
    for (int i = fromIndex; i <= toIndex; i++) {
      ITraceElement te = traceElements.get(i);
      addTraceElement(te);
    }
  }

  private synchronized void removeTraceElementsByIndex(int fromIndex, int toIndex) {
    for (int i = toIndex; i >= fromIndex; i--) {
      ITraceElement te = traceElements.get(i);
      removeTraceElement(te);
    }
  }

  private synchronized void addTraceElement(ITraceElement te) {
    sourceModel.addTraceElement(te);
  }

  private synchronized void removeTraceElement(ITraceElement te) {
    sourceModel.removeTraceElement(te);
  }

  /**
   * Just sets the <code>currentIndex</code>.
   *
   * @param index
   */
  private void updateIndex(int index) {
    currentIndex = index;
    // if (null != traceElements) {
    // logger.info("index = " + currentIndex + " / " + (traceElements.size()
    // - 1));
    // }
  }

  private void loadNavigationMode() {
    switch (navStatus) {
      case GLOBAL:
        // use "current time" from local mode
        reloadSceneByTime(currentPointInTime);
        break;
      case LOCAL:
        // use "current index" from global mode
        reloadSceneByIndex(currentIndex);
        break;
      default:
        throw new UnhandledSwitchBranch(
            "Unknown element of enum 'TraceNavigation' (" + navStatus + ")");
    }

    // especially, if nothing is added to or removed from the scene, let
    // listeners update their buttons etc.
    notifyListeners(ObsNavigationModeChanged.class, null);
  }

  private void reloadSceneByTime(long pointInTime) {
    traceElements = LINE_TIME;

    resetScene();
    jumpToPointInTime(pointInTime);
  }

  private void reloadSceneByIndex(int index) {
    ITraceElement te = loadNextTraceElement(index);
    traceElements = featureModel.getAllTraceElements(te.getTraceId());
    selectTrace(te.getTraceId());

    resetScene();
    jumpToEvent(te.getTraceId(), te.getEoi());
  }

  private synchronized void resetScene() {
    sourceModel.resetScene();
    // reset values in order to not remove already removed elements
    currentIndex = UNSELECTED;
    currentPointInTime = 0;
    logger.info("RESET scene");
  }

  private ITraceElement loadNextTraceElement(int startIndex) {
    for (int index = startIndex; index >= 0; index--) {
      ITraceElement te = traceElements.get(index);
      if (te instanceof DMethodInstance) {
        return te;
      }
    }
    for (int index = startIndex + 1; index < traceElements.size(); index++) {
      ITraceElement te = traceElements.get(index);
      if (te instanceof DMethodInstance) {
        return te;
      }
    }

    throw new IllegalStateException("There is no trace that contains any method calls.");
  }

  @Override
  public PlayMode getPlayMode() {
    return playMode;
  }

  @Override
  public void setPlayMode(PlayMode mode) {
    this.playMode = mode;
    Configuration.getInstance().setPlayMode(playMode);
  }

  @Override
  public synchronized void playMovie() {
    if (isPlaying()) {
      throw new IllegalStateException("Movie is still being played.");
    }
    createTimerTasks();
    timer = new Timer();
    switch (playMode) {
      case EVENT_BACK:
        timer.schedule(taskEventBack, 0, movieInterval);
        break;
      case EVENT_FORWARD:
        timer.schedule(taskEventForward, 0, movieInterval);
        break;
      case TIME_BACK:
        timer.schedule(taskRealBack, 0, movieInterval);
        break;
      case TIME_FORWARD:
        timer.schedule(taskRealForward, 0, movieInterval);
        break;
      default:
        break;
    }
    notifyListeners(ObsMovieStarted.class, null);
  }

  @Override
  public void setMovieIntervall(long intervalInMs) {
    this.movieInterval = intervalInMs;
    Configuration.getInstance().setMovieInterval(intervalInMs);
  }

  @Override
  public long getMovieIntervall() {
    return movieInterval;
  }

  @Override
  public boolean isReplayEnabled() {
    return replayEnabled;
  }

  @Override
  public void setReplayEnabled(boolean replayEnabled) {
    this.replayEnabled = replayEnabled;
    Configuration.getInstance().setReplay(replayEnabled);
  }

  @Override
  public boolean isShowExecutionPath() {
    return showExecutionPath;
  }

  @Override
  public void setShowExecutionPath(boolean showTraceTail) {
    this.showExecutionPath = showTraceTail;
    Configuration.getInstance().setExecutionPath(showTraceTail);
    if (isDynamicsLoaded() && currentIndex != UNSELECTED) {
      if (showTraceTail) {
        // add all trace elements until the current one exclusively
        addTraceElementsByIndex(0, currentIndex - 1);
      } else {
        // remove all trace elements except the current one
        removeTraceElementsByIndex(0, currentIndex - 1);
      }
    }
  }

  @Override
  public int getCurrentIndex() {
    return currentIndex;
  }

  @Override
  public int getNumTraceElements() {
    return traceElements.size();
  }

  @Override
  public void selectTraceElement(long traceId, ITraceElement te) {
    switch (navStatus) {
      case GLOBAL:
        jumpToPointInTime(te.getTin());
        break;
      case LOCAL:
        if (currentTraceId != traceId) {
          selectTrace(traceId);
          resetScene();
        }
        if (te.getTraceId() != traceId) {
          jumpToEvent(traceId, 0);
          // jumpToPointInTime(0);
        } else {
          jumpToEvent(traceId, te.getEoi());
        }
        break;
      default:
        break;
    }
  }

  private void selectTrace(long traceId) {
    traceElements = featureModel.getAllTraceElements(traceId);
    if (null == traceElements) {
      throw new IllegalArgumentException("The trace (" + traceId + ") does not exist");
    }

    currentTraceId = traceId;
    logger.info("CHANGED trace to id = " + traceId);
  }

  private void printState() {
    logger.info("index = " + currentIndex);
    logger.info("time = " + currentPointInTime);
  }

  private void resetState() {
    updateIndex(UNSELECTED);
    currentPointInTime = 0;
    navStatus = TraceNavigation.GLOBAL;
  }

  @Override
  public List<Class<? extends IObserver>> getRegisterableObserverGroups() {
    List<Class<? extends IObserver>> list = new ArrayList<Class<? extends IObserver>>();
    list.add(ObsDynamicsLoaded.class);
    return list;
  }

  @Override
  public void update(Object caller, Class<? extends IObserver> clazz, Object arg) {
    resetState();

    LINE_TIME = featureModel.getTraceEvents();
    Collections.sort(LINE_TIME, compaTin);
    logger.info("Events sorted by tin. " + LINE_TIME.size());

    loadNavigationMode();
  }
}