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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryParserException;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.api.exception.query.QueryResolverException;
import org.teiid.common.buffer.BlockedException;
import org.teiid.common.buffer.BufferManager;
import org.teiid.common.buffer.TupleBatch;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidProcessingException;
import org.teiid.core.id.IDGenerator;
import org.teiid.core.types.DataTypeManager;
import org.teiid.language.SQLConstants;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.metadata.Column;
import org.teiid.metadata.Table;
import org.teiid.metadata.Trigger;
import org.teiid.query.QueryPlugin;
import org.teiid.query.analysis.AnalysisRecord;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.optimizer.capabilities.CapabilitiesFinder;
import org.teiid.query.parser.QueryParser;
import org.teiid.query.processor.CollectionTupleSource;
import org.teiid.query.processor.ProcessorDataManager;
import org.teiid.query.processor.ProcessorPlan;
import org.teiid.query.processor.proc.ForEachRowPlan;
import org.teiid.query.processor.proc.ProcedurePlan;
import org.teiid.query.resolver.QueryResolver;
import org.teiid.query.sql.LanguageVisitor;
import org.teiid.query.sql.ProcedureReservedWords;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.proc.CreateProcedureCommand;
import org.teiid.query.sql.proc.TriggerAction;
import org.teiid.query.sql.symbol.Constant;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.query.util.CommandContext;

/**
 * Handles the planning of triggers from source events
 */
public final class SourceTriggerActionPlanner implements CommandPlanner {
    
    /**
     * TODO: elevate the transaction handling?
     */
    public static class CompositeProcessorPlan extends ProcessorPlan {
        
        private List<ProcessorPlan> plans;
        private int planIndex;
        private boolean open;
        private List<String> names;
        private Table table;

        public CompositeProcessorPlan(List<ProcessorPlan> plans, List<String> names, Table t) {
            this.plans = plans;
            this.names = names;
            this.table = t;
        }

        @Override
        public List getOutputElements() {
            return Command.getUpdateCommandSymbol();
        }

        @Override
        public void open() throws TeiidComponentException,
                TeiidProcessingException {
        }
        
        @Override
        public void initialize(CommandContext context,
                ProcessorDataManager dataMgr, BufferManager bufferMgr) {
            super.initialize(context, dataMgr, bufferMgr);
            for (ProcessorPlan plan : plans) {
                plan.initialize(context, dataMgr, bufferMgr);
            }
        }

        @Override
        public TupleBatch nextBatch() throws BlockedException,
                TeiidComponentException, TeiidProcessingException {
            while (planIndex < plans.size()) {
                try {
                    if (!open) {
                        plans.get(planIndex).open();
                    }
                    plans.get(planIndex).nextBatch();
                    plans.get(planIndex).close();
                } catch (TeiidProcessingException e) {
                    LogManager.logWarning(LogConstants.CTX_DQP, e, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31214, names.get(planIndex), table));
                } catch (BlockedException e) {
                    throw e;
                }
                //allow other exception types to bubble up
                open = false;
                planIndex++;
            }
            TupleBatch batch = new TupleBatch(1, new List<?>[0]);
            batch.setTerminationFlag(true);
            return batch;
        }

        @Override
        public void close() throws TeiidComponentException {
            
        }

        @Override
        public ProcessorPlan clone() {
            throw new UnsupportedOperationException();
        }
        
    }
    
    /**
     * Represents a source event as a Command - is localized here
     * as it's not directly callable by a user
     */
    public static class SourceEventCommand extends Command {

        private Table table;
        private Object[] oldValues;
        private Object[] newValues;
        private String[] columnNames;
        
        public SourceEventCommand(Table t, Object[] old, Object[] newValues,
                String[] columnNames) {
            this.table = t;
            this.oldValues = old;
            this.newValues = newValues;
            this.columnNames = columnNames;
        }

        @Override
        public void acceptVisitor(LanguageVisitor visitor) {
        }

        @Override
        public int getType() {
            return TYPE_SOURCE_EVENT;
        }

        @Override
        public Object clone() {
            return this;
        }

        @Override
        public List<Expression> getProjectedSymbols() {
            return Command.getUpdateCommandSymbol();
        }

        @Override
        public boolean areResultsCachable() {
            return false;
        }
        
        public Table getTable() {
            return table;
        }
        
        public Object[] getOldValues() {
            return oldValues;
        }
        
        public Object[] getNewValues() {
            return newValues;
        }
        
        public String[] getColumnNames() {
            return columnNames;
        }
        
        @Override
        public String toString() {
            return "AFTER EVENT ON " + table; //$NON-NLS-1$
        }
        
    }
    
    @Override
    public ProcessorPlan optimize(Command command, IDGenerator idGenerator,
            QueryMetadataInterface metadata, CapabilitiesFinder capFinder,
            AnalysisRecord analysisRecord, CommandContext context)
            throws QueryPlannerException, QueryMetadataException,
            TeiidComponentException {
		SourceEventCommand sec = (SourceEventCommand)command;
        
        Map<Expression, Integer> lookup = new HashMap<Expression, Integer>();
        Map<ElementSymbol, Expression> params = new HashMap<ElementSymbol, Expression>();
		List<Object> tuple = new ArrayList<Object>();
		
        Map<String, Integer> map = null;
        
        if (sec.getColumnNames() != null) {
            map = new TreeMap<String, Integer>(String.CASE_INSENSITIVE_ORDER);
            for (String name : sec.getColumnNames()) {
                map.put(name, map.size());
            }
        }
        
        GroupSymbol changingGroup = new GroupSymbol(ProcedureReservedWords.CHANGING);
        if (sec.newValues != null) {
            GroupSymbol newGroup = new GroupSymbol(SQLConstants.Reserved.NEW);
            newGroup.setMetadataID(sec.table);
            for (int i = 0; i < sec.getTable().getColumns().size(); i++) {
                Column c = sec.getTable().getColumns().get(i);
                Integer index = null;
                if (map != null) {
                    index = map.get(c.getName());
                } else {
                    index = i;
                }
                ElementSymbol newElement = new ElementSymbol(c.getName(), newGroup);
                newElement.setMetadataID(c);
                ElementSymbol changingElement = new ElementSymbol(c.getName(), changingGroup);
                lookup.put(newElement, tuple.size());
                lookup.put(changingElement, tuple.size() + 1);
                params.put(newElement, newElement);
                params.put(changingElement, changingElement);
                if (index == null) {
                    //not changing
                    tuple.add(new Constant(null));
                    tuple.add(new Constant(Boolean.FALSE));
                } else {
                    //changing
                    tuple.add(new Constant(DataTypeManager.convertToRuntimeType(sec.newValues[index], true)));
                    tuple.add(new Constant(Boolean.TRUE));
                }
            }
        }
        if (sec.oldValues != null) {
            GroupSymbol oldGroup = new GroupSymbol(SQLConstants.Reserved.OLD);
            oldGroup.setMetadataID(sec.table);
            for (int i = 0; i < sec.getTable().getColumns().size(); i++) {
                Column c = sec.getTable().getColumns().get(i);
                Integer index = null;
                if (map != null) {
                    index = map.get(c.getName());
                } else {
                    index = i;
                }
                ElementSymbol oldElement = new ElementSymbol(c.getName(), oldGroup);
                oldElement.setMetadataID(c);
                lookup.put(oldElement, tuple.size());
                params.put(oldElement, oldElement);
                if (index != null) {
                    tuple.add(new Constant(DataTypeManager.convertToRuntimeType(sec.oldValues[index], true)));
                }
            }
        }
        
        List<ProcessorPlan> plans = new ArrayList<ProcessorPlan>();
        List<String> names = new ArrayList<String>();
        
        for (Trigger tr : sec.getTable().getTriggers().values()) {
            int updateType = Command.TYPE_UPDATE;
            switch (tr.getEvent()) {
            case DELETE:
                updateType = Command.TYPE_DELETE;
                if (sec.newValues != null) {
                    continue;
                }
                break;
            case INSERT:
                updateType = Command.TYPE_INSERT;
                if (sec.oldValues != null) {
                    continue;
                }
                break;
            case UPDATE:
                if (sec.oldValues == null || sec.newValues == null) {
                    continue;
                }
                break;
            }
            //create plan
            ForEachRowPlan result = new ForEachRowPlan();
            result.setSingleRow(true);
            result.setParams(params);
            TriggerAction parseProcedure;
            GroupSymbol gs = new GroupSymbol(sec.table.getFullName());
            try {
                parseProcedure = (TriggerAction)QueryParser.getQueryParser().parseProcedure(tr.getPlan(), true);
                QueryResolver.resolveCommand(parseProcedure, gs, updateType, metadata.getDesignTimeMetadata(), false);
            } catch (QueryParserException e) {
                //should have been validated
                throw new TeiidComponentException(e);
            } catch (QueryResolverException e) {
                //should have been validated
                throw new TeiidComponentException(e);
            }
            CreateProcedureCommand cpc = new CreateProcedureCommand(parseProcedure.getBlock());
            gs.setMetadataID(sec.table);
            cpc.setVirtualGroup(gs);
            cpc.setUpdateType(updateType);
            ProcedurePlan rowProcedure = (ProcedurePlan)QueryOptimizer.optimizePlan(cpc, metadata, idGenerator, capFinder, analysisRecord, context);
            rowProcedure.setRunInContext(false);
            result.setRowProcedure(rowProcedure);
            result.setLookupMap(lookup);
            result.setTupleSource(new CollectionTupleSource(Arrays.asList(tuple).iterator()));
            plans.add(result);
            names.add(tr.getName());
        }
        
        return new CompositeProcessorPlan(plans, names, sec.table);
	}

}
