/*******************************************************************************
 * Copyright (c) 2013 SAP AG.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Tobias Pfeifer (SAP AG) - initial implementation
 *******************************************************************************/
package org.eclipse.egit.core.internal.rebase;

import java.io.File;
import java.io.IOException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.internal.CoreText;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffChangedListener;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffData;
import org.eclipse.jgit.errors.IllegalTodoFileModification;
import org.eclipse.jgit.events.ListenerHandle;
import org.eclipse.jgit.events.RefsChangedEvent;
import org.eclipse.jgit.events.RefsChangedListener;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RebaseTodoFile;
import org.eclipse.jgit.lib.RebaseTodoLine;
import org.eclipse.jgit.lib.RebaseTodoLine.Action;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.GitDateFormatter;

/**
 * Representation of the {@link RebaseTodoFile} for Rebase-Todo and
 * Rebase-Done-File of a {@link Repository}.
 *
 * Reparses the rebase plan when the index changes or when a {@code Ref} is
 * moving in order to keep the in-memory plan in sync with the one on disk.
 */
public class RebaseInteractivePlan implements IndexDiffChangedListener,
		RefsChangedListener {

	/**
	 * Classes that implement this interface provide methods that deal with
	 * changes made to a {@link RebaseInteractivePlan}
	 * <p>
	 * An Instance that implements this interface it can be added to a
	 * {@link RebaseInteractivePlan} by using the
	 * {@link RebaseInteractivePlan#addRebaseInteractivePlanChangeListener(RebaseInteractivePlanChangeListener)
	 * addRebaseInteractivePlanChangeListener} method and removed using the
	 * {@link RebaseInteractivePlan#removeRebaseInteractivePlanChangeListener(RebaseInteractivePlanChangeListener)
	 * removeRebaseInteractivePlanChangeListener} method. When a change is made
	 * to an {@link PlanElement} in this {@link RebaseInteractivePlan} (including
	 * structural changes) the appropriate method will be invoked.
	 */
	public static interface RebaseInteractivePlanChangeListener {
		/**
		 * Will be invoked if the order of either the Rebase-Todo-File or
		 * Rebase-Done-File changes (structural change).
		 *
		 * @param rebaseInteractivePlan
		 * @param element
		 * @param oldIndex
		 * @param newIndex
		 */
		public void planElementsOrderChanged(
				RebaseInteractivePlan rebaseInteractivePlan, PlanElement element,
				int oldIndex, int newIndex);

		/**
		 * Will be invoked if the {@link ElementType} of an {@link PlanElement}
		 * changes.
		 *
		 * @param rebaseInteractivePlan
		 * @param element
		 * @param oldType
		 * @param newType
		 */
		public void planElementTypeChanged(
				RebaseInteractivePlan rebaseInteractivePlan, PlanElement element,
				ElementAction oldType, ElementAction newType);

		/**
		 * Will be invoked after the list of {@link PlanElement Elements} has been
		 * parsed from the {@link Repository}
		 *
		 * @param plan
		 */
		public void planWasUpdatedFromRepository(RebaseInteractivePlan plan);
	}

	private ArrayList<RebaseInteractivePlanChangeListener> planChangeListeners = new ArrayList<RebaseInteractivePlanChangeListener>();

	private List<PlanElement> todoList;

	private List<PlanElement> doneList;

	private JoinedList<List<PlanElement>, PlanElement> planList;

	private final Repository repository;

	private ListenerHandle refsChangedListener;

	private static final Map<File, RebaseInteractivePlan> planRegistry = new HashMap<File, RebaseInteractivePlan>();

	private static final String REBASE_TODO = "rebase-merge/git-rebase-todo"; //$NON-NLS-1$

	private static final String REBASE_DONE = "rebase-merge/done"; //$NON-NLS-1$

	/**
	 * Provides a singleton instance of {@link RebaseInteractivePlan} for a
	 * given {@link Repository}
	 * <p>
	 * If a {@link RebaseInteractivePlan} for the given {@link Repository} has
	 * already been created and has not been disposed yet, this instance is
	 * returned, otherwise a newly created instance is returned.
	 *
	 * @param repo
	 * @return the {@link RebaseInteractivePlan} for the given
	 *         {@link Repository}
	 */
	public static RebaseInteractivePlan getPlan(Repository repo) {
		RebaseInteractivePlan plan = planRegistry.get(repo.getDirectory());
		if (plan == null) {
			plan = new RebaseInteractivePlan(repo);
			planRegistry.put(repo.getDirectory(), plan);
		}
		return plan;
	}

	private RebaseInteractivePlan(Repository repo) {
		this.repository = repo;
		reparsePlan();
		registerIndexDiffChangeListener();
		registerRefChangedListener();
	}

	private void registerIndexDiffChangeListener() {
		IndexDiffCacheEntry entry = org.eclipse.egit.core.Activator
				.getDefault().getIndexDiffCache()
				.getIndexDiffCacheEntry(this.repository);

		entry.addIndexDiffChangedListener(this);
	}

	private void unregisterIndexDiffChangeListener() {
		IndexDiffCacheEntry entry = org.eclipse.egit.core.Activator
				.getDefault().getIndexDiffCache()
				.getIndexDiffCacheEntry(this.repository);

		entry.removeIndexDiffChangedListener(this);
	}

	private void registerRefChangedListener() {
		refsChangedListener = Repository.getGlobalListenerList()
				.addRefsChangedListener(this);
	}

	/**
	 * Reparse plan when {@code IndexDiff} changed
	 */
	public void indexDiffChanged(Repository repo, IndexDiffData indexDiffData) {
		if (RebaseInteractivePlan.this.repository == repo)
			reparsePlan();
	}

	/**
	 * Reparse plan when a {@code Ref} changed
	 *
	 * @param event
	 */
	public void onRefsChanged(RefsChangedEvent event) {
		Repository repo = event.getRepository();
		if (this.repository == repo)
			reparsePlan();
	}

	/**
	 * Dispose the plan.
	 * <p>
	 * The next invocation of {@link RebaseInteractivePlan#getPlan(Repository)}
	 * will create a new {@link RebaseInteractivePlan} instance.
	 */
	public void dispose() {
		reparsePlan();
		notifyPlanWasUpdatedFromRepository();
		planRegistry.remove(this.repository.getDirectory());
		planList.clear();
		planChangeListeners.clear();
		unregisterIndexDiffChangeListener();
		refsChangedListener.remove();
	}

	/**
	 * @return a list representation of the {@link PlanElement Elements} for this
	 *         plan.
	 */
	public List<PlanElement> getList() {
		return planList;
	}

	/**
	 * @return the repository
	 */
	public Repository getRepository() {
		return repository;
	}

	/**
	 * Adds a {@link RebaseInteractivePlanChangeListener} to this
	 * {@link RebaseInteractivePlan} if it has not been registered yet
	 *
	 * @param listener
	 *            the {@link RebaseInteractivePlanChangeListener} to be added
	 * @return true if the listener has been added, otherwise false
	 */
	public boolean addRebaseInteractivePlanChangeListener(
			RebaseInteractivePlanChangeListener listener) {
		if (planChangeListeners.contains(listener))
			return false;
		return planChangeListeners.add(listener);
	}

	/**
	 * Removes a {@link RebaseInteractivePlanChangeListener} from this
	 * {@link RebaseInteractivePlan} if it has been registered before
	 *
	 * @param listener
	 *            the {@link RebaseInteractivePlanChangeListener} to be removed
	 * @return true if the listener has been removed, otherwise false
	 */
	public boolean removeRebaseInteractivePlanChangeListener(
			RebaseInteractivePlanChangeListener listener) {
		return planChangeListeners.remove(listener);
	}

	private void notifyPlanElementsOrderChange(PlanElement element, int oldIndex,
			int newIndex) {
		persist();
		for (RebaseInteractivePlanChangeListener listener : planChangeListeners)
			listener.planElementsOrderChanged(this, element, oldIndex, newIndex);
	}

	private void notifyPlanElementActionChange(PlanElement element,
			ElementAction oldType, ElementAction newType) {
		persist();
		for (RebaseInteractivePlanChangeListener listener : planChangeListeners)
			listener.planElementTypeChanged(this, element, oldType, newType);
	}

	private void notifyPlanWasUpdatedFromRepository() {
		for (RebaseInteractivePlanChangeListener listener : planChangeListeners)
			listener.planWasUpdatedFromRepository(this);
	}

	private void reparsePlan() {
		RevWalk walk = new RevWalk(repository.newObjectReader());
		try {
			doneList = parseDone(walk);
			todoList = parseTodo(walk);
		} finally {
			walk.release();
		}
		planList = JoinedList.wrap(doneList, todoList);
		notifyPlanWasUpdatedFromRepository();
	}

	private List<PlanElement> parseTodo(RevWalk walk) {
		List<RebaseTodoLine> rebaseTodoLines;
		try {
			rebaseTodoLines = repository.readRebaseTodo(REBASE_TODO, true);
		} catch (IOException e) {
			rebaseTodoLines = new LinkedList<RebaseTodoLine>();
		}
		List<PlanElement> todoElements = createElementList(rebaseTodoLines,
				walk);
		return todoElements;
	}

	private List<PlanElement> parseDone(RevWalk walk) {
		List<RebaseTodoLine> rebaseDoneLines;
		try {
			rebaseDoneLines = repository.readRebaseTodo(REBASE_DONE, false);
		} catch (IOException e) {
			rebaseDoneLines = new LinkedList<RebaseTodoLine>();
		}
		List<PlanElement> doneElements = createElementList(rebaseDoneLines,
				walk);
		return doneElements;
	}

	private List<PlanElement> createElementList(
			List<RebaseTodoLine> rebaseTodoLines, RevWalk walk) {
		List<PlanElement> planElements = new ArrayList<PlanElement>(
				rebaseTodoLines.size());
		for (RebaseTodoLine todoLine : rebaseTodoLines) {
			PlanElement element = createElement(todoLine, walk);
			planElements.add(element);
		}
		return planElements;
	}

	private PlanElement createElement(RebaseTodoLine todoLine, RevWalk walk) {
		PersonIdent author = null;
		PersonIdent committer = null;

		RevCommit commit = loadCommit(todoLine.getCommit(), walk);
		if (commit != null) {
			author = commit.getAuthorIdent();
			committer = commit.getCommitterIdent();
		}

		PlanElement element = new PlanElement(todoLine, author, committer);
		return element;
	}

	private RevCommit loadCommit(AbbreviatedObjectId abbreviatedObjectId,
			RevWalk walk) {
		if (abbreviatedObjectId != null) {
			try {
				Collection<ObjectId> resolved = walk.getObjectReader().resolve(
						abbreviatedObjectId);
				if (resolved.size() == 1) {
					RevCommit commit = walk.parseCommit(resolved
							.iterator().next());
					return commit;
				}
			} catch (IOException e) {
				// ignore, we assume no author/committer then
			}
		}
		return null;
	}

	/**
	 * @return true if the rebase has already been started processing the plan,
	 *         otherwise false
	 */
	public boolean hasRebaseBeenStartedYet() {
		return isRebasingInteractive() && doneList.size() > 0;
	}

	/**
	 * @return true if repository state is
	 *         {@link RepositoryState#REBASING_INTERACTIVE}
	 */
	public boolean isRebasingInteractive() {
		return repository.getRepositoryState() == RepositoryState.REBASING_INTERACTIVE;
	}

	/**
	 * Moves an {@link PlanElement} of Type {@link ElementType#TODO} down if
	 * possible
	 *
	 * @param element
	 *            the {@link PlanElement} to move down
	 */
	public void moveTodoEntryDown(PlanElement element) {
		new MoveHelper(todoList, this).moveTodoEntryDown(element);
	}

	/**
	 * Moves an {@link PlanElement} of Type {@link ElementType#TODO} up if possible
	 *
	 * @param element
	 *            the {@link PlanElement} to move up
	 */
	public void moveTodoEntryUp(PlanElement element) {
		new MoveHelper(todoList, this).moveTodoEntryUp(element);
	}

	/**
	 * Moves a given {@link PlanElement sourceElement} of Type
	 * {@link ElementType#TODO} to the current position of a {@link PlanElement
	 * targetElement} in it's list representation (considering that this list
	 * representation may be reversed). If <code>before</code> is true the
	 * {@link PlanElement sourceElement} will be placed just before the
	 * {@link PlanElement targetElement}
	 *
	 * @param sourceElement
	 * @param targetElement
	 * @param before
	 */
	public void moveTodoEntry(PlanElement sourceElement, PlanElement targetElement,
			boolean before) {
		new MoveHelper(todoList, this).moveTodoEntry(sourceElement,
				targetElement, before);
	}

	/**
	 * Writes the plan to the FS.
	 * <p>
	 * Only {@link PlanElement Elements} of {@link ElementType#TODO} are
	 * persisted.
	 *
	 * @return true if the todo file has been written successfully, otherwise
	 *         false
	 */
	public boolean persist() {
		if (!isRebasingInteractive())
			return false;
		List<RebaseTodoLine> todoLines = new LinkedList<RebaseTodoLine>();
		for (PlanElement element : planList.getSecondList())
			todoLines.add(element.getRebaseTodoLine());
		try {
			repository.writeRebaseTodoFile(REBASE_TODO, todoLines, false);
		} catch (IOException e) {
			Activator.logError(CoreText.RebaseInteractivePlan_WriteRebaseTodoFailed, e);
			throw new RuntimeException(e);
		}
		return true;
	}

	/**
	 * Parses the plan from the FS by reading the todo-File and the done-File if
	 * in state RebaseInteractive
	 *
	 * @throws IOException
	 */
	public void parse() throws IOException {
		if (!isRebasingInteractive())
			return;
		reparsePlan();
	}

	/**
	 * This class wraps a {@link RebaseTodoLine} and holds additional
	 * information about the underlying commit, if available.
	 */
	public class PlanElement {
		private final RebaseTodoLine line;

		/** author info, may be null */
		private final PersonIdent author;

		/** committer info, may be null */
		private final PersonIdent committer;

		private PlanElement(RebaseTodoLine line, PersonIdent author,
				PersonIdent committer) {
			if (line == null)
				throw new IllegalArgumentException();
			this.line = line;
			this.author = author;
			this.committer = committer;
		}

		/**
		 * @return the {@link ElementType} for this {@link PlanElement}
		 */
		public ElementType getElementType() {
			if (todoList.indexOf(this) != -1)
				return ElementType.TODO;
			int indexInDone = doneList.indexOf(this);
			if (indexInDone != -1) {
				if (indexInDone == doneList.size() - 1
						&& isRebasingInteractive())
					return ElementType.DONE_CURRENT;
				return ElementType.DONE;
			}
			return null;
		}

		private RebaseTodoLine getRebaseTodoLine() {
			return line;
		}

		/**
		 * @return the CommitId of the wrapped {@link RebaseTodoLine}
		 */
		public AbbreviatedObjectId getCommit() {
			return line.getCommit();
		}

		/**
		 * @return the shortMessage of the wrapped {@link RebaseTodoLine}
		 */
		public String getShortMessage() {
			return line.getShortMessage();
		}

		/**
		 * @return the author name of the underlying commit
		 */
		public String getAuthor() {
			if (author == null)
				return ""; //$NON-NLS-1$
			else
				return author.getName();
		}

		/**
		 * @param dateFormatter
		 * @return the authored date of the underlying commit
		 */
		public String getAuthoredDate(GitDateFormatter dateFormatter) {
			if (author == null)
				return ""; //$NON-NLS-1$
			else
				return dateFormatter.formatDate(author);
		}

		/**
		 * @return the committer name of the underlying commit
		 */
		public String getCommitter() {
			if (committer == null)
				return ""; //$NON-NLS-1$
			else
				return committer.getName();
		}

		/**
		 * @param dateFormatter
		 * @return the commit date of the underlying commit
		 */
		public String getCommittedDate(GitDateFormatter dateFormatter) {
			if (committer == null)
				return ""; //$NON-NLS-1$
			else
				return dateFormatter.formatDate(committer);
		}

		/**
		 * This method maps the given {@link ElementAction} to the wrapped
		 * {@link RebaseTodoLine RebaseTodoLines} {@link Action}. If the
		 * {@link ElementAction} changes the registered
		 * {@link RebaseInteractivePlanChangeListener
		 * RebaseInteractivePlanChangeListeners} are notified.
		 *
		 * @param newAction
		 *            the {@link ElementAction} to be set
		 */
		public void setPlanElementAction(ElementAction newAction) {
			if (isComment()) {
				if (newAction == null)
					return;
				throw new IllegalArgumentException();
			}
			ElementAction oldAction = this.getPlanElementAction();
			if (oldAction == newAction)
				return;
			try {
				switch (newAction) {
				case SKIP:
					line.setAction(Action.COMMENT);
					break;
				case EDIT:
					line.setAction(Action.EDIT);
					break;
				case FIXUP:
					line.setAction(Action.FIXUP);
					break;
				case PICK:
					line.setAction(Action.PICK);
					break;
				case REWORD:
					line.setAction(Action.REWORD);
					break;
				case SQUASH:
					line.setAction(Action.SQUASH);
					break;
				default:
					throw new IllegalArgumentException();
				}
			} catch (IllegalTodoFileModification e) {
				// shouldn't happen
				throw new IllegalArgumentException(e);
			}
			notifyPlanElementActionChange(this, oldAction, newAction);
		}

		/**
		 * @return the {@link ElementAction} for this {@link PlanElement}
		 */
		public ElementAction getPlanElementAction() {
			if (isSkip())
				return ElementAction.SKIP;
			if (isComment())
				return null;
			switch (line.getAction()) {
			case EDIT:
				return ElementAction.EDIT;
			case FIXUP:
				return ElementAction.FIXUP;
			case PICK:
				return ElementAction.PICK;
			case SQUASH:
				return ElementAction.SQUASH;
			case REWORD:
				return ElementAction.REWORD;
			default:
				throw new IllegalStateException();
			}

		}

		/**
		 * @return true, if the given line is a pure comment, i.e. a comment
		 *         that doesn't hold a valid action line, otherwise false
		 */
		public boolean isComment() {
			return (Action.COMMENT.equals(line.getAction()) && null == line
					.getCommit());
		}

		/**
		 * @return true if this element is marked for deletion, i.e. a valid
		 *         action line has been commented out, otherwise false
		 */
		public boolean isSkip() {
			return (Action.COMMENT.equals(line.getAction()) && null != line
					.getCommit());
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			PlanElement other = (PlanElement) obj;
			if (other.line.getCommit() == null) {
				if (this.line.getCommit() == null)
					return true;
				return false;
			}
			if (!other.line.getCommit().equals(this.line.getCommit()))
				return false;
			if (!other.getPlanElementAction().equals(
					this.getPlanElementAction()))
				return false;
			return true;
		}

		@Override
		public int hashCode() {
			return super.hashCode();
		}

		@Override
		public String toString() {
			return line.toString();
		}
	}

	/**
	 * Wraps {@link Action} and additionally provides
	 * {@link ElementAction#SKIP}
	 */
	public enum ElementAction {
		/**
		 * The {@link PlanElement} will not be cherry-picked, i.e. changes are
		 * lost on the new branch. Internally this is mapped to
		 * {@link Action#COMMENT}, to comment out a {@link RebaseTodoLine}
		 */
		SKIP,
		/**
		 * Equivalent to {@link Action#EDIT};
		 */
		EDIT,
		/**
		 * Equivalent to {@link Action#PICK};
		 */
		PICK,
		/**
		 * Equivalent to {@link Action#SQUASH};
		 */
		SQUASH,
		/**
		 * Equivalent to {@link Action#FIXUP};
		 */
		FIXUP,
		/**
		 * Equivalent to {@link Action#REWORD};
		 */
		REWORD;
	}

	/**
	 * The type of an {@link PlanElement}
	 */
	public static enum ElementType {
		/**
		 * The {@link PlanElement} is present in the done-File, i.e. the
		 * {@link RebaseTodoLine} has already been processed.
		 */
		DONE,
		/**
		 * The {@link PlanElement} is present in the todo-File, i.e. the
		 * {@link RebaseTodoLine} has not been processed yet.
		 */
		TODO,
		/**
		 * Special case of {@link ElementType#DONE}.
		 * <p>
		 * The {@link PlanElement} is the last entry in the done-File, i.e. the
		 * {@link RebaseTodoLine} has already been processed and furthermore the
		 * rebase has not been finished yet
		 */
		DONE_CURRENT;
	}

	/**
	 * Helper class for moving plan elements
	 */
	public static class MoveHelper {

		private List<PlanElement> todoList;

		private RebaseInteractivePlan plan;

		/**
		 * @param todoList
		 * @param plan
		 */
		public MoveHelper(List<PlanElement> todoList, RebaseInteractivePlan plan) {
			this.todoList = todoList;
			this.plan = plan;
		}

		/**
		 * Move the element at the given index in the given list up if possible,
		 * otherwise this method has no effect on the list. Moving an element up
		 * is not possible if the list does not contain an element with the
		 * given index or the element at the given index has no next element.
		 *
		 * @param index
		 * @param list
		 * @return true if an element has been moved up, otherwise false
		 */
		private static boolean moveUp(final int index, final List<?> list) {
			if (index <= 0 || index > list.size())
				return false;
			Collections.swap(list, index, index - 1);
			return true;
		}

		/**
		 * Move the element at the given index in the given list down if
		 * possible, otherwise this method has no effect on the list. Moving an
		 * element down is not possible if the list does not contain an element
		 * with the given index or the element at the given index has no
		 * previous element.
		 *
		 * @param index
		 * @param list
		 * @return true if an element has been moved down, otherwise false
		 */
		private static boolean moveDown(final int index, final List<?> list) {
			if (index < 0 || index >= list.size() - 1)
				return false;
			Collections.swap(list, index, index + 1);
			return true;
		}

		/**
		 * Moves an {@link PlanElement} of Type {@link ElementType#TODO} down if
		 * possible
		 *
		 * @param element
		 *            the {@link PlanElement} to move down
		 */
		public void moveTodoEntryDown(PlanElement element) {
			List<PlanElement> list = todoList;
			int initialIndex = list.indexOf(element);
			int oldIndex;
			do {
				oldIndex = list.indexOf(element);
				moveDown(oldIndex, list);
			} while (list.get(oldIndex).isComment());
			int newIndex = list.indexOf(element);
			if (initialIndex != newIndex)
				plan.notifyPlanElementsOrderChange(element, initialIndex,
						newIndex);
		}

		/**
		 * Moves an {@link PlanElement} of Type {@link ElementType#TODO} up if
		 * possible
		 *
		 * @param element
		 *            the {@link PlanElement} to move up
		 */
		public void moveTodoEntryUp(PlanElement element) {
			List<PlanElement> list = todoList;
			int initialIndex = list.indexOf(element);
			int oldIndex;
			do {
				oldIndex = list.indexOf(element);
				moveUp(oldIndex, list);
			} while (list.get(oldIndex).isComment());
			int newIndex = list.indexOf(element);
			if (initialIndex != newIndex)
				plan.notifyPlanElementsOrderChange(element, initialIndex,
						newIndex);
		}

		private static void move(int sourceIndex, int targetIndex,
				final List<PlanElement> list) {
			if (sourceIndex == targetIndex)
				return;
			if (sourceIndex < targetIndex) {
				Collections.rotate(list.subList(sourceIndex, targetIndex + 1),
						-1);
			} else {
				Collections.rotate(list.subList(targetIndex, sourceIndex + 1),
						1);
			}
		}

		/**
		 * Moves a given {@link PlanElement sourceElement} of Type
		 * {@link ElementType#TODO} to the current position of a
		 * {@link PlanElement targetElement} in it's list representation
		 * (considering that this list representation may be reversed). If
		 * <code>before</code> is true the {@link PlanElement sourceElement}
		 * will be placed just before the {@link PlanElement targetElement}
		 *
		 * @param sourceElement
		 * @param targetElement
		 * @param before
		 */
		public void moveTodoEntry(PlanElement sourceElement,
				PlanElement targetElement, boolean before) {
			if (sourceElement == targetElement)
				return;
			Assert.isNotNull(sourceElement);
			Assert.isNotNull(targetElement);
			if (ElementType.TODO != sourceElement.getElementType())
				throw new IllegalArgumentException();

			List<PlanElement> list = todoList;

			int initialSourceIndex = list.indexOf(sourceElement);
			int targetIndex = list.indexOf(targetElement);

			if (targetIndex == -1 || initialSourceIndex == -1)
				return;
			if (targetIndex == initialSourceIndex)
				return;

			if (targetIndex > initialSourceIndex && before)
				targetIndex--;
			if (targetIndex < initialSourceIndex && !before)
				targetIndex++;

			move(initialSourceIndex, targetIndex, list);
			int newIndex = list.indexOf(sourceElement);
			if (initialSourceIndex != newIndex)
				plan.notifyPlanElementsOrderChange(sourceElement,
						initialSourceIndex, newIndex);
		}
	}

	/**
	 * List that provides a view to two joined lists
	 *
	 * @param <L>
	 *            The concrete type of the two lists
	 * @param <T>
	 *            The type of the elements in the lists
	 */
	public static class JoinedList<L extends List<T>, T> extends
			AbstractList<T> {

		private final L firstList, secondList;

		/**
		 * @return the first list
		 */
		public L getFirstList() {
			return firstList;
		}

		/**
		 * @return the second list
		 */
		public L getSecondList() {
			return secondList;
		}

		/**
		 * @param first
		 * @param second
		 */
		private JoinedList(L first, L second) {
			super();
			Assert.isNotNull(first);
			Assert.isNotNull(second);
			this.firstList = first;
			this.secondList = second;
		}

		/**
		 * Creates a newly List that provides a view to two joined lists.
		 *
		 * @param first
		 * @param second
		 * @return a new view on a concatenation of both lists
		 */
		public static <L extends List<T>, T> JoinedList<L, T> wrap(L first,
				L second) {
			return new JoinedList<L, T>(first, second);
		}

		private static class RelativeIndex<T> {
			private final int relativeIndex;

			private final List<T> list;

			public final int getRelativeIndex() {
				return relativeIndex;
			}

			public final List<T> getList() {
				return list;
			}

			RelativeIndex(int relativeIndex, List<T> list) {
				super();
				this.relativeIndex = relativeIndex;
				this.list = list;
			}
		}

		private RelativeIndex<T> mapAbsolutIndex(int index) {
			if (index < firstList.size())
				return new RelativeIndex<T>(index, firstList);
			return new RelativeIndex<T>(index - firstList.size(), secondList);
		}

		/**
		 * if the given index is smaller than the first lists size the element
		 * is added to the first list. if the given index points to the seam of
		 * the joined lists, the given element will be added to the second list.
		 * More precisely a element is added to the second list if the given
		 * index is greater or equals the first lists size, otherwise it's added
		 * to the first list.
		 */
		@Override
		public void add(int index, T element) {
			RelativeIndex<T> rel = mapAbsolutIndex(index);
			rel.getList().add(rel.getRelativeIndex(), element);
			modCount++;
		}

		public T get(int index) {
			RelativeIndex<T> rel = mapAbsolutIndex(index);
			return rel.getList().get(rel.getRelativeIndex());
		}

		public T remove(int index) {
			RelativeIndex<T> rel = mapAbsolutIndex(index);
			modCount++;
			return rel.getList().remove(rel.getRelativeIndex());
		}

		public T set(int index, T element) {
			RelativeIndex<T> rel = mapAbsolutIndex(index);
			return rel.getList().set(rel.getRelativeIndex(), element);
		}

		public int size() {
			return firstList.size() + secondList.size();
		}
	}
}
