/*******************************************************************************
 * Copyright (c) 2002, 2017 QNX Software Systems 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:
 *     QNX Software Systems - initial API and implementation
 *     Red Hat Inc. - multiple build console support
 *     Dmitry Kozlov (CodeSourcery) - Build error highlighting and navigation
 *     Alex Collins (Broadcom Corp.) - Global build console
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.buildconsole;

import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.cdt.core.resources.IConsole;
import org.eclipse.cdt.internal.core.LocalProjectScope;
import org.eclipse.cdt.internal.ui.preferences.BuildConsolePreferencePage;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.IBuildConsoleEvent;
import org.eclipse.cdt.ui.IBuildConsoleListener;
import org.eclipse.cdt.ui.IBuildConsoleManager;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsoleConstants;
import org.eclipse.ui.console.IConsoleView;
import org.osgi.service.prefs.Preferences;

public class BuildConsoleManager implements IBuildConsoleManager, IResourceChangeListener, IPropertyChangeListener {
	/*package*/ static final String PREF_QUALIFIER = CUIPlugin.PLUGIN_ID;
	public static final String KEY_KEEP_LOG = "keepLog"; //$NON-NLS-1$
	public static final String KEY_LOG_LOCATION = "logLocation"; //$NON-NLS-1$
	public static final boolean CONSOLE_KEEP_LOG_DEFAULT = true;

	private static final String BUILD_CONSOLE_NODE = "buildConsole"; //$NON-NLS-1$
	private static final String PROJECT_LOG_EXT = ".build.log"; //$NON-NLS-1$

	private ListenerList<IBuildConsoleListener> listeners = new ListenerList<>();
	/** UI console object in which per-project consoles are shown */
	private BuildConsole fConsole;
	private Map<IProject, BuildConsolePartitioner> fConsoleMap = new HashMap<>();
	private Color infoColor;
	private Color outputColor;
	private Color errorColor;
	private Color backgroundColor;
	private Color problemHighlightedColor;
	private Color problemErrorBackgroundColor;
	private Color problemInfoBackgroundColor;
	private Color problemWarningBackgroundColor;

	public Color getProblemHighlightedColor() {
		return problemHighlightedColor;
	}

	/**
	 * This function returns background color for errors only now
	 */
	public Color getProblemBackgroundColor() {
		return problemErrorBackgroundColor;
	}

	public Color getWarningBackgroundColor() {
		return problemWarningBackgroundColor;
	}

	public Color getInfoBackgroundColor() {
		return problemInfoBackgroundColor;
	}

	private BuildConsoleStreamDecorator infoStream;
	private BuildConsoleStreamDecorator outputStream;
	private BuildConsoleStreamDecorator errorStream;

	private String fName;
	private String fContextMenuId;

	static public final int BUILD_STREAM_TYPE_INFO = 0;
	static public final int BUILD_STREAM_TYPE_OUTPUT = 1;
	static public final int BUILD_STREAM_TYPE_ERROR = 2;

	static public final String DEFAULT_CONTEXT_MENU_ID = CUIPlugin.PLUGIN_ID + ".CDTBuildConsole"; //$NON-NLS-1$

	private IProject fLastProject;

	/**
	 * Default constructor.
	 */
	public BuildConsoleManager() {
	}

	/**
	 * Notifies the console manager that console activity has started on the
	 * project The manager will open the console if the preference is set to
	 * show the console, and notify listeners
	 */
	protected void startConsoleActivity(IProject project) {
		if (!listeners.isEmpty()) {
			for (IBuildConsoleListener listener : listeners) {
				ConsoleEvent event = new ConsoleEvent(BuildConsoleManager.this, project,
						IBuildConsoleEvent.CONSOLE_START);
				listener.consoleChange(event);
			}
		}
		showConsole(true);
	}

	/**
	 * Opens the console view. If the view is already open, it is brought to the
	 * front if requested and the respective user preference is set. The console
	 * that is shown is the console that was last on top.
	 */
	protected void showConsole(boolean bringToTop) {
		IWorkbenchWindow window = CUIPlugin.getActiveWorkbenchWindow();
		if (window != null) {
			IWorkbenchPage page = window.getActivePage();
			if (page != null) {
				IViewPart consoleView = page.findView(IConsoleConstants.ID_CONSOLE_VIEW);
				if (consoleView == null && BuildConsolePreferencePage.isAutoOpenConsole()) {
					IWorkbenchPart activePart = page.getActivePart();
					try {
						consoleView = page.showView(IConsoleConstants.ID_CONSOLE_VIEW);
					} catch (PartInitException pie) {
						CUIPlugin.log(pie);
					}
					//restore focus stolen by the creation of the
					// console
					page.activate(activePart);
				}
				if (bringToTop && shouldBringToTop(consoleView)) {
					page.bringToTop(consoleView);
				}
				if (consoleView instanceof IConsoleView) {
					if (BuildConsole.getCurrentPage() == null)
						((IConsoleView) consoleView).display(fConsole);
					else
						((IConsoleView) consoleView).display(BuildConsole.getCurrentPage().getConsole());
				}
			}
		}
	}

	boolean shouldBringToTop(IViewPart consoleView) {
		boolean bringToTop = false;
		if (consoleView instanceof IConsoleView) {
			IConsoleView cView = (IConsoleView) consoleView;
			return !cView.isPinned() && BuildConsolePreferencePage.isConsoleOnTop();
		}
		return bringToTop;
	}

	/**
	 * Traverses the delta looking for added/removed/changed launch
	 * configuration files.
	 *
	 * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent)
	 */
	@Override
	public void resourceChanged(IResourceChangeEvent event) {
		IResource resource = event.getResource();
		if (resource != null && resource.getType() == IResource.PROJECT) {
			if (event.getType() == IResourceChangeEvent.PRE_DELETE
					|| event.getType() == IResourceChangeEvent.PRE_CLOSE) {
				IDocumentPartitioner partioner = fConsoleMap.remove(resource);
				if (partioner != null) {
					partioner.disconnect();
					if (!listeners.isEmpty()) {
						for (IBuildConsoleListener listener : listeners) {
							ConsoleEvent consoleEvent = new ConsoleEvent(this, (IProject) resource,
									IBuildConsoleEvent.CONSOLE_CLOSE);
							listener.consoleChange(consoleEvent);
						}
					}
				}
			}
		}
	}

	/**
	 * Release resources allocated on {@link #startup(String, String, URL)}.
	 */
	public void shutdown() {
		if (infoColor != null) {
			infoColor.dispose();
			outputColor.dispose();
			errorColor.dispose();
			backgroundColor.dispose();
			problemErrorBackgroundColor.dispose();
			problemWarningBackgroundColor.dispose();
			problemInfoBackgroundColor.dispose();
			problemHighlightedColor.dispose();
		}
		ConsolePlugin.getDefault().getConsoleManager()
				.removeConsoles(new org.eclipse.ui.console.IConsole[] { fConsole });
		CUIPlugin.getWorkspace().removeResourceChangeListener(this);
		CUIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this);
	}

	private void runUI(Runnable run) {
		Display display;
		display = Display.getCurrent();
		if (display == null) {
			display = Display.getDefault();
			display.asyncExec(run);
		} else {
			run.run();
		}
	}

	/**
	 * Create new build console. Subclasses may override to create a specialized
	 * console.
	 *
	 * @param name - name of console to appear in the list of consoles in context menu
	 *    in the Console view.
	 * @param contextId - context menu id in the Console view.
	 * @param iconUrl - a {@link URL} of the icon for the context menu of the Console
	 *    view. The url is expected to point to an image in eclipse OSGi bundle.
	 *    {@code iconUrl} can be <b>null</b>, in that case the default image is used.
	 * @return newly created build console.
	 */
	protected BuildConsole createBuildConsole(String name, String contextId, final URL iconUrl) {
		return new BuildConsole(this, name, contextId, iconUrl);
	}

	/**
	 * Start console activities. This will create a new console in the Console view,
	 * create streams, color resources, register listeners etc.
	 * Most work is done in UI thread.
	 *
	 * Use {@link #shutdown()} after the console activity ends.
	 *
	 * @param name - name of the console to appear in the Console view.
	 * @param contextId - context menu id in the Console view.
	 * @param iconUrl - icon to show in the context menu.
	 */
	public void startup(String name, String contextId, final URL iconUrl) {
		// Ensure global console is initialized before any other build console
		if (!(this instanceof GlobalBuildConsoleManager))
			GlobalBuildConsoleManager.startup();

		infoStream = new BuildConsoleStreamDecorator();
		outputStream = new BuildConsoleStreamDecorator();
		errorStream = new BuildConsoleStreamDecorator();
		fName = name;
		fContextMenuId = contextId;

		runUI(() -> {
			// add console to the Console view
			fConsole = createBuildConsole(fName, fContextMenuId, iconUrl);
			ConsolePlugin.getDefault().getConsoleManager()
					.addConsoles(new org.eclipse.ui.console.IConsole[] { fConsole });

			infoStream.setConsole(fConsole);
			infoColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_INFO_COLOR);
			infoStream.setColor(infoColor);
			outputStream.setConsole(fConsole);
			outputColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_OUTPUT_COLOR);
			outputStream.setColor(outputColor);
			errorStream.setConsole(fConsole);
			errorColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_ERROR_COLOR);
			errorStream.setColor(errorColor);
			backgroundColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_BACKGROUND_COLOR);
			fConsole.setBackground(backgroundColor);
			problemHighlightedColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_HIGHLIGHTED_COLOR);
			problemErrorBackgroundColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_BACKGROUND_COLOR);
			problemWarningBackgroundColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_WARNING_BACKGROUND_COLOR);
			problemInfoBackgroundColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_INFO_BACKGROUND_COLOR);
		});
		CUIPlugin.getWorkspace().addResourceChangeListener(this);
		CUIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);
	}

	@Override
	public void propertyChange(PropertyChangeEvent event) {
		String property = event.getProperty();
		// colors
		if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_INFO_COLOR)) {
			Color newColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_INFO_COLOR);
			infoStream.setColor(newColor);
			infoColor.dispose();
			infoColor = newColor;
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_OUTPUT_COLOR)) {
			Color newColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_OUTPUT_COLOR);
			outputStream.setColor(newColor);
			outputColor.dispose();
			outputColor = newColor;
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_ERROR_COLOR)) {
			Color newColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_ERROR_COLOR);
			errorStream.setColor(newColor);
			errorColor.dispose();
			errorColor = newColor;
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_BACKGROUND_COLOR)) {
			Color newColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_BACKGROUND_COLOR);
			fConsole.setBackground(newColor);
			backgroundColor.dispose();
			backgroundColor = newColor;
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_HIGHLIGHTED_COLOR)) {
			Color newColor = createColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_HIGHLIGHTED_COLOR);
			problemHighlightedColor.dispose();
			problemHighlightedColor = newColor;
			redrawTextViewer();
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_BACKGROUND_COLOR)) {
			Color newColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_BACKGROUND_COLOR);
			problemErrorBackgroundColor.dispose();
			problemErrorBackgroundColor = newColor;
			redrawTextViewer();
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_WARNING_BACKGROUND_COLOR)) {
			Color newColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_WARNING_BACKGROUND_COLOR);
			problemWarningBackgroundColor.dispose();
			problemWarningBackgroundColor = newColor;
			redrawTextViewer();
		} else if (property.equals(BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_INFO_BACKGROUND_COLOR)) {
			Color newColor = createBackgroundColor(CUIPlugin.getStandardDisplay(),
					BuildConsolePreferencePage.PREF_BUILDCONSOLE_PROBLEM_INFO_BACKGROUND_COLOR);
			problemInfoBackgroundColor.dispose();
			problemInfoBackgroundColor = newColor;
			redrawTextViewer();
		}
	}

	private void redrawTextViewer() {
		final BuildConsolePage p = BuildConsole.getCurrentPage();
		if (p == null)
			return;
		final BuildConsoleViewer v = p.getViewer();
		if (v == null)
			return;
		Display display = Display.getDefault();
		display.asyncExec(() -> v.getTextWidget().redraw());
	}

	public IBuildConsoleStreamDecorator getStreamDecorator(int type) throws CoreException {
		switch (type) {
		case BUILD_STREAM_TYPE_ERROR:
			return errorStream;
		case BUILD_STREAM_TYPE_INFO:
			return infoStream;
		case BUILD_STREAM_TYPE_OUTPUT:
			return outputStream;
		}
		throw new CoreException(new Status(IStatus.ERROR, CUIPlugin.PLUGIN_ID, -1, "No Such Console", null)); //$NON-NLS-1$
	}

	/**
	 * Returns a color instance based on data from a preference field.
	 */
	private Color createColor(Display display, String preference) {
		RGB rgb = PreferenceConverter.getColor(CUIPlugin.getDefault().getPreferenceStore(), preference);
		return new Color(display, rgb);
	}

	/**
	 * Returns a background color instance based on data from a preference field.
	 * This is a workaround for black console bug 320723.
	 */
	private Color createBackgroundColor(Display display, String preference) {
		IPreferenceStore preferenceStore = CUIPlugin.getDefault().getPreferenceStore();
		RGB rgb;
		if (preferenceStore.contains(preference)) {
			rgb = PreferenceConverter.getColor(preferenceStore, preference);
		} else {
			rgb = new RGB(200, 200, 200); // gray background
		}
		return new Color(display, rgb);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public IConsole getConsole(IProject project) {
		return new MultiBuildConsoleAdapter(getProjectConsole(project), GlobalBuildConsoleManager.getGlobalConsole());
	}

	/**
	 * {@inheritDoc}
	 *
	 * @return the console for the specified project. Returns {@code null}
	 *   if project is {@code null} or not accessible.
	 */
	@Override
	public IConsole getProjectConsole(IProject project) {
		if (project == null || !project.isAccessible())
			return null;

		fLastProject = project;
		return getProjectConsolePartioner(project).getConsole();
	}

	@Override
	public IProject getLastBuiltProject() {
		return fLastProject;
	}

	/**
	 * @return the partitioner for the specified projects build console
	 */
	private BuildConsolePartitioner getProjectConsolePartioner(IProject project) {
		BuildConsolePartitioner partitioner = fConsoleMap.get(project);
		if (partitioner == null) {
			partitioner = new BuildConsolePartitioner(project, this);
			fConsoleMap.put(project, partitioner);
		}
		return partitioner;
	}

	/**
	 * @return the document backing the build console for the specified project
	 */
	@Override
	public IDocument getConsoleDocument(IProject project) {
		Assert.isNotNull(project);
		return getProjectConsolePartioner(project).getDocument();
	}

	@Override
	public void addConsoleListener(IBuildConsoleListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removeConsoleListener(IBuildConsoleListener listener) {
		listeners.remove(listener);
	}

	/**
	 * @return logging preferences for a given project.
	 * @param project to get logging preferences for, can't be {@code null}.
	 */
	public Preferences getBuildLogPreferences(IProject project) {
		return new LocalProjectScope(project).getNode(PREF_QUALIFIER).node(BUILD_CONSOLE_NODE);
	}

	/**
	 * @return default location of logs for a project.
	 * @param project to get default log location for, can't be {@code null}.
	 */
	public String getDefaultConsoleLogLocation(IProject project) {
		IPath defaultLogLocation = CUIPlugin.getDefault().getStateLocation()
				.append(project.getName() + PROJECT_LOG_EXT);
		return defaultLogLocation.toOSString();
	}

	/**
	 * Where the log for per-project console is kept.
	 *
	 * @param project - the project. Cannot be {@code null}.
	 * @return {@link URI} of build log or {@code null} if not available.
	 */
	public URI getLogURI(IProject project) {
		Assert.isNotNull(project);

		if (fContextMenuId != DEFAULT_CONTEXT_MENU_ID)
			return null;

		URI logURI = null;

		Preferences prefs = getBuildLogPreferences(project);
		boolean keepLog = prefs.getBoolean(KEY_KEEP_LOG, CONSOLE_KEEP_LOG_DEFAULT);
		if (keepLog) {
			String strLocation;
			strLocation = prefs.get(KEY_LOG_LOCATION, getDefaultConsoleLogLocation(project));
			if (strLocation.trim().length() > 0) {
				logURI = URIUtil.toURI(strLocation);
			}
			if (logURI == null) {
				IStatus status = new Status(IStatus.ERROR, CUIPlugin.PLUGIN_ID,
						"Can't determine URI for location=[" + strLocation + "]"); //$NON-NLS-1$ //$NON-NLS-2$
				CUIPlugin.log(status);
			}
		}
		return logURI;
	}

}
