/***************************************************************************
    Copyright          : (C) 2002 by Neoworks Limited. All rights reserved
    URL                : http://www.neoworks.com
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

/*
 * JukeXParser.java
 *
 * Created on 12 April 2002, 16:59
 */

package com.neoworks.jukex.query;

import com.neoworks.rdc.*;
import org.apache.log4j.Category;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;

import com.neoworks.jukex.Attribute;
import com.neoworks.jukex.TrackStore;
import com.neoworks.jukex.TrackStoreFactory;

/**
 * A recursive descent compiler which parses the JukeX Query Language into an
 * object which represents a query.
 *
 * @author Nick Vincent <a href="mailto:nick@neoworks.com">nick@neoworks.com</a>
 */
public class JukeXParser extends Parser
{
	private static final Category log = Category.getInstance(JukeXParser.class.getName());
	private static final boolean logDebugEnabled = log.isDebugEnabled();
	private static final boolean logInfoEnabled = log.isInfoEnabled();
		
	// Relational operators
	private static final int RELOP	= 10;
	private static final int EQUALS	= 20;
	private static final int LTHAN	= 30;
	private static final int GTHAN	= 40;
	private static final int LTEQTO	= 50;
	private static final int GTEQTO	= 60;
	private static final int REGEXP = 70;
	
	// Logical operators
	private static final int LOGOP	= 100;
	private static final int AND	= 110;
	private static final int OR		= 120;
	private static final int NOT	= 130;
	
	// Keywords
	private static final int KEYWORD= 200;
	private static final int SELECT	= 210;
	private static final int WHERE	= 220;
	private static final int ORDER	= 230;
	private static final int BY	= 240;
	private static final int ASC	= 250;
	private static final int DESC	= 260;
	
	// Symbols
	private static final int SYMBOL	= 300;
	private static final int LBRACE	= 310;
	private static final int RBRACE	= 320;
	
	// Types
	private static final int TYPE = 400;
	private static final int TRACK = 410;
	//private static final int ATTRIBUTE = 420;
	
	// Attributes
	private static final int ATTRIBUTE = 500;
	
	// Operator
	private static final int OPERATOR = 600;
	private static final int COMMA = 610;

	/**
	 * Result sorting (ORDER BY) flag indicating that results should not be sorted
	 */
	public static final int SORT_NONE = 0;
	/**
	 * Result sorting (ORDER BY) flag indicating that results should be sorted in ascending order
	 */
	public static final int SORT_ASC = 1;
	/**
	 * Result sorting (ORDER BY) flag indicating that results should be sorted in descending order
	 */
	public static final int SORT_DESC = 2;
		
	private Set attributes = null;			// Set of all attributes in the expression
	private List selectAttributes = null;		// List of select attributes (ordered, dupes allowed)
	private List orderingAttributes = null;		// List of ordering attributes (ordered)
	private String parseString = null;		// Local copy of what we parsed, in case it needs to be retrieved later
	private int sortOrder = SORT_NONE;
		
	/**
	 * Private constructor
	 *
	 * @param parseString The String to parse
	 */
	private JukeXParser( String parseString )
	{
		super( parseString , JukeXParser.makeSymbolTable() );
		this.attributes = new HashSet();
		this.selectAttributes = new LinkedList();
		this.orderingAttributes = new LinkedList();
		this.parseString = parseString;
		if (logDebugEnabled) log.debug("Creating JukeXParser with String " + parseString);
	}

	/**
	 * Create a new parser for the supplied String
	 *
	 * @param parseString The String to parse
	 * @return A JukeXParser for the parseString
	 */
	public static JukeXParser createParser( String parseString )
	{
		StringBuffer caseAdjustedString = new StringBuffer();
		StringTokenizer tokens = new StringTokenizer( parseString , " " );
		
		Set keywords = new HashSet();
		keywords.add( "SELECT" );
		keywords.add( "WHERE" );
		keywords.add( "AND" );
		keywords.add( "OR" );
		keywords.add( "REGEX" );
		keywords.add( "ORDER" );
		keywords.add( "BY" );
				
		while ( tokens.hasMoreTokens() )
		{
			String currToken = tokens.nextToken();
			String upperCurrToken = currToken.toUpperCase();
			if ( keywords.contains( upperCurrToken ) )
			{
				caseAdjustedString.append( upperCurrToken ).append( ' ' );
			}
			else
			{
				caseAdjustedString.append( currToken ).append( ' ' );
			}
		}
		
		return new JukeXParser( caseAdjustedString.toString() );
	}

	/**
	 * Parse the string.
	 *
	 * @return A Query object corresponding to the query string parsed
	 * @exception ParserException If the string does not represent a valid query
	 */
	public Query parse() throws ParserException
	{
		Expression e = null;
		QueryType q = null;
		String attrName = null;

		match( KEYWORD , SELECT );
		if (logDebugEnabled) log.debug("SELECT");

		if ( lookAhead( TYPE , TRACK ) )		// We're looking up a track..or...
		{
			match( TYPE , TRACK );
			
			q = new QueryType.Track();

			if ( lookAhead( KEYWORD , WHERE ) )
			{
				match ( KEYWORD , WHERE );
				e = expression();
			}
		}
		else	// We're matching a list of one or more attributes
		{
			q = new QueryType.Attribute();
			
			do
			{
				if ( lookAhead( OPERATOR , COMMA ) ) match( OPERATOR , COMMA );
				
				attrName = getLookAheadData();
				match( ATTRIBUTE );
				this.attributes.add( attrName );
				this.selectAttributes.add( attrName );
			}
			while ( lookAhead( OPERATOR , COMMA ) );
			
			if ( lookAhead( KEYWORD , WHERE ) )
			{
				match( KEYWORD , WHERE );
				e = expression();
			}	
		}

		if (lookAhead( KEYWORD , ORDER ))
		{
			match( KEYWORD , ORDER );
			match( KEYWORD , BY );
			do {
				if ( lookAhead(OPERATOR,COMMA)) match(OPERATOR,COMMA);
				attrName = getLookAheadData();
				match(ATTRIBUTE);
				this.orderingAttributes.add(attrName);
			} while ( lookAhead(OPERATOR, COMMA));
		}

		if (lookAhead( KEYWORD , ASC ))
		{
			match( KEYWORD, ASC);
			sortOrder = SORT_ASC;
		} else if (lookAhead( KEYWORD, DESC )) {
			match ( KEYWORD, DESC);
			sortOrder = SORT_DESC;
		}
				
		match( Lexer.END );
		
		if ( e == null ) e = new JukeXExpression.NullOp();
		
		return new Query ( q , e , this.attributes , this.selectAttributes, this.orderingAttributes , this.parseString , sortOrder );
	}

	/**
	 * Parse an expression
	 *
	 * @return The resulting Expression
	 * @exception ParserException If there is a parse error
	 */
	public Expression expression() throws ParserException
	{
		return logicalop();
	}

	/**
	 * Parse a logical operation
	 *
	 * @return The resulting Expression
	 * @exception ParserException If there is a parse error
	 */
	public Expression logicalop() throws ParserException
	{
		Expression e = prefixop();

		if ( lookAhead( LOGOP , AND ) )
		{
			match( LOGOP , AND );
			return new JukeXExpression.Infix( e , "AND" , logicalop() );
		}
		else if ( lookAhead( LOGOP , OR ) )
		{
			match( LOGOP , OR );
			return new JukeXExpression.Infix( e , "OR" , logicalop() );
		}
		else
		{
			return e;
		}
	}

	/**
	 * Parse a prefix operation.
	 *
	 * @return The resulting Expression
	 * @exception ParserException If there is a parse error
	 */
	public Expression prefixop() throws ParserException
	{
		if ( lookAhead( LOGOP , NOT ) )
		{
			match( LOGOP , NOT );
			return new JukeXExpression.Not( relopexpression() );
		}
		else
		{
			return relopexpression();
		}
	}

	/**
	 * Parse a relational operation.
	 *
	 * @return The resulting Expression
	 * @exception ParserException If there is a parse error
	 */
	public Expression relopexpression() throws ParserException
	{
		if ( lookAhead ( SYMBOL ) )
		{
			match ( SYMBOL , LBRACE );
			Expression e = expression();
			match ( SYMBOL , RBRACE );
			return e;
		}
		else
		{
			// Check the attribute exists
			String attributeName = getLookAheadData();
			match( ATTRIBUTE );
			
			this.attributes.add( attributeName );
			JukeXExpression.Variable variable = new JukeXExpression.Variable( attributeName );
								
			String operator = getLookAheadData();
			match( RELOP );
		
			JukeXExpression.Literal literal = null;
			if ( getLookAheadType() == Lexer.NUM )
			{
				literal = new JukeXExpression.Literal( new Integer( getLookAheadValue() ) );
			}
			else if ( getLookAheadType() == Lexer.STRING )
			{
				literal = new JukeXExpression.Literal( getLookAheadData() );
				// cheat ;)
				if (operator.equals("="))
				{
					operator = "LIKE";
				}
			}
			else
			{
				throw new ParserException( "Unexpected type where literal (String or int) expected" );
			}
			match();
			
			return new JukeXExpression.Relop( variable , operator , literal );
		}
	}

	/**
	 * Build the symbol table
	 *
	 * @return The symbol table as a Vector
	 */
	private static Vector makeSymbolTable()
	{
		Vector symbolTable = new Vector();
		
		symbolTable.add( new Token( RELOP , REGEXP , "REGEXP" ) );
		symbolTable.add( new Token( RELOP , EQUALS , "=" ) );
		symbolTable.add( new Token( RELOP , LTHAN , "<" ) );
		symbolTable.add( new Token( RELOP , GTHAN , ">" ) );
		symbolTable.add( new Token( RELOP , LTEQTO , "<=" ) );
		symbolTable.add( new Token( RELOP, GTEQTO , ">=" ) );
		
		symbolTable.add( new Token( LOGOP , AND , "AND" ) );
		symbolTable.add( new Token( LOGOP , OR , "OR" ) );
		symbolTable.add( new Token( LOGOP , NOT , "NOT" ) );
		
		symbolTable.add( new Token( KEYWORD , SELECT , "SELECT" ) );
		symbolTable.add( new Token( KEYWORD , WHERE , "WHERE" ) );
		symbolTable.add( new Token( KEYWORD , ORDER , "ORDER" ) );
		symbolTable.add( new Token( KEYWORD , BY , "BY" ) );
		symbolTable.add( new Token( KEYWORD , ASC , "ASC" ) );
		symbolTable.add( new Token( KEYWORD , DESC , "DESC" ) );
		
		symbolTable.add( new Token( SYMBOL , LBRACE , "(" ) );
		symbolTable.add( new Token( SYMBOL , RBRACE , ")" ) );
		
		symbolTable.add( new Token( TYPE , TRACK , "Track" ) );
		symbolTable.add( new Token( TYPE , ATTRIBUTE , "Attribute" ) );
		
		symbolTable.add( new Token( OPERATOR , COMMA , "," ) );
		
		// Populate the symbol table with the allowed Attributes
		Set attributes = TrackStoreFactory.getTrackStore().getAttributes();
		Iterator i = attributes.iterator();
		int val = 1000;
		while ( i.hasNext() )
		{
			symbolTable.add( new Token( ATTRIBUTE , val++ , ((Attribute)i.next()).getName() ) );
		}
		
		return symbolTable;
	}
	
	/**
	 * Returns a <code>Set</code> of <code>String</code> objects representing
	 * the set of attribute names present in the query.
	 * 
	 * @return A <code>Set</code> of <code>String</code> objects
	 */
	public Set getAttributes()
	{
		return new HashSet( this.attributes );
	}

	/**
	 * Get a List of Strings representing the attributes present in the SELECT statement of the query.
	 *
	 * @return A List of the names of the Attributes present.
	 */
	public List getSelectAttributes()
	{
		return new LinkedList( this.selectAttributes );
	}
}
