/*******************************************************************************
  * Copyright (c) 2007-2012 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.common.el.core.resolver;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.ui.internal.contentassist.ContentAssistUtils;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr;
import org.jboss.tools.common.el.core.ELCorePlugin;
import org.jboss.tools.common.el.core.model.ELExpression;
import org.jboss.tools.common.el.core.parser.ELParserFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * This class helps to find var/value attributes in DOM tree.   
 * @author Alexey Kazakov
 */
@SuppressWarnings("restriction")
public class ElVarSearcher {

	private final static String VAR_ATTRIBUTE_NAME = "var"; //$NON-NLS-1$
	private final static String VALUE_ATTRIBUTE_NAME = "value"; //$NON-NLS-1$

	private IFile file;
	private ELCompletionEngine engine;

	/**
	 * Constructor.
	 * @param project Seam project where we will look for vars.
	 * @param file File where we will look for vars.
	 * @param engine Competion Engine that we will use for resolving vars.
	 */
	public ElVarSearcher(IFile file, ELCompletionEngine engine) {
		this.file = file;
		this.engine = engine;
	}

	/**
	 * Constructor.
	 * @param project Seam project where we will look for vars.
	 * @param engine Competion Engine that we will use for resolving vars.
	 */
	public ElVarSearcher(ELCompletionEngine engine) {
		this(null, engine);
	}

	/**
	 * @param file File where we will look for vars.
	 */
	public void setFile(IFile file) {
		this.file = file;
	}

	/**
	 * @param viewer
	 * @param offset
	 * @return
	 */
	public static Node getNode(ITextViewer viewer, int offset) {
		IndexedRegion treeNode = ContentAssistUtils.getNodeAt(viewer, offset);
		if(treeNode instanceof Node) {
			return (Node)treeNode;
		}
		return null;
	}

	/**
	 * @param viewer
	 * @param offset
	 * @return
	 */

	public static Node getNode(IFile file, int offset) {
		IndexedRegion treeNode = getNodeAt(file, offset);
		if(treeNode instanceof Node) {
			return (Node)treeNode;
		}
		return null;
	}

	/**
	 * Returns the closest IndexedRegion for the offset and viewer allowing
	 * for differences between viewer offsets and model positions. note: this
	 * method returns an IndexedRegion for read only
	 * 
	 * @param file
	 *            the file whose document is used to compute the proposals
	 * @param documentOffset
	 *            an offset within the document for which completions should
	 *            be computed
	 * @return an IndexedRegion
	 */

	public static IndexedRegion getNodeAt(IFile file, int documentOffset) {
		IndexedRegion node = null;
		if (file != null) { 
			IModelManager mm = StructuredModelManager.getModelManager();
			IStructuredModel model = null;
			if (mm != null) {
				try {
					model = mm.getModelForRead(file);
					if (model != null) {
						int lastOffset = documentOffset;
						node = model.getIndexedRegion(documentOffset);
						while (node == null && lastOffset >= 0) {
							lastOffset--;
							node = model.getIndexedRegion(lastOffset);
						}
					}
				} catch (IOException e) {
					ELCorePlugin.getDefault().logError(e);
					node = null;
				} catch (CoreException e) {
					ELCorePlugin.getDefault().logError(e);
					node = null;
				} finally {
					if (model != null)
						model.releaseFromRead();
				}
			}
		}
		return node;
	}

	/**
	 * @param node
	 * @return All var/value that can be used in this position and null if can't find anyone.
	 */
	public List<Var> findAllVars(ITextViewer viewer, int offset) {
		Node node = getNode(viewer, offset);
		if(node!=null) {
			return findAllVars(node);
		}
		return Collections.emptyList();
	}

	/**
	 * @param node
	 * @return All var/value that can be used in this position and null if can't find anyone.
	 */
	public List<Var> findAllVars(IFile file, int offset) {
		return findAllVars(file, offset, engine.getParserFactory());
	}

	/**
	 * @param context
	 * @param offset
	 * @param resolver
	 * @return All var/value that can be used in this position and null if can't find anyone.
	 */
	public static List<Var> findAllVars(ELContext context, int offset, ELResolver resolver) {
		Node node = getNode(context.getResource(), offset);
		if(node!=null) {
			List<Var> result = findAllVars(node, resolver.getParserFactory());
			for (Var v: result) v.setFile(context.getResource());
			return result;
		}
		return Collections.emptyList();
	}

	/**
	 * @param node
	 * @param factory
	 * @return All var/value that can be used in this position and null if can't find anyone.
	 */
	public static List<Var> findAllVars(IFile file, int offset, ELParserFactory factory) {
		Node node = getNode(file, offset);
		if(node!=null) {
			List<Var> result = findAllVars(node, factory);
			for (Var v: result) v.setFile(file);
			return result;
		}
		return Collections.emptyList();
	}

	/**
	 * @param node
	 * @return All var/value that can be used in this position and null if can't find anyone.
	 */
	public static List<Var> findAllVars(ITextViewer viewer, int offset, ELParserFactory factory) {
		Node node = getNode(viewer, offset);
		if(node!=null) {
			return findAllVars(node, factory);
		}
		return Collections.emptyList();
	}

	/**
	 * @param node
	 * @return All var/value that can be used in node and null if can't find anyone.
	 */
	public List<Var> findAllVars(Node node) {
		return findAllVars(node, engine.getParserFactory());
	}

	/**
	 * @param node
	 * @param factory
	 * @return All var/value that can be used in node and null if can't find anyone.
	 */
	public static List<Var> findAllVars(Node node, ELParserFactory factory) {
		ArrayList<Var> vars = new ArrayList<Var>();
		Node parentNode = node;
		while(parentNode!=null) {
			Var var = findVar(parentNode, factory);
			if(var!=null) {
				vars.add(0, var);
			}
			parentNode = parentNode.getParentNode();
		}
		return vars;
	}

	/**
	 * @param node
	 * @return found var/value that can be used in this position and null if can't find anyone.
	 */
	public Var findVar(IFile file, int offset) {
		Node node = getNode(file, offset);
		if(node!=null) {
			Var result = findVar(node);
			if(result != null) result.setFile(file);
			return result;
		}
		return null;
	}

	/**
	 * Finds var/value attribute in node
	 * @param node
	 * @param vars
	 * @return found var/value or null
	 */
	public Var findVar(Node node) {
		return findVar(node, engine.getParserFactory());
	}

	/**
	 * Finds var/value attribute in node
	 * @param node
	 * @param vars
	 * @param factory
	 * @return found var/value or null
	 */
	public static Var findVar(Node node, ELParserFactory factory) {
		if(factory!=null && node!=null && Node.ELEMENT_NODE == node.getNodeType()) {
			Element element = (Element)node;
			synchronized (element) {
				if(element.hasAttribute(VAR_ATTRIBUTE_NAME)) {
					String var = element.getAttribute(VAR_ATTRIBUTE_NAME);
					int declOffset = 0;
					int declLength = 0;
					Node varAttr = element.getAttributeNode(VAR_ATTRIBUTE_NAME); 
					if (varAttr instanceof IDOMAttr) {
						int varNameStart = ((IDOMAttr)varAttr).getNameRegionStartOffset();
						int varNameEnd = ((IDOMAttr)varAttr).getNameRegionEndOffset();
						declOffset = varNameStart;
						declLength = varNameEnd - varNameStart;
					}
					var = var.trim();
					if(!"".equals(var)) { //$NON-NLS-1$					
						if(element.hasAttribute(VALUE_ATTRIBUTE_NAME)) {
							String value = element.getAttribute(VALUE_ATTRIBUTE_NAME);
							value = value.trim();
							Var newVar = new Var(factory, var, value, declOffset, declLength);
							if(newVar.getElToken()!=null) {
								return newVar;
							}
						}
					}
				}
			}
		}

		return null;
	}

	/**
	 * Finds var in list of vars that is used in given EL.
	 * @param el EL without brackets.
	 * @param vars
	 * @param initializeNestedVars
	 * @return
	 */
	public Var findVarForEl(String el, ELContext context, List<Var> vars, boolean initializeNestedVars) {
		if(vars!=null) {
			ArrayList<Var> parentVars = new ArrayList<Var>();
			for (Var var : vars) {
				if(isRelevantVar(var, el, context, vars, parentVars, initializeNestedVars)) {
					return var;
				}
			}
		}
		return null;
	}

	public List<Var> findVarsForEl(String el, ELContext context, List<Var> vars, boolean initializeNestedVars) {
		List<Var> result = new ArrayList<Var>();
		ArrayList<Var> parentVars = new ArrayList<Var>();
		for (Var var : vars) {
			if(isRelevantVar(var, el, context, vars, parentVars, initializeNestedVars)) {
				result.add(var);
			}
		}
		return result;
	}

	private boolean isRelevantVar(Var var, String el, ELContext context, List<Var> vars, ArrayList<Var> parentVars, boolean initializeNestedVars) {
		boolean result = false;
		ELExpression token = var.getElToken();
		if(token!=null && !token.getText().endsWith(".")) { //$NON-NLS-1$
			String varName = var.getName();
			if(el.equals(varName) || el.startsWith(varName.trim()+".")) { //$NON-NLS-1$
				if(var.getElToken()!=null && initializeNestedVars) {
					ELContext c = context;
					if(!var.getFile().equals(context.getResource())) {
						//TODO
					}
					Var parentVar = findVarForEl(var.getElToken().getText(), c, parentVars, true);
					if(parentVar!=null) {
						ELExpression resolvedToken = parentVar.getResolvedElToken();
						if(resolvedToken==null && parentVar.getElToken()!=null) {
							try {
								// Initialize parent vars.
								engine.resolveELOperand(file, context, var.getElToken(), true, parentVars, this, (var.getRegion() == null ? 0 : var.getRegion().getOffset()));
								resolvedToken = parentVar.getResolvedElToken();
							} catch (StringIndexOutOfBoundsException e) {
								ELCorePlugin.getPluginLog().logError(e);
							} catch (BadLocationException e) {
								ELCorePlugin.getPluginLog().logError(e);
							}
						}
						if(resolvedToken!=null) {
							String oldText = var.getElToken().getText();
							String newValue = "#{" + resolvedToken.getText() + oldText.substring(parentVar.getName().length()) + "}"; //$NON-NLS-1$ //$NON-NLS-2$
							var.value = newValue;
							var.elToken = var.parseEl(newValue);
						}
					}
				}
				result = true;
			}
		}
		parentVars.add(var);
		return result;
	}
}