/*******************************************************************************
 * Copyright (c) 2006, 2012 Wind River Systems, Inc. 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:
 *     Markus Schorn - initial API and implementation
 *     Andrew Ferguson (Symbian)
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.core.testplugin.util;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ILinkage;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexFile;
import org.eclipse.cdt.core.index.IIndexFileLocation;
import org.eclipse.cdt.core.index.IndexLocationFactory;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.testplugin.CTestPlugin;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.junit.Assert;
import org.osgi.framework.Bundle;

/**
 * Utilities for reading test source code from plug-in .java sources
 */
public class TestSourceReader {
	private final Bundle bundle;
	private final String srcRoot;
	private final Class clazz;
	private final int numSections;

	/**
	 * @param bundle the bundle containing the source, if {@code null} can try to load using
	 *     classpath (source folder has to be in the classpath for this to work)
	 * @param srcRoot the directory inside the bundle containing the packages
	 * @param clazz the name of the class containing the test
	 */
	public TestSourceReader(Bundle bundle, String srcRoot, Class clazz) {
		this(bundle, srcRoot, clazz, 0);
	}

	/**
	 * @param bundle the bundle containing the source, if {@code null} can try to load using
	 *     classpath (source folder has to be in the classpath for this to work)
	 * @param srcRoot the directory inside the bundle containing the packages
	 * @param clazz the name of the class containing the test
	 * @param numSections the number of comment sections preceding the named test to return.
	 *     Pass zero to get all available sections.
	 */
	public TestSourceReader(Bundle bundle, String srcRoot, Class clazz, int numSections) {
		this.bundle = bundle;
		this.srcRoot = srcRoot;
		this.clazz = clazz;
		this.numSections = numSections;
	}

	public StringBuilder[] getContentsForTest(final String testName) throws IOException {
		return getContentsForTest(bundle, srcRoot, clazz, testName, numSections);
	}

	public String readTaggedComment(String tag) throws IOException {
		return readTaggedComment(bundle, tag, clazz, tag);
	}

	/**
	 * Returns an array of StringBuilder objects for each comment section found preceding the named
	 * test in the source code.
	 *
	 * @param bundle the bundle containing the source, if {@code null} can try to load using
	 *      classpath (source folder has to be in the classpath for this to work)
	 * @param srcRoot the directory inside the bundle containing the packages
	 * @param clazz the name of the class containing the test
	 * @param testName the name of the test
	 * @param numSections the number of comment sections preceding the named test to return.
	 *     Pass zero to get all available sections.
	 * @return an array of StringBuilder objects for each comment section found preceding the named
	 *     test in the source code.
	 * @throws IOException
	 */
	public static StringBuilder[] getContentsForTest(Bundle bundle, String srcRoot, Class clazz,
			final String testName, int numSections) throws IOException {
		// Walk up the class inheritance chain until we find the test method.
		try {
			while (clazz.getMethod(testName).getDeclaringClass() != clazz) {
				clazz = clazz.getSuperclass();
			}
		} catch (SecurityException e) {
			Assert.fail(e.getMessage());
		} catch (NoSuchMethodException e) {
			Assert.fail(e.getMessage());
		}

		while (true) {
			// Find and open the .java file for the class clazz.
			String fqn = clazz.getName().replace('.', '/');
			fqn = fqn.indexOf("$") == -1 ? fqn : fqn.substring(0, fqn.indexOf("$"));
			String classFile = fqn + ".java";
			IPath filePath= new Path(srcRoot + '/' + classFile);

			InputStream in;
			Class superclass = clazz.getSuperclass();
			try {
				if (bundle != null) {
					in = FileLocator.openStream(bundle, filePath, false);
				} else {
					in = clazz.getResourceAsStream('/' + classFile);
				}
			} catch (IOException e) {
				if (superclass == null || !superclass.getPackage().equals(clazz.getPackage())) {
					throw e;
				}
				clazz = superclass;
				continue;
			}

		    BufferedReader br = new BufferedReader(new InputStreamReader(in));
		    try {
		    	// Read the java file collecting comments until we encounter the test method.
			    List<StringBuilder> contents = new ArrayList<StringBuilder>();
			    StringBuilder content = new StringBuilder();
			    for (String line = br.readLine(); line != null; line = br.readLine()) {
			    	line = line.replaceFirst("^\\s*", ""); // Replace leading whitespace, preserve trailing
			    	if (line.startsWith("//")) {
			    		content.append(line.substring(2) + "\n");
			    	} else {
			    		if (!line.startsWith("@") && content.length() > 0) {
			    			contents.add(content);
			    			if (numSections > 0 && contents.size() == numSections + 1)
			    				contents.remove(0);
			    			content = new StringBuilder();
			    		}
			    		if (line.length() > 0 && !contents.isEmpty()) {
			    			int idx= line.indexOf(testName);
			    			if (idx != -1 && !Character.isJavaIdentifierPart(line.charAt(idx + testName.length()))) {
			    				return contents.toArray(new StringBuilder[contents.size()]);
			    			}
			    			if (!line.startsWith("@")) {
			    				contents.clear();
			    			}
			    		}
			    	}
			    }
		    } finally {
		    	br.close();
		    }

			if (superclass == null || !superclass.getPackage().equals(clazz.getPackage())) {
			    throw new IOException("Test data not found for " + clazz.getName() + "." + testName);
			}
			clazz = superclass;
		}
	}

	/**
	 * Searches for the offset of the first occurrence of a string in a workspace file.
	 * @param lookfor string to be searched for
	 * @param fullPath full path of the workspace file
	 * @return the offset or -1
	 * @throws Exception
	 * @throws UnsupportedEncodingException
	 * @since 4.0
	 */
	public static int indexOfInFile(String lookfor, Path fullPath) throws Exception {
		IFile file= ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath);
		Reader reader= new BufferedReader(new InputStreamReader(file.getContents(), file.getCharset()));
		Assert.assertTrue(lookfor.indexOf('\n') == -1);
		try {
			int c= 0;
			int offset= 0;
			StringBuilder buf= new StringBuilder();
			while ((c = reader.read()) >= 0) {
				buf.append((char) c);
				if (c == '\n') {
					int idx= buf.indexOf(lookfor);
					if (idx >= 0) {
						return idx + offset;
					}
					offset += buf.length();
					buf.setLength(0);
				}
			}
			int idx= buf.indexOf(lookfor);
			if (idx >= 0) {
				return idx + offset;
			}
			return -1;
		} finally {
			reader.close();
		}
	}

	public static int getLineNumber(int offset, Path fullPath) throws Exception {
		IFile file= ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath);
		Reader reader= new BufferedReader(new InputStreamReader(file.getContents(), file.getCharset()));
		try {
			int line = 1;
			for (int i = 0; i < offset; i++) {
				int c= reader.read();
				Assert.assertTrue(c >= 0);
				if (c == '\n')
					line++;
			}
			return line;
		} finally {
			reader.close();
		}
	}

	/**
	 * Reads a section in comments form the source of the given class. The section
	 * is started with '// {tag}' and ends with the first line not started by '//'
	 * @since 4.0
	 */
	public static String readTaggedComment(Bundle bundle, String srcRoot, Class clazz, final String tag) throws IOException {
	    IPath filePath= new Path(srcRoot + '/' + clazz.getName().replace('.', '/') + ".java");

	    InputStream in= FileLocator.openStream(bundle, filePath, false);
	    LineNumberReader reader= new LineNumberReader(new InputStreamReader(in));
	    boolean found= false;
	    final StringBuilder content= new StringBuilder();
	    try {
	        String line= reader.readLine();
	        while (line != null) {
	        	line= line.trim();
	            if (line.startsWith("//")) {
	                line= line.substring(2);
	                if (found) {
	                    content.append(line);
	                    content.append('\n');
	                } else {
	                    line= line.trim();
	                    if (line.startsWith("{" + tag)) {
	                        if (line.length() == tag.length() + 1 ||
	                                !Character.isJavaIdentifierPart(line.charAt(tag.length() + 1))) {
	                            found= true;
	                        }
	                    }
	                }
	            } else if (found) {
	                break;
	            }
	            line= reader.readLine();
	        }
	    } finally {
	        reader.close();
	    }
	    Assert.assertTrue("Tag '" + tag + "' is not defined inside of '" + filePath + "'.", found);
	    return content.toString();
	}

	/**
	 * Creates a file with content at the given path inside the given container.
	 * If the file exists its content is replaced.
	 * @param container a container to create the file in
	 * @param filePath the path relative to the container to create the file at
	 * @param contents the content for the file
	 * @return a file object.
	 * @throws CoreException
	 * @since 4.0
	 */
	public static IFile createFile(final IContainer container, final IPath filePath,
			final CharSequence contents) throws CoreException {
		final IWorkspace ws = ResourcesPlugin.getWorkspace();
		final IFile result[] = new IFile[1];
		ws.run(new IWorkspaceRunnable() {
			@Override
			public void run(IProgressMonitor monitor) throws CoreException {
				// Obtain file handle
				IFile file = container.getFile(filePath);

				InputStream stream;
				try {
					stream = new ByteArrayInputStream(contents.toString().getBytes("UTF-8"));
				} catch (UnsupportedEncodingException e) {
					throw new CoreException(new Status(IStatus.ERROR, CTestPlugin.PLUGIN_ID, null, e));
				}
				// Create file input stream
				if (file.exists()) {
					long timestamp= file.getLocalTimeStamp();
					file.setContents(stream, false, false, new NullProgressMonitor());
					if (file.getLocalTimeStamp() == timestamp) {
						file.setLocalTimeStamp(timestamp + 1000);
					}
				} else {
					createFolders(file);
					file.create(stream, true, new NullProgressMonitor());
				}
				result[0]= file;
			}

			private void createFolders(IResource res) throws CoreException {
				IContainer container= res.getParent();
				if (!container.exists() && container instanceof IFolder) {
					createFolders(container);
					((IFolder) container).create(true, true, new NullProgressMonitor());
				}
			}
		}, null);
		return result[0];
	}

	/**
	 * Creates a file with content at the given path inside the given container.
	 * If the file exists its content is replaced.
	 * @param container a container to create the file in
	 * @param filePath the path relative to the container to create the file at
	 * @param contents the content for the file
	 * @return a file object.
	 * @since 4.0
	 */
	public static IFile createFile(IContainer container, String filePath, String contents) throws CoreException {
		return createFile(container, new Path(filePath), contents);
	}

	/**
	 * Waits until the given file is indexed. Fails if this does not happen within the
	 * given time.
	 * @param file
	 * @param maxmillis
	 * @throws Exception
	 * @since 4.0
	 */
	public static void waitUntilFileIsIndexed(IIndex index, IFile file, int maxmillis) throws Exception {
		long fileTimestamp = file.getLocalTimeStamp();
		IIndexFileLocation indexFileLocation = IndexLocationFactory.getWorkspaceIFL(file);

		long endTime= System.currentTimeMillis() + maxmillis;
		int timeLeft= maxmillis;
		while (timeLeft >= 0) {
			Assert.assertTrue(CCorePlugin.getIndexManager().joinIndexer(timeLeft, new NullProgressMonitor()));
			index.acquireReadLock();
			try {
				IIndexFile[] files= index.getFiles(ILinkage.CPP_LINKAGE_ID, indexFileLocation);
				if (files.length > 0 && areAllFilesNotOlderThan(files, fileTimestamp)) {
					Assert.assertTrue(CCorePlugin.getIndexManager().joinIndexer(timeLeft, new NullProgressMonitor()));
					return;
				}
				files= index.getFiles(ILinkage.C_LINKAGE_ID, indexFileLocation);
				if (files.length > 0 && areAllFilesNotOlderThan(files, fileTimestamp)) {
					Assert.assertTrue(CCorePlugin.getIndexManager().joinIndexer(timeLeft, new NullProgressMonitor()));
					return;
				}
			} finally {
				index.releaseReadLock();
			}

			Thread.sleep(50);
			timeLeft= (int) (endTime - System.currentTimeMillis());
		}
		Assert.fail("Indexing of " + file.getFullPath() + " did not complete in " + maxmillis / 1000. + " sec");
	}

	private static boolean areAllFilesNotOlderThan(IIndexFile[] files, long timestamp) throws CoreException {
		for (IIndexFile file : files) {
			if (file.getTimestamp() < timestamp) {
				return false;
			}
		}
		return true;
	}

	public static IASTTranslationUnit createIndexBasedAST(IIndex index, ICProject project, IFile file) throws CModelException, CoreException {
    	ICElement elem= project.findElement(file.getFullPath());
    	if (elem instanceof ITranslationUnit) {
    		ITranslationUnit tu= (ITranslationUnit) elem;
   			return tu.getAST(index, ITranslationUnit.AST_SKIP_INDEXED_HEADERS);
    	}
    	Assert.fail("Could not create AST for " + file.getFullPath());
    	return null;
    }
}
