/******************************************************************************* 
 * Copyright (c) 2007 Red Hat, Inc. 
 * Distributed under license by Red Hat, Inc. All rights reserved. 
 * This program is 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: 
 * Red Hat, Inc. - initial API and implementation 
 ******************************************************************************/ 
package org.jboss.tools.seam.internal.core.scanner.java;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.jboss.tools.common.model.project.ext.IValueInfo;
import org.jboss.tools.common.model.project.ext.impl.ValueInfo;
import org.jboss.tools.seam.core.BeanType;
import org.jboss.tools.seam.core.BijectedAttributeType;
import org.jboss.tools.seam.core.ISeamXmlComponentDeclaration;
import org.jboss.tools.seam.core.SeamComponentMethodType;
import org.jboss.tools.seam.internal.core.BijectedAttribute;
import org.jboss.tools.seam.internal.core.DataModelSelectionAttribute;
import org.jboss.tools.seam.internal.core.Role;
import org.jboss.tools.seam.internal.core.SeamAnnotatedFactory;
import org.jboss.tools.seam.internal.core.SeamComponentMethod;
import org.jboss.tools.seam.internal.core.SeamJavaComponentDeclaration;
import org.jboss.tools.seam.internal.core.scanner.LoadedDeclarations;

/**
 * Builds component using results of ASTVisitorImpl
 * 
 * @author Viacheslav Kabanovich
 */
public class ComponentBuilder implements SeamAnnotations {
	LoadedDeclarations ds = null;

	AnnotatedASTNode<?> annotatedType = null;
	Set<AnnotatedASTNode<FieldDeclaration>> annotatedFields = null;
	Set<AnnotatedASTNode<MethodDeclaration>> annotatedMethods = null;
	
	SeamJavaComponentDeclaration component = new SeamJavaComponentDeclaration();
	
	
	public ComponentBuilder(LoadedDeclarations ds, ASTVisitorImpl.TypeData visitor) {
		this.ds = ds;
		annotatedType = visitor.annotatedType;
		annotatedFields = visitor.annotatedFields;
		annotatedMethods = visitor.annotatedMethods;

		String n = visitor.type.getElementName();
		n = JavaScanner.getResolvedType(visitor.type, n);

		ds.getComponents().add(component);
		component.setType(visitor.type);
		component.setId(visitor.type);
		component.setClassName(n);
		
	}
	
	void process() {
		if(annotatedType == null) return;
		ResolvedAnnotation[] as = annotatedType.getAnnotations();
		if(as != null) for (int i = 0; i < as.length; i++) {
			String type = as[i].getType();
			if(NAME_ANNOTATION_TYPE.equals(type)) {
				component.setName(ValueInfo.getValueInfo(as[i].getAnnotation(), null));
			} else if(SCOPE_ANNOTATION_TYPE.equals(type)) {
				ValueInfo scope = ValueInfo.getValueInfo(as[i].getAnnotation(), null);
				if(scope != null && scope.getValue() != null) {
					int q = scope.getValue().lastIndexOf('.');
					if(q >= 0) scope.setValue(scope.getValue().substring(q + 1).toLowerCase());
				}
				component.setScope(scope);
			} else if(INSTALL_ANNOTATION_TYPE.equals(type)) {
				component.setPrecedence(ValueInfo.getValueInfo(as[i].getAnnotation(), "precedence")); //$NON-NLS-1$
			} else if(IMPORT_ANNOTATION_TYPE.equals(type)) {
				processImport(as[i].getAnnotation());
			}
		}
		
		if(as != null) {
			Map<BeanType, IValueInfo> types = new HashMap<BeanType, IValueInfo>();
			for (int i = 0; i < BeanType.values().length; i++) {
				Annotation a = findAnnotation(annotatedType, BeanType.values()[i].getAnnotationType());
				if(a != null) {
					ValueInfo v = new ValueInfo();
					v.setValue("true"); //$NON-NLS-1$
					v.valueStartPosition = a.getStartPosition();
					v.valueLength = a.getLength();
					types.put(BeanType.values()[i], v);
				}
			}
			if(!types.isEmpty()) {
				component.setTypes(types);
			}
		}
		
		processFactories();
		processBijections();
		processComponentMethods();
		processRoles();
		
	}
	
	void processFactories() {
		for (AnnotatedASTNode<MethodDeclaration> n: annotatedMethods) {
			Annotation a = findAnnotation(n, FACTORY_ANNOTATION_TYPE);
			if(a == null) continue;
			MethodDeclaration m = n.getNode();
			ValueInfo factoryName = ValueInfo.getValueInfo(a, null);
			if(factoryName == null) {
				factoryName = new ValueInfo();
				factoryName.setValue(toPropertyName(m.getName().getIdentifier(), "get"));
				factoryName.valueLength = m.getName().getLength();
				factoryName.valueStartPosition = m.getName().getStartPosition();
			}
			ValueInfo scope = ValueInfo.getValueInfo(a, ISeamXmlComponentDeclaration.SCOPE);
			ValueInfo autoCreate = ValueInfo.getValueInfo(a, "autoCreate"); //$NON-NLS-1$

			SeamAnnotatedFactory factory = new SeamAnnotatedFactory();
			factory.setParentDeclaration(component);
			IMethod im = findMethod(m);
			factory.setSourceMember(im);
			factory.setId(im);
			factory.setSourcePath(component.getSourcePath());
			factory.setName(factoryName);
			if(autoCreate != null) factory.setAutoCreate(true);
			if(scope != null) {
				factory.setScope(scope);
			}

			ValueInfo _a = new ValueInfo();
			_a.setValue(FACTORY_ANNOTATION_TYPE);
			_a.valueStartPosition = a.getStartPosition();
			_a.valueLength = a.getLength();
			factory.addAttribute(FACTORY_ANNOTATION_TYPE, _a);

			ds.getFactories().add(factory);
		}
	}
	
	private String toPropertyName(String methodName, String prefix) {
		if(methodName == null) {
			return methodName;
		}
		if(methodName.startsWith(prefix) && methodName.length() > prefix.length()) {
			String root = methodName.substring(prefix.length());
			return root.substring(0, 1).toLowerCase() + root.substring(1);
		} 
		return methodName;
	}
	
	void processBijections() {
		Map<BijectedAttributeType, Annotation> as = new HashMap<BijectedAttributeType, Annotation>();
		List<BijectedAttributeType> types = new ArrayList<BijectedAttributeType>();

		for (AnnotatedASTNode<MethodDeclaration> n: annotatedMethods) {
			getBijectedType(n, as, types);

			if(as.isEmpty()) continue;
			boolean isDataModelSelectionType = !types.get(0).isUsingMemberName();
			
			MethodDeclaration m = n.getNode();

			BijectedAttribute att = createBijectedAttribute(types);
			
			Annotation in = as.get(BijectedAttributeType.IN);
			Annotation out = as.get(BijectedAttributeType.OUT);
			Annotation data = as.get(BijectedAttributeType.DATA_BINDER);
			addLocation(att, in, IN_ANNOTATION_TYPE);
			addLocation(att, out, OUT_ANNOTATION_TYPE);
			addLocation(att, data, DATA_MODEL_ANNOTATION_TYPE);
			
			ValueInfo name = findValueInfo(as, null);
			att.setValue(name);
			if(name == null || isDataModelSelectionType
				|| name.getValue() == null || name.getValue().length() == 0) {
				name = new ValueInfo();
				name.valueStartPosition = m.getStartPosition();
				name.valueLength = m.getLength();
				name.setValue(m.getName().getIdentifier());
				if(in != null) {
					name.setValue(toPropertyName(name.getValue(), "set"));
				} else if(out != null || data != null) {
					name.setValue(toPropertyName(name.getValue(), "get"));
				}
			}
			
			att.setName(name);

			ValueInfo scope = findValueInfo(as, "scope"); //$NON-NLS-1$
			if(scope != null) att.setScope(scope);
			
			IMethod im = findMethod(m);
			att.setSourceMember(im);
			att.setId(im);
		}

		for (AnnotatedASTNode<FieldDeclaration> n: annotatedFields) {
			getBijectedType(n, as, types);

			if(as.isEmpty()) continue;
			boolean isDataModelSelectionType = !types.get(0).isUsingMemberName();
			
			FieldDeclaration m = n.getNode();

			BijectedAttribute att = createBijectedAttribute(types);
			
			addLocation(att, as.get(BijectedAttributeType.IN), IN_ANNOTATION_TYPE);
			addLocation(att, as.get(BijectedAttributeType.OUT), OUT_ANNOTATION_TYPE);
			addLocation(att, as.get(BijectedAttributeType.DATA_BINDER), DATA_MODEL_ANNOTATION_TYPE);

			ValueInfo name = findValueInfo(as, null);
			att.setValue(name);
			if(name == null || isDataModelSelectionType
					|| name.getValue() == null || name.getValue().length() == 0) {
				name = new ValueInfo();
				name.valueStartPosition = m.getStartPosition();
				name.valueLength = m.getLength();
				name.setValue(getFieldName(m));
			}
			
			att.setName(name);

			ValueInfo scope = findValueInfo(as, "scope"); //$NON-NLS-1$
			if(scope != null) att.setScope(scope);
			
			IField f = findField(m);
			att.setSourceMember(f);
			att.setId(f);
		}
	}
	private void getBijectedType(AnnotatedASTNode<?> n,
				Map<BijectedAttributeType, Annotation> as, List<BijectedAttributeType> types) {
		as.clear();
		types.clear();
		for (int i = 0; i < BijectedAttributeType.values().length; i++) {
			Annotation a = findAnnotation(n, BijectedAttributeType.values()[i].getAnnotationType());
			if(a != null) {
				as.put(BijectedAttributeType.values()[i], a);
				types.add(BijectedAttributeType.values()[i]);
			}
		}
	}
	private ValueInfo findValueInfo(Map<BijectedAttributeType, Annotation> as, String name) {
		for (BijectedAttributeType t: BijectedAttributeType.values()) {
			Annotation a = as.get(t);
			if(a == null) continue;
			ValueInfo result = ValueInfo.getValueInfo(a, name);
			if(result != null && result.getValue() != null && result.getValue().length() > 0) return result;
		}
		return null;
	}
	private BijectedAttribute createBijectedAttribute(List<BijectedAttributeType> types) {
		boolean isDataModelSelectionType = !types.get(0).isUsingMemberName();
		BijectedAttribute att = (!isDataModelSelectionType)
				? new BijectedAttribute() : new DataModelSelectionAttribute();
		component.addBijectedAttribute(att);
		att.setTypes(types.toArray(new BijectedAttributeType[0]));

		return att;
	}
	private void addLocation(BijectedAttribute att, Annotation a, String name) {
		if(a != null) {
			ValueInfo _a = new ValueInfo();
			_a.setValue(name);
			_a.valueStartPosition = a.getStartPosition();
			_a.valueLength = a.getLength();
			att.addAttribute(name, _a);
		}
	}
	
	void processComponentMethods() {
		for (AnnotatedASTNode<MethodDeclaration> n: annotatedMethods) {
			SeamComponentMethod cm = null;
			for (int i = 0; i < SeamComponentMethodType.values().length; i++) {
				SeamComponentMethodType type = SeamComponentMethodType.values()[i];
				Annotation a = findAnnotation(n, type.getAnnotationType());
				if(a == null) continue;
				if(cm == null) {
					cm = new SeamComponentMethod();
					component.addMethod(cm);
					MethodDeclaration m = n.getNode();
					IMethod im = findMethod(m);
					cm.setSourceMember(im);
					cm.setId(im);
				}
				cm.getTypes().add(type);
			}
		}
	}
	
	void processRoles() {
		Annotation roles = findAnnotation(annotatedType, ROLES_ANNOTATION_TYPE);
		if(roles != null) {
			RolesVisitor visitor = new RolesVisitor((IType)component.getSourceMember());
			roles.accept(visitor);
			List<Annotation> rs = visitor.annotations;
			if(rs != null) for (Annotation role : rs) {
				createRole(role);
			}
		}
		ResolvedAnnotation[] as = annotatedType.getAnnotations();
		if(as != null) for (int i = 0; i < as.length; i++) {
			if(ROLE_ANNOTATION_TYPE.equals(as[i].getType())) {
				createRole(as[i].getAnnotation());
			}
		}		
	}
	
	void createRole(Annotation role) {
		Role r = new Role();
		r.setSourcePath(component.getSourcePath());
		r.setSourceMember(component.getSourceMember());
		
		ValueInfo name = ValueInfo.getValueInfo(role, "name"); //$NON-NLS-1$
		if(name == null) return;
		
		r.setId("" + component.getName() + ":" + name.getValue()); //$NON-NLS-1$ //$NON-NLS-2$
		r.setName(name);

		ValueInfo scope = ValueInfo.getValueInfo(role, "scope"); //$NON-NLS-1$
		if(scope != null) r.setScope(scope);
		
		component.addRole(r);
	}

	private void processImport(Annotation a) {
		List<String> vs = getArrayValue(a);
		for (String v: vs) component.addImport(v);
	}

	private Annotation findAnnotation(AnnotatedASTNode<?> n, String type) {
		ResolvedAnnotation[] as = n.getAnnotations();
		if(as == null) return null;
		for (int i = 0; i < as.length; i++) {
			if(type.equals(as[i].getType())) return as[i].getAnnotation();
		}
		return null;
	}
	
	private IMethod findMethod(MethodDeclaration m) {
		if(m == null || m.getName() == null) return null;
		IType type = (IType)component.getSourceMember();
		IMethod[] ms = null;
		try {
			ms = type.getMethods();
		} catch (JavaModelException e) {
			//ignore
		}
		String name = m.getName().getIdentifier();
		if(ms != null) for (int i = 0; i < ms.length; i++) {
			if(!name.equals(ms[i].getElementName())) continue;
			int s = m.getStartPosition() + m.getLength() / 2;
			try {
				ISourceRange range = ms[i].getSourceRange();
				if(range == null) {
					//no source and we cannot check position.
					return ms[i];
				}
				int b = range.getOffset();
				int e = b + range.getLength();
				if(s >= b && s <= e) return ms[i];
			} catch (JavaModelException e) {
				return ms[i];
			}
		}
		return null;
	}

	private IField findField(FieldDeclaration f) {
		if(f == null || getFieldName(f) == null) return null;
		IType type = (IType)component.getSourceMember();
		IField[] fs = null;

		try {
			fs = type.getFields();
		} catch (JavaModelException e1) {
			// ignore
		}

		String name = getFieldName(f);
		if(fs != null) for (int i = 0; i < fs.length; i++) {
			if(!name.equals(fs[i].getElementName())) continue;
			int s = f.getStartPosition() + f.getLength() / 2;
			try {
				ISourceRange range = fs[i].getSourceRange();
				if(range == null) {
					//no source and we cannot check position.
					return fs[i];
				}
				int b = range.getOffset();
				int e = b + range.getLength();
				if(s >= b && s <= e) return fs[i];
			} catch (JavaModelException e) {
				return fs[i];
			}
		}
		return null;
	}
	
	/**
	 * Returns name of first field
	 * @param node
	 * @return
	 */
	String getFieldName(FieldDeclaration node) {
		List<?> fragments = node.fragments();
		for (int i = 0; i < fragments.size(); i++) {
			VariableDeclaration vd = (VariableDeclaration)fragments.get(i);
			String name = vd.getName().getIdentifier();
			return name;
		}
		return null;
	}
	
	static List<String> getArrayValue(Annotation a) {
		List<String> result = new ArrayList<String>();
		if(a instanceof SingleMemberAnnotation) {
			SingleMemberAnnotation s = (SingleMemberAnnotation)a;
			Expression exp = s.getValue();
			if(exp instanceof StringLiteral) {
				result.add(((StringLiteral)exp).getLiteralValue());
			} else if(exp instanceof QualifiedName) {
				Object o = exp.resolveConstantExpressionValue();
				if(o != null) result.add(o.toString());
				result.add(exp.toString());
			} else if(exp instanceof ArrayInitializer) {
				ArrayInitializer arr = (ArrayInitializer)exp;
				List<?> es = arr.expressions();
				if(es != null) for (Object e: es) {
					Expression exp1 = (Expression)e;
					if(exp1 instanceof StringLiteral) {
						result.add(((StringLiteral)exp1).getLiteralValue());
					}
				}
			}
		}
		return result;
	}

}

class RolesVisitor extends ASTVisitor implements SeamAnnotations {
	boolean arrayFound = false;
	IType type;
	List<Annotation> annotations = new ArrayList<Annotation>();
	
	public RolesVisitor(IType type) {
		this.type = type;
	}

	public boolean visit(SingleMemberAnnotation node) {
		return true;
	}

	public boolean visit(NormalAnnotation node) {
		String typeName = ASTVisitorImpl.resolveType(type, node);
		if(ROLE_ANNOTATION_TYPE.equals(typeName)) {
			annotations.add(node);
			return true;
		}
		return false;
	}

	public boolean visit(ArrayInitializer node) {
		return true;
	}
}
