/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.pig.newplan.logical.visitor;

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

import org.apache.pig.PigException;
import org.apache.pig.data.DataType;
import org.apache.pig.impl.logicalLayer.FrontendException;
import org.apache.pig.impl.logicalLayer.validators.TypeCheckerException;
import org.apache.pig.impl.plan.CompilationMessageCollector;
import org.apache.pig.impl.plan.VisitorException;
import org.apache.pig.impl.plan.CompilationMessageCollector.MessageType;
import org.apache.pig.impl.util.MultiMap;
import org.apache.pig.newplan.DependencyOrderWalker;
import org.apache.pig.newplan.Operator;
import org.apache.pig.newplan.OperatorPlan;
import org.apache.pig.newplan.logical.expression.CastExpression;
import org.apache.pig.newplan.logical.expression.LogicalExpression;
import org.apache.pig.newplan.logical.expression.LogicalExpressionPlan;
import org.apache.pig.newplan.logical.expression.ProjectExpression;
import org.apache.pig.newplan.logical.relational.LOCogroup;
import org.apache.pig.newplan.logical.relational.LOCross;
import org.apache.pig.newplan.logical.relational.LODistinct;
import org.apache.pig.newplan.logical.relational.LOFilter;
import org.apache.pig.newplan.logical.relational.LOForEach;
import org.apache.pig.newplan.logical.relational.LOGenerate;
import org.apache.pig.newplan.logical.relational.LOInnerLoad;
import org.apache.pig.newplan.logical.relational.LOJoin;
import org.apache.pig.newplan.logical.relational.LOLimit;
import org.apache.pig.newplan.logical.relational.LOLoad;
import org.apache.pig.newplan.logical.relational.LORank;
import org.apache.pig.newplan.logical.relational.LOSort;
import org.apache.pig.newplan.logical.relational.LOSplit;
import org.apache.pig.newplan.logical.relational.LOSplitOutput;
import org.apache.pig.newplan.logical.relational.LOStore;
import org.apache.pig.newplan.logical.relational.LOUnion;
import org.apache.pig.newplan.logical.relational.LogicalPlan;
import org.apache.pig.newplan.logical.relational.LogicalRelationalNodesVisitor;
import org.apache.pig.newplan.logical.relational.LogicalRelationalOperator;
import org.apache.pig.newplan.logical.relational.LogicalSchema;
import org.apache.pig.newplan.logical.relational.LogicalSchema.LogicalFieldSchema;

/*
All the getType() of these operators always return BAG.
We just have to :-
1) Check types of inputs, expression plans
2) Compute output schema with type information
   (At the moment, the parser does only return GetSchema with correct aliases)
3) Insert casting if necessary

 */
public class TypeCheckingRelVisitor extends LogicalRelationalNodesVisitor {

    private CompilationMessageCollector msgCollector;

    public TypeCheckingRelVisitor(OperatorPlan plan, CompilationMessageCollector msgCollector)
    throws FrontendException {
        super(plan, new DependencyOrderWalker(plan));
        this.msgCollector = msgCollector;

    }

    @Override
    public void visit(LOLoad load){
        // do nothing
    }

    @Override
    public void visit(LOStore store)
    throws FrontendException {
        store.resetSchema();
        store.getSchema();
    }

    /***
     * The schema of filter output will be the same as filter input
     * @throws FrontendException 
     */
    @Override
    public void visit(LOFilter filter) throws FrontendException {
        filter.resetSchema();
        LogicalExpressionPlan comparisonPlan = filter.getFilterPlan() ;

        // Check that the inner plan has only 1 output port
        if (comparisonPlan.getSources().size() > 1) {
            int errCode = 1057;
            String msg = "Filter's cond plan can only have one output" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(filter, msg, errCode, PigException.INPUT, null) ;
        }

        // visit the filter expression
        visitExpressionPlan(comparisonPlan, filter);


        //check filter expression type
        byte innerCondType = ((LogicalExpression)comparisonPlan.getSources().get(0)).getType();
        if (innerCondType != DataType.BOOLEAN) {
            int errCode = 1058;
            String msg = "Filter's condition must evaluate to boolean. Found: " + 
                         DataType.findTypeName(innerCondType);
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(filter, msg, errCode, PigException.INPUT, null) ;
        }       

        try {
            // re-compute the schema
            filter.resetSchema();
            filter.getSchema() ;
        } 
        catch (FrontendException fe) {
            int errCode = 1059;
            String msg = "Problem while reconciling output schema of Filter" ;
            msgCollector.collect(msg, MessageType.Error);
            throwTypeCheckerException(filter, msg, errCode, PigException.INPUT, fe) ;
        }
    }
    
    private void throwTypeCheckerException(Operator op, String msg,
            int errCode, byte input, FrontendException fe) throws TypeCheckerException {
        if( fe == null ) {
            throw new TypeCheckerException(op, msg, errCode, PigException.INPUT);
        }
        throw new TypeCheckerException(op, msg, errCode, PigException.INPUT, fe);
    }

	@Override
    public void visit(LOGenerate gen) throws FrontendException {
        for(int i=0; i < gen.getOutputPlans().size(); i++) {
            LogicalExpressionPlan expPlan = gen.getOutputPlans().get(i);
            // Check that the inner plan has only 1 output port
            if (expPlan.getSources().size() > 1) {
                int errCode = 1057;
                String msg = "LOGenerate expression plan can only have one output" ;
                msgCollector.collect(msg, MessageType.Error) ;
                throwTypeCheckerException( gen, msg, errCode, PigException.BUG, null) ;
            }
            // visit the filter expression
            visitExpressionPlan( expPlan, gen );

       }
        gen.resetSchema();
        gen.getSchema();
    }

    @Override
    public void visit(LOInnerLoad innerLoad) throws FrontendException{
        innerLoad.resetSchema();
        innerLoad.getSchema();
    }
    
    @Override
    public void visit(LOForEach forEach) throws FrontendException {
        try {
            // visit inner plan
            new TypeCheckingRelVisitor( forEach.getInnerPlan(), msgCollector ).visit();
            // re-compute the schema
            forEach.resetSchema();
            forEach.getSchema() ;
        }  catch (FrontendException fe) {
            int errCode = 1059;
            String msg = "Problem while reconciling output schema of ForEach" ;
            msgCollector.collect(msg, MessageType.Error);
            throwTypeCheckerException(forEach, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    private void visitExpressionPlan(LogicalExpressionPlan explPlan,
            LogicalRelationalOperator relOp)
    throws FrontendException {
        TypeCheckingExpVisitor expTypeCheck =
            new TypeCheckingExpVisitor(explPlan, msgCollector, relOp);
        expTypeCheck.visit();

    }

    /* (non-Javadoc)
     * @see org.apache.pig.newplan.logical.relational.LogicalRelationalNodesVisitor#visit(org.apache.pig.newplan.logical.relational.LOUnion)
     *     The output schema of LOUnion is the merge of all input schemas.
     *     Operands on left side always take precedance on aliases.
     *     We allow type promotion here
     */
    @Override
    public void visit(LOUnion u) throws FrontendException {
        u.resetSchema();
        // Have to make a copy, because as we insert operators, this list will
        // change under us.
        List<Operator> inputs = new ArrayList<Operator>(u.getInputs());

        // There is no point to union only one operand
        // it should be a problem in the parser
        if (inputs.size() < 2) {
            throw new AssertionError("Union with Count(Operand) < 2") ;
        }

        LogicalSchema schema = null ;
        try {
            // Compute the schema
            schema = u.getSchema() ;

        }
        catch (FrontendException fee) {
            int errCode = 1055;
            String msg = "Problem while reading schemas from inputs of Union" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(u, msg, errCode, PigException.INPUT, fee) ;
        }

        // Do cast insertion only if we are typed 
        // and if its not union-onschema. In case of union-onschema the
        // foreach with cast is added in UnionOnSchemaSetter
        if (schema != null && !u.isOnSchema()) {
            // Insert casting to inputs if necessary
            for (int i=0; i< inputs.size() ;i++) {
                LOForEach insertedOp
                = insertCastForEachInBetweenIfNecessary((LogicalRelationalOperator)inputs.get(i), u) ;

                // We may have to compute the schema of the input again
                // because we have just inserted
                if (insertedOp != null) {
                    if(insertedOp.getAlias()==null){
                        insertedOp.setAlias(((LogicalRelationalOperator)inputs.get(i)).getAlias());
                    }
                    try {
                        this.visit(insertedOp);
                    }
                    catch (FrontendException fee) {
                        int errCode = 1056;
                        String msg = "Problem while casting inputs of Union" ;
                        msgCollector.collect(msg, MessageType.Error) ;
                        throwTypeCheckerException(u, msg, errCode, PigException.INPUT, fee) ;
                    }
                }
            }
        }
        u.resetSchema();
        u.getSchema();
    }

    /***
     * For casting insertion for relational operators
     * only if it's necessary
     * Currently this only does "shallow" casting
     * @param fromOp
     * @param toOp
     * @return the inserted operator. null is no insertion
     * @throws FrontendException 
     */
    private LOForEach insertCastForEachInBetweenIfNecessary(
            LogicalRelationalOperator fromOp,
            LogicalRelationalOperator toOp)
    throws FrontendException {


        // Make sure that they are adjacent and the direction
        // is from "fromOp" to "toOp"
        List<Operator> preList = plan.getPredecessors(toOp) ;
        boolean found = false ;
        for(Operator tmpOp: preList) {
            // compare by reference
            if (tmpOp == fromOp) {
                found = true ;
                break ;
            }
        }

        if (!found) {
            int errCode = 1077;
            String msg = "Two operators that require a cast in between are not adjacent.";
            throwTypeCheckerException(fromOp, msg, errCode, PigException.INPUT, null);
        }

        // retrieve input schema to be casted
        // this will be used later
        LogicalSchema fromSchema = null ;
        LogicalSchema toSchema = null ;
        try {
            fromSchema = fromOp.getSchema() ;
            toSchema = toOp.getSchema();
        }
        catch(FrontendException fe) {
            int errCode = 1055;
            String msg = "Problem while reading schema from input of " 
                + fromOp.getClass().getSimpleName();
            throwTypeCheckerException(fromOp, msg, errCode, PigException.BUG, fe);
        }

        // make sure the supplied targetSchema has the same number of members
        // as number of output fields from "fromOp"
        if (fromSchema.size() != toSchema.size()) {
            int errCode = 1078;
            String msg = "Schema size mismatch for casting. Input schema size: " 
                + fromSchema.size() + ". Target schema size: " + toSchema.size();
            throwTypeCheckerException(toOp, msg, errCode, PigException.INPUT, null);
        }

        // Plans inside Generate. Fields that do not need casting will only
        // have Project. Fields that need casting will have Project + Cast
        ArrayList<LogicalExpressionPlan> generatePlans = new ArrayList<LogicalExpressionPlan>() ;
        LogicalPlan innerPlan = new LogicalPlan();

        // create LOGenerate for foreach
        LOGenerate loGen = new LOGenerate(innerPlan, generatePlans, 
                new boolean[toSchema.size()]);
        innerPlan.add(loGen);

        // Create ForEach to be inserted
        LOForEach foreach = new LOForEach(plan);
        foreach.setInnerPlan(innerPlan);


        int castNeededCounter = 0 ;
        for(int i=0;i < fromSchema.size(); i++) {

            LOInnerLoad innerLoad = new LOInnerLoad(innerPlan, foreach, i);
            innerPlan.add(innerLoad);
            innerPlan.connect(innerLoad, loGen);

            LogicalExpressionPlan genPlan = new LogicalExpressionPlan() ;
            ProjectExpression project = new ProjectExpression(genPlan, i, 0, loGen);
            genPlan.add(project);

            // add casting if necessary by comparing target types
            // to the input schema
            LogicalFieldSchema fs = null ;
            fs = fromSchema.getField(i) ;

            // This only does "shallow checking"

            LogicalFieldSchema outFieldSchema ;

            outFieldSchema = toSchema.getField(i) ;

            if (outFieldSchema.type != fs.type) {
                castNeededCounter++ ;
                CastExpression castexp = new CastExpression(genPlan, project, outFieldSchema);
                castexp.setLocation(toOp.getLocation());
            }

            generatePlans.add(genPlan) ;
        }

        // if we really need casting
        if (castNeededCounter > 0)  {
            // Flatten List
            // This is just cast insertion so we don't have any flatten
            ArrayList<Boolean> flattenList = new ArrayList<Boolean>() ;
            for(int i=0;i < toSchema.size(); i++) {
                flattenList.add(Boolean.valueOf(false)) ;
            }

            // Manipulate the plan structure
            plan.add(foreach);
            plan.insertBetween(fromOp, foreach, toOp);
            return foreach;

        }
        else {
            plan.remove(foreach);
            return null ;
        }
    }


    @Override
    public void visit(LOSplitOutput op) throws FrontendException {
        op.resetSchema();
        OperatorPlan lp = op.getPlan();
        // LOSplitOutput can only have 1 input
        List<Operator> list = lp.getPredecessors(op) ;
        if (list.size() != 1) {
            int errCode = 2008;
            String msg = "LOSplitOutput cannot have more than one input. Found: " + list.size() + " input(s).";
            throwTypeCheckerException(op, msg, errCode, PigException.BUG, null) ;
        }

        LogicalExpressionPlan condPlan = op.getFilterPlan() ;

        // Check that the inner plan has only 1 output port
        if (condPlan.getSources().size() != 1) {
            int errCode = 1057;
            String msg = "Split's inner plan can only have one output (leaf)" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(op, msg, errCode, PigException.INPUT, null) ;
        }

        visitExpressionPlan(condPlan, op);

        byte innerCondType = ((LogicalExpression)condPlan.getSources().get(0)).getType() ;
        if (innerCondType != DataType.BOOLEAN) {
            int errCode = 1058;
            String msg = "Split's condition must evaluate to boolean. Found: " + DataType.findTypeName(innerCondType) ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(op, msg, errCode, PigException.INPUT, null) ;
        }

        try {
            // Compute the schema
            op.getSchema() ;
        }
        catch (FrontendException fe) {
            int errCode = 1055;
            String msg = "Problem while reading"
                + " schemas from inputs of SplitOutput" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(op, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    /***
     *  LODistinct, output schema should be the same as input
     * @param op
     * @throws VisitorException
     */

    @Override
    public void visit(LODistinct op) throws VisitorException {
        op.resetSchema();

        try {
            // Compute the schema
            op.getSchema() ;
        }
        catch (FrontendException fe) {
            int errCode = 1055;
            String msg = "Problem while reading"
                + " schemas from inputs of Distinct" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(op, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    @Override
    public void visit(LOLimit limit) throws FrontendException {
        limit.resetSchema();
        LogicalExpressionPlan expressionPlan = limit.getLimitPlan();
        if (expressionPlan != null) {
            // Check that the inner plan has only 1 output port
            if (expressionPlan.getSources().size() > 1) {
                int errCode = 1057;
                String msg = "Limit's expression plan can only have one output";
                msgCollector.collect(msg, MessageType.Error);
                throwTypeCheckerException(limit, msg, errCode, PigException.INPUT, null);
            }

            // visit the limit expression
            visitExpressionPlan(expressionPlan, limit);

            // check limit expression type
            byte innerCondType = ((LogicalExpression) expressionPlan.getSources().get(0))
                    .getType();
            // cast to long if it is a bytearray
            if (innerCondType == DataType.BYTEARRAY)
                insertAtomicCastForInnerPlan(expressionPlan, limit, DataType.LONG);
            // else it must be an int or a long
            else if (innerCondType != DataType.LONG && innerCondType != DataType.INTEGER) {
                int errCode = 1058;
                String msg = "Limit's expression must evaluate to Long or Integer. Found: "
                        + DataType.findTypeName(innerCondType);
                msgCollector.collect(msg, MessageType.Error);
                throwTypeCheckerException(limit, msg, errCode, PigException.INPUT, null);
            }
        }
        try {
            // Compute the schema
            limit.getSchema();
        } catch (FrontendException fe) {
            int errCode = 1055;
            String msg = "Problem while reading schemas from inputs of Limit";
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(limit, msg, errCode, PigException.INPUT, fe);
        }
    }

    /***
     * Return concatenated of all fields from all input operators
     * If one of the inputs have no schema then we cannot construct
     * the output schema.
     * @param cs
     * @throws VisitorException
     */
    public void visit(LOCross cs) throws VisitorException {
        cs.resetSchema();

        try {
            // Compute the schema
            cs.getSchema() ;
        }
        catch (FrontendException fe) {
            int errCode = 1055;
            String msg = "Problem while reading"
                + " schemas from inputs of Cross" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(cs, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    /***
     * The schema of sort output will be the same as sort input.
     * @throws FrontendException 
     *
     */
    public void visit(LOSort sort) throws FrontendException {
        sort.resetSchema();
        // Type checking internal plans.
        for(int i=0;i < sort.getSortColPlans().size(); i++) {

            LogicalExpressionPlan sortColPlan = sort.getSortColPlans().get(i) ;

            // Check that the inner plan has only 1 output port
            if (sortColPlan.getSources().size() != 1) {
                int errCode = 1057;
                String msg = "Sort's inner plan can only have one output (leaf)" ;
                msgCollector.collect(msg, MessageType.Error) ;
                throwTypeCheckerException(sort, msg, errCode, PigException.INPUT, null) ;
            }

            visitExpressionPlan(sortColPlan, sort);
        }

        try {
            // Compute the schema
            sort.getSchema() ;
        }
        catch (FrontendException fee) {
            int errCode = 1059;
            String msg = "Problem while reconciling output schema of Sort" ;
            msgCollector.collect(msg, MessageType.Error);
            throwTypeCheckerException(sort, msg, errCode, PigException.INPUT, fee) ;
        }
    }

    /***
     * The schema of rank output will be the same as input, plus a rank field.
     * @throws FrontendException
     *
     */
    public void visit(LORank rank) throws FrontendException {
        rank.resetSchema();

        // Type checking internal plans.
        List<LogicalExpressionPlan> rankColPlans = rank.getRankColPlans();

        for(int i=0;i < rankColPlans.size(); i++) {
            LogicalExpressionPlan rankColPlan = rankColPlans.get(i) ;

            // Check that the inner plan has only 1 output port
            if (rankColPlan.getSources().size() != 1) {
                int errCode = 1057;
                String msg = "Rank's inner plan can only have one output (leaf)" ;
                msgCollector.collect(msg, MessageType.Error) ;
                throwTypeCheckerException(rank, msg, errCode, PigException.INPUT, null) ;
            }

            visitExpressionPlan(rankColPlan, rank);

        }

        try {
            // Compute the schema
            rank.getSchema() ;
        }
        catch (FrontendException fee) {
            int errCode = 1059;
            String msg = "Problem while reconciling output schema of Rank" ;
            msgCollector.collect(msg, MessageType.Error);
            throwTypeCheckerException(rank, msg, errCode, PigException.INPUT, fee) ;
        }

    }

    /***
     * The schema of split output will be the same as split input
     */

    public void visit(LOSplit split) throws VisitorException {
        OperatorPlan lp = split.getPlan();
        List<Operator> inputList = lp.getPredecessors(split);

        if (inputList.size() != 1) {            
            int errCode = 2008;
            String msg = "LOSplit cannot have more than one input. Found: " + inputList.size() + " input(s).";
            throwTypeCheckerException(split, msg, errCode, PigException.BUG, null) ;
        }

        split.resetSchema();
        try {
            // Compute the schema
            split.getSchema();
        }
        catch (FrontendException fe) {
            int errCode = 1059;
            String msg = "Problem while reconciling output schema of Split" ;
            msgCollector.collect(msg, MessageType.Error);
            throwTypeCheckerException(split, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    /**
     * LOJoin visitor
     * @throws FrontendException 
     */
    public void visit(LOJoin join) throws FrontendException {
        try {
            join.resetSchema();
            join.getSchema();
        } catch (FrontendException fe) {
            int errCode = 1060;
            String msg = "Cannot resolve Join output schema" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(join, msg, errCode, PigException.INPUT, fe) ;
        }

        MultiMap<Integer, LogicalExpressionPlan> joinColPlans
        = join.getExpressionPlans() ;
        List<Operator> inputs = join.getInputs((LogicalPlan) plan) ;

        // Type checking internal plans.
        for(int i=0;i < inputs.size(); i++) {
            ArrayList<LogicalExpressionPlan> innerPlans
            = new ArrayList<LogicalExpressionPlan>(joinColPlans.get(i)) ;

            for(int j=0; j < innerPlans.size(); j++) {

                LogicalExpressionPlan innerPlan = innerPlans.get(j) ;

                // Check that the inner plan has only 1 output port
                if (innerPlan.getSources().size() != 1) {
                    int errCode = 1057;
                    String msg = "Join's inner plans can only"
                        + " have one output (leaf)" ;
                    msgCollector.collect(msg, MessageType.Error) ;
                    throwTypeCheckerException(join, msg, errCode, PigException.INPUT, null) ;
                }
                visitExpressionPlan(innerPlan, join);
            }
        }

        try {

            if (!isJoinOnMultiCols(join)) {
                // merge all the inner plan outputs so we know what type
                // our group column should be
                byte groupType = getAtomicJoinColType(join);

                // go through all inputs again to add cast if necessary
                for(int i=0;i < inputs.size(); i++) {
                    Collection<LogicalExpressionPlan> exprPlans = join.getJoinPlan(i);

                    //there should be one and only expression plan - that gets 
                    // checked in getAtomicJoinColType()
                    LogicalExpressionPlan exprPlan = exprPlans.iterator().next();

                    // Checking innerPlan size already done above
                    byte innerType =
                        ((LogicalExpression)exprPlan.getSources().get(0)).getType();

                    if (innerType != groupType) {
                        insertAtomicCastForInnerPlan(exprPlan, join, groupType);
                    }
                }
            }
            else {
                //schema of the group-by key
                LogicalSchema groupBySchema = getSchemaFromInnerPlans(join.getExpressionPlans(), join) ;

                // go through all inputs again to add cast if necessary
                for(int i=0;i < inputs.size(); i++) {
                    List<LogicalExpressionPlan> innerPlans = 
                        new ArrayList<LogicalExpressionPlan>(join.getJoinPlan(i)) ;
                    for(int j=0;j < innerPlans.size(); j++) {
                        LogicalExpressionPlan innerPlan = innerPlans.get(j) ;
                        LogicalExpression outputExp = ((LogicalExpression)innerPlan.getSources().get(0));
                        byte innerType = outputExp.getType() ;

                        byte expectedType = groupBySchema.getField(j).type ;

                        if (!DataType.isAtomic(innerType) && (DataType.TUPLE != innerType)) {
                            int errCode = 1057;
                            String msg = "Join's inner plans can only"
                                + "have one output (leaf)" ;
                            msgCollector.collect(msg, MessageType.Error) ;
                            throwTypeCheckerException(join, msg, errCode, PigException.INPUT, null) ;
                        }
                        if (innerType != expectedType) {
                            insertAtomicCastForInnerPlan(
                                    innerPlan,join, expectedType
                            ) ;
                        }
                    }
                }
            }
        }
        catch (FrontendException fe) {
            int errCode = 1060;
            String msg = "Cannot resolve Join output schema" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(join, msg, errCode, PigException.INPUT, fe) ;
        }

        try {
            join.resetSchema();
            join.getSchema(); 
        }
        catch (FrontendException fe) {
            int errCode = 1060;
            String msg = "Cannot resolve Join output schema" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(join, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    /**
     * @param join
     * @return true if there is more than one join column for an input
     */
    private boolean isJoinOnMultiCols(LOJoin join) {
        MultiMap<Integer, LogicalExpressionPlan> exprPlans = join.getExpressionPlans();
        if(exprPlans == null || exprPlans.size() == 0){
            throw new AssertionError("LOJoin.isJoinOnMultiCols() can only be called "
                    + " after it has an join expression plans ") ;
        }
        return exprPlans.get(0).size() > 1;
    }

    /**
     * This can be used to get the merged type of output join col
     * only when the join col is of atomic type
     * @return The type of the join col 
     * @throws FrontendException 
     */
    private byte getAtomicJoinColType(LOJoin join) throws FrontendException {
        if (isJoinOnMultiCols(join)) {
            int errCode = 1010;
            String msg = "getAtomicJoinColType is used only when"
                + " dealing with atomic group col";
            throw new FrontendException(msg, errCode, PigException.INPUT, false, null) ;
        }

        byte groupType = DataType.BYTEARRAY ;
        // merge all the inner plan outputs so we know what type
        // our group column should be
        for(int i=0;i < plan.getPredecessors(join).size() ; i++) {
            List<LogicalExpressionPlan> innerPlans = 
                new ArrayList<LogicalExpressionPlan>(join.getJoinPlan(i)) ;
            if (innerPlans.size() != 1) {
                int errCode = 1012;
                String msg = "Each COGroup input has to have "
                    + "the same number of inner plans";
                throw new FrontendException(msg, errCode, PigException.INPUT, false, null) ;
            }
            byte innerType = ((LogicalExpression)innerPlans.get(0).getSources().get(0)).getType() ;
            byte newGroupType = DataType.mergeType(groupType, innerType) ;
            if (newGroupType == -1)
            {
                int errCode = 1107;
                String msg = "Cannot merge join keys, incompatible types. Outer: " + DataType.findTypeName(groupType) + "; inner: " + DataType.findTypeName(innerType);
                throw new FrontendException(msg, errCode, PigException.INPUT) ;
            } else {
                groupType = newGroupType;
            }
        }

        return groupType ; 
    }

    /**
     * This can be used to get the merged type of output join col
     * only when the join/cogroup col is of atomic type
     * @return The type of the join col 
     * @throws FrontendException 
     */
    private byte getAtomicColType(MultiMap<Integer, LogicalExpressionPlan> allExprPlans) throws FrontendException {
        if (isMultiExprPlanPerInput(allExprPlans)) {
            int errCode = 1010;
            String msg = "getAtomicJoinColType is used only when"
                + " dealing with atomic group col";
            throw new FrontendException(msg, errCode, PigException.INPUT, false, null) ;
        }

        byte groupType = DataType.BYTEARRAY ;
        // merge all the inner plan outputs so we know what type
        // our group column should be
        for(int i=0;i < allExprPlans.size() ; i++) {
            List<LogicalExpressionPlan> innerPlans = 
                new ArrayList<LogicalExpressionPlan>(allExprPlans.get(i)) ;
            if (innerPlans.size() != 1) {
                int errCode = 1012;
                String msg = "Each COGroup input has to have "
                    + "the same number of inner plans";
                throw new FrontendException(msg, errCode, PigException.INPUT, false, null) ;
            }
            byte innerType = ((LogicalExpression)innerPlans.get(0).getSources().get(0)).getType() ;
            byte newGroupType = DataType.mergeType(groupType, innerType) ;
            if (newGroupType == -1)
            {
                int errCode = 1107;
                String msg = "Cannot merge join keys, incompatible types. Outer: " + DataType.findTypeName(groupType) + "; inner: " + DataType.findTypeName(innerType);
                throw new FrontendException(msg, errCode, PigException.INPUT) ;
            } else {
                groupType = newGroupType;
            }
        }

        return groupType ; 
    }



    private boolean isMultiExprPlanPerInput(
            MultiMap<Integer, LogicalExpressionPlan> exprPlans) {
        if(exprPlans == null || exprPlans.size() == 0){
            throw new AssertionError("LOJoin.isJoinOnMultiCols() can only be called "
                    + " after it has an join expression plans ") ;
        }
        return exprPlans.get(0).size() > 1;
    }

    /**
     * Cast the single output operator of innerPlan to toType
     * @param innerPlan
     * @param relOp - join or cogroup
     * @param toType
     * @throws FrontendException
     */
    private void insertAtomicCastForInnerPlan(LogicalExpressionPlan innerPlan,
            LogicalRelationalOperator relOp, byte toType) throws FrontendException {
        if (!DataType.isUsableType(toType)) {
            int errCode = 1051;
            String msg = "Cannot cast to "
                + DataType.findTypeName(toType);
            throwTypeCheckerException(relOp, msg, errCode, PigException.INPUT, null);
        }

        List<Operator> outputs = innerPlan.getSources();
        if (outputs.size() > 1) {
            int errCode = 2060;
            String msg = "Expected one output. Found " + outputs.size() + "  outputs.";
            throwTypeCheckerException(relOp, msg, errCode, PigException.BUG, null);
        }
        LogicalExpression currentOutput = (LogicalExpression) outputs.get(0);
        TypeCheckingExpVisitor.collectCastWarning(
                relOp, currentOutput.getType(),
                toType, msgCollector
        );
        LogicalFieldSchema newFS = new LogicalFieldSchema(
                currentOutput.getFieldSchema().alias, null, toType
        );
        //add cast
        new CastExpression(innerPlan, currentOutput, newFS);

        //visit modified inner plan
        visitExpressionPlan(innerPlan, relOp);
    }

    /**
     * Create combined group-by/join column schema based on join/cogroup 
     * expression plans for all inputs.
     * This implementation is based on the assumption that all the 
     * inputs have the same join col tuple arity.
     * 
     * @param exprPlans
     * @return
     * @throws FrontendException
     */
    private LogicalSchema getSchemaFromInnerPlans(
            MultiMap<Integer, LogicalExpressionPlan> exprPlans,
            LogicalRelationalOperator op
    )
    throws FrontendException {
        // this fsList represents all the columns in group tuple
        List<LogicalFieldSchema> fsList = new ArrayList<LogicalFieldSchema>() ;

        int outputSchemaSize = exprPlans.get(0).size();

        // by default, they are all bytearray
        // for type checking, we don't care about aliases
        for(int i=0; i<outputSchemaSize; i++) {
            fsList.add(new LogicalFieldSchema(null, null, DataType.BYTEARRAY));
        }

        // merge all the inner plan outputs so we know what type
        // our group column should be
        for(int i=0;i < exprPlans.size(); i++) {
            List<LogicalExpressionPlan> innerPlans =
                new ArrayList<LogicalExpressionPlan>(exprPlans.get(i)) ;

            for(int j=0;j < innerPlans.size(); j++) {
                LogicalExpression eOp = (LogicalExpression)innerPlans.get(j).getSources().get(0);
                byte innerType = eOp.getType();

                if(eOp instanceof ProjectExpression) {
                    if(((ProjectExpression)eOp).isProjectStar()) {
                        //there is a project star and there is more than one 
                        // expression plan
                        int errCode = 1013;
                        String msg = "Grouping attributes can either be star (*) " +
                        "or a list of expressions, but not both.";
                        msgCollector.collect(msg, MessageType.Error) ;
                        throw new FrontendException(
                                msg, errCode, PigException.INPUT, false, null
                        );         
                    }
                }
                //merge the type
                LogicalFieldSchema groupFs = fsList.get(j);
                groupFs.type = DataType.mergeType(groupFs.type, innerType) ;
                if(groupFs.type == DataType.ERROR){
                    String colType = "join";
                    if(op instanceof LOCogroup){
                        colType = "group";
                    }
                    String msg =
                        colType + " column no. " +
                        (j+1) + " in relation no. " + (i+1) + " of  " + colType + 
                        " statement has datatype " + DataType.findTypeName(innerType) +
                        " which is incompatible with type of corresponding column" +
                        " in earlier relation(s) in the statement";
                    msgCollector.collect(msg, MessageType.Error) ;
                    TypeCheckerException ex =
                        new TypeCheckerException(op, msg, 1130, PigException.INPUT);
                    ex.setMarkedAsShowToUser(true);
                    throw ex;
                }
            }

        }
        //create schema from field schemas
        LogicalSchema tupleSchema = new LogicalSchema();
        for(LogicalFieldSchema fs : fsList){
            tupleSchema.addField(fs);
        }
        return tupleSchema;
    }

    /**
     * COGroup
     * All group by cols from all inputs have to be of the
     * same type
     * @throws FrontendException 
     */
    @Override
    public void visit(LOCogroup cg) throws FrontendException {
        try {
            cg.resetSchema();
            cg.getSchema();
        } catch (FrontendException fe) {
            int errCode = 1060;
            String msg = "Cannot resolve COGroup output schema" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(cg, msg, errCode, PigException.INPUT, fe) ;
        }

        MultiMap<Integer, LogicalExpressionPlan> groupByPlans = 
            cg.getExpressionPlans();

        List<Operator> inputs = cg.getInputs((LogicalPlan)plan);

        // Type checking internal plans.
        for(int i=0;i < inputs.size(); i++) {
            List<LogicalExpressionPlan> innerPlans =
                new ArrayList<LogicalExpressionPlan>(cg.getExpressionPlans().get(i)) ;

            for(int j=0; j < innerPlans.size(); j++) {

                LogicalExpressionPlan innerPlan = innerPlans.get(j) ;

                // Check that the inner plan has only 1 output port
                if (innerPlan.getSources().size() != 1) {
                    int errCode = 1057;
                    String msg = "COGroup's inner plans can only"
                        + "have one output (leaf)" ;
                    msgCollector.collect(msg, MessageType.Error) ;
                    throwTypeCheckerException(cg, msg, errCode, PigException.INPUT, null) ;
                }
                visitExpressionPlan(innerPlan, cg);
            }

        }

        try {

            if (!isCoGroupOnMultiCols(cg)) {
                // merge all the inner plan outputs so we know what type
                // our group column should be
                byte groupType = getAtomicColType(cg.getExpressionPlans());

                // go through all inputs again to add cast if necessary
                for(int i=0;i < inputs.size(); i++) {
                    List<LogicalExpressionPlan> innerPlans =
                        new ArrayList<LogicalExpressionPlan>(cg.getExpressionPlans().get(i)) ;
                    // Checking innerPlan size already done above
                    byte innerType = ((LogicalExpression)innerPlans.get(0).getSources().get(0)).getType() ;
                    if (innerType != groupType) {
                        insertAtomicCastForInnerPlan(
                                innerPlans.get(0),cg, groupType
                        ) ;
                    }
                }
            }
            else {

                LogicalSchema groupBySchema = getSchemaFromInnerPlans(cg.getExpressionPlans(), cg);

                // go through all inputs again to add cast if necessary
                for(int i=0;i < inputs.size(); i++) {

                    List<LogicalExpressionPlan> innerPlans = 
                        new ArrayList<LogicalExpressionPlan>(groupByPlans.get(i)) ;
                    for(int j=0;j < innerPlans.size(); j++) {
                        LogicalExpressionPlan innerPlan = innerPlans.get(j) ;
                        byte innerType = ((LogicalExpression)innerPlan.getSources().get(0)).getType() ;
                        byte expectedType = DataType.BYTEARRAY ;

                        if (!DataType.isAtomic(innerType) && (DataType.TUPLE != innerType)) {
                            int errCode = 1061;
                            String msg = "Sorry, group by complex types"
                                + " will be supported soon" ;
                            msgCollector.collect(msg, MessageType.Error) ;
                            throwTypeCheckerException(cg, msg, errCode, PigException.INPUT, null) ;
                        }

                        expectedType = groupBySchema.getField(j).type ;

                        if (innerType != expectedType) {
                            insertAtomicCastForInnerPlan(
                                    innerPlan, cg, expectedType
                            );
                        }
                    }
                }
            }
        }
        catch (FrontendException fe) {
            int errCode = 1060;
            String msg = "Cannot resolve COGroup output schema" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(cg, msg, errCode, PigException.INPUT, fe) ;
        }

        try {
            cg.resetSchema();
            cg.getSchema();
        }
        catch (FrontendException fe) {
            int errCode = 1060;
            String msg = "Cannot resolve COGroup output schema" ;
            msgCollector.collect(msg, MessageType.Error) ;
            throwTypeCheckerException(cg, msg, errCode, PigException.INPUT, fe) ;
        }
    }

    /**
     * @param coGroup
     * @return true if there is more than one join column for an input
     */
    private boolean isCoGroupOnMultiCols(LOCogroup coGroup) {
        MultiMap<Integer, LogicalExpressionPlan> exprPlans = coGroup.getExpressionPlans();
        if(exprPlans == null || exprPlans.size() == 0){
            throw new AssertionError("LOCoGroup.isJoinOnMultiCols() can only becalled "
                    + " after it has an join expression plans ") ;
        }
        return exprPlans.get(0).size() > 1;
    }

}
