/*
 * @(#)Matcher.java  1.0  2002-02-11
 *
 * Copyright (c) 2002 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package ch.randelshofer.util.regex;

import javax.swing.text.*;

/**
 * Searches for an occurence of a case sensitive text on a document.
 *
 * @author  Werner Randelshofer
 * @version 1.0 2002-02-11
 */
public class Matcher {
    /**
     * The document to be examined.
     */
    private Document document;
    /**
     * The string to be matched.
     */
    private String findString;
    /**
     * The start index for the next find operation.
     */
    private int startIndex;
    
    /**
     * The array of lower case matching chars.
     */
    private char[] matchLowerCase;
    
    /**
     * The array of upper case matching chars.
     */
    private char[] matchUpperCase;
    
    /**
     * This flag is set to true, when word
     * match is to be performed.
     */
    private boolean isMatchWord;
    
    /**
     * Creates a new instance of Matcher which
     * performs a case sensitive search.
     *
     * @param document The document to be examined
     * @param findString The string to be searched.
     */
    public Matcher(Document document, String findString) {
        this(document, findString, true, false);
    }
    
    /**
     * Creates a new instance of Matcher
     *
     * @param document The document to be examined
     * @param findString The string to be searched.
     * @param matchCase Set to true for case sensitive search.
     * @param matchWord Set to true for word match search.
     */
    public Matcher(Document document, String findString, boolean matchCase, boolean matchWord) {
        this.document = document;
        this.findString = findString;
        startIndex = 0;
        
        // Convert to chars for efficiency
        if (matchCase) {
            matchLowerCase = matchUpperCase = findString.toCharArray();
        } else {
            matchUpperCase = findString.toUpperCase().toCharArray();
            matchLowerCase = findString.toLowerCase().toCharArray();
        }
        
        isMatchWord = matchWord;
    }
    
    public String getFindString() {
        return findString;
    }
    
    
    /**
     * Resets this matcher and then attempts to find the next
     * subsequence of the input sequence that matches the pattern,
     * starting at the specified index.
     *
     * @param startIndex the index from which to start the search.
     *
     * @return The index of the first occurrence of the search string,
     * starting at the specified offset, or -1 if no occurrence was found.
     */
    public int find(int startIndex) {
        this.startIndex = startIndex;
        return find();
    }
    /**
     * Attempts to find the next subsequence of the
     * input sequence that matches the pattern.
     * <p>
     * This method starts at the beginning of
     * the input sequence or, if a previous invocation
     * of the method was successful and the matcher has not
     * since been reset, at the first character not matched by
     * the previous match.
     *
     * @return Returns the index of the first occurrence of the search string,
     * starting at the specified offset, or -1 if no occurrence was found.
     */
    public int find() {
        // Don't match empty strings and don't if we are at the end of the document.
        if (findString.length() == 0 || document.getLength() <= startIndex) return -1;
        
        try {
            int nextMatch = 0; // index of next matching character
            char prevChar = '\0'; // previous character (used for word match);
            
            // Iterate throug all segments of the document
            // starting from offset
            int nleft = document.getLength() - startIndex;
            Segment text = new Segment();
            //text.setPartialReturn(true); //<- Activate this for JDK 1.4
            iterateAllSegments: while (nleft > 0) {
                document.getText(startIndex, nleft, text);
                
                // Try to match the characters of the text segment with our matchChars
                for (text.first(); text.getIndex() < text.getEndIndex(); text.next()) {
                    char current = text.current();
                    if (current == matchUpperCase[nextMatch] || current == matchLowerCase[nextMatch]) {
                        if (! isMatchWord || nextMatch > 0 || prevChar <= ' ') {
                            nextMatch++;
                        }
                        
                        // Did we match all search characters?
                        if (nextMatch == matchLowerCase.length) {
                            if (isMatchWord) {
                                if (text.next() <= ' ') {
                                    startIndex = text.getIndex();
                                    return startIndex - matchLowerCase.length;
                                } else {
                                    //text.previous(); not needed since we must skip the next char anyway
                                    nextMatch = 0;
                                }
                            } else {
                                
                                startIndex = text.getIndex();
                                return startIndex - matchLowerCase.length + 1;
                            }
                        }
                    } else {
                        nextMatch = 0;
                    }
                    prevChar = current;
                }
                
                nleft -= text.count;
                startIndex += text.count;
            }
            startIndex = document.getLength();
            return -1;
        } catch (BadLocationException e) {
            throw new IndexOutOfBoundsException();
        }
    }
    
    /**
     * Resets the startIndex of the matcher to 0.
     */
    public void reset() {
        startIndex = 0;
    }
}

