/*******************************************************************************
 * 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 net.sourceforge.phpdt.core;

import java.util.StringTokenizer;

import net.sourceforge.phpdt.core.compiler.CharOperation;
import net.sourceforge.phpdt.core.compiler.ITerminalSymbols;
import net.sourceforge.phpdt.core.compiler.InvalidInputException;
import net.sourceforge.phpdt.internal.compiler.parser.Scanner;
import net.sourceforge.phpdt.internal.core.ClasspathEntry;
import net.sourceforge.phpdt.internal.core.JavaModelStatus;
import net.sourceforge.phpdt.internal.core.util.Util;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

/**
 * Provides methods for checking Java-specific conventions such as name syntax.
 * <p>
 * This class provides static methods and constants only; it is not intended to
 * be instantiated or subclassed by clients.
 * </p>
 */
public final class JavaConventions {

	private final static char DOT = '.';

	private final static Scanner SCANNER = new Scanner();

	private JavaConventions() {
		// Not instantiable
	}

	/**
	 * Returns whether the given package fragment root paths are considered to
	 * overlap.
	 * <p>
	 * Two root paths overlap if one is a prefix of the other, or they point to
	 * the same location. However, a JAR is allowed to be nested in a root.
	 * 
	 * @param rootPath1
	 *            the first root path
	 * @param rootPath2
	 *            the second root path
	 * @return true if the given package fragment root paths are considered to
	 *         overlap, false otherwise
	 * @deprecated Overlapping roots are allowed in 2.1
	 */
	public static boolean isOverlappingRoots(IPath rootPath1, IPath rootPath2) {
		if (rootPath1 == null || rootPath2 == null) {
			return false;
		}
		// String extension1 = rootPath1.getFileExtension();
		// String extension2 = rootPath2.getFileExtension();
		// if (extension1 != null &&
		// (extension1.equalsIgnoreCase(SuffixConstants.EXTENSION_JAR) ||
		// extension1.equalsIgnoreCase(SuffixConstants.EXTENSION_ZIP))) {
		// return false;
		// }
		// if (extension2 != null &&
		// (extension2.equalsIgnoreCase(SuffixConstants.EXTENSION_JAR) ||
		// extension2.equalsIgnoreCase(SuffixConstants.EXTENSION_ZIP))) {
		// return false;
		// }
		return rootPath1.isPrefixOf(rootPath2)
				|| rootPath2.isPrefixOf(rootPath1);
	}

	/*
	 * Returns the current identifier extracted by the scanner (without unicode
	 * escapes) from the given id. Returns <code>null</code> if the id was not
	 * valid
	 */
	private static synchronized char[] scannedIdentifier(String id) {
		if (id == null) {
			return null;
		}
		String trimmed = id.trim();
		if (!trimmed.equals(id)) {
			return null;
		}
		try {
			SCANNER.setSource(id.toCharArray());
			int token = SCANNER.getNextToken();
			char[] currentIdentifier;
			try {
				currentIdentifier = SCANNER.getCurrentIdentifierSource();
			} catch (ArrayIndexOutOfBoundsException e) {
				return null;
			}
			int nextToken = SCANNER.getNextToken();
			if (token == ITerminalSymbols.TokenNameIdentifier
					&& nextToken == ITerminalSymbols.TokenNameEOF
					&& SCANNER.startPosition == SCANNER.source.length) { // to
																			// handle
																			// case
																			// where
																			// we
																			// had
																			// an
																			// ArrayIndexOutOfBoundsException
				// while reading the last token
				return currentIdentifier;
			} else {
				return null;
			}
		} catch (InvalidInputException e) {
			return null;
		}
	}

	/**
	 * Validate the given compilation unit name. A compilation unit name must
	 * obey the following rules:
	 * <ul>
	 * <li> it must not be null
	 * <li> it must include the <code>".java"</code> suffix
	 * <li> its prefix must be a valid identifier
	 * <li> it must not contain any characters or substrings that are not valid
	 * on the file system on which workspace root is located.
	 * </ul>
	 * </p>
	 * 
	 * @param name
	 *            the name of a compilation unit
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as a compilation unit name, otherwise a status
	 *         object indicating what is wrong with the name
	 */
	public static IStatus validateCompilationUnitName(String name) {
		if (name == null) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.unit.nullName"), null); //$NON-NLS-1$
		}
		if (!net.sourceforge.phpdt.internal.compiler.util.Util
				.isJavaFileName(name)) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.unit.notJavaName"), null); //$NON-NLS-1$
		}
		String identifier;
		int index;
		index = name.lastIndexOf('.');
		if (index == -1) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.unit.notJavaName"), null); //$NON-NLS-1$
		}
		identifier = name.substring(0, index);
		// JSR-175 metadata strongly recommends "package-info.java" as the
		// file in which to store package annotations and
		// the package-level spec (replaces package.html)
		if (!identifier.equals("package-info")) { //$NON-NLS-1$
			IStatus status = validateIdentifier(identifier);
			if (!status.isOK()) {
				return status;
			}
		}
		IStatus status = ResourcesPlugin.getWorkspace().validateName(name,
				IResource.FILE);
		if (!status.isOK()) {
			return status;
		}
		return JavaModelStatus.VERIFIED_OK;
	}

	/**
	 * Validate the given .class file name. A .class file name must obey the
	 * following rules:
	 * <ul>
	 * <li> it must not be null
	 * <li> it must include the <code>".class"</code> suffix
	 * <li> its prefix must be a valid identifier
	 * <li> it must not contain any characters or substrings that are not valid
	 * on the file system on which workspace root is located.
	 * </ul>
	 * </p>
	 * 
	 * @param name
	 *            the name of a .class file
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as a .class file name, otherwise a status object
	 *         indicating what is wrong with the name
	 * @since 2.0
	 */
	// public static IStatus validateClassFileName(String name) {
	// if (name == null) {
	// return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1,
	// Util.bind("convention.classFile.nullName"), null); //$NON-NLS-1$
	// }
	// if
	// (!net.sourceforge.phpdt.internal.compiler.util.Util.isClassFileName(name))
	// {
	// return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1,
	// Util.bind("convention.classFile.notClassFileName"), null); //$NON-NLS-1$
	// }
	// String identifier;
	// int index;
	// index = name.lastIndexOf('.');
	// if (index == -1) {
	// return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1,
	// Util.bind("convention.classFile.notClassFileName"), null); //$NON-NLS-1$
	// }
	// identifier = name.substring(0, index);
	// IStatus status = validateIdentifier(identifier);
	// if (!status.isOK()) {
	// return status;
	// }
	// status = ResourcesPlugin.getWorkspace().validateName(name,
	// IResource.FILE);
	// if (!status.isOK()) {
	// return status;
	// }
	// return JavaModelStatus.VERIFIED_OK;
	// }
	/**
	 * Validate the given field name.
	 * <p>
	 * Syntax of a field name corresponds to VariableDeclaratorId (JLS2 8.3).
	 * For example, <code>"x"</code>.
	 * 
	 * @param name
	 *            the name of a field
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as a field name, otherwise a status object
	 *         indicating what is wrong with the name
	 */
	public static IStatus validateFieldName(String name) {
		return validateIdentifier(name);
	}

	/**
	 * Validate the given Java identifier. The identifier must not have the same
	 * spelling as a Java keyword, boolean literal (<code>"true"</code>,
	 * <code>"false"</code>), or null literal (<code>"null"</code>). See
	 * section 3.8 of the <em>Java Language Specification, Second Edition</em>
	 * (JLS2). A valid identifier can act as a simple type name, method name or
	 * field name.
	 * 
	 * @param id
	 *            the Java identifier
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         identifier is a valid Java identifier, otherwise a status object
	 *         indicating what is wrong with the identifier
	 */
	public static IStatus validateIdentifier(String id) {
		if (scannedIdentifier(id) != null) {
			return JavaModelStatus.VERIFIED_OK;
		} else {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util.bind(
					"convention.illegalIdentifier", id), null); //$NON-NLS-1$
		}
	}

	/**
	 * Validate the given import declaration name.
	 * <p>
	 * The name of an import corresponds to a fully qualified type name or an
	 * on-demand package name as defined by ImportDeclaration (JLS2 7.5). For
	 * example, <code>"java.util.*"</code> or
	 * <code>"java.util.Hashtable"</code>.
	 * 
	 * @param name
	 *            the import declaration
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as an import declaration, otherwise a status object
	 *         indicating what is wrong with the name
	 */
	public static IStatus validateImportDeclaration(String name) {
		if (name == null || name.length() == 0) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.import.nullImport"), null); //$NON-NLS-1$
		}
		if (name.charAt(name.length() - 1) == '*') {
			if (name.charAt(name.length() - 2) == '.') {
				return validatePackageName(name.substring(0, name.length() - 2));
			} else {
				return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
						.bind("convention.import.unqualifiedImport"), null); //$NON-NLS-1$
			}
		}
		return validatePackageName(name);
	}

	/**
	 * Validate the given Java type name, either simple or qualified. For
	 * example, <code>"java.lang.Object"</code>, or <code>"Object"</code>.
	 * <p>
	 * 
	 * @param name
	 *            the name of a type
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as a Java type name, a status with code
	 *         <code>IStatus.WARNING</code> indicating why the given name is
	 *         discouraged, otherwise a status object indicating what is wrong
	 *         with the name
	 */
	public static IStatus validateJavaTypeName(String name) {
		if (name == null) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.type.nullName"), null); //$NON-NLS-1$
		}
		String trimmed = name.trim();
		if (!name.equals(trimmed)) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.type.nameWithBlanks"), null); //$NON-NLS-1$
		}
		int index = name.lastIndexOf('.');
		char[] scannedID;
		if (index == -1) {
			// simple name
			scannedID = scannedIdentifier(name);
		} else {
			// qualified name
			String pkg = name.substring(0, index).trim();
			IStatus status = validatePackageName(pkg);
			if (!status.isOK()) {
				return status;
			}
			String type = name.substring(index + 1).trim();
			scannedID = scannedIdentifier(type);
		}

		if (scannedID != null) {
			IStatus status = ResourcesPlugin.getWorkspace().validateName(
					new String(scannedID), IResource.FILE);
			if (!status.isOK()) {
				return status;
			}
			if (CharOperation.contains('$', scannedID)) {
				return new Status(IStatus.WARNING, JavaCore.PLUGIN_ID, -1, Util
						.bind("convention.type.dollarName"), null); //$NON-NLS-1$
			}
			if ((scannedID.length > 0 && Character.isLowerCase(scannedID[0]))) {
				return new Status(IStatus.WARNING, JavaCore.PLUGIN_ID, -1, Util
						.bind("convention.type.lowercaseName"), null); //$NON-NLS-1$
			}
			return JavaModelStatus.VERIFIED_OK;
		} else {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util.bind(
					"convention.type.invalidName", name), null); //$NON-NLS-1$
		}
	}

	/**
	 * Validate the given method name. The special names "&lt;init&gt;" and
	 * "&lt;clinit&gt;" are not valid.
	 * <p>
	 * The syntax for a method name is defined by Identifier of MethodDeclarator
	 * (JLS2 8.4). For example "println".
	 * 
	 * @param name
	 *            the name of a method
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as a method name, otherwise a status object
	 *         indicating what is wrong with the name
	 */
	public static IStatus validateMethodName(String name) {

		return validateIdentifier(name);
	}

	/**
	 * Validate the given package name.
	 * <p>
	 * The syntax of a package name corresponds to PackageName as defined by
	 * PackageDeclaration (JLS2 7.4). For example, <code>"java.lang"</code>.
	 * <p>
	 * Note that the given name must be a non-empty package name (that is,
	 * attempting to validate the default package will return an error status.)
	 * Also it must not contain any characters or substrings that are not valid
	 * on the file system on which workspace root is located.
	 * 
	 * @param name
	 *            the name of a package
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         name is valid as a package name, otherwise a status object
	 *         indicating what is wrong with the name
	 */
	public static IStatus validatePackageName(String name) {

		if (name == null) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.package.nullName"), null); //$NON-NLS-1$
		}
		int length;
		if ((length = name.length()) == 0) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.package.emptyName"), null); //$NON-NLS-1$
		}
		if (name.charAt(0) == DOT || name.charAt(length - 1) == DOT) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.package.dotName"), null); //$NON-NLS-1$
		}
		if (CharOperation.isWhitespace(name.charAt(0))
				|| CharOperation.isWhitespace(name.charAt(name.length() - 1))) {
			return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
					.bind("convention.package.nameWithBlanks"), null); //$NON-NLS-1$
		}
		int dot = 0;
		while (dot != -1 && dot < length - 1) {
			if ((dot = name.indexOf(DOT, dot + 1)) != -1 && dot < length - 1
					&& name.charAt(dot + 1) == DOT) {
				return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
						.bind("convention.package.consecutiveDotsName"), null); //$NON-NLS-1$
			}
		}
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		StringTokenizer st = new StringTokenizer(name, new String(
				new char[] { DOT }));
		boolean firstToken = true;
		IStatus warningStatus = null;
		while (st.hasMoreTokens()) {
			String typeName = st.nextToken();
			typeName = typeName.trim(); // grammar allows spaces
			char[] scannedID = scannedIdentifier(typeName);
			if (scannedID == null) {
				return new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, -1, Util
						.bind("convention.illegalIdentifier", typeName), null); //$NON-NLS-1$
			}
			IStatus status = workspace.validateName(new String(scannedID),
					IResource.FOLDER);
			if (!status.isOK()) {
				return status;
			}
			if (firstToken && scannedID.length > 0
					&& Character.isUpperCase(scannedID[0])) {
				if (warningStatus == null) {
					warningStatus = new Status(IStatus.WARNING,
							JavaCore.PLUGIN_ID, -1,
							Util.bind("convention.package.uppercaseName"), null); //$NON-NLS-1$
				}
			}
			firstToken = false;
		}
		if (warningStatus != null) {
			return warningStatus;
		}
		return JavaModelStatus.VERIFIED_OK;
	}

	/**
	 * Validate a given classpath and output location for a project, using the
	 * following rules:
	 * <ul>
	 * <li> Classpath entries cannot collide with each other; that is, all entry
	 * paths must be unique.
	 * <li> The project output location path cannot be null, must be absolute
	 * and located inside the project.
	 * <li> Specific output locations (specified on source entries) can be null,
	 * if not they must be located inside the project,
	 * <li> A project entry cannot refer to itself directly (that is, a project
	 * cannot prerequisite itself).
	 * <li> Classpath entries or output locations cannot coincidate or be nested
	 * in each other, except for the following scenarii listed below:
	 * <ul>
	 * <li> A source folder can coincidate with its own output location, in
	 * which case this output can then contain library archives. However, a
	 * specific output location cannot coincidate with any library or a distinct
	 * source folder than the one referring to it. </li>
	 * <li> A source/library folder can be nested in any source folder as long
	 * as the nested folder is excluded from the enclosing one. </li>
	 * <li> An output location can be nested in a source folder, if the source
	 * folder coincidates with the project itself, or if the output location is
	 * excluded from the source folder.
	 * </ul>
	 * </ul>
	 * 
	 * Note that the classpath entries are not validated automatically. Only
	 * bound variables or containers are considered in the checking process
	 * (this allows to perform a consistency check on a classpath which has
	 * references to yet non existing projects, folders, ...).
	 * <p>
	 * This validation is intended to anticipate classpath issues prior to
	 * assigning it to a project. In particular, it will automatically be
	 * performed during the classpath setting operation (if validation fails,
	 * the classpath setting will not complete).
	 * <p>
	 * 
	 * @param javaProject
	 *            the given java project
	 * @param rawClasspath
	 *            the given classpath
	 * @param projectOutputLocation
	 *            the given output location
	 * @return a status object with code <code>IStatus.OK</code> if the given
	 *         classpath and output location are compatible, otherwise a status
	 *         object indicating what is wrong with the classpath or output
	 *         location
	 * @since 2.0
	 */
	public static IJavaModelStatus validateClasspath(IJavaProject javaProject,
			IClasspathEntry[] rawClasspath, IPath projectOutputLocation) {

		return ClasspathEntry.validateClasspath(javaProject, rawClasspath,
				projectOutputLocation);
	}

	/**
	 * Returns a Java model status describing the problem related to this
	 * classpath entry if any, a status object with code <code>IStatus.OK</code>
	 * if the entry is fine (that is, if the given classpath entry denotes a
	 * valid element to be referenced onto a classpath).
	 * 
	 * @param project
	 *            the given java project
	 * @param entry
	 *            the given classpath entry
	 * @param checkSourceAttachment
	 *            a flag to determine if source attachement should be checked
	 * @return a java model status describing the problem related to this
	 *         classpath entry if any, a status object with code
	 *         <code>IStatus.OK</code> if the entry is fine
	 * @since 2.0
	 */
	public static IJavaModelStatus validateClasspathEntry(IJavaProject project,
			IClasspathEntry entry, boolean checkSourceAttachment) {
		return ClasspathEntry.validateClasspathEntry(project, entry,
				checkSourceAttachment, true/* recurse in container */);
	}
}
