/**
 *#########################################################################
 *
 * A component of the Gatherer application, part of the Greenstone digital
 * library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * Author: John Thompson, Greenstone Digital Library, University of Waikato
 *
 * Copyright (C) 1999 New Zealand Digital Library Project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */
package org.greenstone.gatherer.gui.metaaudit;


import java.util.ArrayList;
import org.greenstone.gatherer.util.StaticStrings;

/** An Autofilter object stores the filters set on a single column, and provides a method for determining if a certain value passes the filter.
 * @author John Thompson
 * @version 2.3
 */
public class Autofilter {

    static public void main(String[] args) {
	if(args.length != 2) {
	    System.err.println("Usage: java Autofilter <string> <pattern>");
	}
	else {
	    switch(compareToPattern(args[0], args[1])) {
	    case LESS_THAN:
		System.err.print(args[0] + " is less than");
		break;
	    case GREATER_THAN:
		System.err.print(args[0] + " is greater than");
		break;
	    case EQUAL:
		System.err.print(args[0] + " is equal to");
		break;
	    default:
		System.err.print(args[0] + " is something to");
	    }
	    System.err.println(" " + args[1]);
	}
    }

    /** A String comparison which also recognises the WildCard character. The basic idea is that '*' matches zero or more of any characters, and if we limited patterns to be like "a*" then as soon as we hit the star we could return a match. The problem is that we want patterns like "b*y" which would match "boy" but not "bee" nor "bus" (but should return less than or greater than for each of these respectively). Thus our testing has to become non-deterministic so the matches are conducted like so: 
     * Given p = [ b * y ]
     * Test  s = [ b e e ]  Result: p0=s0,{(p2>s1=>ERROR),(p1=s1,{(p1>s2=>ERROR),(p2>s2=>LESS_THAN)})} => LESS_THAN
     * Test  s = [ b o y ]  Result: p0=s0,{(p2>s1=>ERROR),(p1=s1,{(p1>s2=>ERROR),(p2=s2=>EQUAL)})} => EQUAL
     * Test  s = [ b u s ]  Result: p0=s0,{(p2>s1=>ERROR),(p1=s1,{(p1>s2=>ERROR),(p2<s2=>GREATER_THAN)})} => GREATER_THAN
     * where the two () phrases within a {} represent two different 'threads' of processing. Errors are generated when one string runs out of characters before the other. Only non-error results are propogated. I've decided to implement this non-deterministic matching as a recursive function call.
     * If you analyse it you can see there are three possible actions when a '*' is detected.
     * 1. * is an empty string, ignore it in pattern and try to compare the next character in p with the current character in s. This case only occurs if there are more characters in p (otherwise apple would be greater than a*)
     * 2. * matches exactly the current character in s, try to match the next character in p with the next character in s
     * 3. * matches some string of characters including the current character in s, try to match '*' against the next character in s
     * @param  s the String being matched
     * @param  p the Pattern to match it against
     * @return -1 if s is less than p, 0 if they are equal, 1 if s is greater than p
     */
    static private int compareToPattern(String s, String p) {
	if(p.indexOf(StaticStrings.STAR_CHAR) == -1) {
	    return compareToPattern(s, p, 0, 0, false);
	}
	else {
	    return compareToPattern(s, p, 0, 0, true);
	}
    }

    static final private int LESS_THAN = -1;
    static final private int EQUAL = 0;
    static final private int GREATER_THAN = 1;
    static final private int ERROR = 42;

    static private int compareToPattern(String s, String p, int s_index, int p_index, boolean wildcard_matching) {
	// Lets do the simple cases first.
	// Both out of characters at the same time
	if(s_index >= s.length() && p_index >= p.length()) {
	    ///ystem.err.println("Both out of characters. Equal.");
	    return EQUAL;
	}
	// s out of characters first
	if(s_index >= s.length()) {
	    // Not allowed to run out if matching wildcard
	    if(wildcard_matching) {
		return ERROR;
	    }
	    // Remember that wildcard matches the empty string too, so as long as there are only '*' characters left in the pattern we can say they are equal as well.
	    else {
		while(p_index < p.length()) {
		    if(p.charAt(p_index) == StaticStrings.STAR_CHAR) {
			p_index++;
		    }
		    else {
			///ystem.err.println("S out of characters. Less Than.");
			return LESS_THAN;
		    }
		}
		// We've run out of pattern and it only contained '*' so we're still equal
		///ystem.err.println("Both out of characters. Equal.");
		return EQUAL;
	    }
	}
	// p out of characters first
	if(p_index >= p.length()) {
	    // Not allowed to run out if matching wildcard
	    if(wildcard_matching) {
		return ERROR;
	    }
	    // so it is greater than
	    else {
		///ystem.err.println("P out of characters. Greater Than.");
		return GREATER_THAN;
	    }
	}
	char s_char = s.charAt(s_index);
	char p_char = p.charAt(p_index);
	///ystem.err.println("Comparing " + s_char + " against pattern character " + p_char);
	// Equivelent characters
	// Now the tricky-dicky bit - WildCard matching.
	if(p_char == StaticStrings.STAR_CHAR) {
	    int result;
	    // Case 1
	    ///ystem.err.println("WildCard Case 1");
	    if(p_index + 1 < p.length()) {
		if((result = compareToPattern(s, p, s_index, p_index + 1, wildcard_matching)) != ERROR) {
		    return result;
		}
	    }
	    // Case 2
	    ///ystem.err.println("WildCard Case 2");
	    if((result = compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching)) != ERROR) {
		return result;
	    } 
	    // Case 3
	    ///ystem.err.println("WildCard Case 3");
	    if((result = compareToPattern(s, p, s_index + 1, p_index, wildcard_matching)) != ERROR) {
		return result;
	    }
	}
	if(s_char == p_char) {
	    return compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching);
	}
	// A preceeding character
	if(s_char < p_char) {
	    ///ystem.err.println("s char is less than p char");
	    if(wildcard_matching) {
		///ystem.err.println("Because we are wildcard matching we still have to match the rest of s incase of errors.");
		if(compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching) != ERROR) {
		    ///ystem.err.println("No error. Less than.");
		    return LESS_THAN;
		}
		else {
		    ///ystem.err.println("Error detected.");
		    return ERROR;
		}
	    }
	    else {
		///ystem.err.println("Less than.");
		return LESS_THAN;
	    }
	}
	// A succeeding character
	if(s_char > p_char) {
	    ///ystem.err.println("s char is greater than p char");
	    if(wildcard_matching) {
		///ystem.err.println("Because we are wildcard matching we still have to match the rest of s incase of errors.");
		if(compareToPattern(s, p, s_index + 1, p_index + 1, wildcard_matching) != ERROR) {
		    ///ystem.err.println("No error. Greater than.");
		    return GREATER_THAN;
		}
		else {
		    ///ystem.err.println("Error detected.");
		    return ERROR;
		}
	    }
	    else {
		///ystem.err.println("Greater than.");
		return GREATER_THAN;
	    }
	}
	// Oh-No. Well, we'll just assume that string is less than pattern
	return LESS_THAN;
    }

    /** <i>true</i> if the filter should be applied, <i>false</i> to indicate the filter is turned off. */
    public boolean active;
    /** <i>true</i> if the matching for the first expression should be case sensitive, <i>false</i> otherwise. */
    public boolean casesense_one;
    /** <i>true</i> if the matching for the second expression should be case sensitive, <i>false</i> otherwise. */
    public boolean casesense_two;
    /** Used to determine the operation intended when applying two filters, and set using the values of the OPERATION_TYPE enumeration. */
    public boolean operation;
    /** Used to determine how the column this filter is applied to should be sorted, and set using the values of the SORT_TYPE enumeration. */
    public boolean sort;
    /** The method to be used for the first filter expression, set from the values of the METHOD_LIST enumeration. */
    public int method_one;
    /** The method to be used for the second filter expression, set from the values of the METHOD_LIST enumeration. */
    public int method_two;
    /** The value to be matched against for the first expression. */
    public String value_one;
    /** The value to be matched against for the second expression. */
    public String value_two;
    /** An element of the SORT_TYPE enumeration, indicates lowest to highest value column ordering. */
    public static final boolean ASCENDING = true;
    /** An element of the OPERATION_TYPE enumeration, indicates that both filter expressions must be met (conjunction). */
    public static final boolean AND = true;
    /** An element of the SORT_TYPE enumeration, indicates highest to lowest value column ordering. */
    public static final boolean DESCENDING = false;
    /** An element of the OPERATION_TYPE enumeration, indicates that either (or both) filter expressions must be met (disjunction). */
    public static final boolean OR = false;
    /** An enumeration of symbolic names of various matching methods. */
    public static final String METHOD_LIST[] = { "Autofilter.eqeq", "Autofilter.!eq", "Autofilter.<", "Autofilter.<eq", "Autofilter.>", "Autofilter.>eq", "Autofilter.^", "Autofilter.!^", "Autofilter.$", "Autofilter.!$", "Autofilter.?", "Autofilter.!?" };

    /** Default Constructor. */
    public Autofilter() {
	operation = OR;
	sort = ASCENDING;
    }
    /** Determine if this filter is currently active.
     * @return <i>true</i> if it is active, <i>false</i> otherwise.
     */
    public boolean active() {
	return active;
    }
    /** Determine if this list of values (for a certain cell) passes the filter.
     * @param values An <strong>ArrayList</strong> of values sourced from a single cell in the associated column.
     * @return <i>true</i> if the values match and should be displayed, <i>false</i> otherwise.
     */
    public boolean filter(ArrayList values) {
	boolean result = false;
	if(value_one != null) {
	    result = filter(values, method_one, value_one, casesense_one);
	    if(result) {
		if(operation == AND && value_two != null) {
		    result = filter(values, method_two, value_two, casesense_two);
		}
	    }
	    else if(operation == OR && value_two != null) {
		result = filter(values, method_two, value_two, casesense_two);
	    }
	}
	return result;
    }
    /** Set the current activity state of this filter.
     * @param active The new state of this filter, <i>true</i> to activate, <i>false</i> otherwise.
     */
    public void setActive(boolean active) {
	this.active = active;
    }
    /** Set one of the filter expressions using the given information.
     * @param number The number of the filter you wish to set as an <i>int</i>. Either 1 or 2.
     * @param method An <i>int</i> indicating the method to be used when matching.
     * @param value The <strong>String</strong> to be matched against.
     * @param casesense <i>true</i> if this expression should be case sensitive, <i>false</i> otherwise.
     */
    public void setFilter(int number, int method, String value, boolean casesense) {
	if(!casesense && value != null) {
	    value = value.toLowerCase();
	}
	if(number == 1) {
	    casesense_one = casesense;
	    method_one = method;
	    value_one = value;
	}
	else {
	    casesense_two = casesense;
	    method_two = method;
	    value_two = value;
	}
    }
    /** Set the operation to be used to join the two filters (if a second filter is set).
     * @param operation <i>true</i> for conjunct filters, <i>false</i> for disjunct.
     */ 
    public void setOperation(boolean operation) {
	this.operation = operation;
    }
    /** Set the sort order of this column.
     * @param sort <i>true</i> for ascending sort, <i>false</i> for descending.
     */
    public void setSort(boolean sort) {
	this.sort = sort;
    }
    /** Decide whether a row should be displayed or filtered. The result depends on the selector set with setFilterType.
     * @param values An <strong>ArrayList</strong> of values to be checked against the filter.
     * @param method The method of matching to be used, as an <i>int</i>.
     * @param target The <strong>String</Strong> to match against.
     * @param casesense <i>true</i> if the match is to be case sensitive, <i>false</i> otherwise.
     * @return <i>true</i> to display the row, <i>false</i> to hide it.
     */
    public boolean filter(ArrayList values, int method, String target, boolean casesense) {
	boolean result = false;
	// There are several special cases when the filter always returns turn, such as when the target is the wildcard character.
	if(target == null || target.length() == 0 || target.equals("*")) {
	    result = true;
	}
	else {
	    // For each value in the list...
	    for(int i = 0; i < values.size(); i++) {
		boolean pass;
		String source;
		// Account for case sensitivity.
		if(casesense) {
		    source = values.get(i).toString();
		}
		else {
		    source = values.get(i).toString().toLowerCase();
		}
		///ystem.err.println("Testing " + source + " against pattern " + target);
		// Perform the match, based on the selected method.
		switch(method) {
		case 1: // !EQ
		    pass = (compareToPattern(source, target) != 0);
		    break;
		case 2: // <
		    pass = (compareToPattern(source, target) < 0);
		    break;
		case 3: // <eq
		    pass = (compareToPattern(source, target) <= 0);
		    break;
		case 4: // >
		    pass = (compareToPattern(source, target) > 0);
		    break;
		case 5: // >eq
		    pass = (compareToPattern(source, target) >= 0);
		    break;
		case 6: // ^
		    pass = source.startsWith(target);
		    break;
		case 7: // !^
		    pass = !(source.startsWith(target));
		    break;
		case 8: // $
		    pass = source.endsWith(target);
		    break;
		case 9: // !$
		    pass = !(source.endsWith(target));
		    break;
		case 10: // ?
		    pass = (source.indexOf(target) != -1);
		    break;
		case 11: // !?
		    pass = (source.indexOf(target) == -1);
		    break;
		default: // EQEQ
		    pass = (compareToPattern(source, target) == 0);
		    break;
		}
		result = result || pass;
	    }
	}
	return result;		 
    }
    /** Produce a textual representation of this autofilter.
     * @return A <strong>String</strong> displaying details of this autofilter.
     */
    public String toString() {
	String result = "One: " + method_one + " - " + value_one + " - " + casesense_one;
	if(value_two != null) {
	    result = result + "\n" + "Operation: " + (operation?"AND":"OR") + "\n";
	    result = result + "Two: " + method_two + " - " + value_two + " - " + casesense_two;
	}
	return result;
    }
}
