/*
 * 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.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
// https://issues.jboss.org/browse/TEIID-4008
// https://issues.jboss.org/browse/TEIID-2281
// https://issues.jboss.org/browse/TEIID-2572
// https://issues.jboss.org/browse/TEIID-3523
// https://issues.jboss.org/browse/TEIID-2176
// https://issues.jboss.org/browse/TEIID-2177
// https://issues.redhat.com/browse/TEIID-6043
// https://issues.redhat.com/browse/TEIID-5528
// https://issues.redhat.com/browse/TEIID-5313
// https://issues.redhat.com/browse/TEIID-3168
// https://issues.redhat.com/browse/TEIID-4202
/*
 */
package org.teiid.translator.jdbc;

import org.teiid.core.types.BinaryType;
import org.teiid.language.*;
import org.teiid.language.Argument.Direction;
import org.teiid.language.Like.MatchMode;
import org.teiid.language.SQLConstants.Reserved;
import org.teiid.language.SQLConstants.Tokens;
import org.teiid.language.SetQuery.Operation;
import org.teiid.language.visitor.SQLStringVisitor;
import org.teiid.metadata.AbstractMetadataRecord;
import org.teiid.metadata.FunctionMethod;
import org.teiid.metadata.Procedure;
import org.teiid.translator.ExecutionContext;
import org.teiid.translator.TypeFacility;

import java.sql.Time;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static org.teiid.language.SQLConstants.Reserved.AS;


/**
 * This visitor takes an ICommand and does DBMS-specific conversion on it
 * to produce a SQL String.  This class is expected to be subclassed.
 */
public class SQLConversionVisitor extends SQLStringVisitor implements SQLStringVisitor.Substitutor {
	public static final String TEIID_NON_PREPARED = AbstractMetadataRecord.RELATIONAL_URI + "non-prepared"; //$NON-NLS-1$


	// BUG: Using the default Locale can produce wrong number conversions (e.g. a comma is used instead of a dot in decimal numbers)  
	// ISSUE: DV-72
	// SOLUTION: force the English Locale in the DecimalFormat
	// FIXED by Salvatore Raunich
	// date 06.02.2013
//    private static DecimalFormat DECIMAL_FORMAT = 
//            new DecimalFormat("#############################0.0#############################"); //$NON-NLS-1$    
    private static DecimalFormat DECIMAL_FORMAT = 
         new DecimalFormat("#############################0.0#############################", new DecimalFormatSymbols(Locale.ENGLISH)); //$NON-NLS-1$    	
	// BUG END
    private static double SCIENTIC_LOW = Math.pow(10, -3);
    private static double SCIENTIC_HIGH = Math.pow(10, 7);

    private ExecutionContext context;
    private JDBCExecutionFactory executionFactory;

    private boolean prepared;
	private boolean usingBinding;
    
    private List preparedValues = new ArrayList();
    
    private Set<LanguageObject> recursionObjects = Collections.newSetFromMap(new IdentityHashMap<LanguageObject, Boolean>());
    private Map<LanguageObject, Object> translations = new IdentityHashMap<LanguageObject, Object>(); 
    
    private boolean replaceWithBinding = false;
    
    // EXTENSION
 	// ISSUE: DVCORE-2598
 	// FIXED BY: Salvatore Raunich
 	// DATE: 2015.01.14
    protected boolean useDoubleSlashToEscapeRegex = false;
	protected boolean isLikeRegex = false;
	// END EXTENSION
    
	protected boolean isUpdate = false;

    private boolean addNullCast;
    private int startDerivedColumn = -1;

    protected boolean forceUnicodePrefix = false;

	public SQLConversionVisitor(JDBCExecutionFactory ef) {
        this.executionFactory = ef;
        this.prepared = executionFactory.usePreparedStatements();
    	// EXTENSION
    	// ISSUE: DVCORE-749
    	// FIXED BY: Roman Pedchenko
    	// DATE: 2013.11.20
        doQuote = executionFactory.forceQuotedIdentifiers();
    	// END EXTENSION
        
        // EXTENSION
    	// ISSUE: DVCORE-2598
    	// FIXED BY: Salvatore Raunich
    	// DATE: 2015.01.14
        useDoubleSlashToEscapeRegex = executionFactory.useDoubleSlashToEscapeRegex();
        // END EXTENSION
    }
    
    @Override
    public void append(LanguageObject obj) {
    	// EXTENSION
    	// ISSUE: DVCORE-2598
    	// FIXED BY: Salvatore Raunich
    	// DATE: 2015.01.14
    	if ((obj instanceof Like) && ((Like) obj).getMode().equals(MatchMode.REGEX))
    		this.isLikeRegex = true;
    	// END EXTENSION
        boolean replacementMode = replaceWithBinding;
        if (obj instanceof Command || obj instanceof Function) {
    	    /*
    	     * In general it is not appropriate to use bind values within a function
    	     * unless the particulars of the function parameters are know.  
    	     * As needed, other visitors or modifiers can set the literals used within
    	     * a particular function as bind variables.  
    	     */
        	this.replaceWithBinding = false;
        }
    	List<?> parts = null;
    	if (!recursionObjects.contains(obj)) {
    		Object trans = this.translations.get(obj);
    		if (trans instanceof List<?>) {
    			parts = (List<?>)trans;
    		} else if (trans instanceof LanguageObject) {
    			obj = (LanguageObject)trans;
    		} else {
    			parts = executionFactory.translate(obj, context, isUpdate);
    			if (parts != null) {
    				this.translations.put(obj, parts);
    			} else {
    				this.translations.put(obj, obj);
    			}
    		}
    	}
		if (parts != null) {
			recursionObjects.add(obj);
			for (Object part : parts) {
			    if(part instanceof LanguageObject) {
			        append((LanguageObject)part);
			    } else {
			        buffer.append(part);
			    }
			}
			recursionObjects.remove(obj);
		} else {
			super.append(obj);
		}
        this.replaceWithBinding = replacementMode;
        // EXTENSION
    	// ISSUE: DVCORE-2598
    	// FIXED BY: Salvatore Raunich
    	// DATE: 2015.01.14
        if ((obj instanceof Like) && ((Like) obj).getMode().equals(MatchMode.REGEX))
    		this.isLikeRegex = false;
        // END EXTENSION
    }

    protected boolean preserveNullTyping() {
        return executionFactory.preserveNullTyping();
    }

    /**
     * @param type
     * @param object
     * @param valuesbuffer
     */
    protected void translateSQLType(Class<?> type, Object obj, StringBuilder valuesbuffer) {
        translateSQLType(type, obj, valuesbuffer, false);
    }

    protected void translateSQLType(Class<?> type, Object obj, StringBuilder valuesbuffer, boolean noPrefix) {
        if (obj == null) {
            valuesbuffer.append(Reserved.NULL);
        } else {
            if(Number.class.isAssignableFrom(type)) {
                boolean useFormatting = false;

                if (Double.class.isAssignableFrom(type)){
                    obj = executionFactory.pretranslateDouble(obj);
                    double value = ((Double)obj).doubleValue();
                    useFormatting = (value <= SCIENTIC_LOW || value >= SCIENTIC_HIGH);
                }
                else if (Float.class.isAssignableFrom(type)){
                    obj = executionFactory.pretranslateFloat(obj);
                    float value = ((Float)obj).floatValue();
                    useFormatting = (value <= SCIENTIC_LOW || value >= SCIENTIC_HIGH);
                }

                if (Double.class.isAssignableFrom(type))
                    obj = executionFactory.translateLiteralDouble((Double) obj);
                else if (Float.class.isAssignableFrom(type))
                    obj = executionFactory.translateLiteralFloat((Float) obj);

                // The formatting is to avoid the so-called "scientic-notation"
                // where toString will use for numbers greater than 10p7 and
                // less than 10p-3, where database may not understand.
                if (!(obj instanceof String) // if not translated already
                        && useFormatting) {
                	synchronized (DECIMAL_FORMAT) {
                        valuesbuffer.append(DECIMAL_FORMAT.format(obj));
					}
                }
                else {
                    valuesbuffer.append(obj);
                }
            } else if(type.equals(TypeFacility.RUNTIME_TYPES.BOOLEAN)) {
                valuesbuffer.append(executionFactory.translateLiteralBoolean((Boolean)obj));
            } else if(type.equals(TypeFacility.RUNTIME_TYPES.TIMESTAMP)) {
                valuesbuffer.append(executionFactory.translateLiteralTimestamp((Timestamp)obj));
            } else if(type.equals(TypeFacility.RUNTIME_TYPES.TIME)) {
                valuesbuffer.append(executionFactory.translateLiteralTime((Time)obj));
            } else if(type.equals(TypeFacility.RUNTIME_TYPES.DATE)) {
                valuesbuffer.append(executionFactory.translateLiteralDate((java.sql.Date)obj));
            } else if(type.equals(TypeFacility.RUNTIME_TYPES.VARBINARY)) {
	            valuesbuffer.append(executionFactory.translateLiteralBinaryType((BinaryType)obj));
            } else {
                // If obj is string, toSting() will not create a new String 
                // object, it returns it self, so new object creation.
            	// EXTENSION
            	// ISSUE: DVCORE-2598
            	// FIXED BY: Salvatore Raunich
            	// DATE: 2015.01.14
//            	valuesbuffer.append(Tokens.QUOTE)
//                	.append(escapeString(obj.toString(), Tokens.QUOTE))
//                	.append(Tokens.QUOTE);
                String val = obj.toString();
                final String prefix = noPrefix ? "" : executionFactory.translateGetStringLiteralPrefix();
                if (prefix == null || prefix.isEmpty()) {
                    if (forceUnicodePrefix || (useUnicodePrefix() && isNonAscii(val))) {
                        buffer.append("N"); //$NON-NLS-1$
                    }
                }
                val = executionFactory.stringCutConvertFail(val);
            	if (useDoubleSlashToEscapeRegex && isLikeRegex) {
            		String valueToAppend = escapeString(val, Tokens.QUOTE);
            		valueToAppend = escapeString(valueToAppend, "\\");
					valuesbuffer.append(prefix).append(Tokens.QUOTE)
						.append(valueToAppend)
						.append(Tokens.QUOTE);
            	} else {
	                valuesbuffer.append(prefix).append(Tokens.QUOTE)
	                      .append(escapeString(val, Tokens.QUOTE))
	                      .append(Tokens.QUOTE);
            	}
            	// END EXTENSION
            }
        }        
    }

    public static boolean isNonAscii(String val) {
        for (int i = 0; i < val.length(); i++) {
            if (val.charAt(i) > 127) {
                return true;
            }
        }
        return false;
    }

    protected boolean useUnicodePrefix() {
        return this.executionFactory.useUnicodePrefix();
    }
    /**
     * @see org.teiid.language.visitor.SQLStringVisitor#visit(org.teiid.language.Call)
     */
    public void visit(Call obj) {
	    usingBinding = true;
    	Procedure p = obj.getMetadataObject();
    	if (p != null) {
	    	String nativeQuery = p.getProperty(TEIID_NATIVE_QUERY, false);
	    	if (nativeQuery != null) {
	    		this.prepared = !Boolean.valueOf(p.getProperty(TEIID_NON_PREPARED, false));
	    		if (this.prepared) {
	    			this.preparedValues = new ArrayList<Object>();
	    		}
	    		parseNativeQueryParts(nativeQuery, obj.getArguments(), buffer, this);
	    		return;
	    	}
    	}
	    if (obj.isTableReference()) {
		    usingBinding = false;
		    super.visit(obj);
		    return;
	    }
		this.prepared = true;
		/*
         * preparedValues is now a list of procedure params instead of just values
         */
        this.preparedValues = obj.getArguments();
        buffer.append(generateSqlForStoredProcedure(obj));
    }

	public void visit(Function obj) {
		FunctionMethod f = obj.getMetadataObject();
		if (f != null) {
			String nativeQuery = f.getProperty(TEIID_NATIVE_QUERY, false);
			if (nativeQuery != null) {
				List<Argument> args = new ArrayList<Argument>(obj.getParameters().size());
				for (Expression p : obj.getParameters()) {
					args.add(new Argument(Direction.IN, p, p.getType(), null));
				}
				parseNativeQueryParts(nativeQuery, args, buffer, this);
				return;
			}
		}
        if (obj.getParameters().size() == 1) {
            Expression param = obj.getParameters().get(0);
            if (param instanceof Literal) {
                Literal l = (Literal)param;
                addNullCast = l.getValue() == null;
            }
        }
        super.visit(obj);
        addNullCast = false;
	}

    @Override
    public void visit(AggregateFunction obj) {
        if (obj.getParameters().size() == 1) {
            Expression param = obj.getParameters().get(0);
            if (param instanceof Literal) {
                Literal l = (Literal) param;
                addNullCast = l.getValue() == null;
            }
        }
        super.visit(obj);
        addNullCast = false;
    }

	@Override
	public void visit(Parameter obj) {
        addBinding(obj);
    }

    /**
     * Add a bind ? value to the sql string and to the binding value list
     * @param value should be an {@link Argument}, {@link Parameter}, or {@link Argument}
     */
    protected void addBinding(LanguageObject value) {
        buffer.append(UNDEFINED_PARAM);
        preparedValues.add(value);
        if (columnHelper != null)
            this.executionFactory.prepareBind(columnHelper, null, buffer);
        usingBinding = true;
    }

    public static class NoPrefixLiteral extends Literal {
        public NoPrefixLiteral(Literal lit) {
            super(lit.getValue(), lit.getType());
        }
    }

    /**
     * @see org.teiid.language.visitor.SQLStringVisitor#visit(org.teiid.language.Literal)
     */
    public void visit(Literal obj) {
        if (this.prepared && ((replaceWithBinding && obj.isBindEligible()) || TranslatedCommand.isBindEligible(obj))) {
            addBinding(obj);
        } else {
            //check to see if the null should be in a cast
            //this is somewhat limited in scope to situations that could be ambiguous or
            //requried in projection
            if (preserveNullTyping() && obj.getValue() == null && (addNullCast || startDerivedColumn == buffer.length())) {
                String type = TypeFacility.RUNTIME_NAMES.INTEGER;
                if (obj.getType() != TypeFacility.RUNTIME_TYPES.NULL) {
                    type = TypeFacility.getDataTypeName(obj.getType());
                }
                addNullCast = false;
                append(ConvertModifier.createConvertFunction(LanguageFactory.INSTANCE, obj, type));
                addNullCast = true;
                return;
            }
            boolean noPrefix = obj instanceof NoPrefixLiteral;
            translateSQLType(obj.getType(), obj.getValue(), buffer, noPrefix);
            if (columnHelper != null)
                this.executionFactory.prepareBind(columnHelper, obj, buffer);
        }
    }
    
    @Override
    public void visit(In obj) {
        replaceWithBinding = true;
        super.visit(obj);
    }

    @Override
    public void visit(Like obj) {
        replaceWithBinding = true;
        super.visit(obj);
    }

    @Override
    public void visit(Comparison obj) {
        replaceWithBinding = true;
        super.visit(obj);
    }

    @Override
    public void visit(ExpressionValueSource obj) {
        replaceWithBinding = true;
        super.visit(obj);
    }
    
    @Override
    public void visit(SetClause obj) {
        replaceWithBinding = true;
        columnHelper = obj.getSymbol();
        super.visit(obj);
        columnHelper = null;
    }
    
    @Override
    public void visit(DerivedColumn obj) {
    	replaceWithBinding = false;
        startDerivedColumn = buffer.length();
        super.visit(obj);
        startDerivedColumn = -1;
    }
    
    @Override
    public void visit(SearchedCase obj) {
    	replaceWithBinding = false;
    	super.visit(obj);
    }

    /**
     * Set the per-command execution context on this visitor. 
     * @param context ExecutionContext
     * @since 4.3
     */
    public void setExecutionContext(ExecutionContext context) {
        this.context = context;
    }
    
    /**
     * Retrieve the per-command execution context for this visitor 
     * (intended for subclasses to use).
     * @return
     * @since 4.3
     */
    protected ExecutionContext getExecutionContext() {
        return this.context;
    }

    protected String getSourceComment(Command command) {
    	return this.executionFactory.getSourceComment(this.context, command);
    }
    
    /**
     * This is a generic implementation. Subclass should override this method
     * if necessary.
     * @param exec The command for the stored procedure.
     * @return String to be executed by CallableStatement.
     */
    protected String generateSqlForStoredProcedure(Call exec) {
        StringBuffer prepareCallBuffer = new StringBuffer();
        prepareCallBuffer.append("{ "); //$NON-NLS-1$

        List<Argument> params = exec.getArguments();

        //check whether a "?" is needed if there are returns
        boolean needQuestionMark = exec.getReturnType() != null;
        
        prepareCallBuffer.append(getSourceComment(exec));
        
        if(needQuestionMark){
            prepareCallBuffer.append("?="); //$NON-NLS-1$
        }

        prepareCallBuffer.append(" call ");//$NON-NLS-1$
        prepareCallBuffer.append(exec.getMetadataObject() != null ? getName(exec.getMetadataObject()) : exec.getProcedureName());
        prepareCallBuffer.append("("); //$NON-NLS-1$

        int numberOfParameters = 0;
        for (Argument param : params) {
            if(param.getDirection() == Direction.IN || param.getDirection() == Direction.OUT || param.getDirection() == Direction.INOUT){
                if(numberOfParameters > 0){
                    prepareCallBuffer.append(","); //$NON-NLS-1$
                }
                prepareCallBuffer.append("?"); //$NON-NLS-1$
                numberOfParameters++;
            }
        }
        prepareCallBuffer.append(")"); //$NON-NLS-1$
        prepareCallBuffer.append("}"); //$NON-NLS-1$
        return prepareCallBuffer.toString();
    }
    
    /** 
     * @return the preparedValues
     */
    public List getPreparedValues() {
        return this.preparedValues;
    }
    
    public boolean isPrepared() {
		return prepared;
	}
    
    public void setPrepared(boolean prepared) {
		this.prepared = prepared;
	}

	public boolean isUsingBinding() {
		return usingBinding;
    }
    
    @Override
    protected boolean useAsInGroupAlias() {
    	return this.executionFactory.useAsInGroupAlias();
    }
        
    @Override
    protected boolean useParensForSetQueries() {
    	return executionFactory.useParensForSetQueries();
    }
    	
	@Override
	protected String replaceElementName(String group, String element) {
		return executionFactory.replaceElementName(group, element);
	}
	
	@Override
	protected void appendSetOperation(Operation operation) {
		buffer.append(executionFactory.getSetOperationString(operation));
	}
    
	@Override
    protected boolean useParensForJoins() {
    	return executionFactory.useParensForJoins();
    }
	
	protected boolean useSelectLimit() {
		return executionFactory.useSelectLimit();
	}
	
	@Override
	protected String getLikeRegexString() {
		return executionFactory.getLikeRegexString();
	}
	
	@Override
	protected void appendBaseName(NamedTable obj) {
		if (obj.getMetadataObject() != null) {
			String nativeQuery = obj.getMetadataObject().getProperty(TEIID_NATIVE_QUERY, false);
	    	if (nativeQuery != null) {
	    		buffer.append(Tokens.LPAREN).append(nativeQuery).append(Tokens.RPAREN);
	    		if (obj.getCorrelationName() == null) {
	                buffer.append(Tokens.SPACE);
	    			if (useAsInGroupAlias()){
	                    buffer.append(AS).append(Tokens.SPACE);
	                }
	    		}
	    	}
		}
		super.appendBaseName(obj);
	}

	@Override
	public void substitute(Argument arg, StringBuilder builder, int index) {
		if (this.prepared && arg.getExpression() instanceof Literal) {
			buffer.append(UNDEFINED_PARAM);
			this.preparedValues.add(arg);
		} else {
			visit(arg);
		}
	}

	@Override
	public void visit(Insert obj) {
		isUpdate = true;
		super.visit(obj);
	}

	@Override
	public void visit(Update obj) {
		isUpdate = true;
		super.visit(obj);
	}

	@Override
	protected void appendLateralKeyword() {
		buffer.append(this.executionFactory.getLateralKeyword());
	}
}
