/*******************************************************************************
 * Copyright (c) 2006, 2018 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Remy Chi Jian Suen <remy.suen@gmail.com> - bug 201566
 *     Benjamin Cabe <benjamin.cabe@anyware-tech.com> - bug 227105
 *******************************************************************************/

package org.eclipse.pde.internal.ui.editor.contentassist;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IResource;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ContentAssistEvent;
import org.eclipse.jface.text.contentassist.ICompletionListener;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.pde.core.IBaseModel;
import org.eclipse.pde.core.IIdentifiable;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginExtension;
import org.eclipse.pde.core.plugin.IPluginExtensionPoint;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.IPluginObject;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.ischema.IMetaAttribute;
import org.eclipse.pde.internal.core.ischema.ISchemaAttribute;
import org.eclipse.pde.internal.core.ischema.ISchemaComplexType;
import org.eclipse.pde.internal.core.ischema.ISchemaCompositor;
import org.eclipse.pde.internal.core.ischema.ISchemaElement;
import org.eclipse.pde.internal.core.ischema.ISchemaObject;
import org.eclipse.pde.internal.core.ischema.ISchemaRestriction;
import org.eclipse.pde.internal.core.ischema.ISchemaSimpleType;
import org.eclipse.pde.internal.core.text.AbstractEditingModel;
import org.eclipse.pde.internal.core.text.IDocumentAttributeNode;
import org.eclipse.pde.internal.core.text.IDocumentElementNode;
import org.eclipse.pde.internal.core.text.IDocumentRange;
import org.eclipse.pde.internal.core.text.IDocumentTextNode;
import org.eclipse.pde.internal.core.text.IReconcilingParticipant;
import org.eclipse.pde.internal.core.text.plugin.PluginModelBase;
import org.eclipse.pde.internal.core.util.IdUtil;
import org.eclipse.pde.internal.core.util.PDESchemaHelper;
import org.eclipse.pde.internal.ui.PDEPluginImages;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.editor.PDEFormEditor;
import org.eclipse.pde.internal.ui.editor.PDESourcePage;
import org.eclipse.pde.internal.ui.editor.text.XMLUtil;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.forms.editor.FormEditor;

public class XMLContentAssistProcessor extends TypePackageCompletionProcessor implements ICompletionListener {

	protected boolean fAssistSessionStarted;

	// Specific assist types

	protected static final int F_INFER_BY_OBJECT = -1;

	protected static final int F_EXTENSION_POINT = 0;

	protected static final int F_EXTENSION = 1;

	protected static final int F_ELEMENT = 2;

	protected static final int F_ATTRIBUTE = 3;

	protected static final int F_CLOSE_TAG = 4;

	protected static final int F_ATTRIBUTE_VALUE = 5;

	protected static final int F_EXTENSION_ATTRIBUTE_POINT_VALUE = 6;

	protected static final int F_EXTENSION_POINT_AND_VALUE = 7;

	protected static final int F_ATTRIBUTE_ID_VALUE = 8;

	protected static final int F_ATTRIBUTE_BOOLEAN_VALUE = 9;

	protected static final int F_TOTAL_TYPES = 10;

	// proposal generation type
	private static final int F_NO_ASSIST = 0, F_ADD_ATTRIB = 1, F_ADD_CHILD = 2, F_OPEN_TAG = 3;

	private static final ArrayList<VirtualSchemaObject> F_V_BOOLS = new ArrayList<>();
	static {
		F_V_BOOLS.add(new VirtualSchemaObject("true", null, F_ATTRIBUTE_BOOLEAN_VALUE)); //$NON-NLS-1$
		F_V_BOOLS.add(new VirtualSchemaObject("false", null, F_ATTRIBUTE_BOOLEAN_VALUE)); //$NON-NLS-1$
	}

	private static final String F_STR_EXT_PT = "extension-point"; //$NON-NLS-1$
	private static final String F_STR_EXT = "extension"; //$NON-NLS-1$

	private final PDESourcePage fSourcePage;
	private final Image[] fImages = new Image[F_TOTAL_TYPES];
	// TODO add a listener to add/remove extension points as they are added/removed from working models
	private IDocumentRange fRange;
	private int fDocLen = -1;

	/** All external plug-in extension points */
	private ArrayList<ISchemaObject> fExternalExtPoints;

	/** All internal plug-in extension points */
	private ArrayList<VirtualSchemaObject> fInternalExtPoints;

	/** All external and internal plug-in extension points */
	private ArrayList<ISchemaObject> fAllExtPoints;

	public XMLContentAssistProcessor(PDESourcePage sourcePage) {
		fSourcePage = sourcePage;
	}

	@Override
	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
		IDocument doc = viewer.getDocument();
		int docLen = doc.getLength();
		if (docLen == fDocLen)
			return null; // left/right cursor has been pressed - cancel content assist

		fDocLen = docLen;
		IBaseModel model = getModel();
		if (model instanceof AbstractEditingModel && fSourcePage.isDirty() && ((AbstractEditingModel) model).isStale() && fRange == null) {
			((AbstractEditingModel) model).reconciled(doc);
		} else if (fAssistSessionStarted) {
			// Always reconcile when content assist is first invoked
			// Fix Bug # 149478
			((AbstractEditingModel) model).reconciled(doc);
			fAssistSessionStarted = false;
		}

		if (fRange == null) {
			assignRange(offset);
		} else {
			// TODO - we may be looking at the wrong fRange
			// when this happens --> reset it and reconcile
			// how can we tell if we are looking at the wrong one... ?
			boolean resetAndReconcile = false;
			if (!(fRange instanceof IDocumentAttributeNode))
				// too easy to reconcile.. this is temporary
				resetAndReconcile = true;

			if (resetAndReconcile) {
				fRange = null;
				if (model instanceof IReconcilingParticipant)
					((IReconcilingParticipant) model).reconciled(doc);
			}
		}
		// Get content assist text if any
		XMLContentAssistText caText = XMLContentAssistText.parse(offset, doc);

		if (caText != null) {
			return computeCATextProposal(doc, offset, caText);
		} else if (fRange instanceof IDocumentAttributeNode) {
			return computeCompletionProposal((IDocumentAttributeNode) fRange, offset, doc);
		} else if (fRange instanceof IDocumentElementNode) {
			return computeCompletionProposal((IDocumentElementNode) fRange, offset, doc);
		} else if (fRange instanceof IDocumentTextNode) {
			return null;
		} else if (model instanceof PluginModelBase) {
			// broken model - infer from text content
			return computeBrokenModelProposal(((PluginModelBase) model).getLastErrorNode(), offset, doc);
		}
		return null;
	}

	protected ICompletionProposal[] debugPrintProposals(ICompletionProposal[] proposals, String id, boolean print) {
		if (proposals == null) {
			System.out.println("[0] " + id); //$NON-NLS-1$
			return proposals;
		}
		System.out.println("[" + proposals.length + "] " + id); //$NON-NLS-1$ //$NON-NLS-2$
		if (print == false) {
			return proposals;
		}
		for (ICompletionProposal proposal : proposals) {
			System.out.println(proposal.getDisplayString());
		}
		return proposals;
	}

	private void assignRange(int offset) {
		fRange = fSourcePage.getRangeElement(offset, true);
		if (fRange == null)
			return;
		// if we are rigth AT (cursor before) the range, we want to contribute
		// to its parent
		if (fRange instanceof IDocumentAttributeNode) {
			if (((IDocumentAttributeNode) fRange).getNameOffset() == offset)
				fRange = ((IDocumentAttributeNode) fRange).getEnclosingElement();
		} else if (fRange instanceof IDocumentElementNode) {
			if (fRange.getOffset() == offset)
				fRange = ((IDocumentElementNode) fRange).getParentNode();
		} else if (fRange instanceof IDocumentTextNode) {
			if (fRange.getOffset() == offset)
				fRange = ((IDocumentTextNode) fRange).getEnclosingElement();
		}
	}

	private ICompletionProposal[] computeCATextProposal(IDocument doc, int offset, XMLContentAssistText caText) {
		fRange = fSourcePage.getRangeElement(offset, true);
		if ((fRange != null) && (fRange instanceof IDocumentTextNode)) {
			// We have a text node.
			// Get its parent element
			fRange = ((IDocumentTextNode) fRange).getEnclosingElement();
		}
		if ((fRange != null) && (fRange instanceof IDocumentElementNode)) {
			return computeAddChildProposal((IDocumentElementNode) fRange, caText.getStartOffset(), doc, caText.getText());
		}
		return null;
	}

	private ICompletionProposal[] computeCompletionProposal(IDocumentAttributeNode attr, int offset, IDocument doc) {
		if (offset < attr.getValueOffset())
			return null;
		int[] offests = new int[] {offset, offset, offset};
		String[] guess = guessContentRequest(offests, doc, false);
		if (guess == null)
			return null;
//		String element = guess[0];
//		String attribute = guess[1];
		String attrValue = guess[2];

		IPluginObject obj = XMLUtil.getTopLevelParent(attr);
		if (obj instanceof IPluginExtension) {
			if (attr.getAttributeName().equals(IPluginExtension.P_POINT) && offset >= attr.getValueOffset()) {
				return computeExtPointAttrProposals(attr, offset, attrValue);
			}
			ISchemaAttribute sAttr = XMLUtil.getSchemaAttribute(attr, ((IPluginExtension) obj).getPoint());
			if (sAttr == null)
				return null;

			if (sAttr.getKind() == IMetaAttribute.JAVA) {
				IResource resource = obj.getModel().getUnderlyingResource();
				if (resource == null)
					return null;
				// Revisit: NEW CODE HERE
				ArrayList<Object> list = new ArrayList<>();
				ICompletionProposal[] proposals = null;

				generateTypePackageProposals(attrValue, resource.getProject(), list, offset - attrValue.length(), IJavaSearchConstants.CLASS_AND_INTERFACE);

				if (!list.isEmpty()) {
					// Convert the results array list into an array of completion
					// proposals
					proposals = list.toArray(new ICompletionProposal[list.size()]);
					sortCompletions(proposals);
					return proposals;
				}
				return null;
			} else if (sAttr.getKind() == IMetaAttribute.RESOURCE) {
				// provide proposals with all resources in current plugin?

			} else if (sAttr.getKind() == IMetaAttribute.IDENTIFIER) {
				String[] validAttributes = PDESchemaHelper.getValidAttributes(sAttr).keySet().toArray(new String[0]);
				Arrays.sort(validAttributes);
				ArrayList<VirtualSchemaObject> objs = new ArrayList<>(validAttributes.length);
				for (String validAttribute : validAttributes)
					objs.add(new VirtualSchemaObject(validAttribute, null, F_ATTRIBUTE_ID_VALUE));
				return computeAttributeProposal(attr, offset, attrValue, objs);
			} else { // we have an IMetaAttribute.STRING kind
				if (sAttr.getType() == null)
					return null;
				ISchemaRestriction sRestr = (sAttr.getType()).getRestriction();
				ArrayList<VirtualSchemaObject> objs = new ArrayList<>();
				if (sRestr == null) {
					ISchemaSimpleType type = sAttr.getType();
					if (type != null && type.getName().equals("boolean")) //$NON-NLS-1$
						objs = F_V_BOOLS;
				} else {
					Object[] restrictions = sRestr.getChildren();
					for (Object restriction : restrictions)
						if (restriction instanceof ISchemaObject)
							objs.add(new VirtualSchemaObject(((ISchemaObject) restriction).getName(), null, F_ATTRIBUTE_VALUE));
				}
				return computeAttributeProposal(attr, offset, attrValue, objs);
			}
		} else if (obj instanceof IPluginExtensionPoint) {
			if (attr.getAttributeValue().equals(IPluginExtensionPoint.P_SCHEMA)) {
				// provide proposals with all schema files in current plugin?

			}
		}
		return null;
	}

	private ICompletionProposal[] computeExtPointAttrProposals(IDocumentAttributeNode attribute, int offset, String currentAttributeValue) {
		// Get all the applicable extension points
		ArrayList<ISchemaObject> allExtensionPoints = getAllExtensionPoints(F_EXTENSION_ATTRIBUTE_POINT_VALUE);
		// Ensure we found extension points
		if ((allExtensionPoints == null) || (allExtensionPoints.isEmpty())) {
			return null;
		}
		// If there is no current attribute value, then the applicable extension
		// points do not need to be filtered.  Return the list as is
		if ((currentAttributeValue == null) || (currentAttributeValue.length() == 0)) {
			return convertListToProposal(allExtensionPoints, attribute, offset);
		}
		// Create the filtered proposal list
		ArrayList<ISchemaObject> filteredProposalList = new ArrayList<>();
		// Filter the applicable extension points by the current attribute
		// value
		filterExtPointAttrProposals(filteredProposalList, allExtensionPoints, currentAttributeValue);
		// Convert into a digestable list of proposals
		return convertListToProposal(filteredProposalList, attribute, offset);
	}

	private ICompletionProposal[] computeRootNodeProposals(IDocumentElementNode node, int offset, String filter) {
		// Create the filtered proposal list
		ArrayList<ISchemaObject> filteredProposalList = new ArrayList<>();
		// Add extension to the list
		addToList(filteredProposalList, filter, new VirtualSchemaObject(F_STR_EXT, PDEUIMessages.XMLContentAssistProcessor_extensions, F_EXTENSION));
		// Add extension point to the list
		addToList(filteredProposalList, filter, new VirtualSchemaObject(F_STR_EXT_PT, PDEUIMessages.XMLContentAssistProcessor_extensionPoints, F_EXTENSION_POINT));
		// Get all avaliable extensions with point attribute values
		ArrayList<ISchemaObject> allExtensionPoints = getAllExtensionPoints(F_EXTENSION_POINT_AND_VALUE);
		// Ensure we found extension points
		if ((allExtensionPoints == null) || (allExtensionPoints.isEmpty())) {
			// Return the current proposal list without extension points
			return convertListToProposal(filteredProposalList, node, offset);
		}
		// If there is no current value, then the applicable extension
		// points do not need to be filtered.  Merge the list as is with the
		// existing proposals
		if ((filter == null) || (filter.length() == 0)) {
			filteredProposalList.addAll(allExtensionPoints);
			return convertListToProposal(filteredProposalList, node, offset);
		}
		// Filter the applicable extension points by the current value
		filterExtPointAttrProposals(filteredProposalList, allExtensionPoints, filter);
		// Convert into a digestable list of proposals
		return convertListToProposal(filteredProposalList, node, offset);
	}

	/**
	 * @param filteredProposalList - Must not be NULL
	 * @param allExtensionPoints - Must not be NULL
	 * @param filter - Must not be NULL
	 */
	private void filterExtPointAttrProposals(ArrayList<ISchemaObject> filteredProposalList, ArrayList<ISchemaObject> allExtensionPoints, String filter) {
		// Create a regex pattern out of the current attribute value
		// Ignore case
		String patternString = "(?i)" + filter; //$NON-NLS-1$
		// Compile the pattern
		Pattern pattern = Pattern.compile(patternString);
		// Iterate over the applicable extension points and add extension points
		// matching the pattern to the filtered proposal list
		Iterator<ISchemaObject> iterator = allExtensionPoints.iterator();
		while (iterator.hasNext()) {
			// Get the schema object
			ISchemaObject schemaObject = iterator.next();
			// Ensure the schema object is defined
			if (schemaObject == null) {
				continue;
			}
			// Get the name of the schema object
			String name = schemaObject.getName();
			// If the current attribute value matches some part of the name
			// add it to the list
			if (pattern.matcher(name).find()) {
				filteredProposalList.add(schemaObject);
			}
		}
	}

	private ICompletionProposal[] computeAttributeProposal(IDocumentAttributeNode attr, int offset, String currValue, List<VirtualSchemaObject> validValues) {
		if (validValues == null)
			return null;
		ArrayList<ISchemaObject> list = new ArrayList<>();
		for (int i = 0; i < validValues.size(); i++)
			addToList(list, currValue, validValues.get(i));

		return convertListToProposal(list, attr, offset);
	}

	private ICompletionProposal[] computeCompletionProposal(IDocumentElementNode node, int offset, IDocument doc) {
		int prop_type = determineAssistType(node, doc, offset);
		return switch (prop_type) {
			case F_ADD_ATTRIB -> computeAddAttributeProposal(F_INFER_BY_OBJECT, node, offset, doc, null, node.getXMLTagName());
			case F_OPEN_TAG -> computeOpenTagProposal(node, offset, doc);
			case F_ADD_CHILD -> computeAddChildProposal(node, offset, doc, null);
			default -> null;
		};
	}

	private int determineAssistType(IDocumentElementNode node, IDocument doc, int offset) {
		int len = node.getLength();
		int off = node.getOffset();
		if (len == -1 || off == -1)
			return F_NO_ASSIST;

		offset = offset - off; // look locally
		if (offset > node.getXMLTagName().length() + 1) {
			try {
				String eleValue = doc.get(off, len);
				int ind = eleValue.indexOf('>');
				if (ind > 0 && eleValue.charAt(ind - 1) == '/')
					ind -= 1;
				if (offset <= ind) {
					if (canInsertAttrib(eleValue, offset))
						return F_ADD_ATTRIB;
					return F_NO_ASSIST;
				}
				ind = eleValue.lastIndexOf('<');
				if (ind == 0 && offset == len - 1)
					return F_OPEN_TAG; // childless node - check if it can be cracked open
				if (ind + 1 < len && eleValue.charAt(ind + 1) == '/' && offset <= ind)
					return F_ADD_CHILD;
			} catch (BadLocationException e) {
			}
		}
		return F_NO_ASSIST;
	}

	private boolean canInsertAttrib(String eleValue, int offset) {
		// character before offset must be whitespace
		// character on offset must be whitespace, '/' or '>'
		char c = eleValue.charAt(offset);
		return offset - 1 >= 0 && Character.isWhitespace(eleValue.charAt(offset - 1)) && (Character.isWhitespace(c) || c == '/' || c == '>');
	}

	private ICompletionProposal[] computeAddChildProposal(IDocumentElementNode node, int offset, IDocument doc, String filter) {
		ArrayList<ISchemaObject> propList = new ArrayList<>();
		if (node instanceof IPluginBase) {
			return computeRootNodeProposals(node, offset, filter);
		} else if (node instanceof IPluginExtensionPoint) {
			return null;
		} else {
			IPluginObject obj = XMLUtil.getTopLevelParent(node);
			if (obj instanceof IPluginExtension) {
				ISchemaElement sElement = XMLUtil.getSchemaElement(node, ((IPluginExtension) obj).getPoint());
				if ((sElement != null) && (sElement.getType() instanceof ISchemaComplexType)) {
					// We have a schema complex type.  Either the element has attributes
					// or the element has children.
					// Generate the list of element proposals
					Set<ISchemaElement> elementSet = XMLElementProposalComputer.computeElementProposal(sElement, node);
					// Filter the list of element proposals
					Iterator<ISchemaElement> iterator = elementSet.iterator();
					while (iterator.hasNext()) {
						addToList(propList, filter, iterator.next());
					}
				} else {
					return null;
				}
			}
		}
		return convertListToProposal(propList, node, offset);
	}

	private ICompletionProposal[] computeOpenTagProposal(IDocumentElementNode node, int offset, IDocument doc) {
		IPluginObject obj = XMLUtil.getTopLevelParent(node);
		if (obj instanceof IPluginExtension) {
			ISchemaElement sElem = XMLUtil.getSchemaElement(node, ((IPluginExtension) obj).getPoint());
			if (sElem == null)
				return null;
			ISchemaCompositor comp = ((ISchemaComplexType) sElem.getType()).getCompositor();
			if (comp != null)
				return new ICompletionProposal[] {new XMLCompletionProposal(node, null, offset, this)};
		}
		return null;
	}

	private ICompletionProposal[] computeAddAttributeProposal(int type, IDocumentElementNode node, int offset, IDocument doc, String filter, String tag) {
		String nodeName = tag;
		if (nodeName == null && node != null)
			nodeName = node.getXMLTagName();
		if (type == F_EXTENSION || node instanceof IPluginExtension) {
			ISchemaElement sElem = XMLUtil.getSchemaElement(node, node != null ? ((IPluginExtension) node).getPoint() : null);
			ISchemaObject[] sAttrs = sElem != null ? sElem.getAttributes() : new ISchemaObject[] {new VirtualSchemaObject(IIdentifiable.P_ID, PDEUIMessages.XMLContentAssistProcessor_extId, F_ATTRIBUTE), new VirtualSchemaObject(IPluginObject.P_NAME, PDEUIMessages.XMLContentAssistProcessor_extName, F_ATTRIBUTE), new VirtualSchemaObject(IPluginExtension.P_POINT, PDEUIMessages.XMLContentAssistProcessor_extPoint, F_ATTRIBUTE)};
			return computeAttributeProposals(sAttrs, node, offset, filter, nodeName);
		} else if (type == F_EXTENSION_POINT || node instanceof IPluginExtensionPoint) {
			ISchemaObject[] sAttrs = new ISchemaObject[] {new VirtualSchemaObject(IIdentifiable.P_ID, PDEUIMessages.XMLContentAssistProcessor_extPointId, F_ATTRIBUTE), new VirtualSchemaObject(IPluginObject.P_NAME, PDEUIMessages.XMLContentAssistProcessor_extPointName, F_ATTRIBUTE), new VirtualSchemaObject(IPluginExtensionPoint.P_SCHEMA, PDEUIMessages.XMLContentAssistProcessor_schemaLocation, F_ATTRIBUTE)};
			return computeAttributeProposals(sAttrs, node, offset, filter, nodeName);
		} else {
			IPluginObject obj = XMLUtil.getTopLevelParent(node);
			if (obj instanceof IPluginExtension) {
				ISchemaElement sElem = XMLUtil.getSchemaElement(node, node != null ? ((IPluginExtension) obj).getPoint() : null);
				ISchemaObject[] sAttrs = sElem != null ? sElem.getAttributes() : null;
				return computeAttributeProposals(sAttrs, node, offset, filter, nodeName);
			}
		}
		return null;
	}

	private void addToList(ArrayList<ISchemaObject> list, String filter, ISchemaObject object) {
		if (object == null)
			return;
		if (filter == null || filter.length() == 0)
			list.add(object);
		else {
			String name = object.getName();
			if (filter.regionMatches(true, 0, name, 0, filter.length()))
				list.add(object);
		}
	}

	private ICompletionProposal[] computeBrokenModelProposal(IDocumentElementNode parent, int offset, IDocument doc) {
		if (parent == null)
			return null;

		int[] offArr = new int[] {offset, offset, offset};
		String[] guess = guessContentRequest(offArr, doc, true);
		if (guess == null)
			return null;

		int elRepOffset = offArr[0];
		int atRepOffset = offArr[1];
		int atValRepOffest = offArr[2];
		String element = guess[0];
		String attr = guess[1];
		String attVal = guess[2];

		IPluginObject obj = XMLUtil.getTopLevelParent(parent);
		if (obj instanceof IPluginExtension) {
			String point = ((IPluginExtension) obj).getPoint();
			if (attr == null)
				// search for element proposals
				return computeAddChildProposal(parent, elRepOffset, doc, element);

			ISchemaElement sEle = XMLUtil.getSchemaElement(parent, point);
			if (sEle == null)
				return null;
			sEle = sEle.getSchema().findElement(element);
			if (sEle == null)
				return null;

			if (attr.indexOf('=') != -1)
				// search for attribute content proposals
				return computeBrokenModelAttributeContentProposal(parent, atValRepOffest, element, attr, attVal);

			// search for attribute proposals
			return computeAttributeProposals(sEle.getAttributes(), null, atRepOffset, attr, element);
		} else if (parent instanceof IPluginBase) {
			if (attr == null)
				return computeAddChildProposal(parent, elRepOffset, doc, element);
			if (element.equalsIgnoreCase(F_STR_EXT))
				return computeAddAttributeProposal(F_EXTENSION, null, atRepOffset, doc, attr, F_STR_EXT);
			if (element.equalsIgnoreCase(F_STR_EXT_PT))
				return computeAddAttributeProposal(F_EXTENSION_POINT, null, atRepOffset, doc, attr, F_STR_EXT_PT);
		}
		return null;
	}

	private ICompletionProposal[] computeBrokenModelAttributeContentProposal(IDocumentElementNode parent, int offset, String element, String attr, String filter) {
		// TODO use computeCompletionProposal(IDocumentAttributeNode attr, int offset) if possible
		// or refactor above to be used here
		// CURRENTLY: attribute completion only works in non-broken models
		return null;
	}

	private String[] guessContentRequest(int[] offset, IDocument doc, boolean brokenModel) {
		StringBuilder nodeBuffer = new StringBuilder();
		StringBuilder attrBuffer = new StringBuilder();
		StringBuilder attrValBuffer = new StringBuilder();
		String node = null;
		String attr = null;
		String attVal = null;
		int quoteCount = 0;
		try {
			while (--offset[0] >= 0) {
				char c = doc.getChar(offset[0]);
				if (c == '"') {
					quoteCount += 1;
					nodeBuffer.setLength(0);
					attrBuffer.setLength(0);
					if (attVal != null) // ran into 2nd quotation mark, we are out of range
						continue;
					offset[2] = offset[0];
					attVal = attrValBuffer.toString();
				} else if (Character.isWhitespace(c)) {
					nodeBuffer.setLength(0);
					if (attr == null) {
						offset[1] = offset[0];
						int attBuffLen = attrBuffer.length();
						if (attBuffLen > 0 && attrBuffer.charAt(attBuffLen - 1) == '=')
							attrBuffer.setLength(attBuffLen - 1);
						attr = attrBuffer.toString();
					}
				} else if (c == '<') {
					node = nodeBuffer.toString();
					break;
				} else if (c == '>') {
					// only enable content assist if user is inside an open tag
					return null;
				} else {
					attrValBuffer.insert(0, c);
					attrBuffer.insert(0, c);
					nodeBuffer.insert(0, c);
				}
			}
		} catch (BadLocationException e) {
		}
		if (node == null)
			return null;

		if (quoteCount % 2 == 0)
			attVal = null;
		else if (brokenModel)
			return null; // open quotes - don't provide assist

		return new String[] {node, attr, attVal};
	}

	protected IBaseModel getModel() {
		return fSourcePage.getInputContext().getModel();
	}

	protected ITextSelection getCurrentSelection() {
		ISelection sel = fSourcePage.getSelectionProvider().getSelection();
		if (sel instanceof ITextSelection)
			return (ITextSelection) sel;
		return null;
	}

	protected void flushDocument() {
		fSourcePage.getInputContext().flushEditorInput();
	}

	private ICompletionProposal[] computeAttributeProposals(ISchemaObject[] sAttrs, IDocumentElementNode node, int offset, String filter, String parentName) {
		if (sAttrs == null || sAttrs.length == 0)
			return null;
		IDocumentAttributeNode[] attrs = node != null ? node.getNodeAttributes() : new IDocumentAttributeNode[0];

		ArrayList<ISchemaObject> list = new ArrayList<>();
		for (ISchemaObject attr : sAttrs) {
			int k; // if we break early we wont add
			for (k = 0; k < attrs.length; k++)
				if (attrs[k].getAttributeName().equals(attr.getName()))
					break;
			if (k == attrs.length)
				addToList(list, filter, attr);
		}
		if (filter != null && filter.length() == 0)
			list.add(0, new VirtualSchemaObject(parentName, null, F_CLOSE_TAG));
		return convertListToProposal(list, node, offset);
	}

	private ICompletionProposal[] convertListToProposal(ArrayList<ISchemaObject> list, IDocumentRange range, int offset) {
		ICompletionProposal[] proposals = new ICompletionProposal[list.size()];
		if (proposals.length == 0)
			return null;
		for (int i = 0; i < proposals.length; i++)
			proposals[i] = new XMLCompletionProposal(range, list.get(i), offset, this);
		return proposals;
	}

	@Override
	public void assistSessionEnded(ContentAssistEvent event) {
		fRange = null;

		// Reset cached internal and external extension point proposals
		fAllExtPoints = null;
		// Reset cached internal point proposals
		// Note: Not resetting cached external point proposals
		// Assumption is that the users workspace can change; but, not the
		// platform including all the external plugins
		fInternalExtPoints = null;

		fDocLen = -1;
	}

	@Override
	public void assistSessionStarted(ContentAssistEvent event) {
		fAssistSessionStarted = true;
	}

	@Override
	public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) {
	}

	@SuppressWarnings("unchecked")
	private ArrayList<ISchemaObject> getAllExtensionPoints(int vSchemaType) {
		// Return the previous extension points if defined
		if (fAllExtPoints != null) {
			return fAllExtPoints;
		}
		// Get the plugin model base
		IPluginModelBase model = getPluginModelBase();
		// Note: All plug-in extension points are cached except the
		// extension points defined by the plugin.xml we are currently
		// editing. This means if a plug-in in the workspace defines a new
		// extension point and the plugin.xml editor is still open, the
		// new extension point will not show up as a proposal because it is
		// using a cached list of extension points.
		// External extensions points are all extension points not defined by
		// the plugin currently being edited - opposed to non-workpspace
		// plugins.  Internal extension points are the extension points defined
		// by the plugin currently being edited.  May want to modify this
		// behaviour in the future.
		// Add all external extension point proposals to the list
		fAllExtPoints = (ArrayList<ISchemaObject>) getExternalExtensionPoints(model, vSchemaType).clone();
		// Add all internal extension point proposals to the list
		fAllExtPoints.addAll(getInternalExtensionPoints(model, vSchemaType));

		return fAllExtPoints;
	}

	private ArrayList<ISchemaObject> getExternalExtensionPoints(IPluginModelBase model, int vSchemaType) {
		// Return the previous external extension points if defined
		if (fExternalExtPoints != null) {
			updateExternalExtPointTypes(vSchemaType);
			return fExternalExtPoints;
		}
		// Query for all external extension points
		fExternalExtPoints = new ArrayList<>();
		// Get all plug-ins in the workspace
		IPluginModelBase[] plugins = PluginRegistry.getActiveModels();
		// Process each plugin
		for (IPluginModelBase plugin : plugins) {
			// Make sure this plugin is not the one we are currently
			// editing which defines internal extension points.
			// We don't want to cache internal extension points because the
			// workspace can change.
			if (plugin.getPluginBase().getId().equals(model.getPluginBase().getId())) {
				// Skip this plugin
				continue;
			}
			// Get all extension points defined by this plugin
			IPluginExtensionPoint[] points = plugin.getPluginBase().getExtensionPoints();
			// Process each extension point
			for (IPluginExtensionPoint point : points) {
				VirtualSchemaObject vObject = new VirtualSchemaObject(IdUtil.getFullId(point, model), point, vSchemaType);
				// Add the proposal to the list
				fExternalExtPoints.add(vObject);
			}
		}
		return fExternalExtPoints;
	}

	/**
	 * Handles edge case.
	 * External extension points are cached.
	 * As a result, these types persist over each other depending on what
	 * context content assist is invoked first:
	 * F_EXTENSION_ATTRIBUTE_POINT_VALUE
	 * F_EXTENSION_POINT_AND_VALUE
	 */
	private void updateExternalExtPointTypes(int newVType) {
		// Ensure we have proposals
		if (fExternalExtPoints.isEmpty()) {
			return;
		}
		// Get the first proposal
		VirtualSchemaObject vObject = (VirtualSchemaObject) fExternalExtPoints.get(0);
		// If the first proposals type is the same as the new type, then we
		// do not have to do the update.  That is because all the proposals
		// have the same type.
		if (vObject.getVType() == newVType) {
			return;
		}
		// Update all the proposals with the new type
		Iterator<ISchemaObject> iterator = fExternalExtPoints.iterator();
		while (iterator.hasNext()) {
			((VirtualSchemaObject) iterator.next()).setVType(newVType);
		}
	}

	private ArrayList<VirtualSchemaObject> getInternalExtensionPoints(IPluginModelBase model, int vSchemaType) {
		// Return the previous internal extension points if defined
		if (fInternalExtPoints != null) {
			// Realistically, this line should never be hit
			return fInternalExtPoints;
		}
		fInternalExtPoints = new ArrayList<>();
		// Get all extension points defined by this plugin
		IPluginExtensionPoint[] points = model.getPluginBase().getExtensionPoints();
		// Process each extension point
		for (IPluginExtensionPoint point : points) {
			VirtualSchemaObject vObject = new VirtualSchemaObject(IdUtil.getFullId(point, model), point, vSchemaType);
			// Add the proposal to the list
			fInternalExtPoints.add(vObject);
		}
		return fInternalExtPoints;
	}

	/**
	 * Returns a BundlePluginModel which has a getId() method that works.
	 * getModel() method returns a PluginModel whose getId() method does not
	 * work.
	 */
	private IPluginModelBase getPluginModelBase() {
		FormEditor formEditor = fSourcePage.getEditor();
		if ((formEditor instanceof PDEFormEditor) == false) {
			return null;
		}
		IBaseModel bModel = ((PDEFormEditor) formEditor).getAggregateModel();
		if ((bModel instanceof IPluginModelBase) == false) {
			return null;
		}
		return (IPluginModelBase) bModel;
	}

	public Image getImage(int type) {
		if (fImages[type] == null) {
			fImages[type] = switch (type) {
				case F_EXTENSION_POINT, F_EXTENSION_ATTRIBUTE_POINT_VALUE -> PDEPluginImages.DESC_EXT_POINT_OBJ.createImage();
				case F_EXTENSION_POINT_AND_VALUE, F_EXTENSION -> PDEPluginImages.DESC_EXTENSION_OBJ.createImage();
				case F_ELEMENT, F_CLOSE_TAG -> PDEPluginImages.DESC_XML_ELEMENT_OBJ.createImage();
				case F_ATTRIBUTE, F_ATTRIBUTE_VALUE -> PDEPluginImages.DESC_ATT_URI_OBJ.createImage();
				case F_ATTRIBUTE_ID_VALUE -> PDEPluginImages.DESC_ATT_ID_OBJ.createImage();
				case F_ATTRIBUTE_BOOLEAN_VALUE -> PDEPluginImages.DESC_ATT_BOOLEAN_OBJ.createImage();
				default -> fImages[type];
			};
		}
		return fImages[type];
	}

	public void dispose() {
		for (int i = 0; i < fImages.length; i++)
			if (fImages[i] != null && !fImages[i].isDisposed())
				fImages[i].dispose();
	}

	public PDESourcePage getSourcePage() {
		return fSourcePage;
	}

}
