/*
 * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License
 *       at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jsyntaxpane.actions;

import java.awt.Component;
import java.text.MessageFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import jsyntaxpane.SyntaxDocument;
import jsyntaxpane.actions.gui.QuickFindDialog;
import jsyntaxpane.actions.gui.ReplaceDialog;

/**
 * Data that is shared by Find / Replace and Find Next actions for a Document
 * The data here will be added as a property of the Document using the key
 * PROPERTY_KEY.  Only through the getFtmEditor can you crate a new instance.
 *
 * The class is responsible for handling the doFind and doReplace all actions.
 *
 * The class is also responsible for displaying the Find / Replace dialog
 *
 * @author Ayman Al-Sairafi
 */
public class DocumentSearchData {

	private static final String PROPERTY_KEY = "SearchData";
	private Pattern pattern = null;
	private boolean wrap = true;
	private ReplaceDialog replaceDlg;
	private QuickFindDialog quickFindDlg;

	/**
	 * This prevent creating a new instance.  You must call the getFromEditor
	 * to crate a new instance attached to a Document
	 *
	 */
	private DocumentSearchData() {
	}

	public Pattern getPattern() {
		return pattern;
	}

	/**
	 * Set the pattern to the given compiled pattern.
	 * @see this#setPattern(String, boolean, boolean)
	 * @param pattern
	 */
	public void setPattern(Pattern pattern) {
		this.pattern = pattern;
	}

	/**
	 * Sets the pattern from a string and flags
	 * @param pat String of pattern
	 * @param regex true if the pattern should be a regexp
	 * @param ignoreCase true to ignore case
	 * @throws java.util.regex.PatternSyntaxException
	 */
	public void setPattern(String pat, boolean regex, boolean ignoreCase)
		throws PatternSyntaxException {
		if (pat != null && pat.length() > 0) {
			int flag = (regex) ? 0 : Pattern.LITERAL;
			flag |= (ignoreCase) ? Pattern.CASE_INSENSITIVE : 0;
			setPattern(Pattern.compile(pat, flag));
		} else {
			setPattern(null);
		}
	}

	public boolean isWrap() {
		return wrap;
	}

	public void setWrap(boolean wrap) {
		this.wrap = wrap;
	}

	/**
	 * Get the Search data from a Document.  If document does not have any
	 * search data, then a new instance is added, put and reurned.
	 * @param target JTextCOmponent we are attaching to
	 * @return
	 */
	public static DocumentSearchData getFromEditor(JTextComponent target) {
		if (target == null) {
			return null;
		}
		Object o = target.getDocument().getProperty(PROPERTY_KEY);
		if (o instanceof DocumentSearchData) {
			DocumentSearchData documentSearchData = (DocumentSearchData) o;
			return documentSearchData;
		} else {
			DocumentSearchData newDSD = new DocumentSearchData();
			target.getDocument().putProperty(PROPERTY_KEY, newDSD);
			return newDSD;
		}
	}

	/**
	 * Perform a replace all operation on the given component.
	 * Note that this create a new duplicate String big as the entire
	 * document and then assign it to the target text component
	 * @param target
	 * @param replacement
	 */
	public void doReplaceAll(JTextComponent target, String replacement) {
		if (replacement == null) {
			replacement = "";
		}
		SyntaxDocument sDoc = ActionUtils.getSyntaxDocument(target);
		if (sDoc == null) {
			return;
		}
		if (getPattern() == null) {
			return;
		}
		Matcher matcher = sDoc.getMatcher(getPattern());
		String newText = matcher.replaceAll(replacement);
		try {
			sDoc.replace(0, sDoc.getLength(), newText, null);
		} catch (BadLocationException ex) {
			Logger.getLogger(DocumentSearchData.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	/**
	 * Replace single occurrence of match with the replacement.
	 * @param target
	 * @param replacement
	 */
	public void doReplace(JTextComponent target, String replacement) {
		if (target.getSelectedText() != null) {
			target.replaceSelection(replacement == null ? "" : replacement);
			doFindNext(target);
		}
	}

	/**
	 * FInd the previous match
	 * @param target
	 * @return
	 */
	public boolean doFindPrev(JTextComponent target) {
		if (getPattern() == null) {
			return false;
		}
		SyntaxDocument sDoc = ActionUtils.getSyntaxDocument(target);
		if (sDoc == null) {
			return false;
		}
		int dot = target.getSelectionStart();
		Matcher matcher = sDoc.getMatcher(getPattern());
		if (matcher == null) {
			return false;
		}
		// we have no way of jumping to last match, so we need to
		// go throw all matches, and stop when we reach current pos
		int start = -1;
		int end = -1;
		while (matcher.find()) {
			if (matcher.end() >= dot) {
				break;
			}
			start = matcher.start();
			end = matcher.end();
		}
		if (end > 0) {
			target.select(start, end);
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Perform a FindNext operation on the given text component.  Position
	 * the caret at the start of the next found pattern.
	 * @param target
	 * @return true if pattern is found, false otherwise
	 */
	public boolean doFindNext(JTextComponent target) {
		if (getPattern() == null) {
			return false;
		}
		SyntaxDocument sDoc = ActionUtils.getSyntaxDocument(target);
		if (sDoc == null) {
			return false;
		}
		int start = target.getSelectionEnd();
		if (target.getSelectionEnd() == target.getSelectionStart()) {
			// we must advance the position by one, otherwise we will find
			// the same text again
			start++;
		}
		if (start >= sDoc.getLength()) {
			start = sDoc.getLength();
		}
		Matcher matcher = sDoc.getMatcher(getPattern(), start);
		if (matcher != null && matcher.find()) {
			// since we used an offset in the matcher, the matcher location
			// MUST be offset by that location
			target.select(matcher.start() + start, matcher.end() + start);
			return true;
		} else {
			if (isWrap()) {
				matcher = sDoc.getMatcher(getPattern());
				if (matcher != null && matcher.find()) {
					target.select(matcher.start(), matcher.end());
					return true;
				} else {
					return false;
				}
			} else {
				return false;
			}
		}
	}

	/**
	 * Display an OptionPane dialog that the search string is not found
	 * @param target
	 */
	public void msgNotFound(Component target) {
		JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(target),
			MessageFormat.format(java.util.ResourceBundle.getBundle("jsyntaxpane/Bundle").getString("DocumentSearchData.SearchStringNotFound"), getPattern()),
			java.util.ResourceBundle.getBundle("jsyntaxpane/Bundle").getString("DocumentSearchData.Find"), JOptionPane.INFORMATION_MESSAGE);
	}

	/**
	 * Show the Find and Replace dialog for the given frame
	 * @param target
	 */
	public void showReplaceDialog(JTextComponent target) {
		if (replaceDlg == null) {
			replaceDlg = new ReplaceDialog(target, this);
		}
		replaceDlg.setVisible(true);
	}

	public void showQuickFindDialog(JTextComponent target) {
		if (quickFindDlg == null) {
			quickFindDlg = new QuickFindDialog(target, this);
		}
		quickFindDlg.showFor(target);
	}
}
