/**
 * Copyright © 2012-2022 by Data Virtuality GmbH
 */
package org.teiid.metadata.odata;

import org.apache.olingo.commons.api.edm.provider.*;
import org.teiid.metadata.*;

import java.util.*;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * @author Dmitry Pogorelov
 * @date 2022.08.02
 */
public class ODataMetadata {

	private NavigableMap<String, ODataSchemaBuilder.ODataSchemaInfo> schemaInfos = new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER);
	private Set<String> aliases = Collections.synchronizedSet(new TreeSet<>(String.CASE_INSENSITIVE_ORDER));
	private NavigableMap<String, String> mapAliases = new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER);

	public static List<String> RESERVED = Arrays.asList("teiid", "edm", "core", "olingo-extensions"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	public String namespace;

	public ODataMetadata(String namespace) {
		this.namespace = namespace;
		aliases.addAll(RESERVED);
	}

	public void putRecord(AbstractMetadataRecord newValue) {
		if (newValue instanceof Schema) {
			if (getAliasBySchemaName(newValue.getName()) == null) {
				buildSchema((Schema) newValue);
				// in case of refresh metadata
				buildFullMetadata((Schema) newValue);
			}
		} else if (newValue instanceof Table) {
			buildEntityType((Table) newValue, null, null);
		} else if (newValue instanceof KeyRecord) {
			KeyRecord key = (KeyRecord) newValue;
			if (key.getType() == KeyRecord.Type.Primary || key.getType() == KeyRecord.Type.Unique) {
				Table table = key.getParent();
				if (table != null && table.getParent() != null)
					buildEntityType(table, key, null);
			}
		} /*else if (newValue instanceof Column) {
			ColumnSet<?> colSet = ((Column) newValue).getParent();
			if (colSet instanceof Table) {
				buildEntityType((Table) colSet, null, (Column) newValue);
			} else {
				Object parent = colSet.getParent();
				if (parent instanceof Procedure) {
					buildProcedure((Procedure) parent);
				}
			}
		}*/ else if (newValue instanceof Procedure) {
			buildProcedure((Procedure) newValue);
		} /*else if (newValue instanceof ProcedureParameter) {
			ProcedureParameter procParam = (ProcedureParameter) newValue;
			buildProcedure(procParam.getParent());
		}*/
	}

	private void buildEntityType(Table table, KeyRecord key, Column col) {
		Schema schema = table.getParent();
		if (getAliasBySchemaName(schema.getName()) == null)
			buildSchema(schema);

		ODataSchemaBuilder.ODataSchemaInfo info = schemaInfos.get(getAliasBySchemaName(schema.getName()));
		if (!ODataSchemaBuilder.buildEntityType(table, key, col, info))
			removeRecord(table);
	}

	private void buildProcedure(Procedure proc) {
		Schema schema = proc.getParent();
		if (getAliasBySchemaName(schema.getName()) == null)
			buildSchema(schema);

		ODataSchemaBuilder.ODataSchemaInfo schemaInfo = schemaInfos.get(getAliasBySchemaName(schema.getName()));
		if (!ODataSchemaBuilder.buildProcedure(proc, schemaInfo))
			removeRecord(proc);
	}

	private void buildSchema(Schema s) {
		//prevent collisions with the reserved, and then with any other schemas
		int i = 1;
		String alias = s.getName();
		while (!aliases.add(alias)) {
			alias = alias + "_" + i++; //$NON-NLS-1$
		}
		mapAliases.putIfAbsent(alias, s.getName());
		schemaInfos.putIfAbsent(alias, ODataSchemaBuilder.buildSchema(namespace, s, alias));
	}

	private void buildFullMetadata(Schema schema) {
		ODataSchemaBuilder.ODataSchemaInfo schemaInfo = schemaInfos.get(getAliasBySchemaName(schema.getName()));
		if (schemaInfo.tableInfos.isEmpty() && schemaInfo.procedureInfos.isEmpty()) {
			schema.getTables().values().forEach(t -> buildEntityType(t, null, null));
			schema.getProcedures().values().forEach(p -> buildProcedure(p));
		}
	}

	private String getAliasBySchemaName(String schemaName) {
		for (Map.Entry<String, String> entry : mapAliases.entrySet()) {
			if (entry.getValue().equalsIgnoreCase(schemaName))
				return entry.getKey();
		}
		return null;
	}

	public void removeRecord(AbstractMetadataRecord record) {
		if (record instanceof Schema) {
			removeSchema((Schema) record);
		} else if (record instanceof Table) {
			removeTable((Table) record);
		} /*else if (record instanceof KeyRecord) {
			KeyRecord keyRecord = (KeyRecord) record;
			removeTable(keyRecord.getParent());
		} else if (record instanceof Column) {
			ColumnSet<?> colSet = ((Column) record).getParent();
			if (colSet instanceof Table) {
				removeTable((Table) colSet);
			} else {
				Object parent = colSet.getParent();
				if (parent instanceof Procedure) {
					removeProcedure((Procedure) parent);
				}
			}
		}*/ else if (record instanceof Procedure) {
			removeProcedure((Procedure) record);
		}/* else if (record instanceof ProcedureParameter) {
			ProcedureParameter procParam = (ProcedureParameter) record;
			removeProcedure(procParam.getParent());
		}*/
	}

	public void removeProcedure(String schema, String procName) {
		String alias = getAliasBySchemaName(schema);
		if (alias != null) {
			ODataSchemaBuilder.ODataSchemaInfo schemaInfo = schemaInfos.get(alias);
			schemaInfo.procedureInfos.remove(procName);
		}
	}


	private void removeSchema(Schema s) {
		String alias = getAliasBySchemaName(s.getName());
		if (alias != null) {
			aliases.remove(alias);
			mapAliases.remove(alias);
			schemaInfos.remove(alias);
		}
	}

	private void removeTable(Table table) {
		Schema s = table.getParent();
		String alias = getAliasBySchemaName(s.getName());
		if (alias != null) {
			ODataSchemaBuilder.ODataSchemaInfo schemaInfo = schemaInfos.get(alias);
			schemaInfo.tableInfos.remove(table.getName());
		}
	}

	private void removeProcedure(Procedure proc) {
		Schema s = proc.getParent();
		String alias = getAliasBySchemaName(s.getName());
		if (alias != null) {
			ODataSchemaBuilder.ODataSchemaInfo schemaInfo = schemaInfos.get(alias);
			schemaInfo.procedureInfos.remove(proc.getName());
		}
	}

	public interface SecurityChecker {
		Set<String> getNotPermittedObjects(Set<String> resources);
	}

	public ODataSchemaBuilder.ODataSchemaInfo build(Schema schema, SecurityChecker securityChecker) {
		String alias = getAliasBySchemaName(schema.getName());
		if (alias != null) {
			ODataSchemaBuilder.ODataSchemaInfo schemaInfo = schemaInfos.get(alias);
			Set<String> notPermittedObjects = securityChecker.getNotPermittedObjects(schemaInfo.getAllResources());

			// entity container is holder entity sets, association sets, function imports, action imports
			List<CsdlEntitySet> entitySets = schemaInfo.schema.getEntityContainer().getEntitySets();
			entitySets.clear();
			// build entity schema
			List<CsdlEntityType> entityTypes = schemaInfo.schema.getEntityTypes();
			entityTypes.clear();
			for (Map.Entry<String, ODataSchemaBuilder.ODataTableInfo> entry : schemaInfo.tableInfos.entrySet()) {
				ODataSchemaBuilder.ODataTableInfo tableInfo = entry.getValue();
				if (notPermittedObjects.isEmpty() || !notPermittedObjects.contains(tableInfo.table.getFullName())) {
					entityTypes.add(tableInfo.entityType);
					entitySets.add(tableInfo.entitySet);
				}
			}

			List<CsdlComplexType> complexTypes = schemaInfo.schema.getComplexTypes();
			complexTypes.clear();
			List<CsdlFunction> functions = schemaInfo.schema.getFunctions();
			functions.clear();
			List<CsdlAction> actions = schemaInfo.schema.getActions();
			actions.clear();
			List<CsdlFunctionImport> functionImports = schemaInfo.schema.getEntityContainer().getFunctionImports();
			functionImports.clear();
			List<CsdlActionImport> actionImports = schemaInfo.schema.getEntityContainer().getActionImports();
			actionImports.clear();
			for (Map.Entry<String, ODataSchemaBuilder.ODataProcedureInfo> entry : schemaInfo.procedureInfos.entrySet()) {
				ODataSchemaBuilder.ODataProcedureInfo procedureInfo = entry.getValue();
				if (!notPermittedObjects.contains(procedureInfo.procedure.getFullName()) && !procedureInfo.procedure.isInvisibleInMetadata()) {
					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);
				}
			}
			return schemaInfo;
		}

		return null;
	}

	public ODataSchemaBuilder.ODataSchemaInfo getSchemaInfo(String schema) {
		return schemaInfos.get(schema);
	}

	public ODataMetadata clone(String vdbName) {
		ODataMetadata cloned = new ODataMetadata(vdbName);
		cloned.aliases.addAll(this.aliases);
		if((this.schemaInfos != null) && (this.schemaInfos.size() > 0)) {
			for (Map.Entry<String, ODataSchemaBuilder.ODataSchemaInfo> entry : this.schemaInfos.entrySet()) {
				cloned.schemaInfos.put(entry.getKey(), entry.getValue().clone());
			}
		}
		cloned.mapAliases.putAll(this.mapAliases);
		return cloned;
	}
}
