/*******************************************************************************

 * Copyright (c) 2006 IONA Technologies PLC

 * 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:

 *     IONA Technologies PLC - initial API and implementation

 *******************************************************************************/
package org.eclipse.stp.sc.annvalidator.anntree;

import java.util.Iterator;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.resources.IFile;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.stp.common.logging.LoggingProxy;
import org.eclipse.stp.sc.common.utils.JDTUtils;
import org.eclipse.stp.sc.common.validator.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This class is used to build xml tree from all annotations in java source code
 * 
 * @author jma
 * 
 */
public class AnnXMLTreeBuilder {

	private static final LoggingProxy LOG = LoggingProxy
			.getlogger(AnnXMLTreeBuilder.class);

	public static final String ATTR_XMLNS = "xmlns";

	public static final String ATTR_PREFIX = "tns";

	public static final String TAG_CLASS = "Class";

	public static final String TAG_METHOD = "Method";
	
	public static final String TAG_PARAM = "Param";

	public static final String TAG_LINENUMBER = "linenumber";

	public static final String TAG_MODIFIER = "modifier";

	public static final String TAG_NAME = "name";

	public static final String TAG_RETURN = "return";

	public static final String TAG_VALUE = "value";

	private DocumentBuilderFactory factory = null;

	private DocumentBuilder builder = null;

	private ICompilationUnit javaUnit = null;

	private CompilationUnit sourceUnit = null;

	public AnnXMLTreeBuilder() {
		try {
			factory = DocumentBuilderFactory.newInstance();
			factory.setNamespaceAware(true);
			builder = factory.newDocumentBuilder();
		} catch (FactoryConfigurationError e) {
			LOG.error("factory error", e);
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			LOG.error("parser config error", e);
		}

	}

	/**
	 * build class element in the xml tree
	 * 
	 * @return
	 */
	protected Element buildElementFromCls(Document document,
			ICompilationUnit cls) throws Exception {
		// create class element
		Element clsElem = document.createElement(TAG_CLASS);

		IType clsType = cls.findPrimaryType();
        if (clsType == null) {
            return null;
        }
		setTypeAttributes(clsElem, clsType);
		// create elements for all annotations defined for this class
		addAnnotationNodes(document, clsType, clsElem);
//		int annCount = addAnnotationNodes(document, clsType, clsElem);
//		if (annCount == 0) {
//			return null;
//		}

		// look up all methods
		IMethod[] methods = clsType.getMethods();
		for (IMethod method : methods) {
			Element methodElem = buildElementFromMethod(document, method);
			if (methodElem != null) {
			    clsElem.appendChild(methodElem);
			}
		}
		return clsElem;
	}

	/**
	 * add all annotaion elements node to parent node
	 * 
	 * @param document
	 * @param type
	 * @param parent
	 */
	private int addAnnotationNodes(Document document, IMember type,
			Element parent) {
		List<Annotation> annList = JDTUtils.getAnnotations(type);
		return addAnnotationNodes(document, annList, parent);
	}
	
	private int addAnnotationNodes(Document document, List<Annotation> annList, Element parent) {
		Element annElem = null;
		for (Annotation ann : annList) {
			if (ann instanceof MarkerAnnotation) {
				annElem = buildElementFromAnn(document, (MarkerAnnotation) ann);
			} else if (ann instanceof NormalAnnotation) {
				annElem = buildElementFromAnn(document, (NormalAnnotation) ann);
			} else if (ann instanceof SingleMemberAnnotation) {
				annElem = buildElementFromAnn(document,
						(SingleMemberAnnotation) ann);
			}

			if (annElem != null) {
				annElem.setAttribute(TAG_LINENUMBER, Integer
						.toString(getLineNumber(ann)));
				parent.appendChild(annElem);
			}
		}
		return annList.size();
	}

	private void setTypeAttributes(Element elem, IMember type) throws Exception {
		// set name
		String elemName = type.getElementName();
		if (elemName.lastIndexOf(".") > 0) {
			// remove package name
			elemName = elemName.substring(0, elemName.lastIndexOf("."));
		}
		elem.setAttribute(TAG_NAME, elemName);
		// set modifier
		int flag = type.getFlags();
		String modifierStr = Flags.toString(flag);
		if (Flags.isInterface(flag)) {
			modifierStr += "|" + "interface";
		}
		elem.setAttribute(TAG_MODIFIER, modifierStr);
		// set linenumber
		int lineNumber = getLineNumber(type);
		elem.setAttribute(TAG_LINENUMBER, Integer.toString(lineNumber));
	}

	/**
	 * build method element in the xml tree
	 * 
	 * @return
	 */
	private Element buildElementFromMethod(Document document, IMethod method)
			throws Exception {
		// create method element
		Element methodElem = document.createElement(TAG_METHOD);
		setTypeAttributes(methodElem, method);
		methodElem.setAttribute(TAG_RETURN, Signature
				.getSignatureSimpleName(method.getReturnType()));

		// create elements for all annotations defined for this method
		int annCount = addAnnotationNodes(document, method, methodElem);
		if (annCount == 0) {
			//it is not annotated method. do not add to the tree
			return null; 
		}
		
		
        //look up all parameters
		String[] params = method.getParameterNames();
		for (String param : params) {
			Element paramElem = buildElementFromParam(document, method, param);
			if (paramElem != null) {
			    methodElem.appendChild(paramElem);
			}
		}		
		return methodElem;
	}
	
	
	private Element buildElementFromParam(Document document, IMethod method, String param) throws Exception {
		Element paramElem = document.createElement(TAG_PARAM);
		paramElem.setAttribute(TAG_NAME, param);
		
		//use method linenumber for its parameter
		int lineNumber = getLineNumber(method);
		paramElem.setAttribute(TAG_LINENUMBER, Integer.toString(lineNumber));

		
		SingleVariableDeclaration methodParam = JDTUtils.getMethodParamDeclaration(sourceUnit,
				method, param);
		if (methodParam == null) {
			return null;
		}
		List<Annotation> annList = JDTUtils.getAnnotationsFromParamDecl(methodParam);
		addAnnotationNodes(document, annList, paramElem);
		return paramElem;
	}
	

	private int getLineNumber(IMember member) {
		try {
			return sourceUnit
					.getLineNumber(member.getSourceRange().getOffset());
		} catch (Exception e) {
			e.printStackTrace();
			return 1;
		}
	}

	private int getLineNumber(Annotation ann) {
		try {
			return sourceUnit.getLineNumber(ann.getStartPosition());
		} catch (Exception e) {
			e.printStackTrace();
			return 1;
		}
	}

	/**
	 * create xml element for marker annotation
	 * 
	 * @param document
	 * @param parent
	 * @param ann
	 * @return
	 */
	private Element buildElementFromAnn(Document document, MarkerAnnotation ann) {
		return buildBasicAnnElem(document, ann);
	}

	/**
	 * create xml element for SingleMemeberAnnotation
	 * 
	 * @param document
	 * @param parent
	 * @param ann
	 * @return
	 */
	private Element buildElementFromAnn(Document document,
			SingleMemberAnnotation ann) {
		Element root = buildBasicAnnElem(document, ann);
		root.setAttribute(TAG_VALUE, convertExpressToStr(ann.getValue()));
		return root;
	}

	private Element buildBasicAnnElem(Document document, Annotation annotation) {
		Name typeName = annotation.getTypeName();

		// String namespaceURI =
		// XMLUtils.getNSFromClsName(typeName.getFullyQualifiedName());
		String rootName = typeName.getFullyQualifiedName();
		if (rootName.indexOf(".") > 0) {
			rootName = rootName.substring(rootName.lastIndexOf(".") + 1,
					rootName.length());
		}

		// Element root = document.createElementNS(namespaceURI, rootName);
		Element root = document.createElement(rootName);
		return root;
	}

	/**
	 * build the xml element for the NormalAnnotation.
	 * 
	 * @return
	 */
	private Element buildElementFromAnn(Document document,
			NormalAnnotation annotation) {
		Element root = buildBasicAnnElem(document, annotation);
		// root.setAttribute(ATTR_XMLNS + ":" + ATTR_PREFIX,
		// root.getNamespaceURI());
		// root.setPrefix(ATTR_PREFIX);
		Iterator itor = annotation.values().iterator();
		while (itor.hasNext()) {
			MemberValuePair member = (MemberValuePair) itor.next();
			root.setAttribute(member.getName().getFullyQualifiedName(),
					convertExpressToStr(member.getValue()));
		}
		return root;

	}

	private String convertExpressToStr(Expression exp) {
		if (exp instanceof StringLiteral) {
			return ((StringLiteral) exp).getLiteralValue();
		} else if (exp instanceof BooleanLiteral) {
			boolean value = ((BooleanLiteral) exp).booleanValue();
			return Boolean.toString(value);
		} else if (exp instanceof NumberLiteral) {
			((NumberLiteral) exp).getToken();
		}
		LOG.debug("Unsupported express type:" + exp.getClass().getName());
		return exp.toString();
	}

	public Document loadXMLFile(IFile xmlFile) {
		IFile file = (IFile) xmlFile;
		Document document = null;
		try {
            if (!xmlFile.exists()) {
                return null;
            }
			javaUnit = JDTUtils.getJavaUnitFromFile(file);
			if (!javaUnit.exists()) {
				return null;
			}
			sourceUnit = JDTUtils.getDomRootCompilationUnit(javaUnit);
			document = builder.newDocument();
			Element clsElem = buildElementFromCls(document, javaUnit);
			if (clsElem == null) {
				return null;
			}
			document.appendChild(clsElem);
			LOG.debug("xml generate from ann:"
					+ XMLUtils.getXMLAsString(document));
		} catch (Exception e) {
			LOG.error("", e);
		}

		// free memory
		javaUnit = null;
		sourceUnit = null;
		return document;
	}

}
