/******************************************************************************
 *  Copyright (c) 2012 GitHub Inc.
 *  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:
 *    Kevin Sawicki (GitHub Inc.) - initial API and implementation
 *****************************************************************************/
package org.eclipse.egit.ui.internal.branch;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.egit.core.GitProvider;
import org.eclipse.egit.core.internal.util.ProjectUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.clone.ProjectRecord;
import org.eclipse.egit.ui.internal.clone.ProjectUtils;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.XMLMemento;

/**
 * Class to track which projects are imported for each branch.
 * <p>
 * A unique preference is set for each repository/branch combination that is
 * persisted that includes information to be able to restore projects when the
 * branch is later checked out.
 *
 * <p>
 * The workflow is as follows:
 * <p>
 * 1. Call {@link #snapshot()} to get the current projects for the currently
 * checked out branch
 * <p>
 * 2. Call {@link #save(IMemento)} after the new branch has been successfully
 * checked out with the memento returned from step 1
 * <p>
 * 3. Call {@link #restore(IProgressMonitor)} to restore the projects for the
 * newly checked out branch
 *
 */
class BranchProjectTracker {

	private static final String PREFIX = "BranchProjectTracker_"; //$NON-NLS-1$

	private static final String KEY_PROJECTS = "projects"; //$NON-NLS-1$

	private static final String KEY_PROJECT = "project"; //$NON-NLS-1$

	private static final String KEY_BRANCH = "branch"; //$NON-NLS-1$

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

	private final Repository repository;

	/**
	 * Create tracker for repository
	 *
	 * @param repository
	 */
	public BranchProjectTracker(final Repository repository) {
		this.repository = repository;
	}

	private String getBranch() {
		try {
			return repository.getBranch();
		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * Get preference key for branch. This will be unique to the repository and
	 * branch.
	 *
	 * @param branch
	 * @return key
	 */
	public String getPreference(final String branch) {
		if (branch == null)
			throw new IllegalArgumentException("Branch cannot be null"); //$NON-NLS-1$
		if (branch.length() == 0)
			throw new IllegalArgumentException("Branch cannot be empty"); //$NON-NLS-1$

		return PREFIX + '_' + repository.getDirectory().getAbsolutePath() + '_'
				+ branch;
	}

	/**
	 * Snapshot the projects currently associated with the repository
	 * <p>
	 * The memento returned can be later passed to {@link #save(IMemento)} to
	 * persist it
	 *
	 * @see #save(IMemento)
	 * @return memento, will be null on failures
	 */
	public IMemento snapshot() {
		String branch = getBranch();
		if (branch == null)
			return null;

		IProject[] projects;
		try {
			projects = ProjectUtil.getValidOpenProjects(repository);
		} catch (CoreException e) {
			return null;
		}
		XMLMemento memento = XMLMemento.createWriteRoot(KEY_PROJECTS);
		memento.putString(KEY_BRANCH, branch);
		final String workDir = repository.getWorkTree().getAbsolutePath();
		for (IProject project : projects) {
			IPath path = project.getLocation();
			if (path == null)
				continue;
			// Only remember mapped projects
			if (!(RepositoryProvider.getProvider(project) instanceof GitProvider))
				continue;
			String fullPath = path.toOSString();
			if (fullPath.startsWith(workDir)) {
				String relative = fullPath.substring(workDir.length());
				if (relative.length() == 0)
					relative = REPO_ROOT;
				IMemento child = memento.createChild(KEY_PROJECT);
				child.putTextData(relative);
			}
		}
		return memento;
	}

	/**
	 * Associate projects with branch. The specified memento must the one
	 * previously returned from a call to {@link #snapshot()}.
	 *
	 * @see #snapshot()
	 * @param memento
	 * @return this tracker
	 */
	public BranchProjectTracker save(final IMemento memento) {
		if (!(memento instanceof XMLMemento))
			throw new IllegalArgumentException("Invalid memento"); //$NON-NLS-1$

		String branch = memento.getString(KEY_BRANCH);
		IPreferenceStore store = Activator.getDefault().getPreferenceStore();
		String pref = getPreference(branch);
		StringWriter writer = new StringWriter();
		try {
			((XMLMemento) memento).save(writer);
			store.setValue(pref, writer.toString());
		} catch (IOException e) {
			Activator.logError("Error writing branch-project associations", e); //$NON-NLS-1$
		}
		return this;
	}

	/**
	 * Load the project paths associated with the currently checked out branch.
	 * These paths will be relative to the repository root.
	 *
	 * @return non-null but possibly empty array of projects
	 */
	public String[] getProjectPaths() {
		String branch = getBranch();
		if (branch == null)
			return new String[0];
		return getProjectPaths(branch);
	}

	/**
	 * Load the project paths associated with the given branch. These paths will
	 * be relative to the repository root.
	 *
	 * @param branch
	 * @return non-null but possibly empty array of projects
	 */
	public String[] getProjectPaths(final String branch) {
		String pref = getPreference(branch);
		String value = Activator.getDefault().getPreferenceStore()
				.getString(pref);
		if (value.length() == 0)
			return new String[0];
		XMLMemento memento;
		try {
			memento = XMLMemento.createReadRoot(new StringReader(value));
		} catch (WorkbenchException e) {
			Activator.logError("Error reading branch-project associations", e); //$NON-NLS-1$
			return new String[0];
		}
		IMemento[] children = memento.getChildren(KEY_PROJECT);
		if (children.length == 0)
			return new String[0];
		List<String> projects = new ArrayList<String>(children.length);
		for (int i = 0; i < children.length; i++) {
			String path = children[i].getTextData();
			if (path != null && path.length() > 0)
				projects.add(path);
		}
		return projects.toArray(new String[projects.size()]);
	}

	/**
	 * Restore projects associated with the currently checked out branch to the
	 * workspace
	 *
	 * @param monitor
	 */
	public void restore(final IProgressMonitor monitor) {
		String branch = getBranch();
		if (branch != null)
			restore(branch, monitor);
	}

	/**
	 * Restore projects associated with the given branch to the workspace
	 *
	 * @param branch
	 * @param monitor
	 */
	public void restore(final String branch, final IProgressMonitor monitor) {
		String[] paths = getProjectPaths(branch);
		if (paths.length == 0)
			return;

		Set<ProjectRecord> records = new LinkedHashSet<ProjectRecord>();
		File parent = repository.getWorkTree();
		for (String path : paths) {
			File root;
			if (!REPO_ROOT.equals(path))
				root = new File(parent, path);
			else
				root = parent;

			if (!root.isDirectory())
				continue;
			File projectDescription = new File(root,
					IProjectDescription.DESCRIPTION_FILE_NAME);
			if (!projectDescription.isFile())
				continue;
			records.add(new ProjectRecord(projectDescription));
		}
		if (records.isEmpty())
			return;
		IProgressMonitor importMonitor;
		if (monitor != null)
			importMonitor = monitor;
		else
			importMonitor = new NullProgressMonitor();
		try {
			ProjectUtils.createProjects(records, true, null, importMonitor);
		} catch (InvocationTargetException e) {
			Activator
					.logError("Error restoring branch-project associations", e); //$NON-NLS-1$
		} catch (InterruptedException ignored) {
			// Ignored
		}
	}
}
