/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.atlas.typesystem.types;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.atlas.AtlasException;
import org.apache.atlas.classification.InterfaceAudience;
import org.apache.atlas.typesystem.TypesDef;
import org.apache.atlas.typesystem.exception.TypeExistsException;
import org.apache.atlas.typesystem.exception.TypeNotFoundException;
import org.apache.atlas.typesystem.types.cache.DefaultTypeCache;
import org.apache.atlas.typesystem.types.cache.TypeCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;
import java.lang.reflect.Constructor;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;

@Singleton
@InterfaceAudience.Private
public class TypeSystem {
    private static final Logger LOG = LoggerFactory.getLogger(TypeSystem.class);

    private static final TypeSystem INSTANCE = new TypeSystem();
    private static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
        @Override
        public SimpleDateFormat initialValue() {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            return dateFormat;
        }
    };

    private TypeCache typeCache  = new DefaultTypeCache();
    private IdType idType;
    private Map<String, IDataType> coreTypes;

    public TypeSystem() {
        initialize();
    }

    public static TypeSystem getInstance() {
        return INSTANCE;
    }

    /**
     * This is only used for testing purposes. Not intended for public use.
     */
    @InterfaceAudience.Private
    public TypeSystem reset() {

        typeCache.clear(); // clear all entries in cache
        initialize();

        return this;
    }

    public void setTypeCache(TypeCache typeCache) {
        this.typeCache = typeCache;
    }

    private void initialize() {

        coreTypes = new ConcurrentHashMap<>();

        registerPrimitiveTypes();
        registerCoreTypes();
    }

    public ImmutableList<String> getCoreTypes() {
        return ImmutableList.copyOf(coreTypes.keySet());
    }

    public ImmutableList<String> getTypeNames() throws AtlasException {
        List<String> typeNames = new ArrayList<>(typeCache.getAllTypeNames());
        return ImmutableList.copyOf(typeNames);
    }

    public ImmutableList<String> getTypeNamesByCategory(final DataTypes.TypeCategory typeCategory) throws AtlasException {
        return getTypeNames(new HashMap<TypeCache.TYPE_FILTER, String>() {{
            put(TypeCache.TYPE_FILTER.CATEGORY, typeCategory.name());
        }});
    }

    public ImmutableList<String> getTypeNames(Map<TypeCache.TYPE_FILTER, String> filterMap) throws AtlasException {
        return ImmutableList.copyOf(typeCache.getTypeNames(filterMap));
    }

    private void registerPrimitiveTypes() {
        coreTypes.put(DataTypes.BOOLEAN_TYPE.getName(), DataTypes.BOOLEAN_TYPE);
        coreTypes.put(DataTypes.BYTE_TYPE.getName(), DataTypes.BYTE_TYPE);
        coreTypes.put(DataTypes.SHORT_TYPE.getName(), DataTypes.SHORT_TYPE);
        coreTypes.put(DataTypes.INT_TYPE.getName(), DataTypes.INT_TYPE);
        coreTypes.put(DataTypes.LONG_TYPE.getName(), DataTypes.LONG_TYPE);
        coreTypes.put(DataTypes.FLOAT_TYPE.getName(), DataTypes.FLOAT_TYPE);
        coreTypes.put(DataTypes.DOUBLE_TYPE.getName(), DataTypes.DOUBLE_TYPE);
        coreTypes.put(DataTypes.BIGINTEGER_TYPE.getName(), DataTypes.BIGINTEGER_TYPE);
        coreTypes.put(DataTypes.BIGDECIMAL_TYPE.getName(), DataTypes.BIGDECIMAL_TYPE);
        coreTypes.put(DataTypes.DATE_TYPE.getName(), DataTypes.DATE_TYPE);
        coreTypes.put(DataTypes.STRING_TYPE.getName(), DataTypes.STRING_TYPE);
    }

    /*
     * The only core OOB type we will define is the Struct to represent the Identity of an Instance.
     */
    private void registerCoreTypes() {

        idType = new IdType();
        coreTypes.put(idType.getStructType().getName(), idType.getStructType());
    }

    public IdType getIdType() {
        return idType;
    }

    public boolean isRegistered(String typeName) throws AtlasException {
        return isCoreType(typeName) || typeCache.has(typeName);
    }

    protected boolean isCoreType(String typeName) {

        return coreTypes.containsKey(typeName);
    }

    public <T> T getDataType(Class<T> cls, String name) throws AtlasException {
        if (isCoreType(name)) {
            return cls.cast(coreTypes.get(name));
        }

        if (typeCache.has(name)) {
            try {
                return cls.cast(typeCache.get(name));
            } catch (ClassCastException cce) {
                throw new AtlasException(cce);
            }
        }

        /*
         * is this an Array Type?
         */
        String arrElemType = TypeUtils.parseAsArrayType(name);
        if (arrElemType != null) {
            IDataType dT = defineArrayType(getDataType(IDataType.class, arrElemType));
            return cls.cast(dT);
        }

        /*
         * is this a Map Type?
         */
        String[] mapType = TypeUtils.parseAsMapType(name);
        if (mapType != null) {
            IDataType dT =
                    defineMapType(getDataType(IDataType.class, mapType[0]), getDataType(IDataType.class, mapType[1]));
            return cls.cast(dT);
        }

        /*
         * Invoke cache callback to possibly obtain type from other storage.
         */
        IDataType dT = typeCache.onTypeFault(name);
        if (dT != null) {
            return cls.cast(dT);
        }

        throw new TypeNotFoundException(String.format("Unknown datatype: %s", name));
    }

    public StructType defineStructType(String name, boolean errorIfExists, AttributeDefinition... attrDefs)
    throws AtlasException {
        return defineStructType(name, null, errorIfExists, attrDefs);
    }

    public StructType defineStructType(String name, String description, boolean errorIfExists, AttributeDefinition... attrDefs)
    throws AtlasException {
        StructTypeDefinition structDef = new StructTypeDefinition(name, description, attrDefs);
        defineTypes(ImmutableList.<EnumTypeDefinition>of(), ImmutableList.of(structDef),
                ImmutableList.<HierarchicalTypeDefinition<TraitType>>of(),
                ImmutableList.<HierarchicalTypeDefinition<ClassType>>of());
        return getDataType(StructType.class, structDef.typeName);
    }

    /**
     * construct a temporary StructType for a Query Result. This is not registered in the
     * typeSystem.
     * The attributes in the typeDefinition can only reference permanent types.
     * @param name     struct type name
     * @param attrDefs struct type definition
     * @return temporary struct type
     * @throws AtlasException
     */
    public StructType defineQueryResultType(String name, Map<String, IDataType> tempTypes,
            AttributeDefinition... attrDefs) throws AtlasException {

        AttributeInfo[] infos = new AttributeInfo[attrDefs.length];
        for (int i = 0; i < attrDefs.length; i++) {
            infos[i] = new AttributeInfo(this, attrDefs[i], tempTypes);
        }

        return new StructType(this, name, null, infos);
    }

    public TraitType defineTraitType(HierarchicalTypeDefinition<TraitType> traitDef) throws AtlasException {
        defineTypes(ImmutableList.<EnumTypeDefinition>of(), ImmutableList.<StructTypeDefinition>of(),
                ImmutableList.of(traitDef), ImmutableList.<HierarchicalTypeDefinition<ClassType>>of());
        return getDataType(TraitType.class, traitDef.typeName);
    }

    public ClassType defineClassType(HierarchicalTypeDefinition<ClassType> classDef) throws AtlasException {
        defineTypes(ImmutableList.<EnumTypeDefinition>of(), ImmutableList.<StructTypeDefinition>of(),
                ImmutableList.<HierarchicalTypeDefinition<TraitType>>of(), ImmutableList.of(classDef));
        return getDataType(ClassType.class, classDef.typeName);
    }

    public Map<String, IDataType> defineTraitTypes(HierarchicalTypeDefinition<TraitType>... traitDefs)
    throws AtlasException {
        TransientTypeSystem transientTypes =
                new TransientTypeSystem(ImmutableList.<EnumTypeDefinition>of(),
                        ImmutableList.<StructTypeDefinition>of(), ImmutableList.copyOf(traitDefs),
                        ImmutableList.<HierarchicalTypeDefinition<ClassType>>of());
        return transientTypes.defineTypes(false);
    }

    public Map<String, IDataType> defineClassTypes(HierarchicalTypeDefinition<ClassType>... classDefs)
    throws AtlasException {
        TransientTypeSystem transientTypes = new TransientTypeSystem(ImmutableList.<EnumTypeDefinition>of(),
                ImmutableList.<StructTypeDefinition>of(), ImmutableList.<HierarchicalTypeDefinition<TraitType>>of(),
                ImmutableList.copyOf(classDefs));
        return transientTypes.defineTypes(false);
    }

    public Map<String, IDataType> updateTypes(TypesDef typesDef) throws AtlasException {
        ImmutableList<EnumTypeDefinition> enumDefs = ImmutableList.copyOf(typesDef.enumTypesAsJavaList());
        ImmutableList<StructTypeDefinition> structDefs = ImmutableList.copyOf(typesDef.structTypesAsJavaList());
        ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs =
                ImmutableList.copyOf(typesDef.traitTypesAsJavaList());
        ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs =
                ImmutableList.copyOf(typesDef.classTypesAsJavaList());

        TransientTypeSystem transientTypes = new TransientTypeSystem(enumDefs, structDefs, traitDefs, classDefs);
        return transientTypes.defineTypes(true);
    }

    public Map<String, IDataType> defineTypes(TypesDef typesDef) throws AtlasException {
        ImmutableList<EnumTypeDefinition> enumDefs = ImmutableList.copyOf(typesDef.enumTypesAsJavaList());
        ImmutableList<StructTypeDefinition> structDefs = ImmutableList.copyOf(typesDef.structTypesAsJavaList());
        ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs =
                ImmutableList.copyOf(typesDef.traitTypesAsJavaList());
        ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs =
                ImmutableList.copyOf(typesDef.classTypesAsJavaList());

        return defineTypes(enumDefs, structDefs, traitDefs, classDefs);
    }

    public Map<String, IDataType> defineTypes(ImmutableList<EnumTypeDefinition> enumDefs,
            ImmutableList<StructTypeDefinition> structDefs,
            ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs,
            ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs) throws AtlasException {
        TransientTypeSystem transientTypes = new TransientTypeSystem(enumDefs, structDefs, traitDefs, classDefs);
        return transientTypes.defineTypes(false);
    }

    public DataTypes.ArrayType defineArrayType(IDataType elemType) throws AtlasException {
        assert elemType != null;
        DataTypes.ArrayType dT = new DataTypes.ArrayType(elemType);
        return dT;
    }

    public DataTypes.MapType defineMapType(IDataType keyType, IDataType valueType) throws AtlasException {
        assert keyType != null;
        assert valueType != null;
        DataTypes.MapType dT = new DataTypes.MapType(keyType, valueType);
        return dT;
    }

    public EnumType defineEnumType(String name, EnumValue... values) throws AtlasException {
        return defineEnumType(new EnumTypeDefinition(name, values));
    }

    public EnumType defineEnumType(String name, String description, EnumValue... values) throws AtlasException {
        return defineEnumType(new EnumTypeDefinition(name, description, values));
    }

    public EnumType defineEnumType(EnumTypeDefinition eDef) throws AtlasException {
        assert eDef.name != null;
        if (isRegistered(eDef.name)) {
            throw new AtlasException(String.format("Redefinition of type %s not supported", eDef.name));
        }

        EnumType eT = new EnumType(this, eDef.name, eDef.description, eDef.enumValues);
        typeCache.put(eT);
        return eT;
    }

    public SimpleDateFormat getDateFormat() {
        return dateFormat.get();
    }

    public boolean allowNullsInCollections() {
        return false;
    }

    /**
     * Create an instance of {@link TransientTypeSystem} with the types defined in the {@link TypesDef}.
     *
     * As part of this, a set of verifications are run on the types defined.
     * @param typesDef The new list of types to be created or updated.
     * @param isUpdate True, if types are updated, false otherwise.
     * @return {@link TransientTypeSystem} that holds the newly added types.
     * @throws AtlasException
     */
    public TransientTypeSystem createTransientTypeSystem(TypesDef typesDef, boolean isUpdate) throws AtlasException {
        ImmutableList<EnumTypeDefinition> enumDefs = ImmutableList.copyOf(typesDef.enumTypesAsJavaList());
        ImmutableList<StructTypeDefinition> structDefs = ImmutableList.copyOf(typesDef.structTypesAsJavaList());
        ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs =
                ImmutableList.copyOf(typesDef.traitTypesAsJavaList());
        ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs =
                ImmutableList.copyOf(typesDef.classTypesAsJavaList());
        TransientTypeSystem transientTypeSystem = new TransientTypeSystem(enumDefs, structDefs, traitDefs, classDefs);
        transientTypeSystem.verifyTypes(isUpdate);
        return transientTypeSystem;
    }

    /**
     * Commit the given types to this {@link TypeSystem} instance.
     *
     * This step should be called only after the types have been committed to the backend stores successfully.
     * @param typesAdded newly added types.
     * @throws AtlasException
     */
    public void commitTypes(Map<String, IDataType> typesAdded) throws AtlasException {
        for (Map.Entry<String, IDataType> typeEntry : typesAdded.entrySet()) {
            IDataType type = typeEntry.getValue();
            //Add/replace the new type in the typesystem
            typeCache.put(type);
        }
    }

    public class TransientTypeSystem extends TypeSystem {

        final ImmutableList<StructTypeDefinition> structDefs;
        final ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs;
        final ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs;
        private final ImmutableList<EnumTypeDefinition> enumDefs;

        Map<String, StructTypeDefinition> structNameToDefMap = new HashMap<>();
        Map<String, HierarchicalTypeDefinition<TraitType>> traitNameToDefMap = new HashMap<>();
        Map<String, HierarchicalTypeDefinition<ClassType>> classNameToDefMap = new HashMap<>();

        Map<String, IDataType> transientTypes = null;

        List<AttributeInfo> recursiveRefs = new ArrayList<>();
        List<DataTypes.ArrayType> recursiveArrayTypes = new ArrayList<>();
        List<DataTypes.MapType> recursiveMapTypes = new ArrayList<>();


        TransientTypeSystem(ImmutableList<EnumTypeDefinition> enumDefs, ImmutableList<StructTypeDefinition> structDefs,
                            ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs,
                            ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs) {
            this.enumDefs = enumDefs;
            this.structDefs = structDefs;
            this.traitDefs = traitDefs;
            this.classDefs = classDefs;
            transientTypes = new HashMap<>();
        }

        private IDataType dataType(String name) throws AtlasException {
            if (transientTypes.containsKey(name)) {
                return transientTypes.get(name);
            }

            return TypeSystem.this.getDataType(IDataType.class, name);
        }

        /*
         * Step 1:
         * - validate cannot redefine types
         * - setup shallow Type instances to facilitate recursive type graphs
         */
        private void validateAndSetupShallowTypes(boolean update) throws AtlasException {
            for (EnumTypeDefinition eDef : enumDefs) {
                assert eDef.name != null;
                if (!update && isRegistered(eDef.name)) {
                    throw new AtlasException(String.format("Redefinition of type %s not supported", eDef.name));
                }

                EnumType eT = new EnumType(this, eDef.name, eDef.description, eDef.enumValues);
                transientTypes.put(eDef.name, eT);
            }

            for (StructTypeDefinition sDef : structDefs) {
                assert sDef.typeName != null;
                if (!update && isRegistered(sDef.typeName)) {
                    throw new TypeExistsException(String.format("Cannot redefine type %s", sDef.typeName));
                }
                StructType sT = new StructType(this, sDef.typeName, sDef.typeDescription, sDef.attributeDefinitions.length);
                structNameToDefMap.put(sDef.typeName, sDef);
                transientTypes.put(sDef.typeName, sT);
            }

            for (HierarchicalTypeDefinition<TraitType> traitDef : traitDefs) {
                assert traitDef.typeName != null;
                if (!update && isRegistered(traitDef.typeName)) {
                    throw new TypeExistsException(String.format("Cannot redefine type %s", traitDef.typeName));
                }
                TraitType tT = new TraitType(this, traitDef.typeName, traitDef.typeDescription, traitDef.superTypes,
                        traitDef.attributeDefinitions.length);
                traitNameToDefMap.put(traitDef.typeName, traitDef);
                transientTypes.put(traitDef.typeName, tT);
            }

            for (HierarchicalTypeDefinition<ClassType> classDef : classDefs) {
                assert classDef.typeName != null;
                if (!update && isRegistered(classDef.typeName)) {
                    throw new TypeExistsException(String.format("Cannot redefine type %s", classDef.typeName));
                }

                ClassType cT = new ClassType(this, classDef.typeName, classDef.typeDescription, classDef.superTypes,
                        classDef.attributeDefinitions.length);
                classNameToDefMap.put(classDef.typeName, classDef);
                transientTypes.put(classDef.typeName, cT);
            }
        }

        @Override
        public boolean isRegistered(String typeName) throws AtlasException {
            return transientTypes.containsKey(typeName) || TypeSystem.this.isRegistered(typeName);
        }

        private <U extends HierarchicalType> void validateSuperTypes(Class<U> cls, HierarchicalTypeDefinition<U> def)
        throws AtlasException {
            for (String superTypeName : def.superTypes) {

                IDataType dT = dataType(superTypeName);

                if (dT == null) {
                    throw new AtlasException(
                            String.format("Unknown superType %s in definition of type %s", superTypeName,
                                    def.typeName));
                }

                if (!cls.isAssignableFrom(dT.getClass())) {
                    throw new AtlasException(
                            String.format("SuperType %s must be a %s, in definition of type %s", superTypeName,
                                    cls.getName(), def.typeName));
                }
            }
        }

        /*
         * Step 2:
         * - for Hierarchical Types, validate SuperTypes.
         * - for each Hierarchical Type setup their SuperTypes Graph
         */
        private void validateAndSetupSuperTypes() throws AtlasException {
            for (HierarchicalTypeDefinition<TraitType> traitDef : traitDefs) {
                validateSuperTypes(TraitType.class, traitDef);
                TraitType traitType = getDataType(TraitType.class, traitDef.typeName);
                traitType.setupSuperTypesGraph();
            }

            for (HierarchicalTypeDefinition<ClassType> classDef : classDefs) {
                validateSuperTypes(ClassType.class, classDef);
                ClassType classType = getDataType(ClassType.class, classDef.typeName);
                classType.setupSuperTypesGraph();
            }
        }

        private AttributeInfo constructAttributeInfo(AttributeDefinition attrDef) throws AtlasException {
            AttributeInfo info = new AttributeInfo(this, attrDef, null);
            if (transientTypes.keySet().contains(attrDef.dataTypeName)) {
                recursiveRefs.add(info);
            }
            if (info.dataType().getTypeCategory() == DataTypes.TypeCategory.ARRAY) {
                DataTypes.ArrayType arrType = (DataTypes.ArrayType) info.dataType();
                if (transientTypes.keySet().contains(arrType.getElemType().getName())) {
                    recursiveArrayTypes.add(arrType);
                }
            }
            if (info.dataType().getTypeCategory() == DataTypes.TypeCategory.MAP) {
                DataTypes.MapType mapType = (DataTypes.MapType) info.dataType();
                if (transientTypes.keySet().contains(mapType.getKeyType().getName())) {
                    recursiveMapTypes.add(mapType);
                } else if (transientTypes.keySet().contains(mapType.getValueType().getName())) {
                    recursiveMapTypes.add(mapType);
                }
            }

            if (info.multiplicity.upper > 1 && !(info.dataType().getTypeCategory() == DataTypes.TypeCategory.MAP
                    || info.dataType().getTypeCategory() == DataTypes.TypeCategory.ARRAY)) {
                throw new AtlasException(
                        String.format("A multiplicty of more than one requires a collection type for attribute '%s'",
                                info.name));
            }

            return info;
        }

        private StructType constructStructureType(StructTypeDefinition def) throws AtlasException {
            AttributeInfo[] infos = new AttributeInfo[def.attributeDefinitions.length];
            for (int i = 0; i < def.attributeDefinitions.length; i++) {
                infos[i] = constructAttributeInfo(def.attributeDefinitions[i]);
            }

            StructType type = new StructType(this, def.typeName, def.typeDescription, infos);
            transientTypes.put(def.typeName, type);
            return type;
        }

        private <U extends HierarchicalType> U constructHierarchicalType(Class<U> cls,
                HierarchicalTypeDefinition<U> def) throws AtlasException {
            AttributeInfo[] infos = new AttributeInfo[def.attributeDefinitions.length];
            for (int i = 0; i < def.attributeDefinitions.length; i++) {
                infos[i] = constructAttributeInfo(def.attributeDefinitions[i]);
            }

            try {
                Constructor<U> cons = cls.getDeclaredConstructor(TypeSystem.class, String.class, String.class, ImmutableSet.class,
                        AttributeInfo[].class);
                U type = cons.newInstance(this, def.typeName, def.typeDescription, def.superTypes, infos);
                transientTypes.put(def.typeName, type);
                return type;
            } catch (Exception e) {
                e.printStackTrace();
                throw new AtlasException(String.format("Cannot construct Type of MetaType %s - %s", cls.getName(), def.typeName), e);
            }
        }

        /*
         * Step 3:
         * - Order Hierarchical Types in order of SuperType before SubType.
         * - Construct all the Types
         */
        private void orderAndConstructTypes() throws AtlasException {

            List<TraitType> traitTypes = new ArrayList<>();
            for (String traitTypeName : traitNameToDefMap.keySet()) {
                traitTypes.add(getDataType(TraitType.class, traitTypeName));
            }
            traitTypes = HierarchicalTypeDependencySorter.sortTypes(traitTypes);

            List<ClassType> classTypes = new ArrayList<>();
            for (String classTypeName : classNameToDefMap.keySet()) {
                classTypes.add(getDataType(ClassType.class, classTypeName));
            }
            classTypes = HierarchicalTypeDependencySorter.sortTypes(classTypes);

            for (StructTypeDefinition structDef : structDefs) {
                constructStructureType(structDef);
            }

            for (TraitType traitType : traitTypes) {
                constructHierarchicalType(TraitType.class, traitNameToDefMap.get(traitType.getName()));
            }

            for (ClassType classType : classTypes) {
                constructHierarchicalType(ClassType.class, classNameToDefMap.get(classType.getName()));
            }
        }

        /*
         * Step 4:
         * - fix up references in recursive AttrInfo and recursive Collection Types.
         */
        private void setupRecursiveTypes() throws AtlasException {
            for (AttributeInfo info : recursiveRefs) {
                info.setDataType(dataType(info.dataType().getName()));
            }
            for (DataTypes.ArrayType arrType : recursiveArrayTypes) {
                arrType.setElemType(dataType(arrType.getElemType().getName()));
            }
            for (DataTypes.MapType mapType : recursiveMapTypes) {
                mapType.setKeyType(dataType(mapType.getKeyType().getName()));
                mapType.setValueType(dataType(mapType.getValueType().getName()));
            }
        }

        /**
         * Step 5:
         * - Validate that the update can be done
         */
        private void validateUpdateIsPossible() throws TypeUpdateException, AtlasException {
            //If the type is modified, validate that update can be done
            for (IDataType newType : transientTypes.values()) {
                IDataType oldType = null;
                try {
                    oldType = TypeSystem.this.getDataType(IDataType.class, newType.getName());
                } catch (TypeNotFoundException e) {
                    LOG.debug("No existing type %s found - update OK", newType.getName());
                }
                if (oldType != null) {
                    oldType.validateUpdate(newType);
                }
            }
        }

        Map<String, IDataType> defineTypes(boolean update) throws AtlasException {
            verifyTypes(update);
            Map<String, IDataType> typesAdded = getTypesAdded();
            commitTypes(typesAdded);
            return typesAdded;
        }

        @Override
        public ImmutableList<String> getTypeNames() throws AtlasException {
            Set<String> typeNames = transientTypes.keySet();
            typeNames.addAll(TypeSystem.this.getTypeNames());
            return ImmutableList.copyOf(typeNames);
        }

        //get from transient types. Else, from main type system
        @Override
        public <T> T getDataType(Class<T> cls, String name) throws AtlasException {
            if (transientTypes != null) {
                if (transientTypes.containsKey(name)) {
                    try {
                        return cls.cast(transientTypes.get(name));
                    } catch (ClassCastException cce) {
                        throw new AtlasException(cce);
                    }
                }

            /*
             * is this an Array Type?
             */
                String arrElemType = TypeUtils.parseAsArrayType(name);
                if (arrElemType != null) {
                    IDataType dT = defineArrayType(getDataType(IDataType.class, arrElemType));
                    return cls.cast(dT);
                }

            /*
             * is this a Map Type?
             */
                String[] mapType = TypeUtils.parseAsMapType(name);
                if (mapType != null) {
                    IDataType dT =
                            defineMapType(getDataType(IDataType.class, mapType[0]), getDataType(IDataType.class, mapType[1]));
                    return cls.cast(dT);
                }
            }

            return TypeSystem.this.getDataType(cls, name);
        }

        @Override
        public StructType defineStructType(String name, boolean errorIfExists, AttributeDefinition... attrDefs)
        throws AtlasException {
            throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
        }

        @Override
        public TraitType defineTraitType(HierarchicalTypeDefinition traitDef) throws AtlasException {
            throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
        }

        @Override
        public ClassType defineClassType(HierarchicalTypeDefinition<ClassType> classDef) throws AtlasException {
            throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
        }

        @Override
        public Map<String, IDataType> defineTypes(ImmutableList<EnumTypeDefinition> enumDefs,
                ImmutableList<StructTypeDefinition> structDefs,
                ImmutableList<HierarchicalTypeDefinition<TraitType>> traitDefs,
                ImmutableList<HierarchicalTypeDefinition<ClassType>> classDefs) throws AtlasException {
            throw new AtlasException("Internal Error: define type called on TransientTypeSystem");
        }

        @Override
        public DataTypes.ArrayType defineArrayType(IDataType elemType) throws AtlasException {
            return super.defineArrayType(elemType);
        }

        @Override
        public DataTypes.MapType defineMapType(IDataType keyType, IDataType valueType) throws AtlasException {
            return super.defineMapType(keyType, valueType);
        }

        void verifyTypes(boolean isUpdate) throws AtlasException {
            validateAndSetupShallowTypes(isUpdate);
            validateAndSetupSuperTypes();
            orderAndConstructTypes();
            setupRecursiveTypes();
            if (isUpdate) {
                validateUpdateIsPossible();
            }
        }

        @Override
        public void commitTypes(Map<String, IDataType> typesAdded) throws AtlasException {
            TypeSystem.this.commitTypes(typesAdded);
        }

        public Map<String, IDataType> getTypesAdded() {
            return new HashMap<>(transientTypes);
        }

        /**
         * The core types do not change and they are registered
         * once in the main type system.
         */
        @Override
        public ImmutableList<String> getCoreTypes() {
            return TypeSystem.this.getCoreTypes();
        }
    }

    public class IdType {
        private static final String ID_ATTRNAME = "guid";
        private static final String TYPENAME_ATTRNAME = "typeName";
        private static final String STATE_ATTRNAME = "state";
        private static final String TYP_NAME = "__IdType";

        private StructType type;

        private IdType() {
            AttributeDefinition idAttr =
                    new AttributeDefinition(ID_ATTRNAME, DataTypes.STRING_TYPE.getName(), Multiplicity.REQUIRED, false,
                            null);
            AttributeDefinition typNmAttr =
                    new AttributeDefinition(TYPENAME_ATTRNAME, DataTypes.STRING_TYPE.getName(), Multiplicity.REQUIRED,
                            false, null);
            AttributeDefinition stateAttr =
                    new AttributeDefinition(STATE_ATTRNAME, DataTypes.STRING_TYPE.getName(), Multiplicity.REQUIRED,
                            false, null);
            try {
                AttributeInfo[] infos = new AttributeInfo[3];
                infos[0] = new AttributeInfo(TypeSystem.this, idAttr, null);
                infos[1] = new AttributeInfo(TypeSystem.this, typNmAttr, null);
                infos[2] = new AttributeInfo(TypeSystem.this, stateAttr, null);

                type = new StructType(TypeSystem.this, TYP_NAME, null, infos);
            } catch (AtlasException me) {
                throw new RuntimeException(me);
            }
        }

        public StructType getStructType() {
            return type;
        }

        public String getName() {
            return TYP_NAME;
        }

        public String idAttrName() {
            return ID_ATTRNAME;
        }

        public String typeNameAttrName() {
            return TYPENAME_ATTRNAME;
        }

        public String stateAttrName() {
            return STATE_ATTRNAME;
        }
    }

    public static final String ID_STRUCT_ID_ATTRNAME = IdType.ID_ATTRNAME;
    public static final String ID_STRUCT_TYP_NAME = IdType.TYP_NAME;
}
