/*******************************************************************************
 * Copyright (c) 2006, 2012 Steffen Pingel and others.
 * 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:
 *     Steffen Pingel - initial API and implementation
 *     Benjamin Muskalla - bug 386920
 *******************************************************************************/

package org.eclipse.mylyn.internal.trac.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.internal.trac.core.client.ITracClient;
import org.eclipse.mylyn.internal.trac.core.client.InvalidTicketException;
import org.eclipse.mylyn.internal.trac.core.model.TracAction;
import org.eclipse.mylyn.internal.trac.core.model.TracAttachment;
import org.eclipse.mylyn.internal.trac.core.model.TracComment;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket.Key;
import org.eclipse.mylyn.internal.trac.core.model.TracTicketField;
import org.eclipse.mylyn.internal.trac.core.util.TracUtil;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITaskMapping;
import org.eclipse.mylyn.tasks.core.RepositoryResponse;
import org.eclipse.mylyn.tasks.core.RepositoryResponse.ResponseKind;
import org.eclipse.mylyn.tasks.core.RepositoryStatus;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
import org.eclipse.mylyn.tasks.core.data.TaskAttachmentMapper;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMetaData;
import org.eclipse.mylyn.tasks.core.data.TaskCommentMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.data.TaskOperation;
import org.eclipse.osgi.util.NLS;

/**
 * @author Steffen Pingel
 * @author Benjamin Muskalla
 */
public class TracTaskDataHandler extends AbstractTaskDataHandler {

	private static final String TASK_TYPE_TASK = "task"; //$NON-NLS-1$

	public static final String TASK_TYPE_STORY = "story"; //$NON-NLS-1$

	private static final String TASK_DATA_VERSION = "2"; //$NON-NLS-1$

	public static final String ATTRIBUTE_BLOCKED_BY = "blockedby"; //$NON-NLS-1$

	public static final String ATTRIBUTE_BLOCKING = "blocking"; //$NON-NLS-1$

	private static final String CC_DELIMETER = ", "; //$NON-NLS-1$

	private static final String TRAC_KEY = "tracKey"; //$NON-NLS-1$

	private final TracRepositoryConnector connector;

	public TracTaskDataHandler(TracRepositoryConnector connector) {
		this.connector = connector;
	}

	public TaskData getTaskData(TaskRepository repository, String taskId, IProgressMonitor monitor)
			throws CoreException {
		monitor = Policy.monitorFor(monitor);
		try {
			monitor.beginTask("Task Download", IProgressMonitor.UNKNOWN); //$NON-NLS-1$
			return downloadTaskData(repository, TracRepositoryConnector.getTicketId(taskId), monitor);
		} finally {
			monitor.done();
		}
	}

	public TaskData downloadTaskData(TaskRepository repository, int taskId, IProgressMonitor monitor)
			throws CoreException {
		ITracClient client = connector.getClientManager().getTracClient(repository);
		TracTicket ticket;
		try {
			client.updateAttributes(monitor, false);
			ticket = client.getTicket(taskId, monitor);
		} catch (OperationCanceledException e) {
			throw e;
		} catch (Exception e) {
			// TODO catch TracException
			throw new CoreException(TracCorePlugin.toStatus(e, repository));
		}
		return createTaskDataFromTicket(client, repository, ticket, monitor);
	}

	public TaskData createTaskDataFromTicket(ITracClient client, TaskRepository repository, TracTicket ticket,
			IProgressMonitor monitor) throws CoreException {
		TaskData taskData = new TaskData(getAttributeMapper(repository), TracCorePlugin.CONNECTOR_KIND,
				repository.getRepositoryUrl(), ticket.getId() + ""); //$NON-NLS-1$
		taskData.setVersion(TASK_DATA_VERSION);
		try {
			if (!TracRepositoryConnector.hasRichEditor(repository)) {
				createDefaultAttributes(taskData, client, true);
				Set<TaskAttribute> changedAttributes = updateTaskData(repository, taskData, ticket);
				// remove attributes that were not set, i.e. were not received from the server
				List<TaskAttribute> attributes = new ArrayList<TaskAttribute>(taskData.getRoot()
						.getAttributes()
						.values());
				for (TaskAttribute attribute : attributes) {
					if (!changedAttributes.contains(attribute) && !TracAttributeMapper.isInternalAttribute(attribute)) {
						taskData.getRoot().removeAttribute(attribute.getId());
					}
				}
				taskData.setPartial(true);
			} else {
				createDefaultAttributes(taskData, client, true);
				updateTaskData(repository, taskData, ticket);
			}
			removeEmptySingleSelectAttributes(taskData);
			return taskData;
		} catch (OperationCanceledException e) {
			throw e;
		} catch (Exception e) {
			// TODO catch TracException
			throw new CoreException(TracCorePlugin.toStatus(e, repository));
		}
	}

	private void removeEmptySingleSelectAttributes(TaskData taskData) {
		List<TaskAttribute> attributes = new ArrayList<TaskAttribute>(taskData.getRoot().getAttributes().values());
		for (TaskAttribute attribute : attributes) {
			if (TaskAttribute.TYPE_SINGLE_SELECT.equals(attribute.getMetaData().getType())
					&& attribute.getValue().length() == 0 && attribute.getOptions().isEmpty()) {
				taskData.getRoot().removeAttribute(attribute.getId());
			}
		}
	}

	public static Set<TaskAttribute> updateTaskData(TaskRepository repository, TaskData data, TracTicket ticket) {
		Set<TaskAttribute> changedAttributes = new HashSet<TaskAttribute>();

		Date lastChanged = ticket.getLastChanged();
		if (lastChanged != null) {
			TaskAttribute taskAttribute = data.getRoot().getAttribute(TracAttribute.CHANGE_TIME.getTracKey());
			taskAttribute.setValue(TracUtil.toTracTime(lastChanged) + ""); //$NON-NLS-1$
			changedAttributes.add(taskAttribute);
		}

		if (ticket.getCreated() != null) {
			TaskAttribute taskAttribute = data.getRoot().getAttribute(TracAttribute.TIME.getTracKey());
			taskAttribute.setValue(TracUtil.toTracTime(ticket.getCreated()) + ""); //$NON-NLS-1$
			changedAttributes.add(taskAttribute);
		}

		Map<String, String> valueByKey = ticket.getValues();
		for (String key : valueByKey.keySet()) {
			TaskAttribute taskAttribute = data.getRoot().getAttribute(key);
			if (taskAttribute != null) {
				if (Key.CC.getKey().equals(key)) {
					StringTokenizer t = new StringTokenizer(valueByKey.get(key), CC_DELIMETER);
					while (t.hasMoreTokens()) {
						taskAttribute.addValue(t.nextToken());
					}
				} else {
					taskAttribute.setValue(valueByKey.get(key));
				}
				changedAttributes.add(taskAttribute);
			} else {
				// TODO log missing attribute?
			}
		}

		TracComment[] comments = ticket.getComments();
		if (comments != null) {
			int count = 1;
			for (int i = 0; i < comments.length; i++) {
				if (!"comment".equals(comments[i].getField()) || "".equals(comments[i].getNewValue())) { //$NON-NLS-1$ //$NON-NLS-2$
					continue;
				}

				TaskCommentMapper mapper = new TaskCommentMapper();
				mapper.setAuthor(repository.createPerson(comments[i].getAuthor()));
				mapper.setCreationDate(comments[i].getCreated());
				mapper.setText(comments[i].getNewValue());
				// TODO mapper.setUrl();
				mapper.setNumber(count);

				TaskAttribute attribute = data.getRoot().createAttribute(TaskAttribute.PREFIX_COMMENT + count);
				mapper.applyTo(attribute);
				count++;
			}
		}

		TracAttachment[] attachments = ticket.getAttachments();
		if (attachments != null) {
			for (int i = 0; i < attachments.length; i++) {
				TaskAttachmentMapper mapper = new TaskAttachmentMapper();
				mapper.setAuthor(repository.createPerson(attachments[i].getAuthor()));
				mapper.setDescription(attachments[i].getDescription());
				mapper.setFileName(attachments[i].getFilename());
				mapper.setLength((long) attachments[i].getSize());
				if (attachments[i].getCreated() != null) {
					if (lastChanged == null || attachments[i].getCreated().after(lastChanged)) {
						lastChanged = attachments[i].getCreated();
					}
					mapper.setCreationDate(attachments[i].getCreated());
				}
				mapper.setUrl(repository.getRepositoryUrl() + ITracClient.TICKET_ATTACHMENT_URL + ticket.getId() + "/" //$NON-NLS-1$
						+ TracUtil.encodeUrl(attachments[i].getFilename()));
				mapper.setAttachmentId(i + ""); //$NON-NLS-1$

				TaskAttribute attribute = data.getRoot().createAttribute(TaskAttribute.PREFIX_ATTACHMENT + (i + 1));
				mapper.applyTo(attribute);
			}
		}

		TracAction[] actions = ticket.getActions();
		if (actions != null) {
			// add actions and set first as default
			for (TracAction action : actions) {
				addOperation(repository, data, ticket, action, action == actions[0]);
			}
		}

		return changedAttributes;
	}

	private static void addOperation(TaskRepository repository, TaskData data, TracTicket ticket, TracAction action,
			boolean setAsDefault) {
		String label = action.getLabel();
		if (label == null) {
			if ("leave".equals(action.getId())) { //$NON-NLS-1$
				String status = ticket.getValue(Key.STATUS);
				if (status != null) {
					String resolution = ticket.getValue(Key.RESOLUTION);
					if (resolution != null) {
						label = NLS.bind(Messages.TracTaskDataHandler_Leave_as_Status_Resolution, status, resolution);
					} else {
						label = NLS.bind(Messages.TracTaskDataHandler_Leave_as_Status, status);
					}
				} else {
					label = Messages.TracTaskDataHandler_Leave;
				}
			} else if ("accept".equals(action.getId())) { //$NON-NLS-1$
				label = Messages.TracTaskDataHandler_Accept;
			} else if ("resolve".equals(action.getId())) { //$NON-NLS-1$
				label = Messages.TracTaskDataHandler_Resolve_as;
			} else if ("reopen".equals(action.getId())) { //$NON-NLS-1$
				label = Messages.TracTaskDataHandler_Reopen;
			} else if ("reassign".equals(action.getId())) { //$NON-NLS-1$
				// do not add reassign for Trac 0.10 since the assigned to field is editable  
			} else {
				label = action.getId();
			}
		}

		if (label != null) {
			TaskAttribute attribute = data.getRoot().createAttribute(TaskAttribute.PREFIX_OPERATION + action.getId());
			TaskOperation.applyTo(attribute, action.getId(), label);
			if (!action.getFields().isEmpty()) {
				// TODO support more than one field
				TracTicketField field = action.getFields().get(0);
				TaskAttribute fieldAttribute = createAttribute(data, field);
				fieldAttribute.getMetaData().setKind(null);
				attribute.getMetaData().putValue(TaskAttribute.META_ASSOCIATED_ATTRIBUTE_ID, fieldAttribute.getId());
			} else if ("resolve".equals(action.getId())) { //$NON-NLS-1$
				attribute.getMetaData().putValue(TaskAttribute.META_ASSOCIATED_ATTRIBUTE_ID,
						TracAttribute.RESOLUTION.getTracKey());
			}

			if (setAsDefault) {
				TaskAttribute operationAttribute = data.getRoot().createAttribute(TaskAttribute.OPERATION);
				TaskOperation.applyTo(operationAttribute, action.getId(), label);
			}
		}
	}

	public static void createDefaultAttributes(TaskData data, ITracClient client, boolean existingTask) {
		data.setVersion(TASK_DATA_VERSION);

		createAttribute(data, client, TracAttribute.SUMMARY);
		createAttribute(data, client, TracAttribute.DESCRIPTION);
		if (existingTask) {
			createAttribute(data, client, TracAttribute.TIME);
			createAttribute(data, client, TracAttribute.CHANGE_TIME);
			createAttribute(data, client, TracAttribute.STATUS);
			TaskAttribute attribute = createAttribute(data, client, TracAttribute.RESOLUTION);
			// reset default value to avoid "fixed" resolution on tasks created through web 
			attribute.setValue(""); //$NON-NLS-1$
			// internal attributes
			createAttribute(data, null, TracAttribute.TOKEN);
		}
		createAttribute(data, client, TracAttribute.COMPONENT);
		createAttribute(data, client, TracAttribute.VERSION);
		createAttribute(data, client, TracAttribute.PRIORITY);
		createAttribute(data, client, TracAttribute.SEVERITY);
		createAttribute(data, client, TracAttribute.MILESTONE);
		createAttribute(data, client, TracAttribute.TYPE);
		createAttribute(data, client, TracAttribute.KEYWORDS);
		// custom fields
		TracTicketField[] fields = client.getTicketFields();
		if (fields != null) {
			for (TracTicketField field : fields) {
				if (field.isCustom()) {
					createAttribute(data, field);
				}
			}
		}
		// people
		createAttribute(data, client, TracAttribute.OWNER);
		if (existingTask) {
			createAttribute(data, client, TracAttribute.REPORTER);
		}
		createAttribute(data, client, TracAttribute.CC);
		if (existingTask) {
			data.getRoot()
					.createAttribute(TracAttributeMapper.NEW_CC)
					.getMetaData()
					.setType(TaskAttribute.TYPE_SHORT_TEXT)
					.setReadOnly(false);
			data.getRoot().createAttribute(TracAttributeMapper.REMOVE_CC);
			data.getRoot()
					.createAttribute(TaskAttribute.COMMENT_NEW)
					.getMetaData()
					.setType(TaskAttribute.TYPE_LONG_RICH_TEXT)
					.setReadOnly(false);
		}
		// operations
		data.getRoot().createAttribute(TaskAttribute.OPERATION).getMetaData().setType(TaskAttribute.TYPE_OPERATION);
	}

	private static TaskAttribute createAttribute(TaskData data, TracTicketField field) {
		TaskAttribute attr = data.getRoot().createAttribute(field.getName());
		TaskAttributeMetaData metaData = attr.getMetaData();
		metaData.defaults();
		metaData.setLabel(field.getLabel() + ":"); //$NON-NLS-1$
		metaData.setKind(TaskAttribute.KIND_DEFAULT);
		metaData.setReadOnly(false);
		metaData.putValue(TRAC_KEY, field.getName());
		if (field.getType() == TracTicketField.Type.CHECKBOX) {
			// attr.addOption("True", "1");
			// attr.addOption("False", "0");
			metaData.setType(TaskAttribute.TYPE_BOOLEAN);
			attr.putOption("1", "1"); //$NON-NLS-1$ //$NON-NLS-2$
			attr.putOption("0", "0"); //$NON-NLS-1$ //$NON-NLS-2$
			if (field.getDefaultValue() != null) {
				attr.setValue(field.getDefaultValue());
			}
		} else if (field.getType() == TracTicketField.Type.SELECT || field.getType() == TracTicketField.Type.RADIO) {
			metaData.setType(TaskAttribute.TYPE_SINGLE_SELECT);
			String[] values = field.getOptions();
			if (values != null && values.length > 0) {
				if (field.isOptional()) {
					attr.putOption("", ""); //$NON-NLS-1$ //$NON-NLS-2$
				}
				for (String value : values) {
					attr.putOption(value, value);
				}
				if (field.getDefaultValue() != null) {
					try {
						int index = Integer.parseInt(field.getDefaultValue());
						if (index > 0 && index < values.length) {
							attr.setValue(values[index]);
						}
					} catch (NumberFormatException e) {
						for (String value : values) {
							if (field.getDefaultValue().equals(value.toString())) {
								attr.setValue(value);
								break;
							}
						}
					}
				}
			}
		} else if (field.getType() == TracTicketField.Type.TEXTAREA) {
			metaData.setType(TaskAttribute.TYPE_LONG_TEXT);
			if (field.getDefaultValue() != null) {
				attr.setValue(field.getDefaultValue());
			}
		} else {
			metaData.setType(TaskAttribute.TYPE_SHORT_TEXT);
			if (field.getDefaultValue() != null) {
				attr.setValue(field.getDefaultValue());
			}
		}
		if (ATTRIBUTE_BLOCKED_BY.equals(field.getName()) || ATTRIBUTE_BLOCKING.equals(field.getName())) {
			metaData.setType(TaskAttribute.TYPE_TASK_DEPENDENCY);
		}
		return attr;
	}

	public static TaskAttribute createAttribute(TaskData data, TracAttribute tracAttribute) {
		return createAttribute(data, null, tracAttribute);
	}

	public static TaskAttribute createAttribute(TaskData data, ITracClient client, TracAttribute tracAttribute) {
		TaskAttribute attr = data.getRoot().createAttribute(tracAttribute.getTracKey());
		TaskAttributeMetaData metaData = attr.getMetaData();
		metaData.setType(tracAttribute.getType());
		metaData.setKind(tracAttribute.getKind());
		metaData.setLabel(tracAttribute.toString());
		metaData.setReadOnly(tracAttribute.isReadOnly());
		metaData.putValue(TRAC_KEY, tracAttribute.getTracKey());
		if (client != null) {
			TracTicketField field = client.getTicketFieldByName(tracAttribute.getTracKey());
			Map<String, String> values = TracAttributeMapper.getRepositoryOptions(client, attr.getId());
			if (values != null && values.size() > 0) {
				boolean setDefault = field == null || !field.isOptional();
				for (Entry<String, String> value : values.entrySet()) {
					attr.putOption(value.getKey(), value.getValue());
					// set first value as default, may get overwritten below
					if (setDefault) {
						attr.setValue(value.getKey());
					}
					setDefault = false;
				}
			} else if (TaskAttribute.TYPE_SINGLE_SELECT.equals(tracAttribute.getType())) {
				attr.getMetaData().setReadOnly(true);
			}
			if (field != null) {
				String defaultValue = field.getDefaultValue();
				if (defaultValue != null && defaultValue.length() > 0) {
					attr.setValue(defaultValue);
				}
			}
		}
		return attr;
	}

	@Override
	public RepositoryResponse postTaskData(TaskRepository repository, TaskData taskData,
			Set<TaskAttribute> oldAttributes, IProgressMonitor monitor) throws CoreException {
		try {
			TracTicket ticket = TracTaskDataHandler.getTracTicket(repository, taskData);
			ITracClient server = connector.getClientManager().getTracClient(repository);
			if (taskData.isNew()) {
				int id = server.createTicket(ticket, monitor);
				return new RepositoryResponse(ResponseKind.TASK_CREATED, id + ""); //$NON-NLS-1$
			} else {
				String newComment = ""; //$NON-NLS-1$
				TaskAttribute newCommentAttribute = taskData.getRoot().getMappedAttribute(TaskAttribute.COMMENT_NEW);
				if (newCommentAttribute != null) {
					newComment = newCommentAttribute.getValue();
				}
				server.updateTicket(ticket, newComment, monitor);
				return new RepositoryResponse(ResponseKind.TASK_UPDATED, ticket.getId() + ""); //$NON-NLS-1$
			}
		} catch (OperationCanceledException e) {
			throw e;
		} catch (Exception e) {
			// TODO catch TracException
			throw new CoreException(TracCorePlugin.toStatus(e, repository));
		}
	}

	@Override
	public boolean initializeTaskData(TaskRepository repository, TaskData data, ITaskMapping initializationData,
			IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		try {
			ITracClient client = connector.getClientManager().getTracClient(repository);
			client.updateAttributes(monitor, false);
			createDefaultAttributes(data, client, false);
			removeEmptySingleSelectAttributes(data);
			return true;
		} catch (OperationCanceledException e) {
			throw e;
		} catch (Exception e) {
			// TODO catch TracException
			throw new CoreException(TracCorePlugin.toStatus(e, repository));
		}
	}

	@Override
	public boolean initializeSubTaskData(TaskRepository repository, TaskData taskData, TaskData parentTaskData,
			IProgressMonitor monitor) throws CoreException {
		initializeTaskData(repository, taskData, null, monitor);
		TaskAttribute blockingAttribute = taskData.getRoot().getMappedAttribute(ATTRIBUTE_BLOCKING);
		if (blockingAttribute == null) {
			throw new CoreException(new RepositoryStatus(repository, IStatus.ERROR, TracCorePlugin.ID_PLUGIN,
					RepositoryStatus.ERROR_REPOSITORY, "The repository does not support subtasks")); //$NON-NLS-1$
		}

		TaskMapper mapper = new TaskMapper(taskData);
		mapper.merge(new TaskMapper(parentTaskData));
		mapper.setDescription(""); //$NON-NLS-1$
		mapper.setSummary(""); //$NON-NLS-1$
		blockingAttribute.setValue(parentTaskData.getTaskId());
		TaskAttribute blockedByAttribute = taskData.getRoot().getMappedAttribute(ATTRIBUTE_BLOCKED_BY);
		if (blockedByAttribute != null) {
			blockedByAttribute.clearValues();
		}
		// special handling for stories which should have tasks as subtasks
		TaskAttribute typeAttribute = taskData.getRoot().getAttribute(TracAttribute.TYPE.getTracKey());
		if (typeAttribute != null && TASK_TYPE_STORY.equals(typeAttribute.getValue())) {
			if (typeAttribute.getOptions().containsKey(TASK_TYPE_TASK)) {
				typeAttribute.setValue(TASK_TYPE_TASK);
			}
		}
		return true;
	}

	@Override
	public boolean canInitializeSubTaskData(TaskRepository taskRepository, ITask task) {
		return Boolean.parseBoolean(task.getAttribute(TracRepositoryConnector.TASK_KEY_SUPPORTS_SUBTASKS));
	}

//	/**
//	 * Updates attributes of <code>taskData</code> from <code>ticket</code>.
//	 */
//	public void updateTaskDataFromTicket(TaskData taskData, TracTicket ticket, ITracClient client) {
//		DefaultTaskSchema schema = new DefaultTaskSchema(taskData);
//		if (ticket.getValue(Key.SUMMARY) != null) {
//			schema.setSummary(ticket.getValue(Key.SUMMARY));
//		}
//
//		if (TracRepositoryConnector.isCompleted(ticket.getValue(Key.STATUS))) {
//			schema.setCompletionDate(ticket.getLastChanged());
//		} else {
//			schema.setCompletionDate(null);
//		}
//
//		String priority = ticket.getValue(Key.PRIORITY);
//		TracPriority[] tracPriorities = client.getPriorities();
//		schema.setPriority(TracRepositoryConnector.getTaskPriority(priority, tracPriorities));
//
//		if (ticket.getValue(Key.TYPE) != null) {
//			TaskKind taskKind = TracRepositoryConnector.TaskKind.fromType(ticket.getValue(Key.TYPE));
//			schema.setTaskKind((taskKind != null) ? taskKind.toString() : ticket.getValue(Key.TYPE));
//		}
//
//		if (ticket.getCreated() != null) {
//			schema.setCreationDate(ticket.getCreated());
//		}
//
//		if (ticket.getCustomValue(TracTaskDataHandler.ATTRIBUTE_BLOCKING) != null) {
//			taskData.addAttribute(ATTRIBUTE_BLOCKED_BY, new TaskAttribute(ATTRIBUTE_BLOCKED_BY, "Blocked by", true));
//		}
//	}

	@Override
	public TaskAttributeMapper getAttributeMapper(TaskRepository repository) {
		ITracClient client = connector.getClientManager().getTracClient(repository);
		return new TracAttributeMapper(repository, client);
	}

	public boolean supportsSubtasks(TaskData taskData) {
		return taskData.getRoot().getAttribute(ATTRIBUTE_BLOCKED_BY) != null;
	}

	public static TracTicket getTracTicket(TaskRepository repository, TaskData data) throws InvalidTicketException,
			CoreException {
		TracTicket ticket = (data.isNew()) ? new TracTicket() : new TracTicket(
				TracRepositoryConnector.getTicketId(data.getTaskId()));

		Collection<TaskAttribute> attributes = data.getRoot().getAttributes().values();
		for (TaskAttribute attribute : attributes) {
			if (TracAttributeMapper.isInternalAttribute(attribute)
					|| TracAttribute.RESOLUTION.getTracKey().equals(attribute.getId())) {
				// ignore internal attributes, resolution is set through operations
			} else if (!attribute.getMetaData().isReadOnly() || Key.TOKEN.getKey().equals(attribute.getId())) {
				ticket.putValue(attribute.getId(), attribute.getValue());
			}
		}

		// set cc value
		StringBuilder sb = new StringBuilder();
		List<String> removeValues = TracRepositoryConnector.getAttributeValues(data, TracAttributeMapper.REMOVE_CC);
		List<String> values = TracRepositoryConnector.getAttributeValues(data, TaskAttribute.USER_CC);
		for (String user : values) {
			if (!removeValues.contains(user)) {
				if (sb.length() > 0) {
					sb.append(","); //$NON-NLS-1$
				}
				sb.append(user);
			}
		}
		if (TracRepositoryConnector.getAttributeValue(data, TracAttributeMapper.NEW_CC).length() > 0) {
			if (sb.length() > 0) {
				sb.append(","); //$NON-NLS-1$
			}
			sb.append(TracRepositoryConnector.getAttributeValue(data, TracAttributeMapper.NEW_CC));
		}
		if (Boolean.TRUE.equals(TracRepositoryConnector.getAttributeValue(data, TaskAttribute.ADD_SELF_CC))) {
			if (sb.length() > 0) {
				sb.append(","); //$NON-NLS-1$
			}
			sb.append(repository.getUserName());
		}
		ticket.putBuiltinValue(Key.CC, sb.toString());

		ticket.putValue("owner", TracRepositoryConnector.getAttributeValue(data, TaskAttribute.USER_ASSIGNED)); //$NON-NLS-1$

		TaskAttribute operationAttribute = data.getRoot().getMappedAttribute(TaskAttribute.OPERATION);
		if (operationAttribute != null) {
			TaskOperation operation = TaskOperation.createFrom(operationAttribute);
			String action = operation.getOperationId();
			if (!"leave".equals(action)) { //$NON-NLS-1$
				if ("accept".equals(action)) { //$NON-NLS-1$
					ticket.putValue("status", TracRepositoryConnector.TaskStatus.ASSIGNED.toStatusString()); //$NON-NLS-1$
				} else if ("resolve".equals(action)) { //$NON-NLS-1$
					ticket.putValue("status", TracRepositoryConnector.TaskStatus.CLOSED.toStatusString()); //$NON-NLS-1$
					ticket.putValue("resolution", TracRepositoryConnector.getAttributeValue(data, //$NON-NLS-1$
							TaskAttribute.RESOLUTION));
				} else if ("reopen".equals(action)) { //$NON-NLS-1$
					ticket.putValue("status", TracRepositoryConnector.TaskStatus.REOPENED.toStatusString()); //$NON-NLS-1$
					ticket.putValue("resolution", ""); //$NON-NLS-1$ //$NON-NLS-2$
				} else if ("reassign".equals(action)) { //$NON-NLS-1$
					ticket.putValue("status", TracRepositoryConnector.TaskStatus.NEW.toStatusString()); //$NON-NLS-1$
				}
			}
			ticket.putValue("action", action); //$NON-NLS-1$
		}

		Date lastChanged = TracUtil.parseDate(TracRepositoryConnector.getAttributeValue(data,
				TracAttribute.CHANGE_TIME.getTracKey()));
		ticket.setLastChanged(lastChanged);

		return ticket;
	}

	@Override
	public void migrateTaskData(TaskRepository taskRepository, TaskData taskData) {
		int version = 0;
		if (taskData.getVersion() != null) {
			try {
				version = Integer.parseInt(taskData.getVersion());
			} catch (NumberFormatException e) {
				// ignore
			}
		}

		if (version < 1) {
			TaskAttribute root = taskData.getRoot();
			List<TaskAttribute> attributes = new ArrayList<TaskAttribute>(root.getAttributes().values());
			for (TaskAttribute attribute : attributes) {
				if (TaskAttribute.TYPE_OPERATION.equals(attribute.getMetaData().getType())
						&& "reassign".equals(attribute.getValue())) { //$NON-NLS-1$
					root.removeAttribute(attribute.getId());
				} else if (TaskAttribute.OPERATION.equals(attribute.getId())) {
					attribute.getMetaData().setType(TaskAttribute.TYPE_OPERATION);
				} else if (TracAttributeMapper.NEW_CC.equals(attribute.getId())) {
					attribute.getMetaData().setType(TaskAttribute.TYPE_SHORT_TEXT).setReadOnly(false);
				} else {
					TracAttribute tracAttribute = TracAttribute.getByTracKey(attribute.getId());
					if (tracAttribute != null) {
						attribute.getMetaData().setType(tracAttribute.getType());
						attribute.getMetaData().setKind(tracAttribute.getKind());
						attribute.getMetaData().setReadOnly(tracAttribute.isReadOnly());
					}
				}
			}
			if (root.getAttribute(TracAttributeMapper.REMOVE_CC) == null) {
				root.createAttribute(TracAttributeMapper.REMOVE_CC);
			}
			if (root.getAttribute(TaskAttribute.COMMENT_NEW) == null) {
				root.createAttribute(TaskAttribute.COMMENT_NEW)
						.getMetaData()
						.setType(TaskAttribute.TYPE_LONG_RICH_TEXT)
						.setReadOnly(false);
			}
		}
		if (version < 2) {
			List<TaskAttribute> attributes = new ArrayList<TaskAttribute>(taskData.getRoot().getAttributes().values());
			for (TaskAttribute attribute : attributes) {
				if (!TracAttributeMapper.isInternalAttribute(attribute)) {
					TaskAttributeMetaData metaData = attribute.getMetaData();
					metaData.putValue(TRAC_KEY, attribute.getId());
					if (metaData.getType() == null) {
						metaData.setType(TaskAttribute.TYPE_SHORT_TEXT);
					}
				}
			}
			taskData.setVersion(TASK_DATA_VERSION);
		}
	}

}
