/*******************************************************************************
 * Copyright (c) 2000, 2003 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.internal.core.jdom;

import net.sourceforge.phpdt.core.IJavaElement;
import net.sourceforge.phpdt.core.IType;
import net.sourceforge.phpdt.core.Signature;
import net.sourceforge.phpdt.core.jdom.IDOMMethod;
import net.sourceforge.phpdt.core.jdom.IDOMNode;
import net.sourceforge.phpdt.internal.compiler.util.Util;
import net.sourceforge.phpdt.internal.core.util.CharArrayBuffer;
import net.sourceforge.phpdt.internal.core.util.CharArrayOps;

/**
 * DOMMethod provides an implementation of IDOMMethod.
 * 
 * @see IDOMMethod
 * @see DOMNode
 */

class DOMMethod extends DOMMember implements IDOMMethod {

	/**
	 * Contains the return type of the method when the return type has been
	 * altered from the contents in the document, otherwise <code>null</code>.
	 */
	protected String fReturnType;

	/**
	 * The original inclusive source range of the method's return type in the
	 * document, or -1's if no return type is present in the document. If the
	 * return type of this method is qualified with '[]' following the parameter
	 * list, this array has four entries. In this case, the last two entries of
	 * the array are the inclusive source range of the array qualifiers.
	 */
	protected int[] fReturnTypeRange;

	/**
	 * Contains the textual representation of the method's parameter list,
	 * including open and closing parentheses when the parameters had been
	 * altered from the contents in the document, otherwise <code>null</code>.
	 */
	protected char[] fParameterList;

	/**
	 * The original inclusive source range of the method's parameter list in the
	 * document.
	 */
	protected int[] fParameterRange;

	/**
	 * Contains the textual representation of the method's exception list when
	 * the exceptions had been altered from the contents in the document,
	 * otherwise <code>null</code>. The exception list is a comment delimited
	 * list of exceptions, not including the "throws" keyword.
	 */
	protected char[] fExceptionList;

	/**
	 * The original inclusive source range of the method's exception list in the
	 * document.
	 */
	protected int[] fExceptionRange;

	/**
	 * Contains the method's body when the body has been altered from the
	 * contents in the document, otherwise <code>null</code>. The body
	 * includes everything between and including the enclosing braces, and
	 * trailing whitespace.
	 */
	protected String fBody;

	/**
	 * The original inclusive source range of the method's body.
	 */
	protected int[] fBodyRange;

	/**
	 * Names of parameters in the method parameter list, or <code>null</code>
	 * if the method has no parameters.
	 */
	protected String[] fParameterNames;

	/**
	 * Types of parameters in the method parameter list, or <code>null</code>
	 * if the method has no parameters.
	 */
	protected String[] fParameterTypes;

	/**
	 * The exceptions the method throws, or <code>null</code> if the method
	 * throws no exceptions.
	 */
	protected String[] fExceptions;

	/**
	 * Constructs an empty method node.
	 */
	DOMMethod() {

	}

	/**
	 * Creates a new detailed METHOD document fragment on the given range of the
	 * document.
	 * 
	 * @param document -
	 *            the document containing this node's original contents
	 * @param sourceRange -
	 *            a two element array of integers describing the entire
	 *            inclusive source range of this node within its document.
	 *            Contents start on and include the character at the first
	 *            position. Contents end on and include the character at the
	 *            last position. An array of -1's indicates this node's contents
	 *            do not exist in the document.
	 * @param name -
	 *            the identifier portion of the name of this node, or
	 *            <code>null</code> if this node does not have a name
	 * @param nameRange -
	 *            a two element array of integers describing the entire
	 *            inclusive source range of this node's name within its
	 *            document, including any array qualifiers that might
	 *            immediately follow the name or -1's if this node does not have
	 *            a name.
	 * @param commentRange -
	 *            a two element array describing the comments that precede the
	 *            member declaration. The first matches the start of this node's
	 *            sourceRange, and the second is the new-line or first
	 *            non-whitespace character following the last comment. If no
	 *            comments are present, this array contains two -1's.
	 * @param flags -
	 *            an integer representing the modifiers for this member. The
	 *            integer can be analyzed with net.sourceforge.phpdt.core.Flags
	 * @param modifierRange -
	 *            a two element array describing the location of modifiers for
	 *            this member within its source range. The first integer is the
	 *            first character of the first modifier for this member, and the
	 *            second integer is the last whitespace character preceeding the
	 *            next part of this member declaration. If there are no
	 *            modifiers present in this node's source code (that is, package
	 *            default visibility), this array contains two -1's.
	 * @param isConstructor -
	 *            true if the method is a contructor, otherwise false
	 * @param returnType -
	 *            the normalized return type of this method
	 * @param returnTypeRange -
	 *            a two element array describing the location of the return type
	 *            within the method's source range. The first integer is is the
	 *            position of the first character in the return type, and the
	 *            second integer is the position of the last character in the
	 *            return type. For constructors, the contents of this array are
	 *            -1's. If the return type of this method is qualified with '[]'
	 *            following the parameter list, this array has four entries. In
	 *            this case, the last two entries of the array are the inclusive
	 *            source range of the array qualifiers.
	 * @param parameterTypes -
	 *            an array of parameter types in the method declaration or
	 *            <code>null</code> if the method has no parameters
	 * @param parameterNames -
	 *            an array of parameter names in the method declaration or
	 *            <code>null</code> if the method has no parameters
	 * @param parameterRange -
	 *            a two element array describing the location of the parameter
	 *            list in the method. The first integer is the location of the
	 *            open parenthesis and the second integer is the location of the
	 *            closing parenthesis.
	 * @param exceptions -
	 *            an array of the names of exceptions thrown by this method or
	 *            <code>null</code> if the method throws no exceptions
	 * @param exceptionRange -
	 *            a two element array describing the location of the exception
	 *            list in the method declaration. The first integer is the
	 *            position of the first character in the first exception the
	 *            method throws, and the second integer is the position of the
	 *            last character of the last exception this method throws.
	 * @param bodyRange -
	 *            a two element array describing the location of the method's
	 *            body. The first integer is the first character following the
	 *            method's parameter list, or exception list (if present). The
	 *            second integer is the location of the last character in the
	 *            method's source range.
	 */
	DOMMethod(char[] document, int[] sourceRange, String name, int[] nameRange,
			int[] commentRange, int flags, int[] modifierRange,
			boolean isConstructor, String returnType, int[] returnTypeRange,
			String[] parameterTypes, String[] parameterNames,
			int[] parameterRange, String[] exceptions, int[] exceptionRange,
			int[] bodyRange) {
		super(document, sourceRange, name, nameRange, commentRange, flags,
				modifierRange);

		setMask(MASK_IS_CONSTRUCTOR, isConstructor);
		fReturnType = returnType;
		fReturnTypeRange = returnTypeRange;
		fParameterTypes = parameterTypes;
		fParameterNames = parameterNames;
		fParameterRange = parameterRange;
		fExceptionRange = exceptionRange;
		fExceptions = exceptions;
		setHasBody(true);
		fBodyRange = bodyRange;
		setMask(MASK_DETAILED_SOURCE_INDEXES, true);

	}

	/**
	 * Creates a new simple METHOD document fragment on the given range of the
	 * document.
	 * 
	 * @param document -
	 *            the document containing this node's original contents
	 * @param sourceRange -
	 *            a two element array of integers describing the entire
	 *            inclusive source range of this node within its document.
	 *            Contents start on and include the character at the first
	 *            position. Contents end on and include the character at the
	 *            last position. An array of -1's indicates this node's contents
	 *            do not exist in the document.
	 * @param name -
	 *            the identifier portion of the name of this node, or
	 *            <code>null</code> if this node does not have a name
	 * @param nameRange -
	 *            a two element array of integers describing the entire
	 *            inclusive source range of this node's name within its
	 *            document, including any array qualifiers that might
	 *            immediately follow the name or -1's if this node does not have
	 *            a name.
	 * @param flags -
	 *            an integer representing the modifiers for this member. The
	 *            integer can be analyzed with net.sourceforge.phpdt.core.Flags
	 * @param isConstructor -
	 *            true if the method is a contructor, otherwise false
	 * @param returnType -
	 *            the normalized return type of this method
	 * @param parameterTypes -
	 *            an array of parameter types in the method declaration or
	 *            <code>null</code> if the method has no parameters
	 * @param parameterNames -
	 *            an array of parameter names in the method declaration or
	 *            <code>null</code> if the method has no parameters
	 * @param exceptions -
	 *            an array of the names of exceptions thrown by this method or
	 *            <code>null</code> if the method throws no exceptions
	 */
	DOMMethod(char[] document, int[] sourceRange, String name, int[] nameRange,
			int flags, boolean isConstructor, String returnType,
			String[] parameterTypes, String[] parameterNames,
			String[] exceptions) {
		this(document, sourceRange, name, nameRange, new int[] { -1, -1 },
				flags, new int[] { -1, -1 }, isConstructor, returnType,
				new int[] { -1, -1 }, parameterTypes, parameterNames,
				new int[] { -1, -1 }, exceptions, new int[] { -1, -1 },
				new int[] { -1, -1 });
		setMask(MASK_DETAILED_SOURCE_INDEXES, false);
	}

	/**
	 * @see IDOMMethod#addException(String)
	 */
	public void addException(String name) throws IllegalArgumentException {
		if (name == null) {
			throw new IllegalArgumentException(Util
					.bind("dom.nullExceptionType")); //$NON-NLS-1$
		}
		if (fExceptions == null) {
			fExceptions = new String[1];
			fExceptions[0] = name;
		} else {
			fExceptions = appendString(fExceptions, name);
		}
		setExceptions(fExceptions);
	}

	/**
	 * @see IDOMMethod#addParameter(String, String)
	 */
	public void addParameter(String type, String name)
			throws IllegalArgumentException {
		if (type == null) {
			throw new IllegalArgumentException(Util
					.bind("dom.nullTypeParameter")); //$NON-NLS-1$
		}
		if (name == null) {
			throw new IllegalArgumentException(Util
					.bind("dom.nullNameParameter")); //$NON-NLS-1$
		}
		if (fParameterNames == null) {
			fParameterNames = new String[1];
			fParameterNames[0] = name;
		} else {
			fParameterNames = appendString(fParameterNames, name);
		}
		if (fParameterTypes == null) {
			fParameterTypes = new String[1];
			fParameterTypes[0] = type;
		} else {
			fParameterTypes = appendString(fParameterTypes, type);
		}
		setParameters(fParameterTypes, fParameterNames);
	}

	/**
	 * @see DOMMember#appendMemberBodyContents(CharArrayBuffer)
	 */
	protected void appendMemberBodyContents(CharArrayBuffer buffer) {
		if (fBody != null) {
			buffer.append(fBody);
		} else {
			buffer.append(fDocument, fBodyRange[0], fBodyRange[1] + 1
					- fBodyRange[0]);
		}
	}

	/**
	 * @see DOMMember#appendMemberDeclarationContents(CharArrayBuffer)
	 */
	protected void appendMemberDeclarationContents(CharArrayBuffer buffer) {

		if (isConstructor()) {
			buffer.append(getConstructorName()).append(fDocument,
					fNameRange[1] + 1, fParameterRange[0] - fNameRange[1] - 1);
		} else {
			buffer.append(getReturnTypeContents());
			if (fReturnTypeRange[0] >= 0) {
				buffer.append(fDocument, fReturnTypeRange[1] + 1, fNameRange[0]
						- fReturnTypeRange[1] - 1);
			} else {
				buffer.append(' ');
			}
			buffer.append(getNameContents()).append(fDocument,
					fNameRange[1] + 1, fParameterRange[0] - fNameRange[1] - 1);
		}
		if (fParameterList != null) {
			buffer.append(fParameterList);
		} else {
			buffer.append(fDocument, fParameterRange[0], fParameterRange[1] + 1
					- fParameterRange[0]);
		}
		int start;
		if (hasTrailingArrayQualifier() && isReturnTypeAltered()) {
			start = fReturnTypeRange[3] + 1;
		} else {
			start = fParameterRange[1] + 1;
		}
		if (fExceptions != null) {
			// add 'throws' keyword
			if (fExceptionRange[0] >= 0) {
				buffer.append(fDocument, start, fExceptionRange[0] - start);
			} else {
				buffer.append(" throws "); //$NON-NLS-1$
			}
			// add exception list
			if (fExceptionList != null) {
				buffer.append(fExceptionList);
				// add space before body
				if (fExceptionRange[0] >= 0) {
					buffer.append(fDocument, fExceptionRange[1] + 1,
							fBodyRange[0] - fExceptionRange[1] - 1);
				} else {
					buffer.append(fDocument, fParameterRange[1] + 1,
							fBodyRange[0] - fParameterRange[1] - 1);
				}
			} else {
				// add list and space before body
				buffer.append(fDocument, fExceptionRange[0], fBodyRange[0]
						- fExceptionRange[0]);
			}
		} else {
			// add space before body
			if (fExceptionRange[0] >= 0) {
				buffer.append(fDocument, fExceptionRange[1] + 1, fBodyRange[0]
						- fExceptionRange[1] - 1);
			} else {
				buffer.append(fDocument, start, fBodyRange[0] - start);
			}
		}

	}

	/**
	 * @see DOMNode#appendSimpleContents(CharArrayBuffer)
	 */
	protected void appendSimpleContents(CharArrayBuffer buffer) {
		// append eveything before my name
		buffer.append(fDocument, fSourceRange[0], fNameRange[0]
				- fSourceRange[0]);
		// append my name
		if (isConstructor()) {
			buffer.append(getConstructorName());
		} else {
			buffer.append(fName);
		}
		// append everything after my name
		buffer.append(fDocument, fNameRange[1] + 1, fSourceRange[1]
				- fNameRange[1]);
	}

	/**
	 * @see IDOMMethod#getBody()
	 */
	public String getBody() {
		becomeDetailed();
		if (hasBody()) {
			if (fBody != null) {
				return fBody;
			} else {
				return CharArrayOps.substring(fDocument, fBodyRange[0],
						fBodyRange[1] + 1 - fBodyRange[0]);
			}
		} else {
			return null;
		}
	}

	/**
	 * Returns the simple name of the enclsoing type for this constructor. If
	 * the constuctor is not currently enclosed in a type, the original name of
	 * the constructor as found in the documnent is returned.
	 */
	protected String getConstructorName() {

		if (isConstructor()) {
			if (getParent() != null) {
				return getParent().getName();
			} else {
				// If there is no parent use the original name
				return new String(getNameContents());
			}
		} else {
			return null;
		}

	}

	/**
	 * @see DOMNode#getDetailedNode()
	 */
	// protected DOMNode getDetailedNode() {
	// return (DOMNode)getFactory().createMethod(getContents());
	// }
	/**
	 * @see IDOMMethod#getExceptions()
	 */
	public String[] getExceptions() {
		return fExceptions;
	}

	/**
	 * @see IDOMNode#getJavaElement
	 */
	public IJavaElement getJavaElement(IJavaElement parent)
			throws IllegalArgumentException {
		if (parent.getElementType() == IJavaElement.TYPE) {
			// translate parameter types to signatures
			String[] sigs = null;
			if (fParameterTypes != null) {
				sigs = new String[fParameterTypes.length];
				int i;
				for (i = 0; i < fParameterTypes.length; i++) {
					sigs[i] = Signature.createTypeSignature(fParameterTypes[i]
							.toCharArray(), false);
				}
			}
			String name = null;
			if (isConstructor()) {
				name = getConstructorName();
			} else {
				name = getName();
			}
			return ((IType) parent).getMethod(name, sigs);
		} else {
			throw new IllegalArgumentException(Util
					.bind("element.illegalParent")); //$NON-NLS-1$
		}
	}

	/**
	 * @see DOMMember#getMemberDeclarationStartPosition()
	 */
	protected int getMemberDeclarationStartPosition() {
		if (fReturnTypeRange[0] >= 0) {
			return fReturnTypeRange[0];
		} else {
			return fNameRange[0];
		}
	}

	/**
	 * @see IDOMNode#getName()
	 */
	public String getName() {
		if (isConstructor()) {
			return null;
		} else {
			return super.getName();
		}
	}

	/**
	 * @see IDOMNode#getNodeType()
	 */
	public int getNodeType() {
		return IDOMNode.METHOD;
	}

	/**
	 * @see IDOMMethod#getParameterNames()
	 */
	public String[] getParameterNames() {
		return fParameterNames;
	}

	/**
	 * @see IDOMMethod#getParameterTypes()
	 */
	public String[] getParameterTypes() {
		return fParameterTypes;
	}

	/**
	 * @see IDOMMethod#getReturnType()
	 */
	public String getReturnType() {
		if (isConstructor()) {
			return null;
		} else {
			return fReturnType;
		}
	}

	/**
	 * Returns the source code to be used for this method's return type
	 */
	protected char[] getReturnTypeContents() {
		if (isConstructor()) {
			return null;
		} else {
			if (isReturnTypeAltered()) {
				return fReturnType.toCharArray();
			} else {
				return CharArrayOps.subarray(fDocument, fReturnTypeRange[0],
						fReturnTypeRange[1] + 1 - fReturnTypeRange[0]);
			}

		}
	}

	/**
	 * Returns true if this method's return type has array qualifiers ('[]')
	 * following the parameter list.
	 */
	protected boolean hasTrailingArrayQualifier() {
		return fReturnTypeRange.length > 2;
	}

	/**
	 * @see IDOMMethod#isConstructor()
	 */
	public boolean isConstructor() {
		return getMask(MASK_IS_CONSTRUCTOR);
	}

	/**
	 * Returns true if this method's return type has been altered from the
	 * original document contents.
	 */
	protected boolean isReturnTypeAltered() {
		return getMask(MASK_RETURN_TYPE_ALTERED);
	}

	/**
	 * @see IDOMNode#isSigantureEqual(IDOMNode).
	 * 
	 * <p>
	 * Two methods have equal signatures if there names are the same and their
	 * parameter types are the same.
	 */
	public boolean isSignatureEqual(IDOMNode node) {
		boolean ok = node.getNodeType() == getNodeType();
		if (ok) {
			IDOMMethod method = (IDOMMethod) node;
			ok = (isConstructor() && method.isConstructor())
					|| (!isConstructor() && !method.isConstructor());
			if (ok && !isConstructor()) {
				ok = getName().equals(method.getName());
			}
			if (!ok) {
				return false;
			}

			String[] types = method.getParameterTypes();
			if (fParameterTypes == null || fParameterTypes.length == 0) {
				// this method has no parameters
				if (types == null || types.length == 0) {
					// the other method has no parameters either
					return true;
				}
			} else {
				// this method has parameters
				if (types == null || types.length == 0) {
					// the other method has no parameters
					return false;
				}
				if (fParameterTypes.length != types.length) {
					// the methods have a different number of parameters
					return false;
				}
				int i;
				for (i = 0; i < types.length; i++) {
					if (!fParameterTypes[i].equals(types[i])) {
						return false;
					}
				}
				return true;
			}
		}
		return false;

	}

	/**
	 * @see DOMNode
	 */
	protected DOMNode newDOMNode() {
		return new DOMMethod();
	}

	/**
	 * Offsets all the source indexes in this node by the given amount.
	 */
	protected void offset(int offset) {
		super.offset(offset);
		offsetRange(fBodyRange, offset);
		offsetRange(fExceptionRange, offset);
		offsetRange(fParameterRange, offset);
		offsetRange(fReturnTypeRange, offset);
	}

	/**
	 * @see IDOMMethod#setBody
	 */
	public void setBody(String body) {
		becomeDetailed();
		fragment();
		fBody = body;
		setHasBody(body != null);
		if (!hasBody()) {
			fBody = ";" + Util.LINE_SEPARATOR; //$NON-NLS-1$
		}
	}

	/**
	 * Sets the end of the body range
	 */
	void setBodyRangeEnd(int end) {
		fBodyRange[1] = end;
	}

	/**
	 * @see IDOMMethod#setConstructor(boolean)
	 */
	public void setConstructor(boolean b) {
		becomeDetailed();
		setMask(MASK_IS_CONSTRUCTOR, b);
		fragment();
	}

	/**
	 * @see IDOMMethod#setExceptions(char[][])
	 */
	public void setExceptions(String[] names) {
		becomeDetailed();
		if (names == null || names.length == 0) {
			fExceptions = null;
		} else {
			fExceptions = names;
			CharArrayBuffer buffer = new CharArrayBuffer();
			char[] comma = new char[] { ',', ' ' };
			for (int i = 0, length = names.length; i < length; i++) {
				if (i > 0)
					buffer.append(comma);
				buffer.append(names[i]);
			}
			fExceptionList = buffer.getContents();
		}
		fragment();
	}

	/**
	 * @see IDOMMethod#setName
	 */
	public void setName(String name) {
		if (name == null) {
			throw new IllegalArgumentException(Util.bind("element.nullName")); //$NON-NLS-1$
		} else {
			super.setName(name);
		}
	}

	/**
	 * @see IDOMMethod#setParameters(char[][], char[][])
	 */
	public void setParameters(String[] types, String[] names)
			throws IllegalArgumentException {
		becomeDetailed();
		if (types == null || names == null) {
			if (types == null && names == null) {
				fParameterTypes = null;
				fParameterNames = null;
				fParameterList = new char[] { '(', ')' };
			} else {
				throw new IllegalArgumentException(Util
						.bind("dom.mismatchArgNamesAndTypes")); //$NON-NLS-1$
			}
		} else if (names.length != types.length) {
			throw new IllegalArgumentException(Util
					.bind("dom.mismatchArgNamesAndTypes")); //$NON-NLS-1$
		} else if (names.length == 0) {
			setParameters(null, null);
		} else {
			fParameterNames = names;
			fParameterTypes = types;
			CharArrayBuffer parametersBuffer = new CharArrayBuffer();
			parametersBuffer.append("("); //$NON-NLS-1$
			char[] comma = new char[] { ',', ' ' };
			for (int i = 0; i < names.length; i++) {
				if (i > 0) {
					parametersBuffer.append(comma);
				}
				parametersBuffer.append(types[i]).append(' ').append(names[i]);
			}
			parametersBuffer.append(')');
			fParameterList = parametersBuffer.getContents();
		}
		fragment();
	}

	/**
	 * @see IDOMMethod#setReturnType(char[])
	 */
	public void setReturnType(String name) throws IllegalArgumentException {
		if (name == null) {
			throw new IllegalArgumentException(Util.bind("dom.nullReturnType")); //$NON-NLS-1$
		}
		becomeDetailed();
		fragment();
		setReturnTypeAltered(true);
		fReturnType = name;
	}

	/**
	 * Sets the state of this method declaration as having the return type
	 * altered from the original document.
	 */
	protected void setReturnTypeAltered(boolean typeAltered) {
		setMask(MASK_RETURN_TYPE_ALTERED, typeAltered);
	}

	/**
	 */
	protected void setSourceRangeEnd(int end) {
		super.setSourceRangeEnd(end);
		fBodyRange[1] = end;
	}

	/**
	 * @see DOMNode#shareContents(DOMNode)
	 */
	protected void shareContents(DOMNode node) {
		super.shareContents(node);
		DOMMethod method = (DOMMethod) node;
		fBody = method.fBody;
		fBodyRange = rangeCopy(method.fBodyRange);
		fExceptionList = method.fExceptionList;
		fExceptionRange = rangeCopy(method.fExceptionRange);
		fExceptions = method.fExceptions;
		fParameterList = method.fParameterList;
		fParameterNames = method.fParameterNames;
		fParameterRange = rangeCopy(method.fParameterRange);
		fParameterTypes = method.fParameterTypes;
		fReturnType = method.fReturnType;
		fReturnTypeRange = rangeCopy(method.fReturnTypeRange);
	}

	/**
	 * @see IDOMNode#toString()
	 */
	public String toString() {
		if (isConstructor()) {
			return "CONSTRUCTOR"; //$NON-NLS-1$
		} else {
			return "METHOD: " + getName(); //$NON-NLS-1$
		}
	}
}
