/*******************************************************************************
 * Copyright (c) 2007, 2018 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.net;

import java.net.Authenticator;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import org.eclipse.core.net.proxy.IProxyChangeEvent;
import org.eclipse.core.net.proxy.IProxyChangeListener;
import org.eclipse.core.net.proxy.IProxyData;
import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.RegistryFactory;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.osgi.util.NLS;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

public class ProxyManager implements IProxyService, IPreferenceChangeListener {

	static final String PREF_NON_PROXIED_HOSTS = "nonProxiedHosts"; //$NON-NLS-1$
	static final String PREF_ENABLED = "proxiesEnabled"; //$NON-NLS-1$
	static final String PREF_OS = "systemProxiesEnabled"; //$NON-NLS-1$

	private static IProxyService proxyManager;

	private AbstractProxyProvider nativeProxyProvider;

	private PreferenceManager preferenceManager;

	ListenerList<IProxyChangeListener> listeners = new ListenerList<>(ListenerList.IDENTITY);
	private String[] nonProxiedHosts;
	private final ProxyType[] proxies = new ProxyType[] {
			new ProxyType(IProxyData.HTTP_PROXY_TYPE),
			new ProxyType(IProxyData.HTTPS_PROXY_TYPE),
			new ProxyType(IProxyData.SOCKS_PROXY_TYPE)
		};

	private ProxyManager() {
		try {
			nativeProxyProvider = (AbstractProxyProvider) Class.forName(
					"org.eclipse.core.net.ProxyProvider").getDeclaredConstructor().newInstance(); //$NON-NLS-1$
		} catch (ClassNotFoundException e) {
			// no class found
		} catch (Exception e) {
			Activator.logInfo("Problems occured during the proxy provider initialization.", e); //$NON-NLS-1$
		}
		preferenceManager = Activator.getInstance().getPreferenceManager();
	}

	/**
	 * Return the proxy manager.
	 * @return the proxy manager
	 */
	public synchronized static IProxyService getProxyManager() {
		if (proxyManager == null)
			proxyManager = new ProxyManager();
		return proxyManager;
	}

	@Override
	public void addProxyChangeListener(IProxyChangeListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removeProxyChangeListener(IProxyChangeListener listener) {
		listeners.remove(listener);
	}

	private void fireChange(final IProxyChangeEvent event) {
		for (final IProxyChangeListener listener : listeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.proxyInfoChanged(event);
				}
				@Override
				public void handleException(Throwable exception) {
					// Logged by SafeRunner
				}
			});
		}
	}

	@Override
	public synchronized String[] getNonProxiedHosts() {
		checkMigrated();
		if (nonProxiedHosts == null) {
			String prop = preferenceManager.getString(PreferenceManager.ROOT, PREF_NON_PROXIED_HOSTS);
			nonProxiedHosts = ProxyType.convertPropertyStringToHosts(prop);
		}
		if (nonProxiedHosts.length == 0)
			return nonProxiedHosts;
		String[] result = new String[nonProxiedHosts.length];
		System.arraycopy(nonProxiedHosts, 0, result, 0, nonProxiedHosts.length );
		return result;
	}

	public String[] getNativeNonProxiedHosts() {
		if (hasSystemProxies()) {
			return nativeProxyProvider.getNonProxiedHosts();
		}
		return new String[0];
	}

	@Override
	public void setNonProxiedHosts(String[] hosts) {
		checkMigrated();
		Assert.isNotNull(hosts);
		for (int i = 0; i < hosts.length; i++) {
			String host = hosts[i];
			Assert.isNotNull(host);
			Assert.isTrue(host.length() > 0);
		}
		String[] oldHosts = nonProxiedHosts;
		if (Arrays.equals(oldHosts, hosts)) {
			return;
		}
		nonProxiedHosts = hosts;
		preferenceManager.putString(PreferenceManager.ROOT, PREF_NON_PROXIED_HOSTS, ProxyType.convertHostsToPropertyString(nonProxiedHosts));
		try {
			preferenceManager.flush();
		} catch (BackingStoreException e) {
			Activator.logError(
					"An error occurred while writing out the non-proxied hosts list", e); //$NON-NLS-1$
		}
		IProxyData[] data = getProxyData();
		IProxyChangeEvent event = new ProxyChangeEvent(IProxyChangeEvent.NONPROXIED_HOSTS_CHANGED, oldHosts, getNonProxiedHosts(), data, new IProxyData[0]);
		updateSystemProperties();
		fireChange(event);
	}


	@Override
	public IProxyData[] getProxyData() {
		checkMigrated();
		IProxyData[] result = new IProxyData[proxies.length];
		for (int i = 0; i < proxies.length; i++) {
			ProxyType type = proxies[i];
			result[i] = type.getProxyData(ProxyType.VERIFY_EQUAL);
		}
		return resolveType(result);
	}

	public IProxyData[] getNativeProxyData() {
		if (hasSystemProxies()) {
			return resolveType(nativeProxyProvider.getProxyData());
		}
		return new IProxyData[0];
	}

	@Override
	public void setProxyData(IProxyData[] proxies) {
		checkMigrated();
		doSetProxyData(proxies);
	}

	private void doSetProxyData(IProxyData[] proxyDatas) {
		IProxyData[] oldData = getProxyData();
		String[] hosts = getNonProxiedHosts();
		IProxyData[] changedProxies = internalSetProxyData(proxyDatas);
		if (changedProxies.length > 0) {
			IProxyChangeEvent event = new ProxyChangeEvent(IProxyChangeEvent.PROXY_SERVICE_ENABLEMENT_CHANGE, hosts, hosts, oldData, changedProxies);
			fireChange(event);
		}
	}

	private IProxyData[] internalSetProxyData(IProxyData[] proxyDatas) {
		List<IProxyData> result = new ArrayList<>();
		for (int i = 0; i < proxyDatas.length; i++) {
			IProxyData proxyData = proxyDatas[i];
			ProxyType type = getType(proxyData);
			if (type != null && type.setProxyData(proxyData)) {
				result.add(proxyData);
			}
		}
		return result.toArray(new IProxyData[result.size()]);
	}

	private ProxyType getType(IProxyData proxyData) {
		for (int i = 0; i < proxies.length; i++) {
			ProxyType type = proxies[i];
			if (type.getName().equals(proxyData.getType())) {
				return type;
			}
		}
		return null;
	}

	@Override
	public boolean isProxiesEnabled() {
		checkMigrated();
		return internalIsProxiesEnabled()
				&& (!isSystemProxiesEnabled() || (isSystemProxiesEnabled() && hasSystemProxies()));
	}

	private boolean internalIsProxiesEnabled() {
		return preferenceManager.getBoolean(PreferenceManager.ROOT, PREF_ENABLED);
	}

	@Override
	public void setProxiesEnabled(boolean enabled) {
		checkMigrated();
		boolean current = internalIsProxiesEnabled();
		if (current == enabled)
			return;
		// Setting the preference will trigger the system property update
		// (see preferenceChange)
		preferenceManager.putBoolean(PreferenceManager.ROOT, PREF_ENABLED, enabled);
	}

	private void internalSetEnabled(boolean enabled, boolean systemEnabled) {
		Properties sysProps = System.getProperties();
		sysProps.put("proxySet", enabled ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		sysProps.put("systemProxySet", systemEnabled ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		updateSystemProperties();
		try {
			preferenceManager.flush();
		} catch (BackingStoreException e) {
			Activator.logError(
					"An error occurred while writing out the enablement state", e); //$NON-NLS-1$
		}
		String[] hosts = getNonProxiedHosts();
		IProxyData[] data = getProxyData();
		IProxyChangeEvent event = new ProxyChangeEvent(IProxyChangeEvent.PROXY_DATA_CHANGED, hosts, hosts, data, data);
		fireChange(event);
	}

	private void updateSystemProperties() {
		for (int i = 0; i < proxies.length; i++) {
			ProxyType type = proxies[i];
			type.updateSystemProperties(internalGetProxyData(type.getName(), ProxyType.DO_NOT_VERIFY));
		}
	}

	public void initialize() {
		checkMigrated();
		preferenceManager.addPreferenceChangeListener(PreferenceManager.ROOT, this);
		// Now initialize each proxy type
		for (int i = 0; i < proxies.length; i++) {
			ProxyType type = proxies[i];
			type.initialize();
		}
		registerAuthenticator();
	}

	@Override
	public IProxyData getProxyData(String type) {
		checkMigrated();
		return resolveType(internalGetProxyData(type, ProxyType.VERIFY_EQUAL));
	}

	private IProxyData internalGetProxyData(String type, int verifySystemProperties) {
		for (int i = 0; i < proxies.length; i++) {
			ProxyType pt = proxies[i];
			if (pt.getName().equals(type)) {
				return pt.getProxyData(verifySystemProperties);
			}
		}
		return null;
	}

	@Override
	public IProxyData[] getProxyDataForHost(String host) {
		checkMigrated();
		if (!internalIsProxiesEnabled()) {
			return new IProxyData[0];
		}
		URI uri = tryGetURI(host);
		if (uri == null) {
			return new IProxyData[0];
		}
		if (hasSystemProxies() && isSystemProxiesEnabled()) {
			return resolveType(nativeProxyProvider.select(uri));
		}

		if (isHostFiltered(uri))
			return new IProxyData[0];
		IProxyData[] data = getProxyData();
		List<IProxyData> result = new ArrayList<>();
		for (int i = 0; i < data.length; i++) {
			IProxyData proxyData = data[i];
			if (proxyData.getHost() != null)
				result.add(proxyData);
		}
		IProxyData ret[] = result.toArray(new IProxyData[result.size()]);
		return resolveType(ret);
	}

	public static URI tryGetURI(String host) {
		try {
			int i = host.indexOf(":"); //$NON-NLS-1$
			if (i == -1) {
				return new URI("//" + host); //$NON-NLS-1$
			}
			return new URI(host.substring(i + 1));
		} catch (URISyntaxException e) {
			return null;
		}
	}

	private boolean isHostFiltered(URI uri) {
		String[] filters = getNonProxiedHosts();
		for (int i = 0; i < filters.length; i++) {
			String filter = filters[i];
			if (StringUtil.hostMatchesFilter(uri.getHost(), filter))
				return true;
		}
		return false;
	}

	@Override
	public IProxyData getProxyDataForHost(String host, String type) {
		checkMigrated();
		if (!internalIsProxiesEnabled()) {
			return null;
		}
		if (hasSystemProxies() && isSystemProxiesEnabled())
			try {
				URI uri = new URI(type, "//" + host, null); //$NON-NLS-1$
				IProxyData[] proxyDatas = nativeProxyProvider.select(uri);
				return proxyDatas.length > 0 ? resolveType(proxyDatas[0]) : null;
			} catch (URISyntaxException e) {
				return null;
			}

		IProxyData[] data = getProxyDataForHost(host);
		for (int i = 0; i < data.length; i++) {
			IProxyData proxyData = data[i];
			if (proxyData.getType().equalsIgnoreCase(type)
					&& proxyData.getHost() != null)
				return resolveType(proxyData);
		}
		return null;
	}

	private void registerAuthenticator() {
		Authenticator a = getPluggedInAuthenticator();
		if (a != null) {
			Authenticator.setDefault(a);
		}
	}

	private Authenticator getPluggedInAuthenticator() {
		IExtension[] extensions = RegistryFactory.getRegistry().getExtensionPoint(Activator.ID, Activator.PT_AUTHENTICATOR).getExtensions();
		if (extensions.length == 0)
			return null;
		IExtension extension = extensions[0];
		IConfigurationElement[] configs = extension.getConfigurationElements();
		if (configs.length == 0) {
			Activator.log(IStatus.ERROR, NLS.bind("Authenticator {0} is missing required fields", (new Object[] {extension.getUniqueIdentifier()})), null);//$NON-NLS-1$
			return null;
		}
		try {
			IConfigurationElement config = configs[0];
			return (Authenticator) config.createExecutableExtension("class");//$NON-NLS-1$
		} catch (CoreException ex) {
			Activator.log(IStatus.ERROR, NLS.bind("Unable to instantiate authenticator {0}", (new Object[] {extension.getUniqueIdentifier()})), ex);//$NON-NLS-1$
			return null;
		}
	}

	private synchronized void checkMigrated() {
		if (preferenceManager.isMigrated() || !Activator.getInstance().instanceLocationAvailable()) {
			return;
		}
		preferenceManager.migrate(proxies);
	}

	void migrateInstanceScopePreferences(Preferences instance,
			Preferences configuration, boolean isInitialize) {
		preferenceManager.migrateInstanceScopePreferences(instance,
				configuration, proxies, isInitialize);
	}

	@Override
	public void preferenceChange(PreferenceChangeEvent event) {
		if (event.getKey().equals(PREF_ENABLED) || event.getKey().equals(PREF_OS)) {
			checkMigrated();
			internalSetEnabled(preferenceManager.getBoolean(PreferenceManager.ROOT, PREF_ENABLED),
					preferenceManager.getBoolean(PreferenceManager.ROOT, PREF_OS));
		}
	}

	@Override
	public boolean hasSystemProxies() {
		return nativeProxyProvider != null;
	}

	@Override
	public boolean isSystemProxiesEnabled() {
		checkMigrated();
		return preferenceManager.getBoolean(PreferenceManager.ROOT, PREF_OS);
	}

	@Override
	public void setSystemProxiesEnabled(boolean enabled) {
		checkMigrated();
		boolean current = isSystemProxiesEnabled();
		if (current == enabled)
			return;
		// Setting the preference will trigger the system property update
		// (see preferenceChange)
		preferenceManager.putBoolean(PreferenceManager.ROOT, PREF_OS, enabled);
	}

	@Override
	public IProxyData[] select(URI uri) {
		IProxyData data = getProxyDataForHost(uri.getHost(), uri.getScheme());
		if (data != null) {
			return resolveType(new IProxyData[] { data });
		}
		return new IProxyData[0];
	}

	public IProxyData resolveType(IProxyData data) {
		if (data == null) {
			return null;
		}
		ProxyData d = (ProxyData) data;
		if (d.getType().equalsIgnoreCase(IProxyData.HTTP_PROXY_TYPE)) {
			d.setType(IProxyData.HTTP_PROXY_TYPE);
		} else if (d.getType().equalsIgnoreCase(IProxyData.HTTPS_PROXY_TYPE)) {
			d.setType(IProxyData.HTTPS_PROXY_TYPE);
		} else if (d.getType().equalsIgnoreCase(IProxyData.SOCKS_PROXY_TYPE)) {
			d.setType(IProxyData.SOCKS_PROXY_TYPE);
		}
		return d;
	}

	public IProxyData[] resolveType(IProxyData[] data) {
		if (data == null) {
			return null;
		}
		for (int i = 0; i < data.length; i++) {
			resolveType(data[i]);
		}
		return data;
	}

}
