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

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;

import org.core4j.Enumerable;
import org.odata4j.edm.*;
import org.odata4j.edm.EdmFunctionParameter.Mode;
import org.odata4j.edm.EdmProperty.CollectionKind;
import org.odata4j.edm.EdmSchema.Builder;
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.BaseColumn.NullType;
import org.teiid.metadata.*;

public class ODataEntitySchemaBuilder {
	
	static EdmDataServices buildMetadata(MetadataStore metadataStore) {
		try {
			List<EdmSchema.Builder> edmSchemas = new ArrayList<EdmSchema.Builder>();
			for (Schema schema:metadataStore.getSchemaList()) {
				buildEntityTypes(schema, edmSchemas, true);
				buildFunctionImports(schema, edmSchemas, true);
				buildAssosiationSets(schema, edmSchemas, true);
			}
			return EdmDataServices.newBuilder().addSchemas(edmSchemas).build();
		} catch (Exception e) {
			throw new TeiidRuntimeException(e);
		}
	}
	
	static EdmDataServices buildMetadata(Schema schema) {
		try {
			List<EdmSchema.Builder> edmSchemas = new ArrayList<EdmSchema.Builder>();
			buildEntityTypes(schema, edmSchemas, true);
			buildFunctionImports(schema, edmSchemas, true);
			buildAssosiationSets(schema, edmSchemas, true);
			return EdmDataServices.newBuilder().addSchemas(edmSchemas).build();
		} catch (Exception e) {
			throw new TeiidRuntimeException(e);
		}
	}
	
	static org.odata4j.edm.EdmEntitySet.Builder findEntitySet(List<Builder> edmSchemas, String schemaName, String enitityName) {
		for (EdmSchema.Builder modelSchema:edmSchemas) {
			if (modelSchema.getNamespace().equals(schemaName)) {
				for (EdmEntityContainer.Builder entityContainter:modelSchema.getEntityContainers()) {
					for(EdmEntitySet.Builder entitySet:entityContainter.getEntitySets()) {
						if (entitySet.getName().equals(enitityName)) {
							return entitySet;
						}
					}
				}
			}
		}
		return null;
	}
	
	static EdmSchema.Builder findSchema(List<Builder> edmSchemas, String schemaName) {
		for (EdmSchema.Builder modelSchema:edmSchemas) {
			if (modelSchema.getNamespace().equals(schemaName)) {
				return modelSchema;
			}
		}
		return null;
	}	
	
	static org.odata4j.edm.EdmEntityType.Builder findEntityType(List<Builder> edmSchemas, String schemaName, String enitityName) {
		for (EdmSchema.Builder modelSchema:edmSchemas) {
			if (modelSchema.getNamespace().equals(schemaName)) {
				for (EdmEntityType.Builder type:modelSchema.getEntityTypes()) {
					if (type.getName().equals(enitityName)) {
						return type;
					}
				}
			}
		}
		return null;
	}	
	
	static EdmEntityContainer.Builder findEntityContainer(List<Builder> edmSchemas, String schemaName) {
		for (EdmSchema.Builder modelSchema:edmSchemas) {
			if (modelSchema.getNamespace().equals(schemaName)) {
				for (EdmEntityContainer.Builder entityContainter:modelSchema.getEntityContainers()) {
					return entityContainter;
				}
			}
		}
		return null;
	}	
	
	public static void buildEntityTypes(Schema schema, List<EdmSchema.Builder> edmSchemas, boolean preserveEntityTypeName) {
		List<EdmEntitySet.Builder> entitySets = new ArrayList<EdmEntitySet.Builder>();
		List<EdmEntityType.Builder> entityTypes = new ArrayList<EdmEntityType.Builder>();
		LinkedHashMap<String, EdmComplexType.Builder> complexTypes = new LinkedHashMap<String, EdmComplexType.Builder>();
	    
		if (preserveEntityTypeName) {
			//first pass, build complex types
			for (Table table: schema.getTables().values()) {
				// skip if the table does not have the PK or unique
				KeyRecord primaryKey = table.getPrimaryKey();
				List<KeyRecord> uniques = table.getUniqueKeys();
				if (primaryKey == null && uniques.isEmpty()) {
					LogManager.logDetail(LogConstants.CTX_ODATA, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17017, table.getFullName()));
					continue;
				}
				
				for (Column c : table.getColumns()) {
					String columnGroup = c.getProperty(ODataMetadataProcessor.COLUMN_GROUP, false);
					String name = c.getName();
					if (columnGroup != null) {
						//use the real name not the group_column name
						name = c.getSourceName();
					}
					String complexType = c.getProperty(ODataMetadataProcessor.COMPLEX_TYPE, false);
					if (complexType == null) {
						continue;
					}
					EdmComplexType.Builder complexTypeBuilder = complexTypes.get(complexType);
					if (complexTypeBuilder == null) {
						complexTypeBuilder = EdmComplexType.newBuilder();
						complexTypes.put(complexType, complexTypeBuilder); 
						complexTypeBuilder.setName(complexType);
						complexTypeBuilder.setNamespace(schema.getName());
					} else if (complexTypeBuilder.findProperty(name) != null) {
						continue; //already added
					}
					EdmProperty.Builder property = EdmProperty.newBuilder(name)
							.setType(ODataTypeManager.odataType(c.getRuntimeType()))
							.setNullable(isPartOfPrimaryKey(table, c.getName())?false:c.getNullType() == NullType.Nullable);
					if (c.getRuntimeType().equals(DataTypeManager.DefaultDataTypes.STRING)) {
						property.setFixedLength(c.isFixedLength())
							.setMaxLength(c.getLength())
							.setUnicode(true);
					}
					complexTypeBuilder.addProperties(property);
				}
			}
		}
		
		//second pass, add all columns
		for (Table table: schema.getTables().values()) {
			
			// skip if the table does not have the PK or unique
			KeyRecord primaryKey = table.getPrimaryKey();
			List<KeyRecord> uniques = table.getUniqueKeys();
			if (primaryKey == null && uniques.isEmpty()) {
				LogManager.logDetail(LogConstants.CTX_ODATA, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17017, table.getFullName()));
				continue;
			}

			String entityTypeName = table.getName();
			if (preserveEntityTypeName && table.getProperty(ODataMetadataProcessor.ENTITY_TYPE, false) != null) {
				entityTypeName = table.getProperty(ODataMetadataProcessor.ENTITY_TYPE, false);
			}
	    	EdmEntityType.Builder entityType = EdmEntityType
					.newBuilder().setName(entityTypeName)
					.setNamespace(schema.getName());
			
	    	// adding key
	    	if (primaryKey != null) {
				for (Column c : primaryKey.getColumns()) {
					entityType.addKeys(c.getName());
				}
	    	}
	    	else {
				for (Column c : uniques.get(0).getColumns()) {
					entityType.addKeys(c.getName());
				}
	    	}
	    	
	    	HashSet<String> columnGroups = new HashSet<String>();
			// adding properties
			for (Column c : table.getColumns()) {
				String complexType = c.getProperty(ODataMetadataProcessor.COMPLEX_TYPE, false);
				if (complexType != null && preserveEntityTypeName) {
					String columnGroup = c.getProperty(ODataMetadataProcessor.COLUMN_GROUP, false);
					if (!columnGroups.add(columnGroup)) {
						continue;
					}
					EdmProperty.Builder property = EdmProperty.newBuilder(columnGroup)
							.setType(complexTypes.get(complexType).build());
					entityType.addProperties(property);
					continue;
				}
				EdmProperty.Builder property = EdmProperty.newBuilder(c.getName())
						.setType(ODataTypeManager.odataType(c.getRuntimeType()))
						.setNullable(isPartOfPrimaryKey(table, c.getName())?false:c.getNullType() == NullType.Nullable);
				if (c.getRuntimeType().equals(DataTypeManager.DefaultDataTypes.STRING)) {
					property.setFixedLength(c.isFixedLength())
						.setMaxLength(c.getLength())
						.setUnicode(true);
				}
				entityType.addProperties(property);
			}
							
			// entity set one for one entity type
			EdmEntitySet.Builder entitySet = EdmEntitySet.newBuilder()
					.setName(table.getName())
					.setEntityType(entityType);
			
			entityType.setNamespace(schema.getName());
			entitySets.add(entitySet);

			// add enitity types for entity schema
			entityTypes.add(entityType);
	    }
		
		// entity container is holder entity sets, association sets, function imports
		EdmEntityContainer.Builder entityContainer = EdmEntityContainer
				.newBuilder().setName(schema.getName())
				.setIsDefault(false)
				.addEntitySets(entitySets);
		
		// build entity schema
		EdmSchema.Builder modelSchema = EdmSchema.newBuilder()
				.setNamespace(schema.getName())
				.addEntityTypes(entityTypes)
				.addEntityContainers(entityContainer)
				.addComplexTypes(complexTypes.values());
		
		edmSchemas.add(modelSchema);
	}	
	
    static boolean isPartOfPrimaryKey(Table table, String columnName) {
        KeyRecord pk = table.getPrimaryKey();
        if (hasColumn(pk, columnName)) {
            return true;
        }
        for (KeyRecord key:table.getUniqueKeys()) {
            if (hasColumn(key, columnName)) {
                return true;
            }            
        }
        return false;
    }
    
    static boolean hasColumn(KeyRecord pk, String columnName) {
        if (pk != null) {
        	return pk.getColumnByName(columnName) != null;
        }
        return false;
    }
	
	private static String getEntityTypeName(Table t, boolean preserveEntityTypeName) {
		String entityTypeName = t.getName();
		if (preserveEntityTypeName && t.getProperty(ODataMetadataProcessor.ENTITY_TYPE, false) != null) {
			entityTypeName = t.getProperty(ODataMetadataProcessor.ENTITY_TYPE, false);
		}
		return entityTypeName;
	}
	
	public static void buildAssosiationSets(Schema schema, List<Builder> edmSchemas, boolean preserveEntityTypeName) {
		EdmSchema.Builder odataSchema = findSchema(edmSchemas, schema.getName());
		EdmEntityContainer.Builder entityContainer = findEntityContainer(edmSchemas, schema.getName());
		List<EdmAssociationSet.Builder> assosiationSets = new ArrayList<EdmAssociationSet.Builder>();
		List<EdmAssociation.Builder> assosiations = new ArrayList<EdmAssociation.Builder>();
		
		for (Table table: schema.getTables().values()) {
			
			// skip if the table does not have the PK or unique
			KeyRecord primaryKey = table.getPrimaryKey();
			List<KeyRecord> uniques = table.getUniqueKeys();				
			if (primaryKey == null && uniques.isEmpty()) {
				continue;
			}
			
			// build Associations
			for (ForeignKey fk:table.getForeignKeys()) {
				
				EdmEntitySet.Builder entitySet = findEntitySet(edmSchemas,schema.getName(), table.getName());
				EdmEntitySet.Builder refEntitySet = findEntitySet(edmSchemas, schema.getName(),fk.getReferenceTableName());
				EdmEntityType.Builder entityType = findEntityType(edmSchemas, schema.getName(), getEntityTypeName(table, preserveEntityTypeName));				
				EdmEntityType.Builder refEntityType = findEntityType(edmSchemas, schema.getName(), getEntityTypeName(fk.getPrimaryKey().getParent(), preserveEntityTypeName));
				
				// check to see if fk is part of this table's pk, then it is 1 to 1 relation
				boolean onetoone = sameColumnSet(table.getPrimaryKey(), fk);
				
				// Build Association Ends				
				EdmAssociationEnd.Builder endSelf = EdmAssociationEnd.newBuilder()
						.setRole(table.getName())
						.setType(entityType)
						.setMultiplicity(onetoone?EdmMultiplicity.ZERO_TO_ONE:EdmMultiplicity.MANY);
				
				EdmAssociationEnd.Builder endRef = EdmAssociationEnd.newBuilder()
						.setRole(fk.getReferenceTableName())
						.setType(refEntityType)
						.setMultiplicity(EdmMultiplicity.ZERO_TO_ONE);
				

				// Build Association
				EdmAssociation.Builder association = EdmAssociation.newBuilder();
				association.setName(table.getName()+"_"+fk.getName()); //$NON-NLS-1$
				association.setEnds(endSelf, endRef);
				association.setNamespace(refEntityType.getFullyQualifiedTypeName().substring(0, refEntityType.getFullyQualifiedTypeName().indexOf('.')));
				assosiations.add(association);
				
				// Build ReferentialConstraint
				if (fk.getReferenceColumns() != null) {
					EdmReferentialConstraint.Builder erc = EdmReferentialConstraint.newBuilder();
					erc.setPrincipalRole(fk.getReferenceTableName());
					erc.addPrincipalReferences(fk.getReferenceColumns());
					erc.setDependentRole(table.getName());
					erc.addDependentReferences(getColumnNames(fk.getColumns()));
					association.setRefConstraint(erc);
				}					
				
				if (!fk.getReferenceTableName().equalsIgnoreCase(table.getName())) {
				    // Add EdmNavigationProperty to entity type
				    String navigationName = getNavigationName(entityType, fk.getReferenceTableName());
    				EdmNavigationProperty.Builder nav = EdmNavigationProperty.newBuilder(navigationName);
    				nav.setRelationshipName(fk.getName());
    				nav.setFromToName(table.getName(), fk.getReferenceTableName());
    				nav.setRelationship(association);
    				nav.setFromTo(endSelf, endRef);
    				entityType.addNavigationProperties(nav);
				}
				
				// Add EdmNavigationProperty to Reference entity type
				String navigationName = getNavigationName(refEntityType, table.getName());
				EdmNavigationProperty.Builder refNav = EdmNavigationProperty.newBuilder(navigationName);
				refNav.setRelationshipName(fk.getName());
				refNav.setFromToName(fk.getReferenceTableName(), table.getName());
				refNav.setRelationship(association);
				refNav.setFromTo(endRef, endSelf);
				refEntityType.addNavigationProperties(refNav);					
				
				// build AssosiationSet
				EdmAssociationSet.Builder assosiationSet = EdmAssociationSet.newBuilder()
						.setName(table.getName()+"_"+fk.getName()) //$NON-NLS-1$
						.setAssociationName(fk.getName());	
				
				// Build AssosiationSet Ends
				EdmAssociationSetEnd.Builder endOne = EdmAssociationSetEnd.newBuilder()
						.setEntitySet(entitySet)
						.setRoleName(table.getName())
						.setRole(EdmAssociationEnd.newBuilder().setType(entityType).setRole(entityType.getName()));
				
				EdmAssociationSetEnd.Builder endTwo = EdmAssociationSetEnd.newBuilder()
						.setEntitySet(refEntitySet)
						.setRoleName(fk.getReferenceTableName())
						.setRole(EdmAssociationEnd.newBuilder().setType(refEntityType).setRole(refEntityType.getName()));					
				assosiationSet.setEnds(endOne, endTwo);
				
				assosiationSet.setAssociation(association);
				assosiationSets.add(assosiationSet);
			}
		}
		entityContainer.addAssociationSets(assosiationSets);
		odataSchema.addAssociations(assosiations);
	}	
	
	private static String getNavigationName(
            org.odata4j.edm.EdmEntityType.Builder entityType,
            String tableName) {

	    String navigationName = tableName;
	    int i = 1;
	    while(true) {
    	    for (EdmNavigationProperty.Builder b:entityType.getNavigationProperties()) {
    	        if (b.getName().equals(navigationName)) {
    	            navigationName = null;
    	            break;
    	        }
    	    }
    	    if (navigationName != null) {
    	        return navigationName;
    	    }
    	    navigationName = tableName+(i++);
	    }
    }

    public static void buildFunctionImports(Schema schema, List<Builder> edmSchemas, boolean preserveEntityTypeName) {
		EdmSchema.Builder odataSchema = findSchema(edmSchemas, schema.getName());
		EdmEntityContainer.Builder entityContainer = findEntityContainer(edmSchemas, schema.getName());
		
		// procedures
		for(Procedure proc:schema.getProcedures().values()) {
			EdmFunctionImport.Builder edmProcedure = EdmFunctionImport.newBuilder();
			edmProcedure.setName(proc.getName());
			String httpMethod = "POST"; //$NON-NLS-1$
			
			for (ProcedureParameter pp:proc.getParameters()) {
				if (pp.getName().equals("return")) { //$NON-NLS-1$
					httpMethod = "GET"; //$NON-NLS-1$
					edmProcedure.setReturnType(ODataTypeManager.odataType(pp.getRuntimeType()));						
					continue;
				}
				
				EdmFunctionParameter.Builder param = EdmFunctionParameter.newBuilder();
				param.setName(pp.getName());
				param.setType(ODataTypeManager.odataType(pp.getRuntimeType()));

				if (pp.getType() == ProcedureParameter.Type.In) {
					param.setMode(Mode.In);
				}
				else if (pp.getType() == ProcedureParameter.Type.InOut) {
					param.setMode(Mode.InOut);
				}
				else if (pp.getType() == ProcedureParameter.Type.Out) {
					param.setMode(Mode.Out);
				}
				
				param.setNullable(pp.getNullType() == NullType.Nullable);
				edmProcedure.addParameters(param);
			}
			
			// add a complex type for return resultset.
			ColumnSet<Procedure> returnColumns = proc.getResultSet();
			if (returnColumns != null) {
				httpMethod = "GET"; //$NON-NLS-1$
				EdmComplexType.Builder complexType = EdmComplexType.newBuilder();
				String entityTypeName = proc.getName()+"_"+returnColumns.getName(); //$NON-NLS-1$
				if (preserveEntityTypeName && proc.getProperty(ODataMetadataProcessor.ENTITY_TYPE, false) != null) {
					entityTypeName = proc.getProperty(ODataMetadataProcessor.ENTITY_TYPE, false);
				}				
				complexType.setName(entityTypeName); 
				complexType.setNamespace(schema.getName());
				for (Column c:returnColumns.getColumns()) {
					EdmProperty.Builder property = EdmProperty.newBuilder(c.getName())
							.setType(ODataTypeManager.odataType(c.getRuntimeType()))
							.setNullable(c.getNullType() == NullType.Nullable);
					if (c.getRuntimeType().equals(DataTypeManager.DefaultDataTypes.STRING)) {
						property.setFixedLength(c.isFixedLength())
							.setMaxLength(c.getLength())
							.setUnicode(true);
					}
					complexType.addProperties(property);
				}
				odataSchema.addComplexTypes(complexType);
				edmProcedure.setIsCollection(true);
				edmProcedure.setReturnType(EdmCollectionType.newBuilder().setCollectionType(complexType).setKind(CollectionKind.Collection));
			}
			edmProcedure.setHttpMethod(httpMethod);
			entityContainer.addFunctionImports(edmProcedure);
		}
	}	
	
	static List<String> getColumnNames(List<Column> columns){
		ArrayList<String> names = new ArrayList<String>();
		for (Column c: columns) {
			names.add(c.getName());
		}
		return names;
	}
	
	static boolean sameColumnSet(KeyRecord recordOne, KeyRecord recordTwo) {
		
		if (recordOne == null || recordTwo == null) {
			return false;
		}
		
		List<Column> setOne = recordOne.getColumns();
		List<Column> setTwo = recordTwo.getColumns();
		
		if (setOne.size() != setTwo.size()) {
			return false;
		}
		for (int i = 0; i < setOne.size(); i++) {
			Column one = setOne.get(i);
			Column two = setTwo.get(i);
			if (!one.getName().equals(two.getName())) {
				return false;
			}
		}
		return true;
	}
	
	static EdmEntitySet removeModelName(EdmEntitySet src) {
	    EdmEntityType srcType = src.getType();
	    String schemaName = srcType.getName().substring(0, srcType.getName().indexOf('.'));
	    String name = srcType.getName().substring(srcType.getName().indexOf('.')+1);
	    
        EdmEntityType.Builder targetType = EdmEntityType
                .newBuilder().setName(name)
                .setNamespace(schemaName);
        
        targetType.addKeys(srcType.getKeys());
        
        Enumerable<EdmProperty> properties = srcType.getProperties();
        for (EdmProperty srcProperty:properties.toList()) {

            EdmProperty.Builder tgtProperty = EdmProperty.newBuilder(srcProperty.getName())
                    .setType(srcProperty.getType())
                    .setNullable(srcProperty.isNullable())
                    .setFixedLength(srcProperty.getFixedLength())
                    .setMaxLength(srcProperty.getMaxLength())
                    .setUnicode(true);
            targetType.addProperties(tgtProperty);
        }
	    
        EdmEntitySet.Builder target = EdmEntitySet.newBuilder()
                .setName(src.getName())
                .setEntityType(targetType);
        return target.build();
	}
}
