// $Id: IdentNode.java,v 1.1 2005/07/12 20:27:16 steveebersole Exp $
package org.hibernate.hql.ast.tree;

import java.util.List;

import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.hql.antlr.SqlTokenTypes;
import org.hibernate.hql.ast.util.ColumnHelper;
import org.hibernate.persister.collection.QueryableCollection;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.sql.JoinFragment;
import org.hibernate.type.CollectionType;
import org.hibernate.type.Type;
import org.hibernate.util.StringHelper;

import antlr.SemanticException;
import antlr.collections.AST;

/**
 * Represents an identifier all by itself, which may be a function name,
 * a class alias, or a form of naked property-ref depending on the
 * context.
 *
 * @author josh Aug 16, 2004 7:20:55 AM
 */
public class IdentNode extends FromReferenceNode implements SelectExpression {

	private static final int UNKNOWN = 0;
	private static final int PROPERTY_REF = 1;
	private static final int COMPONENT_REF = 2;

	private boolean nakedPropertyRef = false;
	private boolean root;
	
	public void setRoot() {
		root = true;
	}

	public void resolveIndex(AST parent) throws SemanticException {
		// An ident node can represent an index expression if the ident
		// represents a naked property ref
		//      *Note: this makes the assumption (which is currently the case
		//      in the hql-sql grammar) that the ident is first resolved
		//      itself (addrExpr -> resolve()).  The other option, if that
		//      changes, is to call resolve from here; but it is
		//      currently un-needed overhead.
		if ( !( isResolved() && nakedPropertyRef ) ) {
			throw new UnsupportedOperationException();
		}

		String propertyName = getOriginalText();
		if ( !getDataType().isCollectionType() ) {
			throw new SemanticException( "Collection expected; [" + propertyName + "] does not refer to a collection property" );
		}

		// TODO : most of below was taken verbatim from DotNode; should either delegate this logic or super-type it
		CollectionType type = ( CollectionType ) getDataType();
		String role = type.getRole();
		QueryableCollection queryableCollection = getSessionFactoryHelper().requireQueryableCollection( role );

		String alias = null;  // DotNode uses null here...
		String columnTableAlias = getFromElement().getTableAlias();
		int joinType = JoinFragment.INNER_JOIN;
		boolean fetch = false;

		FromElementFactory factory = new FromElementFactory(
		        getWalker().getCurrentFromClause(),
		        getFromElement(),
		        propertyName,
				alias,
		        getFromElement().toColumns( columnTableAlias, propertyName, false ),
		        true
		);
		FromElement elem = factory.createCollection( queryableCollection, role, joinType, fetch, true );
		setFromElement( elem );
		getWalker().addQuerySpaces( queryableCollection.getCollectionSpaces() );	// Always add the collection's query spaces.
	}

	public void resolve(boolean generateJoin, boolean implicitJoin, String classAlias, AST parent) {
		if ( !isResolved() ) {
			if ( getWalker().getCurrentFromClause().isFromElementAlias( getText() ) ) {
				if ( resolveAsAlias() ) {
					setResolved();
					// We represent a from-clause alias
				}
			}
			else if ( parent != null && parent.getType() == SqlTokenTypes.DOT ) {
				DotNode dot = ( DotNode ) parent;
				if ( parent.getFirstChild() == this ) {
					if ( resolveAsNakedComponentPropertyRefLHS( dot ) ) {
						// we are the LHS of the DOT representing a naked comp-prop-ref
						setResolved();
					}
				}
				else {
					if ( resolveAsNakedComponentPropertyRefRHS( dot ) ) {
						// we are the RHS of the DOT representing a naked comp-prop-ref
						setResolved();
					}
				}
			}
			else {
				int result = resolveAsNakedPropertyRef();
				if ( result == PROPERTY_REF ) {
					// we represent a naked (simple) prop-ref
					setResolved();
				}
				else if ( result == COMPONENT_REF ) {
					// EARLY EXIT!!!  return so the resolve call explicitly coming from DotNode can
					// resolve this...
					return;
				}
			}

			// if we are still not resolved, we might represent a constant.
			//      needed to add this here because the allowance of
			//      naked-prop-refs in the grammar collides with the
			//      definition of literals/constants ("nondeterminism").
			//      TODO: cleanup the grammar so that "processConstants" is always just handled from here
			if ( !isResolved() ) {
				// Avoid recursion from LiteralProcessor...
				if ( !getWalker().getCurrentFromClause().isFromElementAlias( getText() ) ) {
					try {
						getWalker().getLiteralProcessor().processConstant( this );
					}
					catch( Throwable ignore ) {
						// just ignore it for now, it'll get resolved later...
					}
				}
			}
		}
	}

	private boolean resolveAsAlias() {
		// This is not actually a constant, but a reference to FROM element.
		FromElement element = getWalker().getCurrentFromClause().getFromElement( getText() );
		if ( element != null ) {
			setFromElement( element );
			setText( element.getIdentityColumn() );
			setType( SqlTokenTypes.ALIAS_REF );
			return true;
		}
		return false;
	}

	private int resolveAsNakedPropertyRef() {
		FromElement fromElement = locateSingleFromElement();
		if ( fromElement == null ) {
			return UNKNOWN;
		}
		Queryable persister = fromElement.getQueryable();
		if ( persister == null ) {
			return UNKNOWN;
		}

		String property = getText();
		Type propertyType = null;
		try {
			propertyType = fromElement.getPropertyType( getText(), property );
		}
		catch( Throwable t ) {
			// assume this ident's text does *not* refer to a property on the given persister
			return UNKNOWN;
		}

		if ( propertyType.isComponentType() && !root ) {
			return COMPONENT_REF;
		}

		setFromElement( fromElement );

		if ( persister != null ) {
			String[] columns = getWalker().isSelectStatement()
					? persister.toColumns( fromElement.getTableAlias(), property )
					: persister.toColumns( property );
			String text = StringHelper.join( ", ", columns );
			setText( columns.length==1 ? text : "(" + text + ")" );
			setType( SqlTokenTypes.SQL_TOKEN );
		}

		// these pieces are needed for usage in select clause
		super.setDataType( propertyType );
		nakedPropertyRef = true;

		return PROPERTY_REF;
	}

	private boolean resolveAsNakedComponentPropertyRefLHS(DotNode parent) {
		FromElement fromElement = locateSingleFromElement();
		if ( fromElement == null ) {
			return false;
		}

		String propertyPath = getText() + "." + getNextSibling().getText();
		Type propertyType = null;  // used to inject the dot node
		try {
			// check to see if our "propPath" actually
			// represents a property on the persister
			propertyType = fromElement.getPropertyType( getText(), propertyPath );
		}
		catch( Throwable t ) {
			// assume we do *not* refer to a property on the given persister
			return false;
		}

		setFromElement( fromElement );
		parent.setPropertyPath( propertyPath );
		parent.setDataType( propertyType );

		return true;
	}

	private boolean resolveAsNakedComponentPropertyRefRHS(DotNode parent) {
		FromElement fromElement = locateSingleFromElement();
		if ( fromElement == null ) {
			return false;
		}

		Type propertyType = null;
		String propertyPath = parent.getLhs().getText() + "." + getText();
		try {
			// check to see if our "propPath" actually
			// represents a property on the persister
			propertyType = fromElement.getPropertyType( getText(), propertyPath );
		}
		catch( Throwable t ) {
			// assume we do *not* refer to a property on the given persister
			return false;
		}

		setFromElement( fromElement );

		// this piece is needed for usage in select clause
		super.setDataType( propertyType );
		nakedPropertyRef = true;

		return true;
	}

	private FromElement locateSingleFromElement() {
		List fromElements = getWalker().getCurrentFromClause().getFromElements();
		if ( fromElements == null || fromElements.size() != 1 ) {
			// TODO : should this be an error?
			return null;
		}
		FromElement element = ( FromElement ) fromElements.get(0);
		if ( element.getClassAlias() != null ) {
			// naked property-refs cannot be used with an aliased from element
			return null;
		}
		return element;
	}
	
	public Type getDataType() {
		Type type = super.getDataType();
		if (type!=null) return type;
		FromElement fe = getFromElement();
		if (fe!=null) return fe.getDataType();
		SQLFunction sf = getWalker().getSessionFactoryHelper().findSQLFunction( getText() );
		return sf==null ? null : sf.getReturnType(null, null);
	}

	public void setScalarColumnText(int i) throws SemanticException {
		if ( nakedPropertyRef ) {
			// do *not* over-write the column text, as that has already been
			// "rendered" during resolve
			ColumnHelper.generateSingleScalarColumn( this, i );
		}
		else {
			FromElement fe = getFromElement();
			if (fe!=null) {
				setText( fe.renderScalarIdentifierSelect( i ) );
			}
			else {
				ColumnHelper.generateSingleScalarColumn( this, i );
			}
		}
	}

	public String getDisplayText() {
		StringBuffer buf = new StringBuffer();

		if ( getType() == SqlTokenTypes.ALIAS_REF ) {
			buf.append( "{alias=" ).append( getOriginalText() );
			if ( getFromElement() == null ) {
				buf.append( ", no from element" );
			}
			else {
				buf.append( ", className=" ).append( getFromElement().getClassName() );
				buf.append( ", tableAlias=" ).append( getFromElement().getTableAlias() );
			}
			buf.append( "}" );
		}
		else {
			buf.append( "{originalText=" + getOriginalText() ).append( "}" );
		}
		return buf.toString();
	}

}
