/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.ui;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IDebugEventSetListener;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchesListener;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.ISourceLocator;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.internal.ui.DebugUIPlugin;
import org.eclipse.debug.ui.IValueDetailListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.debug.core.IEvaluationRunnable;
import org.eclipse.jdt.debug.core.IJavaArray;
import org.eclipse.jdt.debug.core.IJavaArrayType;
import org.eclipse.jdt.debug.core.IJavaClassType;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaPrimitiveValue;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.eval.IAstEvaluationEngine;
import org.eclipse.jdt.debug.eval.ICompiledExpression;
import org.eclipse.jdt.debug.eval.IEvaluationListener;
import org.eclipse.jdt.debug.eval.IEvaluationResult;
import org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

import com.sun.jdi.InvocationException;

public class JavaDetailFormattersManager implements IPropertyChangeListener, IDebugEventSetListener, ILaunchesListener {
	/**
	 * The default detail formatters manager.
	 */
	static private JavaDetailFormattersManager fgDefault;
	
	/**
	 * Return the default detail formatters manager.
	 * 
	 * @return default detail formatters manager.
	 */
	static public JavaDetailFormattersManager getDefault() {
		if (fgDefault == null) {
			fgDefault= new JavaDetailFormattersManager();
		}
		return fgDefault;
	}
	
	/**
	 * Map of types to the associated formatter (code snippet).
	 * (<code>String</code> -> <code>String</code>)
	 */
	private HashMap fDetailFormattersMap;
	
	/**
	 * Cache of compiled expressions.
	 * Associate a pair type name/debug target to a compiled expression.
	 */
	private HashMap fCacheMap;
	
	/**
	 * JavaDetailFormattersManager constructor.
	 */
	private JavaDetailFormattersManager() {
		populateDetailFormattersMap();
		JDIDebugUIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);
		DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this);
		DebugPlugin.getDefault().addDebugEventListener(this);
		fCacheMap= new HashMap();
	}
	
	/**
	 * Populate the detail formatters map with data from preferences.
	 */
	private void populateDetailFormattersMap() {
		String[] detailFormattersList= JavaDebugOptionsManager.parseList(JDIDebugUIPlugin.getDefault().getPreferenceStore().getString(IJDIPreferencesConstants.PREF_DETAIL_FORMATTERS_LIST));
		fDetailFormattersMap= new HashMap(detailFormattersList.length / 3);
		for (int i= 0, length= detailFormattersList.length; i < length;) {
			String typeName= detailFormattersList[i++];
			String snippet= detailFormattersList[i++].replace('\u0000', ',');
			boolean enabled= ! JavaDetailFormattersPreferencePage.DETAIL_FORMATTER_IS_DISABLED.equals(detailFormattersList[i++]);
			fDetailFormattersMap.put(typeName, new DetailFormatter(typeName, snippet, enabled));
		}
	}
	
	/**
	 * Compute asynchronously the 'toString' of the given value. If a formatter is associated to
	 * the type of the given value, this formatter is used instead of the <code>toString()</code>
	 * method.
	 * The result is return through the listener.
	 * 
	 * @param objectValue the value to 'format' 
	 * @param thread the thread to use to performed the evaluation
	 * @param listener the listener
	 */	
	public void computeValueDetail(final IJavaValue objectValue, final IJavaThread thread, final IValueDetailListener listener) {
		Runnable postEventDispatch = new Runnable() {
			public void run() {
				Runnable postEventProcess = new Runnable() {
					public void run() {
						if (thread.isPerformingEvaluation() && thread.isSuspended()) {
							listener.detailComputed(objectValue, DebugUIMessages.getString("JavaDetailFormattersManager.9")); //$NON-NLS-1$
						} else {
							thread.queueRunnable(new Runnable() {
								public void run() {
									resolveFormatter(objectValue, thread, listener);
								}
							});
						}
					}
				};
				JDIDebugUIPlugin.getStandardDisplay().asyncExec(postEventProcess);
			}
		};
		DebugPlugin.getDefault().asyncExec(postEventDispatch);
	}
	
	private void resolveFormatter(final IJavaValue value, final IJavaThread thread, final IValueDetailListener listener) {
		ICompiledExpression compiledExpression= null;
		EvaluationListener evaluationListener= new EvaluationListener(value, thread, listener);
		if (value instanceof IJavaObject && !(value instanceof IJavaArray)) {
			IJavaObject objectValue= (IJavaObject) value;
			IJavaProject project= getJavaProject(thread);
			if (project != null) {
				// get the evaluation engine
				JDIDebugTarget debugTarget= (JDIDebugTarget) thread.getDebugTarget();
				IAstEvaluationEngine evaluationEngine= JDIDebugUIPlugin.getDefault().getEvaluationEngine(project, debugTarget);
				// get the compiled expression to use
				try {
					compiledExpression= getCompiledExpression(objectValue, debugTarget, evaluationEngine);
					if (compiledExpression != null) {
						evaluationEngine.evaluateExpression(compiledExpression, objectValue, thread, evaluationListener, DebugEvent.EVALUATION_IMPLICIT, false);
						return;
					}
				} catch (DebugException e) {
					DebugUIPlugin.log(e);
					return;
				}
			}
		}
		try {
			evaluationListener.valueToString(value);
		} catch (DebugException e) {
			listener.detailComputed(value, e.getStatus().getMessage());
		}
	}
	
	private IJavaProject getJavaProject(IJavaThread thread) {
		ILaunch launch= thread.getLaunch();
		if (launch == null) {
			return null;
		}
		ISourceLocator locator= launch.getSourceLocator();
		if (locator == null)
			return null;
		
		Object sourceElement;
		try {
			IStackFrame frame = thread.getTopStackFrame();
			if (frame == null)
				return null;
			sourceElement= locator.getSourceElement(frame);
		} catch (DebugException e) {
			DebugUIPlugin.log(e);
			return null;
		}
		if (sourceElement instanceof IJavaElement) {
			return ((IJavaElement) sourceElement).getJavaProject();
		}
		if (sourceElement instanceof IResource) {
			IJavaProject project = JavaCore.create(((IResource)sourceElement).getProject());
			if (project.exists()) {
				return project;
			}
		}
		// if no source element, try the project associated with the launch - bug 27837
		ILaunchConfiguration configuration = launch.getLaunchConfiguration();
		if (configuration != null) {
			try {
				return JavaRuntime.getJavaProject(configuration);
			} catch (CoreException e) {
				JDIDebugUIPlugin.log(e);
			}
		}
		return null;
	}
	
	
	
	public boolean hasAssociatedDetailFormatter(IJavaType type) {
		return getAssociatedDetailFormatter(type) != null;
	}
	
	public DetailFormatter getAssociatedDetailFormatter(IJavaType type) {
		String typeName;
		try {
			while (type instanceof IJavaArrayType) {
				type= ((IJavaArrayType)type).getComponentType();
			}
			if (type instanceof IJavaClassType) {
				typeName= type.getName();
			} else {
				return null;
			}
		} catch (DebugException e) {
			return null;
		}
		return (DetailFormatter)fDetailFormattersMap.get(typeName);
	}
	
	public void setAssociatedDetailFormatter(DetailFormatter detailFormatter) {
		fDetailFormattersMap.put(detailFormatter.getTypeName(), detailFormatter);
		savePreference();
	}
	
	
	private void savePreference() {
		Collection valuesList= fDetailFormattersMap.values();
		String[] values= new String[valuesList.size() * 3];
		int i= 0;
		for (Iterator iter= valuesList.iterator(); iter.hasNext();) {
			DetailFormatter detailFormatter= (DetailFormatter) iter.next();
			values[i++]= detailFormatter.getTypeName();
			values[i++]= detailFormatter.getSnippet().replace(',','\u0000');
			values[i++]= detailFormatter.isEnabled() ? JavaDetailFormattersPreferencePage.DETAIL_FORMATTER_IS_ENABLED : JavaDetailFormattersPreferencePage.DETAIL_FORMATTER_IS_DISABLED;
		}
		String pref = JavaDebugOptionsManager.serializeList(values);
		JDIDebugUIPlugin.getDefault().getPreferenceStore().setValue(IJDIPreferencesConstants.PREF_DETAIL_FORMATTERS_LIST, pref);
		JDIDebugUIPlugin.getDefault().savePluginPreferences();
	}
	/**
	 * Return the detail formatter (code snippet) associate with
	 * the given type or one of its super type.
	 */
	private String getDetailFormatter(IJavaClassType type) throws DebugException {
		if (type == null) {
			return null;
		}
		String typeName= type.getName();
		if (fDetailFormattersMap.containsKey(typeName)) {
			DetailFormatter detailFormatter= (DetailFormatter)fDetailFormattersMap.get(typeName);
			if (detailFormatter.isEnabled()) {
				return detailFormatter.getSnippet();
			}
		}
		return getDetailFormatter(type.getSuperclass());
	}
	
	/**
	 * Return the compiled expression which corresponds to the code formatter associated
	 * with the type of the given object.
	 * The code snippet is compiled in the context of the given object.
	 */
	private ICompiledExpression getCompiledExpression(IJavaObject javaObject, JDIDebugTarget debugTarget, IAstEvaluationEngine evaluationEngine) throws DebugException {
		IJavaClassType type= (IJavaClassType)javaObject.getJavaType();
		String typeName= type.getName();
		Key key= new Key(typeName, debugTarget);
		if (fCacheMap.containsKey(key)) {
			return (ICompiledExpression) fCacheMap.get(key);
		} else {
			String snippet= getDetailFormatter(type);
			if (snippet != null) {
				ICompiledExpression res= evaluationEngine.getCompiledExpression(snippet, javaObject);
				fCacheMap.put(key, res);
				return res;
			}
		}
		return null;
	}
	
	/**
	 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(PropertyChangeEvent)
	 */
	public void propertyChange(PropertyChangeEvent event) {
		if (event.getProperty().equals(IJDIPreferencesConstants.PREF_DETAIL_FORMATTERS_LIST)) {
			populateDetailFormattersMap();
			fCacheMap.clear();
		}
	}
	/**
	 * @see org.eclipse.debug.core.IDebugEventSetListener#handleDebugEvents(DebugEvent[])
	 */
	public void handleDebugEvents(DebugEvent[] events) {
		for (int i = 0; i < events.length; i++) {
			DebugEvent event = events[i];
			if (event.getSource() instanceof JDIDebugTarget && event.getKind() == DebugEvent.TERMINATE) {
				deleteCacheForTarget((JDIDebugTarget) event.getSource());
			}	
		}
	}
	
	/**
	 * @see org.eclipse.debug.core.ILaunchesListener#launchesAdded(ILaunch[])
	 */
	public void launchesAdded(ILaunch[] launches) {
	}
	
	/**
	 * @see org.eclipse.debug.core.ILaunchesListener#launchesChanged(ILaunch[])
	 */
	public void launchesChanged(ILaunch[] launches) {
	}
	
	/**
	 * @see org.eclipse.debug.core.ILaunchesListener#launchesRemoved(ILaunch[])
	 */
	public void launchesRemoved(ILaunch[] launches) {
		for (int i = 0; i < launches.length; i++) {
			ILaunch launch = launches[i];
			IDebugTarget[] debugTargets= launch.getDebugTargets();
			for (int j = 0; j < debugTargets.length; j++) {
				if (debugTargets[j] instanceof JDIDebugTarget) {
					deleteCacheForTarget((JDIDebugTarget)debugTargets[j]);		
				}
			}
		}
	}
	
	/**
	 * Remove from the cache compiled expression associated with
	 * the given debug target.
	 * 
	 * @param debugTarget 
	 */
	private synchronized void deleteCacheForTarget(JDIDebugTarget debugTarget) {
		for (Iterator iter= fCacheMap.keySet().iterator(); iter.hasNext();) {
			Key key= (Key) iter.next();
			if ((key).fDebugTarget == debugTarget) {
				iter.remove();
			}
		}
	}
	
	/**
	 * Object used as the key in the cache map for associate a compiled
	 * expression with a pair type name/debug target
	 */
	static private class Key {
		private String fTypeName;
		private JDIDebugTarget fDebugTarget;
		
		Key(String typeName, JDIDebugTarget debugTarget) {
			fTypeName= typeName;
			fDebugTarget= debugTarget;
		}
		
		public boolean equals(Object obj) {
			if (obj instanceof Key) {
				Key key= (Key) obj;
				return fTypeName != null && fDebugTarget != null && fTypeName.equals(key.fTypeName) && fDebugTarget.equals(key.fDebugTarget);
			} else {
				return false;
			}
		}
		
		public int hashCode() {
			return fTypeName.hashCode() / 2 + fDebugTarget.hashCode() / 2;
		}
	}
	
	/**
	 * Listener use to manage the result of the formatter.
	 * Utilise the 'standart' pretty printer methods to return the result.
	 */
	static private class EvaluationListener implements IEvaluationListener {
		
		/**
		 * The selector of <code>java.lang.Object#toString()</code>,
		 * used to evaluate 'toString()' for displaying details of values.
		 */
		private static final String fgToString= "toString"; //$NON-NLS-1$
		
		
		/**
		 * The signature of <code>java.lang.Object#toString()</code>,
		 * used to evaluate 'toString()' for displaying details of values.
		 */
		private static final String fgToStringSignature= "()Ljava/lang/String;"; //$NON-NLS-1$
		
		private IJavaValue fValue;
		
		private IValueDetailListener fListener;
		
		private IJavaThread fThread;
		
		public EvaluationListener(IJavaValue value, IJavaThread thread, IValueDetailListener listener) {
			fValue= value;
			fThread= thread;
			fListener= listener;
		}
		
		public void evaluationComplete(IEvaluationResult result) {
			if (result.hasErrors()) {
				StringBuffer error= new StringBuffer(DebugUIMessages.getString("JavaDetailFormattersManager.Detail_formatter_error___1")); //$NON-NLS-1$
				DebugException exception= result.getException();
				if (exception != null) {
					Throwable throwable= exception.getStatus().getException();
					error.append("\n\t\t"); //$NON-NLS-1$
					if (throwable instanceof InvocationException) {
						error.append(MessageFormat.format(DebugUIMessages.getString("JavaDetailFormattersManager.An_exception_occurred__{0}_3"), new String[] {((InvocationException) throwable).exception().referenceType().name()})); //$NON-NLS-1$
					} else {
						error.append(exception.getStatus().getMessage());
					}
				} else {
					String[] errors= result.getErrorMessages();
					for (int i= 0, length= errors.length; i < length; i++) {
						error.append("\n\t\t").append(errors[i]); //$NON-NLS-1$
					}
				}
				fListener.detailComputed(fValue, error.toString());
			} else {
				try {
					valueToString(result.getValue());
				} catch (DebugException e) {
					fListener.detailComputed(fValue, e.getStatus().getMessage());
				}
			}
		}
		
		public void valueToString(final IJavaValue objectValue) throws DebugException {
			IEvaluationRunnable eval = new IEvaluationRunnable() {
				public void run(IJavaThread thread, IProgressMonitor monitor) throws DebugException {
					StringBuffer result= new StringBuffer();
					if (objectValue.getSignature() == null) {
						// no need to spawn a thread for a null fValue
						result.append(DebugUIMessages.getString("JavaDetailFormattersManager.null")); //$NON-NLS-1$
					} else if (objectValue instanceof IJavaPrimitiveValue) {
						// no need to spawn a thread for a primitive value
						appendJDIPrimitiveValueString(result, objectValue);
					} else if (fThread == null || !fThread.isSuspended()) {
						// no thread available
						result.append(DebugUIMessages.getString("JavaDetailFormattersManager.no_suspended_threads")); //$NON-NLS-1$
						appendJDIValueString(result, objectValue);
					} else if (objectValue instanceof IJavaArray) {
						appendArrayDetail(result, (IJavaArray) objectValue);
					} else if (objectValue instanceof IJavaObject) {
						appendObjectDetail(result, (IJavaObject) objectValue);
					} else {
						appendJDIValueString(result, objectValue);
					}
					fListener.detailComputed(fValue, result.toString());
				}
			};
			fThread.runEvaluation(eval, null, DebugEvent.EVALUATION_IMPLICIT, false);
		}
		
		/*
		 * Tries to use Arrays.asList() on target because List has a better toString() to 
		 * display. If not possible (or if array is of a primitive type), appendArrayDetailIndividually
		 * is called.
		 */
		protected void appendArrayDetail(StringBuffer result, IJavaArray arrayValue) throws DebugException {
			IJavaType componentType = null;
			try {
				IJavaArrayType javaArrayType = (IJavaArrayType) arrayValue.getJavaType();
				componentType = javaArrayType.getComponentType();
			} catch (DebugException de) {
				JDIDebugUIPlugin.log(de);
				result.append(de.getStatus().getMessage());
				return;	
			}
			
			if (!(componentType instanceof IJavaReferenceType)) {
				//if it is an array of primatives, cannot use Arrays.asList()
				appendArrayDetailIndividually(result, arrayValue);
				return;
			}
			
			IJavaDebugTarget target = (IJavaDebugTarget) arrayValue.getDebugTarget();
			
			//Load java.util.Arrays
			IJavaType[] types;
			try {
				types = target.getJavaTypes("java.lang.Class"); //$NON-NLS-1$
			} catch (DebugException de) {
				types = null;
			}
			
			if (types != null && types.length >0) {
				try {
					IJavaClassType type = (IJavaClassType) types[0];
					IJavaValue arg = target.newValue("java.util.Arrays"); //$NON-NLS-1$
					type.sendMessage("forName", "(Ljava/lang/String;)Ljava/lang/Class;", new IJavaValue[] {arg}, fThread);  //$NON-NLS-1$//$NON-NLS-2$
				} catch (DebugException de) {
					//java.util.Arrays didn't load properly. Can't use Arrays.asList()
					appendArrayDetailIndividually(result, arrayValue);
				}
			} else {
				//didn't get java.lang.Class, can't load java.utils.Arrays.
				appendArrayDetailIndividually(result, arrayValue);
			}
			
			types = null;
			types = target.getJavaTypes("java.util.Arrays"); //$NON-NLS-1$
			if (types != null && types.length >0) {
				IJavaClassType type = (IJavaClassType) types[0];
				IJavaObject javaObject;
				try {
					//execute Arrays.asList() on target
					javaObject = (IJavaObject) type.sendMessage("asList", "([Ljava/lang/Object;)Ljava/util/List;", new IJavaValue[] {arrayValue}, fThread); //$NON-NLS-1$ //$NON-NLS-2$
				} catch (DebugException de) {
					//asList() failed.
					appendArrayDetailIndividually(result, arrayValue);
					return;
				}
				appendObjectDetail(result, javaObject);
			} else {
				// didn't get java.util.Arrays. Can't use asList() 
				appendArrayDetailIndividually(result, arrayValue);
			}
		}
		
		/*
		 * Gets all values in array and appends the toString() if it is an array of Objects or the value if primative.
		 * NB - this method is only called by appendArrayDetail which first tries to use Arrays.asList() to minimize
		 * toString() calls on remote target (ie one call to List.toString() instead of one call per item in the array). 
		 */
		private void appendArrayDetailIndividually(StringBuffer result, IJavaArray arrayValue) throws DebugException {
			result.append('[');
			IJavaValue[] arrayValues;
			try {				
				arrayValues= arrayValue.getValues();				
			} catch (DebugException de) {
				JDIDebugUIPlugin.log(de);
				result.append(de.getStatus().getMessage());
				return;
			}
			for (int i= 0; i < arrayValues.length; i++) {
				IJavaValue value= arrayValues[i];
				if (value instanceof IJavaArray) {
					appendArrayDetail(result, (IJavaArray) value);
				} else if (value instanceof IJavaObject) {
					appendObjectDetail(result, (IJavaObject) value);
				} else {
					appendJDIValueString(result, value);
				}
				if (i < arrayValues.length - 1) {
					result.append(',');
					result.append(' ');
				}
			}
			result.append(']');	
		}
		
		protected void appendJDIPrimitiveValueString(StringBuffer result, IJavaValue value) throws DebugException {
			result.append(value.getValueString());
		}
		
		
		protected void appendJDIValueString(StringBuffer result, IJavaValue value) throws DebugException {
			result.append(value.getValueString());
		}
		
		
		protected void appendObjectDetail(StringBuffer result, IJavaObject objectValue) throws DebugException {
			IJavaValue toStringValue= objectValue.sendMessage(EvaluationListener.fgToString, EvaluationListener.fgToStringSignature, null, fThread, false);
			if (toStringValue == null) {
				result.append(DebugUIMessages.getString("JavaDetailFormattersManager.<unknown>")); //$NON-NLS-1$
			} else {
				appendJDIValueString(result, toStringValue);
			}
		}
		
		
		
	}
	
	/**
	 * (non java-doc)
	 * Remove the provided <code>detailFormatter</code> from the map
	 * @param detailFormatter
	 */
	public void removeAssociatedDetailFormatter(DetailFormatter detailFormatter) {
		fDetailFormattersMap.remove(detailFormatter.getTypeName());
		savePreference();
	}
	
}
