/*********************************************************************
 * Copyright (c) 2010, 2013 Sony Ericsson/ST Ericsson 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:
 *      Sony Ericsson/ST Ericsson - initial API and implementation
 *      Tasktop Technologies - improvements
 *      GitHub, Inc. - fixes for bug 354753
 *      Sascha Scholz (SAP) - improvements
 *********************************************************************/
package org.eclipse.mylyn.internal.gerrit.core;

import java.net.UnknownHostException;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritAuthenticationState;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritClient;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritConfiguration;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritException;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritHttpException;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritLoginException;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritSystemInfo;
import org.eclipse.mylyn.internal.gerrit.core.client.JSonSupport;
import org.eclipse.mylyn.internal.gerrit.core.client.data.GerritQueryResult;
import org.eclipse.mylyn.reviews.core.spi.ReviewsConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITaskMapping;
import org.eclipse.mylyn.tasks.core.RepositoryStatus;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.TaskRepositoryLocationFactory;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;
import org.eclipse.osgi.util.NLS;

import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.server.StandardKeyEncoder;

/**
 * The Gerrit connector core.
 * 
 * @author Mikael Kober
 * @author Thomas Westling
 * @author Sascha Scholz
 * @author Miles Parker
 */
public class GerritConnector extends ReviewsConnector {

	static Logger logger = Logger.getLogger("com.google.gson.ParameterizedTypeHandlerMap"); //$NON-NLS-1$

	static {
		KeyUtil.setEncoderImpl(new StandardKeyEncoder());
		// disable logging of Overriding the existing type handler for class java.sql.Timestamp message
		logger.setLevel(Level.OFF);
	}

	private static final Pattern CHANGE_ID_PATTERN = Pattern.compile("(/#change,|/#/c/)(\\d+)"); //$NON-NLS-1$

	/**
	 * Prefix for task id in a task-url: http://[gerrit-repository]/#change,[task.id] for Gerrit 2.1.
	 */
	public static final String CHANGE_PREFIX_OLD = "/#change,"; //$NON-NLS-1$

	/**
	 * Prefix for task id in a task-url: http://[gerrit-repository]/#/c/[task.id] for Gerrit 2.2 and later.
	 */
	public static final String CHANGE_PREFIX_NEW = "/#/c/"; //$NON-NLS-1$

	/**
	 * Connector kind
	 */
	public static final String CONNECTOR_KIND = "org.eclipse.mylyn.gerrit"; //$NON-NLS-1$

	/**
	 * Label for the connector.
	 */
	public static final String CONNECTOR_LABEL = "Gerrit Code Review"; //$NON-NLS-1$

	public static final String KEY_REPOSITORY_CONFIG = CONNECTOR_KIND + ".config"; //$NON-NLS-1$

	public static final String KEY_REPOSITORY_AUTH = CONNECTOR_KIND + ".auth"; //$NON-NLS-1$

	public static final String KEY_REPOSITORY_ACCOUNT_ID = CONNECTOR_KIND + ".accountId"; //$NON-NLS-1$

	public static final String KEY_REPOSITORY_OPEN_ID_ENABLED = CONNECTOR_KIND + ".openId.enabled"; //$NON-NLS-1$

	public static final String KEY_REPOSITORY_OPEN_ID_PROVIDER = CONNECTOR_KIND + ".openId.provider"; //$NON-NLS-1$

	public static final String GERRIT_RPC_URI = "/gerrit/rpc/"; //$NON-NLS-1$

	public static final String GERRIT_260_RPC_URI = "/gerrit_ui/rpc/"; //$NON-NLS-1$

	private final GerritTaskDataHandler taskDataHandler = new GerritTaskDataHandler(this);

	private TaskRepositoryLocationFactory taskRepositoryLocationFactory = new TaskRepositoryLocationFactory();

	private final ConcurrentMap<TaskRepository, GerritConfiguration> configurationCache = new ConcurrentHashMap<TaskRepository, GerritConfiguration>();

	public GerritConnector() {
		if (GerritCorePlugin.getDefault() != null) {
			GerritCorePlugin.getDefault().setConnector(this);
		}
	}

	/**
	 * Not supported, yet.
	 */
	@Override
	public boolean canCreateNewTask(TaskRepository arg0) {
		return false;
	}

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

	public GerritClient getClient(TaskRepository repository) {
		return (GerritClient) getReviewClient(repository);
	}

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

	@Override
	public String getLabel() {
		return "Gerrit Code Review (supports 2.4 and later)";
	}

	@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 String getRepositoryUrlFromTaskUrl(String url) {
		if (url == null) {
			return null;
		}

		int i = url.indexOf(CHANGE_PREFIX_OLD);
		if (i != -1) {
			return url.substring(0, i);
		}

		i = url.indexOf(CHANGE_PREFIX_NEW);
		if (i != -1) {
			return url.substring(0, i);
		}

		return null;
	}

	@Override
	public String getTaskIdFromTaskUrl(String url) {
		if (url == null) {
			return null;
		}

		// example: https://review.sonyericsson.net/#change,14175
		// example: https://review.sonyericsson.net/#/c/14175
		Matcher matcher = CHANGE_ID_PATTERN.matcher(url);
		if (matcher.find()) {
			return matcher.group(2);
		}

		return null;
	}

	@Override
	public ITaskMapping getTaskMapping(TaskData taskData) {
		return new TaskMapper(taskData);
	}

	public synchronized TaskRepositoryLocationFactory getTaskRepositoryLocationFactory() {
		return taskRepositoryLocationFactory;
	}

	@Override
	public String getTaskUrl(String repositoryUrl, String taskId) {
		repositoryUrl = StringUtils.removeEnd(repositoryUrl, "/"); //$NON-NLS-1$
		return repositoryUrl + CHANGE_PREFIX_NEW + taskId + "/"; //$NON-NLS-1$
	}

	@Override
	public boolean hasTaskChanged(TaskRepository repository, ITask task, TaskData taskData) {
		ITaskMapping taskMapping = getTaskMapping(taskData);
		Date repositoryDate = taskMapping.getModificationDate();
		Date localDate = task.getModificationDate();
		return repositoryDate == null || !repositoryDate.equals(localDate);
	}

	@Override
	public IStatus performQuery(TaskRepository repository, IRepositoryQuery query, TaskDataCollector resultCollector,
			ISynchronizationSession session, IProgressMonitor monitor) {
		try {
			monitor.beginTask("Executing query", IProgressMonitor.UNKNOWN);
			GerritClient client = getClient(repository);
			client.refreshConfigOnce(monitor);

			List<GerritQueryResult> result = null;
			if (GerritQuery.ALL_OPEN_CHANGES.equals(query.getAttribute(GerritQuery.TYPE))) {
				result = client.queryAllReviews(monitor);
			} else if (GerritQuery.CUSTOM.equals(query.getAttribute(GerritQuery.TYPE))) {
				String queryString = query.getAttribute(GerritQuery.QUERY_STRING);
				result = client.executeQuery(monitor, queryString);
			} else if (GerritQuery.MY_CHANGES.equals(query.getAttribute(GerritQuery.TYPE))) {
				result = client.queryMyReviews(monitor);
			} else if (GerritQuery.MY_WATCHED_CHANGES.equals(query.getAttribute(GerritQuery.TYPE))) {
				result = client.queryWatchedReviews(monitor);
			} else if (GerritQuery.OPEN_CHANGES_BY_PROJECT.equals(query.getAttribute(GerritQuery.TYPE))) {
				String project = query.getAttribute(GerritQuery.PROJECT);
				result = client.queryByProject(monitor, project);
			}

			if (result != null) {
				for (GerritQueryResult changeInfo : result) {
					TaskData taskData = taskDataHandler.createPartialTaskData(repository,
							Integer.toString(changeInfo.getNumber()), monitor);
					taskDataHandler.updateTaskData(repository, taskData, changeInfo);
					resultCollector.accept(taskData);
				}
				return Status.OK_STATUS;
			}

			return new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, NLS.bind("Unknown query type: {0}",
					query.getAttribute(GerritQuery.PROJECT)));
		} catch (UnsupportedClassVersionError e) {
			return toStatus(repository, e);
		} catch (GerritException e) {
			return toStatus(repository, "Problem performing query", e);
		} finally {
			monitor.done();
		}
	}

	public synchronized void setTaskRepositoryLocationFactory(
			TaskRepositoryLocationFactory taskRepositoryLocationFactory) {
		this.taskRepositoryLocationFactory = taskRepositoryLocationFactory;
	}

	@Override
	public void updateRepositoryConfiguration(TaskRepository repository, IProgressMonitor monitor) throws CoreException {
		try {
			getClient(repository).refreshConfig(monitor);
		} catch (GerritException e) {
			throw toCoreException(repository, "Problem updating repository", e);
		}
	}

	@Override
	public void updateTaskFromTaskData(TaskRepository taskRepository, ITask task, TaskData taskData) {
		Date oldModificationDate = task.getModificationDate();

		TaskMapper mapper = (TaskMapper) getTaskMapping(taskData);
		mapper.applyTo(task);
		String key = task.getTaskKey();
		if (key != null) {
			task.setSummary(NLS.bind("{0} [{1}]", mapper.getSummary(), key)); //$NON-NLS-1$
			task.setTaskKey(task.getTaskId());
		}

		// retain modification date to force an update when full task data is received
		if (taskData.isPartial()) {
			task.setModificationDate(oldModificationDate);
		}
	}

	public GerritSystemInfo validate(TaskRepository repository, IProgressMonitor monitor) throws CoreException {
		// only allow user prompting in case of Open ID authentication 
		if (!Boolean.parseBoolean(repository.getProperty(GerritConnector.KEY_REPOSITORY_OPEN_ID_ENABLED))) {
			monitor = Policy.backgroundMonitorFor(monitor);
		}
		try {
			return createTransientReviewClient(repository).getInfo(monitor);
		} catch (UnsupportedClassVersionError e) {
			throw toCoreException(repository, e);
		} catch (GerritException e) {
			throw toCoreException(repository, "Invalid repository", e);
		}
	}

	@Override
	protected GerritClient createReviewClient(final TaskRepository repository, boolean b) {
		GerritConfiguration config = loadConfiguration(repository);
		GerritAuthenticationState authState = loadAuthState(repository);
		return new GerritClient(repository, taskRepositoryLocationFactory.createWebLocation(repository), config,
				authState) {
			@Override
			protected void configurationChanged(GerritConfiguration config) {
				saveConfiguration(repository, config);
			}

			@Override
			protected void authStateChanged(GerritAuthenticationState authState) {
				repository.setProperty(KEY_REPOSITORY_AUTH, GerritClient.authStateToString(authState));
			}
		};
	}

	protected GerritClient createTransientReviewClient(final TaskRepository repository) {
		return new GerritClient(repository, taskRepositoryLocationFactory.createWebLocation(repository));
	}

	private GerritAuthenticationState loadAuthState(final TaskRepository repository) {
		String authState = repository.getProperty(KEY_REPOSITORY_AUTH);
		if (authState != null) {
			return GerritClient.authStateFromString(authState);
		}
		return null;
	}

	protected GerritConfiguration loadConfiguration(TaskRepository repository) {
		GerritConfiguration configuration = configurationCache.get(repository);
		if (configuration == null) {
			configuration = configurationFromString(repository.getProperty(KEY_REPOSITORY_CONFIG));
			if (configuration != null) {
				configurationCache.put(repository, configuration);
			}
		}
		return configuration;
	}

	protected void saveConfiguration(TaskRepository repository, GerritConfiguration configuration) {
		configurationCache.put(repository, configuration);
		repository.setProperty(KEY_REPOSITORY_CONFIG, configurationToString(configuration));
	}

	public GerritConfiguration getConfiguration(TaskRepository repository) {
		GerritConfiguration configuration = configurationCache.get(repository);
		if (configuration == null) {
			configuration = loadConfiguration(repository);
		}
		return configuration;
	}

	private static GerritConfiguration configurationFromString(String token) {
		if (token == null) {
			return null;
		}
		try {
			JSonSupport support = new JSonSupport();
			return support.parseResponse(token, GerritConfiguration.class);
		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID,
					"Failed to deserialize configuration: '" + token + "'", e));
			return null;
		}
	}

	private static String configurationToString(GerritConfiguration config) {
		try {
			JSonSupport support = new JSonSupport();
			return support.toJson(config);
		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID,
					"Failed to serialize configuration", e));
			return null;
		}
	}

	public CoreException toCoreException(TaskRepository repository, String qualifier, GerritException e) {
		return new CoreException(toStatus(repository, qualifier, e));
	}

	public static CoreException toCoreException(TaskRepository repository, UnsupportedClassVersionError e) {
		return new CoreException(toStatus(repository, e));
	}

	Status toStatus(TaskRepository repository, String qualifier, Exception e) {
		if (StringUtils.isEmpty(qualifier)) {
			qualifier = "";
		} else if (!StringUtils.endsWith(qualifier, ": ")) {
			qualifier += ": ";
		}
		if (e instanceof GerritHttpException) {
			int code = ((GerritHttpException) e).getResponseCode();
			return createErrorStatus(repository, qualifier + HttpStatus.getStatusText(code));
		} else if (e instanceof GerritLoginException) {
			if (repository != null) {
				return RepositoryStatus.createLoginError(repository.getUrl(), GerritCorePlugin.PLUGIN_ID);
			} else {
				return createErrorStatus(null, qualifier + "Unknown Host");
			}
		} else if (e instanceof UnknownHostException) {
			return createErrorStatus(repository, qualifier + "Unknown Host");
		} else if (e instanceof GerritException && e.getCause() != null) {
			Throwable cause = e.getCause();
			if (cause instanceof Exception) {
				return toStatus(repository, qualifier, (Exception) cause);
			}
		} else if (e instanceof GerritException && e.getMessage() != null) {
			return createErrorStatus(repository, NLS.bind("{0}Gerrit connection issue: {1}", qualifier, e.getMessage()));
		}
		String message = NLS.bind("{0}Unexpected error while connecting to Gerrit: {1}", qualifier, e.getMessage());
		if (repository != null) {
			return RepositoryStatus.createStatus(repository, IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message);
		} else {
			return createErrorStatus(repository, message);
		}
	}

	protected Status createErrorStatus(TaskRepository repository, String message) {
		if (repository != null) {
			return RepositoryStatus.createStatus(repository, IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message);
		} else {
			return new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message + " (Repository Unknown)");
		}
	}

	public static Status toStatus(TaskRepository repository, UnsupportedClassVersionError e) {
		String message = NLS.bind("The Gerrit Connector requires at Java 1.6 or higer (installed version: {0})",
				System.getProperty("java.version"));
		return new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message, e);
	}

	public static boolean isClosed(String status) {
		return "MERGED".equals(status) || "ABANDONED".equals(status);
	}
}
