/*******************************************************************************
 * Copyright (c) 2004, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Eric Rizzo - added API to set the list of recent workspaces.
 *     Jan-Ove Weichel <ovi.weichel@gmail.com> - Bug 463039
 *     Jan-Ove Weichel <janove.weichel@vogella.com> - Bug 411578
 *******************************************************************************/
package org.eclipse.ui.internal.ide;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.util.StringTokenizer;

import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.XMLMemento;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.preferences.ScopedPreferenceStore;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

/**
 * This class stores the information behind the "Launch Workspace" dialog. The
 * class is able to read and write itself to a well known configuration file.
 */
public class ChooseWorkspaceData {
	/**
	 * The default max length of the recent workspace mru list.
	 */
	private static final int RECENT_MAX_LENGTH = 10;

	/**
	 * The directory within the config area that will be used for the
	 * receiver's persisted data.
	 */
	private static final String PERS_FOLDER = "org.eclipse.ui.ide"; //$NON-NLS-1$

	/**
	 * The name of the file within the config area that will be used for
	 * the recever's persisted data.
	 * @see PERS_FOLDER
	 */
	private static final String PERS_FILENAME = "recentWorkspaces.xml"; //$NON-NLS-1$

	/**
	 * In the past a file was used to store persist these values.  This file was written
	 * with this value as its protocol identifier.
	 */
	private static final int PERS_ENCODING_VERSION = 1;

	/**
	 * This is the first version of the encode/decode protocol that uses the
	 * configuration area preference store for persistence. The only encoding
	 * done is to convert the recent workspace list into a comma-separated list.
	 */
	private static final int PERS_ENCODING_VERSION_CONFIG_PREFS = 2;

	/**
	 * This is the second version of the encode/decode protocol that uses the
	 * configuration area preferences store for persistence. This version is the
	 * same as the previous version except it uses a \n character to separate
	 * the path entries instead of commas. (see bug 98467)
	 *
	 * @since 3.3.1
	 */
	private static final int PERS_ENCODING_VERSION_CONFIG_PREFS_NO_COMMAS = 3;

	private boolean showDialog = true;

	private boolean showRecentWorkspaces;

	private String initialDefault;

	private String selection;

	private String[] recentWorkspaces;

	// xml tags
	private static interface XML {
		public static final String PROTOCOL = "protocol"; //$NON-NLS-1$

		public static final String VERSION = "version"; //$NON-NLS-1$

		public static final String ALWAYS_ASK = "alwaysAsk"; //$NON-NLS-1$

		public static final String SHOW_DIALOG = "showDialog"; //$NON-NLS-1$

		public static final String WORKSPACE = "workspace"; //$NON-NLS-1$

		public static final String RECENT_WORKSPACES = "recentWorkspaces"; //$NON-NLS-1$

		public static final String MAX_LENGTH = "maxLength"; //$NON-NLS-1$

		public static final String PATH = "path"; //$NON-NLS-1$
	}

	/**
	 * Creates a new instance, loading persistent data if its found.
	 */
	public ChooseWorkspaceData(String initialDefault) {
		readPersistedData();
		setInitialDefault(initialDefault);
	}

	/**
	 * Creates a new instance, loading persistent data if its found.
	 */
	public ChooseWorkspaceData(URL instanceUrl) {
		readPersistedData();
		if (instanceUrl != null) {
			setInitialDefault(new File(instanceUrl.getFile()).toString());
		}
	}

	/**
	 * Return the folder to be used as a default if no other information
	 * exists. Does not return null.
	 */
	public String getInitialDefault() {
		if (initialDefault == null) {
			setInitialDefault(System.getProperty("user.dir") //$NON-NLS-1$
					+ File.separator + "workspace"); //$NON-NLS-1$
		}
		return initialDefault;
	}

	/**
	 * Set this data's initialDefault parameter to a properly formatted version
	 * of the argument directory string. The proper format is to the platform
	 * appropriate separator character without meaningless leading or trailing
	 * separator characters.
	 */
	private void setInitialDefault(String dir) {
		if (dir == null || dir.length() <= 0) {
			initialDefault = null;
			return;
		}

		dir = new Path(dir).toOSString();
		while (dir.charAt(dir.length() - 1) == File.separatorChar) {
			dir = dir.substring(0, dir.length() - 1);
		}
		initialDefault = dir;
	}

	/**
	 * Return the currently selected workspace or null if nothing is selected.
	 */
	public String getSelection() {
		return selection;
	}

	/**
	 * Return the currently selected workspace or null if nothing is selected.
	 */
	public boolean getShowDialog() {
		return showDialog;
	}

	/**
	 * Returns whether the "Recent Workspaces" should be shown
	 */
	public boolean isShowRecentWorkspaces() {
		return showRecentWorkspaces;
	}

	/**
	 * Return an array of recent workspaces sorted with the most recently used at
	 * the start.
	 */
	public String[] getRecentWorkspaces() {
		return recentWorkspaces;
	}

	/**
	 * The argument workspace has been selected, update the receiver.  Does not
	 * persist the new values.
	 */
	public void workspaceSelected(String dir) {
		// this just stores the selection, it is not inserted and persisted
		// until the workspace is actually selected
		selection = dir;
	}

	/**
	 * Toggle value of the showDialog persistent setting.
	 */
	public void toggleShowDialog() {
		showDialog = !showDialog;
	}

	/**
	 * Set if the "Recent Workspaces" should be shown
	 */
	public void setShowRecentWorkspaces(boolean showRecentWorkspaces) {
		this.showRecentWorkspaces = showRecentWorkspaces;
	}

	/**
	 * Sets the list of recent workspaces.
	 */
	public void setRecentWorkspaces(String[] workspaces) {
		if (workspaces == null) {
			recentWorkspaces = new String[0];
		} else {
			recentWorkspaces = workspaces;
		}
	}

	/**
	 * Update the persistent store. Call this function after the currently
	 * selected value has been found to be ok.
	 */
	public void writePersistedData() {
		// 1. get config pref node
		Preferences node = ConfigurationScope.INSTANCE.getNode(IDEWorkbenchPlugin.IDE_WORKBENCH);

		// 2. get value for showDialog
		node.putBoolean(
				IDE.Preferences.SHOW_WORKSPACE_SELECTION_DIALOG,
				showDialog);

		// 3. use value of numRecent to create proper length array
		node.putInt(IDE.Preferences.MAX_RECENT_WORKSPACES,
				recentWorkspaces.length);

		// move the new selection to the front of the list
		if (selection != null) {
			File newFolder = new File(selection);
			String oldEntry = recentWorkspaces[0];
			recentWorkspaces[0] = selection;
			for (int i = 1; i < recentWorkspaces.length && oldEntry != null; ++i) {
				File oldFolder = new File (oldEntry);
				// If selection represents a file location we already have, don't store it
				if (newFolder.compareTo(oldFolder) == 0){
					break;
				}
				String tmp = recentWorkspaces[i];
				recentWorkspaces[i] = oldEntry;
				oldEntry = tmp;
			}
		}

		// 4. store values of recent workspaces into array
		String encodedRecentWorkspaces = encodeStoredWorkspacePaths(recentWorkspaces);
		node.put(IDE.Preferences.RECENT_WORKSPACES,
				encodedRecentWorkspaces);

		// 5. store the protocol version used to encode the list
		node.putInt(IDE.Preferences.RECENT_WORKSPACES_PROTOCOL,
				PERS_ENCODING_VERSION_CONFIG_PREFS_NO_COMMAS);

		// 6. store if the "Recent Workspaces" should be shown
		node.putBoolean(IDE.Preferences.SHOW_RECENT_WORKSPACES, showRecentWorkspaces);

		// 7. store the node
		try {
			node.flush();
		} catch (BackingStoreException e) {
			// do nothing
		}
	}

	/**
	 * Look for and read data that might have been persisted from some previous
	 * run. Leave the receiver in a default state if no persistent data is
	 * found.
	 *
	 * @return true if a file was successfully read and false otherwise
	 */
	private boolean readPersistedData_file() {
		URL persUrl = null;

		Location configLoc = Platform.getConfigurationLocation();
		if (configLoc != null) {
			persUrl = getPersistenceUrl(configLoc.getURL(), false);
		}

		try {
			// inside try to get the safe default creation in the finally
			// clause
			if (persUrl == null) {
				return false;
			}

			// E.g.,
			//	<launchWorkspaceData>
			//		<protocol version="1"/>
			//      <alwaysAsk showDialog="1"/>
			// 		<recentWorkspaces maxLength="5">
			//			<workspace path="C:\eclipse\workspace0"/>
			//			<workspace path="C:\eclipse\workspace1"/>
			//		</recentWorkspaces>
			//	</launchWorkspaceData>

			Reader reader = new FileReader(persUrl.getFile());
			XMLMemento memento = XMLMemento.createReadRoot(reader);
			if (memento == null || !compatibleFileProtocol(memento)) {
				return false;
			}

			IMemento alwaysAskTag = memento.getChild(XML.ALWAYS_ASK);
			showDialog = alwaysAskTag == null ? true : alwaysAskTag.getInteger(
					XML.SHOW_DIALOG).intValue() == 1;

			IMemento recent = memento.getChild(XML.RECENT_WORKSPACES);
			if (recent == null) {
				return false;
			}

			Integer maxLength = recent.getInteger(XML.MAX_LENGTH);
			int max = RECENT_MAX_LENGTH;
			if (maxLength != null) {
				max = maxLength.intValue();
			}

			IMemento indices[] = recent.getChildren(XML.WORKSPACE);
			if (indices == null || indices.length <= 0) {
				return false;
			}

			// if a user has edited maxLength to be shorter than the listed
			// indices, accept the list (its tougher for them to retype a long
			// list of paths than to update a max number)
			max = Math.max(max, indices.length);

			recentWorkspaces = new String[max];
			for (int i = 0; i < indices.length; ++i) {
				String path = indices[i].getString(XML.PATH);
				if (path == null) {
					break;
				}
				recentWorkspaces[i] = path;
			}
		} catch (IOException | WorkbenchException e) {
			// cannot log because instance area has not been set
			return false;
		} finally {
			// create safe default if needed
			if (recentWorkspaces == null) {
				recentWorkspaces = new String[RECENT_MAX_LENGTH];
			}
		}

		return true;
	}

	/**
	 * Return the current (persisted) value of the "showDialog on startup"
	 * preference. Return the global default if the file cannot be accessed.
	 */
	public static boolean getShowDialogValue() {
		// TODO See the long comment in #readPersistedData -- when the
		//      transition time is over this method can be changed to
		//      read the preference directly.

		ChooseWorkspaceData data = new ChooseWorkspaceData(""); //$NON-NLS-1$

		// return either the value in the file or true, which is the global
		// default
		return data.readPersistedData() ? data.showDialog : true;
	}

	/**
	 * Return the current (persisted) value of the "showDialog on startup"
	 * preference. Return the global default if the file cannot be accessed.
	 */
	public static void setShowDialogValue(boolean showDialog) {
		// TODO See the long comment in #readPersistedData -- when the
		//      transition time is over this method can be changed to
		//      read the preference directly.

		ChooseWorkspaceData data = new ChooseWorkspaceData(""); //$NON-NLS-1$

		// update the value and write the new settings
		data.showDialog = showDialog;
		data.writePersistedData();
	}

	/**
	 * Look in the config area preference store for the list of recently used
	 * workspaces.
	 *
	 * NOTE: During the transition phase the file will be checked if no config
	 * preferences are found.
	 *
	 * @return true if the values were successfully retrieved and false
	 *         otherwise
	 */
	public boolean readPersistedData() {
		IPreferenceStore store = new ScopedPreferenceStore(ConfigurationScope.INSTANCE,
				IDEWorkbenchPlugin.IDE_WORKBENCH);

		// The old way was to store this information in a file, the new is to
		// use the configuration area preference store. To help users with the
		// transition, this code always looks for values in the preference
		// store; they are used if found. If there aren't any related
		// preferences, then the file method is used instead. This class always
		// writes to the preference store, so the fall-back should be needed no
		// more than once per-user, per-configuration.

		// This code always sets the value of the protocol to a non-zero value
		// (currently at 2).  If the value comes back as the default (0), then
		// none of the preferences were set, revert to the file method.

		int protocol = store.getInt(IDE.Preferences.RECENT_WORKSPACES_PROTOCOL);
		if (protocol == IPreferenceStore.INT_DEFAULT_DEFAULT
				&& readPersistedData_file()) {
			return true;
		}

		// 2. get value for showDialog
		showDialog = store.getBoolean(IDE.Preferences.SHOW_WORKSPACE_SELECTION_DIALOG);

		// 3. use value of numRecent to create proper length array
		int max = store.getInt(IDE.Preferences.MAX_RECENT_WORKSPACES);
		max = Math.max(max, RECENT_MAX_LENGTH);

		// 4. load values of recent workspaces into array
		String workspacePathPref = store.getString(IDE.Preferences.RECENT_WORKSPACES);
		recentWorkspaces = decodeStoredWorkspacePaths(protocol, max, workspacePathPref);

		// 5. get value for showRecentWorkspaces
		showRecentWorkspaces = store.getBoolean(IDE.Preferences.SHOW_RECENT_WORKSPACES);

		return true;
	}

	/**
	 * The the list of recent workspaces must be stored as a string in the preference node.
	 */
	private static String encodeStoredWorkspacePaths(String[] recent) {
		StringBuilder buff = new StringBuilder();

		String path = null;
		for (String recentPath : recent) {
			if (recentPath == null) {
				break;
			}

			// as of 3.3.1 pump this out using newlines instead of commas
			if (path != null) {
				buff.append("\n"); //$NON-NLS-1$
			}

			path = recentPath;
			buff.append(path);
		}

		return buff.toString();
	}

	/**
	 * The the preference for recent workspaces must be converted from the
	 * storage string into an array.
	 */
	private static String[] decodeStoredWorkspacePaths(int protocol, int max, String prefValue) {
		String[] paths = new String[max];
		if (prefValue == null || prefValue.length() <= 0) {
			return paths;
		}

		// if we're using the latest version of the protocol use the newline as a
		// token.  Otherwise use the older comma.
		String tokens = null;
		switch (protocol) {
			case PERS_ENCODING_VERSION_CONFIG_PREFS_NO_COMMAS :
				tokens = "\n"; //$NON-NLS-1$
				break;
			case PERS_ENCODING_VERSION_CONFIG_PREFS :
				tokens = ","; //$NON-NLS-1$
				break;
		}
		if (tokens == null) {
			// Unknown version? corrupt file? we can't log it because we don't
			// have a workspace yet...
			return new String[0];
		}

		StringTokenizer tokenizer = new StringTokenizer(prefValue, tokens);
		for (int i = 0; i < paths.length && tokenizer.hasMoreTokens(); ++i) {
			paths[i] = tokenizer.nextToken();
		}

		return paths;
	}

	/**
	 * Returns true if the protocol used to encode the argument memento is
	 * compatible with the receiver's implementation and false otherwise.
	 */
	private static boolean compatibleFileProtocol(IMemento memento) {
		IMemento protocolMemento = memento.getChild(XML.PROTOCOL);
		if (protocolMemento == null) {
			return false;
		}

		Integer version = protocolMemento.getInteger(XML.VERSION);
		return version != null && version.intValue() == PERS_ENCODING_VERSION;
	}

	/**
	 * The workspace data is stored in the well known file pointed to by the result
	 * of this method.
	 * @param create If the directory and file does not exist this parameter
	 *               controls whether it will be created.
	 * @return An url to the file and null if it does not exist or could not
	 *         be created.
	 */
	private static URL getPersistenceUrl(URL baseUrl, boolean create) {
		if (baseUrl == null) {
			return null;
		}

		try {
			// make sure the directory exists
			URL url = new URL(baseUrl, PERS_FOLDER);
			File dir = new File(url.getFile());
			if (!dir.exists() && (!create || !dir.mkdir())) {
				return null;
			}

			// make sure the file exists
			url = new URL(dir.toURL(), PERS_FILENAME);
			File persFile = new File(url.getFile());
			if (!persFile.exists() && (!create || !persFile.createNewFile())) {
				return null;
			}

			return persFile.toURL();
		} catch (IOException e) {
			// cannot log because instance area has not been set
			return null;
		}
	}
}
