/*******************************************************************************
 * 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
 *     Gunnar Wagenknecht - Bug 179695 - [prefs] NPE when using Preferences API without a product
 *     Thirumala Reddy Mutchukota, Google Inc - Bug 380859 - [prefs] Inconsistency between DefaultPreferences and InstancePreferences
 *******************************************************************************/
package org.eclipse.core.internal.preferences;

import java.io.*;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.*;
import org.eclipse.core.internal.preferences.exchange.IProductPreferencesService;
import org.eclipse.core.internal.runtime.RuntimeLog;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.preferences.BundleDefaultsScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.osgi.util.tracker.ServiceTracker;

/**
 * @since 3.0
 */
public class DefaultPreferences extends EclipsePreferences {
	// cache which nodes have been loaded from disk
	private static Set<String> loadedNodes = Collections.synchronizedSet(new HashSet<String>());
	private static final String KEY_PREFIX = "%"; //$NON-NLS-1$
	private static final String KEY_DOUBLE_PREFIX = "%%"; //$NON-NLS-1$
	private static final IPath NL_DIR = new Path("$nl$"); //$NON-NLS-1$

	private static final String PROPERTIES_FILE_EXTENSION = "properties"; //$NON-NLS-1$
	private static Properties productCustomization;
	private static Properties productTranslation;
	private static Properties commandLineCustomization;
	private EclipsePreferences loadLevel;
	private Thread initializingThread;

	// cached values
	private String qualifier;
	private int segmentCount;
	private WeakReference<Object> pluginReference;

	public static String pluginCustomizationFile = null;

	/**
	 * Default constructor for this class.
	 */
	public DefaultPreferences() {
		this(null, null);
	}

	private DefaultPreferences(EclipsePreferences parent, String name, Object context) {
		this(parent, name);
		this.pluginReference = new WeakReference<>(context);
	}

	private DefaultPreferences(EclipsePreferences parent, String name) {
		super(parent, name);

		if (parent instanceof DefaultPreferences)
			this.pluginReference = ((DefaultPreferences) parent).pluginReference;

		// cache the segment count
		String path = absolutePath();
		segmentCount = getSegmentCount(path);
		if (segmentCount < 2)
			return;

		// cache the qualifier
		qualifier = getSegment(path, 1);
	}

	/*
	 * Apply the values set in the bundle's install directory.
	 *
	 * In Eclipse 2.1 this is equivalent to:
	 *		/eclipse/plugins/<pluginID>/prefs.ini
	 */
	private void applyBundleDefaults() {
		Bundle bundle = PreferencesOSGiUtils.getDefault().getBundle(name());
		if (bundle == null)
			return;
		URL url = FileLocator.find(bundle, new Path(IPreferencesConstants.PREFERENCES_DEFAULT_OVERRIDE_FILE_NAME), null);
		if (url == null) {
			if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL)
				PrefsMessages.message("Preference default override file not found for bundle: " + bundle.getSymbolicName()); //$NON-NLS-1$
			return;
		}
		URL transURL = FileLocator.find(bundle, NL_DIR.append(IPreferencesConstants.PREFERENCES_DEFAULT_OVERRIDE_BASE_NAME).addFileExtension(PROPERTIES_FILE_EXTENSION), null);
		if (transURL == null && EclipsePreferences.DEBUG_PREFERENCE_GENERAL)
			PrefsMessages.message("Preference translation file not found for bundle: " + bundle.getSymbolicName()); //$NON-NLS-1$
		applyDefaults(name(), loadProperties(url), loadProperties(transURL));
	}

	/*
	 * Apply the default values as specified in the file
	 * as an argument on the command-line.
	 */
	private void applyCommandLineDefaults() {
		if (commandLineCustomization != null)
			applyDefaults(null, commandLineCustomization, null);
	}

	/*
	 * If the qualifier is null then the file is of the format:
	 * 	pluginID/key=value
	 * otherwise the file is of the format:
	 * 	key=value
	 */
	private void applyDefaults(String id, Properties defaultValues, Properties translations) {
		for (Enumeration<?> e = defaultValues.keys(); e.hasMoreElements();) {
			String fullKey = (String) e.nextElement();
			String value = defaultValues.getProperty(fullKey);
			if (value == null)
				continue;
			String localQualifier = id;
			String fullPath = fullKey;
			int firstIndex = fullKey.indexOf(PATH_SEPARATOR);
			if (id == null && firstIndex > 0) {
				localQualifier = fullKey.substring(0, firstIndex);
				fullPath = fullKey.substring(firstIndex, fullKey.length());
			}
			String[] splitPath = decodePath(fullPath);
			String childPath = splitPath[0];
			childPath = makeRelative(childPath);
			String key = splitPath[1];
			if (name().equals(localQualifier)) {
				value = translatePreference(value, translations);
				if (EclipsePreferences.DEBUG_PREFERENCE_SET)
					PrefsMessages.message("Setting default preference: " + (new Path(absolutePath()).append(childPath).append(key)) + '=' + value); //$NON-NLS-1$
				((EclipsePreferences) internalNode(childPath.toString(), false, null)).internalPut(key, value);
			}
		}
	}

	public IEclipsePreferences node(String childName, Object context) {
		return internalNode(childName, true, context);
	}

	private boolean containsNode(Properties props, IPath path) {
		if (props == null)
			return false;
		for (Enumeration<?> e = props.keys(); e.hasMoreElements();) {
			String fullKey = (String) e.nextElement();
			if (props.getProperty(fullKey) == null)
				continue;
			// remove last segment which stands for key
			IPath nodePath = new Path(fullKey).removeLastSegments(1);
			if (path.isPrefixOf(nodePath))
				return true;
		}
		return false;
	}

	@Override
	public boolean nodeExists(String path) throws BackingStoreException {
		// use super implementation for empty and absolute paths
		if (path.length() == 0 || path.charAt(0) == IPath.SEPARATOR)
			return super.nodeExists(path);
		// if the node already exists, nothing more to do
		if (super.nodeExists(path))
			return true;
		// if the node does not exist, maybe it has not been loaded yet
		initializeCustomizations();
		// scope based path is a path relative to the "/default" node; this is the path that appears in customizations
		IPath scopeBasedPath = new Path(absolutePath() + PATH_SEPARATOR + path).removeFirstSegments(1);
		return containsNode(productCustomization, scopeBasedPath) || containsNode(commandLineCustomization, scopeBasedPath);
	}

	private void initializeCustomizations() {
		// prime the cache the first time
		if (productCustomization == null) {
			BundleContext context = Activator.getContext();
			if (context != null) {
				ServiceTracker<?, IProductPreferencesService> productTracker = new ServiceTracker<>(context, IProductPreferencesService.class, null);
				productTracker.open();
				IProductPreferencesService productSpecials = productTracker.getService();
				if (productSpecials != null) {
					productCustomization = productSpecials.getProductCustomization();
					productTranslation = productSpecials.getProductTranslation();
				}
				productTracker.close();
			} else {
				PrefsMessages.message("Product-specified preferences called before plugin is started"); //$NON-NLS-1$
			}
			if (productCustomization == null)
				productCustomization = new Properties();
		}
		if (commandLineCustomization == null) {
			String filename = pluginCustomizationFile;
			if (filename == null) {
				if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL)
					PrefsMessages.message("Command-line preferences customization file not specified."); //$NON-NLS-1$
			} else {
				if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL)
					PrefsMessages.message("Using command-line preference customization file: " + filename); //$NON-NLS-1$
				commandLineCustomization = loadProperties(filename);
			}
		}
	}

	/*
	 * Runtime defaults are the ones which are specified in code at runtime.
	 *
	 * In the Eclipse 2.1 world they were the ones which were specified in the
	 * over-ridden Plugin#initializeDefaultPluginPreferences() method.
	 *
	 * In Eclipse 3.0 they are set in the code which is indicated by the
	 * extension to the plug-in default customizer extension point.
	 */
	private void applyRuntimeDefaults() {
		WeakReference<Object> ref = PreferencesService.getDefault().applyRuntimeDefaults(name(), pluginReference);
		if (ref != null)
			pluginReference = ref;
	}

	/*
	 * Apply the default values as specified by the file
	 * in the product extension.
	 *
	 * In Eclipse 2.1 this is equivalent to the plugin_customization.ini
	 * file in the primary feature's plug-in directory.
	 */
	private void applyProductDefaults() {
		if (!productCustomization.isEmpty())
			applyDefaults(null, productCustomization, productTranslation);
	}


	@Override
	public void flush() {
		// default values are not persisted
	}

	@Override
	protected IEclipsePreferences getLoadLevel() {
		if (loadLevel == null) {
			if (qualifier == null)
				return null;
			// Make it relative to this node rather than navigating to it from the root.
			// Walk backwards up the tree starting at this node.
			// This is important to avoid a chicken/egg thing on startup.
			EclipsePreferences node = this;
			for (int i = 2; i < segmentCount; i++)
				node = (EclipsePreferences) node.parent();
			loadLevel = node;
		}
		return loadLevel;
	}

	@Override
	protected EclipsePreferences internalCreate(EclipsePreferences nodeParent, String nodeName, Object context) {
		return new DefaultPreferences(nodeParent, nodeName, context);
	}

	@Override
	protected boolean isAlreadyLoaded(IEclipsePreferences node) {
		return loadedNodes.contains(node.name());
	}


	@Override
	protected void load() {
		setInitializingBundleDefaults();
		try {
			applyRuntimeDefaults();
			applyBundleDefaults();
		} finally {
			clearInitializingBundleDefaults();
		}
		initializeCustomizations();
		applyProductDefaults();
		applyCommandLineDefaults();
	}


	@Override
	protected String internalPut(String key, String newValue) {
		// set the value in this node
		String result = super.internalPut(key, newValue);

		// if we are setting the bundle defaults, then set the corresponding value in
		// the bundle_defaults scope
		if (isInitializingBundleDefaults()) {
			String relativePath = getScopeRelativePath(absolutePath());
			if (relativePath != null) {
				Preferences node = PreferencesService.getDefault().getRootNode().node(BundleDefaultsScope.SCOPE).node(relativePath);
				node.put(key, newValue);
			}
		}
		return result;
	}

	/*
	 * Set that we are in the middle of initializing the bundle defaults.
	 * This is stored on the load level so we know where to look when
	 * we are setting values on sub-nodes.
	 */
	private void setInitializingBundleDefaults() {
		IEclipsePreferences node = getLoadLevel();
		if (node instanceof DefaultPreferences) {
			DefaultPreferences loader = (DefaultPreferences) node;
			loader.initializingThread = Thread.currentThread();
		}
	}

	/*
	 * Clear the bit saying we are in the middle of initializing the bundle defaults.
	 * This is stored on the load level so we know where to look when
	 * we are setting values on sub-nodes.
	 */
	private void clearInitializingBundleDefaults() {
		IEclipsePreferences node = getLoadLevel();
		if (node instanceof DefaultPreferences) {
			DefaultPreferences loader = (DefaultPreferences) node;
			loader.initializingThread = null;
		}
	}

	/*
	 * Are we in the middle of initializing defaults from the bundle
	 * initializer or found in the bundle itself? Look on the load level in
	 * case we are in a sub-node.
	 */
	private boolean isInitializingBundleDefaults() {
		IEclipsePreferences node = getLoadLevel();
		if (node instanceof DefaultPreferences) {
			DefaultPreferences loader = (DefaultPreferences) node;
			return loader.initializingThread == Thread.currentThread();
		}
		return false;
	}

	/*
	 * Return a path which is relative to the scope of this node.
	 * e.g. com.example.foo for /instance/com.example.foo
	 */
	protected static String getScopeRelativePath(String absolutePath) {
		// shouldn't happen but handle empty or root
		if (absolutePath.length() < 2)
			return null;
		int index = absolutePath.indexOf('/', 1);
		if (index == -1 || index + 1 >= absolutePath.length())
			return null;
		return absolutePath.substring(index + 1);
	}

	private Properties loadProperties(URL url) {
		Properties result = new Properties();
		if (url == null)
			return result;
		InputStream input = null;
		try {
			input = url.openStream();
			result.load(input);
		} catch (IOException | IllegalArgumentException e) {
			if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) {
				PrefsMessages.message("Problem opening stream to preference customization file: " + url); //$NON-NLS-1$
				e.printStackTrace();
			}
		}
		finally {
			if (input != null)
				try {
					input.close();
				} catch (IOException e) {
					// ignore
				}
		}
		return result;
	}

	private Properties loadProperties(String filename) {
		Properties result = new Properties();
		InputStream input = null;
		try {
			input = new BufferedInputStream(new FileInputStream(filename));
			result.load(input);
		} catch (FileNotFoundException e) {
			if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL)
				PrefsMessages.message("Preference customization file not found: " + filename); //$NON-NLS-1$
		} catch (IOException e) {
			String message = NLS.bind(PrefsMessages.preferences_loadException, filename);
			IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e);
			RuntimeLog.log(status);
		} catch (IllegalArgumentException e) {
			String message = NLS.bind(PrefsMessages.preferences_loadException, filename);
			IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e);
			RuntimeLog.log(status);
		} finally {
			if (input != null)
				try {
					input.close();
				} catch (IOException e) {
					// ignore
				}
		}
		return result;
	}

	@Override
	protected void loaded() {
		loadedNodes.add(name());
	}


	@Override
	public void sync() {
		// default values are not persisted
	}

	/**
	 * Takes a preference value and a related resource bundle and
	 * returns the translated version of this value (if one exists).
	 */
	private String translatePreference(String origValue, Properties props) {
		if (props == null || origValue.startsWith(KEY_DOUBLE_PREFIX))
			return origValue;
		if (origValue.startsWith(KEY_PREFIX)) {
			String value = origValue.trim();
			int ix = value.indexOf(" "); //$NON-NLS-1$
			String key = ix == -1 ? value.substring(1) : value.substring(1, ix);
			String dflt = ix == -1 ? value : value.substring(ix + 1);
			return props.getProperty(key, dflt);
		}
		return origValue;
	}
}
