/*******************************************************************************
 * Copyright (c) 2004, 2011 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.runtime.adaptor;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import org.eclipse.core.runtime.internal.adaptor.*;
import org.eclipse.osgi.framework.adaptor.FrameworkAdaptor;
import org.eclipse.osgi.framework.internal.core.Constants;
import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
import org.eclipse.osgi.internal.baseadaptor.AdaptorUtil;
import org.eclipse.osgi.service.datalocation.Location;

/**
 * This class is used to manage the various Locations for Eclipse.
 * <p>
 * Clients may not extend this class.
 * </p>
 * @since 3.1
 * @noextend This class is not intended to be subclassed by clients.
 */
public class LocationManager {
	private static Location installLocation = null;
	private static Location configurationLocation = null;
	private static Location userLocation = null;
	private static Location instanceLocation = null;
	private static Location eclipseHomeLocation = null;

	public static final String READ_ONLY_AREA_SUFFIX = ".readOnly"; //$NON-NLS-1$
	public static final String PROP_INSTALL_AREA = "osgi.install.area"; //$NON-NLS-1$
	public static final String PROP_CONFIG_AREA = "osgi.configuration.area"; //$NON-NLS-1$
	public static final String PROP_CONFIG_AREA_DEFAULT = "osgi.configuration.area.default"; //$NON-NLS-1$
	public static final String PROP_SHARED_CONFIG_AREA = "osgi.sharedConfiguration.area"; //$NON-NLS-1$
	public static final String PROP_INSTANCE_AREA = "osgi.instance.area"; //$NON-NLS-1$
	public static final String PROP_INSTANCE_AREA_DEFAULT = "osgi.instance.area.default"; //$NON-NLS-1$
	public static final String PROP_USER_AREA = "osgi.user.area"; //$NON-NLS-1$
	public static final String PROP_USER_AREA_DEFAULT = "osgi.user.area.default"; //$NON-NLS-1$
	public static final String PROP_MANIFEST_CACHE = "osgi.manifest.cache"; //$NON-NLS-1$
	public static final String PROP_USER_HOME = "user.home"; //$NON-NLS-1$
	public static final String PROP_USER_DIR = "user.dir"; //$NON-NLS-1$
	public static final String PROP_HOME_LOCATION_AREA = "eclipse.home.location"; //$NON-NLS-1$
	static final String PROP_LAUNCHER = "eclipse.launcher"; //$NON-NLS-1$

	// configuration area file/dir names
	public static final String BUNDLES_DIR = "bundles"; //$NON-NLS-1$
	public static final String STATE_FILE = ".state"; //$NON-NLS-1$
	public static final String LAZY_FILE = ".lazy"; //$NON-NLS-1$
	public static final String BUNDLE_DATA_FILE = ".bundledata"; //$NON-NLS-1$
	public static final String MANIFESTS_DIR = "manifests"; //$NON-NLS-1$
	public static final String CONFIG_FILE = "config.ini"; //$NON-NLS-1$
	public static final String ECLIPSE_PROPERTIES = "eclipse.properties"; //$NON-NLS-1$

	// Constants for configuration location discovery
	private static final String ECLIPSE = "eclipse"; //$NON-NLS-1$
	private static final String PRODUCT_SITE_MARKER = ".eclipseproduct"; //$NON-NLS-1$
	private static final String PRODUCT_SITE_ID = "id"; //$NON-NLS-1$
	private static final String PRODUCT_SITE_VERSION = "version"; //$NON-NLS-1$

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

	// Data mode constants for user, configuration and data locations.
	private static final String NONE = "@none"; //$NON-NLS-1$
	private static final String NO_DEFAULT = "@noDefault"; //$NON-NLS-1$
	private static final String USER_HOME = "@user.home"; //$NON-NLS-1$
	private static final String USER_DIR = "@user.dir"; //$NON-NLS-1$

	private static final String INSTANCE_DATA_AREA_PREFIX = ".metadata/.plugins/"; //$NON-NLS-1$

	/**
	 * Builds a URL with the given specification
	 * @param spec the URL specification
	 * @param trailingSlash flag to indicate a trailing slash on the spec
	 * @return a URL
	 */
	public static URL buildURL(String spec, boolean trailingSlash) {
		return LocationHelper.buildURL(spec, trailingSlash);
	}

	private static void mungeConfigurationLocation() {
		// if the config property was set, munge it for backwards compatibility.
		String location = FrameworkProperties.getProperty(PROP_CONFIG_AREA);
		if (location != null) {
			if (location.endsWith(".cfg")) { //$NON-NLS-1$
				int index = location.lastIndexOf('/');
				if (index < 0)
					index = location.lastIndexOf('\\');
				location = location.substring(0, index + 1);
				FrameworkProperties.setProperty(PROP_CONFIG_AREA, location);
			}
		}
	}

	/**
	 * Initializes the Location objects for the LocationManager.
	 */
	public static void initializeLocations() {
		// set the osgi storage area if it exists
		String osgiStorage = FrameworkProperties.getProperty(Constants.FRAMEWORK_STORAGE);
		if (osgiStorage != null)
			FrameworkProperties.setProperty(PROP_CONFIG_AREA, osgiStorage);
		// do install location initialization first since others may depend on it
		// assumes that the property is already set
		installLocation = buildLocation(PROP_INSTALL_AREA, null, "", true, false, null); //$NON-NLS-1$

		// TODO not sure what the data area prefix should be here for the user area
		Location temp = buildLocation(PROP_USER_AREA_DEFAULT, null, "", false, false, null); //$NON-NLS-1$
		URL defaultLocation = temp == null ? null : temp.getURL();
		if (defaultLocation == null)
			defaultLocation = buildURL(new File(FrameworkProperties.getProperty(PROP_USER_HOME), "user").getAbsolutePath(), true); //$NON-NLS-1$
		userLocation = buildLocation(PROP_USER_AREA, defaultLocation, "", false, false, null); //$NON-NLS-1$

		temp = buildLocation(PROP_INSTANCE_AREA_DEFAULT, null, "", false, false, INSTANCE_DATA_AREA_PREFIX); //$NON-NLS-1$
		defaultLocation = temp == null ? null : temp.getURL();
		if (defaultLocation == null)
			defaultLocation = buildURL(new File(FrameworkProperties.getProperty(PROP_USER_DIR), "workspace").getAbsolutePath(), true); //$NON-NLS-1$
		instanceLocation = buildLocation(PROP_INSTANCE_AREA, defaultLocation, "", false, false, INSTANCE_DATA_AREA_PREFIX); //$NON-NLS-1$

		mungeConfigurationLocation();
		// compute a default but it is very unlikely to be used since main will have computed everything
		temp = buildLocation(PROP_CONFIG_AREA_DEFAULT, null, "", false, false, null); //$NON-NLS-1$
		defaultLocation = temp == null ? null : temp.getURL();
		if (defaultLocation == null && FrameworkProperties.getProperty(PROP_CONFIG_AREA) == null)
			// only compute the default if the configuration area property is not set
			defaultLocation = buildURL(computeDefaultConfigurationLocation(), true);
		configurationLocation = buildLocation(PROP_CONFIG_AREA, defaultLocation, "", false, false, null); //$NON-NLS-1$
		// get the parent location based on the system property. This will have been set on the 
		// way in either by the caller/user or by main.  There will be no parent location if we are not 
		// cascaded.
		URL parentLocation = computeSharedConfigurationLocation();
		if (parentLocation != null && !parentLocation.equals(configurationLocation.getURL())) {
			Location parent = new BasicLocation(null, parentLocation, true, null);
			((BasicLocation) configurationLocation).setParent(parent);
		}
		initializeDerivedConfigurationLocations();

		if (FrameworkProperties.getProperty(PROP_HOME_LOCATION_AREA) == null) {
			String eclipseLauncher = FrameworkProperties.getProperty(PROP_LAUNCHER);
			String eclipseHomeLocationPath = getEclipseHomeLocation(eclipseLauncher);
			if (eclipseHomeLocationPath != null)
				FrameworkProperties.setProperty(PROP_HOME_LOCATION_AREA, eclipseHomeLocationPath);
		}
		// if eclipse.home.location is not set then default to osgi.install.area
		if (FrameworkProperties.getProperty(PROP_HOME_LOCATION_AREA) == null && FrameworkProperties.getProperty(PROP_INSTALL_AREA) != null)
			FrameworkProperties.setProperty(PROP_HOME_LOCATION_AREA, FrameworkProperties.getProperty(PROP_INSTALL_AREA));
		eclipseHomeLocation = buildLocation(PROP_HOME_LOCATION_AREA, null, "", true, true, null); //$NON-NLS-1$
	}

	private static String getEclipseHomeLocation(String launcher) {
		if (launcher == null)
			return null;
		File launcherFile = new File(launcher);
		if (launcherFile.getParent() == null)
			return null;
		File launcherDir = new File(launcherFile.getParent());
		// check for mac os; the os check is copied from EclipseEnvironmentInfo.
		String macosx = org.eclipse.osgi.service.environment.Constants.OS_MACOSX;
		if (macosx.equals(EclipseEnvironmentInfo.getDefault().getOS()))
			launcherDir = getMacOSEclipsoeHomeLocation(launcherDir);
		return (launcherDir.exists() && launcherDir.isDirectory()) ? launcherDir.getAbsolutePath() : null;
	}

	private static File getMacOSEclipsoeHomeLocation(File launcherDir) {
		// TODO for now we go up three directories from the launcher dir as long as the parent dir is named MacOS; is this always the case?
		// TODO not sure if case is important
		if (!launcherDir.getName().equalsIgnoreCase("macos")) //$NON-NLS-1$
			return launcherDir; // don't do the up three stuff if not in macos directory
		String launcherParent = launcherDir.getParent();
		if (launcherParent != null)
			launcherParent = new File(launcherParent).getParent();
		if (launcherParent != null)
			launcherParent = new File(launcherParent).getParent();
		return launcherParent == null ? null : new File(launcherParent);
	}

	@SuppressWarnings("deprecation")
	private static Location buildLocation(String property, URL defaultLocation, String userDefaultAppendage, boolean readOnlyDefault, boolean computeReadOnly, String dataAreaPrefix) {
		String location = FrameworkProperties.clearProperty(property);
		// the user/product may specify a non-default readOnly setting   
		String userReadOnlySetting = FrameworkProperties.getProperty(property + READ_ONLY_AREA_SUFFIX);
		boolean readOnly = (userReadOnlySetting == null ? readOnlyDefault : Boolean.valueOf(userReadOnlySetting).booleanValue());
		// if the instance location is not set, predict where the workspace will be and 
		// put the instance area inside the workspace meta area.
		if (location == null)
			return new BasicLocation(property, defaultLocation, userReadOnlySetting != null || !computeReadOnly ? readOnly : !canWrite(defaultLocation), dataAreaPrefix);
		String trimmedLocation = location.trim();
		if (trimmedLocation.equalsIgnoreCase(NONE))
			return null;
		if (trimmedLocation.equalsIgnoreCase(NO_DEFAULT))
			return new BasicLocation(property, null, readOnly, dataAreaPrefix);
		if (trimmedLocation.startsWith(USER_HOME)) {
			String base = substituteVar(location, USER_HOME, PROP_USER_HOME);
			location = new File(base, userDefaultAppendage).getAbsolutePath();
		} else if (trimmedLocation.startsWith(USER_DIR)) {
			String base = substituteVar(location, USER_DIR, PROP_USER_DIR);
			location = new File(base, userDefaultAppendage).getAbsolutePath();
		}
		URL url = buildURL(location, true);
		BasicLocation result = null;
		if (url != null) {
			result = new BasicLocation(property, null, userReadOnlySetting != null || !computeReadOnly ? readOnly : !canWrite(url), dataAreaPrefix);
			result.setURL(url, false);
		}
		return result;
	}

	private static String substituteVar(String source, String var, String prop) {
		String value = FrameworkProperties.getProperty(prop, ""); //$NON-NLS-1$
		return value + source.substring(var.length());
	}

	private static void initializeDerivedConfigurationLocations() {
		if (FrameworkProperties.getProperty(PROP_MANIFEST_CACHE) == null)
			FrameworkProperties.setProperty(PROP_MANIFEST_CACHE, getConfigurationFile(MANIFESTS_DIR).getAbsolutePath());
	}

	private static URL computeInstallConfigurationLocation() {
		String property = FrameworkProperties.getProperty(PROP_INSTALL_AREA);
		if (property != null)
			return LocationHelper.buildURL(property, true);
		return null;
	}

	private static URL computeSharedConfigurationLocation() {
		String property = FrameworkProperties.getProperty(PROP_SHARED_CONFIG_AREA);
		if (property == null)
			return null;
		try {
			URL sharedConfigurationURL = LocationHelper.buildURL(property, true);
			if (sharedConfigurationURL == null)
				return null;
			if (sharedConfigurationURL.getPath().startsWith("/")) //$NON-NLS-1$
				// absolute
				return sharedConfigurationURL;
			URL installURL = installLocation.getURL();
			if (!sharedConfigurationURL.getProtocol().equals(installURL.getProtocol()))
				// different protocol
				return sharedConfigurationURL;
			sharedConfigurationURL = new URL(installURL, sharedConfigurationURL.getPath());
			FrameworkProperties.setProperty(PROP_SHARED_CONFIG_AREA, sharedConfigurationURL.toExternalForm());
		} catch (MalformedURLException e) {
			// do nothing here since it is basically impossible to get a bogus url 
		}
		return null;
	}

	private static String computeDefaultConfigurationLocation() {
		// 1) We store the config state relative to the 'eclipse' directory if possible
		// 2) If this directory is read-only 
		//    we store the state in <user.home>/.eclipse/<application-id>_<version> where <user.home> 
		//    is unique for each local user, and <application-id> is the one 
		//    defined in .eclipseproduct marker file. If .eclipseproduct does not
		//    exist, use "eclipse" as the application-id.

		URL installURL = computeInstallConfigurationLocation();
		if (installURL != null && "file".equals(installURL.getProtocol())) { //$NON-NLS-1$
			File installDir = new File(installURL.getFile());
			File defaultConfigDir = new File(installDir, CONFIG_DIR);
			if (!defaultConfigDir.exists())
				defaultConfigDir.mkdirs();
			if (defaultConfigDir.exists() && AdaptorUtil.canWrite(defaultConfigDir))
				return defaultConfigDir.getAbsolutePath();
		}
		// We can't write in the eclipse install dir so try for some place in the user's home dir
		return computeDefaultUserAreaLocation(CONFIG_DIR);
	}

	private static boolean canWrite(URL location) {
		if (location != null && "file".equals(location.getProtocol())) { //$NON-NLS-1$
			File locationDir = new File(location.getFile());
			if (!locationDir.exists())
				locationDir.mkdirs();
			if (locationDir.exists() && AdaptorUtil.canWrite(locationDir))
				return true;
		}
		return false;
	}

	private static String computeDefaultUserAreaLocation(String pathAppendage) {
		//    we store the state in <user.home>/.eclipse/<application-id>_<version> where <user.home> 
		//    is unique for each local user, and <application-id> is the one 
		//    defined in .eclipseproduct marker file. If .eclipseproduct does not
		//    exist, use "eclipse" as the application-id.
		String installProperty = FrameworkProperties.getProperty(PROP_INSTALL_AREA);
		URL installURL = buildURL(installProperty, true);
		if (installURL == null)
			return null;
		File installDir = new File(installURL.getFile());
		// compute an install dir hash to prevent configuration area collisions with other eclipse installs
		int hashCode;
		try {
			hashCode = installDir.getCanonicalPath().hashCode();
		} catch (IOException ioe) {
			// fall back to absolute path
			hashCode = installDir.getAbsolutePath().hashCode();
		}
		if (hashCode < 0)
			hashCode = -(hashCode);
		String installDirHash = String.valueOf(hashCode);

		String appName = "." + ECLIPSE; //$NON-NLS-1$
		File eclipseProduct = new File(installDir, PRODUCT_SITE_MARKER);
		if (eclipseProduct.exists()) {
			Properties props = new Properties();
			try {
				props.load(new FileInputStream(eclipseProduct));
				String appId = props.getProperty(PRODUCT_SITE_ID);
				if (appId == null || appId.trim().length() == 0)
					appId = ECLIPSE;
				String appVersion = props.getProperty(PRODUCT_SITE_VERSION);
				if (appVersion == null || appVersion.trim().length() == 0)
					appVersion = ""; //$NON-NLS-1$
				appName += File.separator + appId + "_" + appVersion + "_" + installDirHash; //$NON-NLS-1$ //$NON-NLS-2$
			} catch (IOException e) {
				// Do nothing if we get an exception.  We will default to a standard location 
				// in the user's home dir.
				// add the hash to help prevent collisions
				appName += File.separator + installDirHash;
			}
		} else {
			// add the hash to help prevent collisions
			appName += File.separator + installDirHash;
		}
		String userHome = FrameworkProperties.getProperty(PROP_USER_HOME);
		return new File(userHome, appName + "/" + pathAppendage).getAbsolutePath(); //$NON-NLS-1$
	}

	/**
	 * Returns the user Location object
	 * @return the user Location object
	 */
	public static Location getUserLocation() {
		return userLocation;
	}

	/**
	 * Returns the configuration Location object
	 * @return the configuration Location object
	 */
	public static Location getConfigurationLocation() {
		return configurationLocation;
	}

	/**
	 * Returns the install Location object
	 * @return the install Location object
	 */
	public static Location getInstallLocation() {
		return installLocation;
	}

	/**
	 * Returns the instance Location object
	 * @return the instance Location object
	 */
	public static Location getInstanceLocation() {
		return instanceLocation;
	}

	public static Location getEclipseHomeLocation() {
		return eclipseHomeLocation;
	}

	/**
	 * Returns the File object under the configuration location used for the OSGi configuration
	 * @return the OSGi configuration directory
	 */
	public static File getOSGiConfigurationDir() {
		// TODO assumes the URL is a file: url
		return new File(configurationLocation.getURL().getFile(), FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME);
	}

	/**
	 * Returns a file from the configuration area that can be used by the framework
	 * @param filename the filename
	 * @return a file from the configuration area
	 */
	public static File getConfigurationFile(String filename) {
		File dir = getOSGiConfigurationDir();
		if (!dir.exists())
			dir.mkdirs();
		return new File(dir, filename);
	}
}
