/******************************************************************************* 
 * 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.lib;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.env.EnumConstantSignature;
import org.eclipse.jdt.internal.compiler.env.IBinaryAnnotation;
import org.eclipse.jdt.internal.compiler.env.IBinaryElementValuePair;
import org.eclipse.jdt.internal.compiler.env.IBinaryField;
import org.eclipse.jdt.internal.compiler.env.IBinaryMethod;
import org.eclipse.jdt.internal.compiler.impl.Constant;
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.SeamComponentMethodType;
import org.jboss.tools.seam.core.SeamCorePlugin;
import org.jboss.tools.seam.internal.core.AbstractContextVariable;
import org.jboss.tools.seam.internal.core.BijectedAttribute;
import org.jboss.tools.seam.internal.core.DataModelSelectionAttribute;
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.SeamMessages;
import org.jboss.tools.seam.internal.core.scanner.LoadedDeclarations;
import org.jboss.tools.seam.internal.core.scanner.Util;
import org.jboss.tools.seam.internal.core.scanner.java.SeamAnnotations;

public class TypeScanner implements SeamAnnotations {

	/**
	 * Checks if class may be a source of seam components. 
	 * @param f
	 * @return
	 */
	public boolean isLikelyComponentSource(ClassFileReader cls) {
		return cls != null && isSeamAnnotatedClass(cls);
	}

	/**
	 * Loads seam components from class.
	 * Returns object that contains loaded components or null;
	 * @param type
	 * @param cls
	 * @param path
	 * @return
	 */
	public LoadedDeclarations parse(IType type, ClassFileReader cls, IPath path) {
		if(!isLikelyComponentSource(cls)) return null;
		LoadedDeclarations ds = new LoadedDeclarations();
		
		SeamJavaComponentDeclaration component = new SeamJavaComponentDeclaration();
		component.setSourcePath(path);
		component.setId(type);
		component.setType(type);
		component.setClassName(type.getFullyQualifiedName());
		process(cls, component, ds);
		
		ds.getComponents().add(component);
		for (int i = 0; i < ds.getFactories().size(); i++) {
			AbstractContextVariable f = (AbstractContextVariable)ds.getFactories().get(i);
			f.setSourcePath(path);
			f.getId();
		}
		return ds;		
	}

	/**
	 * Check if class has at least one seam annotation.
	 * @param cls
	 * @return
	 */
	boolean isSeamAnnotatedClass(ClassFileReader cls) {
		if(cls == null || ((cls.getModifiers() & ClassFileConstants.AccInterface) > 0)) return false;
		IBinaryAnnotation[] as = cls.getAnnotations();
		if(as != null) for (int i = 0; i < as.length; i++) {
			String type = getTypeName(as[i]);
			if(Util.isSeamAnnotationType(type)) {
				return true;
			}
		}
		return false;
	}
	
	public static String getTypeName(IBinaryAnnotation a) {
		if(a.getTypeName() == null) return ""; //$NON-NLS-1$
		String t = new String(a.getTypeName());
		if(t.startsWith("L") && t.endsWith(";")) { //$NON-NLS-1$ //$NON-NLS-2$
			t = t.substring(1, t.length() - 1);
		}
		t = t.replace('/', '.');
		
		return t;
	}

	Map<String,IBinaryAnnotation> getSeamAnnotations(IBinaryAnnotation[] as) {
		if(as == null || as.length == 0) return null;
		Map<String,IBinaryAnnotation> map = null;
		for (int i = 0; i < as.length; i++) {
			String type = getTypeName(as[i]);
			if(Util.isSeamAnnotationType(type)) {
				if(map == null) map = new HashMap<String, IBinaryAnnotation>();
				map.put(type, as[i]);
			}
		}
		return map;
	}

	private void process(ClassFileReader cls, SeamJavaComponentDeclaration component, LoadedDeclarations ds) {
		Map<String, IBinaryAnnotation> map = getSeamAnnotations(cls.getAnnotations());
		if(map != null) {
			IBinaryAnnotation a = map.get(NAME_ANNOTATION_TYPE);
			if(a != null) {
				String name = (String)getValue(a, "value"); //$NON-NLS-1$
				if(name != null) component.setName(name);
			}
			a = map.get(SCOPE_ANNOTATION_TYPE);
			if(a != null) {
				Object scope = getValue(a, "value"); //$NON-NLS-1$
				if(scope != null) component.setScope(scope.toString());
			}
			a = map.get(INSTALL_ANNOTATION_TYPE);
			if(a != null) {
				String precedence = getValue(a, "precedence"); //$NON-NLS-1$
				try {
					int i = Integer.parseInt(precedence);
					component.setPrecedence(i);
				} catch (NumberFormatException e) {
					//ignore
				}
			}
		}
		
		Map<BeanType, IValueInfo> types = new HashMap<BeanType, IValueInfo>();
		for (int i = 0; i < BeanType.values().length; i++) {
			BeanType t = BeanType.values()[i];
			IBinaryAnnotation a = map.get(t.getAnnotationType());
			if(a != null) {
				ValueInfo v = new ValueInfo();
				v.setValue("true"); //$NON-NLS-1$
				types.put(t, v);
			}
		}
		if(!types.isEmpty()) {
			component.setTypes(types);
		}

		
		IBinaryMethod[] ms = null;
		try {
			ms = cls.getMethods();
		} catch (NoClassDefFoundError e) {
			//ignore
		}
		if(ms != null) for (int i = 0; i < ms.length; i++) {
			process(ms[i], component, ds);
		}
		
//		IBinaryField[] fs = null;
//		try {
//			fs = cls.getFields();
//		} catch (NoClassDefFoundError e) {
//			//ignore
//		}
//		if(fs != null) for (int i = 0; i < fs.length; i++) {
//			//TODO
//		}
	}

	private void process(IBinaryMethod m, SeamJavaComponentDeclaration component, LoadedDeclarations ds) {
		Map<String,IBinaryAnnotation> map = getSeamAnnotations(m.getAnnotations());
		if(map == null || map.isEmpty()) return;
		IBinaryAnnotation a = map.get(FACTORY_ANNOTATION_TYPE);
		if(a != null) {
			processFactory(m, a, component, ds);
		}
		processBijection(m, map, component, ds);
		processComponentMethod(m, map, component, ds);
	}
	
	private void processFactory(IBinaryMethod m, IBinaryAnnotation a, SeamJavaComponentDeclaration component, LoadedDeclarations ds) {
		if(a == null) return;
		String name = (String)getValue(a, "value"); //$NON-NLS-1$
		if(name == null || name.length() == 0) {
			name = new String(m.getSelector());
		}
		SeamAnnotatedFactory factory = 
			("org.jboss.seam.international.messages".equals(name) && new String(m.getSelector()).equals("getMessages")) 
			? new SeamMessages()
			: new SeamAnnotatedFactory();
		factory.setParentDeclaration(component);
		factory.setSourcePath(component.getSourcePath());
		ds.getFactories().add(factory);
		IMethod im = findIMethod(component, m);
		
		factory.setId(im);
		factory.setSourceMember(im);
		factory.setName(name);
			
		Object scope = getValue(a, "scope"); //$NON-NLS-1$
		if(scope != null) factory.setScopeAsString(scope.toString());
		Object autoCreate = getValue(a, "autoCreate"); //$NON-NLS-1$
		if(autoCreate instanceof Boolean) {
			factory.setAutoCreate((Boolean)autoCreate);
		}
	}
	
	private void processBijection(IBinaryMethod m, Map<String,IBinaryAnnotation> map, SeamJavaComponentDeclaration component, LoadedDeclarations ds) {
		Map<BijectedAttributeType, IBinaryAnnotation> as = new HashMap<BijectedAttributeType, IBinaryAnnotation>();
		List<BijectedAttributeType> types = new ArrayList<BijectedAttributeType>();
		IBinaryAnnotation main = null;
		for (int i = 0; i < BijectedAttributeType.values().length; i++) {
			IBinaryAnnotation a = map.get(BijectedAttributeType.values()[i].getAnnotationType());
			if(a != null) {
				as.put(BijectedAttributeType.values()[i], a);
				if(main == null) main = a;
				types.add(BijectedAttributeType.values()[i]);
			}
		}
		if(as.isEmpty()) return;
		
		boolean isDataModelSelectionType = !types.get(0).isUsingMemberName();

		BijectedAttribute att = (!isDataModelSelectionType)
			? new BijectedAttribute() : new DataModelSelectionAttribute();
		component.addBijectedAttribute(att);

		att.setTypes(types.toArray(new BijectedAttributeType[0]));

		String name = (String)getValue(main, "value"); //$NON-NLS-1$
		att.setValue(name);
		if(name == null || name.length() == 0 || isDataModelSelectionType) {
			name = new String(m.getSelector());
		}
		att.setName(name);
		Object scope = getValue(main, "scope"); //$NON-NLS-1$
		if(scope != null) att.setScopeAsString(scope.toString());

		IMember im = findIMethod(component, m);
		att.setSourceMember(im);
		
	}

	void processComponentMethod(IBinaryMethod m, Map<String,IBinaryAnnotation> map, SeamJavaComponentDeclaration component, LoadedDeclarations ds) {
		SeamComponentMethod cm = null;
		for (int i = 0; i < SeamComponentMethodType.values().length; i++) {
			SeamComponentMethodType type = SeamComponentMethodType.values()[i];
			IBinaryAnnotation a = map.get(type.getAnnotationType());
			if(a == null) continue;
			if(cm == null) {
				cm = new SeamComponentMethod();
				component.addMethod(cm);
				IMethod im = findIMethod(component, m);
				cm.setSourceMember(im);
				cm.setId(im);
			}
			cm.getTypes().add(type);
		}
	}

	public static String getValue(IBinaryAnnotation a, String method) {
		try {
			IBinaryElementValuePair[] ps = a.getElementValuePairs();
			if(ps != null) for (int i = 0; i < ps.length; i++) {
				if(method.equals(new String(ps[i].getName()))) {
					Object v = ps[i].getValue();
					if(v == null) return null;
					if(v instanceof EnumConstantSignature) {
						EnumConstantSignature cs = (EnumConstantSignature)v;
						char[] cv = cs.getEnumConstantName();
						return cv == null ? null : new String(cv);
					} else if(v instanceof Constant) {
						Constant ic = (Constant)v;
						return ic.stringValue();
					}
					v = v.toString();
					return (String)v;
				}
			}
		} catch (Throwable e) {
			SeamCorePlugin.getPluginLog().logError(e);
		}
		return null;
	}

	private IMethod findIMethod(SeamJavaComponentDeclaration component, IBinaryMethod m) {
		String name = new String(m.getSelector());
		IType type = (IType)component.getSourceMember();
		String signature = new String(m.getMethodDescriptor());
		IMethod im = null;
		IMethod[] ms = null;
		try {
			ms = type.getMethods();
		} catch (JavaModelException e) {
			SeamCorePlugin.getDefault().logError(e);
		}
		if(ms != null) for (int i = 0; i < ms.length; i++) {
			if(!ms[i].getElementName().equals(name)) continue;
			//check parameters
			try {
				if(ms[i].getParameterNames().length != m.getArgumentNames().length) continue;
			} catch (JavaModelException e) {
				continue;
			}
			//compare
			return ms[i];
		}
		return null;
	}

	private IField findIField(SeamJavaComponentDeclaration component, Field m) {
		IType type = (IType)component.getSourceMember();
		return type.getField(m.getName());
	}

}
