/*
 * 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.olingo.service;

import static org.teiid.language.SQLConstants.Reserved.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Stack;

import org.apache.olingo.commons.api.edm.EdmEntityType;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.core.edm.primitivetype.SingletonPrimitiveType;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.uri.*;
import org.apache.olingo.server.api.uri.queryoption.expression.*;
import org.apache.olingo.server.core.RequestURLHierarchyVisitor;
import org.teiid.core.TeiidException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.types.DataTypeManager;
import org.teiid.core.types.JDBCSQLTypeInfo;
import org.teiid.core.util.Assertion;
import org.teiid.metadata.Column;
import org.teiid.metadata.ForeignKey;
import org.teiid.metadata.MetadataStore;
import org.teiid.odata.api.SQLParameter;
import org.teiid.olingo.ODataExpressionVisitor;
import org.teiid.olingo.ODataPlugin;
import org.teiid.olingo.ProjectedColumn;
import org.teiid.olingo.common.ODataTypeManager;
import org.teiid.olingo.service.ODataSQLBuilder.URLParseService;
import org.teiid.olingo.service.TeiidServiceHandler.UniqueNameGenerator;
import org.teiid.query.sql.lang.*;
import org.teiid.query.sql.symbol.AggregateSymbol;
import org.teiid.query.sql.symbol.AliasSymbol;
import org.teiid.query.sql.symbol.CaseExpression;
import org.teiid.query.sql.symbol.Constant;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Function;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.query.sql.symbol.Reference;
import org.teiid.query.sql.symbol.ScalarSubquery;
import org.teiid.translator.SourceSystemFunctions;

public class ODataExpressionToSQLVisitor extends RequestURLHierarchyVisitor implements  ODataExpressionVisitor{
    private final Stack<org.teiid.query.sql.symbol.Expression> stack = new Stack<org.teiid.query.sql.symbol.Expression>();
    private List<SQLParameter> params;
    private boolean prepared = false;
    private final UriInfo uriInfo;
    private MetadataStore metadata;
    private DocumentNode ctxQuery;
    private DocumentNode ctxExpression;
    private DocumentNode ctxLambda;
    private UniqueNameGenerator nameGenerator;
    private URLParseService parseService;
    private String lastPropertyType;
    private OData odata;
    private boolean root;
    
    public ODataExpressionToSQLVisitor(DocumentNode resource,
            boolean prepared, UriInfo info, MetadataStore metadata, OData odata,
            UniqueNameGenerator nameGenerator, 
            List<SQLParameter> params, URLParseService parseService) {
        this.ctxQuery = resource;
        this.prepared = prepared;
        this.uriInfo = info;
        this.metadata = metadata;
        this.nameGenerator = nameGenerator;
        this.params = params;
        this.parseService = parseService;
        this.ctxExpression = this.ctxQuery;
        this.odata = odata;
    }

    public org.teiid.query.sql.symbol.Expression getExpression(Expression expr) throws TeiidException {
    	try {
    		accept(expr);
    	} catch (TeiidRuntimeException e) {
    		if (e.getCause() instanceof TeiidException) {
    			throw (TeiidException)e.getCause();
    		}
    		throw e;
    	}
        return this.stack.pop();
    }
    
    public org.teiid.query.sql.symbol.Expression getExpression(UriInfoResource info) throws TeiidException {
        try {
        	visit(info);
    	} catch (TeiidRuntimeException e) {
    		if (e.getCause() instanceof TeiidException) {
    			throw (TeiidException)e.getCause();
    		}
    		throw e;
    	}
        return this.stack.pop();
    }
    
    public DocumentNode getEntityResource() {
        return this.ctxQuery;
    }

    @Override
    public void visit(Alias expr) {
        String strValue = this.uriInfo.getValueForAlias(expr.getParameterName());
        try {
            if (strValue == null) {
                this.stack.add(new Constant(null));
            }
            else if (strValue.startsWith("$root")) {
                this.stack.add(new ScalarSubquery(this.parseService.parse(strValue)));
            }
            else {
                String type = "Edm.String";
                if (this.lastPropertyType != null) {
                    EdmPrimitiveTypeKind kind = ODataTypeManager.odataType(this.lastPropertyType);
                    type = kind.getFullQualifiedName().getFullQualifiedNameAsString();
                    this.lastPropertyType = null;
                }
                Object value = ODataTypeManager.parseLiteral(type, strValue);
                if (this.prepared) {
                    stack.add(new Reference(this.params.size()));
                    this.params.add(new SQLParameter(value, JDBCSQLTypeInfo.getSQLTypeFromClass(value.getClass().getName())));
                } else {
                    this.stack.add(new Constant(value));
                }
            }
        } catch (TeiidException e) {
            throw new TeiidRuntimeException(e);
        }
        
    }

    @Override
    public void visit(Binary expr) {
        accept(expr.getLeftOperand());
        org.teiid.query.sql.symbol.Expression lhs = this.stack.pop();
        
        accept(expr.getRightOperand());
        org.teiid.query.sql.symbol.Expression rhs = this.stack.pop();

        org.teiid.query.sql.symbol.Expression binaryExpr = null;
        switch (expr.getOperator()) {
        case HAS:
            // TODO: not supported. What would be SQL equivalent?
            throw new TeiidRuntimeException(new TeiidNotImplementedException(
                    ODataPlugin.Event.TEIID16036,
                    ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16036)));
        case MUL:
            binaryExpr = new Function("*", new org.teiid.query.sql.symbol.Expression[] { lhs, rhs }); //$NON-NLS-1$
            break;
        case DIV:
            binaryExpr  = new Function("/", new org.teiid.query.sql.symbol.Expression[] { lhs, rhs }); //$NON-NLS-1$
            break;
        case MOD:
            binaryExpr  = new Function("MOD", new org.teiid.query.sql.symbol.Expression[] { lhs, rhs }); //$NON-NLS-1$
            break;
        case ADD:
            binaryExpr  = new Function("+", new org.teiid.query.sql.symbol.Expression[] { lhs, rhs }); //$NON-NLS-1$
            break;
        case SUB:
            binaryExpr  = new Function("-", new org.teiid.query.sql.symbol.Expression[] { lhs, rhs }); //$NON-NLS-1$
            break;
        case GT:
            binaryExpr  = new CompareCriteria(lhs, CompareCriteria.GT, rhs);
            break;
        case GE:
            binaryExpr  = new CompareCriteria(lhs, CompareCriteria.GE, rhs);
            break;
        case LT:
            binaryExpr  = new CompareCriteria(lhs, CompareCriteria.LT, rhs);
            break;
        case LE:
            binaryExpr  = new CompareCriteria(lhs, CompareCriteria.LE, rhs);
            break;
        case EQ:
            if (rhs instanceof Constant && ((Constant) rhs).getType() == DataTypeManager.DefaultDataClasses.NULL) {
                binaryExpr  = new IsNullCriteria(lhs);
            } else {
                binaryExpr  = new CompareCriteria(lhs, CompareCriteria.EQ, rhs);
            }
            break;
        case NE:
            if (rhs instanceof Constant && ((Constant) rhs).getType() == DataTypeManager.DefaultDataClasses.NULL) {
                IsNullCriteria crit = new IsNullCriteria(lhs);
                crit.setNegated(true);
                binaryExpr  = crit;
            } else {
                binaryExpr  = new CompareCriteria(lhs, CompareCriteria.NE, rhs);
            }
            break;
        case AND:
            binaryExpr  = new CompoundCriteria(CompoundCriteria.AND, (Criteria) lhs, (Criteria) rhs);
            break;
        case OR:
            binaryExpr  = new CompoundCriteria(CompoundCriteria.OR, (Criteria) lhs, (Criteria) rhs);
            break;
        }
        this.stack.push(binaryExpr);
    }

    @Override
    public void visit(Enumeration expr) {
    	throw new TeiidRuntimeException(new TeiidException("unsupported option"));//$NON-NLS-1$
    }

    @Override
    public void visit(LambdaRef expr) {
        // TODO: any and All implementations
    }

    @Override
    public void visit(Literal expr) {
        try {
            Object value = null;
            if (expr.getText() != null && !expr.getText().equalsIgnoreCase("null")) {
                String type = expr.getType().getFullQualifiedName().getFullQualifiedNameAsString();
                value = ODataTypeManager.parseLiteral(type, expr.getText());
            }
            if (this.prepared) {                
                if (value == null) {
                    this.stack.add(new Constant(value));                    
                } else {
                    Function ref = new Function(
                            CONVERT,
                            new org.teiid.query.sql.symbol.Expression[] {
                                    new Reference(this.params.size()),
                                    new Constant(DataTypeManager.getDataTypeName(value.getClass())) });
                    stack.add(ref);
                    this.params.add(new SQLParameter(value, 
                            JDBCSQLTypeInfo.getSQLTypeFromClass(value.getClass().getName())));
                }
            } else {
                this.stack.add(new Constant(value));
            }
        } catch (TeiidException e) {
        	throw new TeiidRuntimeException(e);
        }
    }

    @Override
    public void visit(Member expr) {
        // this is seg way into RequestURLHierarchyVisitor based parsing
        visit(expr.getResourcePath());
    }

    private org.teiid.query.sql.symbol.Expression addOne(
            org.teiid.query.sql.symbol.Expression expr) {
                
        org.teiid.query.sql.symbol.Expression when = new CompareCriteria(expr,
                CompareCriteria.LT, new Constant(0));
        CaseExpression caseExpr = new CaseExpression(expr, Arrays.asList(when),
                Arrays.asList(expr));
        caseExpr.setElseExpression(new Function("+",
                new org.teiid.query.sql.symbol.Expression[] { expr, new Constant(1) }));
        return caseExpr;
    }

    private org.teiid.query.sql.symbol.Expression minusOne(
            org.teiid.query.sql.symbol.Expression expr) {
        return new Function("-", new org.teiid.query.sql.symbol.Expression[] {
                expr, new Constant(1) });        
    }
    
    @Override
    public void visit(Method expr) {
        List<org.teiid.query.sql.symbol.Expression> teiidExprs = new ArrayList<org.teiid.query.sql.symbol.Expression>();
        for (Expression exp : expr.getParameters()) {
            accept(exp);
            teiidExprs.add(this.stack.pop());
        }

        switch (expr.getMethod()) {
        case CONTAINS:
            CompareCriteria criteria = new CompareCriteria(new Function("LOCATE", 
                    new org.teiid.query.sql.symbol.Expression[] {
                        teiidExprs.get(1), teiidExprs.get(0), new Constant(1) }), 
                        CompareCriteria.GE, new Constant(1)); //$NON-NLS-1$
            this.stack.push(criteria);
            break;
        case STARTSWITH:
            criteria = new CompareCriteria(new Function("LOCATE", 
                    new org.teiid.query.sql.symbol.Expression[] {
                    teiidExprs.get(1), teiidExprs.get(0), new Constant(1) }), 
                    CompareCriteria.EQ, new Constant(1)); //$NON-NLS-1$
            this.stack.push(criteria);
            break;
        case ENDSWITH:
            criteria = new CompareCriteria(new Function("ENDSWITH", 
                    new org.teiid.query.sql.symbol.Expression[] {teiidExprs.get(1), teiidExprs.get(0) }), 
                    CompareCriteria.EQ, new Constant(Boolean.TRUE));//$NON-NLS-1$
            this.stack.push(criteria);
            break;
        case LENGTH:
            this.stack.push(new Function("LENGTH", 
                    new org.teiid.query.sql.symbol.Expression[] { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case INDEXOF:
            stack.push(minusOne(new Function("LOCATE", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(1), teiidExprs.get(0)}))); //$NON-NLS-1$
            break;
        case SUBSTRING:
            org.teiid.query.sql.symbol.Expression[] exprs = 
                teiidExprs.toArray(new org.teiid.query.sql.symbol.Expression[teiidExprs.size()]);
                exprs[1] = addOne(exprs[1]);
            this.stack.push(new Function("SUBSTRING", exprs)); //$NON-NLS-1$
            break;
        case TOLOWER:
            this.stack.push(new Function("LCASE", 
                    new org.teiid.query.sql.symbol.Expression[] { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case TOUPPER:
            this.stack.push(new Function("UCASE", 
                    new org.teiid.query.sql.symbol.Expression[] { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case TRIM:
            this.stack.push(new Function("TRIM", 
                    new org.teiid.query.sql.symbol.Expression[] { new Constant("BOTH"), 
                    new Constant(' '), teiidExprs.get(0) })); //$NON-NLS-1$ //$NON-NLS-2$
            break;
        case CONCAT:
            this.stack.push(new Function("CONCAT2", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0), teiidExprs.get(1) })); //$NON-NLS-1$
            break;
        case YEAR:
            this.stack.push(new Function("YEAR", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case MONTH:
            this.stack.push(new Function("MONTH", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case DAY:
            this.stack.push(new Function("DAYOFMONTH", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case HOUR:
            this.stack.push(new Function("HOUR", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case MINUTE:
            this.stack.push(new Function("MINUTE", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case SECOND:
            this.stack.push(new Function("SECOND", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case NOW:
            this.stack.push(new Function("NOW", new org.teiid.query.sql.symbol.Expression[] {})); //$NON-NLS-1$
            break;
        case ROUND:
            stack.push(new Function("ROUND", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0), new Constant(0) })); //$NON-NLS-1$
            break;
        case FLOOR:
            this.stack.push(new Function("FLOOR", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case CEILING:
            this.stack.push(new Function("CEILING", new org.teiid.query.sql.symbol.Expression[] 
                    { teiidExprs.get(0) })); //$NON-NLS-1$
            break;
        case CAST:
            this.stack.push(new Function(CONVERT,new org.teiid.query.sql.symbol.Expression[] 
                    {teiidExprs.get(0), teiidExprs.get(1) }));
            break;
            
        case DATE:
            this.stack.push(new Function(CONVERT,new org.teiid.query.sql.symbol.Expression[] 
                    {teiidExprs.get(0),  new Constant(DataTypeManager.DefaultDataTypes.DATE)}));            
            break;
        case TIME:
            this.stack.push(new Function(CONVERT,new org.teiid.query.sql.symbol.Expression[] 
                    {teiidExprs.get(0),  new Constant(DataTypeManager.DefaultDataTypes.TIME)}));            
            break;
        case FRACTIONALSECONDS:
        case TOTALSECONDS:
        case TOTALOFFSETMINUTES:
        case MINDATETIME:
        case MAXDATETIME:
        case GEODISTANCE:
        case GEOLENGTH:
        case GEOINTERSECTS:
        case ISOF:
        default:
            throw new TeiidRuntimeException(new TeiidNotImplementedException(
                    ODataPlugin.Event.TEIID16027, 
                    ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16027, expr.getMethod())));
        }
    }

    @Override
    public void visit(TypeLiteral expr) {
        this.stack.push(new Constant(ODataTypeManager.teiidType((SingletonPrimitiveType) expr.getType(), false)));
    }

    @Override
    public void visit(Unary expr) {
        accept(expr.getOperand());
        org.teiid.query.sql.symbol.Expression teiidExpr = this.stack.pop();
        switch (expr.getOperator()) {
        case MINUS:
            this.stack.push(new Function(SourceSystemFunctions.MULTIPLY_OP,
                    new org.teiid.query.sql.symbol.Expression[] {new Constant(-1), teiidExpr }));
            break;
        case NOT:
            this.stack.push(new NotCriteria(new ExpressionCriteria(teiidExpr)));
            break;
        }
    }
    
    private void accept(Expression expr) {
        if (expr instanceof Alias) {
            visit((Alias) expr);
        } else if (expr instanceof Binary) {
            visit((Binary) expr);
        } else if (expr instanceof Enumeration) {
            visit((Enumeration) expr);
        } else if (expr instanceof LambdaRef) {
            visit((LambdaRef) expr);
        } else if (expr instanceof Literal) {
            visit((Literal) expr);
        } else if (expr instanceof Member) {
            visit((Member) expr);
        } else if (expr instanceof Method) {
            visit((Method) expr);
        } else if (expr instanceof TypeLiteral) {
            visit((TypeLiteral) expr);
        } else if (expr instanceof Unary) {
            visit((Unary) expr);
        }
    }
    
    /////////////////////////////////////////////////////////////////////////
    //RequestURLHierarchyVisitor specific methods
    /////////////////////////////////////////////////////////////////////////
    @Override
    public void visit(UriResourcePrimitiveProperty info) {
        if (this.root) {
            this.stack.add(new ScalarSubquery(buildRootSubQuery(info.getProperty().getName(), this.ctxExpression)));
            root = false;
        }
        else {
            this.stack.add(new ElementSymbol(info.getProperty().getName(), this.ctxExpression.getGroupSymbol()));
        }
        // hack to resolve the property type.
        Column c = this.ctxExpression.getColumnByName(info.getProperty().getName());
        this.lastPropertyType = c.getRuntimeType();
        //revert back to the query context
        this.ctxExpression = this.ctxQuery;
    }
    
    @Override
    public void visit(UriResourceCount option) {
    }
    
    @Override
    public void visit(UriResourceNavigation info) {
        try {
            DocumentNode navigationResource = DocumentNode.build(
                    (EdmEntityType) info.getType(), info.getKeyPredicates(), this.metadata, this.odata,
                    this.nameGenerator, true, getUriInfo(), this.parseService);

            Query query = new Query();
            query.setSelect(new Select(Arrays.asList(new AggregateSymbol(AggregateSymbol.Type.COUNT.name(), false, null))));
            query.setFrom(new From(Arrays.asList(navigationResource.getFromClause())));

            Criteria criteria = null;
            ForeignKey fk = null;
            if (info.isCollection()) {
                fk = DocumentNode.joinFK(navigationResource, this.ctxQuery);    
            }
            else {
                fk = DocumentNode.joinFK(this.ctxQuery, navigationResource);
            }
            
            if (fk != null) {
                List<String> lhsColumns = DocumentNode.getColumnNames(fk.getColumns());
                List<String> rhsColumns = fk.getReferenceColumns();
                GroupSymbol leftSymbol = this.ctxQuery.getGroupSymbol();
                GroupSymbol rightSymbol = navigationResource.getGroupSymbol();
                if (info.isCollection()) {
                    leftSymbol = navigationResource.getGroupSymbol();
                    rightSymbol = this.ctxQuery.getGroupSymbol();                    
                }
                for (int i = 0; i < lhsColumns.size(); i++) {
                    if (criteria == null) {
                        criteria = new CompareCriteria(new ElementSymbol(lhsColumns.get(i), leftSymbol),
                                CompareCriteria.EQ, new ElementSymbol(rhsColumns.get(i), rightSymbol));
                    } else {
                        Criteria subcriteria = new CompareCriteria(new ElementSymbol(lhsColumns.get(i), leftSymbol),
                                CompareCriteria.EQ, new ElementSymbol(rhsColumns.get(i), rightSymbol));
                        criteria = new CompoundCriteria(CompoundCriteria.AND, criteria, subcriteria);
                    }
                }
            }
            else {
                throw new TeiidException(ODataPlugin.Event.TEIID16037, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16037));
            }            
            query.setCriteria(criteria);
            this.stack.add(new ScalarSubquery(query));
        } catch (TeiidException e) {
        	throw new TeiidRuntimeException(e);
        }
    }
    
    @Override
    public void visit(UriResourceLambdaAll all) {
        accept(all.getExpression());
        if (this.ctxLambda != null) {
            org.teiid.query.sql.symbol.Expression predicate = this.stack.pop();
                predicate = new SubqueryCompareCriteria(new Constant(true), buildSubquery(
                        this.ctxLambda, predicate), CompareCriteria.EQ,
                        SubqueryCompareCriteria.ALL);
                this.stack.push(predicate);
        }
        this.ctxLambda = null;
    }

    @Override
    public void visit(UriResourceLambdaAny any) {
        accept(any.getExpression());
        if (this.ctxLambda != null) {
            org.teiid.query.sql.symbol.Expression predicate = this.stack.pop();
            Query q = buildSubquery(this.ctxLambda, new Constant(1));
            Criteria crit = null;
            if (predicate instanceof Criteria) {
                crit = (Criteria)predicate;
            } else {
                crit = new ExpressionCriteria(predicate);
            }
            q.setCriteria(Criteria.combineCriteria(q.getCriteria(), crit));
            predicate = new ExistsCriteria(q);
            this.stack.push(predicate);
        }
        this.ctxLambda = null;
    }

    @Override
    public void visit(UriResourceLambdaVariable resource) {
        try {
            if (this.ctxLambda == null) {
                DocumentNode lambda = DocumentNode.build(
                        (EdmEntityType) resource.getType(), null, this.metadata, this.odata,
                        this.nameGenerator, false, this.uriInfo,
                        this.parseService);
                lambda.setGroupSymbol(new GroupSymbol(resource.getVariableName(), lambda.getFullName()));
                
                this.ctxLambda = lambda;
            }
            
            this.ctxExpression = ctxLambda;
        } catch (TeiidException e) {
        	throw new TeiidRuntimeException(e);
        }
    }    
    
    private Query buildSubquery(DocumentNode eResource,
            org.teiid.query.sql.symbol.Expression projected) {
        Select s1 = new Select();
        s1.addSymbol(projected); 
        
        Query q = new Query();
        From from = new From();
        from.addGroup(eResource.getGroupSymbol());
        q.setFrom(from);    
        q.setCriteria(DocumentNode.buildJoinCriteria(eResource, this.ctxQuery));
        
        q.setSelect(s1);
        return q;
    }
    
    @Override
    public void visit(UriResourceIt info) {
        if (info.getType() instanceof SingletonPrimitiveType) {
        	org.teiid.query.sql.symbol.Expression ex = null;
        	if (this.ctxQuery.getIterator() == null) {
                String group = this.nameGenerator.getNextGroup();
                GroupSymbol groupSymbol = new GroupSymbol(group);

                StoredProcedure procedure = new StoredProcedure();
                procedure.setProcedureName("arrayiterate");
                
                //the projected should only be the collection property at this point
                //we may need more checks here to ensure that is valid
                Collection<ProjectedColumn> values = this.ctxQuery.getProjectedColumns().values();
                Assertion.assertTrue(values.size() == 1);
				ProjectedColumn projectedColumn = values.iterator().next();
				ElementSymbol projectedEs = (ElementSymbol)projectedColumn.getExpression();
                List<SPParameter> params = new ArrayList<SPParameter>();
                SPParameter param = new SPParameter(1, SPParameter.IN, "val");
                param.setExpression(projectedEs);
                params.add(param);
                
                procedure.setParameter(param);
                
                SubqueryFromClause fromClause = new SubqueryFromClause(group, procedure);
                fromClause.setLateral(true);
            
                ElementSymbol es = new ElementSymbol("col", groupSymbol);
                String type = ODataTypeManager.teiidType((SingletonPrimitiveType)info.getType(), false);
                Function castFunction = new Function(CAST,new org.teiid.query.sql.symbol.Expression[] {es, new Constant(type)});            

                DocumentNode itResource = new DocumentNode();
                org.teiid.query.sql.symbol.Expression clone = (org.teiid.query.sql.symbol.Expression) castFunction.clone();
                AggregateSymbol symbol = new AggregateSymbol(AggregateSymbol.Type.ARRAY_AGG.name(), false, clone);
				AliasSymbol expression = new AliasSymbol(projectedEs.getShortName(), symbol);
                
                itResource.setFromClause(fromClause);
                itResource.setGroupSymbol(groupSymbol);            
                itResource.addProjectedColumn(expression, info.getType(), projectedColumn.getProperty(), true);

                this.ctxQuery.getProjectedColumns().remove(projectedColumn.getExpression());
                this.ctxQuery.setIterator(itResource);
                
                ex = castFunction;
        	} else {
        		GroupSymbol groupSymbol = this.ctxQuery.getIterator().getGroupSymbol();
                ElementSymbol es = new ElementSymbol("col", groupSymbol);
                String type = ODataTypeManager.teiidType((SingletonPrimitiveType)info.getType(), false);
                ex = new Function(CAST,new org.teiid.query.sql.symbol.Expression[] {es, new Constant(type)});            
        	}
        	
            this.stack.push(ex);
        }
        else {
            boolean ex = true;
            if (this.ctxQuery instanceof ExpandDocumentNode) {
                ExpandDocumentNode node = (ExpandDocumentNode)this.ctxQuery;
                DocumentNode parent = node.getCollectionContext();
                if (parent != null) {
                    this.ctxExpression = parent;
                    ex = false;
                }
            }
            if (ex) {
                throw new TeiidRuntimeException(new TeiidNotImplementedException(
                        ODataPlugin.Event.TEIID16010,
                        ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16010)));
            }
        }
    }
    
    @Override
    public void visit(UriResourceRoot info) {
        this.root = true;
    }
    
    @Override
    public void visit(UriResourceEntitySet info) {
        EdmEntityType edmEntityType = info.getEntitySet().getEntityType();
        if (this.root) {
            try {
                this.ctxExpression = DocumentNode.build(edmEntityType,
                        info.getKeyPredicates(), this.metadata, this.odata, this.nameGenerator,
                        true, getUriInfo(), null);
            } catch (TeiidException e) {
            	throw new TeiidRuntimeException(e);
            }
        }
        else {
            if (this.ctxQuery.getEdmEntityType().getFullQualifiedName().equals(edmEntityType.getFullQualifiedName())) {
                this.ctxExpression = this.ctxQuery;
            }
            else {
                for (DocumentNode er: this.ctxQuery.getSibilings()) {
                    if (er.getEdmEntityType().getFullQualifiedName().equals(edmEntityType.getFullQualifiedName())) {
                        this.ctxExpression = er;
                        break;
                    }
                }
            }
        }
    }
    
    public QueryCommand buildRootSubQuery(String element, DocumentNode resource) {
        Select s1 = new Select();
        s1.addSymbol(new ElementSymbol(element, resource.getGroupSymbol())); 
        From f1 = new From();
        f1.addGroup(resource.getGroupSymbol());
        Query q1 = new Query();
        q1.setSelect(s1);
        q1.setFrom(f1);    
        q1.setCriteria(resource.getCriteria());  
        return q1;
    }
}
