/*******************************************************************************
 * 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
 *     Jevgeni Holodkov - improvements
 *     Atlassian - improvements for bug 319397
 *******************************************************************************/

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

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryDelta.Type;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryMigrator;
import org.eclipse.mylyn.tasks.core.IRepositoryListener;
import org.eclipse.mylyn.tasks.core.IRepositoryManager;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.TaskRepository;

/**
 * Provides facilities for managing the life-cycle of and access to task repositories.
 * 
 * @author Mik Kersten
 * @author Rob Elves
 * @author Jevgeni Holodkov
 * @author Steffen Pingel
 * @since 3.0
 */
public class TaskRepositoryManager implements IRepositoryManager {

	public static final String OLD_REPOSITORIES_FILE = "repositories.xml"; //$NON-NLS-1$

	public static final String DEFAULT_REPOSITORIES_FILE = "repositories.xml.zip"; //$NON-NLS-1$

	public static final String PREF_REPOSITORIES = "org.eclipse.mylyn.tasklist.repositories."; //$NON-NLS-1$

	private final Map<String, AbstractRepositoryConnector> repositoryConnectors = new HashMap<String, AbstractRepositoryConnector>();

	// connector kinds to corresponding repositories
	private final Map<String, Set<TaskRepository>> repositoryMap = new HashMap<String, Set<TaskRepository>>();

	private final Set<IRepositoryListener> listeners = new CopyOnWriteArraySet<IRepositoryListener>();

	private final Set<TaskRepository> orphanedRepositories = new HashSet<TaskRepository>();

	public static final String MESSAGE_NO_REPOSITORY = Messages.TaskRepositoryManager_No_repository_available;

	public static final String PREFIX_LOCAL = "local-"; //$NON-NLS-1$

	private static final Map<String, Category> repositoryCategories = new HashMap<String, Category>();

	private final PropertyChangeListener PROPERTY_CHANGE_LISTENER = new PropertyChangeListener() {
		public void propertyChange(PropertyChangeEvent evt) {
			TaskRepositoryManager.this.notifyRepositorySettingsChanged((TaskRepository) evt.getSource(),
					new TaskRepositoryDelta(Type.PROPERTY, evt.getPropertyName()));
		}
	};

	private final TaskRepositoriesExternalizer externalizer = new TaskRepositoriesExternalizer();

	private List<AbstractRepositoryMigrator> migrators;

	public TaskRepositoryManager() {
		this.migrators = Collections.emptyList();
		Category catTasks = new Category(TaskRepository.CATEGORY_TASKS, "Tasks", 0); //$NON-NLS-1$
		repositoryCategories.put(catTasks.getId(), catTasks);
		Category catBugs = new Category(TaskRepository.CATEGORY_BUGS, "Bugs", 100); //$NON-NLS-1$
		repositoryCategories.put(catBugs.getId(), catBugs);
		Category catBuild = new Category(TaskRepository.CATEGORY_BUILD, "Builds", 200); //$NON-NLS-1$
		repositoryCategories.put(catBuild.getId(), catBuild);
		Category catReview = new Category(TaskRepository.CATEGORY_REVIEW, "Reviews", 300); //$NON-NLS-1$
		repositoryCategories.put(catReview.getId(), catReview);
		Category catOther = new Category(IRepositoryConstants.CATEGORY_OTHER, "Other", 400); //$NON-NLS-1$
		repositoryCategories.put(catOther.getId(), catOther);
	}

	public synchronized Collection<AbstractRepositoryConnector> getRepositoryConnectors() {
		return new ArrayList<AbstractRepositoryConnector>(repositoryConnectors.values());
	}

	public synchronized AbstractRepositoryConnector getRepositoryConnector(String connectorKind) {
		return repositoryConnectors.get(connectorKind);
	}

	public synchronized AbstractRepositoryConnector removeRepositoryConnector(String connectorKind) {
		return repositoryConnectors.remove(connectorKind);
	}

	public synchronized void addRepositoryConnector(AbstractRepositoryConnector repositoryConnector) {
		if (!repositoryConnectors.values().contains(repositoryConnector)) {
			repositoryConnectors.put(repositoryConnector.getConnectorKind(), repositoryConnector);
		}
	}

	public synchronized boolean hasUserManagedRepositoryConnectors() {
		for (AbstractRepositoryConnector connector : repositoryConnectors.values()) {
			if (connector.isUserManaged()) {
				return true;
			}
		}
		return false;
	}

	public void addRepository(final TaskRepository repository) {
		synchronized (this) {
			Set<TaskRepository> repositories;
			repositories = repositoryMap.get(repository.getConnectorKind());
			if (repositories == null) {
				repositories = new HashSet<TaskRepository>();
				repositoryMap.put(repository.getConnectorKind(), repositories);
			}

			if (!repositories.add(repository)) {
				// TODO 4.0 return false to indicate that remove was unsuccessful
				return;
			}

			repository.addChangeListener(PROPERTY_CHANGE_LISTENER);
		}

		for (final IRepositoryListener listener : listeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Listener failed: " //$NON-NLS-1$
							+ listener.getClass(), e));
				}

				public void run() throws Exception {
					listener.repositoryAdded(repository);
				}
			});
		}
	}

	@Deprecated
	public void removeRepository(final TaskRepository repository, String repositoryFilePath) {
		removeRepository(repository);
	}

	public void removeRepository(final TaskRepository repository) {
		synchronized (this) {
			Set<TaskRepository> repositories = repositoryMap.get(repository.getConnectorKind());
			if (repositories == null || !repositories.remove(repository)) {
				// TODO 4.0 return false to indicate that remove was unsuccessful
				return;
			}
			repository.flushAuthenticationCredentials();
			repository.removeChangeListener(PROPERTY_CHANGE_LISTENER);
		}
		for (final IRepositoryListener listener : listeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.WARNING, ITasksCoreConstants.ID_PLUGIN, "Listener failed: " //$NON-NLS-1$
							+ listener.getClass(), e));
				}

				public void run() throws Exception {
					listener.repositoryRemoved(repository);
				}
			});
		}
	}

	public void addListener(IRepositoryListener listener) {
		listeners.add(listener);
	}

	public void removeListener(IRepositoryListener listener) {
		listeners.remove(listener);
	}

	/* Public for testing. */
	public static String stripSlashes(String url) {
		Assert.isNotNull(url);
		StringBuilder sb = new StringBuilder(url.trim());
		while (sb.length() > 0 && sb.charAt(sb.length() - 1) == '/') {
			sb.setLength(sb.length() - 1);
		}
		return sb.toString();
	}

	public Category getCategory(String id) {
		Category category = repositoryCategories.get(IRepositoryConstants.CATEGORY_OTHER);
		Category cat = repositoryCategories.get(id);
		if (cat != null) {
			category = cat;
		}
		return category;
	}

	public Collection<Category> getCategories() {
		return Collections.unmodifiableCollection(repositoryCategories.values());
	}

	public TaskRepository getRepository(String kind, String urlString) {
		Assert.isNotNull(kind);
		Assert.isNotNull(urlString);
		urlString = stripSlashes(urlString);
		synchronized (this) {
			if (repositoryMap.containsKey(kind)) {
				for (TaskRepository repository : repositoryMap.get(kind)) {
					if (stripSlashes(repository.getRepositoryUrl()).equals(urlString)) {
						return repository;
					}
				}
			}
		}
		return null;
	}

	/**
	 * @return first repository that matches the given url
	 */
	public TaskRepository getRepository(String urlString) {
		Assert.isNotNull(urlString);
		urlString = stripSlashes(urlString);
		synchronized (this) {
			for (String kind : repositoryMap.keySet()) {
				for (TaskRepository repository : repositoryMap.get(kind)) {
					if (stripSlashes(repository.getRepositoryUrl()).equals(urlString)) {
						return repository;
					}
				}
			}
		}
		return null;
	}

	/**
	 * @return the first connector to accept the URL
	 */
	public AbstractRepositoryConnector getConnectorForRepositoryTaskUrl(String url) {
		Assert.isNotNull(url);
		for (AbstractRepositoryConnector connector : getRepositoryConnectors()) {
			String repositoryUrl = connector.getRepositoryUrlFromTaskUrl(url);
			if (repositoryUrl != null) {
				for (TaskRepository repository : getRepositories(connector.getConnectorKind())) {
					if (repositoryUrl.startsWith(repository.getRepositoryUrl())) {
						return connector;
					}
				}
			}
		}
		return null;
	}

	public Set<TaskRepository> getRepositories(String connectorKind) {
		Assert.isNotNull(connectorKind);
		Set<TaskRepository> result;
		synchronized (this) {
			result = repositoryMap.get(connectorKind);
		}
		if (result == null) {
			return Collections.emptySet();
		}
		return new HashSet<TaskRepository>(result);
	}

	public List<TaskRepository> getAllRepositories() {
		List<TaskRepository> repositories = new ArrayList<TaskRepository>();
		synchronized (this) {
			for (AbstractRepositoryConnector repositoryConnector : repositoryConnectors.values()) {
				if (repositoryMap.containsKey(repositoryConnector.getConnectorKind())) {
					repositories.addAll(repositoryMap.get(repositoryConnector.getConnectorKind()));
				}
			}
		}
		return repositories;
	}

	@Deprecated
	public synchronized TaskRepository getDefaultRepository(String kind) {
		// HACK: returns first repository found
		if (repositoryMap.containsKey(kind)) {
			for (TaskRepository repository : repositoryMap.get(kind)) {
				return repository;
			}
		} else {
			Collection<Set<TaskRepository>> values = repositoryMap.values();
			if (!values.isEmpty()) {
				Set<TaskRepository> repoistorySet = values.iterator().next();
				return repoistorySet.iterator().next();
			}
		}
		return null;
	}

	Map<String, Set<TaskRepository>> readRepositories(String repositoriesFilePath) {

		repositoryMap.clear();
		orphanedRepositories.clear();

		loadRepositories(repositoriesFilePath);

//		for (IRepositoryListener listener : listeners) {
//			try {
//				listener.repositoriesRead();
//			} catch (Throwable t) {
//				StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN,
//						"Repository listener failed", t));
//			}
//		}
		return repositoryMap;
	}

	private void loadRepositories(String repositoriesFilePath) {
		boolean migration = false;
		// String dataDirectory =
		// TasksUiPlugin.getDefault().getDataDirectory();
		File repositoriesFile = new File(repositoriesFilePath);

		// Will only load repositories for which a connector exists
		for (AbstractRepositoryConnector repositoryConnector : repositoryConnectors.values()) {
			repositoryMap.put(repositoryConnector.getConnectorKind(), new HashSet<TaskRepository>());
		}
		if (repositoriesFile.exists()) {
			Set<TaskRepository> repositories = externalizer.readRepositoriesFromXML(repositoriesFile);
			if (repositories != null && repositories.size() > 0) {
				for (TaskRepository repository : repositories) {
					if (removeHttpAuthMigration(repository)) {
						migration = true;
					}
					if (applyMigrators(repository)) {
						migration = true;
					}
					if (repositoryMap.containsKey(repository.getConnectorKind())) {
						repositoryMap.get(repository.getConnectorKind()).add(repository);

						repository.addChangeListener(PROPERTY_CHANGE_LISTENER);

					} else {
						orphanedRepositories.add(repository);
					}
				}
			}
			if (migration) {
				saveRepositories(repositoriesFilePath);
			}
		}
	}

	public boolean applyMigrators(final TaskRepository repository) {
		final boolean[] result = new boolean[1];
		for (AbstractRepositoryMigrator migrator : migrators) {
			if (migrator.getConnectorKind().equals(repository.getConnectorKind())) {

				final AbstractRepositoryMigrator finalRepositoryMigrator = migrator;
				result[0] = false;
				SafeRunner.run(new ISafeRunnable() {

					public void handleException(Throwable e) {
						StatusHandler.log(new Status(IStatus.WARNING, ITasksCoreConstants.ID_PLUGIN,
								"Repository migration failed for repository \"" + repository.getUrl() + "\"", e)); //$NON-NLS-1$ //$NON-NLS-2$
					}

					public void run() throws Exception {
						if (finalRepositoryMigrator.migrateRepository(repository)) {
							result[0] = true;
						}
					}

				});
				break;
			}
		}
		return result[0];
	}

	@SuppressWarnings("deprecation")
	private boolean removeHttpAuthMigration(TaskRepository repository) {
		String httpusername = repository.getProperty(TaskRepository.AUTH_HTTP_USERNAME);
		String httppassword = repository.getProperty(TaskRepository.AUTH_HTTP_PASSWORD);
		if (httpusername != null && httppassword != null) {
			repository.removeProperty(TaskRepository.AUTH_HTTP_USERNAME);
			repository.removeProperty(TaskRepository.AUTH_HTTP_PASSWORD);
			if (httpusername.length() > 0 && httppassword.length() > 0) {
				repository.setHttpAuthenticationCredentials(httpusername, httppassword);
			}
			return true;
		}
		return false;
	}

	protected synchronized boolean saveRepositories(String destinationPath) {
//		if (!Platform.isRunning()) {// || TasksUiPlugin.getDefault() == null) {
//			return false;
//		}
		Set<TaskRepository> repositoriesToWrite = new HashSet<TaskRepository>(getAllRepositories());
		// if for some reason a repository is added/changed to equal one in the
		// orphaned set the orphan is discarded
		for (TaskRepository repository : orphanedRepositories) {
			if (!repositoriesToWrite.contains(repository)) {
				repositoriesToWrite.add(repository);
			}
		}

		try {
			File repositoriesFile = new File(destinationPath);
			externalizer.writeRepositoriesToXML(repositoriesToWrite, repositoriesFile);
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Could not save repositories", t)); //$NON-NLS-1$
			return false;
		}
		return true;
	}

	@Deprecated
	public void clearRepositories(String repositoriesFilePath) {
		clearRepositories();
	}

	/**
	 * For testing.
	 */
	public void clearRepositories() {
		List<TaskRepository> repositories = getAllRepositories();
		for (TaskRepository repository : repositories) {
			removeRepository(repository);
		}
		synchronized (this) {
			repositoryMap.clear();
			orphanedRepositories.clear();
		}
	}

	/*
	 * only used for testing
	 */
	public void notifyRepositorySettingsChanged(final TaskRepository repository) {
		notifyRepositorySettingsChanged(repository, new TaskRepositoryDelta(Type.ALL));
	}

	public void notifyRepositorySettingsChanged(final TaskRepository repository, TaskRepositoryDelta delta) {
		final TaskRepositoryChangeEvent event = new TaskRepositoryChangeEvent(this, repository, delta);
		for (final IRepositoryListener listener : listeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Listener failed: " //$NON-NLS-1$
							+ listener.getClass(), e));
				}

				public void run() throws Exception {
					if (listener instanceof IRepositoryChangeListener) {
						((IRepositoryChangeListener) listener).repositoryChanged(event);
					}
					listener.repositorySettingsChanged(repository);
				}
			});
		}
	}

	@Deprecated
	public void insertRepositories(Set<TaskRepository> repositories, String repositoryFilePath) {
		for (TaskRepository repository : repositories) {
			if (getRepository(repository.getConnectorKind(), repository.getRepositoryUrl()) == null) {
				addRepository(repository);
			}
		}
	}

	public boolean isOwnedByUser(ITask task) {
		if (task instanceof LocalTask) {
			return true;
		}

		AbstractRepositoryConnector connector = getRepositoryConnector(task.getConnectorKind());
		if (connector != null) {
			TaskRepository repository = getRepository(task.getConnectorKind(), task.getRepositoryUrl());
			if (repository != null) {
				return connector.isOwnedByUser(repository, task);
			}
		}
		return false;
	}

	/**
	 * @param repository
	 *            with new url
	 * @param oldUrl
	 *            previous url for this repository
	 */
	public void notifyRepositoryUrlChanged(final TaskRepository repository, final String oldUrl) {
		for (final IRepositoryListener listener : listeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Listener failed: " //$NON-NLS-1$
							+ listener.getClass(), e));
				}

				public void run() throws Exception {
					listener.repositoryUrlChanged(repository, oldUrl);
				}
			});
		}
	}

	public Category getCategory(TaskRepository repository) {
		return getCategory(repository.getCategory());
	}

	public void initialize(List<AbstractRepositoryMigrator> repositoryMigrators) {
		this.migrators = repositoryMigrators;

	}
}
