/*
 * Copyright Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags and
 * the COPYRIGHT.txt file distributed with this work.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.teiid.translator.odata4;

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

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

import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.teiid.language.*;
import org.teiid.language.SQLConstants.Tokens;
import org.teiid.language.visitor.HierarchyVisitor;
import org.teiid.metadata.BaseColumn;
import org.teiid.metadata.Column;
import org.teiid.metadata.FunctionMethod;
import org.teiid.metadata.RuntimeMetadata;
import org.teiid.metadata.Table;
import org.teiid.olingo.common.ODataTypeManager;
import org.teiid.translator.SourceSystemFunctions;
import org.teiid.translator.TranslatorException;

/**
 * Only walks the filter version of the query
 */
public class ODataFilterVisitor extends HierarchyVisitor {

	private static Map<String, String> infixFunctions = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);

	static {
		infixFunctions.put("%", "mod");//$NON-NLS-1$ //$NON-NLS-2$
		infixFunctions.put("mod", "mod");//$NON-NLS-1$ //$NON-NLS-2$
		infixFunctions.put("+", "add");//$NON-NLS-1$ //$NON-NLS-2$
		infixFunctions.put("-", "sub");//$NON-NLS-1$ //$NON-NLS-2$
		infixFunctions.put("*", "mul");//$NON-NLS-1$ //$NON-NLS-2$
		infixFunctions.put("/", "div");//$NON-NLS-1$ //$NON-NLS-2$
	}

	protected StringBuilder filter = new StringBuilder();
	private ODataExecutionFactory ef;
	protected ArrayList<TranslatorException> exceptions = new ArrayList<TranslatorException>();
	private ODataQuery query;
	private RuntimeMetadata metadata;
	private ODataDocumentNode filterOnElement;
	private BaseColumn currentExpression;

	public ODataFilterVisitor(ODataExecutionFactory ef, RuntimeMetadata metadata, ODataQuery query) {
		this.ef = ef;
		this.query = query;
		this.metadata = metadata;
	}

	public void appendFilter(Condition condition) throws TranslatorException {
		append(condition);
		if (!this.exceptions.isEmpty()) {
			throw this.exceptions.get(0);
		}
		if (this.filter.length() > 0) {
			this.filterOnElement.addFilter(this.filter.toString());
		}
	}

	@Override
	public void visit(Comparison obj) {
		append(obj.getLeftExpression());
		this.filter.append(Tokens.SPACE);
		switch (obj.getOperator()) {
			case EQ:
				this.filter.append("eq"); //$NON-NLS-1$
				break;
			case NE:
				this.filter.append("ne"); //$NON-NLS-1$
				break;
			case LT:
				this.filter.append("lt"); //$NON-NLS-1$
				break;
			case LE:
				this.filter.append("le"); //$NON-NLS-1$
				break;
			case GT:
				this.filter.append("gt"); //$NON-NLS-1$
				break;
			case GE:
				this.filter.append("ge"); //$NON-NLS-1$
				break;
		}
		this.filter.append(Tokens.SPACE);
		BaseColumn old = setCurrentExpression(obj.getLeftExpression());
		appendRightComparison(obj);
		this.currentExpression = old;
	}

	private BaseColumn setCurrentExpression(Expression leftExpression) {
		BaseColumn old = currentExpression;
		if (leftExpression instanceof ColumnReference) {
			ColumnReference cr = (ColumnReference) leftExpression;
			currentExpression = cr.getMetadataObject();
		} else if (leftExpression instanceof Function) {
			org.teiid.language.Function function = (Function) leftExpression;
			currentExpression = function.getMetadataObject().getReturnParameter();
		} else {
			currentExpression = null;
		}
		//we are really looking for the native type, if it's not set then don't bother
		if (currentExpression != null && currentExpression.getNativeType() == null) {
			currentExpression = null;
		}
		return old;
	}

	protected void appendRightComparison(Comparison obj) {
		append(obj.getRightExpression());
	}

	@Override
	public void visit(IsNull obj) {
		appendNested(obj.getExpression());
		this.filter.append(Tokens.SPACE);
		this.filter.append(obj.isNegated() ? "ne" : "eq").append(Tokens.SPACE); //$NON-NLS-1$ //$NON-NLS-2$
		this.filter.append(NULL.toLowerCase());
	}

	private void appendNested(Expression ex) {
		boolean useParens = ex instanceof Condition;
		if (useParens) {
			this.filter.append(Tokens.LPAREN);
		}
		append(ex);
		if (useParens) {
			this.filter.append(Tokens.RPAREN);
		}
	}

	@Override
	public void visit(AndOr obj) {
		String opString = obj.getOperator().name().toLowerCase();
		appendNestedCondition(obj, obj.getLeftCondition());
		this.filter.append(Tokens.SPACE)
				.append(opString)
				.append(Tokens.SPACE);
		appendNestedCondition(obj, obj.getRightCondition());
	}

	protected void appendNestedCondition(AndOr parent, Condition condition) {
		if (condition instanceof AndOr) {
			AndOr nested = (AndOr) condition;
			if (nested.getOperator() != parent.getOperator()) {
				this.filter.append(Tokens.LPAREN);
				append(condition);
				this.filter.append(Tokens.RPAREN);
				return;
			}
		}
		append(condition);
	}

	private String odataType(String type, String runtimeType) {
		if (type == null) {
			type = ODataTypeManager.odataType(runtimeType)
					.getFullQualifiedName().getFullQualifiedNameAsString();
		}
		return type;
	}

	@Override
	public void visit(ColumnReference obj) {
		Column column = obj.getMetadataObject();

		ODataDocumentNode schemaElement = this.query.getSchemaElement((Table) column.getParent());
		// check if the column on pseudo column, then move it to the parent.
		if (ODataMetadataProcessor.isPseudo(column)) {
			try {
				Column realColumn = ODataMetadataProcessor.normalizePseudoColumn(this.metadata, column);
				schemaElement = this.query.getSchemaElement((Table) realColumn.getParent());
			} catch (TranslatorException e) {
				this.exceptions.add(e);
			}
		}

		if (this.filterOnElement == null) {
			this.filterOnElement = schemaElement;
		} else if (schemaElement.isExpandType() && (!this.filterOnElement.isExpandType())) {
			this.exceptions.add(new TranslatorException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17026)));
		}

		try {
			if (this.filterOnElement.isComplexType()) {
				if (ODataMetadataProcessor.isPseudo(column)) {
					Column realColumn = ODataMetadataProcessor.normalizePseudoColumn(this.metadata, column);
					this.filter.append(realColumn.getName());
				} else {
					this.filter.append(this.filterOnElement.getName()).append("/").append(column.getName());
				}
			} else {
				if (ODataMetadataProcessor.isPseudo(column)) {
					Column realColumn = ODataMetadataProcessor.normalizePseudoColumn(this.metadata, column);
					this.filter.append(realColumn.getName());
				} else {
					this.filter.append(column.getName());
				}
			}
		} catch (TranslatorException e) {
			this.exceptions.add(e);
		}
	}

	protected boolean isInfixFunction(String function) {
		return infixFunctions.containsKey(function);
	}

	@Override
	public void visit(Function obj) {
		if (this.ef.getFunctionModifiers().containsKey(obj.getName())) {
			List<?> parts = this.ef.getFunctionModifiers().get(obj.getName()).translate(obj);
			if (parts != null) {
				throw new AssertionError(ODataPlugin.Util.gs(ODataPlugin.Event.DVCORE94428));
			}
		}

		String name = obj.getName();
		List<Expression> args = obj.getParameters();
		if (isInfixFunction(name)) {
			this.filter.append(Tokens.LPAREN);
			if (args != null) {
				for (int i = 0; i < args.size(); i++) {
					append(args.get(i));
					if (i < (args.size() - 1)) {
						this.filter.append(Tokens.SPACE);
						this.filter.append(infixFunctions.get(name));
						this.filter.append(Tokens.SPACE);
					}
				}
			}
			this.filter.append(Tokens.RPAREN);
		} else {
			FunctionMethod method = obj.getMetadataObject();
			if (name.startsWith(method.getCategory())) {
				name = name.substring(method.getCategory().length() + 1);
			}
			this.filter.append(name).append(Tokens.LPAREN);
			if (name.equals("cast")) {
				append(args.get(0));
				this.filter.append(Tokens.COMMA);
				Literal literal = (Literal) args.get(1);
				String type = ODataTypeManager
						.odataType((String) literal.getValue())
						.getFullQualifiedName().getFullQualifiedNameAsString();
				this.filter.append(type);
			} else {
				if (args != null && args.size() != 0) {
					if (SourceSystemFunctions.ENDSWITH.equalsIgnoreCase(name)) {
						append(args.get(1));
						this.filter.append(Tokens.COMMA);
						append(args.get(0));
					} else {
						BaseColumn old = currentExpression;
						for (int i = 0; i < args.size(); i++) {
							currentExpression = method.getInputParameters().get(Math.min(i, method.getInputParameters().size() - 1));
							append(args.get(i));
							if (i < args.size() - 1) {
								this.filter.append(Tokens.COMMA);
							}
						}
						currentExpression = old;
					}
				}
			}
			this.filter.append(Tokens.RPAREN);
		}
	}

	@Override
	public void visit(Literal obj) {
		try {
			String odataType = ODataTypeManager.odataType(obj.getType()).toString();
			if (currentExpression != null) {
				//TODO: this is an attempt at contextually figuring out the type, but it
				//may not be sufficient in all cases
				odataType = odataType(currentExpression.getNativeType(), currentExpression.getRuntimeType());
			}
			this.filter.append(ODataTypeManager.convertToODataURIValue(obj.getValue(), odataType));
		} catch (EdmPrimitiveTypeException e) {
			this.exceptions.add(new TranslatorException(e));
		}
	}

	@Override
	public void visit(Not obj) {
		this.filter.append(NOT)
				.append(Tokens.SPACE)
				.append(Tokens.LPAREN);
		append(obj.getCriteria());
		this.filter.append(Tokens.RPAREN);
	}

	public void append(LanguageObject obj) {
		visitNode(obj);
	}

	protected void append(List<? extends LanguageObject> items) {
		if (items != null && items.size() != 0) {
			for (int i = 0; i < items.size(); i++) {
				append(items.get(i));
			}
		}
	}

	protected void append(LanguageObject[] items) {
		if (items != null && items.length != 0) {
			for (int i = 0; i < items.length; i++) {
				append(items[i]);
			}
		}
	}
}
