/*
 * 12/21/2008
 *
 * DefaultCompletionProvider.java - A basic completion provider implementation.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package com.fr.design.gui.autocomplete;

import java.awt.Point;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Segment;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;


/**
 * A basic completion provider implementation.  This provider has no
 * understanding of language semantics.  It simply checks the text entered up
 * to the caret position for a match against known completions.  This is all
 * that is needed in the majority of cases.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class DefaultCompletionProvider extends AbstractCompletionProvider {

	protected Segment seg;

	/**
	 * Used to speed up {@link #getCompletionsAt(JTextComponent, Point)}.
	 */
	private String lastCompletionsAtText;

	/**
	 * Used to speed up {@link #getCompletionsAt(JTextComponent, Point)},
	 * since this may be called multiple times in succession (this is usually
	 * called by <tt>JTextComponent.getToolTipText()</tt>, and if the user
	 * wiggles the mouse while a tool tip is displayed, this method gets
	 * repeatedly called.  It can be costly so we try to speed it up a tad).
	 */
	private List<Completion> lastParameterizedCompletionsAt;

	/**
	 * Constructor.  The returned provider will not be aware of any completions.
	 *
	 * @see #addCompletion(Completion)
	 */
	public DefaultCompletionProvider() {
		init();
	}


	/**
	 * Creates a completion provider that provides completion for a simple
	 * list of words.
	 *
	 * @param words The words to offer as completion suggestions.  If this is
	 *        <code>null</code>, no completions will be known.
	 * @see BasicCompletion
	 */
	public DefaultCompletionProvider(String[] words) {
		init();
		addWordCompletions(words);
	}


	/**
	 * Returns the text just before the current caret position that could be
	 * the start of something auto-completable.<p>
	 *
	 * This method returns all characters before the caret that are matched
	 * by  {@link #isValidChar(char)}.
	 *
	 * {@inheritDoc}
	 */
	public String getAlreadyEnteredText(JTextComponent comp) {
		
		Document doc = comp.getDocument();

		int dot = comp.getCaretPosition();
		Element root = doc.getDefaultRootElement();
		int index = root.getElementIndex(dot);
		Element elem = root.getElement(index);
		int start = elem.getStartOffset();
		int len = dot-start;
		try {
			doc.getText(start, len, seg);
		} catch (BadLocationException ble) {

			return EMPTY_STRING;
		}

		int segEnd = seg.offset + len;
		start = segEnd - 1;
		while (start>=seg.offset && isValidChar(seg.array[start])) {
			start--;
		}
		start++;

		len = segEnd - start;
		return len==0 ? EMPTY_STRING : new String(seg.array, start, len);

	}


	/**
	 * {@inheritDoc}
	 */
	public List<Completion> getCompletionsAt(JTextComponent tc, Point p) {

		int offset = tc.viewToModel(p);
		if (offset<0 || offset>=tc.getDocument().getLength()) {
			lastCompletionsAtText = null;
			return lastParameterizedCompletionsAt = null;
		}

		Segment s = new Segment();
		Document doc = tc.getDocument();
		Element root = doc.getDefaultRootElement();
		int line = root.getElementIndex(offset);
		Element elem = root.getElement(line);
		int start = elem.getStartOffset();
		int end = elem.getEndOffset() - 1;

		try {

			doc.getText(start, end-start, s);

			// Get the valid chars before the specified offset.
			int startOffs = s.offset + (offset-start) - 1;
			while (startOffs>=s.offset && isValidChar(s.array[startOffs])) {
				startOffs--;
			}

			// Get the valid chars at and after the specified offset.
			int endOffs = s.offset + (offset-start);
			while (endOffs<s.offset+s.count && isValidChar(s.array[endOffs])) {
				endOffs++;
			}

			int len = endOffs - startOffs - 1;
			if (len<=0) {
				return lastParameterizedCompletionsAt = null;
			}
			String text = new String(s.array, startOffs+1, len);

			if (text.equals(lastCompletionsAtText)) {
				return lastParameterizedCompletionsAt;
			}

			// Get a list of all Completions matching the text.
			List<Completion> list = getCompletionByInputText(text);
			lastCompletionsAtText = text;
			return lastParameterizedCompletionsAt = list;

		} catch (BadLocationException ble) {
			 // Never happens
		}

		lastCompletionsAtText = null;
		return lastParameterizedCompletionsAt = null;

	}


	/**
	 * {@inheritDoc}
	 */
	public List<ParameterizedCompletion> getParameterizedCompletions(
			JTextComponent tc) {

		List<ParameterizedCompletion> list = null;

		// If this provider doesn't support parameterized completions,
		// bail out now.
		char paramListStart = getParameterListStart();
		if (paramListStart==0) {
			return list; // null
		}

		int dot = tc.getCaretPosition();
		Segment s = new Segment();
		Document doc = tc.getDocument();
		Element root = doc.getDefaultRootElement();
		int line = root.getElementIndex(dot);
		Element elem = root.getElement(line);
		int offs = elem.getStartOffset();
		int len = dot - offs - 1/*paramListStart.length()*/;
		if (len<=0) { // Not enough chars on line for a method.
			return list; // null
		}

		try {

			doc.getText(offs, len, s);

			// Get the identifier preceding the '(', ignoring any whitespace
			// between them.
			offs = s.offset + len - 1;
			while (offs>=s.offset && Character.isWhitespace(s.array[offs])) {
				offs--;
			}
			int end = offs;
			while (offs>=s.offset && isValidChar(s.array[offs])) {
				offs--;
			}

			String text = new String(s.array, offs+1, end-offs);

			// Get a list of all Completions matching the text, but then
			// narrow it down to just the ParameterizedCompletions.
			List<Completion> l = getCompletionByInputText(text);
			if (l!=null && !l.isEmpty()) {
				for (int i=0; i<l.size(); i++) {
					Object o = l.get(i);
					if (o instanceof ParameterizedCompletion) {
						if (list==null) {
							list = new ArrayList<ParameterizedCompletion>(1);
						}
						list.add((ParameterizedCompletion)o);
					}
				}
			}

		} catch (BadLocationException ble) {
			 // Never happens
		}

		return list;

	}


	/**
	 * Initializes this completion provider.
	 */
	protected void init() {
		completions = new ArrayList<Completion>();
		seg = new Segment();
	}


	/**
	 * Returns whether the specified character is valid in an auto-completion.
	 * The default implementation is equivalent to
	 * "<code>Character.isLetterOrDigit(ch) || ch=='_'</code>".  Subclasses
	 * can override this method to change what characters are matched.
	 *
	 * @param ch The character.
	 * @return Whether the character is valid.
	 */
	protected boolean isValidChar(char ch) {
		return Character.isLetterOrDigit(ch) || ch=='_';
	}


	/**
	 * Loads completions from an XML file.  The XML should validate against
	 * <code>CompletionXml.dtd</code>.
	 *
	 * @param file An XML file to load from.
	 * @throws IOException If an IO error occurs.
	 */
	public void loadFromXML(File file) throws IOException {
		BufferedInputStream bin = new BufferedInputStream(
										new FileInputStream(file));
		try {
			loadFromXML(bin);
		} finally {
			bin.close();
		}
	}


	/**
	 * Loads completions from an XML input stream.  The XML should validate
	 * against <code>CompletionXml.dtd</code>.
	 *
	 * @param in The input stream to read from.
	 * @throws IOException If an IO error occurs.
	 */
	public void loadFromXML(InputStream in) throws IOException {
		loadFromXML(in, null);
	}


	/**
	 * Loads completions from an XML input stream.  The XML should validate
	 * against <code>CompletionXml.dtd</code>.
	 *
	 * @param in The input stream to read from.
	 * @param cl The class loader to use when loading any extra classes defined
	 *        in the XML, such as custom {@link FunctionCompletion}s.  This
	 *        may be <code>null</code> if the default is to be used, or if no
	 *        custom completions are defined in the XML.
	 * @throws IOException If an IO error occurs.
	 */
	public void loadFromXML(InputStream in, ClassLoader cl) throws IOException {

		//long start = System.currentTimeMillis();

		SAXParserFactory factory = SAXParserFactory.newInstance();
		factory.setValidating(true);
		CompletionXMLParser handler = new CompletionXMLParser(this, cl);
		BufferedInputStream bin = new BufferedInputStream(in);
		try {
			SAXParser saxParser = factory.newSAXParser();
			saxParser.parse(bin, handler);
			List<Completion> completions =  handler.getCompletions();
			addCompletions(completions);
			char startChar = handler.getParamStartChar();
			if (startChar!=0) {
				char endChar = handler.getParamEndChar();
				String sep = handler.getParamSeparator();
				if (endChar!=0 && sep!=null && sep.length()>0) { // Sanity
					setParameterizedCompletionParams(startChar, sep, endChar);
				}
			}
		} catch (SAXException se) {
			throw new IOException(se.toString());
		} catch (ParserConfigurationException pce) {
			throw new IOException(pce.toString());
		} finally {
			//long time = System.currentTimeMillis() - start;
			//System.out.println("XML loaded in: " + time + "ms");
			bin.close();
		}

	}


	/**
	 * Loads completions from an XML file.  The XML should validate against
	 * <code>CompletionXml.dtd</code>.
	 *
	 * @param resource A resource the current ClassLoader can get to.
	 * @throws IOException If an IO error occurs.
	 */
	public void loadFromXML(String resource) throws IOException {
		ClassLoader cl = getClass().getClassLoader();
		InputStream in = cl.getResourceAsStream(resource);
		if (in==null) {
			File file = new File(resource);
			if (file.isFile()) {
				in = new FileInputStream(file);
			}
			else {
				throw new IOException("No such resource: " + resource);
			}
		}
		BufferedInputStream bin = new BufferedInputStream(in);
		try {
			loadFromXML(bin);
		} finally {
			bin.close();
		}
	}


}