/******************************************************************************* 
 * 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.jsf.jsf2.bean.build;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.jboss.tools.common.EclipseUtil;
import org.jboss.tools.common.model.XModelObject;
import org.jboss.tools.common.model.plugin.ModelPlugin;
import org.jboss.tools.common.model.project.ProjectHome;
import org.jboss.tools.common.model.util.EclipseResourceUtil;
import org.jboss.tools.common.web.WebUtils;
import org.jboss.tools.jsf.JSFModelPlugin;
import org.jboss.tools.jsf.jsf2.bean.model.JSF2ProjectFactory;
import org.jboss.tools.jsf.jsf2.bean.model.impl.DefinitionContext;
import org.jboss.tools.jsf.jsf2.bean.model.impl.FacesConfigDefinition;
import org.jboss.tools.jsf.jsf2.bean.model.impl.JSF2Project;
import org.jboss.tools.jsf.jsf2.bean.model.impl.TypeDefinition;
import org.jboss.tools.jsf.jsf2.bean.scanner.FileSet;
import org.jboss.tools.jst.web.kb.internal.IIncrementalProjectBuilderExtension;

public class JSF2ProjectBuilder extends IncrementalProjectBuilder implements IIncrementalProjectBuilderExtension {

	JSF2ResourceVisitor resourceVisitor = null;

	public JSF2ProjectBuilder() {}

	protected JSF2Project getJSF2Project() {
		IProject p = getProject();
		if(p == null) return null;
		return (JSF2Project)JSF2ProjectFactory.getJSF2Project(p, false);
	}

	JSF2ResourceVisitor getResourceVisitor() {
		if(resourceVisitor == null) {
			resourceVisitor = new JSF2ResourceVisitor();
		}
		return resourceVisitor;
	}

	@Override
	public IProject[] build(int kind, Map<String, String> args,
			IProgressMonitor monitor) throws CoreException {
		resourceVisitor = null;
	
		JSF2Project n = getJSF2Project();
		if(n == null) {
			return null;
		}
		
		if(n.hasNoStorage()) {
			kind = FULL_BUILD;
		}
		
		n.postponeFiring();

		try {
			n.resolveStorage(kind != FULL_BUILD);

			//1. Check class path.
			boolean isClassPathUpdated = n.getClassPath().update();

			Map<String, XModelObject> newJars = new HashMap<String, XModelObject>();
			if(isClassPathUpdated) {
				//2. Update class path. Removed paths will be cached to be applied to working copy of context. 
				n.getClassPath().setSrcs(getResourceVisitor().srcs);
				newJars = n.getClassPath().process();

			}

			//4. Create working copy of context.
			n.getDefinitions().newWorkingCopy(kind == FULL_BUILD);

			//5. Modify working copy of context.
			//5.1 Apply Removed paths.
			if(isClassPathUpdated) {
				n.getClassPath().applyRemovedPaths();
			}
		
			//5.2 Discover sources and build definitions.
			if(isClassPathUpdated) {
				buildJars(newJars);
				
				n.getClassPath().validateProjectDependencies();
				
				kind = FULL_BUILD;
			} else if(n.getClassPath().hasToUpdateProjectDependencies()) {
				n.getClassPath().validateProjectDependencies();
			}

			if (kind == FULL_BUILD) {
				fullBuild(monitor);
			} else {
				IResourceDelta delta = getDelta(getProject());
				if (delta == null) {
					fullBuild(monitor);
				} else {
					incrementalBuild(delta, monitor);
				}
			}

			// 6. Save created definitions to project context and build beans.
			getJSF2Project().getDefinitions().applyWorkingCopy();
			
			try {
				n.store();
			} catch (IOException e) {
				JSFModelPlugin.getDefault().logError(e); //$NON-NLS-1$
			}
		} finally {
			n.fireChanges();
		}

		return null;
	}

	protected void fullBuild(final IProgressMonitor monitor)
			throws CoreException {
			try {
				JSF2ResourceVisitor rv = getResourceVisitor();
				rv.incremental = false;
				getProject().accept(rv);
				FileSet fs = rv.fileSet;
				build(fs, getJSF2Project());
				
			} catch (CoreException e) {
				JSFModelPlugin.getDefault().logError(e);
			}
		}

		protected void incrementalBuild(IResourceDelta delta,
				IProgressMonitor monitor) throws CoreException {
			JSF2ResourceVisitor rv = getResourceVisitor();
			rv.incremental = true;
			delta.accept(new SampleDeltaVisitor());
			FileSet fs = rv.fileSet;
			build(fs, getJSF2Project());
		}

		protected void buildJars(Map<String, XModelObject> newJars) throws CoreException {
			IJavaProject jp = EclipseResourceUtil.getJavaProject(getJSF2Project().getProject());
			if(jp == null) return;
			FileSet fileSet = new FileSet();
			
			for (String jar: newJars.keySet()) {
				Path path = new Path(jar);
				IPackageFragmentRoot root = jp.getPackageFragmentRoot(jar);
				if(root == null) continue;
				if(!root.exists()) {
					IFile f = EclipseResourceUtil.getFile(jar);
					if(f != null && f.exists()) {
						root = jp.getPackageFragmentRoot(f);
					} else {
						f = EclipseResourceUtil.getFile(jar + "/META-INF/web-fragment.xml");
						if(f != null && f.exists()) {
							root = jp.getPackageFragmentRoot(f.getParent().getParent());
						}
					}
				}
				if (root == null || !root.exists())
					continue;
				IJavaElement[] es = root.getChildren();
				for (IJavaElement e : es) {
					if (e instanceof IPackageFragment) {
						IPackageFragment pf = (IPackageFragment) e;
						IClassFile[] cs = pf.getClassFiles();
						for (IClassFile c : cs) {
							fileSet.add(path, c.getType());
						}
					}
				}
			}
			build(fileSet, getJSF2Project());
		}

	void build(FileSet fs, JSF2Project project) {
		DefinitionContext context = getJSF2Project().getDefinitions().getWorkingCopy();
		Map<IPath, Set<IType>> cs = fs.getClasses();
		for (IPath f: cs.keySet()) {
			Set<IType> ts = cs.get(f);
			for (IType type: ts) {
				TypeDefinition def = new TypeDefinition();
				def.setType(type, context, TypeDefinition.FLAG_ALL_MEMBERS);
				context.addType(f, type.getFullyQualifiedName(), def);
			}
		}
		IFile facesConfig = fs.getFacesConfig();
		if(facesConfig != null) {
			FacesConfigDefinition def = new FacesConfigDefinition();
			def.setPath(facesConfig.getFullPath());
			XModelObject o = EclipseResourceUtil.createObjectForResource(facesConfig);
			if(o != null) {
				def.setObject(o);
				context.setFacesConfig(def);
			}
		}
	}

	class SampleDeltaVisitor implements IResourceDeltaVisitor {
		/*
		 * @see org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta)
		 */
		public boolean visit(IResourceDelta delta) throws CoreException {
			IResource resource = delta.getResource();
			switch (delta.getKind()) {
			case IResourceDelta.ADDED:
				return getResourceVisitor().visit(resource);
			case IResourceDelta.REMOVED:
				JSF2Project p = getJSF2Project();
				if(p != null) {
					p.getDefinitions().getWorkingCopy().clean(resource.getFullPath());
				}
				break;
			case IResourceDelta.CHANGED:
				return getResourceVisitor().visit(resource);
			}
			//return true to continue visiting children.
			return true;
		}
	}

	class JSF2ResourceVisitor implements IResourceVisitor {
		boolean incremental = false;
		FileSet fileSet = new FileSet();
		IPath[] outs = new IPath[0];
		IPath[] srcs = new IPath[0];
		IPath[] webinfs = new IPath[0];
		Set<IPath> visited = new HashSet<IPath>();

		public JSF2ResourceVisitor() {
			getJavaSourceRoots(getProject());
			webinfs = WebUtils.getWebInfPaths(getProject());
		}

		void getJavaSourceRoots(IProject project) {
			IJavaProject javaProject = EclipseResourceUtil.getJavaProject(project);
			if(javaProject == null) return;
			List<IPath> ps = new ArrayList<IPath>();
			List<IPath> os = new ArrayList<IPath>();
			try {
				IPath output = javaProject.getOutputLocation();
				if(output != null) os.add(output);
				IClasspathEntry[] es = javaProject.getResolvedClasspath(true);
				for (int i = 0; i < es.length; i++) {
					if(es[i].getEntryKind() == IClasspathEntry.CPE_SOURCE) {
						IResource findMember = ModelPlugin.getWorkspace().getRoot().findMember(es[i].getPath());
						if(findMember != null && findMember.exists()) {
							ps.add(findMember.getFullPath());
						}
						IPath out = es[i].getOutputLocation();
						if(out != null && !os.contains(out)) {
							os.add(out);
						}
					} 
				}
				srcs = ps.toArray(new IPath[0]);
				outs = os.toArray(new IPath[0]);
			} catch(CoreException ce) {
				JSFModelPlugin.getDefault().logError("Error while locating java source roots for " + project, ce);
			}
		}

		@Override
		public boolean visit(IResource resource) throws CoreException {
			IPath path = resource.getFullPath();
			if(resource instanceof IFile) {
				if(visited.contains(path)) {
					return false;
				}
				visited.add(path);
				IFile f = (IFile)resource;
				for (int i = 0; i < outs.length; i++) {
					if(outs[i].isPrefixOf(path)) {
						return false;
					}
				}
				for (int i = 0; i < srcs.length; i++) {
					if(srcs[i].isPrefixOf(path)) {
						if(f.getName().endsWith(".java")) {
							ICompilationUnit unit = EclipseUtil.getCompilationUnit(f);
							if(unit!=null) {
								if(f.getName().equals("package-info.java")) {
									IPackageDeclaration[] pkg = unit.getPackageDeclarations();
									if(pkg != null && pkg.length > 0) {
										fileSet.add(f.getFullPath(), pkg[0]);
										if(incremental) {
											IResource[] ms = resource.getParent().members();
											for (IResource m: ms) {
												if(m instanceof IFile && !m.getName().equals("package-info.java")) {
													visit(m);
												}
											}
										}
									}
								} else {
									IType[] ts = unit.getTypes();
									fileSet.add(f.getFullPath(), ts);
								}
							}
						}
						Set<IFile> ds = getDependentFiles(path, visited);
						if(ds != null) for (IFile d: ds) visit(d);
						return false;
					}
				}
				for (int i = 0; i < webinfs.length; i++) {
					if(webinfs[i].isPrefixOf(path) && f.getName().equals("faces-config.xml")
						&& path.removeLastSegments(1).equals(webinfs[i])) {
						fileSet.setFacesConfig(f);
					}
				}
				Set<IFile> ds = getDependentFiles(path, visited);
				if(ds != null) for (IFile d: ds) visit(d);
			}
			
			if(resource instanceof IFolder) {
				for (int i = 0; i < outs.length; i++) {
					if(outs[i].isPrefixOf(path)) {
						return false;
					}
				}
				for (int i = 0; i < srcs.length; i++) {
					if(srcs[i].isPrefixOf(path) || path.isPrefixOf(srcs[i])) {
						return true;
					}
				}
				if(resource == resource.getProject()) {
					return true;
				}
				for (IPath webinf: webinfs) {
					if(webinf.isPrefixOf(path) || path.isPrefixOf(webinf)) {
						return true;
					}
				}
				return false;
			}
			//return true to continue visiting children.
			return true;
		}
		
	}

	//In case if we will need that
	Set<IFile> getDependentFiles(IPath path, Set<IPath> visited) {
		return null;
	}

}
