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

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryValidatorException;
import org.teiid.core.TeiidComponentException;
import org.teiid.query.QueryPlugin;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.metadata.SupportConstants;
import org.teiid.query.optimizer.relational.PartitionAnalyzer;
import org.teiid.query.resolver.util.ResolverUtil;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Insert;
import org.teiid.query.sql.lang.Query;
import org.teiid.query.sql.lang.SetQuery;
import org.teiid.query.sql.lang.UnaryFromClause;
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.sql.util.SymbolMap;

/**
 * <p> This visitor is used to validate updates through virtual groups. The command defining
 * the virtual group is always a <code>Query</code>. This object visits various parts of
 * this <code>Query</code> and verifies if the virtual group definition will allows it to be
 * updated.</p>
 */
public class UpdateValidator {
	
	public enum UpdateType {
		/**
		 * The default handling should be used
		 */
		INHERENT, 
		/**
		 * An instead of trigger (TriggerAction) has been defined
		 */
		INSTEAD_OF
	}
	
	public static class UpdateMapping {
		private GroupSymbol group;
		private GroupSymbol correlatedName;
		private Map<ElementSymbol, ElementSymbol> updatableViewSymbols = new HashMap<ElementSymbol, ElementSymbol>();
		private boolean insertAllowed = false;
		private boolean updateAllowed = false;
		
		public Map<ElementSymbol, ElementSymbol> getUpdatableViewSymbols() {
			return updatableViewSymbols;
		}
		
		public boolean isInsertAllowed() {
			return insertAllowed;
		}
		
		public boolean isUpdateAllowed() {
			return updateAllowed;
		}
		
		public GroupSymbol getGroup() {
			return group;
		}
		
		public GroupSymbol getCorrelatedName() {
			return correlatedName;
		}
	}
	
	public static class UpdateInfo {
		private Map<String, UpdateMapping> updatableGroups = new HashMap<String, UpdateMapping>();
		private boolean isSimple = true;
		private UpdateMapping deleteTarget;
		private UpdateType updateType;
		private String updateValidationError;
		private UpdateType deleteType;
		private String deleteValidationError;
		private UpdateType insertType;
		private String insertValidationError;
		private Query view;
		private Map<ElementSymbol, List<Set<Constant>>> partitionInfo;
		private List<UpdateInfo> unionBranches = new LinkedList<UpdateInfo>();
		
		public Map<ElementSymbol, List<Set<Constant>>> getPartitionInfo() {
			return partitionInfo;
		}
		 
		public boolean isSimple() {
			return isSimple;
		}
		
		public UpdateMapping getDeleteTarget() {
			return deleteTarget;
		}
		
		public boolean isInherentDelete() {
			return deleteType == UpdateType.INHERENT;
		}
		
		public boolean isInherentInsert() {
			return insertType == UpdateType.INHERENT;
		}
		
		public boolean isInherentUpdate() {
			return updateType == UpdateType.INHERENT;
		}
		
		public UpdateType getUpdateType() {
			return updateType;
		}
		
		public UpdateType getDeleteType() {
			return deleteType;
		}
		
		public boolean hasValidUpdateMapping(Collection<ElementSymbol> updateCols) {
			if (findUpdateMapping(updateCols, false) == null) {
				return false;
			}
			for (UpdateInfo info : this.unionBranches) {
				if (info.findUpdateMapping(updateCols, false) == null) {
					return false;
				}
			}
			return true;
		}
		
		public UpdateMapping findUpdateMapping(Collection<ElementSymbol> updateCols, boolean insert) {
			if (updateCols.isEmpty() && this.updatableGroups.size() > 1) {
				return null;
			}
			for (UpdateMapping entry : this.updatableGroups.values()) {
				if (((insert && entry.insertAllowed) || (!insert && entry.updateAllowed)) && entry.updatableViewSymbols.keySet().containsAll(updateCols)) {
					return entry;
				}
			}
			return null;
		}
		
		public UpdateMapping findInsertUpdateMapping(Insert insert, boolean rewrite) throws QueryValidatorException {
			if (getUnionBranches().isEmpty()) {
				return findUpdateMapping(insert.getVariables(), true);	
			}
			if (insert.getQueryExpression() != null) {
				//TODO: this could be done in a loop, see about adding a validation
				 throw new QueryValidatorException(QueryPlugin.Event.TEIID30239, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30239, insert.getGroup()));
			}
			int partition = -1;
			List<ElementSymbol> filteredColumns = new LinkedList<ElementSymbol>();
			for (Map.Entry<ElementSymbol, List<Set<Constant>>> entry : partitionInfo.entrySet()) {
				int index = insert.getVariables().indexOf(entry.getKey());
				if (index == -1) {
					continue;
				}
				Expression value = (Expression)insert.getValues().get(index);
				if (!(value instanceof Constant)) {
					continue;
				}
				for (int i = 0; i < entry.getValue().size(); i++) {
					if (entry.getValue().get(i).contains(value)) {
						if (entry.getValue().get(i).size() == 1) {
							filteredColumns.add(entry.getKey());
						}
						if (partition == -1) {
							partition = i;
						} else if (partition != i) {
							throw new QueryValidatorException(QueryPlugin.Event.TEIID30240, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30240, insert.getGroup(), insert.getVariables()));
						}
					}
				}
			}
			if (partition == -1) {
				 throw new QueryValidatorException(QueryPlugin.Event.TEIID30241, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30241, insert.getGroup(), insert.getVariables()));
			}
			UpdateInfo info = this;
			if (partition > 0) {
				info = info.getUnionBranches().get(partition - 1);
			}
			List<ElementSymbol> variables = new ArrayList<ElementSymbol>(insert.getVariables());
			variables.removeAll(filteredColumns);
			UpdateMapping mapping = info.findUpdateMapping(variables, true);
			if (rewrite && mapping != null && !filteredColumns.isEmpty()) {
				for (ElementSymbol elementSymbol : filteredColumns) {
					if (mapping.getUpdatableViewSymbols().containsKey(elementSymbol)) {
						continue;
					}
					int index = insert.getVariables().indexOf(elementSymbol);
					insert.getVariables().remove(index);
					if (rewrite) {
						insert.getValues().remove(index);
					}
				}
			}
			return mapping;
		}
		
		public Query getViewDefinition() {
			return view;
		}
		
		public String getDeleteValidationError() {
			return deleteValidationError;
		}
		
		public String getInsertValidationError() {
			return insertValidationError;
		}
		
		public String getUpdateValidationError() {
			return updateValidationError;
		}
		
		public List<UpdateInfo> getUnionBranches() {
			return unionBranches;
		}

		private void setUpdateValidationError(String updateValidationError) {
			if (this.updateValidationError == null) {
				this.updateValidationError = updateValidationError;
			}
		}

		private void setInsertValidationError(String insertValidationError) {
			if (this.insertValidationError == null) {
				this.insertValidationError = insertValidationError;
			}
		}

		private void setDeleteValidationError(String deleteValidationError) {
			if (this.deleteValidationError == null) {
				this.deleteValidationError = deleteValidationError;
			}
		}
		
	}
	
	private QueryMetadataInterface metadata;
	private UpdateInfo updateInfo = new UpdateInfo();
	
	private ValidatorReport report = new ValidatorReport();
	private ValidatorReport insertReport = new ValidatorReport();
	private ValidatorReport updateReport = new ValidatorReport();
	private ValidatorReport deleteReport = new ValidatorReport();
	
	public UpdateValidator(QueryMetadataInterface qmi, UpdateType insertType, UpdateType updateType, UpdateType deleteType) {
		this.metadata = qmi;
		this.updateInfo.deleteType = deleteType;
		this.updateInfo.insertType = insertType;
		this.updateInfo.updateType = updateType;
	}
		
	public UpdateInfo getUpdateInfo() {
		return updateInfo;
	}
	
	public ValidatorReport getReport() {
		return report;
	}
	
	public ValidatorReport getDeleteReport() {
		return deleteReport;
	}
	
	public ValidatorReport getInsertReport() {
		return insertReport;
	}
	
	public ValidatorReport getUpdateReport() {
		return updateReport;
	}
	
	private void handleValidationError(String error, boolean update, boolean insert, boolean delete) {
		if (update && insert && delete) {
			report.handleValidationError(error);
			updateInfo.setUpdateValidationError(error);
			updateInfo.setInsertValidationError(error);
			updateInfo.setDeleteValidationError(error);
		} else {
			if (update) {
				updateReport.handleValidationError(error);
				updateInfo.setUpdateValidationError(error);
			}
			if (insert) {
				insertReport.handleValidationError(error);
				updateInfo.setInsertValidationError(error);
			}
			if (delete) {
				deleteReport.handleValidationError(error);
				updateInfo.setDeleteValidationError(error);
			}
		}
	}
	
    public void validate(Command command, List<ElementSymbol> viewSymbols) throws QueryMetadataException, TeiidComponentException {
    	if (this.updateInfo.deleteType != UpdateType.INHERENT && this.updateInfo.updateType != UpdateType.INHERENT && this.updateInfo.insertType != UpdateType.INHERENT) {
    		return;
    	}
    	if (command instanceof SetQuery) {
    		SetQuery setQuery = (SetQuery)command;
        	if (setQuery.getLimit() != null) {
        		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0013"), true, true, true); //$NON-NLS-1$
        		return;
        	}
    		LinkedList<Query> queries = new LinkedList<Query>();
    		if (!PartitionAnalyzer.extractQueries((SetQuery)command, queries)) {
    			handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0001"), true, true, true); //$NON-NLS-1$
        		return;    			
    		}
        	Map<ElementSymbol, List<Set<Constant>>> partitions = PartitionAnalyzer.extractPartionInfo((SetQuery)command, viewSymbols);
        	this.updateInfo.partitionInfo = partitions;
        	if (partitions.isEmpty()) {
        		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0018"), false, true, false); //$NON-NLS-1$
        	}
        	boolean first = true;
        	for (Query query : queries) {
        		UpdateInfo ui = this.updateInfo;
        		if (!first) {
	        		this.updateInfo = new UpdateInfo();
	        		this.updateInfo.deleteType = ui.deleteType;
	        		this.updateInfo.insertType = ui.insertType;
	        		this.updateInfo.updateType = ui.updateType;
        		}
            	internalValidate(query, viewSymbols);
        		//accumulate the errors on the first branch - will be checked at resolve time
            	if (this.updateInfo.getDeleteValidationError() != null) {
            		ui.setDeleteValidationError(this.updateInfo.getDeleteValidationError());
            	}
            	if (this.updateInfo.getUpdateValidationError() != null) {
            		ui.setUpdateValidationError(this.updateInfo.getUpdateValidationError());
            	}
            	if (this.updateInfo.getInsertValidationError() != null) {
            		ui.setInsertValidationError(this.updateInfo.getInsertValidationError());
            	}
        		if (!first) {
        			ui.unionBranches.add(this.updateInfo);
        			this.updateInfo = ui;
        		} else {
        			first = false;
        		}
			}
        	return;
    	}
    	internalValidate(command, viewSymbols);
    	if (this.updateInfo.deleteType != UpdateType.INHERENT) {
    		this.deleteReport.getItems().clear();
    		this.updateInfo.deleteValidationError = null;
    	}
    	if (this.updateInfo.updateType != UpdateType.INHERENT) {
    		this.updateReport.getItems().clear();
    		this.updateInfo.updateValidationError = null;
    	}
    	if (this.updateInfo.insertType != UpdateType.INHERENT) {
    		this.insertReport.getItems().clear();
    		this.updateInfo.insertValidationError = null;
    	}
    }
	
    private void internalValidate(Command command, List<ElementSymbol> viewSymbols) throws QueryMetadataException, TeiidComponentException {
    	if (!(command instanceof Query)) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0001"), true, true, true); //$NON-NLS-1$
    		return;
        }
    	
    	Query query = (Query)command;

    	if (query.getFrom() == null || query.getInto() != null) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0001"), true, true, true); //$NON-NLS-1$
    		return;
    	}
    	
    	if (query.getWith() != null) {
    		String warning = QueryPlugin.Util.getString("ERR.015.012.0002"); //$NON-NLS-1$
    		updateReport.handleValidationWarning(warning);
    		deleteReport.handleValidationWarning(warning); 
    		updateInfo.isSimple = false;
    	}

    	if (query.hasAggregates()) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0006"), true, true, true); //$NON-NLS-1$
    		return;
    	}
    	
    	if (query.getLimit() != null) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0013"), true, true, true); //$NON-NLS-1$
    		return;
    	}
    	
    	if (query.getSelect().isDistinct()) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0008"), true, true, true); //$NON-NLS-1$
    		return;
    	} 
    	
    	updateInfo.view = query;
    	
    	List<Expression> projectedSymbols = query.getSelect().getProjectedSymbols();
    	
    	for (int i = 0; i < projectedSymbols.size(); i++) {
            Expression symbol = projectedSymbols.get(i);
            Expression ex = SymbolMap.getExpression(symbol);
            
            if (!metadata.elementSupports(viewSymbols.get(i).getMetadataID(), SupportConstants.Element.UPDATE)) {
            	continue;
            }
            if (ex instanceof ElementSymbol) {
            	ElementSymbol es = (ElementSymbol)ex;
            	String groupName = es.getGroupSymbol().getName();
        		UpdateMapping info = updateInfo.updatableGroups.get(groupName);
        		if (es.getGroupSymbol().getDefinition() != null) {
            		ElementSymbol clone = es.clone();
            		clone.setOutputName(null);
            		clone.getGroupSymbol().setName(clone.getGroupSymbol().getNonCorrelationName());
            		clone.getGroupSymbol().setDefinition(null);
            		es = clone;
            	}
            	if (info == null) {
            		info = new UpdateMapping();
            		info.group = es.getGroupSymbol();
            		info.correlatedName = ((ElementSymbol)ex).getGroupSymbol();
            		updateInfo.updatableGroups.put(groupName, info);
            	}
            	//TODO: warn if mapped twice
            	info.updatableViewSymbols.put(viewSymbols.get(i), es);
            } else {
            	//TODO: look for reversable widening conversions
            	
                report.handleValidationWarning(QueryPlugin.Util.getString("ERR.015.012.0007", viewSymbols.get(i), symbol)); //$NON-NLS-1$
            }
    	}
    	
    	if (query.getFrom().getClauses().size() > 1 || (!(query.getFrom().getClauses().get(0) instanceof UnaryFromClause))) {
    	    String warning = QueryPlugin.Util.getString("ERR.015.012.0009", query.getFrom()); //$NON-NLS-1$
    		updateReport.handleValidationWarning(warning); 
    		deleteReport.handleValidationWarning(warning);
    		updateInfo.isSimple = false;
    	}
    	List<GroupSymbol> allGroups = query.getFrom().getGroups();
    	HashSet<GroupSymbol> keyPreservingGroups = new HashSet<GroupSymbol>();
    	
		ResolverUtil.findKeyPreserved(query, keyPreservingGroups, metadata);
    	
		for (GroupSymbol groupSymbol : keyPreservingGroups) {
			setUpdateFlags(groupSymbol);
		}

		allGroups.removeAll(keyPreservingGroups);
		if (updateInfo.isSimple) {
			if (!allGroups.isEmpty()) {
				setUpdateFlags(allGroups.iterator().next());
			}
		} else {
			for (GroupSymbol groupSymbol : allGroups) {
				UpdateMapping info = updateInfo.updatableGroups.get(groupSymbol.getName());
				if (info == null) {
					continue; // not projected
				}
	    		String warning = QueryPlugin.Util.getString("ERR.015.012.0004", info.correlatedName); //$NON-NLS-1$
	    		report.handleValidationWarning(warning);
			}
		}

    	boolean updatable = false;
    	boolean insertable = false;
    	for (UpdateMapping info : updateInfo.updatableGroups.values()) {
    		if (info.updateAllowed) {
    			if (!updatable) {
    				this.updateInfo.deleteTarget = info;
    			} else if (!info.getGroup().equals(this.updateInfo.deleteTarget.getGroup())){
    				//TODO: warning about multiple
    				this.updateInfo.deleteTarget = null;
    			}
    		}
    		updatable |= info.updateAllowed;
    		insertable |= info.insertAllowed;
    	}
    	if ((this.updateInfo.insertType == UpdateType.INHERENT && !insertable)) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0015"), false, true, false); //$NON-NLS-1$
    	} 
    	if (this.updateInfo.updateType == UpdateType.INHERENT && !updatable) {
    		handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0005"), true, false, false); //$NON-NLS-1$
    	}
    	if (this.updateInfo.deleteType == UpdateType.INHERENT && this.updateInfo.deleteTarget == null) {
    		if (this.updateInfo.isSimple && updatable) {
    			this.updateInfo.deleteTarget = this.updateInfo.updatableGroups.values().iterator().next();
    		} else {
    			handleValidationError(QueryPlugin.Util.getString("ERR.015.012.0014"), false, false, true); //$NON-NLS-1$
    		}
    	}
    }

    private void setUpdateFlags(GroupSymbol groupSymbol) throws QueryMetadataException, TeiidComponentException {
    	UpdateMapping info = updateInfo.updatableGroups.get(groupSymbol.getName());

		if (info == null) {
			return; // not projected
		}

		if (!metadata.groupSupports(groupSymbol.getMetadataID(), SupportConstants.Group.UPDATE)) {
			report.handleValidationWarning(QueryPlugin.Util.getString("ERR.015.012.0003", groupSymbol)); //$NON-NLS-1$
			return;
		}

		info.insertAllowed = true;
		for (ElementSymbol es : ResolverUtil.resolveElementsInGroup(info.group, metadata)) {
			if (!info.updatableViewSymbols.values().contains(es) && !validateInsertElement(es)) {
				info.insertAllowed = false;
			}
		}
		info.updateAllowed = true;
    }

	/**
	 * <p> This method validates an elements present in the group specified in the
	 * FROM clause of the query but not specified in its SELECT clause</p>
	 * @param element The <code>ElementSymbol</code> being validated
	 * @throws TeiidComponentException 
	 * @throws QueryMetadataException 
	 */
	private boolean validateInsertElement(ElementSymbol element) throws QueryMetadataException, TeiidComponentException {
		// checking if the elements not specified in the query are required.
		if(metadata.elementSupports(element.getMetadataID(), SupportConstants.Element.NULL) 
			|| metadata.elementSupports(element.getMetadataID(), SupportConstants.Element.DEFAULT_VALUE) 
			|| metadata.elementSupports(element.getMetadataID(), SupportConstants.Element.AUTO_INCREMENT)) {
			return true;
		}
		if (this.updateInfo.insertType == UpdateType.INHERENT) {
			insertReport.handleValidationWarning(QueryPlugin.Util.getString("ERR.015.012.0010", element, element.getGroupSymbol())); //$NON-NLS-1$
		}
	    return false;
	}
}
