/*******************************************************************************
 * Copyright (c) 2000, 2013 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Yevgen Kogan - Bug 403475 - Hot Code Replace drops too much frames in some cases
 *******************************************************************************/
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 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.dom.AST;
import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;

/**
 * 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.
 */
public class CompilationUnitDelta {

	/**
	 * AST of the current source code
	 */
	private CompilationUnit fCurrentAst;
	/**
	 * AST of the previous source code
	 */
	private CompilationUnit fPrevAst;

	/**
	 * AST parser
	 */
	private ASTParser fParser = null;

	/**
	 * AST matcher
	 */
	private ASTMatcher fMatcher = null;

	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 (IFileState state : states) {
			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;
		}

		fPrevAst = parse(oldContents, cu);
		fCurrentAst = parse(newContents, cu);
	}

	/**
	 * 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.
	 */
	public boolean hasChanged(String className, String methodName, String signature) {
		if (!fHasHistory) {
			return false; // optimistic: we have no history, so assume that
							// member hasn't changed
		}
		if (fPrevAst == null || fCurrentAst == null) {
			return true; // pessimistic: unable to build parse trees
		}
		MethodSearchVisitor visitor = new MethodSearchVisitor();
		MethodDeclaration prev = findMethod(fPrevAst, visitor, className, methodName,
				signature);
		if (prev != null) {
			MethodDeclaration curr = findMethod(fCurrentAst, visitor, className, methodName, signature);
			if (curr != null) {
				return !getMatcher().match(prev, curr);
			}
		}
		return true;
	}

	private MethodDeclaration findMethod(CompilationUnit cu,
			MethodSearchVisitor visitor, String className, String name, String signature) {
		visitor.setTargetMethod(className, name, signature);
		cu.accept(visitor);
		return visitor.getMatch();
	}

	// ---- private stuff
	// ----------------------------------------------------------------

	/**
	 * Parses the given input stream and returns a tree of JavaNodes or a null
	 * in case of failure.
	 */
	private CompilationUnit parse(InputStream input, ICompilationUnit cu) {
		char[] buffer = readString(input);
		if (buffer != null) {
			if (fParser == null) {
				fParser = ASTParser.newParser(AST.getJLSLatest());
			}
			fParser.setSource(buffer);
			fParser.setProject(cu.getJavaProject());
			fParser.setResolveBindings(true);
			fParser.setKind(ASTParser.K_COMPILATION_UNIT);
			fParser.setUnitName(cu.getElementName());
			return (CompilationUnit) fParser.createAST(null);
		}
		return null;
	}

	/**
	 * Returns an AST matcher
	 *
	 * @return AST matcher
	 */
	private ASTMatcher getMatcher() {
		if (fMatcher == null) {
			fMatcher = new ASTMatcher();
		}
		return fMatcher;
	}

	/**
	 * Returns null if an error occurred.
	 */
	private char[] readString(InputStream is) {
		if (is == null) {
			return null;
		}
		BufferedReader reader = null;
		try {
			StringBuilder buffer = new StringBuilder();
			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;
	}
}
