/*******************************************************************************
 * 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.core.hcr;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFileState;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.IProblemFactory;
import org.eclipse.jdt.internal.compiler.SourceElementParser;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities;
import org.eclipse.jdt.internal.core.JavaElement;

/**
 * A <code>CompilationUnitDelta</code> represents the source code changes between
 * a CU in the workspace and the same CU at some point in the past
 * (from the local history).
 * <p>
 * This functionality is used in the context of Hot Code Replace
 * to determine which stack frames are affected (and need to be dropped)
 * by a class reload in the Java VM.
 * <p>
 * Typically a <code>CompilationUnitDelta</code> object is generated for a CU
 * when the associated class is replaced in the VM.
 * The <code>CompilationUnitDelta</code> calculates
 * the differences between the current version of the CU and the version in the
 * local history that was effective at the given <bold>last</bold> build time.
 * The differences are stored as a tree which allows for an efficient implementation
 * of a <code>hasChanged(IMember)</code> method.
 */
public class CompilationUnitDelta {
	
	private static final boolean DEBUG= false;
	
	private static class SimpleJavaElement {
		
		private String fName;
		private HashMap fChildren;
		
		SimpleJavaElement(SimpleJavaElement parent, String name) {
			fName= name;
			if (parent != null) {
				if (parent.fChildren == null) {
					parent.fChildren= new HashMap();
				}
				parent.fChildren.put(name, this);
			}
		}
		
		void dump(int level) {
			for (int i= 0; i < level; i++)	
				System.out.print("  ");	//$NON-NLS-1$
			System.out.println(fName);
			
			if (fChildren != null) {
				Iterator iter= fChildren.values().iterator();
				while (iter.hasNext()) {
					SimpleJavaElement e= (SimpleJavaElement) iter.next();
					e.dump(level+1);
				}
			}
		}
		
		boolean find(String[] path, int start) {
			if (start >= path.length) {
				return true;
			}
			String key= path[start];
			if (fChildren != null) {
				SimpleJavaElement child= (SimpleJavaElement) fChildren.get(key);
				if (child != null) {
					return child.find(path, start+1);
				}
			}
			return false;
		}
	}
	
	/**
	 * Used to bail out from ProblemFactory.
	 */
	private static class ParseError extends Error {
	}
	
	/**
	 * This problem factory aborts parsing on first error.
	 */
	private static class ProblemFactory implements IProblemFactory {
		
		public IProblem createProblem(char[] originatingFileName, int problemId, String[] problemArguments, String[] messageArguments, int severity, int startPosition, int endPosition, int lineNumber) {
			if ((severity & ProblemSeverities.Error) > 0) {
				throw new ParseError();
			}
			return null;
		}
		
		public Locale getLocale() {
			return Locale.getDefault();
		}
		
		public String getLocalizedMessage(int problemId, String[] problemArguments) {
			return "" + problemId; //$NON-NLS-1$
		}
	}

	private SimpleJavaElement fRoot;
	private boolean fHasHistory= false;
	
	/**
	 * Creates a new <code>CompilationUnitDelta object that calculates and stores
	 * the changes of the given CU since some point in time.
	 */
	public CompilationUnitDelta(ICompilationUnit cu, long timestamp) throws CoreException {
		
		if (cu.isWorkingCopy()) {
			cu= cu.getPrimary();
		}
		
		// find underlying file
		IFile file= (IFile) cu.getUnderlyingResource();

		// get available editions
		IFileState[] states= file.getHistory(null);
		if (states == null || states.length <= 0) {
			return;
		}
		fHasHistory= true;
		
		IFileState found= null;
		// find edition just before the given time stamp
		for (int i= 0; i < states.length; i++) {
			IFileState state= states[i];
			long d= state.getModificationTime();
			if (d < timestamp) {
				found= state;
				break;
			}
		}
		
		if (found == null) {
			found= states[states.length-1];
		}
		
		InputStream oldContents= null;
		InputStream newContents= null;
		try {
			oldContents= found.getContents();
			newContents= file.getContents();
		} catch (CoreException ex) {
			return;
		}
		
		JavaNode oldStructure= parse(oldContents);
		JavaNode newStructure= parse(newContents);
		
		final boolean[] memberDeleted= new boolean[1];	// visitor returns result here
		
		Differencer differencer= new Differencer() {
			protected Object visit(Object data, int result, Object ancestor, Object left, Object right) {
				String name= null;
				switch (result) {
				case Differencer.CHANGE:
					name= ((JavaNode)left).getId();
					break;
				case Differencer.ADDITION:
					name= ((JavaNode)right).getId();
					break;
				case Differencer.DELETION:
					name= ((JavaNode)left).getId();
					memberDeleted[0]= true;
					break;
				default:
					break;
				}
				if (name != null) {
					return new SimpleJavaElement((SimpleJavaElement) data, name);
				}
				return null;
			}
			protected boolean contentsEqual(Object o1, Object o2) {
				String s1= ((JavaNode) o1).getContents();
				String s2= ((JavaNode) o2).getContents();
				return s1.equals(s2);
			}
			protected Object[] getChildren(Object input) {
				if (input instanceof JavaNode) {
					return ((JavaNode)input).getChildren();
				}
				return null;
			}
		};
		
		fRoot= (SimpleJavaElement) differencer.findDifferences(false, null, null, null, oldStructure, newStructure);
		fHasHistory= (fRoot != null); // if no changes pretend that we had no history

		if (DEBUG) {
			if (fRoot != null) {
				fRoot.dump(0);
			}
		}
			
		if (memberDeleted[0]) {	// shape change because of deleted members
			fRoot= null;	// throw diffs away since hasChanged(..) must always return true
		}
	}
	
	/**
	 * Returns <code>true</code>
	 * <ul>
	 * <li>if the source of the given member has been changed, or
	 * <li>if the element has been deleted, or
	 * <li>if the element has been newly created
	 * </ul>
	 * after the initial timestamp.
	 * 
	 * @exception AssertionFailedException if member is null or member is not a member of this CU.
	 */
	public boolean hasChanged(IMember member) {
		//Assert.isNotNull(member);
		ICompilationUnit cu= member.getCompilationUnit();
		if (cu.isWorkingCopy()) {
			cu= cu.getPrimary();
		}
		//Assert.isTrue(cu.equals(fCompilationUnit));
		
		if (fRoot == null) {
			if (fHasHistory) {
				return true;	// pessimistic: we have a history but we couldn't use it for some reason
			}
			return false;	// optimistic: we have no history, so assume that member hasn't changed
		}
		
		String[] path= createPath(member);
		
		if (DEBUG) {
			for (int i= 0; i < path.length; i++) {	
				System.out.print(path[i] + ' ');
			}
			System.out.println();
		}
		
		return fRoot.find(path, 0);
	}
	
	//---- private stuff ----------------------------------------------------------------
	
	/**
	 * Parses the given input stream and returns a tree of JavaNodes
	 * or a null in case of failure.
	 */
	private static JavaNode parse(InputStream input) {
		
		char[] buffer= readString(input);		
		if (buffer != null) {									
			JavaNode root= new JavaNode(buffer);
			JavaParseTreeBuilder builder= new JavaParseTreeBuilder(root, buffer);
			SourceElementParser parser= new SourceElementParser(builder, new ProblemFactory(), new CompilerOptions(JavaCore.getOptions()));
			try {
				parser.parseCompilationUnit(builder, false);
			} catch (ParseError ex) {
				// parse error: bail out
				return null;
			}
			return root;
		} 
		return null;
	}
			
	private static String[] createPath(IJavaElement je) {
			
		// build a path starting at the given Java element and walk
		// up the parent chain until we reach an ICompilationUnit
		List args= new ArrayList();
		while (je != null) {
			// each path component has a name that uses the same
			// conventions as a JavaNode name
			String name= getJavaElementID(je);
			if (name == null)
				return null;
			args.add(name);
			if (je instanceof ICompilationUnit) {
				break;
			}
			je= je.getParent();
		}
		
		// revert the path
		int n= args.size();
		String[] path= new String[n];
		for (int i= 0; i < n; i++) {
			path[i]= (String) args.get(n-1-i);
		}
			
		return path;
	}
	
	/**
	 * Returns a name for the given Java element that uses the same conventions
	 * as the JavaNode name of a corresponding element.
	 */
	private static String getJavaElementID(IJavaElement je) {
		
		if (je instanceof IMember && ((IMember)je).isBinary()) {
			return null;
		}
			
		StringBuffer sb= new StringBuffer();
		
		switch (je.getElementType()) {
		case IJavaElement.COMPILATION_UNIT:
			sb.append(JavaElement.JEM_COMPILATIONUNIT);
			break;
		case IJavaElement.TYPE:
			sb.append(JavaElement.JEM_TYPE);
			sb.append(je.getElementName());
			break;
		case IJavaElement.FIELD:
			sb.append(JavaElement.JEM_FIELD);
			sb.append(je.getElementName());
			break;
		case IJavaElement.METHOD:
			sb.append(JavaElement.JEM_METHOD);
			
			IMethod method= (IMethod) je;			
			sb.append(method.getElementName());
			
			// parameters
			sb.append('(');
			
			String[] types= method.getParameterTypes();
			int nParams= types != null ? types.length : 0;
			
			for (int i= 0; i < nParams; i++) {
				if (i > 0)
					sb.append(", "); //$NON-NLS-1$
				if (types != null)
					sb.append(unqualifyName(Signature.getSimpleName(Signature.toString(types[i]))));
			}
			sb.append(')');
			break;
		case IJavaElement.INITIALIZER:
			String id= je.getHandleIdentifier();
			int pos= id.lastIndexOf(JavaElement.JEM_INITIALIZER);
			if (pos >= 0)
				sb.append(id.substring(pos));
			break;
		case IJavaElement.PACKAGE_DECLARATION:
			sb.append(JavaElement.JEM_PACKAGEDECLARATION);
			break;
		case IJavaElement.IMPORT_CONTAINER:
			sb.append('<');
			break;
		case IJavaElement.IMPORT_DECLARATION:
			sb.append(JavaElement.JEM_IMPORTDECLARATION);
			sb.append(je.getElementName());			
			break;
		default:
			return null;
		}
		return sb.toString();
	}
	
	private static String unqualifyName(String qualifiedName) {
		int index= qualifiedName.lastIndexOf('/');
		if (index > -1) {
			return qualifiedName.substring(index + 1);
		}
		return qualifiedName;
	}

	/**
	 * Returns null if an error occurred.
	 */
	private static char[] readString(InputStream is) {
		if (is == null) {
			return null;
		}
		BufferedReader reader= null;
		try {
			StringBuffer buffer= new StringBuffer();
			char[] part= new char[2048];
			int read= 0;
			reader= new BufferedReader(new InputStreamReader(is, ResourcesPlugin.getEncoding()));

			while ((read= reader.read(part)) != -1) {
				buffer.append(part, 0, read);
			}
			
			char[] b= new char[buffer.length()];
			buffer.getChars(0, b.length, b, 0);
			return b;
			
		} catch (IOException ex) {
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException ex) {
				}
			}
		}
		return null;
	}
}
