/*******************************************************************************
 * Copyright (c) 2005, 2011 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
 *******************************************************************************/

package org.eclipse.debug.ui.actions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IBreakpointImportParticipant;
import org.eclipse.debug.internal.core.BreakpointManager;
import org.eclipse.debug.internal.ui.IInternalDebugUIConstants;
import org.eclipse.debug.internal.ui.importexport.breakpoints.IImportExportConstants;
import org.eclipse.debug.internal.ui.importexport.breakpoints.ImportExportMessages;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.XMLMemento;

import com.ibm.icu.text.MessageFormat;

/**
 * Imports breakpoints from a file or string buffer into the workspace.
 * <p>
 * This class may be instantiated.
 * <p>
 * @since 3.2
 * @noextend This class is not intended to be subclassed by clients.
 */
public class ImportBreakpointsOperation implements IRunnableWithProgress {

	private boolean fOverwriteAll = false;

	private String fFileName = null;

	private boolean fCreateWorkingSets = false;

	private ArrayList fAdded = new ArrayList();
	
	private String fCurrentWorkingSetProperty = null;

	private BreakpointManager fManager = (BreakpointManager) DebugPlugin.getDefault().getBreakpointManager();
	
	/** 
	 * When a buffer is specified, a file is not used.
	 */
	private StringBuffer fBuffer = null;

	/**
	 * Constructs an operation to import breakpoints.
	 * 
	 * @param fileName the file to read breakpoints from - the file should have been 
	 *            created from an export operation
	 * @param overwrite whether imported breakpoints will overwrite existing equivalent breakpoints
	 * @param createWorkingSets whether breakpoint working sets should be created. Breakpoints
	 * 	are exported with information about the breakpoint working sets they belong to. Those
	 * 	working sets can be optionally re-created on import if they do not already exist in the
	 *            workspace.
	 */
	public ImportBreakpointsOperation(String fileName, boolean overwrite, boolean createWorkingSets) {
		fFileName = fileName;
		fOverwriteAll = overwrite;
		fCreateWorkingSets = createWorkingSets;
	}
	
	/**
	 * Constructs an operation to import breakpoints from a string buffer. The buffer
	 * must contain a memento created an {@link ExportBreakpointsOperation}.
	 * 
	 * @param buffer the string buffer to read breakpoints from - the file should have been 
	 *            created from an export operation
	 * @param overwrite whether imported breakpoints will overwrite existing equivalent breakpoints
	 * @param createWorkingSets whether breakpoint working sets should be created. Breakpoints
	 * 	are exported with information about the breakpoint working sets they belong to. Those
	 * 	working sets can be optionally re-created on import if they do not already exist in the
	 *            workspace.
	 * @since 3.5
	 */
	public ImportBreakpointsOperation(StringBuffer buffer, boolean overwrite, boolean createWorkingSets) {
		fBuffer = buffer;
		fOverwriteAll = overwrite;
		fCreateWorkingSets = createWorkingSets;
	}	

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.IWorkspaceRunnable#run(org.eclipse.core.runtime.IProgressMonitor)
	 */
	public void run(final IProgressMonitor monitor) throws InvocationTargetException {
		SubMonitor localmonitor = SubMonitor.convert(monitor, ImportExportMessages.ImportOperation_0, 1);
		Reader reader = null;
		try {
			if (fBuffer == null) {
				reader = new InputStreamReader(new FileInputStream(fFileName), "UTF-8"); //$NON-NLS-1$
			} else {
				reader = new StringReader(fBuffer.toString());
			}
			XMLMemento memento = XMLMemento.createReadRoot(reader);
			IMemento[] nodes = memento.getChildren(IImportExportConstants.IE_NODE_BREAKPOINT);
			IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
			localmonitor.setWorkRemaining(nodes.length);
			Map attributes = null;
			IBreakpointImportParticipant[] participants = null;
			for(int i = 0; i < nodes.length; i++) {
				if(localmonitor.isCanceled()) {
					return;
				}
				attributes = collectBreakpointProperties(nodes[i]);
				IResource resource = workspace.findMember((String) attributes.get(IImportExportConstants.IE_NODE_PATH));
				// filter resource breakpoints that do not exist in this workspace
				if(resource != null) {	
					try {
						participants = fManager.getImportParticipants((String) attributes.get(IImportExportConstants.IE_NODE_TYPE)); 
					}
					catch(CoreException ce) {}
					IMarker marker = findExistingMarker(attributes, participants);
					if(marker == null) {
						marker = resource.createMarker((String) attributes.get(IImportExportConstants.IE_NODE_TYPE));
						restoreBreakpoint(marker, attributes, participants);
					}
					else {
						if(fOverwriteAll) {
							marker.setAttributes(null);
							restoreBreakpoint(marker, attributes, participants);
						}
					}
				}
				fCurrentWorkingSetProperty = null;
				localmonitor.worked(1);
			}
			if(fAdded.size() > 0) {
				fManager.addBreakpoints((IBreakpoint[])fAdded.toArray(new IBreakpoint[fAdded.size()]));
			}
		} 
		catch(FileNotFoundException e) {
			throw new InvocationTargetException(e, 
					MessageFormat.format("Breakpoint import file not found: {0}", new String[]{fFileName})); //$NON-NLS-1$
		}
		catch (UnsupportedEncodingException e) {
			throw new InvocationTargetException(e, 
					MessageFormat.format("The import file was written in non-UTF-8 encoding.", new String[]{fFileName})); //$NON-NLS-1$
		}
		catch(CoreException ce) {
			throw new InvocationTargetException(ce, 
					MessageFormat.format("There was a problem importing breakpoints from: {0}", new String[] {fFileName})); //$NON-NLS-1$
		}
		finally {
			localmonitor.done();
			if(reader != null) {
				try {
					reader.close();
				} 
				catch (IOException e) {
					throw new InvocationTargetException(e);
				}
			}
		}
	}
	
	/**
	 * Returns a marker backing an existing breakpoint based on the given set of breakpoint attributes
	 * @param attributes the map of attributes to compare for marker equality
	 * @param participants the list of participants to ask if a breakpoint matches the given map of attributes
	 * @return the marker for an existing breakpoint or <code>null</code> if one could not be located
	 * @since 3.5
	 */
	protected IMarker findExistingMarker(Map attributes, IBreakpointImportParticipant[] participants) {
		IBreakpoint[] bps = fManager.getBreakpoints();		 
		for(int i = 0; i < bps.length; i++) {
			for(int j = 0; j < participants.length; j++) {
				try {
					if(participants[j].matches(attributes, bps[i])) {
						return bps[i].getMarker();
					}
				}
				catch(CoreException ce) {}
			}
		}
		return null;
	}
	
	/**
	 * Collects all of the properties for a breakpoint from the memento describing it.
	 * The values in the map will be one of:
	 * <ul>
	 * <li>{@link String}</li>
	 * <li>{@link Integer}</li>
	 * <li>{@link Boolean}</li>
	 * </ul>
	 * @param memento the memento to read breakpoint attributes from
	 * @return a new map of all of the breakpoint attributes from the given memento.
	 * @since 3.5
	 */
	protected Map collectBreakpointProperties(IMemento memento) {
		HashMap map = new HashMap();
		
		//collect attributes from the 'breakpoint' node
		map.put(IImportExportConstants.IE_BP_ENABLED, memento.getBoolean(IImportExportConstants.IE_BP_ENABLED));
		map.put(IImportExportConstants.IE_BP_PERSISTANT, memento.getBoolean(IImportExportConstants.IE_BP_PERSISTANT));
		map.put(IImportExportConstants.IE_BP_REGISTERED, memento.getBoolean(IImportExportConstants.IE_BP_REGISTERED));
		
		//collect attributes from the 'marker' node
		IMemento child = memento.getChild(IImportExportConstants.IE_NODE_MARKER);
		map.put(IImportExportConstants.IE_NODE_TYPE, child.getString(IImportExportConstants.IE_NODE_TYPE));
		map.put(IMarker.LINE_NUMBER, child.getInteger(IMarker.LINE_NUMBER));
		
		//copy all the marker attributes to the map
		IMemento[] children = child.getChildren(IImportExportConstants.IE_NODE_ATTRIB);
		for(int i = 0; i < children.length; i++) {
			readAttribute(children[i], map);
		}

		//collect attributes from the 'resource' node
		child = memento.getChild(IImportExportConstants.IE_NODE_RESOURCE);
		map.put(IImportExportConstants.IE_NODE_PATH, child.getString(IImportExportConstants.IE_NODE_PATH));
		return map;
	}
	
	/**
	 * Collects the 'name' and 'value' key / attribute from the given memento and places it in the specified map
	 * @param memento the memento to read a name / value attribute from 
	 * @param map the map to add the read attribute to
	 */
	private void readAttribute(IMemento memento, Map map) {
		String name = memento.getString(IImportExportConstants.IE_NODE_NAME), 
		   	   value = memento.getString(IImportExportConstants.IE_NODE_VALUE);
		if (value != null && name != null) {
			if (name.equals(IInternalDebugUIConstants.WORKING_SET_NAME)) {
				fCurrentWorkingSetProperty = value;
			}
			Object val = value;
			try {
				val = Integer.valueOf(value);
			} catch (NumberFormatException e) {
				if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("true")) { //$NON-NLS-1$ //$NON-NLS-2$
					val = Boolean.valueOf(value);
				}
			}
			if(val != null) {
				map.put(name, val);
			}
		}
	}
	
	/**
	 * restores all of the attributes back into the given marker, recreates the breakpoint in the
	 * breakpoint manager, and optionally recreates any working set(s) the breakpoint belongs to.
	 * @param marker the marker to create the new breakpoint on
	 * @param attributes the attributes to set in the new breakpoint
	 * @param participants the list of participants used to verify the restored breakpoint
	 * @since 3.5
	 */
	protected void restoreBreakpoint(IMarker marker, final Map attributes, IBreakpointImportParticipant[] participants) {
		String key = null;
		for(Iterator iter = attributes.keySet().iterator(); iter.hasNext();) {
			key = (String) iter.next();
			try {
				marker.setAttribute(key, attributes.get(key));
			}
			catch(CoreException ce) {}
		}
		IBreakpoint breakpoint = null;
		try {
			// create the breakpoint
			breakpoint = fManager.createBreakpoint(marker);
			breakpoint.setEnabled(((Boolean)attributes.get(IImportExportConstants.IE_BP_ENABLED)).booleanValue());
			breakpoint.setPersisted(((Boolean)attributes.get(IImportExportConstants.IE_BP_PERSISTANT)).booleanValue());
			breakpoint.setRegistered(((Boolean)attributes.get(IImportExportConstants.IE_BP_REGISTERED)).booleanValue());
			fAdded.add(breakpoint);
			if (fCreateWorkingSets && fCurrentWorkingSetProperty != null) {
				String[] names = fCurrentWorkingSetProperty.split("\\" + IImportExportConstants.DELIMITER); //$NON-NLS-1$
				updateWorkingSets(names, breakpoint);
			}
			if(participants != null) {
				for(int i = 0; i < participants.length; i++) {
					participants[i].verify(breakpoint);
				}
			}
		}
		catch(CoreException ce) {
			//Something bad happened while trying to restore the breakpoint, remove it from the cached list and delete the marker
			//to ensure the manager does not hold bogus breakpoints
			if(breakpoint != null) {
				try {
					fAdded.remove(breakpoint);
					marker.delete();
				} catch (CoreException e) {}
			}
		}
	}
	
	/**
	 * Updates the working sets the given breakpoint belongs to
	 * @param wsnames the array of working set names
	 * @param breakpoint the breakpoint to add to the working sets
	 * @since 3.5
	 */
	private void updateWorkingSets(String[] wsnames, IBreakpoint breakpoint) {
		IWorkingSetManager mgr = PlatformUI.getWorkbench().getWorkingSetManager();
		IWorkingSet set = null;
		ArrayList sets = new ArrayList();
		collectContainingWorkingsets(breakpoint, sets);
		for (int i = 0; i < wsnames.length; i++) {
			if("".equals(wsnames[i])) { //$NON-NLS-1$
				continue;
			}
			set = mgr.getWorkingSet(wsnames[i]);
			if(set == null) {
				//create working set
				set = mgr.createWorkingSet(wsnames[i], new IAdaptable[] {});
				set.setId(IDebugUIConstants.BREAKPOINT_WORKINGSET_ID);
				mgr.addWorkingSet(set);
			}
			if(!sets.contains(set)) {
				IAdaptable[] elements = set.getElements();
				IAdaptable[] newElements = new IAdaptable[elements.length + 1];
				newElements[newElements.length - 1] = breakpoint;
				System.arraycopy(elements, 0, newElements, 0, elements.length);
				set.setElements(newElements);
			}
			sets.remove(set);
		}
		ArrayList items = null;
		for(Iterator iter = sets.iterator(); iter.hasNext();) {
			set = (IWorkingSet) iter.next();
			items = new ArrayList(Arrays.asList(set.getElements()));
			if(items.remove(breakpoint)) {
				set.setElements((IAdaptable[]) items.toArray(new IAdaptable[items.size()]));
			}
		}
	}
	
	/**
	 * Collects all of the breakpoint working sets that contain the given {@link IBreakpoint}
	 * in the given list
	 * 
	 * @param breakpoint the breakpoint to collect working set containers from 
	 * @param collector the list to collect containing working sets in 
	 * @since 3.5
	 */
	private void collectContainingWorkingsets(IBreakpoint breakpoint, List collector) {
		IWorkingSetManager mgr = PlatformUI.getWorkbench().getWorkingSetManager();
		IWorkingSet[] sets = mgr.getWorkingSets();
		for (int i = 0; i < sets.length; i++) {
			if(IDebugUIConstants.BREAKPOINT_WORKINGSET_ID.equals(sets[i].getId()) &&
					containsBreakpoint(sets[i], breakpoint)) {
				collector.add(sets[i]);
			}
		}
	}
	
	/**
	 * Method to ensure markers and breakpoints are not both added to the working set
	 * @param set the set to check
	 * @param breakpoint the breakpoint to check for existence
	 * @return true if it is present false otherwise
	 */
	private boolean containsBreakpoint(IWorkingSet set, IBreakpoint breakpoint) {
		IAdaptable[] elements = set.getElements();
		for (int i = 0; i < elements.length; i++) {
			if (elements[i].equals(breakpoint)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns the breakpoints that were imported by this operation, possibly
	 * an empty list. 
	 * 
	 * @return breakpoints imported by this operation
	 * @since 3.5
	 */
	public IBreakpoint[] getImportedBreakpoints() {
		return (IBreakpoint[])fAdded.toArray(new IBreakpoint[fAdded.size()]);
	}
}
