/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

/*
 */
package org.teiid.query.processor.relational;

import static org.teiid.query.analysis.AnalysisRecord.*;

import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.teiid.api.exception.query.QueryProcessingException;
import org.teiid.client.plan.PlanNode;
import org.teiid.common.buffer.BlockedException;
import org.teiid.common.buffer.BufferManager.TupleSourceType;
import org.teiid.common.buffer.TupleBatch;
import org.teiid.common.buffer.TupleBuffer;
import org.teiid.common.buffer.TupleSource;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidProcessingException;
import org.teiid.query.QueryPlugin;
import org.teiid.query.eval.Evaluator;
import org.teiid.query.processor.RegisterRequestParameter;
import org.teiid.query.sql.lang.BatchedUpdateCommand;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Criteria;
import org.teiid.query.sql.lang.Insert;
import org.teiid.query.sql.lang.SourceHint;
import org.teiid.query.sql.symbol.Constant;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.translator.ExecutionFactory.TransactionSupport;


public class ProjectIntoNode extends RelationalNode {

	public enum Mode {
		BATCH, ITERATOR, SINGLE
	}
	
    private static int REQUEST_CREATION = 1;
    private static int RESPONSE_PROCESSING = 2;
    
    // Initialization state
    private GroupSymbol intoGroup;
    private List intoElements;
    private String modelName;
    private Mode mode;
    private boolean upsert;
    
    // Processing state
    private long batchRow = 1;
    private long insertCount = 0;
    private int phase = REQUEST_CREATION;    
    private int requestsRegistered = 0;
    private int tupleSourcesProcessed = 0;
    private boolean sourceDone;
    
    private TupleBuffer buffer;
    private TupleBuffer last;
    private TupleBatch currentBatch;
        	
    private TupleSource tupleSource;
    
    private Criteria constraint;
    private Evaluator eval;
    
    private TransactionSupport transactionSupport;
    private SourceHint sourceHint;

    protected ProjectIntoNode() {
        super();
    }
    
    public ProjectIntoNode(int nodeID) {
        super(nodeID);
    }
    
    public void reset() {
        super.reset();     
        this.phase = REQUEST_CREATION;
        this.batchRow = 1;
        this.insertCount = 0; 
        this.tupleSourcesProcessed = 0;
        this.requestsRegistered = 0;
        this.currentBatch=null;
        this.sourceDone=false;
    }

    public void setIntoGroup(GroupSymbol group) { 
        this.intoGroup = group;
    }

    public void setIntoElements(List intoElements) {
        this.intoElements = intoElements;
    }

    public void setModelName(String modelName) {
        this.modelName = modelName;
    }
    
    /**
     * Get batch from child node
     * Walk through each row of child batch
     *    Bind values to insertCommand
     *    Execute insertCommand
     *    Update insertCount
     * When no more data is available, output batch with single row containing insertCount 
     */     
    public TupleBatch nextBatchDirect() 
        throws BlockedException, TeiidComponentException, TeiidProcessingException {
        
        while(phase == REQUEST_CREATION) {
            
            /* If we don't have a batch to work, get the next
             */
            if (currentBatch == null) {
            	if (sourceDone) {
	                phase = RESPONSE_PROCESSING;
	                break;
            	}
                currentBatch = getChildren()[0].nextBatch(); // can throw BlockedException
                sourceDone = currentBatch.getTerminationFlag();
                this.batchRow = currentBatch.getBeginRow();
                
                //normally we would want to skip a 0 sized batch, but it typically represents the terminal batch
                //and for implicit temp tables we need to issue an empty insert
                if(currentBatch.getRowCount() == 0
                		&& (!currentBatch.getTerminationFlag() || mode != Mode.ITERATOR)) {
            		currentBatch = null;
            		continue;
                }
                if (this.constraint != null) {
                	//row based security check
	                if (eval == null) {
	                	eval = new Evaluator(createLookupMap(this.intoElements), this.getDataManager(), getContext());
	                }
	                List<List<?>> tuples = this.currentBatch.getTuples();
	                for (int i = 0; i < tuples.size(); i++) {
	                	if (!eval.evaluate(constraint, tuples.get(i))) {
	                		throw new QueryProcessingException(QueryPlugin.Event.TEIID31130, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31130, new Insert(intoGroup, this.intoElements, convertValuesToConstants(tuples.get(i), intoElements))));
	                	}
	                }
                }
            } 
            
            if (mode != Mode.ITERATOR) { //delay the check in the iterator case to accumulate batches
            	checkExitConditions(); 
            }
            
            int batchSize = currentBatch.getRowCount();
            int requests = 1;
            switch (mode) {
            case ITERATOR:
            	if (buffer == null) {
            		buffer = getBufferManager().createTupleBuffer(intoElements, getConnectionID(), TupleSourceType.PROCESSOR);
            	}
            	
            	if (sourceDone) {
            		//if there is a pending request we can't process the last until it is done
            		checkExitConditions(); 
            	}
            	
            	for (List<?> tuple : currentBatch.getTuples()) {
            		buffer.addTuple(tuple);
            	}
            	
            	try {
            		checkExitConditions();
            	} catch (BlockedException e) {
            		//move to the next batch
                    this.batchRow += batchSize;
                	currentBatch = null;
            		continue;
            	}
            	
            	if (currentBatch.getTerminationFlag() && (buffer.getRowCount() != 0 || intoGroup.isImplicitTempGroupSymbol())) {
            		registerIteratorRequest();
            	} else if (buffer.getRowCount() >= buffer.getBatchSize() * 4) {
        			registerIteratorRequest();
            	} else {
            		requests = 0;
            	}
            	break;
            case BATCH:
                // Register batched update command against source
                long endRow = currentBatch.getEndRow();
                List<Command> rows = new ArrayList<Command>((int)(endRow-batchRow));
                for(long rowNum = batchRow; rowNum <= endRow; rowNum++) {

                    Insert insert = new Insert( intoGroup, 
                                                 intoElements, 
                                                 convertValuesToConstants(currentBatch.getTuple(rowNum), intoElements));
                    insert.setSourceHint(sourceHint);
                    insert.setUpsert(upsert);
                    rows.add( insert );
                }
                registerRequest(new BatchedUpdateCommand( rows ));
                break;
            case SINGLE:
                batchSize = 1;
                // Register insert command against source 
                // Defect 16036 - submit a new INSERT command to the DataManager.
                Insert insert = new Insert(intoGroup, intoElements, convertValuesToConstants(currentBatch.getTuple(batchRow), intoElements));
                insert.setSourceHint(sourceHint);
                insert.setUpsert(upsert);
                registerRequest(insert);
            }
            
            this.batchRow += batchSize;
            if (batchRow > currentBatch.getEndRow()) {
            	currentBatch = null;
            }
            this.requestsRegistered+=requests;
        }
        
        checkExitConditions();
        
        if (this.buffer != null) {
        	this.buffer.remove();
        	this.buffer = null;
        }
        
        // End this node's work
        //report only a max int
        int count = (int)Math.min(Integer.MAX_VALUE, insertCount);
        addBatchRow(Arrays.asList(count));
        terminateBatches();
        return pullBatch();                                                           
    }

	private void registerIteratorRequest() throws TeiidComponentException,
			TeiidProcessingException {
		Insert insert = new Insert(intoGroup, intoElements, null);
		insert.setSourceHint(sourceHint);
		insert.setUpsert(upsert);
		buffer.close();
		insert.setTupleSource(buffer.createIndexedTupleSource(true));
		// Register insert command against source 
		registerRequest(insert);
        //remove the old buffer when the insert is complete
        last = buffer;
        buffer = null;
	}

    private void checkExitConditions()  throws TeiidComponentException, BlockedException, TeiidProcessingException {
    	if (tupleSource != null) {
    		if (mode == Mode.BATCH || mode == Mode.ITERATOR) {
    			List<?> tuple = null;
    			while ((tuple = tupleSource.nextTuple()) != null) {
        			Integer count = (Integer)tuple.get(0);
        			if (count > 0 ||  count == Statement.SUCCESS_NO_INFO) {
        				insertCount++;
        			}
    			}
	    	} else {
	    		Integer count = (Integer)tupleSource.nextTuple().get(0);
	    		insertCount += count.intValue();
	    	}
	        closeRequest();
	        // Mark as processed
	        tupleSourcesProcessed++; // This should set tupleSourcesProcessed to be the same as requestsRegistered
    	}
        // RESPONSE_PROCESSING: process tuple sources
        if (tupleSourcesProcessed < requestsRegistered) {
        	throw BlockedException.block(getContext().getRequestId(), "Blocking on insert update count"); //$NON-NLS-1$
        }
                
    }

    private void registerRequest(Command command) throws TeiidComponentException, TeiidProcessingException {
    	tupleSource = getDataManager().registerRequest(getContext(), command, this.modelName, new RegisterRequestParameter(null, getID(), -1));        
    }
    
    private void closeRequest() {
    	if (this.last != null) {
    		this.last.remove();
    		this.last = null;
    	}
        if (this.tupleSource != null) {
            tupleSource.closeSource();
            this.tupleSource = null;
        }
    }
    
    protected void getNodeString(StringBuffer str) {
        super.getNodeString(str);
        str.append(intoGroup);
    }
    
    public Object clone(){
        ProjectIntoNode clonedNode = new ProjectIntoNode();
        super.copyTo(clonedNode);

        clonedNode.intoGroup = intoGroup;
        clonedNode.intoElements = intoElements;
        clonedNode.modelName = this.modelName;
        clonedNode.mode = this.mode;
        clonedNode.constraint = this.constraint;
        clonedNode.sourceHint = this.sourceHint;
        clonedNode.upsert = this.upsert;
        return clonedNode;
    }

    public PlanNode getDescriptionProperties() {
    	PlanNode props = super.getDescriptionProperties();
        props.addProperty(PROP_INTO_GROUP, intoGroup.toString());
        if (upsert) { 
            props.addProperty(PROP_UPSERT, "true"); //$NON-NLS-1$
        }
        List<String> selectCols = new ArrayList<String>(intoElements.size());
        for(int i=0; i<this.intoElements.size(); i++) {
            selectCols.add(this.intoElements.get(i).toString());
        }
        props.addProperty(PROP_SELECT_COLS, selectCols);

        return props;
    }
    
    private List<Constant> convertValuesToConstants(List<?> values, List<ElementSymbol> elements) {
        ArrayList<Constant> constants = new ArrayList<Constant>(values.size());
        for(int i=0; i<elements.size(); i++) {
            ElementSymbol es = elements.get(i);
            Class<?> type = es.getType();
            constants.add(new Constant(values.get(i),type));
        }
        return constants;
    }  
    
    public Mode getMode() {
		return mode;
	}
            
    public void setMode(Mode mode) {
		this.mode = mode;
	}

    public boolean isTempGroupInsert() {
		return intoGroup.isTempGroupSymbol();
	}

    public void closeDirect() {
    	if (this.buffer != null) {
    		this.buffer.remove();
    		this.buffer = null;
    	}
        closeRequest();
	}
    
    public String getModelName() {
		return modelName;
	}
    
    @Override
    public Boolean requiresTransaction(boolean transactionalReads) {
		Boolean requires = this.getChildren()[0].requiresTransaction(transactionalReads);
		if (requires != null && requires) {
			return true;
		}
		if (transactionSupport == TransactionSupport.NONE) {
			return requires;
		}
		return true;
    }
    
    public void setConstraint(Criteria constraint) {
		this.constraint = constraint;
	}
    
    public void setTransactionSupport(TransactionSupport transactionSupport) {
		this.transactionSupport = transactionSupport;
	}

    public void setSourceHint(SourceHint property) {
        this.sourceHint = property;
    }
    
    public void setUpsert(boolean upsert) {
        this.upsert = upsert;
    }
    
}
