/*
 * 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.
 *
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *
 */
package org.teiid.metadata.odata;

import com.datavirtuality.dv.core.interfaces.DVCaller;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.commons.api.edm.geo.SRID;
import org.apache.olingo.commons.api.edm.provider.*;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlCollection;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlConstantExpression;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlConstantExpression.ConstantExpressionType;
import org.teiid.connector.DataPlugin;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.types.DataTypeManager;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.metadata.*;
import org.teiid.metadata.BaseColumn.NullType;

import java.util.*;
import java.util.regex.Pattern;

public class ODataSchemaBuilder {

	public static final String VISIBLE = MetadataFactory.ODATA_URI + "visible"; //$NON-NLS-1$

	//not validating length - odata specifies up to 128 chars though
	static Pattern NAME_PATTERN = Pattern.compile("[\\p{L}\\p{Nl}][\\p{L}\\p{Nl}\\p{Nd}\\p{Mn}\\p{Mc}\\p{Pc}\\p{Cf}]{0,}"); //$NON-NLS-1$

	public interface SchemaResolver {
		/**
		 * Return the schema info or null if it is not visible
		 * @param schemaName the teiid schema name
		 * @return
		 */
		ODataSchemaInfo getSchemaInfo(String schemaName);
	}

	public final static class DefaultSchemaResolver implements SchemaResolver {
		private ODataSchemaInfo info;
		private Map<String, ODataSchemaInfo> schemaInfos;
		private String contextName;

		public DefaultSchemaResolver(ODataSchemaInfo info, Map<String, ODataSchemaInfo> schemaInfos, String contextName) {
			this.info = info;
			this.schemaInfos = schemaInfos;
			this.contextName = contextName;
		}

		@Override
		public ODataSchemaInfo getSchemaInfo(String name) {
			ODataSchemaInfo result = schemaInfos.get(name);
			if (result != null && info != result) {
				//add a bi-directional relationship
				TeiidEdmProvider infoProvider = info.edmProvider;
				TeiidEdmProvider resultProvider = result.edmProvider;
				infoProvider.addReferenceSchema(
						contextName, result.schema.getNamespace(), result.schema.getAlias(), resultProvider);
				resultProvider.addReferenceSchema(
						contextName, info.schema.getNamespace(), info.schema.getAlias(), infoProvider);
			}
			return result;
		}
	}

	public final static class ODataSchemaInfo {
		public CsdlSchema schema = new CsdlSchema();
		public Map<String, ODataTableInfo> tableInfos = Collections.synchronizedMap(new LinkedHashMap<>());
		public Map<String, ODataProcedureInfo> procedureInfos = Collections.synchronizedMap(new LinkedHashMap<>());
		public TeiidEdmProvider edmProvider;

		public Set<String> getAllResources() {
			Set<String> result = new HashSet<>();
			for (Map.Entry<String, ODataTableInfo> entry : tableInfos.entrySet()) {
				ODataTableInfo tableInfo = entry.getValue();
				result.add(tableInfo.table.getFullName());
			}
			for (Map.Entry<String, ODataProcedureInfo> entry : procedureInfos.entrySet()) {
				ODataProcedureInfo procInfo = entry.getValue();
				result.add(procInfo.procedure.getFullName());
			}
			return result;
		}

		public ODataSchemaInfo clone() {
			ODataSchemaInfo cloned = new ODataSchemaInfo();
			cloned.tableInfos.putAll(this.tableInfos);
			cloned.procedureInfos.putAll(this.procedureInfos);
			// create new edm provider with the new schema instance
			cloned.edmProvider = new TeiidEdmProvider(cloned.schema);
			// schema clone
			cloned.schema.setAlias(this.schema.getAlias());
			cloned.schema.setNamespace(this.schema.getNamespace());
			cloned.schema.getActions().addAll(this.schema.getActions());
			cloned.schema.getFunctions().addAll(this.schema.getFunctions());
			cloned.schema.getAnnotations().addAll(this.schema.getAnnotations());
			cloned.schema.getTerms().addAll(this.schema.getTerms());
			cloned.schema.getEntityTypes().addAll(this.schema.getEntityTypes());
			cloned.schema.getComplexTypes().addAll(this.schema.getComplexTypes());
			cloned.schema.getAnnotationGroups().addAll(this.schema.getAnnotationGroups());
			cloned.schema.getEnumTypes().addAll(this.schema.getEnumTypes());
			cloned.schema.getTypeDefinitions().addAll(this.schema.getTypeDefinitions());
			// entity container
			cloned.schema.setEntityContainer(new CsdlEntityContainer());
			cloned.schema.getEntityContainer().setName(this.schema.getEntityContainer().getName());
			if (this.schema.getEntityContainer().getExtendsContainer() != null) {
				cloned.schema.getEntityContainer().setExtendsContainer(this.schema.getEntityContainer().getExtendsContainer());
			}
			cloned.schema.getEntityContainer().getEntitySets().addAll(this.schema.getEntityContainer().getEntitySets());
			cloned.schema.getEntityContainer().getSingletons().addAll(this.schema.getEntityContainer().getSingletons());
			cloned.schema.getEntityContainer().getActionImports().addAll(this.schema.getEntityContainer().getActionImports());
			cloned.schema.getEntityContainer().getFunctionImports().addAll(this.schema.getEntityContainer().getFunctionImports());
			cloned.schema.getEntityContainer().getAnnotations().addAll(this.schema.getEntityContainer().getAnnotations());
			return cloned;
		}
	}

	public final static class ODataTableInfo {
		public Table table;
		public CsdlEntitySet entitySet;
		public CsdlEntityType entityType;
	}

	public final static class ODataProcedureInfo {
		public Procedure procedure;
		public CsdlComplexType complexType;
		public CsdlFunction function;
		public CsdlAction action;
		public CsdlFunctionImport functionImport;
		public CsdlActionImport actionImport;
	}

	/**
	 * Helper method for tests
	 * @param namespace
	 * @param teiidSchema
	 * @return
	 */
	public static CsdlSchema buildMetadata(String namespace, org.teiid.metadata.Schema teiidSchema) {
		ODataSchemaInfo info = buildStructuralMetadata(namespace, teiidSchema, teiidSchema.getName(), null);
		buildNavigationProperties(teiidSchema, info, new SchemaResolver() {

			@Override
			public ODataSchemaInfo getSchemaInfo(String schemaName) {
				return info;
			}
		});
		return info.schema;
	}

	public static ODataSchemaInfo buildStructuralMetadata(String namespace, org.teiid.metadata.Schema teiidSchema, String alias, Object workContext) {
		try {
			ODataSchemaInfo info = buildSchema(namespace, teiidSchema, alias);
			buildEntityTypes(teiidSchema, info, workContext);
			buildProcedures(teiidSchema, info, workContext);
			return info;
		} catch (Exception e) {
			throw new TeiidRuntimeException(e);
		}
	}

	public static ODataSchemaInfo buildSchema(String namespace, org.teiid.metadata.Schema teiidSchema, String alias) {
		String fullSchemaName = namespace + "." + teiidSchema.getName();
		ODataSchemaInfo info = new ODataSchemaInfo();
		info.schema.setNamespace(fullSchemaName).setAlias(alias);
		info.schema.setEntityContainer(new CsdlEntityContainer().setName(alias));
		info.edmProvider = new TeiidEdmProvider(info.schema);

		return info;
	}

	public static boolean buildEntityType(Table table, KeyRecord key, Column col, ODataSchemaInfo info) {
		String fullSchemaName = info.schema.getNamespace();

		// skip if the table does not have the PK or unique
		KeyRecord primaryKey = key != null ? key : getIdentifier(table);
		if (primaryKey == null) {
			LogManager.logDetail(LogConstants.CTX_ODATA,
					DataPlugin.Util.gs(DataPlugin.Event.TEIID16017,table.getFullName()));
			return false;
		}

		String entityTypeName = table.getName();

		CsdlEntityType entityType = new CsdlEntityType().setName(entityTypeName);

		// adding properties
		List<CsdlProperty> properties = new ArrayList<CsdlProperty>();
		for (Column c : table.getColumns()) {
			addColProperty(primaryKey, c, info, properties);
		}
		if (col != null && !table.getColumns().contains(col)) {
			addColProperty(primaryKey, col, info, properties);
		}
		entityType.setProperties(properties);
		if (hasStream(properties)) {
			entityType.setHasStream(true);
		}

		// set keys
		ArrayList<CsdlPropertyRef> keyProps = new ArrayList<CsdlPropertyRef>();
		for (Column c : primaryKey.getColumns()) {
			keyProps.add(new CsdlPropertyRef().setName(c.getName()));
		}

		entityType.setKey(keyProps);
		addTableAnnotations(table, entityType, info.schema);

		// entity set one for one entity type
		CsdlEntitySet entitySet = new CsdlEntitySet()
				.setName(entityTypeName)
				.setType(new FullQualifiedName(fullSchemaName, entityTypeName))
				.setIncludeInServiceDocument(true);

		// add entity types for entity schema
		ODataTableInfo tableInfo = new ODataTableInfo();
		tableInfo.table = table;
		tableInfo.entityType = entityType;
		tableInfo.entitySet = entitySet;
		info.tableInfos.putIfAbsent(entityTypeName, tableInfo);

		return true;
	}

	private static void addColProperty(KeyRecord primaryKey, Column c, ODataSchemaInfo info, List<CsdlProperty> properties) {
//		boolean nullable = c.getNullType() == NullType.Nullable;
		boolean nullable = c.getNullType() != NullType.No_Nulls;
		CsdlProperty property = buildProperty(c, hasColumn(primaryKey, c.getName())?false:nullable);
		addColumnAnnotations(c, property, info.schema);
		properties.add(property);
	}

	static void buildEntityTypes(org.teiid.metadata.Schema schema, ODataSchemaInfo schemaInfo,
								 Object workContext) {
		for (Table table : schema.getTables().values()) {

			if (!isObjectVisible(table, workContext)) {
				continue;
			}

			buildEntityType(table, null, null, schemaInfo);
		}

		// entity container is holder entity sets, association sets, function
		// imports
		CsdlEntityContainer entityContainer = schemaInfo.schema.getEntityContainer();

		for (Map.Entry<String, ODataTableInfo> entry : schemaInfo.tableInfos.entrySet()) {
			ODataTableInfo tableInfo = entry.getValue();
			entityContainer.getEntitySets().add(tableInfo.entitySet);
			schemaInfo.schema.getEntityTypes().add(tableInfo.entityType);
		}
	}

	public static Set<String> getNotPermittedObjects(Set<String> resources, Object workContext) {
		return (Set<String>) DVCaller.call(213, resources, workContext);
	}
	public static boolean isObjectVisible(AbstractMetadataRecord record, Object workContext) {
		Set<String> resources = new HashSet<>();
		resources.add(record.getFullName());
		Set<String> inaccessibleResources = (Set<String>) DVCaller.call(213, resources, workContext);
		if (inaccessibleResources != null && inaccessibleResources.contains(record.getFullName()))
			return false;
		String visible = record.getProperty(VISIBLE, false);
		return visible == null || Boolean.valueOf(visible);
	}

	private static boolean hasStream(List<CsdlProperty> properties) {
		for (CsdlProperty p : properties) {
			if (p.getType().equals(EdmPrimitiveTypeKind.Stream.getFullQualifiedName().toString())) {
				return true;
			}
		}
		return false;
	}

	private static CsdlProperty buildProperty(Column c, boolean nullable) {
		String runtimeType = c.getRuntimeType();
		CsdlProperty property = new CsdlProperty()
				.setName(c.getName())
				.setType(ODataType.odataType(c).getFullQualifiedName())
				.setNullable(nullable);

		if (DataTypeManager.isArrayType(runtimeType)) {
			property.setCollection(true);
		}

		runtimeType = ODataType.teiidType(property.getType(), false);

		if (runtimeType.equals(DataTypeManager.DefaultDataTypes.STRING)
				|| runtimeType.equals(DataTypeManager.DefaultDataTypes.VARBINARY)) {
			//this will not be correct for a multi-dimensional or other unsupported type
			property.setMaxLength(c.getLength());
			if (runtimeType.equals(DataTypeManager.DefaultDataTypes.STRING)) {
				property.setUnicode(true);
			}
		} else if (runtimeType.equals(DataTypeManager.DefaultDataTypes.BIG_DECIMAL)
				|| runtimeType.equals(DataTypeManager.DefaultDataTypes.BIG_INTEGER)) {
			if (c.getScale() < 0) {
				property.setPrecision((int)Math.min(Integer.MAX_VALUE, (long)c.getPrecision() - c.getScale()));
			} else {
				property.setPrecision(c.getPrecision());
			}
			property.setScale(Math.max(0, c.getScale()));
		} else if (runtimeType.equals(DataTypeManager.DefaultDataTypes.TIMESTAMP)) {
			int precision = c.getScale();
			//timestamp scale is odata precision
			//set it if there's a non default value
			if (precision != 0 || c.getPrecision() != 0) {
				property.setPrecision(precision);
			}
		}
		if (c.getDefaultValue() != null) {
			property.setDefaultValue(c.getDefaultValue());
		}
		String srid = c.getProperty(BaseColumn.SPATIAL_SRID, false);
		if (srid != null) {
			property.setSrid(SRID.valueOf(srid));
		}
		return property;
	}

	static boolean hasColumn(KeyRecord pk, String columnName) {
		if (pk != null) {
			for (Column column : pk.getColumns()) {
				if (column.getName().equals(columnName)) {
					return true;
				}
			}
		}
		return false;
	}

	public static KeyRecord getIdentifier(Table table) {
		if (table.getPrimaryKey() != null) {
			return table.getPrimaryKey();
		}

		for (KeyRecord key:table.getUniqueKeys()) {
			return key;
		}
		return null;
	}

	public static void buildNavigationProperties(org.teiid.metadata.Schema schema,
												 ODataSchemaInfo info, SchemaResolver resolver) {

		for (Table table : schema.getTables().values()) {

			if (info.tableInfos.get(table.getName()) == null) {
				continue;
			}

			// build Associations
			buildNavigationPropertiesByTable(info, table, null, resolver);
		}
	}

	public static void buildNavigationPropertiesByTable(ODataSchemaInfo info, Table table, KeyRecord key, SchemaResolver resolver) {
		for (ForeignKey fk : table.getForeignKeys()) {
			if (fk.getReferenceKey() == null)
				continue;

			KeyRecord primaryKey = key != null ? key : getIdentifier(table);
			// check to see if fk is part of this table's pk, then it is 1 to 1 relation
			List<Column> setOne = primaryKey.getColumns();
			List<Column> setTwo = fk.getColumns();

			boolean fkPKSame = setOne.equals(setTwo);

			addForwardNavigation(info.tableInfos, table, fk, fkPKSame, resolver);
			addReverseNavigation(info, table, fk, fkPKSame, resolver);
		}
	}

	private static void addForwardNavigation(Map<String, ODataTableInfo> tableInfos,
											 Table table, ForeignKey fk, boolean oneToOne, SchemaResolver resolver) {
		Table pkTable = fk.getReferenceKey().getParent();
		ODataSchemaInfo pkSchema = resolver.getSchemaInfo(pkTable.getParent().getName());
		if (pkSchema == null || pkSchema.tableInfos.get(pkTable.getName()) == null) {
			return;
		}
		String fqn = pkSchema.schema.getAlias() + '.' + pkTable.getName();
		CsdlNavigationPropertyBinding navigationBinding = buildNavigationBinding(fk, pkTable, fqn);
		CsdlNavigationProperty navigation = buildNavigation(fk, fqn);
		String entityTypeName = table.getName();

		if (oneToOne) {
			navigation.setNullable(false);
		} else {
			for (Column c : fk.getColumns()) {
				if (c.getNullType() == NullType.No_Nulls) {
					navigation.setNullable(false);
					break;
				}
			}
		}

		List<CsdlReferentialConstraint> constraints = navigation.getReferentialConstraints();
		KeyRecord key = fk.getReferenceKey();
		for (int i = 0; i < key.getColumns().size(); i++) {
			String referencedProperty = key.getColumns().get(i).getName();
			String property = fk.getColumns().get(i).getName();
			boolean add = true;
			for (CsdlReferentialConstraint constraint : constraints) {
				if (constraint.getReferencedProperty().equals(referencedProperty) && constraint.getProperty().equals(property)) {
					add = false;
					break;
				}
			}
			if (add)
				constraints.add(new CsdlReferentialConstraint().setReferencedProperty(referencedProperty)
						.setProperty(property));
		}

		ODataTableInfo tableInfo = tableInfos.get(entityTypeName);
		CsdlEntityType entityType = tableInfo.entityType;
		entityType.getNavigationProperties().add(navigation);

		CsdlEntitySet entitySet = tableInfo.entitySet;
		entitySet.getNavigationPropertyBindings().add(navigationBinding);
	}

	private static void addReverseNavigation(ODataSchemaInfo schema, Table table,
											 ForeignKey fk, boolean oneToOne, SchemaResolver resolver) {
		Table pkTable = fk.getReferenceKey().getParent();
		String entitySchema = pkTable.getParent().getName();
		ODataSchemaInfo pkSchema = resolver.getSchemaInfo(entitySchema);
		if (pkSchema == null || pkSchema.tableInfos.get(pkTable.getName()) == null) {
			return;
		}

		String fqn = schema.schema.getAlias() + '.' + table.getName();
		CsdlNavigationPropertyBinding navigationBinding = buildReverseNavigationBinding(table, pkTable, fk, fqn);
		CsdlNavigationProperty navigation = buildReverseNavigation(table, fk, fqn);
		String entityTypeName = fk.getReferenceTableName();

		if (oneToOne) {
			navigation.setNullable(false);
		} else {
			navigation.setCollection(true);
		}
		ODataTableInfo tableInfo = pkSchema.tableInfos.get(entityTypeName);
		CsdlEntityType entityType = tableInfo.entityType;
		CsdlEntitySet entitySet = tableInfo.entitySet;
		entityType.getNavigationProperties().add(navigation);
		entitySet.getNavigationPropertyBindings().add(navigationBinding);
	}

	private static CsdlNavigationPropertyBinding buildNavigationBinding(ForeignKey fk, Table pkTable, String fqn) {
		CsdlNavigationPropertyBinding navigationBinding = new CsdlNavigationPropertyBinding();
		navigationBinding.setPath(fk.getName());
		if (!fk.getParent().getParent().equals(pkTable.getParent())) {
			navigationBinding.setTarget(fqn);
		} else {
			navigationBinding.setTarget(pkTable.getName());
		}
		return navigationBinding;
	}

	private static CsdlNavigationPropertyBinding buildReverseNavigationBinding(Table table, Table pkTable, ForeignKey fk, String fqn) {
		CsdlNavigationPropertyBinding navigationBinding = new CsdlNavigationPropertyBinding();
		navigationBinding.setPath(table.getName()+"_"+fk.getName());
		if (!table.getParent().equals(pkTable.getParent())) {
			navigationBinding.setTarget(fqn);
		} else {
			navigationBinding.setTarget(table.getName());
		}
		return navigationBinding;
	}

	private static CsdlNavigationProperty buildNavigation(ForeignKey fk, String fqn) {
		CsdlNavigationProperty navigation = new CsdlNavigationProperty();
		navigation.setName(fk.getName()).setType(new FullQualifiedName(fqn));

		List<CsdlReferentialConstraint> constraints = navigation.getReferentialConstraints();
		for (int i = 0; i < fk.getColumns().size(); i++) {
			Column c = fk.getColumns().get(i);
			List<String> refColumns = fk.getReferenceColumns();
			if (refColumns != null) {
				String refColumn = refColumns.get(i);
				CsdlReferentialConstraint constraint = new CsdlReferentialConstraint();
				constraint.setProperty(c.getName());
				constraint.setReferencedProperty(refColumn);
				constraints.add(constraint);
			}
		}
		return navigation;
	}

	private static CsdlNavigationProperty buildReverseNavigation(Table table, ForeignKey fk, String fqn) {
		CsdlNavigationProperty navigation = new CsdlNavigationProperty();
		navigation.setName(table.getName() + "_" + fk.getName()).setType(
				new FullQualifiedName(fqn));

		List<CsdlReferentialConstraint> constraints = navigation.getReferentialConstraints();
		for (int i = 0; i < fk.getColumns().size(); i++) {
			List<String> refColumns = fk.getReferenceColumns();
			if (refColumns != null) {
				Column c = fk.getColumns().get(i);
				String refColumn = refColumns.get(i);
				CsdlReferentialConstraint constraint = new CsdlReferentialConstraint();
				constraint.setProperty(refColumn);
				constraint.setReferencedProperty(c.getName());
				constraints.add(constraint);
			}
		}
		return navigation;
	}

	public static boolean buildProcedure(Procedure proc, ODataSchemaInfo schemaInfo) {
		if (!allowedProcedure(proc)) {
			LogManager.logDetail(LogConstants.CTX_ODATA,
					DataPlugin.Util.gs(DataPlugin.Event.TEIID16032, proc.getFullName()));
			return false;
		}

		ODataProcedureInfo procedureInfo = buildOperation(proc, schemaInfo.schema, isFuntion(proc));
		schemaInfo.procedureInfos.put(proc.getName(), procedureInfo);
		return true;
	}

	public static ProcedureParameter getReturnValue(String schema, String procName) {
		return returnValuesS.get(schema + "." + procName);
	}

	private static final Map<String, ProcedureParameter> returnValuesS = new HashMap<>();
	private static ODataProcedureInfo buildOperation(Procedure proc, CsdlSchema csdlSchema, boolean isFunction) {
		ODataProcedureInfo procedureInfo = new ODataProcedureInfo();
		procedureInfo.procedure = proc;

		CsdlOperation edmOperation = isFunction ? new CsdlFunction() : new CsdlAction();
		String procName = proc.getName();
		edmOperation.setName(procName);
		edmOperation.setBound(false);
		List<CsdlParameter> params = new ArrayList<CsdlParameter>(); // at this time gitcheckouthas noparams
		for (ProcedureParameter pp : proc.getParameters()) {
			EdmPrimitiveTypeKind odatatype = ODataType.odataType(pp);
			// Skip Out params by design
			if (ProcedureParameter.Type.ReturnValue.equals(pp.getType())) {
				edmOperation.setReturnType(new CsdlReturnType().setType(odatatype.getFullQualifiedName()).setCollection(DataTypeManager.isArrayType(pp.getRuntimeType())));
				returnValuesS.put(csdlSchema.getAlias() + "." + proc.getName(), pp);
			} else if (ProcedureParameter.Type.In.equals(pp.getType()) ||
					ProcedureParameter.Type.InOut.equals(pp.getType())) {
				CsdlParameter parameter = buildParameter(pp, odatatype);
				addOperationParameterAnnotations(pp, parameter, csdlSchema);
				params.add(parameter);
			}
		}
		edmOperation.setParameters(params);

		// add a complex type for return resultset.
		ColumnSet<Procedure> returnColumns = proc.getResultSet();
		if (returnColumns != null) {
			CsdlComplexType complexType = buildComplexType(proc, returnColumns, csdlSchema);
			procedureInfo.complexType = complexType;
			FullQualifiedName odataType = new FullQualifiedName(csdlSchema.getAlias(), complexType.getName());
			edmOperation.setReturnType((new CsdlReturnType().setType(odataType).setCollection(true)));
		}

		addOperationAnnotations(proc, edmOperation, csdlSchema);

		if (isFunction) {
			CsdlFunctionImport functionImport = new CsdlFunctionImport();
			functionImport.setName(procName).setFunction(new FullQualifiedName(csdlSchema.getAlias(), procName));

			procedureInfo.function = (CsdlFunction) edmOperation;
			procedureInfo.functionImport = functionImport;
		} else {
			CsdlActionImport actionImport = new CsdlActionImport();
			actionImport.setName(procName).setAction(new FullQualifiedName(csdlSchema.getAlias(), procName));

			procedureInfo.action = (CsdlAction) edmOperation;
			procedureInfo.actionImport = actionImport;
		}

		return procedureInfo;
	}

	static void buildProcedures(org.teiid.metadata.Schema schema, ODataSchemaInfo schemaInfo,
								Object workContext) {
		// procedures
		ArrayList<CsdlComplexType> complexTypes = new ArrayList<CsdlComplexType>();
		ArrayList<CsdlFunction> functions = new ArrayList<CsdlFunction>();
		ArrayList<CsdlFunctionImport> functionImports = new ArrayList<CsdlFunctionImport>();
		ArrayList<CsdlAction> actions = new ArrayList<CsdlAction>();
		ArrayList<CsdlActionImport> actionImports = new ArrayList<CsdlActionImport>();

		for (Procedure proc : schema.getProcedures().values()) {
			if (!isObjectVisible(proc, workContext)) {
				continue;
			}

			if (buildProcedure(proc, schemaInfo)) {
				ODataProcedureInfo procedureInfo = schemaInfo.procedureInfos.get(proc.getName());
				if (procedureInfo.complexType != null)
					complexTypes.add(procedureInfo.complexType);
				if (procedureInfo.function != null)
					functions.add(procedureInfo.function);
				if (procedureInfo.functionImport != null)
					functionImports.add(procedureInfo.functionImport);
				if (procedureInfo.action != null)
					actions.add(procedureInfo.action);
				if (procedureInfo.actionImport != null)
					actionImports.add(procedureInfo.actionImport);
			}
		}
		schemaInfo.schema.setComplexTypes(complexTypes);
		schemaInfo.schema.setFunctions(functions);
		schemaInfo.schema.setActions(actions);
		schemaInfo.schema.getEntityContainer().setFunctionImports(functionImports);
		schemaInfo.schema.getEntityContainer().setActionImports(actionImports);
	}

	private static boolean doesProcedureReturn(Procedure proc) {
		for (ProcedureParameter pp : proc.getParameters()) {
			if (pp.getType().equals(ProcedureParameter.Type.ReturnValue)) {
				return true;
			}
		}
		return (proc.getResultSet() != null);
	}

	private static boolean allowedProcedure(Procedure proc) {
		// any number of in, but can have only one LOB if lob is present
		// only *one* result, or resultset allowed
		int inouts = 0;
		int lobs = 0;
		int outs = 0;
		for (ProcedureParameter pp : proc.getParameters()) {
			if (pp.getType().equals(ProcedureParameter.Type.Out)) {
				continue;
			}

			if (pp.getType().equals(ProcedureParameter.Type.In)
					|| pp.getType().equals(ProcedureParameter.Type.InOut)) {
				inouts++;
				if (DataTypeManager.isLOB(pp.getRuntimeType())) {
					lobs++;
				}
			} else if (pp.getType().equals(ProcedureParameter.Type.ReturnValue)) {
				outs++;
			}
		}

		if (proc.getResultSet() != null) {
			for (Column c : proc.getResultSet().getColumns()) {
				if (DataTypeManager.isLOB(c.getRuntimeType())) {
					return false;
				}
			}
			outs++;
		}

		if (outs > 1) {
			return false;
		}

		if (inouts > 1 && lobs >= 1) {
			return false;
		}

		return true;
	}

	private static boolean isInputParameterLob(Procedure proc) {
		for (ProcedureParameter pp : proc.getParameters()) {
			if (!pp.getType().equals(ProcedureParameter.Type.ReturnValue)
					&& !pp.getType().equals(ProcedureParameter.Type.Out)
					&& DataTypeManager.isLOB(pp.getRuntimeType())) {
				return true;
			}
		}
		return false;
	}

	private static boolean isFuntion(Procedure proc) {
		if (doesProcedureReturn(proc) && proc.getUpdateCount() < 1
				&& !isInputParameterLob(proc)) {
			if (proc.getUpdateCount() == Procedure.AUTO_UPDATECOUNT) {
				//we need to plan this procedure to determine if the update count is actually 0
				//that's a significant change for a narrow case, so we'll just return false for now
				return false;
			}
			return true;
		}
		return false;
	}

	private static CsdlParameter buildParameter(ProcedureParameter pp,
												EdmPrimitiveTypeKind odatatype) {
		CsdlParameter param = new CsdlParameter();
		param.setName(pp.getName());
		param.setType(odatatype.getFullQualifiedName());
		if (DataTypeManager.isArrayType(pp.getRuntimeType())) {
			param.setCollection(true);
		}
		param.setNullable(pp.getNullType() == NullType.Nullable);

		String runtimeType = pp.getRuntimeType();

		if (runtimeType.equals(DataTypeManager.DefaultDataTypes.STRING)
				|| runtimeType.equals(DataTypeManager.DefaultDataTypes.VARBINARY)) {
			//this will not be correct for a multi-dimensional or other unsupported type
			param.setMaxLength(pp.getLength());
		} else if (runtimeType.equals(DataTypeManager.DefaultDataTypes.BIG_DECIMAL)
				|| runtimeType.equals(DataTypeManager.DefaultDataTypes.BIG_INTEGER)) {
			if (pp.getScale() < 0) {
				param.setPrecision((int) Math.min(Integer.MAX_VALUE, (long) pp.getPrecision() - pp.getScale()));
			} else {
				param.setPrecision(pp.getPrecision());
			}
			param.setScale(Math.max(0, pp.getScale()));
		} else if (runtimeType.equals(DataTypeManager.DefaultDataTypes.TIMESTAMP)
				|| runtimeType.equals(DataTypeManager.DefaultDataTypes.TIME)) {
			param.setPrecision(pp.getPrecision() == 0 ? new Integer(4) : pp.getPrecision());
		}
		if (pp.getDefaultValue() != null) {
//            param.setDefaultValue(c.getDefaultValue());
		}
		String srid = pp.getProperty(BaseColumn.SPATIAL_SRID, false);
		if (srid != null) {
			param.setSrid(SRID.valueOf(srid));
		}
		return param;
	}

	private static CsdlComplexType buildComplexType(Procedure proc,
													ColumnSet<Procedure> returnColumns, CsdlSchema csdlSchema) {
		CsdlComplexType complexType = new CsdlComplexType();
		String entityTypeName = proc.getName() + "_" + returnColumns.getName(); //$NON-NLS-1$
		complexType.setName(entityTypeName);

		ArrayList<CsdlProperty> props = new ArrayList<CsdlProperty>();
		for (Column c : returnColumns.getColumns()) {
			CsdlProperty property = buildProperty(c, (c.getNullType() == NullType.Nullable));
			props.add(property);
			addColumnAnnotations(c, property, csdlSchema);
		}
		complexType.setProperties(props);
		return complexType;
	}

	private static void addTableAnnotations(Table table, CsdlEntityType entityType, CsdlSchema csdlSchema) {
		addCommonAnnotations(table, entityType);

		if (table.getCardinality() != -1) {
			addIntAnnotation(entityType, "teiid.CARDINALITY", Long.valueOf(table.getCardinality()).intValue());
		}

		if (table.isMaterialized()) {

			if (table.getMaterializedTable() != null) {
				addStringAnnotation(entityType, "teiid.MATERIALIZED_TABLE", table.getMaterializedTable().getFullName());
			}

			if (table.getMaterializedStageTable() != null) {
				addStringAnnotation(entityType, "teiid.MATERIALIZED_STAGE_TABLE", table.getMaterializedStageTable().getFullName());
			}
		}

		if (table.getAccessPatterns() != null && !table.getAccessPatterns().isEmpty()) {
			ArrayList<String> values = new ArrayList<String>();
			for (KeyRecord record:table.getAccessPatterns()) {
				StringBuilder sb = new StringBuilder();
				for (Column c:record.getColumns()) {
					if (sb.length() > 0) {
						sb.append(",");
					}
					sb.append(c.getName());
				}
				values.add(sb.toString());
			}
			addStringCollectionAnnotation(entityType, "teiid.ACCESS_PATTERNS", values);
		}

		if (table.supportsUpdate()) {
			addBooleanAnnotation(entityType, "teiid.UPDATABLE", table.supportsUpdate());
		}

		// add all custom properties
		for (String property:table.getProperties().keySet()) {
			addTerm(normalizeTermName(property), new String[] {"EntityType"}, csdlSchema);
			addStringAnnotation(entityType, csdlSchema.getAlias()+"."+normalizeTermName(property), table.getProperties().get(property));
		}
	}

	private static void addColumnAnnotations(Column column,
											 CsdlProperty property, CsdlSchema csdlSchema) {
		addCommonAnnotations(column, property);

		if (!column.isSelectable()) {
			addBooleanAnnotation(property, "teiid.SELECTABLE", column.isSelectable());
		}

		if(column.isUpdatable()) {
			addBooleanAnnotation(property, "teiid.UPDATABLE", column.isUpdatable());
		}

		if (column.isCurrency()) {
			addBooleanAnnotation(property, "teiid.CURRENCY", column.isCurrency());
		}

		if (column.isCaseSensitive()) {
			addBooleanAnnotation(property, "teiid.CASE_SENSITIVE", column.isCaseSensitive());
		}

		if (column.isFixedLength()) {
			addBooleanAnnotation(property, "teiid.FIXED_LENGTH", column.isFixedLength());
		}

		if (!column.isSigned()) {
			addBooleanAnnotation(property, "teiid.SIGNED", column.isSigned());
		}

		if (column.getDistinctValues() != -1) {
			addIntAnnotation(property, "teiid.DISTINCT_VALUES", Long.valueOf(column.getDistinctValues()).intValue());
		}

		if (column.getNullValues() != -1) {
			addIntAnnotation(property, "teiid.NULL_VALUE_COUNT", Long.valueOf(column.getNullValues()).intValue());
		}

		// add all custom properties
		for (String str:column.getProperties().keySet()) {
			addTerm(normalizeTermName(str), new String[] {"Property"}, csdlSchema);
			addStringAnnotation(property, csdlSchema.getAlias()+"."+normalizeTermName(str), column.getProperties().get(str));
		}
	}

	private static void addOperationAnnotations(Procedure proc,
												CsdlOperation operation, CsdlSchema csdlSchema) {
		addCommonAnnotations(proc, operation);
		if (proc.getUpdateCount() != Procedure.AUTO_UPDATECOUNT) {
			addIntAnnotation(operation, "teiid.UPDATECOUNT", proc.getUpdateCount());
		}
		// add all custom properties
		for (String str:proc.getProperties().keySet()) {
			addTerm(normalizeTermName(str), new String[] {"Action", "Function"}, csdlSchema);
			addStringAnnotation(operation, csdlSchema.getAlias()+"."+normalizeTermName(str), proc.getProperties().get(str));
		}
	}


	private static void addOperationParameterAnnotations(
			ProcedureParameter procedure, CsdlParameter parameter,
			CsdlSchema csdlSchema) {
		addCommonAnnotations(procedure, parameter);

		// add all custom properties
		for (String str:procedure.getProperties().keySet()) {
			addTerm(normalizeTermName(str), new String[] {"Parameter"}, csdlSchema);
			addStringAnnotation(parameter, csdlSchema.getAlias()+"."+normalizeTermName(str), procedure.getProperties().get(str));
		}
	}

	private static void addCommonAnnotations(AbstractMetadataRecord rec,
											 CsdlAnnotatable recipent) {
		if (rec.getAnnotation() != null) {
			addStringAnnotation(recipent, "Core.Description", rec.getAnnotation());
		}

		if (rec.getRemark() != null) {
			addStringAnnotation(recipent, "Core.Description", rec.getRemark());
		}

		if (rec.getNameInSource() != null) {
			addStringAnnotation(recipent, "teiid.NAMEINSOURCE", rec.getNameInSource());
		}

		if (!NAME_PATTERN.matcher(rec.getName()).matches()) {
			LogManager.logDetail(LogConstants.CTX_ODATA,
					DataPlugin.Util.gs(DataPlugin.Event.TEIID16063,rec.getFullName()));
		}
	}
	private static void addStringAnnotation(CsdlAnnotatable recipent, String term, String value) {
		CsdlAnnotation annotation = new CsdlAnnotation();
		annotation.setTerm(term);
		annotation.setExpression(new CsdlConstantExpression(ConstantExpressionType.String, value));
		recipent.getAnnotations().add(annotation);
	}

	private static void addIntAnnotation(CsdlAnnotatable recipent, String term, int value) {
		CsdlAnnotation annotation = new CsdlAnnotation();
		annotation.setTerm(term);
		annotation.setExpression(new CsdlConstantExpression(ConstantExpressionType.Int, String.valueOf(value)));
		recipent.getAnnotations().add(annotation);
	}

	private static void addBooleanAnnotation(CsdlAnnotatable recipent, String term, boolean value) {
		CsdlAnnotation annotation = new CsdlAnnotation();
		annotation.setTerm(term);
		annotation.setExpression(new CsdlConstantExpression(ConstantExpressionType.Bool, String.valueOf(value)));
		recipent.getAnnotations().add(annotation);
	}

	private static void addStringCollectionAnnotation(CsdlAnnotatable recipent, String term, List<String> values) {
		CsdlAnnotation annotation = new CsdlAnnotation();
		annotation.setTerm(term);
		CsdlCollection collection = new CsdlCollection();
		for (String value:values) {
			collection.getItems().add(new CsdlConstantExpression(ConstantExpressionType.String, value));
		}
		annotation.setExpression(collection);
		recipent.getAnnotations().add(annotation);
	}

	private static void addTerm(String property, String[] appliesTo, CsdlSchema schema) {
		CsdlTerm term = schema.getTerm(property);
		if ( term == null) {
			term = new CsdlTerm();
			term.setName(property);
			term.setType("Edm.String");
			schema.getTerms().add(term);
		}
		for (int i = 0; i < appliesTo.length; i++) {
			if (!term.getAppliesTo().contains(appliesTo[i])) {
				term.getAppliesTo().add(appliesTo[i]);
			}
		}
	}

	private static String normalizeTermName(String name) {
		if (name.startsWith("{")) {
			int end = name.indexOf("}");
			if (end != -1) {
				String modified = null;
				String namespace = name.substring(1, end);
				for (Map.Entry<String, String> entry:MetadataFactory.BUILTIN_NAMESPACES.entrySet()) {
					if (entry.getValue().equals(namespace)) {
						modified = entry.getKey();
						break;
					}
				}
				return modified + ":" + name.substring(end+1);
			}
		}
		return name;
	}
}
