/*******************************************************************************
 * Copyright (c) 2004, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Mike Morearty - Bug 255310: Launching only gets the progress bar to 91%
 *******************************************************************************/
package org.eclipse.debug.core.model;

import com.ibm.icu.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.IStatusHandler;
import org.eclipse.debug.internal.core.DebugCoreMessages;
import org.eclipse.debug.internal.core.IInternalDebugCoreConstants;

/**
 * Default implementation of a launch configuration delegate. Provides
 * convenience methods for computing the build order of projects,
 * building projects, and searching for errors in the workspace. The
 * default pre-launch check prompts the user to launch in debug mode
 * if breakpoints are present in the workspace. 
 * <p>
 * Clients implementing launch configuration delegates should subclass
 * this class.
 * </p>
 * @since 3.0
 */
public abstract class LaunchConfigurationDelegate implements ILaunchConfigurationDelegate2 {
	
	/**
	 * Constant to define debug.core for the status codes
	 * 
	 * @since 3.2
	 */
	private static final String DEBUG_CORE = "org.eclipse.debug.core"; //$NON-NLS-1$
	
	/**
	 * Constant to define debug.ui for the status codes
	 * 
	 * @since 3.2
	 */
	private static final String DEBUG_UI = "org.eclipse.debug.ui"; //$NON-NLS-1$
	
	/**
	 * Status code for which a UI prompter is registered.
	 */
	protected static final IStatus promptStatus = new Status(IStatus.INFO, DEBUG_UI, 200, IInternalDebugCoreConstants.EMPTY_STRING, null);
	
	/**
	 * Status code for which a prompter is registered to ask the user if they
	 * want to launch in debug mode when breakpoints are present.
	 */
	protected static final IStatus switchToDebugPromptStatus = new Status(IStatus.INFO, DEBUG_CORE, 201, IInternalDebugCoreConstants.EMPTY_STRING, null);
	
	/**
	 * Status code for which a prompter is registered to ask the user if the
	 * want to continue launch despite existing compile errors
	 */
	protected static final IStatus complileErrorPromptStatus = new Status(IStatus.INFO, DEBUG_CORE, 202, IInternalDebugCoreConstants.EMPTY_STRING, null);
	
	/**
	 * Status code for which a prompter will ask the user to save any/all of the dirty editors which have only to do
	 * with this launch (scoping them to the current launch/build)
	 * 
	 * @since 3.2
	 */
	protected static final IStatus saveScopedDirtyEditors = new Status(IStatus.INFO, DEBUG_CORE, 222, IInternalDebugCoreConstants.EMPTY_STRING, null);
	
	/**
	 * Status code for which a prompter is registered to ask the user if the
	 * want to continue launch despite existing compile errors in specific
	 * projects. This enhances the 'compileErrorPromptStatus' by specifying
	 * which projects the errors exist in.
	 * 
	 * @since 3.1
	 */
	protected static final IStatus complileErrorProjectPromptStatus = new Status(IStatus.INFO, DEBUG_CORE, 203, IInternalDebugCoreConstants.EMPTY_STRING, null);
	
	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
	 */
	public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) throws CoreException {
		return null;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#buildForLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public boolean buildForLaunch(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
		if (monitor != null) {
			monitor.beginTask("", 1); //$NON-NLS-1$
		}
		try {
			IProject[] projects = getBuildOrder(configuration, mode);
			if (projects == null) {
				return true;
			} 
			buildProjects(projects, new SubProgressMonitor(monitor, 1));
			return false;
		} finally {
			if (monitor != null) {
				monitor.done();
			}
		}
	}
	
	/**
	 * Returns the projects to build before launching the given launch configuration
	 * or <code>null</code> if the entire workspace should be built incrementally.
	 * Subclasses should override as required.
	 * 
	 * @param configuration the configuration being launched
	 * @param mode launch mode
	 * @return projects to build, in build order, or <code>null</code>
	 * @throws CoreException if an exception occurs
	 */
	protected IProject[] getBuildOrder(ILaunchConfiguration configuration, String mode) throws CoreException {
		return null;
	}
	
	/**
	 * Returns the set of projects to use when searching for errors or <code>null</code> 
	 * if no search is to be done.  
	 * 
	 * @param configuration the configuration being launched
	 * @param mode launch mode
	 * @return a list of projects or <code>null</code>
	 * @throws CoreException if an exception occurs
	 */
	protected IProject[] getProjectsForProblemSearch(ILaunchConfiguration configuration, String mode) throws CoreException {
		return null;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#finalLaunchCheck(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public boolean finalLaunchCheck(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
		monitor.beginTask("", 1); //$NON-NLS-1$
		try {
			IProject[] projects = getProjectsForProblemSearch(configuration, mode);
			if (projects == null) {
				return true; //continue launch
			}
			boolean continueLaunch = true;
				
			monitor.subTask(DebugCoreMessages.LaunchConfigurationDelegate_6); 
			List errors = new ArrayList();
			for (int i = 0; i < projects.length; i++) {
				monitor.subTask(MessageFormat.format(DebugCoreMessages.LaunchConfigurationDelegate_7, new String[]{projects[i].getName()})); 
				if (existsProblems(projects[i])) {
					errors.add(projects[i]);
				}	
			}	
			if (!errors.isEmpty()) {
				errors.add(0, configuration);
				IStatusHandler prompter = DebugPlugin.getDefault().getStatusHandler(promptStatus);
				if (prompter != null) {
					continueLaunch = ((Boolean) prompter.handleStatus(complileErrorProjectPromptStatus, errors)).booleanValue();
				}
			}
				
			return continueLaunch;
		} finally {
			monitor.done();
		}
	}
	
	/* (non-Javadoc)
	 * 
	 * If launching in run mode, and the configuration supports debug mode, check
	 * if there are any breakpoints in the workspace, and ask the user if they'd
	 * rather launch in debug mode.
	 * <p>
	 * Since 3.2, this check also performs saving of resources before launching.
	 * </p>
	 * 
	 * @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#preLaunchCheck(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public boolean preLaunchCheck(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
		if (!saveBeforeLaunch(configuration, mode, monitor)) {
			return false;
		}
		if (mode.equals(ILaunchManager.RUN_MODE) && configuration.supportsMode(ILaunchManager.DEBUG_MODE)) {
			IBreakpoint[] breakpoints= getBreakpoints(configuration);
            if (breakpoints == null) {
                return true;
            }
			for (int i = 0; i < breakpoints.length; i++) {
				if (breakpoints[i].isEnabled()) {
					IStatusHandler prompter = DebugPlugin.getDefault().getStatusHandler(promptStatus);
					if (prompter != null) {
						boolean launchInDebugModeInstead = ((Boolean)prompter.handleStatus(switchToDebugPromptStatus, configuration)).booleanValue();
						if (launchInDebugModeInstead) { 
							return false; //kill this launch
						} 
					}
					// if no user prompt, or user says to continue (no need to check other breakpoints)
					return true;
				}
			}
		}	
		// no enabled breakpoints... continue launch
		return true;
	}
	
	/**
	 * Performs the scoped saving of resources before launching and returns whether
	 * the launch should continue. By default, only resources contained within the projects
	 * which are part of the build scope are considered.
	 * <p>
	 * Subclasses may override this method if required.
	 * </p>
	 * 
	 * @param configuration the configuration being launched
	 * @param mode the launch mode
	 * @param monitor progress monitor
	 * @return whether the launch should continue
	 * @throws CoreException if an exception occurs during the save
	 * @since 3.2
	 */
	protected boolean saveBeforeLaunch(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
		monitor.beginTask("", 1); //$NON-NLS-1$
		try {
			IStatusHandler prompter = DebugPlugin.getDefault().getStatusHandler(promptStatus);
			if(prompter != null) {
				//do save here and remove saving from DebugUIPlugin to avoid it 'trumping' this save
				IProject[] buildOrder = getBuildOrder(configuration, mode);
				if(!((Boolean)prompter.handleStatus(saveScopedDirtyEditors, new Object[]{configuration, buildOrder})).booleanValue()) {
					return false;
				}
			}	
			return true;
		} finally {
			monitor.done();
		}
	}

    /**
     * Returns the breakpoint collection that is relevant for this launch delegate.
     * By default this is all the breakpoints registered with the Debug breakpoint manager.
     * 
     * @param configuration the configuration to get associated breakpoints for
     * @since 3.1
     * @return the breakpoints that are relevant for this launch delegate
     */ 
    protected IBreakpoint[] getBreakpoints(ILaunchConfiguration configuration) {
        IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
        if (!breakpointManager.isEnabled()) {
            // no need to check breakpoints individually.
            return null;
        }
        return breakpointManager.getBreakpoints();
    }
	
	/**
	 * Returns an array of projects in their suggested build order
	 * containing all of the projects specified by <code>baseProjects</code>
	 * and all of their referenced projects.
	 *  
	 * @param baseProjects a collection of projects
	 * @return an array of projects in their suggested build order
	 * containing all of the projects specified by <code>baseProjects</code>
	 * @throws CoreException if an error occurs while computing referenced
	 *  projects
	 */
	protected IProject[] computeReferencedBuildOrder(IProject[] baseProjects) throws CoreException {
		HashSet unorderedProjects = new HashSet();
		for(int i = 0; i< baseProjects.length; i++) {
			unorderedProjects.add(baseProjects[i]);
			addReferencedProjects(baseProjects[i], unorderedProjects);
		}
		IProject[] projectSet = (IProject[]) unorderedProjects.toArray(new IProject[unorderedProjects.size()]);
		return computeBuildOrder(projectSet);
	}
	
	
	/**
	 * Adds all projects referenced by <code>project</code> to the given
	 * set.
	 * 
	 * @param project project
	 * @param references set to which referenced projects are added
	 * @throws CoreException if an error occurs while computing referenced
	 *  projects
	 */
	protected void addReferencedProjects(IProject project, Set references) throws CoreException{
		if (project.isOpen()) {
			IProject[] projects = project.getReferencedProjects();
			for (int i = 0; i < projects.length; i++) {
				IProject refProject= projects[i];
				if (refProject.exists() && !references.contains(refProject)) {
					references.add(refProject);
					addReferencedProjects(refProject, references);
				}
			}		
		}
	}
	
	/**  
	 * Returns a list of projects in their suggested build order from the
	 * given unordered list of projects.
	 * 
	 * @param projects the list of projects to sort into build order
	 * @return a new array containing all projects from <code>projects</code> sorted
	 *   according to their build order.
	 */
	protected IProject[] computeBuildOrder(IProject[] projects) { 
		String[] orderedNames = ResourcesPlugin.getWorkspace().getDescription().getBuildOrder();
		if (orderedNames != null) {
			List orderedProjects = new ArrayList(projects.length);
			//Projects may not be in the build order but should be built if selected
			List unorderedProjects = new ArrayList(projects.length);
			for(int i = 0; i < projects.length; ++i) {
				unorderedProjects.add(projects[i]);
			}
			
			for (int i = 0; i < orderedNames.length; i++) {
				String projectName = orderedNames[i];
				for (Iterator iterator = unorderedProjects.iterator(); iterator.hasNext(); ) {
					IProject project = (IProject)iterator.next();
					if (project.getName().equals(projectName)) {
						orderedProjects.add(project);
						iterator.remove();
						break;
					}
				}
			}
			//Add anything not specified before we return
			orderedProjects.addAll(unorderedProjects);
			return (IProject[]) orderedProjects.toArray(new IProject[orderedProjects.size()]);
		}
		
		// Computing build order returned null, try the project prerequisite order
		IWorkspace.ProjectOrder po = ResourcesPlugin.getWorkspace().computeProjectOrder(projects);
		return po.projects;
	}	
	
	/**
	 * Returns whether the given project contains any problem markers of the
	 * specified severity.
	 * 
	 * @param proj the project to search
	 * @return whether the given project contains any problems that should
	 *  stop it from launching
	 * @throws CoreException if an error occurs while searching for
	 *  problem markers
	 */
	protected boolean existsProblems(IProject proj) throws CoreException {
		IMarker[] markers = proj.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
		if (markers.length > 0) {
			for (int i = 0; i < markers.length; i++) {
				if (isLaunchProblem(markers[i])) {
					return true;
				}
			}
		}
		return false;
	}
	
	/**
	 * Returns whether the given problem should potentially abort the launch.
	 * By default if the problem has an error severity, the problem is considered
	 * a potential launch problem. Subclasses may override to specialize error
	 * detection.
	 * 
	 * @param problemMarker candidate problem
	 * @return whether the given problem should potentially abort the launch
	 * @throws CoreException if any exceptions occur while accessing marker attributes
	 */
	protected boolean isLaunchProblem(IMarker problemMarker) throws CoreException {
		Integer severity = (Integer)problemMarker.getAttribute(IMarker.SEVERITY);
		if (severity != null) {
			return severity.intValue() >= IMarker.SEVERITY_ERROR;
		} 
		
		return false;
	}
	
	/**
	 * Performs an incremental build on each of the given projects.
	 * 
	 * @param projects projects to build
	 * @param monitor progress monitor
	 * @throws CoreException if an exception occurs while building
	 */
	protected void buildProjects(final IProject[] projects, IProgressMonitor monitor) throws CoreException {
		IWorkspaceRunnable build = new IWorkspaceRunnable(){
			public void run(IProgressMonitor pm) throws CoreException {
				SubMonitor localmonitor = SubMonitor.convert(pm, DebugCoreMessages.LaunchConfigurationDelegate_scoped_incremental_build, projects.length);
				try {
					for (int i = 0; i < projects.length; i++ ) {
						if (localmonitor.isCanceled()) {
							throw new OperationCanceledException();
						}
						projects[i].build(IncrementalProjectBuilder.INCREMENTAL_BUILD, localmonitor.newChild(1));
					}
				} finally {
					localmonitor.done();
				}
			}
		};
		ResourcesPlugin.getWorkspace().run(build, monitor);
	}
}
