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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.TypesDef;
import org.apache.atlas.typesystem.types.AttributeDefinition;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.EnumTypeDefinition;
import org.apache.atlas.typesystem.types.HierarchicalTypeDefinition;
import org.apache.atlas.typesystem.types.IDataType;
import org.apache.atlas.typesystem.types.StructTypeDefinition;
import org.apache.atlas.typesystem.types.TraitType;
import org.apache.atlas.typesystem.types.TypeSystem;
import org.apache.atlas.typesystem.types.TypeSystem.TransientTypeSystem;
import org.apache.atlas.typesystem.types.TypeUtils;
import org.apache.atlas.typesystem.types.cache.DefaultTypeCache;
import org.apache.atlas.typesystem.types.utils.TypesUtil;

import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.google.inject.Singleton;


/**
 * An extension of {@link DefaultTypeCache} which loads
 * the requested type from the type store if it is not found in the cache,
 * and adds it to the cache if it's found in the store.
 * Any attribute and super types that are required by the requested type
 * are also loaded from the store if they are not already in the cache.
 */
@Singleton
public class StoreBackedTypeCache extends DefaultTypeCache {

    private ITypeStore typeStore;

    private ImmutableList<String> coreTypes;
    private TypeSystem typeSystem;

    @Inject
    public StoreBackedTypeCache(final ITypeStore typeStore) {
        this.typeStore = typeStore;
        typeSystem = TypeSystem.getInstance();
        coreTypes = typeSystem.getCoreTypes();
    }

    private static class Context {
        ImmutableList.Builder<EnumTypeDefinition> enums = ImmutableList.builder();
        ImmutableList.Builder<StructTypeDefinition> structs = ImmutableList.builder();
        ImmutableList.Builder<HierarchicalTypeDefinition<ClassType>> classTypes = ImmutableList.builder();
        ImmutableList.Builder<HierarchicalTypeDefinition<TraitType>> traits = ImmutableList.builder();
        Set<String> loadedFromStore = new HashSet<>();

        public void addTypesDefToLists(TypesDef typesDef) {

            List<EnumTypeDefinition> enumTypesAsJavaList = typesDef.enumTypesAsJavaList();
            enums.addAll(enumTypesAsJavaList);
            for (EnumTypeDefinition etd : enumTypesAsJavaList) {
                loadedFromStore.add(etd.name);
            }
            List<StructTypeDefinition> structTypesAsJavaList = typesDef.structTypesAsJavaList();
            structs.addAll(structTypesAsJavaList);
            for (StructTypeDefinition std : structTypesAsJavaList) {
                loadedFromStore.add(std.typeName);
            }
            List<HierarchicalTypeDefinition<ClassType>> classTypesAsJavaList = typesDef.classTypesAsJavaList();
            classTypes.addAll(classTypesAsJavaList);
            for (HierarchicalTypeDefinition<ClassType> classTypeDef : classTypesAsJavaList) {
                loadedFromStore.add(classTypeDef.typeName);
            }
            List<HierarchicalTypeDefinition<TraitType>> traitTypesAsJavaList = typesDef.traitTypesAsJavaList();
            traits.addAll(traitTypesAsJavaList);
            for (HierarchicalTypeDefinition<TraitType> traitTypeDef : traitTypesAsJavaList) {
                loadedFromStore.add(traitTypeDef.typeName);
            }
        }

        public boolean isLoadedFromStore(String typeName) {
            return loadedFromStore.contains(typeName);
        }

        public TypesDef getTypesDef() {
            return TypesUtil.getTypesDef(enums.build(), structs.build(), traits.build(), classTypes.build());
        }
    }

    /**
     * Checks whether the specified type is cached in memory and does *not*
     * access the type store.  Used for testing.
     *
     * @param typeName
     * @return
     */
    public boolean isCachedInMemory(String typeName) throws AtlasException {
        return super.has(typeName);
    }

    /**
     * Check the type store for the requested type. 
     * If found in the type store, the type and any required super and attribute types
     * are loaded from the type store, and added to the cache.
     */
    @Override
    public IDataType onTypeFault(String typeName) throws AtlasException {

        // Type is not cached - check the type store.
        // Any super and attribute types needed by the requested type
        // which are not cached will also be loaded from the store.
        Context context = new Context();
        TypesDef typesDef = getTypeFromStore(typeName, context);
        if (typesDef.isEmpty()) {
            // Type not found in the type store.
            return null;
        }

        // Add all types that were loaded from the store to the cache.
        TransientTypeSystem transientTypeSystem = typeSystem.createTransientTypeSystem(context.getTypesDef(), false);
        Map<String, IDataType> typesAdded = transientTypeSystem.getTypesAdded();
        putAll(typesAdded.values());
        return typesAdded.get(typeName);
    }

    private void getTypeFromCacheOrStore(String typeName, Context context)
            throws AtlasException {

        if (coreTypes.contains(typeName) || super.has(typeName)) {
            return;
        }

        if (context.isLoadedFromStore(typeName)) {
            return;
        }

        // Type not cached and hasn't been loaded during this operation, so check the store.
        TypesDef typesDef = getTypeFromStore(typeName, context);
        if (typesDef.isEmpty()) {
            // Attribute type not found in cache or store.
            throw new AtlasException(typeName + " not found in type store");
        }
    }

    private TypesDef getTypeFromStore(String typeName, Context context)
            throws AtlasException {

        TypesDef typesDef = typeStore.restoreType(typeName);
        if (!typesDef.isEmpty()) {
            // Type found in store, add it to lists.
            context.addTypesDefToLists(typesDef);

            // Check the attribute and super types that are
            // used by the requested type, and restore them
            // as needed.
            checkAttributeAndSuperTypes(typesDef, context);
        }
        return typesDef;
    }

    private void checkAttributeAndSuperTypes(TypesDef typesDef, Context context)
            throws AtlasException {

        // Check the cache and store for attribute types and super types.
        for (HierarchicalTypeDefinition<ClassType> classTypeDef : typesDef.classTypesAsJavaList()) {
            checkAttributeTypes(classTypeDef.attributeDefinitions, context);
            for (String superTypeName : classTypeDef.superTypes) {
                getTypeFromCacheOrStore(superTypeName, context);
            }
        }
        for (HierarchicalTypeDefinition<TraitType> traitTypeDef : typesDef.traitTypesAsJavaList()) {
            checkAttributeTypes(traitTypeDef.attributeDefinitions, context);
            for (String superTypeName : traitTypeDef.superTypes) {
                getTypeFromCacheOrStore(superTypeName, context);
            }
        }
        for (StructTypeDefinition structTypeDef : typesDef.structTypesAsJavaList()) {
            checkAttributeTypes(structTypeDef.attributeDefinitions, context);
        }
    }

    private void checkAttributeTypes(AttributeDefinition[] attributeDefinitions,
        Context context) throws AtlasException {

        for (AttributeDefinition attrDef : attributeDefinitions) {
            checkAttributeType(attrDef, context);
        }
    }

    private void checkAttributeType(AttributeDefinition attrDef, Context context) throws AtlasException {

        List<String> typeNamesToLookup = new ArrayList<>(2);

        // Get the attribute type(s).
        String elementTypeName = TypeUtils.parseAsArrayType(attrDef.dataTypeName);
        if (elementTypeName != null) {
            // Array attribute, lookup the element type.
            typeNamesToLookup.add(elementTypeName);
        }
        else {
            String[] mapTypeNames = TypeUtils.parseAsMapType(attrDef.dataTypeName);
            if (mapTypeNames != null) {
                // Map attribute, lookup the key and value types.
                typeNamesToLookup.addAll(Arrays.asList(mapTypeNames));
            }
            else {
                // Not an array or map, lookup the attribute type.
                typeNamesToLookup.add(attrDef.dataTypeName);
            }
        }

        for (String typeName : typeNamesToLookup) {
            getTypeFromCacheOrStore(typeName, context);
        }
    }
}