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

import org.apache.atlas.AtlasException;
import org.apache.atlas.annotation.ConditionalOnAtlasProperty;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.DataTypes.TypeCategory;
import org.apache.atlas.typesystem.types.EnumType;
import org.apache.atlas.typesystem.types.HierarchicalType;
import org.apache.atlas.typesystem.types.IDataType;
import org.apache.atlas.typesystem.types.StructType;
import org.apache.atlas.typesystem.types.TraitType;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Caches the types in-memory within the same process space.
 */
@SuppressWarnings("rawtypes")
@Singleton
@Component
@ConditionalOnAtlasProperty(property = "atlas.TypeCache.impl", isDefault = true)
public class DefaultTypeCache implements TypeCache {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultTypeCache.class);

    private Map<String, IDataType> types_ = new ConcurrentHashMap<>();
    private static final List<TypeCategory> validTypeFilterCategories =
            Arrays.asList(TypeCategory.CLASS, TypeCategory.TRAIT, TypeCategory.ENUM, TypeCategory.STRUCT);
    private static final List<TypeCategory> validSupertypeFilterCategories =
            Arrays.asList(TypeCategory.CLASS, TypeCategory.TRAIT);

    /*
     * (non-Javadoc)
     * @see
     * org.apache.atlas.typesystem.types.cache.TypeCache#has(java.lang
     * .String)
     */
    @Override
    public boolean has(String typeName) throws AtlasException {

        return types_.containsKey(typeName);
    }

    /* (non-Javadoc)
     * @see org.apache.atlas.typesystem.types.cache.TypeCache#has(org.
     * apache.atlas.typesystem.types.DataTypes.TypeCategory, java.lang.String)
     */
    @Override
    public boolean has(TypeCategory typeCategory, String typeName)
            throws AtlasException {

        assertValidTypeCategory(typeCategory);
        return has(typeName);
    }

    private void assertValidTypeCategory(String typeCategory) {
        assertValidTypeCategory(TypeCategory.valueOf(typeCategory));
    }

    private void assertValidTypeCategory(TypeCategory typeCategory) {
        // there might no need of 'typeCategory' in this implementation for
        // certain API, but for a distributed cache, it might help for the
        // implementers to partition the types per their category
        // while persisting so that look can be efficient

        if (typeCategory == null) {
            throw new IllegalArgumentException("Category of the types to be filtered is null.");
        }

        if (!validTypeFilterCategories.contains(typeCategory)) {
            throw new IllegalArgumentException("Category of the types should be one of " +
                    StringUtils.join(validTypeFilterCategories, ", "));
        }
    }

    /*
     * (non-Javadoc)
     * @see
     * org.apache.atlas.typesystem.types.cache.TypeCache#get(java.lang
     * .String)
     */
    @Override
    public IDataType get(String typeName) throws AtlasException {

        return types_.get(typeName);
    }

    /* (non-Javadoc)
     * @see org.apache.atlas.typesystem.types.cache.TypeCache#get(org.apache.
     * atlas.typesystem.types.DataTypes.TypeCategory, java.lang.String)
     */
    @Override
    public IDataType get(TypeCategory typeCategory, String typeName) throws AtlasException {

        assertValidTypeCategory(typeCategory);
        return get(typeName);
    }

    /**
     * Return the list of type names in the type system which match the specified filter.
     *
     * @return list of type names
     * @param filterMap - Map of filter for type names. Valid keys are CATEGORY, SUPERTYPE, NOT_SUPERTYPE
     * For example, CATEGORY = TRAIT && SUPERTYPE contains 'X' && SUPERTYPE !contains 'Y'
     */
    @Override
    public Collection<String> getTypeNames(Map<TYPE_FILTER, String> filterMap) throws AtlasException {
        assertFilter(filterMap);

        List<String> typeNames = new ArrayList<>();
        for (IDataType type : types_.values()) {
            if (shouldIncludeType(type, filterMap)) {
                typeNames.add(type.getName());
            }
        }
        return typeNames;
    }

    private boolean shouldIncludeType(IDataType type, Map<TYPE_FILTER, String> filterMap) {
        if (filterMap == null) {
            return true;
        }

        for (Entry<TYPE_FILTER, String> filterEntry : filterMap.entrySet()) {
            switch (filterEntry.getKey()) {
            case CATEGORY:
                if (!filterEntry.getValue().equals(type.getTypeCategory().name())) {
                    return false;
                }
                break;

            case SUPERTYPE:
                if (!validSupertypeFilterCategories.contains(type.getTypeCategory()) ||
                        !((HierarchicalType) type).getAllSuperTypeNames().contains(filterEntry.getValue())) {
                    return false;
                }
                break;

            case NOT_SUPERTYPE:
                if (!validSupertypeFilterCategories.contains(type.getTypeCategory()) ||
                        type.getName().equals(filterEntry.getValue()) ||
                        ((HierarchicalType) type).getAllSuperTypeNames().contains(filterEntry.getValue())) {
                    return false;
                }
                break;
            }
        }
        return true;
    }


    private void assertFilter(Map<TYPE_FILTER, String> filterMap) throws AtlasException {
        if (filterMap == null) {
            return;
        }

        for (Entry<TYPE_FILTER, String> filterEntry : filterMap.entrySet()) {
            switch (filterEntry.getKey()) {
            case CATEGORY:
                assertValidTypeCategory(filterEntry.getValue());
                break;

            case SUPERTYPE:
            case NOT_SUPERTYPE:
                if (!has(filterEntry.getValue())) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("{}: supertype does not exist", filterEntry.getValue());
                    }
                }
                break;

            default:
                throw new IllegalStateException("Unhandled filter " + filterEntry.getKey());
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see
     * org.apache.atlas.typesystem.types.cache.TypeCache#getAllNames()
     */
    @Override
    public Collection<String> getAllTypeNames() throws AtlasException {

        return types_.keySet();
    }

    /*
     * (non-Javadoc)
     * @see
     * org.apache.atlas.typesystem.types.cache.TypeCache#put(org.apache
     * .atlas.typesystem.types.IDataType)
     */
    @Override
    public void put(IDataType type) throws AtlasException {

        assertValidType(type);
        types_.put(type.getName(), type);
    }

    private void assertValidType(IDataType type) throws
        AtlasException {

        if (type == null) {
            throw new AtlasException("type is null.");
        }

        boolean validTypeCategory = (type instanceof ClassType) ||
            (type instanceof TraitType) ||
            (type instanceof EnumType) ||
            (type instanceof StructType);

        if (!validTypeCategory) {
            throw new AtlasException("Category of the types should be one of ClassType | "
                + "TraitType | EnumType | StructType.");
        }
    }

    /*
     * (non-Javadoc)
     * @see
     * org.apache.atlas.typesystem.types.cache.TypeCache#putAll(java
     * .util.Collection)
     */
    @Override
    public void putAll(Collection<IDataType> types) throws AtlasException {

        for (IDataType type : types) {
            assertValidType(type);
            types_.put(type.getName(), type);
        }
    }

    /*
     * (non-Javadoc)
     * @see
     * org.apache.atlas.typesystem.types.cache.TypeCache#remove(java
     * .lang.String)
     */
    @Override
    public void remove(String typeName) throws AtlasException {

        types_.remove(typeName);
    }

    /* (non-Javadoc)
     * @see org.apache.atlas.typesystem.types.cache.TypeCache#remove(org.
     * apache.atlas.typesystem.types.DataTypes.TypeCategory, java.lang.String)
     */
    @Override
    public void remove(TypeCategory typeCategory, String typeName)
            throws AtlasException {

        assertValidTypeCategory(typeCategory);
        remove(typeName);
    }

    /*
     * (non-Javadoc)
     * @see org.apache.atlas.typesystem.types.cache.TypeCache#clear()
     */
    @Override
    public void clear() {

        types_.clear();
    }

    @Override
    public IDataType onTypeFault(String typeName) throws AtlasException {

        return null;
    }
}
