/******************************************************************************* 
 * Copyright (c) 2011 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.cdi.seam.config.core.definition;

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

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.jboss.tools.cdi.internal.core.impl.definition.AbstractMemberDefinition;
import org.jboss.tools.cdi.internal.core.impl.definition.DefinitionContext;
import org.jboss.tools.cdi.internal.core.impl.definition.FieldDefinition;
import org.jboss.tools.cdi.internal.core.impl.definition.MethodDefinition;
import org.jboss.tools.cdi.internal.core.impl.definition.ParameterDefinition;
import org.jboss.tools.cdi.internal.core.impl.definition.TypeDefinition;
import org.jboss.tools.cdi.seam.config.core.CDISeamConfigCorePlugin;
import org.jboss.tools.cdi.seam.config.core.ConfigDefinitionContext;
import org.jboss.tools.cdi.seam.config.core.util.Util;
import org.jboss.tools.cdi.seam.config.core.xml.SAXElement;
import org.jboss.tools.cdi.seam.config.core.xml.SAXNode;
import org.jboss.tools.common.java.IAnnotationDeclaration;
import org.jboss.tools.common.java.IJavaAnnotation;
import org.jboss.tools.common.java.IParametedType;
import org.jboss.tools.common.java.ParametedType;
import org.jboss.tools.common.java.TypeDeclaration;
import org.jboss.tools.common.model.XModelObject;

/**
 * 
 * @author Viacheslav Kabanovich
 *
 */
public class SeamBeansDefinition {
	IResource resource;
	XModelObject file;
	List<SAXNodeProblem> unresolvedNodes = new ArrayList<SAXNodeProblem>();
	Set<String> possibleTypeNames = new HashSet<String>();

	Set<SeamBeanDefinition> beanDefinitions = new HashSet<SeamBeanDefinition>();
	Set<SeamVirtualFieldDefinition> virtualFieldDefinitions = new HashSet<SeamVirtualFieldDefinition>();

	List<TypeDefinition> typeDefinitions = new ArrayList<TypeDefinition>();
	List<IType> replacedAndModified = new ArrayList<IType>();

	public SeamBeansDefinition() {}

	public void setResource(IResource resource) {
		this.resource = resource;
	}

	public void setFileObject(XModelObject file) {
		this.file = file;
	}

	public XModelObject getFileObject() {
		return file;
	}

	public IResource getResource() {
		return resource;
	}

	public List<SAXNodeProblem> getUnresolvedNodes() {
		return unresolvedNodes;
	}

	/**
	 * Returns type names that could resolve unresolved nodes if such types existed.
	 * 
	 * @return
	 */
	public Set<String> getPossibleTypeNames() {
		return possibleTypeNames;
	}

	public void addUnresolvedNode(SAXNodeProblem problem) {
		unresolvedNodes.add(problem);
	}

	public void addUnresolvedNode(SAXNode node, String problemId, String message) {
		addUnresolvedNode(new SAXNodeProblem(node, problemId, message));
	}

	public void addPossibleTypeNames(Set<String> types) {
		possibleTypeNames.addAll(types);
	}

	public void addBeanDefinition(SeamBeanDefinition def) {
		beanDefinitions.add(def);
	}

	public Set<SeamBeanDefinition> getBeanDefinitions() {
		return beanDefinitions;
	}

	public void addVirtualField(SeamVirtualFieldDefinition def) {
		virtualFieldDefinitions.add(def);
	}

	public Set<SeamVirtualFieldDefinition> getVirtualFieldDefinitions() {
		return virtualFieldDefinitions;
	}

	public List<TypeDefinition> getTypeDefinitions() {
		return typeDefinitions;
	}

	public void buildTypeDefinitions(ConfigDefinitionContext context) {
		typeDefinitions.clear();

		for (SeamBeanDefinition def: beanDefinitions) {
			IType type = def.getType();
			ConfigTypeDefinition typeDef = new ConfigTypeDefinition();
			typeDef.setFileObject(file);
			boolean replaces = def.getReplacesLocation() != null;
			boolean modifies = def.getModifiesLocation() != null;
			if(replaces || modifies) {
				replacedAndModified.add(type);
				((DefinitionContext)context.getRootContext()).veto(type);
			}
			//Initialize typeDef taking into account replaces and modifies
			int flags = AbstractMemberDefinition.FLAG_ALL_MEMBERS;
			if(!modifies) {
				//For replacing or created - no annotations loaded.
				flags |= AbstractMemberDefinition.FLAG_NO_ANNOTATIONS;
			}
			typeDef.setType(type, context.getRootContext(), flags);
			def.setConfigType(typeDef);

			mergeTypeDefinition(def, typeDef, context);

			typeDefinitions.add(typeDef);
		}
		
		for (SeamVirtualFieldDefinition def: virtualFieldDefinitions) {
			IType type = def.getType();
			ConfigVirtualFieldDefinition typeDef = new ConfigVirtualFieldDefinition();
			typeDef.setFileObject(file);
			typeDef.setConfig(def);
			int flags = AbstractMemberDefinition.FLAG_NO_ANNOTATIONS;
			typeDef.setType(type, context.getRootContext(), flags);
			mergeAnnotations(def, typeDef, context);
			//That is how field producers differ from class beans. They do not need a bean constructor.
			typeDef.setBeanConstructor(true);
			typeDefinitions.add(typeDef);
		}
	}

	public void clean(ConfigDefinitionContext context) {
		List<IType> ds = replacedAndModified;
		replacedAndModified = new ArrayList<IType>();
		for (IType type: ds) {
			((DefinitionContext)context.getRootContext()).unveto(type);
		}
	}

	public SeamMemberDefinition findExactly(int offset) {
		for (SeamBeanDefinition b: beanDefinitions) {
			SeamMemberDefinition d = b.findExactly(offset);
			if(d != null) return d;
		}
		return null;
	}

	private void mergeTypeDefinition(SeamBeanDefinition def, ConfigTypeDefinition typeDef, ConfigDefinitionContext context) {
		typeDef.setConfig(def);
		mergeAnnotations(def, typeDef, context);
		
		List<FieldDefinition> fieldDefs = typeDef.getFields();
		//virtual field definitions are used for injections of inline beans as values of collections and maps.
		List<FieldDefinition> virtualDefs = new ArrayList<FieldDefinition>();
		for (FieldDefinition fieldDef:fieldDefs) {
			String n = fieldDef.getField().getElementName();
			SeamFieldDefinition f = def.getField(n);
			if(f != null) {
				ParametedType t = null;
				IParametedType collection = null;
				IParametedType map = null;
				IParametedType object = null;
				try {
					String returnType = fieldDef.getField().getTypeSignature();
					t = context.getRootContext().getProject().getTypeFactory().getParametedType(fieldDef.getField(), returnType);
					object = context.getRootContext().getProject().getTypeFactory().getParametedType(fieldDef.getField(), "QObject;");
				} catch (CoreException e) {
					CDISeamConfigCorePlugin.getDefault().logError(e);
				}
				if(t != null && t.getType() != null) {
					collection = getCollection(t);
					map = getMap(t);
				}
				List<SeamFieldValueDefinition> vs = f.getValueDefinitions();
				if(collection != null) {
					List<? extends IParametedType> ps = t.getParameters();
					IParametedType elementType = ps.isEmpty() ? object : ps.get(0);
					for (SeamFieldValueDefinition v: vs) {
						ConfigFieldDefinition virtual = new ConfigFieldDefinition(file);
						virtual.setTypeDefinition(fieldDef.getTypeDefinition());
						virtual.setField(fieldDef.getField(), context.getRootContext(), AbstractMemberDefinition.FLAG_NO_ANNOTATIONS);
						virtual.setOverridenType(new TypeDeclaration((ParametedType)elementType, fieldDef.getField().getResource(), 0, 0));
						virtual.setConfig(v);
						virtualDefs.add(virtual);
						v.setRequiredType(elementType);
						mergeAnnotations(v, virtual, context);
					}
				} else if(map != null) {
					List<? extends IParametedType> ps = t.getParameters();
					IParametedType keyType = ps.isEmpty() ? object : ps.get(0);
					IParametedType valueType = ps.size() < 2 ? object : ps.get(1);
					for (SeamFieldValueDefinition v: vs) {
						ConfigFieldDefinition virtual = new ConfigFieldDefinition(file);
						virtual.setTypeDefinition(fieldDef.getTypeDefinition());
						virtual.setField(fieldDef.getField(), context.getRootContext(), AbstractMemberDefinition.FLAG_NO_ANNOTATIONS);
						IParametedType vType = Util.isKey((SAXElement)v.getNode()) ? keyType : valueType;
						v.setRequiredType(vType);
						virtual.setOverridenType(new TypeDeclaration((ParametedType)vType, fieldDef.getField().getResource(), 0, 0));
						virtual.setConfig(v);
						virtualDefs.add(virtual);
						mergeAnnotations(v, virtual, context);
					}
				} else {
					for (SeamFieldValueDefinition v: vs) {
						v.setRequiredType(t);
					}
				}

				((ConfigFieldDefinition)fieldDef).setConfig(f);
				mergeAnnotations(f, fieldDef, context);
				
				if(!vs.isEmpty() && collection == null && map == null) {
					mergeAnnotations(vs.get(0), fieldDef, context);
				}
				
			}
		}
		fieldDefs.addAll(virtualDefs);
	
		List<MethodDefinition> methodDefs = typeDef.getMethods();
		for (MethodDefinition methodDef: methodDefs) {
			IMethod method = methodDef.getMethod();
			if(method == null) continue;
			SeamMethodDefinition m = def.getMethod(method);
			if(m != null) {
				((ConfigMethodDefinition)methodDef).setConfig(m);
				mergeAnnotations(m, methodDef, context);
				List<ParameterDefinition> psDefs = methodDef.getParameters();
				List<SeamParameterDefinition> ps = m.getParameters();
				if(ps.size() != psDefs.size()) {
					//do nothing
				} else {
					for (int i = 0; i < ps.size(); i++) {
						mergeAnnotations(ps.get(i), psDefs.get(i), context);
					}
				}
			}
		}
		
		typeDef.checkConstructor();
		
	}

	private void mergeAnnotations(SeamMemberDefinition def, AbstractMemberDefinition memberDef, ConfigDefinitionContext context) {
		Map<String, IJavaAnnotation> annotations = def.getAnnotations();
		for (String typeName: annotations.keySet()) {
			IJavaAnnotation ja = annotations.get(typeName);
			IAnnotationDeclaration current = memberDef.getAnnotation(typeName);
			if(current != null) memberDef.removeAnnotation(current);
			memberDef.addAnnotation(ja, context.getRootContext());
		}
	}

	private IParametedType getCollection(ParametedType t) {
		for (IParametedType i: t.getAllTypes()) {
			if("java.util.Collection".equals(i.getType().getFullyQualifiedName())) {
				return i;
			}
		}
		return null;
	}

	private IParametedType getMap(ParametedType t) {
		for (IParametedType i: t.getAllTypes()) {
			if("java.util.Map".equals(i.getType().getFullyQualifiedName())) {
				return i;
			}
		}
		return null;
	}

}
