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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryParserException;
import org.teiid.api.exception.query.QueryResolverException;
import org.teiid.api.exception.query.QueryValidatorException;
import org.teiid.client.metadata.ParameterInfo;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.types.DataTypeManager;
import org.teiid.language.SQLConstants;
import org.teiid.query.QueryPlugin;
import org.teiid.query.analysis.AnalysisRecord;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.metadata.StoredProcedureInfo;
import org.teiid.query.metadata.TempMetadataAdapter;
import org.teiid.query.metadata.TempMetadataID;
import org.teiid.query.metadata.TempMetadataID.Type;
import org.teiid.query.metadata.TempMetadataStore;
import org.teiid.query.parser.QueryParser;
import org.teiid.query.resolver.util.ResolverUtil;
import org.teiid.query.resolver.util.ResolverVisitor;
import org.teiid.query.sql.ProcedureReservedWords;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.GroupContext;
import org.teiid.query.sql.lang.ProcedureContainer;
import org.teiid.query.sql.lang.SPParameter;
import org.teiid.query.sql.lang.StoredProcedure;
import org.teiid.query.sql.proc.CreateProcedureCommand;
import org.teiid.query.sql.proc.TriggerAction;
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.validator.UpdateValidator.UpdateInfo;


public abstract class ProcedureContainerResolver implements CommandResolver {

    public abstract void resolveProceduralCommand(Command command,
                                                  TempMetadataAdapter metadata) throws QueryMetadataException,
                                                                          QueryResolverException,
                                                                          TeiidComponentException;
    
    /**
     * Expand a command by finding and attaching all subcommands to the command.  If
     * some initial resolution must be done for this to be accomplished, that is ok, 
     * but it should be kept to a minimum.
     * @param command The command to expand
     * @param useMetadataCommands True if resolver should use metadata commands to completely resolve
     * @param metadata Metadata access
     * @param analysis The analysis record that will be filled in if doing annotation.
     * 
     * @throws QueryMetadataException If there is a metadata problem
     * @throws QueryResolverException If the query cannot be resolved
     * @throws TeiidComponentException If there is an internal error
     */
    public Command expandCommand(ProcedureContainer procCommand, QueryMetadataInterface metadata, AnalysisRecord analysis)
    throws QueryMetadataException, QueryResolverException, TeiidComponentException {
    	
        // Resolve group so we can tell whether it is an update procedure
        GroupSymbol group = procCommand.getGroup();

        Command subCommand = null;
        
        String plan = getPlan(metadata, procCommand);
        
        if (plan == null) {
            return null;
        }
        
        QueryParser parser = QueryParser.getQueryParser();
        try {
            subCommand = parser.parseProcedure(plan, !(procCommand instanceof StoredProcedure));
        } catch(QueryParserException e) {
             throw new QueryResolverException(QueryPlugin.Event.TEIID30060, e, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30060, group, procCommand.getClass().getSimpleName()));
        }
        
        return subCommand;
    }

    /** 
     * For a given resolver, this returns the unparsed command.
     * 
     * @param metadata
     * @param group
     * @return
     * @throws TeiidComponentException
     * @throws QueryMetadataException
     */
    protected abstract String getPlan(QueryMetadataInterface metadata,
                           GroupSymbol group) throws TeiidComponentException,
                                             QueryMetadataException, QueryResolverException;
        
	public static void addChanging(TempMetadataStore discoveredMetadata,
			GroupContext externalGroups, List<ElementSymbol> elements) {
		List<ElementSymbol> changingElements = new ArrayList<ElementSymbol>(elements.size());
        for(int i=0; i<elements.size(); i++) {
            ElementSymbol virtualElmnt = elements.get(i);
            ElementSymbol changeElement = virtualElmnt.clone();
            changeElement.setType(DataTypeManager.DefaultDataClasses.BOOLEAN);
            changingElements.add(changeElement);
        }

        addScalarGroup(ProcedureReservedWords.CHANGING, discoveredMetadata, externalGroups, changingElements, false);
	}
        
    /** 
     * @see org.teiid.query.resolver.CommandResolver#resolveCommand(org.teiid.query.sql.lang.Command, org.teiid.query.metadata.TempMetadataAdapter, boolean)
     */
    public void resolveCommand(Command command, TempMetadataAdapter metadata, boolean resolveNullLiterals) 
        throws QueryMetadataException, QueryResolverException, TeiidComponentException {
        
        ProcedureContainer procCommand = (ProcedureContainer)command;
        
        resolveGroup(metadata, procCommand);
        
        resolveProceduralCommand(procCommand, metadata);
        
        //getPlan(metadata, procCommand);
    }

	private String getPlan(QueryMetadataInterface metadata, ProcedureContainer procCommand)
			throws TeiidComponentException, QueryMetadataException,
			QueryResolverException {
		if(!procCommand.getGroup().isTempTable() && metadata.isVirtualGroup(procCommand.getGroup().getMetadataID())) {
            String plan = getPlan(metadata, procCommand.getGroup());
            if (plan == null && !metadata.isProcedure(procCommand.getGroup().getMetadataID())) {
            	int type = procCommand.getType();
            	//force validation
            	getUpdateInfo(procCommand.getGroup(), metadata, type, true);
            }
            return plan;
        }
		return null;
	}
	
	public static UpdateInfo getUpdateInfo(GroupSymbol group, QueryMetadataInterface metadata, int type, boolean validate) throws QueryMetadataException, TeiidComponentException, QueryResolverException {
		UpdateInfo info = getUpdateInfo(group, metadata);
		
		if (info == null) {
			return null;
		}
    	if (validate) {
    		String error = validateUpdateInfo(group, type, info);
    		if (error != null) {
    			throw new QueryResolverException(QueryPlugin.Event.TEIID30061, error);
    		}
    	}
    	return info;
	}

	public static String validateUpdateInfo(GroupSymbol group, int type,
			UpdateInfo info) {
		String error = info.getDeleteValidationError();
		String name = "Delete"; //$NON-NLS-1$
		if (type == Command.TYPE_UPDATE) {
			error = info.getUpdateValidationError();
			name = "Update"; //$NON-NLS-1$
		} else if (type == Command.TYPE_INSERT) {
			error = info.getInsertValidationError();
			name = "Insert"; //$NON-NLS-1$
		}
		if (error != null) {
			return QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30061, group, name, error);
		}
		return null;
	}

	public static UpdateInfo getUpdateInfo(GroupSymbol group,
			QueryMetadataInterface metadata) throws TeiidComponentException,
			QueryMetadataException, QueryResolverException {
		if (!QueryResolver.isView(group, metadata)) {
			return null;
		}
		try {
			return QueryResolver.resolveView(group, metadata.getVirtualPlan(group.getMetadataID()), SQLConstants.Reserved.SELECT, metadata).getUpdateInfo();
		} catch (QueryValidatorException e) {
			 throw new QueryResolverException(e);
		}
	}
	
    /** 
     * @param metadata
     * @param procCommand
     * @throws TeiidComponentException
     * @throws QueryResolverException
     */
    protected void resolveGroup(TempMetadataAdapter metadata,
                              ProcedureContainer procCommand) throws TeiidComponentException,
                                                            QueryResolverException {
        // Resolve group so we can tell whether it is an update procedure
        GroupSymbol group = procCommand.getGroup();
        ResolverUtil.resolveGroup(group, metadata);
        if (!group.isTempTable()) {
        	procCommand.setUpdateInfo(ProcedureContainerResolver.getUpdateInfo(group, metadata, procCommand.getType(), false));
        }
    }

    public static GroupSymbol addScalarGroup(String name, TempMetadataStore metadata, GroupContext externalGroups, List<? extends Expression> symbols) {
    	return addScalarGroup(name, metadata, externalGroups, symbols, true);
    }
    
	public static GroupSymbol addScalarGroup(String name, TempMetadataStore metadata, GroupContext externalGroups, List<? extends Expression> symbols, boolean updatable) {
		boolean[] updateArray = new boolean[symbols.size()];
		if (updatable) {
			Arrays.fill(updateArray, true);
		}
		return addScalarGroup(name, metadata, externalGroups, symbols, updateArray);
	}
	
	public static GroupSymbol addScalarGroup(String name, TempMetadataStore metadata, GroupContext externalGroups, List<? extends Expression> symbols, boolean[] updatable) {
		GroupSymbol variables = new GroupSymbol(name);
	    externalGroups.addGroup(variables);
	    TempMetadataID tid = metadata.addTempGroup(name, symbols);
	    tid.setMetadataType(Type.SCALAR);
	    int i = 0;
	    for (TempMetadataID cid : tid.getElements()) {
			cid.setMetadataType(Type.SCALAR);
			cid.setUpdatable(updatable[i++]);
		}
	    variables.setMetadataID(tid);
	    return variables;
	}
	
	/**
	 * Set the appropriate "external" metadata for the given command
	 * @param inferProcedureResultSetColumns 
	 * @throws QueryResolverException 
	 */
	public static void findChildCommandMetadata(Command currentCommand,
			GroupSymbol container, int type, QueryMetadataInterface metadata, boolean inferProcedureResultSetColumns)
			throws QueryMetadataException, TeiidComponentException, QueryResolverException {
		//find the childMetadata using a clean metadata store
	    TempMetadataStore childMetadata = new TempMetadataStore();
	    TempMetadataAdapter tma = new TempMetadataAdapter(metadata, childMetadata);
	    GroupContext externalGroups = new GroupContext();

		if (currentCommand instanceof TriggerAction) {
			TriggerAction ta = (TriggerAction)currentCommand;
			ta.setView(container);
		    //TODO: it seems easier to just inline the handling here rather than have each of the resolvers check for trigger actions
		    List<ElementSymbol> viewElements = ResolverUtil.resolveElementsInGroup(ta.getView(), metadata);
		    if (type == Command.TYPE_UPDATE || type == Command.TYPE_INSERT) {
		    	ProcedureContainerResolver.addChanging(tma.getMetadataStore(), externalGroups, viewElements);
		    	ProcedureContainerResolver.addScalarGroup(SQLConstants.Reserved.NEW, tma.getMetadataStore(), externalGroups, viewElements, false);
		    }
		    if (type == Command.TYPE_UPDATE || type == Command.TYPE_DELETE) {
		    	ProcedureContainerResolver.addScalarGroup(SQLConstants.Reserved.OLD, tma.getMetadataStore(), externalGroups, viewElements, false);
		    }
		} else if (currentCommand instanceof CreateProcedureCommand) {
			CreateProcedureCommand cupc = (CreateProcedureCommand)currentCommand;
			cupc.setVirtualGroup(container);
			if (type == Command.TYPE_STORED_PROCEDURE) {
				StoredProcedureInfo info = metadata.getStoredProcedureInfoForProcedure(container.getName());
		        // Create temporary metadata that defines a group based on either the stored proc
		        // name or the stored query name - this will be used later during planning
		        String procName = info.getProcedureCallableName();
		        
		        // Look through parameters to find input elements - these become child metadata
		        List<ElementSymbol> tempElements = new ArrayList<ElementSymbol>(info.getParameters().size());
		        boolean[] updatable = new boolean[info.getParameters().size()];
		        int i = 0;
		        List<ElementSymbol> rsColumns = Collections.emptyList();
		        for (SPParameter param : info.getParameters()) {
		            if(param.getParameterType() != ParameterInfo.RESULT_SET) {
		                ElementSymbol symbol = param.getParameterSymbol();
		                tempElements.add(symbol);
		                updatable[i++] = param.getParameterType() != ParameterInfo.IN;  
		                if (param.getParameterType() == ParameterInfo.RETURN_VALUE) {
		                	cupc.setReturnVariable(symbol);
		                }
		            } else {
		            	rsColumns = param.getResultSetColumns();
		            }
		        }
		        if (inferProcedureResultSetColumns) {
		        	rsColumns = null;
		        }
		        GroupSymbol gs = ProcedureContainerResolver.addScalarGroup(procName, childMetadata, externalGroups, tempElements, updatable);
		        if (cupc.getReturnVariable() != null) {
		        	ResolverVisitor.resolveLanguageObject(cupc.getReturnVariable(), Arrays.asList(gs), metadata);
		        }
		        cupc.setResultSetColumns(rsColumns);
		        //the relational planner will override this with the appropriate value
		        cupc.setProjectedSymbols(rsColumns);
			} else {
    			cupc.setUpdateType(type);
			}
		}
		
	    QueryResolver.setChildMetadata(currentCommand, childMetadata, externalGroups);
	}

}
