/*******************************************************************************
 * Copyright (c) 2005, 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
 *     Josh Arnold - Bug 180080 Equinox Application Admin spec violations
 *******************************************************************************/

package org.eclipse.equinox.internal.app;

import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.osgi.service.runnable.ApplicationRunnable;
import org.eclipse.osgi.service.runnable.StartupMonitor;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;
import org.osgi.service.application.ApplicationException;
import org.osgi.service.application.ApplicationHandle;

/*
 * An ApplicationHandle that represents a single instance of a running eclipse application.
 */
public class EclipseAppHandle extends ApplicationHandle implements ApplicationRunnable, IApplicationContext {
	// Indicates the application is starting
	private static final int FLAG_STARTING = 0x01;
	// Indicates the application is active
	private static final int FLAG_ACTIVE = 0x02;
	// Indicates the application is stopping
	private static final int FLAG_STOPPING = 0x04;
	// Indicates the application is stopped
	private static final int FLAG_STOPPED = 0x08;
	private static final String STARTING = "org.eclipse.equinox.app.starting"; //$NON-NLS-1$
	private static final String STOPPED = "org.eclipse.equinox.app.stopped"; //$NON-NLS-1$
	private static final String PROP_ECLIPSE_EXITCODE = "eclipse.exitcode"; //$NON-NLS-1$
	private static final Object NULL_RESULT = new Object();

	private volatile ServiceRegistration handleRegistration;
	private int status = EclipseAppHandle.FLAG_STARTING;
	private final Map<String, Object> arguments;
	private Object application;
	private final Boolean defaultAppInstance;
	private Object result;
	private boolean setResult = false;
	private boolean setAsyncResult = false;
	private final boolean[] registrationLock = new boolean[] {true};

	/*
	 * Constructs a handle for a single running instance of a eclipse application.
	 */
	EclipseAppHandle(String instanceId, Map<String, Object> arguments, EclipseAppDescriptor descriptor) {
		super(instanceId, descriptor);
		defaultAppInstance = arguments == null || arguments.get(EclipseAppDescriptor.APP_DEFAULT) == null ? Boolean.FALSE : (Boolean) arguments.remove(EclipseAppDescriptor.APP_DEFAULT);
		if (arguments == null)
			this.arguments = new HashMap<>(2);
		else
			this.arguments = new HashMap<>(arguments);
	}

	@Override
	synchronized public String getState() {
		switch (status) {
			case FLAG_STARTING :
				return STARTING;
			case FLAG_ACTIVE :
				return ApplicationHandle.RUNNING;
			case FLAG_STOPPING :
				return ApplicationHandle.STOPPING;
			case FLAG_STOPPED :
			default :
				// must only check this if the status is STOPPED; otherwise we throw exceptions before we have set the registration.
				if (getServiceRegistration() == null)
					throw new IllegalStateException(NLS.bind(Messages.application_error_state_stopped, getInstanceId()));
				return STOPPED;
		}
	}

	@Override
	protected void destroySpecific() {
		// when this method is called we must force the application to exit.
		// first set the status to stopping
		setAppStatus(EclipseAppHandle.FLAG_STOPPING);
		// now force the application to stop
		IApplication app = getApplication();
		if (app != null)
			app.stop();
		// make sure the app status is stopped
		setAppStatus(EclipseAppHandle.FLAG_STOPPED);
	}

	void setServiceRegistration(ServiceRegistration sr) {
		synchronized (registrationLock) {
			this.handleRegistration = sr;
			registrationLock[0] = sr != null;
			registrationLock.notifyAll();
		}
	}

	private ServiceRegistration getServiceRegistration() {
		synchronized (registrationLock) {
			if (handleRegistration == null && registrationLock[0]) {
				try {
					registrationLock.wait(1000); // timeout after 1 second
				} catch (InterruptedException e) {
					// nothing
				}
			}
			return handleRegistration;
		}
	}

	ServiceReference getServiceReference() {
		ServiceRegistration reg = getServiceRegistration();
		if (reg == null)
			return null;
		try {
			return reg.getReference();
		} catch (IllegalStateException e) {
			return null; // this will happen if the service has been unregistered already
		}
	}

	/*
	 * Gets a snapshot of the current service properties.
	 */
	Dictionary<String, Object> getServiceProperties() {
		Dictionary<String, Object> props = new Hashtable<>(6);
		props.put(ApplicationHandle.APPLICATION_PID, getInstanceId());
		props.put(ApplicationHandle.APPLICATION_STATE, getState());
		props.put(ApplicationHandle.APPLICATION_DESCRIPTOR, getApplicationDescriptor().getApplicationId());
		props.put(EclipseAppDescriptor.APP_TYPE, ((EclipseAppDescriptor) getApplicationDescriptor()).getThreadTypeString());
		props.put(ApplicationHandle.APPLICATION_SUPPORTS_EXITVALUE, Boolean.TRUE);
		if (defaultAppInstance.booleanValue())
			props.put(EclipseAppDescriptor.APP_DEFAULT, defaultAppInstance);
		return props;
	}

	/*
	 * Changes the status of this handle.  This method will properly transition
	 * the state of this handle and will update the service registration accordingly.
	 */
	private synchronized void setAppStatus(int status) {
		if (this.status == status)
			return;
		if ((status & EclipseAppHandle.FLAG_STARTING) != 0)
			throw new IllegalArgumentException("Cannot set app status to starting"); //$NON-NLS-1$
		// if status is stopping and the context is already stopping then return
		if ((status & EclipseAppHandle.FLAG_STOPPING) != 0)
			if ((this.status & (EclipseAppHandle.FLAG_STOPPING | EclipseAppHandle.FLAG_STOPPED)) != 0)
				return;
		// change the service properties to reflect the state change.
		this.status = status;
		ServiceRegistration handleReg = getServiceRegistration();
		if (handleReg == null)
			return;
		handleReg.setProperties(getServiceProperties());
		// if the status is stopped then unregister the service
		if ((this.status & EclipseAppHandle.FLAG_STOPPED) != 0) {
			((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().unlock(this);
			handleReg.unregister();
			setServiceRegistration(null);
		}
	}

	@Override
	public Map getArguments() {
		return arguments;
	}

	@Override
	public Object run(Object context) throws Exception {
		if (context != null) {
			// always force the use of the context if it is not null
			arguments.put(IApplicationContext.APPLICATION_ARGS, context);
		} else {
			// get the context from the arguments
			context = arguments.get(IApplicationContext.APPLICATION_ARGS);
			if (context == null) {
				// if context is null then use the args from CommandLineArgs
				context = CommandLineArgs.getApplicationArgs();
				arguments.put(IApplicationContext.APPLICATION_ARGS, context);
			}
		}
		Object tempResult = null;
		try {
			Object app;
			synchronized (this) {
				if ((status & (EclipseAppHandle.FLAG_STARTING | EclipseAppHandle.FLAG_STOPPING)) == 0)
					throw new ApplicationException(ApplicationException.APPLICATION_INTERNAL_ERROR, NLS.bind(Messages.application_instance_stopped, getInstanceId()));
				application = getConfiguration().createExecutableExtension("run"); //$NON-NLS-1$
				app = application;
				notifyAll();
			}
			if (app instanceof IApplication)
				tempResult = ((IApplication) app).start(this);
			else
				tempResult = EclipseAppContainer.callMethodWithException(app, "run", new Class[] {Object.class}, new Object[] {context}); //$NON-NLS-1$
			if (tempResult == null)
				tempResult = NULL_RESULT;
		} finally {
			tempResult = setInternalResult(tempResult, false, null);
		}

		if (Activator.DEBUG)
			System.out.println(NLS.bind(Messages.application_returned, (new String[] {getApplicationDescriptor().getApplicationId(), tempResult == null ? "null" : tempResult.toString()}))); //$NON-NLS-1$
		return tempResult;
	}

	private synchronized Object setInternalResult(Object result, boolean isAsync, IApplication tokenApp) {
		if (setResult)
			throw new IllegalStateException("The result of the application is already set."); //$NON-NLS-1$
		if (isAsync) {
			if (!setAsyncResult)
				throw new IllegalStateException("The application must return IApplicationContext.EXIT_ASYNC_RESULT to set asynchronous results."); //$NON-NLS-1$
			if (application != tokenApp)
				throw new IllegalArgumentException("The application is not the correct instance for this application context."); //$NON-NLS-1$
		} else {
			if (result == IApplicationContext.EXIT_ASYNC_RESULT) {
				setAsyncResult = true;
				return NULL_RESULT; // the result well be set with setResult
			}
		}
		this.result = result;
		setResult = true;
		application = null;
		notifyAll();
		// The application exited itself; notify the app context
		setAppStatus(EclipseAppHandle.FLAG_STOPPING); // must ensure the STOPPING event is fired
		setAppStatus(EclipseAppHandle.FLAG_STOPPED);
		// only set the exit code property if this is the default application
		// (bug 321386) only set the exit code if the result != null; when result == null we assume an exception was thrown
		if (isDefault() && result != null) {
			int exitCode = result instanceof Integer ? ((Integer) result).intValue() : 0;
			// Use the EnvironmentInfo Service to set properties
			Activator.setProperty(PROP_ECLIPSE_EXITCODE, Integer.toString(exitCode));
		}
		return result;
	}

	@Override
	public void stop() {
		try {
			destroy();
		} catch (IllegalStateException e) {
			// Do nothing; we don't care that the application was already stopped
			// return with no error
		}

	}

	@Override
	public void applicationRunning() {
		// first set the application handle status to running
		setAppStatus(EclipseAppHandle.FLAG_ACTIVE);
		// now run the splash handler
		final ServiceReference[] monitors = getStartupMonitors();
		if (monitors == null)
			return;
		SafeRunner.run(new ISafeRunnable() {
			@Override
			public void handleException(Throwable e) {
				// just continue ... the exception has already been logged by
				// handleException(ISafeRunnable)
			}

			@Override
			public void run() throws Exception {
				for (ServiceReference m : monitors) {
					StartupMonitor monitor = (StartupMonitor) Activator.getContext().getService(m);
					if (monitor != null) {
						monitor.applicationRunning();
						Activator.getContext().ungetService(m);
					}
				}
			}
		});
	}

	private ServiceReference[] getStartupMonitors() {
		// assumes theStartupMonitor is available as a service
		// see EclipseStarter.publishSplashScreen
		ServiceReference[] refs = null;
		try {
			refs = Activator.getContext().getServiceReferences(StartupMonitor.class.getName(), null);
		} catch (InvalidSyntaxException e) {
			// ignore; this cannot happen
		}
		if (refs == null || refs.length == 0)
			return null;
		// Implement our own Comparator to sort services
		Arrays.sort(refs, new Comparator<ServiceReference>() {
			@Override
			public int compare(ServiceReference ref1, ServiceReference ref2) {
				// sort in descending order
				// sort based on service ranking first; highest rank wins
				Object property = ref1.getProperty(Constants.SERVICE_RANKING);
				int rank1 = (property instanceof Integer) ? ((Integer) property).intValue() : 0;
				property = ref2.getProperty(Constants.SERVICE_RANKING);
				int rank2 = (property instanceof Integer) ? ((Integer) property).intValue() : 0;
				if (rank1 != rank2)
					return rank1 > rank2 ? -1 : 1;
				// rankings are equal; sort by id, lowest id wins
				long id1 = ((Long) (ref1.getProperty(Constants.SERVICE_ID))).longValue();
				long id2 = ((Long) (ref2.getProperty(Constants.SERVICE_ID))).longValue();
				return id2 > id1 ? -1 : 1;
			}
		});
		return refs;
	}

	private synchronized IApplication getApplication() {
		if (handleRegistration != null && application == null)
			// the handle has been initialized by the container but the launcher has not
			// gotten around to creating the application object and starting it yet.
			try {
				wait(5000); // timeout after a while in case there was an internal error and there will be no application created
			} catch (InterruptedException e) {
				// do nothing
			}
		return (IApplication) ((application instanceof IApplication) ? application : null);
	}

	private IConfigurationElement getConfiguration() {
		IExtension applicationExtension = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getAppExtension(getApplicationDescriptor().getApplicationId());
		if (applicationExtension == null)
			throw new RuntimeException(NLS.bind(Messages.application_notFound, getApplicationDescriptor().getApplicationId(), ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getAvailableAppsMsg()));
		IConfigurationElement[] configs = applicationExtension.getConfigurationElements();
		if (configs.length == 0)
			throw new RuntimeException(NLS.bind(Messages.application_invalidExtension, getApplicationDescriptor().getApplicationId()));
		return configs[0];
	}

	@Override
	public String getBrandingApplication() {
		IBranding branding = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getBranding();
		return branding == null ? null : branding.getApplication();
	}

	@Override
	public Bundle getBrandingBundle() {
		IBranding branding = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getBranding();
		return branding == null ? null : branding.getDefiningBundle();

	}

	@Override
	public String getBrandingDescription() {
		IBranding branding = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getBranding();
		return branding == null ? null : branding.getDescription();

	}

	@Override
	public String getBrandingId() {
		IBranding branding = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getBranding();
		return branding == null ? null : branding.getId();
	}

	@Override
	public String getBrandingName() {
		IBranding branding = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getBranding();
		return branding == null ? null : branding.getName();

	}

	@Override
	public String getBrandingProperty(String key) {
		IBranding branding = ((EclipseAppDescriptor) getApplicationDescriptor()).getContainerManager().getBranding();
		return branding == null ? null : branding.getProperty(key);
	}

	boolean isDefault() {
		return defaultAppInstance.booleanValue();
	}

	public synchronized Object waitForResult(int timeout) {
		try {
			return getExitValue(timeout);
		} catch (ApplicationException | InterruptedException e) {
			// return null
		}
		return null;
	}

	@Override
	public synchronized Object getExitValue(long timeout) throws ApplicationException, InterruptedException {
		if (handleRegistration == null && application == null)
			return result;
		long startTime = System.currentTimeMillis();
		long delay = timeout;
		while (!setResult && (delay > 0 || timeout == 0)) {
			wait(delay); // only wait for the specified amount of time
			if (timeout > 0)
				delay -= (System.currentTimeMillis() - startTime);
		}
		if (result == null)
			throw new ApplicationException(ApplicationException.APPLICATION_EXITVALUE_NOT_AVAILABLE);
		if (result == NULL_RESULT)
			return null;
		return result;
	}

	@Override
	public void setResult(Object result, IApplication application) {
		setInternalResult(result == null ? NULL_RESULT : result, true, application);
	}
}
