GtdSpmFormat.java

package de.japrost.staproma.spm;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.japrost.staproma.TaskState;
import de.japrost.staproma.task.AnonymousTask;
import de.japrost.staproma.task.FolderTask;
import de.japrost.staproma.task.LeafTask;
import de.japrost.staproma.task.Task;

/**
 * <p>
 * SPM-file format with GTD notation.
 * </p>
 * <p>
 * This notation is devided in two parts: topics and actions. A topic represents a project in GTD (something that
 * requires more than one action to take place). An action represents one single step in a project. Typical for GTD are
 * the next steps.
 * </p>
 * <p>
 * Topics are all lines that start with one or more {@code #} followed by a SPACE. The remains of the line describes the
 * topic. The number of hashes describe a hierarchy of topics. Actions are all lines that start with a single {@code *}
 * followed by a SPACE, followed by the action symbol in braces followed by a SPACE. The remains of the line describes
 * the action. All other lines are supposed to be task content.
 * </p>
 * <em>Action symbols</em>
 * <p>
 * The following action symbols are supported:
 * <table>
 * <caption>Supported symbols</caption>
 * <tr>
 * <th>Symbol</th>
 * <th>Meaning</th>
 * <th>State</th>
 * <th>Remark</th>
 * </tr>
 * <tr>
 * <td>{@code !}</td>
 * <td>Something to do (a next step).</td>
 * <td>{@code CURRENT}</td>
 * <td>This next step has priority 1. {@code '!'} is equivalent to {@code '1')}.</td>
 * </tr>
 * <tr>
 * <td>{@code (n)}</td>
 * <td>Something to do (a next step) with the priority {@code n}.. {@code (1)} and {@code (!)} are equivalent.</td>
 * <td>{@code CURRENT}</td>
 * <td>{@code 'n'} ranges from 1 to 9. 0 means unknown.<br> {@code '1'} is equivalent to {@code '!')}.</td>
 * </tr>
 * <tr>
 * <td>{@code @}</td>
 * <td>Something on a schedule.</td>
 * <td>{@code SCHEDULE}</td>
 * </tr>
 * <tr>
 * <td>{@code /}</td>
 * <td>Something that is done already.</td>
 * <td>{@code DONE}</td>
 * </tr>
 * <tr>
 * <td>{@code ?}</td>
 * <td>Something maybe/somtime (future project).</td>
 * <td>{@code FUTURE}</td>
 * </tr>
 * <tr>
 * <td>{@code #}</td>
 * <td>Something to do in future (a future next step).</td>
 * <td>{@code FUTURE}</td>
 * </tr>
 * <tr>
 * <td>{@code :}</td>
 * <td>Something to wait for someone other fullfills it.</td>
 * <td>{@code WAITING}</td>
 * </tr>
 * </table>
 * All other lines starting with an asteriks and a SPACE will be used as {@code CURRENT} tasks to avoid that typos
 * result in missing tasks. Example:
 * 
 * <pre>
 * {@code
 * # Topic on level one
 * * (!) a next step
 * * (4) a next step with priority 4
 * * (/) something that is done
 * * (#) something to to in some future
 * ## Topic on level two (subtopic)
 * * (:) waiting for someone
 * * (?) maybe do this some time?
 * ** This is some content. Maybe describing the action before
 * This is another content for the maybe action
 * * (@) This action is scheduled
 * # Second topic
 * * The missing symbol will create a next step
 * }
 * </pre>
 * 
 * @author alexxismachine (Ulrich David)
 */
public class GtdSpmFormat implements SpmFormat {
	/**
	 * Pattern to match against "topics".
	 */
	private static final Pattern TOPIC_PATTERN = Pattern.compile("(#*) (.*)");
	/**
	 * Pattern to match against "actions" to take place. The {@code \f} is an additional current task (like a unknown
	 * symbol) for unit testing.
	 */
	private static final Pattern ACTION_PATTERN = Pattern.compile("\\* \\(([!/?#:@\\d\\f])\\) (.*)");

	/**
	 * Parse the lines into a new root task.
	 * 
	 * @param lines
	 *            the lines to parse
	 * @return the root task containing the converted lines.
	 */
	public Task parseLines(List<String> lines) {
		FolderTask rootTask = new FolderTask(null, "Root)");
		parseLines(rootTask, lines);
		return rootTask;
	}

	/**
	 * Parse the lines into the given root task.
	 * 
	 * @param rootTask
	 *            the root task to add the lines to.
	 * @param lines
	 *            the lines to parse.
	 */
	// TODO ? try this with a parser framework (ANTLR?)
	public void parseLines(Task rootTask, List<String> lines) {
		int currentL = 0;
		Task currentT = rootTask;
		Task contentT = rootTask;
		for (String line : lines) {
			Matcher topicMatcher = TOPIC_PATTERN.matcher(line);
			if (topicMatcher.matches()) {
				int level = (topicMatcher.group(1).length());
				System.out.println("#> Going for " + topicMatcher.group(2) + " on " + level);
				if (currentL == level) {
					//System.out.println(" * Same");
					Task addTo = currentT.getParent();
					//System.out.println(" * Adding to " + addTo.getDescription());
					FolderTask task = new FolderTask(addTo, topicMatcher.group(2));
					// task.setState(status); 
					addTo.addChild(task);
					currentT = task;
					contentT = task;
				}
				if (currentL > level) {
					//System.out.println(" * Parent (" + (level - currentL) + ")");
					Task addTo = currentT.getParent().getParent();
					for (int l = 1; l < (currentL - level); l++) {
						addTo = addTo.getParent();
					}
					//System.out.println(" * Adding to " + addTo.getDescription());
					FolderTask task = new FolderTask(addTo, topicMatcher.group(2));
					// task.setState(status);
					addTo.addChild(task);
					currentT = task;
					contentT = task;
				}
				if (currentL < level) {
					//System.out.println(" * Sub (" + (level - currentL) + ")");
					Task addTo = currentT;
					for (int l = 1; l < (level - currentL); l++) {
						AnonymousTask task = new AnonymousTask(addTo);
						// task.setState(status);
						addTo.addChild(task);
						addTo = task;
					}
					//System.out.println(" * Adding to " + addTo.getDescription());
					FolderTask task = new FolderTask(addTo, topicMatcher.group(2));
					//task.setState(status);
					addTo.addChild(task);
					currentT = task;
					contentT = task;
				}
				currentL = level;
				continue; // replace with else later on?
			}
			Matcher stepMatcher = ACTION_PATTERN.matcher(line);
			if (stepMatcher.matches()) {
				System.out.println("*> Going for '" + line.substring(2) + "' with '" + stepMatcher.group(1) + "' on "
						+ currentL);
				String symbol = stepMatcher.group(1);
				TaskState state = null;
				short priority = 0;
				if ("!".equals(symbol)) {
					state = TaskState.CURRENT;
					priority=1;
				} else if ("@".equals(symbol)) {
					state = TaskState.SCHEDULE;
				} else if ("/".equals(symbol)) {
					state = TaskState.DONE;
				} else if ("?".equals(symbol)) {
					state = TaskState.SOMEDAY;
				} else if ("#".equals(symbol)) {
					state = TaskState.FUTURE;
				} else if (":".equals(symbol)) {
					state = TaskState.WAITING;
				} else if ((priority =  parseNumber(symbol))!=0) {
					state = TaskState.CURRENT;
				} else {
					// match but unknown symbol? Can only happen on changed action pattern!
					state = null;
				}
				LeafTask task;
				if (state == null) {
					// No symbol found. Should not happen!
					task = new LeafTask(currentT, line.substring(2));
					task.setState(TaskState.CURRENT);
				} else {
					task = new LeafTask(currentT, stepMatcher.group(2));
					task.setState(state);
					task.setPriority(priority);
				}
				//System.out.println(" * *");
				Task addTo = currentT;
				//System.out.println(" * Adding to " + addTo.getDescription());
				addTo.addChild(task);
				contentT = task;
				continue; // replace with else later on?
			}
			if (line.startsWith("* ")) {
				System.out.println("-> Going for " + line.substring(2) + " on " + currentL);
				//System.out.println(" * *");
				Task addTo = currentT;
				//System.out.println(" * Adding to " + addTo.getDescription());
				LeafTask task = new LeafTask(currentT, line.substring(2));
				task.setState(TaskState.CURRENT);
				addTo.addChild(task);
				contentT = task;
				continue; // replace with else later on?
			}
			// no match -> must be content
			// FIXME do not add content to root task!
			System.out.println(" > Going for " + line + " on " + currentL);
			contentT.addContent(line);
			//System.out.println("<- Current (" + currentL + ") now " + currentT.getDescription());
		}
	}
	
	private short parseNumber(String symbol) {
		short result = 0;
		try {
			result = Short.parseShort(symbol);
		} catch (NumberFormatException e){
			// nothing to do
		}
		return result;
	}
	/* FIXME do formating 
		public List<String> formatTasks(Task task) {
			List<String> lines = new ArrayList<String>();
			doPrintSubTree(task, 0, lines);
			return lines;
		}

		public void renderTasks(Task task, Writer writer) throws IOException {
			renderTasks(task, writer, null);
		}

		public void renderTasks(Task task, Writer writer, String status) throws IOException {
			// FIXME this should render the tasks into their natural representation.
			doRenderSubTree(task, 0, writer, status);
		}

		private List<String> doPrintSubTree(Task task, int level, List<String> lines) {
			int myLevel = level + 1;
			for (Task subTask : task) {
				if (subTask instanceof AnonymousTask) {
					// skip anonymous on output
				} else {
					String line = "";
					if (subTask instanceof FolderTask) {
						for (int i = 0; i < myLevel; i++) {
							//System.out.print("#");
							line = line + "#";
						}
					} else {
						//System.out.print("*");
						line = line + "*";
					}
					line = line + " " + subTask.getDescription();
					//System.out.println(line);
					lines.add(line);
					lines.addAll(subTask.getContent());
				}
				//System.out.println(" " + subTask.getDescription() + " '" + myLevel + "'");
				//System.out.println("C " + subTask.getContent());
				doPrintSubTree(subTask, myLevel, lines);
			}
			return lines;
		}

		private void doRenderSubTree(Task task, int level, Writer writer, String status) throws IOException {
			System.out.println(level + "-> Render for " + task.getDescription() + " in state " + status);
			int myLevel = level + 1;
			if (task.isInState(status)) {
				System.out.println(level + "+> Render for " + task.getDescription() + " in state " + status);
				System.out.println("   write " + task.getDescription() + "\n");
				task.render(writer, level);
				for (Task subTask : task) {
					doRenderSubTree(subTask, myLevel, writer, status);
				}
			} else {
				System.out.println(level + "<- Render for " + task.getDescription());
			}
		}
		*/
}