/*
 * ====================================================================
 * 
 * The ObjectStyle Group Software License, Version 1.0
 * 
 * Copyright (c) 2005 The ObjectStyle Group and individual authors of the
 * software. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 1.
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer. 2. Redistributions in
 * binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other
 * materials provided with the distribution. 3. The end-user documentation
 * included with the redistribution, if any, must include the following
 * acknowlegement: "This product includes software developed by the ObjectStyle
 * Group (http://objectstyle.org/)." Alternately, this acknowlegement may
 * appear in the software itself, if and wherever such third-party
 * acknowlegements normally appear. 4. The names "ObjectStyle Group" and
 * "Cayenne" must not be used to endorse or promote products derived from this
 * software without prior written permission. For written permission, please
 * contact andrus@objectstyle.org. 5. Products derived from this software may
 * not be called "ObjectStyle" nor may "ObjectStyle" appear in their names
 * without prior written permission of the ObjectStyle Group.
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * OBJECTSTYLE GROUP OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 * 
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the ObjectStyle Group. For more information on the ObjectStyle
 * Group, please see <http://objectstyle.org/> .
 *  
 */
package org.objectstyle.wolips.wodclipse.editor;

import java.util.Set;
import java.util.TreeSet;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.WhitespaceRule;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.part.FileEditorInput;
import org.objectstyle.wolips.bindings.api.ApiUtils;
import org.objectstyle.wolips.bindings.utils.BindingReflectionUtils;
import org.objectstyle.wolips.bindings.wod.TypeCache;
import org.objectstyle.wolips.wodclipse.WodclipsePlugin;
import org.objectstyle.wolips.wodclipse.core.completion.WodCompletionProposal;
import org.objectstyle.wolips.wodclipse.core.completion.WodCompletionUtils;
import org.objectstyle.wolips.wodclipse.core.completion.WodParserCache;
import org.objectstyle.wolips.wodclipse.core.parser.AssignmentOperatorWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.BindingNameRule;
import org.objectstyle.wolips.wodclipse.core.parser.BindingValueRule;
import org.objectstyle.wolips.wodclipse.core.parser.CloseDefinitionWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.ElementNameRule;
import org.objectstyle.wolips.wodclipse.core.parser.ElementTypeOperatorWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.ElementTypeRule;
import org.objectstyle.wolips.wodclipse.core.parser.EndAssignmentWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.OpenDefinitionWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.OperatorRule;
import org.objectstyle.wolips.wodclipse.core.parser.RulePosition;
import org.objectstyle.wolips.wodclipse.core.parser.WodScanner;
import org.objectstyle.wolips.wodclipse.core.preferences.PreferenceConstants;

/**
 * @author mike
 */
public class WodCompletionProcessor implements IContentAssistProcessor {
	private WodEditor _editor;

	public WodCompletionProcessor(WodEditor editor) {
		_editor = editor;
	}

	public char[] getContextInformationAutoActivationCharacters() {
		return null;
	}

	public IContextInformationValidator getContextInformationValidator() {
		return null;
	}

	public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
		return null;
	}

	public char[] getCompletionProposalAutoActivationCharacters() {
		return new char[] { ':', '.', '=' };
	}

	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int _offset) {
		Set<WodCompletionProposal> completionProposalsSet = new TreeSet<WodCompletionProposal>();
		try {
			int offset = _offset;
			TypeCache typeCache = WodParserCache.getTypeCache();
			IDocument document = viewer.getDocument();
			IEditorInput input = _editor.getEditorInput();
			if (input instanceof IPathEditorInput) {
				IPathEditorInput pathInput = (IPathEditorInput) input;
				IPath path = pathInput.getPath();
				IFile wodFile = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(path);
				IProject project = wodFile.getProject();
				IJavaProject javaProject = JavaCore.create(project);

				// Without an underlying model, we have to rescan the line to
				// figure out exactly
				// what the current matching rule was, so we know what kind of
				// token we're dealing with.
				IRegion lineRegion = document.getLineInformationOfOffset(offset);
				WodScanner scanner = WodScanner.newWODScanner();
				scanner.setRange(document, lineRegion.getOffset(), lineRegion.getLength());
				boolean foundToken = false;
				RulePosition rulePosition = null;
				while (!foundToken && (rulePosition = scanner.nextRulePosition()) != null) {
					int tokenOffset = rulePosition.getTokenOffset();
					if (offset == lineRegion.getOffset() && offset == tokenOffset) {
						foundToken = true;
					} else if (offset > tokenOffset && offset <= rulePosition.getTokenEndOffset()) {
						foundToken = true;
					}
				}

				// We can't reliably use rulePosition here because it might be
				// null ...
				int tokenOffset = scanner.getTokenOffset();
				int tokenLength = scanner.getTokenLength();
				IRule rule = (rulePosition == null) ? null : rulePosition.getRule();
				// If you make a completion request in the middle of whitespace,
				// we don't want to select the whitespace, so zero out the
				// whitespace token offsets.
				if (rule instanceof WhitespaceRule) {
					int partialOffset = (offset - tokenOffset);
					offset += partialOffset;
					tokenOffset += partialOffset;
					tokenLength = 0;
				} else {
					viewer.setSelectedRange(offset, tokenLength - (offset - tokenOffset));
				}
				String token = document.get(tokenOffset, tokenLength);
				String tokenType = null;
				if (foundToken && rulePosition != null) {
					if (rulePosition.isRuleOfType(ElementNameRule.class)) {
						tokenType = PreferenceConstants.ELEMENT_NAME;
					} else if (rulePosition.isRuleOfType(ElementTypeRule.class)) {
						tokenType = PreferenceConstants.ELEMENT_TYPE;
					} else if (rulePosition.isRuleOfType(BindingNameRule.class)) {
						tokenType = PreferenceConstants.BINDING_NAME;
					} else if (rulePosition.isRuleOfType(BindingValueRule.class)) {
						tokenType = PreferenceConstants.BINDING_VALUE;
					} else if (rulePosition.isRuleOfType(OperatorRule.class)) {
						tokenOffset += tokenLength;
						tokenLength = 0;
						if (RulePosition.isOperatorOfType(rulePosition, CloseDefinitionWordDetector.class)) {
							tokenType = PreferenceConstants.ELEMENT_NAME;
						} else if (RulePosition.isOperatorOfType(rulePosition, ElementTypeOperatorWordDetector.class)) {
							tokenType = PreferenceConstants.ELEMENT_TYPE;
						} else if (RulePosition.isOperatorOfType(rulePosition, OpenDefinitionWordDetector.class) || RulePosition.isOperatorOfType(rulePosition, EndAssignmentWordDetector.class)) {
							tokenType = PreferenceConstants.BINDING_NAME;
						} else if (RulePosition.isOperatorOfType(rulePosition, AssignmentOperatorWordDetector.class)) {
							tokenType = PreferenceConstants.BINDING_VALUE;
						}
					}
				}

				boolean guessed = false;
				// If there was no matching token type, then that means we're
				// in an invalid parse state and we have to "guess". So we
				// backscan
				// until we find an operator that we know about, which will tell
				// us
				// where we are in the document.
				if (tokenType == null) {
					int startOffset = tokenOffset;
					if (startOffset != 0 && startOffset == document.getLength()) {
						startOffset--;
					}
					// int hintChar = -1;
					// for (int startOffset = tokenOffset - 1; tokenType == null
					// && startOffset > 0; startOffset--) {
					boolean tentativeElementType = false;
					for (; tokenType == null && startOffset > 0; startOffset--) {
						int ch = document.getChar(startOffset);
						if (ch == ':') {
							tentativeElementType = true;
							//tokenType = PreferenceConstants.ELEMENT_TYPE;
							//guessed = true;
						} else if (ch == '{' || ch == ';') {
							tokenType = PreferenceConstants.BINDING_NAME;
							guessed = true;
						} else if (ch == '=') {
							tokenType = PreferenceConstants.BINDING_VALUE;
							guessed = true;
						} else if (ch == '}') {
							// just being explicit
							tokenType = PreferenceConstants.ELEMENT_NAME;
							guessed = true;
						}
					}
					if (tentativeElementType && tokenType == PreferenceConstants.BINDING_VALUE) {
						tokenType = PreferenceConstants.BINDING_VALUE_NAMESPACE;
						guessed = true;
					}
					else if (tentativeElementType) {
						tokenType = PreferenceConstants.ELEMENT_TYPE;
						guessed = true;
					}
				}

				if (tokenType == null) {
					tokenType = PreferenceConstants.ELEMENT_NAME;
					guessed = true;
				}

				// ... Fill in completion proposals based on the token type
				if (tokenType == PreferenceConstants.ELEMENT_NAME) {
					// We really need something like the AST ... This is a
					// pretty expensive
					// way to go here. To find element names that have already
					// been mapped, we
					// reparse the wod file. Lame.
					Set<String> alreadyUsedElementNames = WodScanner.getTextForRulesOfType(document, ElementNameRule.class);
					WodParserCache wodParserCache = WodParserCache.parser(((FileEditorInput) _editor.getEditorInput()).getFile());
					WodCompletionUtils.fillInElementNameCompletionProposals(alreadyUsedElementNames, token, tokenOffset, offset, completionProposalsSet, guessed, wodParserCache.getHtmlEntry().getHtmlElementCache());
				} else if (tokenType == PreferenceConstants.ELEMENT_TYPE) {
					WodCompletionUtils.fillInElementTypeCompletionProposals(javaProject, token, tokenOffset, offset, completionProposalsSet, guessed, null);
				} else if (tokenType == PreferenceConstants.BINDING_NAME) {
					IType elementType = findNearestElementType(javaProject, document, scanner, tokenOffset, typeCache);
					WodCompletionUtils.fillInBindingNameCompletionProposals(javaProject, elementType, token, tokenOffset, offset, completionProposalsSet, guessed, typeCache);
				} else if (tokenType == PreferenceConstants.BINDING_VALUE) {
					// MS: We've probably already found the type for our current component. Just get the parser cache entry
					// and pull the type off of it.
					IType elementType = _editor.getComponentsLocateResults().getDotJavaType();
					boolean checkBindingValue = false;
					if (elementType != null) {
						checkBindingValue = WodCompletionUtils.fillInBindingValueCompletionProposals(javaProject, elementType, token, tokenOffset, offset, completionProposalsSet, typeCache);
					}
					if (checkBindingValue) {
						try {
							// We might (probably do) have a syntactically
							// invalid wod file at this point, so we need to
							// hunt for the name of the binding that this value
							// corresponds to ...
							int equalsIndex = WodCompletionProcessor.scanBackFor(document, offset, new char[] { '=' }, false);
							int noSpaceIndex = WodCompletionProcessor.scanBackFor(document, equalsIndex - 1, new char[] { ' ', '\t', '\n', '\r' }, true);
							int spaceIndex = WodCompletionProcessor.scanBackFor(document, noSpaceIndex, new char[] { ' ', '\t', '\n', '\r' }, false);
							String bindingName = document.get(spaceIndex + 1, noSpaceIndex - spaceIndex);
							elementType = findNearestElementType(javaProject, document, scanner, offset, typeCache);
							String[] validValues = ApiUtils.getValidValues(token, javaProject, _editor.getComponentsLocateResults().getDotJavaType(), elementType, bindingName, typeCache);
							if (validValues != null) {
								String partialToken = WodCompletionUtils.partialToken(token, tokenOffset, offset);
								String lowercasePartialToken = partialToken.toLowerCase();
								for (int i = 0; i < validValues.length; i++) {
									if (validValues[i].toLowerCase().startsWith(lowercasePartialToken)) {
										completionProposalsSet.add(new WodCompletionProposal(token, tokenOffset, offset, validValues[i]));
									}
								}
							}
						} catch (Throwable t) {
							t.printStackTrace();
							WodclipsePlugin.getDefault().log(t);
						}
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			WodclipsePlugin.getDefault().log(e);
		}

		return completionProposalsSet.toArray(new WodCompletionProposal[completionProposalsSet.size()]);
	}

	public String getErrorMessage() {
		return null;
	}

	protected IType findNearestElementType(IJavaProject _project, IDocument _document, WodScanner _scanner, int _offset, TypeCache cache) throws BadLocationException, JavaModelException {
		// Go hunting for the element type in a potentially malformed document
		// ...
		IType type = null;
		int offset = _offset;
		while (offset != -1 && type == null) {
			offset = WodCompletionProcessor.scanBackFor(_document, offset, new char[] { '{' }, false, true);
			int colonOffset = WodCompletionProcessor.scanBackFor(_document, offset, new char[] { ':' }, false, true);
			if (colonOffset != -1) {
				_scanner.setRange(_document, colonOffset, _offset - colonOffset);
				RulePosition elementRulePosition = _scanner.getFirstRulePositionOfType(ElementTypeRule.class);
				if (elementRulePosition != null) {
					String elementTypeName = elementRulePosition.getText();
					type = BindingReflectionUtils.findElementType(_project, elementTypeName, false, cache);
					offset = -1;
				} else {
					// we didn't find a ElementTypeRule
					offset--;
				}
			} else {
				// failed colonoscopy
				type = null;
			}
		}
		return type;
	}

	protected static int scanBackFor(IDocument _document, int _offset, char[] _lookForChars, boolean _negate) throws BadLocationException {
		return scanBackFor(_document, _offset, _lookForChars, _negate, false);
	}
	
	protected static int scanBackFor(IDocument _document, int _offset, char[] _lookForChars, boolean _negate, boolean excludeQuoted) throws BadLocationException {
		int offset = _offset;
		if (offset >= _document.getLength()) {
			offset--;
		}
		int foundIndex = -1;
		boolean inQuote = false;
		char quoteChar = 0;
		for (int i = offset; foundIndex == -1 && i >= 0; i--) {
			char ch = _document.getChar(i);
			if (ch == '\'' || ch == '"') {
				boolean escaped = i > 0 && _document.getChar(i-1) == '\\';
				if (inQuote && quoteChar == ch && !escaped) {
					inQuote = false;
					quoteChar = 0;
				}
				else if (!inQuote){
					inQuote = true;
					quoteChar = ch;
				}
			}
			for (int lookForCharNum = 0; foundIndex == -1 && lookForCharNum < _lookForChars.length; lookForCharNum++) {
				if (ch == _lookForChars[lookForCharNum] && !(inQuote && excludeQuoted)) {
					foundIndex = i;
				}
			}
			if (_negate) {
				if (foundIndex != -1) {
					foundIndex = -1;
				} else {
					foundIndex = i;
				}
			}
		}
		return foundIndex;
	}
}
