/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.query.sql.lang;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.teiid.api.exception.query.ExpressionEvaluationException;
import org.teiid.core.util.EquivalenceUtil;
import org.teiid.core.util.HashCodeUtil;
import org.teiid.core.util.LRUCache;
import org.teiid.core.util.PropertiesUtils;
import org.teiid.language.Like.MatchMode;
import org.teiid.query.QueryPlugin;
import org.teiid.query.sql.LanguageVisitor;
import org.teiid.query.sql.lang.PredicateCriteria.Negatable;
import org.teiid.query.sql.symbol.Expression;


/**
 * This class represents a criteria involving a string expression to be matched
 * against a string expression match value.  The match value may contain a few
 * special characters: % represents 0 or more characters and _ represents a single
 * match character.  The escape character can be used to escape an actual % or _ within a
 * match string. 
 */
public class MatchCriteria extends PredicateCriteria implements Negatable {

	/** The default wildcard character - '%' */
	public static final char WILDCARD_CHAR = '%';

	/** The default single match character - '_' */
	public static final char MATCH_CHAR = '_';

	/** The left-hand expression. */
	private Expression leftExpression;
	
	/** The right-hand expression. */
	private Expression rightExpression;
    
	/** The internal null escape character */
	public static final char NULL_ESCAPE_CHAR = 0;

	static char DEFAULT_ESCAPE_CHAR = PropertiesUtils.getBooleanProperty(System.getProperties(), "org.teiid.backslashDefaultMatchEscape", false)?'\\':NULL_ESCAPE_CHAR; //$NON-NLS-1$
	
	/** The escape character or '' if there is none */
	private char escapeChar = DEFAULT_ESCAPE_CHAR;
	
    /** Negation flag. Indicates whether the criteria expression contains a NOT. */
    private boolean negated;
    private MatchMode mode = MatchMode.LIKE;
    
    /**
     * Constructs a default instance of this class.
     */
    public MatchCriteria() {}
    
    /**
     * Constructs an instance of this class from a left and right expression
     * 
     * @param leftExpression The expression to check
     * @param rightExpression The match expression
     */
    public MatchCriteria( Expression leftExpression, Expression rightExpression ) {
        setLeftExpression(leftExpression);
        setRightExpression(rightExpression);
    }

    /**
     * Constructs an instance of this class from a left and right expression
     * and an escape character
     * 
      * @param leftExpression The expression to check
     * @param rightExpression The match expression
     * @param escapeChar The escape character, to allow literal use of wildcard and single match chars
     */
    public MatchCriteria( Expression leftExpression, Expression rightExpression, char escapeChar ) {
		this(leftExpression, rightExpression);
        setEscapeChar(escapeChar);
    }

	/**
	 * Set left expression.
	 * @param expression expression
	 */
	public void setLeftExpression(Expression expression) { 
		this.leftExpression = expression;
	}
	
	/**
	 * Get left expression.
	 * @return Left expression
	 */
	public Expression getLeftExpression() {
		return this.leftExpression;
	}

	/**
	 * Set right expression.
	 * @param expression expression
	 */
	public void setRightExpression(Expression expression) { 
		this.rightExpression = expression;
	}
	
	/**
	 * Get right expression.
	 * @return right expression
	 */
	public Expression getRightExpression() {
		return this.rightExpression;
	}

	/**
	 * Get the escape character, which can be placed before the wildcard or single match
	 * character in the expression to prevent it from being used as a wildcard or single
	 * match.  The escape character must not be used elsewhere in the expression.
	 * For example, to match "35%" without activating % as a wildcard, set the escape character
	 * to '$' and use the expression "35$%".	 
	 * @return Escape character, if not set will return {@link #NULL_ESCAPE_CHAR}
	 */
	public char getEscapeChar() {
		return this.escapeChar;
	}

	/**
	 * Set the escape character which can be used when the wildcard or single
	 * character should be used literally.
	 * @param escapeChar New escape character
	 */
	public void setEscapeChar(char escapeChar) {
		this.escapeChar = escapeChar;
	}

    /**
     * Returns whether this criteria is negated.
     * @return flag indicating whether this criteria contains a NOT
     */
    public boolean isNegated() {
        return negated;
    }
    
    /**
     * Sets the negation flag for this criteria.
     * @param negationFlag true if this criteria contains a NOT; false otherwise
     */
    public void setNegated(boolean negationFlag) {
        negated = negationFlag;
    }
    
    @Override
    public void negate() {
    	this.negated = !this.negated;
    }

    public void acceptVisitor(LanguageVisitor visitor) {
        visitor.visit(this);
    }
	
	/**
	 * Get hash code.  WARNING: The hash code is based on data in the criteria.
	 * If data values are changed, the hash code will change - don't hash this
	 * object and change values.
	 * @return Hash code
	 */
	public int hashCode() {
		int hc = 0;
		hc = HashCodeUtil.hashCode(hc, getLeftExpression());
		hc = HashCodeUtil.hashCode(hc, getRightExpression());
		return hc;
	}
	
    /**
     * Override equals() method.
     * @param obj Other object
     * @return True if equal
     */
    public boolean equals(Object obj) {
    	// Use super.equals() to check obvious stuff and variable
    	if(obj == this) {
			return true;
		}
		
		if(!(obj instanceof MatchCriteria)) {
    		return false;
		}

        MatchCriteria mc = (MatchCriteria)obj;

        if (isNegated() != mc.isNegated()) {
            return false;
        }
        
        if (this.mode != mc.mode) {
        	return false;
        }
        
        return getEscapeChar() == mc.getEscapeChar() &&
        EquivalenceUtil.areEqual(getLeftExpression(), mc.getLeftExpression()) &&
        EquivalenceUtil.areEqual(getRightExpression(), mc.getRightExpression());
	}

	/**
	 * Deep copy of object
	 * @return Deep copy of object
	 */
	public Object clone() {
	    Expression leftCopy = null;
	    if(getLeftExpression() != null) { 
	        leftCopy = (Expression) getLeftExpression().clone();
	    }	
	    Expression rightCopy = null;
	    if(getRightExpression() != null) { 
	        rightCopy = (Expression) getRightExpression().clone();
	    }	
	    MatchCriteria criteriaCopy = new MatchCriteria(leftCopy, rightCopy, getEscapeChar());
        criteriaCopy.setNegated(isNegated());
        criteriaCopy.mode = mode;
        return criteriaCopy;
	}
	
    private final static LRUCache<List<?>, Pattern> patternCache = new LRUCache<List<?>, Pattern>(100);
    
    public static Pattern getPattern(String newPattern, String originalPattern, int flags) throws ExpressionEvaluationException {
    	List<?> key = Arrays.asList(newPattern, flags);
    	Pattern p = patternCache.get(key);
        if (p == null) {
            try {	        
    	        p = Pattern.compile(newPattern, Pattern.DOTALL);
    	        patternCache.put(key, p);
    		} catch(PatternSyntaxException e) {
                 throw new ExpressionEvaluationException(QueryPlugin.Event.TEIID30448, e, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30448, new Object[]{originalPattern, e.getMessage()}));
    		}
        }
        return p;
    }

	/**
     * <p>Utility to convert the pattern into a different match syntax</p>
     */
	public static class PatternTranslator {
	    private char[] reserved;
	    private char newEscape;
	    private char[] toReplace;
	    private String[] replacements;
	    private int flags;
	    private final LRUCache<List<?>, Pattern> cache = new LRUCache<List<?>, Pattern>(100);

	    /**
	     * @param newWildCard replacement for %
	     * @param newSingleMatch replacement for _
	     * @param reserved sorted array of reserved chars in the new match syntax
	     * @param newEscape escape char in the new syntax
	     */
	    public PatternTranslator(char[] toReplace, String[] replacements, char[] reserved, char newEscape, int flags) {
	        this.reserved = reserved;
	        this.newEscape = newEscape;
	        this.toReplace = toReplace;
	        this.replacements = replacements;
	        this.flags = flags;
	    }
	    
	    public Pattern translate(String pattern, char escape) throws ExpressionEvaluationException {
	        List<?> key = Arrays.asList(pattern, escape);
	        Pattern result = null;
	        synchronized (cache) {
	        	result = cache.get(key);
	        }
	        if (result == null) {
		        String newPattern = getPatternString(pattern, escape);
		        result = getPattern(newPattern, pattern, flags);
		        synchronized (cache) {
			        cache.put(key, result);
				}
	        }
	        return result;
	    }

		public String getPatternString(String pattern, char escape)
				throws ExpressionEvaluationException {
			int startChar = 0;
			StringBuffer newPattern = new StringBuffer(pattern.length());
			if (pattern.length() > 0 && pattern.charAt(0) == '%') {
				startChar = 1;
			} else {
				newPattern.append('^');
			}
	        
	        boolean escaped = false;
	        boolean endsWithMatchAny = false;
	        for (int i = startChar; i < pattern.length(); i++) {
	            char character = pattern.charAt(i);
	            
	            if (character == escape && character != NULL_ESCAPE_CHAR) {
	                if (escaped) {
	                    appendCharacter(newPattern, character);
	                    escaped = false;
	                } else {
	                    escaped = true;
	                }
	            } else {
	            	int index = Arrays.binarySearch(toReplace, character);
	            	if (index >= 0) {
		                if (escaped) {
		                    appendCharacter(newPattern, character);
		                    escaped = false;
		                } else {
		                	if (character == '%' && i == pattern.length() - 1) {
		                		endsWithMatchAny = true;
		                		continue;
		                	}
		                    newPattern.append(replacements[index]);
		                }
	            	} else {
		                if (escaped) {
		                     throw new ExpressionEvaluationException(QueryPlugin.Event.TEIID30449, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30449, new Object[] {pattern, new Character(escape)}));
		                }
		                appendCharacter(newPattern, character);
	            	}
	            }
	        }
	        
	        if (escaped) {
	             throw new ExpressionEvaluationException(QueryPlugin.Event.TEIID30449, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30449, new Object[] {pattern, new Character(escape)}));
	        }
	        
	        if (!endsWithMatchAny) {
	        	newPattern.append('$');
	        }
			return newPattern.toString();
		}
	    
	    private void appendCharacter(StringBuffer newPattern, char character) {
	        if (Arrays.binarySearch(this.reserved, character) >= 0) {
	            newPattern.append(this.newEscape);
	        } 
	        newPattern.append(character);
	    }
	    
	}
	
	public MatchMode getMode() {
		return mode;
	}
	
	public void setMode(MatchMode mode) {
		this.mode = mode;
	}
	
}  // END CLASS
