/*******************************************************************************
 * Copyright (c) 2000, 2019 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.debug.internal.ui.views.console;


import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchListener;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.internal.ui.DebugUIPlugin;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.debug.ui.console.ConsoleColorProvider;
import org.eclipse.debug.ui.console.IConsoleColorProvider;
import org.eclipse.debug.ui.console.IConsoleLineTracker;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleManager;

/**
 * Creates documents for processes as they are registered with a launch.
 * The singleton manager is accessible from the debug UI plugin.
 */
public class ProcessConsoleManager implements ILaunchListener {

	/**
	 * Creates console for given process
	 */
	private final class ConsoleCreation extends Job {
		private final ILaunch launch;
		private final IProcess process;

		ConsoleCreation(ILaunch launch, IProcess process) {
			super("Creating console for " + process.getLabel()); //$NON-NLS-1$
			this.launch = launch;
			this.process = process;
			setSystem(true);
			setUser(false);
		}

		@Override
		protected IStatus run(IProgressMonitor monitor) {
			if (monitor.isCanceled() || getConsoleDocument(process) != null) {
				return Status.CANCEL_STATUS;
			}
			IConsoleColorProvider colorProvider = getColorProvider(process.getAttribute(IProcess.ATTR_PROCESS_TYPE));
			String encoding = launch.getAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING);
			ProcessConsole pc = new ProcessConsole(process, colorProvider, encoding);
			pc.setAttribute(IDebugUIConstants.ATTR_CONSOLE_PROCESS, process);

			// add new console to console manager.
			ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { pc });

			// If a launch is removed the associated console is removed too. It can happen
			// that the launch is removed even before the console could be created.
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=546710#c13
			ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
			if (!launchManager.isRegistered(launch)) {
				removeLaunch(launch);
			}
			return Status.OK_STATUS;
		}

		@Override
		public boolean belongsTo(Object family) {
			return process == family || ProcessConsoleManager.class == family;
		}

		@Override
		public boolean shouldSchedule() {
			Job[] jobs = Job.getJobManager().find(process);
			for (Job job : jobs) {
				if (job instanceof ConsoleCreation) {
					return false;
				}
			}
			return true;
		}
	}

	/**
	 * Console document content provider extensions, keyed by extension id
	 */
	private Map<String, IConfigurationElement> fColorProviders;

	/**
	 * The default color provider. Used if no color provider is contributed
	 * for the given process type.
	 */
	private IConsoleColorProvider fDefaultColorProvider;

	/**
	 * Console line trackers; keyed by process type to list of trackers (1:N)
	 */
	private Map<String, List<IConfigurationElement>> fLineTrackers;

	/**
	 * Map of processes for a launch to compute removed processes
	 */
	private Map<ILaunch, IProcess[]> fProcesses;

	/**
	 * Lock for fLineTrackers
	 */
	private final Object fLineTrackersLock = new Object();
	/**
	 * @see ILaunchListener#launchRemoved(ILaunch)
	 */
	@Override
	public void launchRemoved(ILaunch launch) {
		removeLaunch(launch);
	}

	protected void removeLaunch(ILaunch launch) {
		for (IProcess process : launch.getProcesses()) {
			removeProcess(process);
		}
		if (fProcesses != null) {
			fProcesses.remove(launch);
		}
	}

	/**
	 * Removes the console and document associated with the given process.
	 *
	 * @param iProcess process to clean up
	 */
	private void removeProcess(IProcess iProcess) {
		IConsole console = getConsole(iProcess);

		if (console != null) {
			IConsoleManager manager = ConsolePlugin.getDefault().getConsoleManager();
			manager.removeConsoles(new IConsole[]{console});
		}
	}

	/**
	 * Returns the console for the given process, or <code>null</code> if none.
	 *
	 * @param process
	 * @return the console for the given process, or <code>null</code> if none
	 */
	public IConsole getConsole(IProcess process) {
		IConsoleManager manager = ConsolePlugin.getDefault().getConsoleManager();
		for (IConsole console : manager.getConsoles()) {
			if (console instanceof ProcessConsole) {
				ProcessConsole pc = (ProcessConsole)console;
				if (pc.getProcess().equals(process)) {
					return pc;
				}
			}
		}
		return null;
	}

	/**
	 * @see ILaunchListener#launchAdded(ILaunch)
	 */
	@Override
	public void launchAdded(ILaunch launch) {
		launchChanged(launch);
	}

	/**
	 * @see ILaunchListener#launchChanged(ILaunch)
	 */
	@Override
	public void launchChanged(final ILaunch launch) {
		IProcess[] processes= launch.getProcesses();
		for (IProcess process : processes) {
			if (process.getStreamsProxy() == null) {
				continue;
			}
			if (getConsoleDocument(process) == null) {
				// create a new console in a separated thread, see bug 355011.
				Job job = new ConsoleCreation(launch, process);
				job.schedule();
			}
		}
		List<IProcess> removed = getRemovedProcesses(launch);
		if (removed != null) {
			for (IProcess p : removed) {
				removeProcess(p);
			}
		}
	}

	/**
	 * Returns the document for the process, or <code>null</code>
	 * if none.
	 */
	public IDocument getConsoleDocument(IProcess process) {
		ProcessConsole console = (ProcessConsole) getConsole(process);
		return (console != null ? console.getDocument() : null);
	}

	/**
	 * Called by the debug ui plug-in on startup.
	 * The console document manager starts listening for
	 * launches to be registered and initializes if any launches
	 * already exist.
	 */
	public void startup() {
		ILaunchManager launchManager= DebugPlugin.getDefault().getLaunchManager();
		launchManager.addLaunchListener(this);

		//set up the docs for launches already registered
		for (ILaunch launch : launchManager.getLaunches()) {
			launchAdded(launch);
		}
	}

	/**
	 * Called by the debug ui plug-in on shutdown.
	 * The console document manager de-registers as a
	 * launch listener and kills all existing console documents.
	 */
	public void shutdown() {
		Job.getJobManager().cancel(ProcessConsoleManager.class);
		ILaunchManager launchManager= DebugPlugin.getDefault().getLaunchManager();
		for (ILaunch launch : launchManager.getLaunches()) {
			removeLaunch(launch);
		}
		launchManager.removeLaunchListener(this);
		if (fProcesses != null) {
			fProcesses.clear();
		}
	}

	/**
	 * Returns a new console document color provider extension for the given
	 * process type, or <code>null</code> if none.
	 *
	 * @param type corresponds to <code>IProcess.ATTR_PROCESS_TYPE</code>
	 * @return IConsoleColorProvider
	 */
	public IConsoleColorProvider getColorProvider(String type) {
		if (fColorProviders == null) {
			fColorProviders = new HashMap<>();
			IExtensionPoint extensionPoint= Platform.getExtensionRegistry().getExtensionPoint(DebugUIPlugin.getUniqueIdentifier(), IDebugUIConstants.EXTENSION_POINT_CONSOLE_COLOR_PROVIDERS);
			for (IConfigurationElement extension : extensionPoint.getConfigurationElements()) {
				fColorProviders.put(extension.getAttribute("processType"), extension); //$NON-NLS-1$
			}
		}
		IConfigurationElement extension = fColorProviders.get(type);
		if (extension != null) {
			try {
				Object colorProvider = extension.createExecutableExtension("class"); //$NON-NLS-1$
				if (colorProvider instanceof IConsoleColorProvider) {
					return (IConsoleColorProvider)colorProvider;
				}
				DebugUIPlugin.logErrorMessage(MessageFormat.format(
						"Extension {0} must specify an instanceof IConsoleColorProvider for class attribute.", //$NON-NLS-1$
						new Object[] { extension.getDeclaringExtension().getUniqueIdentifier() }));
			} catch (CoreException e) {
				DebugUIPlugin.log(e);
			}
		}
		//no color provider found of specified type, return default color provider.
		if (fDefaultColorProvider == null) {
			fDefaultColorProvider = new ConsoleColorProvider();
		}
		return fDefaultColorProvider;
	}

	/**
	 * Returns the Line Trackers for a given process type.
	 * @param process The process for which line trackers are required.
	 * @return An array of line trackers which match the given process type.
	 */
	public IConsoleLineTracker[] getLineTrackers(IProcess process) {
		String type = process.getAttribute(IProcess.ATTR_PROCESS_TYPE);

		if (fLineTrackers == null) {
			synchronized (fLineTrackersLock) { // can't use fLineTrackers as lock as it is null here
				fLineTrackers = new HashMap<>();
				IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(DebugUIPlugin.getUniqueIdentifier(), IDebugUIConstants.EXTENSION_POINT_CONSOLE_LINE_TRACKERS);
				for (IConfigurationElement extension : extensionPoint.getConfigurationElements()) {
					String processType = extension.getAttribute("processType"); //$NON-NLS-1$
					List<IConfigurationElement> list = fLineTrackers.get(processType);
					if (list == null) {
						list = new ArrayList<>();
						fLineTrackers.put(processType, list);
					}
					list.add(extension);
				}
			}
		}

		ArrayList<IConsoleLineTracker> trackers = new ArrayList<>();
		if (type != null) {
			List<IConfigurationElement> lineTrackerExtensions;
			synchronized (fLineTrackers) {// need to synchronize as the update to list might be still happening
				lineTrackerExtensions = fLineTrackers.get(type);
			}
			if(lineTrackerExtensions != null) {
				for (IConfigurationElement element : lineTrackerExtensions) {
					try {
						trackers.add((IConsoleLineTracker) element.createExecutableExtension("class")); //$NON-NLS-1$
					} catch (CoreException e) {
						DebugUIPlugin.log(e);
					}
				}
			}
		}
		return trackers.toArray(new IConsoleLineTracker[0]);
	}

	/**
	 * Returns the processes that have been removed from the given
	 * launch, or <code>null</code> if none.
	 *
	 * @param launch launch that has changed
	 * @return removed processes or <code>null</code>
	 */
	private List<IProcess> getRemovedProcesses(ILaunch launch) {
		List<IProcess> removed = null;
		if (fProcesses == null) {
			fProcesses = new HashMap<>();
		}
		IProcess[] old = fProcesses.get(launch);
		IProcess[] curr = launch.getProcesses();
		if (old != null) {
			for (IProcess process : old) {
				if (!contains(curr, process)) {
					if (removed == null) {
						removed = new ArrayList<>();
					}
					removed.add(process);
				}
			}
		}
		// update cache with current processes
		fProcesses.put(launch, curr);
		return removed;
	}

	/**
	 * Returns whether the given object is contained in the list.
	 *
	 * @param list list to search
	 * @param object object to search for
	 * @return whether the given object is contained in the list
	 */
	private boolean contains(Object[] list, Object object) {
		for (Object object2 : list) {
			if (object2.equals(object)) {
				return true;
			}
		}
		return false;
	}
}
