/*******************************************************************************
 * Copyright (c) 2004, 2010 Tasktop Technologies 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:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITask.PriorityLevel;
import org.eclipse.mylyn.tasks.core.RepositoryStatus;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskAttachmentHandler;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
import org.eclipse.mylyn.tasks.core.data.TaskHistory;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.data.TaskRelation;
import org.eclipse.mylyn.tasks.core.data.TaskRevision;
import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;

/**
 * @author Mik Kersten
 * @author Rob Elves
 */
public class BugzillaRepositoryConnector extends AbstractRepositoryConnector {

	private static final String BUG_ID = "&bug_id="; //$NON-NLS-1$

	private static final String CHANGED_BUGS_CGI_ENDDATE = "&chfieldto=Now"; //$NON-NLS-1$

	private static final String CHANGED_BUGS_CGI_QUERY = "/buglist.cgi?query_format=advanced&chfieldfrom="; //$NON-NLS-1$

	private static final String CLIENT_LABEL = Messages.BugzillaRepositoryConnector_BUGZILLA_CONNECTOR_LABEL;

	private static final String COMMENT_FORMAT = "yyyy-MM-dd HH:mm"; //$NON-NLS-1$

	//private static final String DEADLINE_FORMAT = "yyyy-MM-dd"; //$NON-NLS-1$

	private static final String TIMESTAMP_WITH_OFFSET = "yyyy-MM-dd HH:mm:ss Z"; //$NON-NLS-1$

	private static final long HOUR = 1000 * 60 * 60;

	private final BugzillaTaskAttachmentHandler attachmentHandler = new BugzillaTaskAttachmentHandler(this);

	private final BugzillaTaskDataHandler taskDataHandler = new BugzillaTaskDataHandler(this);

	protected BugzillaClientManager clientManager;

	protected static BugzillaLanguageSettings enSetting;

	protected static final Set<BugzillaLanguageSettings> languages = new LinkedHashSet<BugzillaLanguageSettings>();

	private static final String ERROR_DELETING_CONFIGURATION = "Error removing corrupt repository configuration file."; //$NON-NLS-1$

	private static final String ERROR_INCOMPATIBLE_CONFIGURATION = "Reset Bugzilla repository configuration cache due to format change"; //$NON-NLS-1$

	private boolean cacheFileRead;

	private File repositoryConfigurationFile;

	private final Map<String, RepositoryConfiguration> repositoryConfigurations = new HashMap<String, RepositoryConfiguration>();

	// A Map from Java's  Platform to Buzilla's
	private static final Map<String, String> java2buzillaPlatformMap = new HashMap<String, String>();

	static {
		enSetting = new BugzillaLanguageSettings(IBugzillaConstants.DEFAULT_LANG);
		enSetting.addLanguageAttribute("error_login", "Login"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("error_login", "log in"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("error_login", "check e-mail"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("error_login", "Invalid Username Or Password"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("error_login", "account locked"); //$NON-NLS-1$//$NON-NLS-2$
		enSetting.addLanguageAttribute("error_collision", "Mid-air collision!"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("error_comment_required", "Comment Required"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("error_logged_out", "logged out"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bad_login", "Login"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bad_login", "log in"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bad_login", "check e-mail"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bad_login", "Invalid Username Or Password"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bad_login", "account locked"); //$NON-NLS-1$//$NON-NLS-2$
		enSetting.addLanguageAttribute("bad_login", "error"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("processed", "processed"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("changes_submitted", "Changes submitted"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("changes_submitted", "added to Bug"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bug", "Bug"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("bug", "Issue"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("submitted", "Submitted"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("submitted", "posted"); //$NON-NLS-1$ //$NON-NLS-2$
		enSetting.addLanguageAttribute("suspicious_action", "Suspicious action"); //$NON-NLS-1$ //$NON-NLS-2$
		languages.add(enSetting);

		java2buzillaPlatformMap.put("x86", "PC"); // can be PC or Macintosh! //$NON-NLS-1$ //$NON-NLS-2$
		java2buzillaPlatformMap.put("x86_64", "PC"); //$NON-NLS-1$ //$NON-NLS-2$
		java2buzillaPlatformMap.put("ia64", "PC"); //$NON-NLS-1$ //$NON-NLS-2$
		java2buzillaPlatformMap.put("ia64_32", "PC"); //$NON-NLS-1$ //$NON-NLS-2$
		java2buzillaPlatformMap.put("sparc", "Sun"); //$NON-NLS-1$ //$NON-NLS-2$
		java2buzillaPlatformMap.put("ppc", "Power PC"); // not Power! //$NON-NLS-1$ //$NON-NLS-2$
	}

	public BugzillaRepositoryConnector() {
		if (BugzillaCorePlugin.getDefault() != null) {
			BugzillaCorePlugin.getDefault().setConnector(this);
			IPath path = BugzillaCorePlugin.getDefault().getConfigurationCachePath();
			this.repositoryConfigurationFile = path.toFile();
		}
	}

	public BugzillaRepositoryConnector(File repositoryConfigurationFile) {
		this.repositoryConfigurationFile = repositoryConfigurationFile;
	}

	@Override
	public String getLabel() {
		return CLIENT_LABEL;
	}

	@Override
	public AbstractTaskAttachmentHandler getTaskAttachmentHandler() {
		return attachmentHandler;
	}

	@Override
	public String getConnectorKind() {
		return BugzillaCorePlugin.CONNECTOR_KIND;
	}

	@Override
	public void updateTaskFromTaskData(TaskRepository repository, ITask task, TaskData taskData) {
		TaskMapper scheme = getTaskMapping(taskData);
		scheme.applyTo(task);

		task.setUrl(BugzillaClient.getBugUrlWithoutLogin(repository.getRepositoryUrl(), taskData.getTaskId()));

		boolean isComplete = false;
		TaskAttribute attributeStatus = taskData.getRoot().getMappedAttribute(TaskAttribute.STATUS);
		if (attributeStatus != null) {
			RepositoryConfiguration configuration = getRepositoryConfiguration(repository.getRepositoryUrl());
			if (configuration == null || configuration.getClosedStatusValues().isEmpty()) {
				isComplete = attributeStatus.getValue().equals(IBugzillaConstants.VALUE_STATUS_RESOLVED)
						|| attributeStatus.getValue().equals(IBugzillaConstants.VALUE_STATUS_CLOSED)
						|| attributeStatus.getValue().equals(IBugzillaConstants.VALUE_STATUS_VERIFIED);
			} else {
				isComplete = configuration.getClosedStatusValues().contains(attributeStatus.getValue());
			}
		}

		if (taskData.isPartial()) {
			if (isComplete) {
				if (task.getCompletionDate() == null) {
					task.setCompletionDate(new Date(0));
				}
			} else {
				if (task.getCompletionDate() != null) {
					task.setCompletionDate(null);
				}
			}
		} else {
			// Completion Date
			if (isComplete) {
				Date completionDate = null;

				List<TaskAttribute> taskComments = taskData.getAttributeMapper().getAttributesByType(taskData,
						TaskAttribute.TYPE_COMMENT);
				if (taskComments != null && taskComments.size() > 0) {
					TaskAttribute lastComment = taskComments.get(taskComments.size() - 1);
					if (lastComment != null) {
						TaskAttribute attributeCommentDate = lastComment.getMappedAttribute(TaskAttribute.COMMENT_DATE);
						if (attributeCommentDate != null) {
							try {
								completionDate = new SimpleDateFormat(COMMENT_FORMAT).parse(attributeCommentDate.getValue());
							} catch (ParseException e) {
								// ignore
							}
						}
					}
				}

				if (completionDate == null) {
					// Use last modified date
					TaskAttribute attributeLastModified = taskData.getRoot().getMappedAttribute(
							TaskAttribute.DATE_MODIFICATION);
					if (attributeLastModified != null && attributeLastModified.getValue().length() > 0) {
						completionDate = taskData.getAttributeMapper().getDateValue(attributeLastModified);
					}
				}

				if (completionDate == null) {
					completionDate = new Date();
				}

				task.setCompletionDate(completionDate);
			} else {
				task.setCompletionDate(null);
			}

			// Bugzilla Specific Attributes

			// Product
			if (scheme.getProduct() != null) {
				task.setAttribute(TaskAttribute.PRODUCT, scheme.getProduct());
			}

			// Severity
			TaskAttribute attrSeverity = taskData.getRoot().getMappedAttribute(BugzillaAttribute.BUG_SEVERITY.getKey());
			if (attrSeverity != null && !attrSeverity.getValue().equals("")) { //$NON-NLS-1$
				task.setAttribute(BugzillaAttribute.BUG_SEVERITY.getKey(), attrSeverity.getValue());
			}

		}

		updateExtendedAttributes(task, taskData);
	}

	private void updateExtendedAttributes(ITask task, TaskData taskData) {
		// Set meta bugzilla task attribute values
		for (BugzillaAttribute bugzillaReportElement : BugzillaAttribute.EXTENDED_ATTRIBUTES) {
			TaskAttribute taskAttribute = taskData.getRoot().getAttribute(bugzillaReportElement.getKey());
			if (taskAttribute != null) {
				task.setAttribute(bugzillaReportElement.getKey(), taskAttribute.getValue());
			}
		}
	}

	@Override
	public void preSynchronization(ISynchronizationSession session, IProgressMonitor monitor) throws CoreException {
		TaskRepository repository = session.getTaskRepository();
		if (session.getTasks().isEmpty()) {
			return;
		}

		monitor = Policy.monitorFor(monitor);
		try {
			monitor.beginTask(Messages.BugzillaRepositoryConnector_checking_for_changed_tasks, session.getTasks()
					.size());

			if (repository.getSynchronizationTimeStamp() == null) {
				for (ITask task : session.getTasks()) {
					session.markStale(task);
				}
				return;
			}

			String dateString = repository.getSynchronizationTimeStamp();
			if (dateString == null) {
				dateString = ""; //$NON-NLS-1$
			}

			String urlQueryBase = repository.getRepositoryUrl() + CHANGED_BUGS_CGI_QUERY
					+ URLEncoder.encode(dateString, repository.getCharacterEncoding()) + CHANGED_BUGS_CGI_ENDDATE;

			StringBuilder urlQueryString = new StringBuilder(Math.min(30 + 9 * session.getTasks().size(), 7009));
			urlQueryString.append(urlQueryBase + BUG_ID);

			Set<ITask> changedTasks = new HashSet<ITask>();
			Iterator<ITask> itr = session.getTasks().iterator();
			int queryCounter = 0;
			Set<ITask> checking = new HashSet<ITask>();
			while (itr.hasNext()) {
				ITask task = itr.next();
				checking.add(task);
				queryCounter++;
				urlQueryString.append(URLEncoder.encode(task.getTaskId() + ",", repository.getCharacterEncoding())); //$NON-NLS-1$
				if (urlQueryString.length() >= 7000) {
					queryForChanged(repository, changedTasks, urlQueryString.toString(), session,
							new SubProgressMonitor(monitor, queryCounter));

					queryCounter = 0;
					urlQueryString.setLength(0);
					urlQueryString.append(urlQueryBase + BUG_ID);
				}

				if (!itr.hasNext() && queryCounter != 0) {
					queryForChanged(repository, changedTasks, urlQueryString.toString(), session,
							new SubProgressMonitor(monitor, queryCounter));
				}
			}

			for (ITask task : session.getTasks()) {
				if (changedTasks.contains(task)) {
					session.markStale(task);
				}
			}

		} catch (UnsupportedEncodingException e) {
			throw new CoreException(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN,
					"Repository configured with unsupported encoding: " + repository.getCharacterEncoding() //$NON-NLS-1$
							+ "\n\n Unable to determine changed tasks.", e)); //$NON-NLS-1$
		} finally {
			monitor.done();
		}
	}

	private void queryForChanged(final TaskRepository repository, Set<ITask> changedTasks, String urlQueryString,
			ISynchronizationSession syncSession, IProgressMonitor monitor) throws UnsupportedEncodingException,
			CoreException {

		HashMap<String, ITask> taskById = new HashMap<String, ITask>();
		for (ITask task : syncSession.getTasks()) {
			taskById.put(task.getTaskId(), task);
		}

		BugzillaTaskDataCollector collector = new BugzillaTaskDataCollector();

		// TODO: Decouple from internals
		IRepositoryQuery query = new RepositoryQuery(repository.getConnectorKind(), ""); //$NON-NLS-1$
		query.setSummary(Messages.BugzillaRepositoryConnector_Query_for_changed_tasks);
		query.setUrl(urlQueryString);
		performQuery(repository, query, collector, syncSession, monitor);
		for (TaskData data : collector.getTaskData()) {
			ITask changedTask = taskById.get(data.getTaskId());
			if (changedTask != null) {
				changedTasks.add(changedTask);
			}
		}
		if (syncSession.getData() == null && collector.getQueryTimestamp() != null) {
			syncSession.setData(collector.getQueryTimestamp());
		}

	}

	@Override
	public boolean canCreateTaskFromKey(TaskRepository repository) {
		return true;
	}

	@Override
	public boolean canCreateNewTask(TaskRepository repository) {
		return true;
	}

	@Override
	public IStatus performQuery(TaskRepository repository, final IRepositoryQuery query,
			TaskDataCollector resultCollector, ISynchronizationSession event, IProgressMonitor monitor) {

		monitor = Policy.monitorFor(monitor);
		try {
			monitor.beginTask(Messages.BugzillaRepositoryConnector_running_query, IProgressMonitor.UNKNOWN);
			BugzillaClient client = getClientManager().getClient(repository, new SubProgressMonitor(monitor, 1));
			TaskAttributeMapper mapper = getTaskDataHandler().getAttributeMapper(repository);
			boolean hitsReceived = client.getSearchHits(query, resultCollector, mapper, monitor);
			if (!hitsReceived) {
				// XXX: HACK in case of ip change bugzilla can return 0 hits
				// due to invalid authorization token, forcing relogin fixes
				client.logout(monitor);
				client.getSearchHits(query, resultCollector, mapper, monitor);
			}

			if (resultCollector instanceof BugzillaTaskDataCollector) {
				BugzillaTaskDataCollector bCollector = (BugzillaTaskDataCollector) resultCollector;
				if (bCollector.getQueryTimestamp() != null) {
					Date queryDate = ((BugzillaAttributeMapper) mapper).getDate(BugzillaAttribute.DELTA_TS.getKey(),
							bCollector.getQueryTimestamp());
					if (queryDate != null) {
						// Ensure time is in right format
						event.setData(new SimpleDateFormat(TIMESTAMP_WITH_OFFSET).format(queryDate));
					}
				}
			}

			return Status.OK_STATUS;
		} catch (UnrecognizedReponseException e) {
			return new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, IStatus.INFO,
					Messages.BugzillaRepositoryConnector_Unrecognized_response_from_server, e);
		} catch (IOException e) {
			return new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, IStatus.ERROR, MessageFormat.format(
					Messages.BugzillaRepositoryConnector_Check_repository_configuration, e.getMessage()), e);
		} catch (CoreException e) {
			return e.getStatus();
		} finally {
			monitor.done();
		}
	}

	@Override
	public String getRepositoryUrlFromTaskUrl(String url) {
		if (url == null) {
			return null;
		}
		int index = url.indexOf(IBugzillaConstants.URL_GET_SHOW_BUG);
		return index == -1 ? null : url.substring(0, index);
	}

	@Override
	public String getTaskIdFromTaskUrl(String url) {
		if (url == null) {
			return null;
		}
		int anchorIndex = url.lastIndexOf("#"); //$NON-NLS-1$
		String bugUrl = url;
		if (anchorIndex != -1) {
			bugUrl = url.substring(0, anchorIndex);
		}

		int index = bugUrl.indexOf(IBugzillaConstants.URL_GET_SHOW_BUG);
		return index == -1 ? null : bugUrl.substring(index + IBugzillaConstants.URL_GET_SHOW_BUG.length());
	}

	@Override
	public String getTaskUrl(String repositoryUrl, String taskId) {
		try {
			return BugzillaClient.getBugUrlWithoutLogin(repositoryUrl, taskId);
		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN,
					"Error constructing task url for " + repositoryUrl + "  id:" + taskId, e)); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return null;
	}

	@Override
	public String getTaskIdPrefix() {
		return "bug"; //$NON-NLS-1$
	}

	public BugzillaClientManager getClientManager() {
		if (clientManager == null) {
			clientManager = new BugzillaClientManager(this);
		}
		return clientManager;
	}

	@Override
	public void updateRepositoryConfiguration(TaskRepository repository, IProgressMonitor monitor) throws CoreException {
		if (repository != null) {
			getRepositoryConfiguration(repository, true, monitor);
		}
	}

	@Override
	public boolean isRepositoryConfigurationStale(TaskRepository repository, IProgressMonitor monitor)
			throws CoreException {
		if (super.isRepositoryConfigurationStale(repository, monitor)) {
			boolean result = true;
			BugzillaClient client = getClientManager().getClient(repository, monitor);
			if (client != null) {
				String timestamp = client.getConfigurationTimestamp(monitor);
				if (timestamp != null) {
					String oldTimestamp = repository.getProperty(IBugzillaConstants.PROPERTY_CONFIGTIMESTAMP);
					if (oldTimestamp != null) {
						result = !timestamp.equals(oldTimestamp);
					}
					repository.setProperty(IBugzillaConstants.PROPERTY_CONFIGTIMESTAMP, timestamp);
				}
			}
			return result;
		}
		return false;
	}

	public static void addLanguageSetting(BugzillaLanguageSettings language) {
		if (!languages.contains(language)) {
			BugzillaRepositoryConnector.languages.add(language);
		}
	}

	public static Set<BugzillaLanguageSettings> getLanguageSettings() {
		return languages;
	}

	/** returns default language if language not found */
	public static BugzillaLanguageSettings getLanguageSetting(String label) {
		for (BugzillaLanguageSettings language : getLanguageSettings()) {
			if (language.getLanguageName().equals(label)) {
				return language;
			}
		}
		return enSetting;
	}

	@Override
	public void postSynchronization(ISynchronizationSession event, IProgressMonitor monitor) throws CoreException {
		try {
			monitor.beginTask("", 1); //$NON-NLS-1$
			if (event.isFullSynchronization() && event.getStatus() == null) {
				event.getTaskRepository().setSynchronizationTimeStamp(getSynchronizationTimestamp(event));
			}
		} finally {
			monitor.done();
		}
	}

	@Override
	public TaskData getTaskData(TaskRepository repository, String taskId, IProgressMonitor monitor)
			throws CoreException {
		return taskDataHandler.getTaskData(repository, taskId, monitor);
	}

	@Override
	public AbstractTaskDataHandler getTaskDataHandler() {
		return taskDataHandler;
	}

	@Override
	public boolean hasTaskChanged(TaskRepository taskRepository, ITask task, TaskData taskData) {
		if (taskData.isPartial() && task.getCreationDate() != null) {
			return false;
		}

		// Security token
		// Updated on the task upon each open (synch) to keep the most current token available for submission - bug#263318
		TaskAttribute attrSecurityToken = taskData.getRoot().getMappedAttribute(BugzillaAttribute.TOKEN.getKey());
		if (attrSecurityToken != null && !attrSecurityToken.getValue().equals("")) { //$NON-NLS-1$
			task.setAttribute(BugzillaAttribute.TOKEN.getKey(), attrSecurityToken.getValue());
		}

		String lastKnownMod = task.getAttribute(BugzillaAttribute.DELTA_TS.getKey());
		if (lastKnownMod != null) {

			TaskAttribute attrModification = taskData.getRoot().getMappedAttribute(TaskAttribute.DATE_MODIFICATION);
			if (attrModification != null && attrModification.getValue() != null
					&& attrModification.getValue().length() > 0) {

				boolean cachedHasTZ = hasTimzone(lastKnownMod);
				boolean repoHasTZ = hasTimzone(attrModification.getValue());
				if (!cachedHasTZ && !repoHasTZ) { // State 1
					return !lastKnownMod.equals(attrModification.getValue());
				}

				BugzillaAttributeMapper mapper = (BugzillaAttributeMapper) taskData.getAttributeMapper();
				Date oldModDate = mapper.getDate(BugzillaAttribute.DELTA_TS.getKey(), lastKnownMod);
				Date newModDate = mapper.getDateValue(attrModification);

				// If either of the dates can't be parsed, fall back to string comparison
				if (oldModDate == null) {
					((AbstractTask) task).setStatus(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN,
							"Unable to parse cached task modification timestamp " + lastKnownMod)); //$NON-NLS-1$
					return !lastKnownMod.equals(attrModification.getValue());
				} else if (newModDate == null) {
					((AbstractTask) task).setStatus(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN,
							"Unable to parse incoming task modification timestamp " + attrModification.getValue())); //$NON-NLS-1$
					return !lastKnownMod.equals(attrModification.getValue());
				}

				if ((cachedHasTZ && !repoHasTZ) || (!cachedHasTZ && repoHasTZ)) { // State 2 (unlikely) || Sate 3
					long delta = Math.abs(newModDate.getTime() - oldModDate.getTime());
					if (delta == 0) {
						return false;
					} else if (delta > 0 && delta % HOUR == 0 && delta < 24 * HOUR) {
						// If same time but in different time zones, ignore/migrate
						return false;
					}
					return true;
				} else if (cachedHasTZ && repoHasTZ) { //State 4 (of 4)
					// Date Compare
					return oldModDate.compareTo(newModDate) != 0;
				}
			}
		}
		return true;
	}

	private boolean hasTimzone(String dateString) {
		if (dateString == null || dateString.length() == 0) {
			return false;
		}
		String[] parts = dateString.split(" "); //$NON-NLS-1$
		boolean hasTimeZone = (parts != null && parts.length == 3);
		return hasTimeZone;
	}

	@Override
	public Collection<TaskRelation> getTaskRelations(TaskData taskData) {
		List<TaskRelation> relations = new ArrayList<TaskRelation>();
		TaskAttribute attribute = taskData.getRoot().getAttribute(BugzillaAttribute.DEPENDSON.getKey());
		if (attribute != null && attribute.getValue().length() > 0) {
			for (String taskId : attribute.getValue().split(",")) { //$NON-NLS-1$
				relations.add(TaskRelation.subtask(taskId.trim()));
			}
		}
		attribute = taskData.getRoot().getAttribute(BugzillaAttribute.BLOCKED.getKey());
		if (attribute != null && attribute.getValue().length() > 0) {
			for (String taskId : attribute.getValue().split(",")) { //$NON-NLS-1$
				relations.add(TaskRelation.parentTask(taskId.trim()));
			}
		}
		return relations;
	}

	private String getSynchronizationTimestamp(ISynchronizationSession event) {
		Date mostRecent = new Date(0);
		String mostRecentTimeStamp = event.getTaskRepository().getSynchronizationTimeStamp();
		if (event.getData() != null) {
			mostRecentTimeStamp = (String) event.getData();
		} else {
			for (ITask task : event.getChangedTasks()) {
				Date taskModifiedDate = task.getModificationDate();
				if (taskModifiedDate != null && taskModifiedDate.after(mostRecent)) {
					mostRecent = taskModifiedDate;
					mostRecentTimeStamp = task.getAttribute(BugzillaAttribute.DELTA_TS.getKey());
				}
			}
		}
		return mostRecentTimeStamp;
	}

	@Override
	public boolean hasRepositoryDueDate(TaskRepository taskRepository, ITask task, TaskData taskData) {
		return taskData.getRoot().getAttribute(BugzillaAttribute.ESTIMATED_TIME.getKey()) != null;
	}

	@Override
	public TaskMapper getTaskMapping(final TaskData taskData) {
		return new TaskMapper(taskData) {
			@Override
			public String getTaskKey() {
				TaskAttribute attribute = getTaskData().getRoot().getAttribute(BugzillaAttribute.BUG_ID.getKey());
				if (attribute != null) {
					return attribute.getValue();
				}
				return super.getTaskKey();
			}

			@Override
			public String getTaskKind() {
				return taskData.getConnectorKind();
			}

			@Override
			public String getTaskUrl() {
				return taskData.getRepositoryUrl();
			}

			@Override
			public PriorityLevel getPriorityLevel() {
				RepositoryConfiguration repositoryConfiguration = BugzillaRepositoryConnector.this.getRepositoryConfiguration(taskData.getRepositoryUrl());
				BugzillaVersion bugzillaVersion;
				if (repositoryConfiguration != null) {
					bugzillaVersion = repositoryConfiguration.getInstallVersion();
				} else {
					bugzillaVersion = BugzillaVersion.MIN_VERSION;
				}
				if (bugzillaVersion.compareTo(BugzillaVersion.BUGZILLA_3_6) >= 0) {
					BugzillaPriorityLevel bugzillaPriorityLevel = BugzillaPriorityLevel.fromPriority(getPriority());
					if (bugzillaPriorityLevel != null) {
						return bugzillaPriorityLevel.toPriorityLevel();
					}
				}
				return super.getPriorityLevel();
			}
		};
	}

	/** public for testing */
	public synchronized void readRepositoryConfigurationFile() {
		if (cacheFileRead || repositoryConfigurationFile == null || !repositoryConfigurationFile.exists()) {
			return;
		}

		synchronized (repositoryConfigurations) {
			ObjectInputStream in = null;
			try {
				in = new ObjectInputStream(new FileInputStream(repositoryConfigurationFile));
				int size = in.readInt();
				for (int nX = 0; nX < size; nX++) {
					RepositoryConfiguration item = (RepositoryConfiguration) in.readObject();
					if (item != null) {
						repositoryConfigurations.put(item.getRepositoryUrl(), item);
					}
				}
			} catch (Exception e) {
				StatusHandler.log(new Status(IStatus.INFO, BugzillaCorePlugin.ID_PLUGIN,
						ERROR_INCOMPATIBLE_CONFIGURATION));
				try {
					if (in != null) {
						in.close();
					}
					if (repositoryConfigurationFile != null && repositoryConfigurationFile.exists()) {
						if (repositoryConfigurationFile.delete()) {
							// successfully deleted
						} else {
							StatusHandler.log(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 0,
									ERROR_DELETING_CONFIGURATION, e));
						}
					}

				} catch (Exception ex) {
					StatusHandler.log(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 0,
							ERROR_DELETING_CONFIGURATION, e));
				}
			} finally {
				cacheFileRead = true;
				if (in != null) {
					try {
						in.close();
					} catch (IOException e) {
						// ignore
					}
				}
			}
		}
	}

	/**
	 * Retrieves the latest repository configuration from the server
	 */
	public RepositoryConfiguration getRepositoryConfiguration(TaskRepository repository, boolean forceRefresh,
			IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		try {
			readRepositoryConfigurationFile();
			RepositoryConfiguration configuration;
			configuration = repositoryConfigurations.get(repository.getRepositoryUrl());
			if (configuration == null || forceRefresh) {
				synchronized (repositoryConfigurations) {
					// check if another thread already retrieved configuration
					configuration = repositoryConfigurations.get(repository.getRepositoryUrl());
					if (configuration == null || forceRefresh) {
						String eTag = null;
						Date lastModifiedHeader = null;
						if (configuration != null && !forceRefresh) {
							eTag = configuration.getETagValue();
							lastModifiedHeader = configuration.getLastModifiedHeader();
						}
						BugzillaClient client = getClientManager().getClient(repository, monitor);
						configuration = client.getRepositoryConfiguration(monitor, eTag);
						boolean newer = true;
						if (configuration != null) {
							if (lastModifiedHeader != null) {
								Date configLastModifiedHeader = configuration.getLastModifiedHeader();
								if (configLastModifiedHeader != null) {
									newer = !configLastModifiedHeader.before(lastModifiedHeader);
								}
							}
							if (newer) {
								String configVersion = configuration.getInstallVersion().toString();
								String repositoryVersion = repository.getVersion();
								if (!configVersion.equals(repositoryVersion)) {
									repository.setVersion(configVersion);
								}
								internalAddConfiguration(configuration);
							}
						}
					}
				}
			}
			return configuration;
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 1,
					"Error retrieving task attributes from repository.\n\n" + e.getMessage(), e)); //$NON-NLS-1$
		} catch (CoreException e) {
			if (e.getMessage().equals("Not changed")) { //$NON-NLS-1$
				RepositoryConfiguration configuration = repositoryConfigurations.get(repository.getRepositoryUrl());
				if (configuration == null) {
					throw new CoreException(new BugzillaStatus(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN,
							RepositoryStatus.ERROR_INTERNAL, "Failed to retrieve repository configuration for " //$NON-NLS-1$
									+ repository.getRepositoryUrl().toString()));

				}
				return configuration;
			} else {
				throw e;
			}
		}
	}

	public void addRepositoryConfiguration(RepositoryConfiguration config) {
		if (config != null) {
			readRepositoryConfigurationFile();
			synchronized (repositoryConfigurations) {
				internalAddConfiguration(config);
			}
		}
	}

	private void internalAddConfiguration(RepositoryConfiguration config) {
		repositoryConfigurations.remove(config.getRepositoryUrl());
		repositoryConfigurations.put(config.getRepositoryUrl(), config);
	}

	public RepositoryConfiguration getRepositoryConfiguration(String repositoryUrl) {
		readRepositoryConfigurationFile();
		return repositoryConfigurations.get(repositoryUrl);
	}

	/** public for testing */
	public void removeConfiguration(RepositoryConfiguration config) {
		synchronized (repositoryConfigurations) {
			repositoryConfigurations.remove(config.getRepositoryUrl());
		}
	}

	/** public for testing */
	public void writeRepositoryConfigFile() {
		if (repositoryConfigurationFile != null) {
			ObjectOutputStream out = null;
			try {
				Set<RepositoryConfiguration> tempConfigs;
				synchronized (repositoryConfigurations) {
					tempConfigs = new HashSet<RepositoryConfiguration>(repositoryConfigurations.values());
				}
				if (tempConfigs.size() > 0) {
					out = new ObjectOutputStream(new FileOutputStream(repositoryConfigurationFile));
					out.writeInt(tempConfigs.size());
					for (RepositoryConfiguration repositoryConfiguration : tempConfigs) {
						if (repositoryConfiguration != null) {
							out.writeObject(repositoryConfiguration);
						}
					}
				}
			} catch (IOException e) {
				StatusHandler.log(new Status(IStatus.WARNING, BugzillaCorePlugin.ID_PLUGIN, 0,
						"Failed to write repository configuration cache", e)); //$NON-NLS-1$
			} finally {
				if (out != null) {
					try {
						out.close();
					} catch (IOException e) {
						// ignore
					}
				}
			}
		}
	}

	public void stop() {
		writeRepositoryConfigFile();
	}

	public void setPlatformDefaultsOrGuess(TaskRepository repository, TaskData newBugModel) {

		String platform = repository.getProperty(IBugzillaConstants.BUGZILLA_DEF_PLATFORM);
		String os = repository.getProperty(IBugzillaConstants.BUGZILLA_DEF_OS);

		// set both or none
		if (null != os && null != platform) {
			TaskAttribute opSysAttribute = newBugModel.getRoot().getAttribute(BugzillaAttribute.OP_SYS.getKey());
			TaskAttribute platformAttribute = newBugModel.getRoot().getAttribute(
					BugzillaAttribute.REP_PLATFORM.getKey());

			// TODO something can still go wrong when the allowed values on the repository change...
			opSysAttribute.setValue(os);
			platformAttribute.setValue(platform);
			return;
		}
		// fall through to old code
		setPlatformOptions(newBugModel);
	}

	public void setPlatformOptions(TaskData newBugModel) {
		try {

			// Get OS Lookup Map
			// Check that the result is in Values, if it is not, set it to other
			// Defaults to the first of each (sorted) list All, All
			TaskAttribute opSysAttribute = newBugModel.getRoot().getAttribute(BugzillaAttribute.OP_SYS.getKey());
			TaskAttribute platformAttribute = newBugModel.getRoot().getAttribute(
					BugzillaAttribute.REP_PLATFORM.getKey());

			String OS = Platform.getOS();
			String platform = Platform.getOSArch();
			String ws = Platform.getWS();

			String bugzillaOS = null; // Bugzilla String for OS
			String bugzillaPlatform = null; // Bugzilla String for Platform
			String[] wsExtentions = null;
/*
			AIX -> AIX
			Linux -> Linux
			HP-UX -> HP-UX
			Solaris -> Solaris
			MacOS X -> Mac OS X
 */

			if (ws.length() > 1) {
				char first = ws.charAt(0);
				char firstLower = Character.toLowerCase(first);
				char firstUpper = Character.toUpperCase(first);
				String[] wsExtentionsTemp = { " - " + firstUpper + ws.substring(1, ws.length()), //$NON-NLS-1$
						" - " + firstLower + ws.substring(1, ws.length()), //$NON-NLS-1$
						" " + firstUpper + ws.substring(1, ws.length()), //$NON-NLS-1$
						" " + firstLower + ws.substring(1, ws.length()), "" }; //$NON-NLS-1$//$NON-NLS-2$
				wsExtentions = wsExtentionsTemp;
			} else if (ws.length() == 1) {
				char first = ws.charAt(0);
				char firstLower = Character.toLowerCase(first);
				char firstUpper = Character.toUpperCase(first);
				String[] wsExtentionsTemp = { " - " + firstUpper, " - " + firstLower, " " + firstUpper, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						" " + firstLower, "" }; //$NON-NLS-1$//$NON-NLS-2$
				wsExtentions = wsExtentionsTemp;
			} else {
				String[] wsExtentionsTemp = { "" }; //$NON-NLS-1$
				wsExtentions = wsExtentionsTemp;
			}

			bugzillaOS = System.getProperty("os.name") + " " + System.getProperty("os.version"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			// We start with the most specific Value as the Search String.
			// If we didn't find it we remove the last part of the version String or the OS Name from
			// the Search String and continue with the test until we found it or the Search String is empty.
			//
			// The search in casesensitive.
			if (opSysAttribute != null) {
				for (String element : wsExtentions) {
					String bugzillaOSTemp = bugzillaOS;
					while (bugzillaOSTemp != null && opSysAttribute.getOption(bugzillaOSTemp + element) == null) {
						int dotindex = bugzillaOSTemp.lastIndexOf('.');
						if (dotindex > 0) {
							bugzillaOSTemp = bugzillaOSTemp.substring(0, dotindex);
						} else {
							int spaceindex = bugzillaOSTemp.lastIndexOf(' ');
							if (spaceindex > 0) {
								bugzillaOSTemp = bugzillaOSTemp.substring(0, spaceindex);
							} else {
								bugzillaOSTemp = null;
							}
						}
					}
					if (bugzillaOSTemp != null) {
						bugzillaOS = bugzillaOSTemp + element;
						break;
					}
				}
			} else {
				bugzillaOS = null;
			}

			if (platform != null && java2buzillaPlatformMap.containsKey(platform)) {
				bugzillaPlatform = java2buzillaPlatformMap.get(platform);
				// Bugzilla knows the following Platforms [All, Macintosh, Other, PC, Power PC, Sun]
				// Platform.getOSArch() returns "x86" on Intel Mac's and "ppc" on Power Mac's
				// so bugzillaPlatform is "Power" or "PC".
				//
				// If the OS is "macosx" we change the Platform to "Macintosh"
				//
				if (bugzillaPlatform != null
						&& (bugzillaPlatform.compareTo("Power") == 0 || bugzillaPlatform.compareTo("PC") == 0) //$NON-NLS-1$ //$NON-NLS-2$
						&& OS != null && OS.compareTo("macosx") == 0) { //$NON-NLS-1$
					// TODO: this may not even be a legal value in another repository!
					bugzillaPlatform = "Macintosh"; //$NON-NLS-1$
				} else if (platformAttribute != null && platformAttribute.getOption(bugzillaPlatform) == null) {
					// If the platform we found is not int the list of available
					// optinos, set the
					// Bugzilla Platform to null, and juse use "other"
					bugzillaPlatform = null;
				}
			}
			// Set the OS and the Platform in the taskData
			if (bugzillaOS != null && opSysAttribute != null && opSysAttribute.getOption(bugzillaOS) != null) {
				opSysAttribute.setValue(bugzillaOS);
			}

			/*else if (opSysAttribute != null && opSysAttribute.getOption(OPTION_ALL) != null) {
				opSysAttribute.setValue(OPTION_ALL);
			}*/

			if (bugzillaPlatform != null && platformAttribute != null
					&& platformAttribute.getOption(bugzillaPlatform) != null) {
				platformAttribute.setValue(bugzillaPlatform);
			}
			/*else if (opSysAttribute != null && platformAttribute != null
					&& platformAttribute.getOption(OPTION_ALL) != null) {
				opSysAttribute.setValue(OPTION_ALL);
			}*/

		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, "could not set platform options", //$NON-NLS-1$
					e));
		}
	}

	public enum BugzillaPriorityLevel {
		HIGHEST, HIGH, NORMAL, LOW, LOWEST, NONE;

		public static BugzillaPriorityLevel fromPriority(String priority) {
			if (priority == null) {
				return null;
			}
			if (priority.equals("Highest")) { //$NON-NLS-1$
				return HIGHEST;
			}
			if (priority.equals("High")) { //$NON-NLS-1$
				return HIGH;
			}
			if (priority.equals("Normal")) { //$NON-NLS-1$
				return NORMAL;
			}
			if (priority.equals("Low")) { //$NON-NLS-1$
				return LOW;
			}
			if (priority.equals("Lowest")) { //$NON-NLS-1$
				return LOWEST;
			}
			if (priority.equals("Lowest")) { //$NON-NLS-1$
				return LOWEST;
			}
			if (priority.equals("---")) { //$NON-NLS-1$
				return NONE;
			}
			return null;
		}

		public PriorityLevel toPriorityLevel() {
			switch (this) {
			case HIGHEST:
				return PriorityLevel.P1;
			case HIGH:
				return PriorityLevel.P2;
			case NORMAL:
				return PriorityLevel.P3;
			case LOW:
				return PriorityLevel.P4;
			case LOWEST:
				return PriorityLevel.P5;
			case NONE:
				return PriorityLevel.P3;
			default:
				return null;
			}
		}

		@Override
		public String toString() {
			switch (this) {
			case HIGHEST:
				return "Highest"; //$NON-NLS-1$
			case HIGH:
				return "High"; //$NON-NLS-1$
			case NORMAL:
				return "Normal"; //$NON-NLS-1$
			case LOW:
				return "Low"; //$NON-NLS-1$
			case LOWEST:
				return "Lowest"; //$NON-NLS-1$
			case NONE:
				return "---"; //$NON-NLS-1$
			default:
				return null;
			}
		}
	}

	@Override
	public boolean canGetTaskHistory(TaskRepository repository, ITask task) {
		return Boolean.parseBoolean(repository.getProperty(IBugzillaConstants.BUGZILLA_USE_XMLRPC));
	}

	@Override
	public TaskHistory getTaskHistory(TaskRepository repository, ITask task, IProgressMonitor monitor)
			throws CoreException {
		BugzillaClient client = getClientManager().getClient(repository, monitor);
		BugHistory bugHistory = client.getBugHistory(task.getTaskId(), monitor).get(0);
		TaskHistory history = new TaskHistory(repository, task);
		for (BugHistory.Revision bugRevision : bugHistory.getRevisions()) {
			TaskRevision revision = new TaskRevision(Long.toString(bugRevision.getWhen().getTime()),
					bugRevision.getWhen(), repository.createPerson(bugRevision.getWho()));
			history.add(revision);
			for (BugHistory.Change bugChange : bugRevision.getChanges()) {
				String attributeId;
				if (bugChange.getAttachmentId() > 0) {
					attributeId = TaskAttribute.PREFIX_ATTACHMENT + bugChange.getAttachmentId();
				} else {
					attributeId = bugChange.getFieldName();
				}
				TaskRevision.Change change = new TaskRevision.Change(attributeId, bugChange.getFieldName(),
						bugChange.getRemoved(), bugChange.getAdded());
				revision.add(change);
			}
		}
		return history;
	}
}
