/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.core.internal.preferences;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.osgi.service.prefs.PreferencesService;

/**
 * <p>
 * Implements OSGi PreferencesService using the Eclipse preference system.
 * </p>
 *
 * <p>
 * Note: Eclipse preferences are accessible through the OSGi Preferences API and vice
 *  versa.
 * </p>
 */
public class OSGiPreferencesServiceImpl implements PreferencesService {

	/**
	 * Adaptor that implements OSGi Preferences interface on top of EclipsePreferences.
	 * Creates a "local root" since OSGi preferences have lots of roots but eclipse
	 * only has one.
	 */
	private static final class OSGiLocalRootPreferences implements Preferences {

		//The "local" root of this preference tree (not the real Eclipse root)
		private Preferences root;

		//the node this node is wrappering
		private Preferences wrapped;

		private OSGiLocalRootPreferences(Preferences root) {
			this(root, root);
		}

		private OSGiLocalRootPreferences(Preferences wrapped, Preferences root) {
			this.root = root;
			this.wrapped = wrapped;
		}

		/**
		 * If pathName is absolute make it "absolute" with respect to this root.
		 * If pathName is relative, just return it
		 */
		private String fixPath(String pathName) {
			if (pathName.startsWith("/")) { //$NON-NLS-1$
				if (pathName.equals("/")) { //$NON-NLS-1$
					return root.absolutePath();
				}
				//fix absolute path
				return root.absolutePath().concat(pathName);
			}
			//pass-through relative path
			return pathName;
		}

		/**
		 * Override node(String pathName) to be more strict about forbidden names -
		 * EclipsePreferences implementation does a best-effort instead of throwing
		 * {@link IllegalArgumentException}.
		 */
		@Override
		public Preferences node(String pathName) {
			pathName = fixPath(pathName);

			if ((pathName.length() > 1 && pathName.endsWith("/")) //$NON-NLS-1$
					|| pathName.indexOf("//") != -1) { //$NON-NLS-1$
				throw new IllegalArgumentException();
			}
			return new OSGiLocalRootPreferences(wrapped.node(pathName), root);
		}

		/**
		 * <p>
		 * Override getByteArray(String key, byte [] defaultValue) to be more strict when
		 * decoding byte values.  EclipsePreferences implementation pads bytes if they are not 4
		 * bytes long, but the OSGi TCK expects this function to return null if the length of
		 * the byte array is not an even multiple of 4.
		 * </p>
		 * <p>
		 * Also catches any decoding exceptions and returns the default value instead of
		 * propagating the exception.
		 * </p>
		 */
		@Override
		public byte[] getByteArray(String key, byte[] defaultValue) {
			String value = wrapped.get(key, null);
			byte[] byteArray = null;
			if (value != null) {
				byte[] encodedBytes = value.getBytes();
				if (encodedBytes.length % 4 == 0) {
					try {
						byteArray = Base64.decode(encodedBytes);
					} catch (Exception e) {
						//do not raise exception - return defaultValue
					}
				}
			}
			return byteArray == null ? defaultValue : byteArray;
		}

		@Override
		public Preferences parent() {
			if (wrapped == root) {
				try {
					if (!wrapped.nodeExists("")) { //$NON-NLS-1$
						throw new IllegalStateException();
					}
				} catch (BackingStoreException e) {
					//best effort
				}
				return null;
			}
			return new OSGiLocalRootPreferences(wrapped.parent(), root);
		}

		@Override
		public boolean nodeExists(String pathName) throws BackingStoreException {
			return wrapped.nodeExists(fixPath(pathName));
		}

		@Override
		public String absolutePath() {
			if (wrapped == root) {
				return "/"; //$NON-NLS-1$
			}
			return wrapped.absolutePath().substring(root.absolutePath().length(), wrapped.absolutePath().length());
		}

		@Override
		public String name() {
			if (wrapped == root) {
				return ""; //$NON-NLS-1$
			}
			return wrapped.name();
		}

		//delegate to wrapped preference
		@Override
		public void put(String key, String value) {
			wrapped.put(key, value);
		}

		@Override
		public String get(String key, String def) {
			return wrapped.get(key, def);
		}

		@Override
		public void remove(String key) {
			wrapped.remove(key);
		}

		@Override
		public void clear() throws BackingStoreException {
			wrapped.clear();
		}

		@Override
		public void putInt(String key, int value) {
			wrapped.putInt(key, value);
		}

		@Override
		public int getInt(String key, int def) {
			return wrapped.getInt(key, def);
		}

		@Override
		public void putLong(String key, long value) {
			wrapped.putLong(key, value);
		}

		@Override
		public long getLong(String key, long def) {
			return wrapped.getLong(key, def);
		}

		@Override
		public void putBoolean(String key, boolean value) {
			wrapped.putBoolean(key, value);
		}

		@Override
		public boolean getBoolean(String key, boolean def) {
			return wrapped.getBoolean(key, def);
		}

		@Override
		public void putFloat(String key, float value) {
			wrapped.putFloat(key, value);
		}

		@Override
		public float getFloat(String key, float def) {
			return wrapped.getFloat(key, def);
		}

		@Override
		public void putDouble(String key, double value) {
			wrapped.putDouble(key, value);
		}

		@Override
		public double getDouble(String key, double def) {
			return wrapped.getDouble(key, def);
		}

		@Override
		public void putByteArray(String key, byte[] value) {
			wrapped.putByteArray(key, value);
		}

		@Override
		public String[] keys() throws BackingStoreException {
			return wrapped.keys();
		}

		@Override
		public String[] childrenNames() throws BackingStoreException {
			return wrapped.childrenNames();
		}

		@Override
		public void removeNode() throws BackingStoreException {
			wrapped.removeNode();
		}

		@Override
		public void flush() throws BackingStoreException {
			wrapped.flush();
		}

		@Override
		public void sync() throws BackingStoreException {
			wrapped.sync();
		}

	} //end static inner class OSGiLocalRootPreferences

	private IEclipsePreferences bundlePreferences;

	OSGiPreferencesServiceImpl(IEclipsePreferences bundlePreferences) {
		this.bundlePreferences = bundlePreferences;
	}

	@Override
	public Preferences getSystemPreferences() {
		return new OSGiLocalRootPreferences(bundlePreferences.node("system")); //$NON-NLS-1$
	}

	@Override
	public Preferences getUserPreferences(String name) {
		return new OSGiLocalRootPreferences(bundlePreferences.node("user/" + name)); //$NON-NLS-1$
	}

	@Override
	public String[] getUsers() {
		String[] users = null;
		try {
			users = bundlePreferences.node("user").childrenNames(); //$NON-NLS-1$
		} catch (BackingStoreException e) {
			//best effort
		}
		return users == null ? new String[0] : users;
	}

}
