/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.type;

import org.apache.atlas.ApplicationProperties;
import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.AtlasException;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.model.instance.AtlasStruct;
import org.apache.atlas.model.typedef.AtlasStructDef;
import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef;
import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.Cardinality;
import org.apache.atlas.model.typedef.AtlasStructDef.AtlasConstraintDef;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.apache.atlas.model.TypeCategory.OBJECT_ID_TYPE;
import static org.apache.atlas.model.typedef.AtlasBaseTypeDef.ATLAS_TYPE_STRING;
import static org.apache.atlas.model.typedef.AtlasStructDef.AtlasConstraintDef.CONSTRAINT_PARAM_ATTRIBUTE;
import static org.apache.atlas.model.typedef.AtlasStructDef.AtlasConstraintDef.CONSTRAINT_TYPE_INVERSE_REF;
import static org.apache.atlas.model.typedef.AtlasStructDef.AtlasConstraintDef.CONSTRAINT_TYPE_OWNED_REF;

/**
 * class that implements behaviour of a struct-type.
 */
public class AtlasStructType extends AtlasType {
    private static final Logger LOG = LoggerFactory.getLogger(AtlasStructType.class);

    public static final String UNIQUE_ATTRIBUTE_SHADE_PROPERTY_PREFIX = "__u_";

    protected static boolean skipCheckForParentChildAttributeName;

    private static final Configuration configuration;
    private static final String        SKIP_CHECK_FOR_PARENT_CHILD_ATTRIBUTE_NAME = "atlas.skip.check.for.parent.child.attribute.name";

    private final        AtlasStructDef structDef;
    protected Map<String, AtlasAttribute> allAttributes  = Collections.emptyMap();
    protected Map<String, AtlasAttribute> uniqAttributes = Collections.emptyMap();

    public AtlasStructType(AtlasStructDef structDef) {
        super(structDef);

        this.structDef = structDef;
    }

    public AtlasStructType(AtlasStructDef structDef, AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        super(structDef);

        this.structDef = structDef;

        this.resolveReferences(typeRegistry);
    }

    static AtlasEntityType getReferencedEntityType(AtlasType type) {
        if (type instanceof AtlasArrayType) {
            type = ((AtlasArrayType) type).getElementType();
        }

        if (type instanceof AtlasMapType) {
            type = ((AtlasMapType) type).getValueType();
        }

        return type instanceof AtlasEntityType ? (AtlasEntityType) type : null;
    }

    public AtlasStructDef getStructDef() {
        return structDef;
    }

    public AtlasType getAttributeType(String attributeName) {
        AtlasAttribute attribute = getAttribute(attributeName);

        return attribute != null ? attribute.getAttributeType() : null;
    }

    public AtlasAttributeDef getAttributeDef(String attributeName) {
        AtlasAttribute attribute = getAttribute(attributeName);

        return attribute != null ? attribute.getAttributeDef() : null;
    }

    @Override
    public AtlasStruct createDefaultValue() {
        AtlasStruct ret = new AtlasStruct(structDef.getName());

        populateDefaultValues(ret);

        return ret;
    }

    @Override
    public Object createDefaultValue(Object defaultValue) {
        AtlasStruct ret = new AtlasStruct(structDef.getName());

        populateDefaultValues(ret);

        return ret;
    }

    @Override
    public boolean isValidValue(Object obj) {
        if (obj != null) {
            if (obj instanceof AtlasStruct) {
                AtlasStruct structObj = (AtlasStruct) obj;

                for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                    if (!isAssignableValue(structObj.getAttribute(attributeDef.getName()), attributeDef)) {
                        return false;
                    }
                }
            } else if (obj instanceof Map) {
                Map<?, ?> map = AtlasTypeUtil.toStructAttributes((Map<?, ?>) obj);

                for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                    if (!isAssignableValue(map.get(attributeDef.getName()), attributeDef)) {
                        return false; // no value for non-optinal attribute
                    }
                }
            } else {
                return false;
            }
        }

        return true;
    }

    @Override
    public boolean areEqualValues(Object val1, Object val2, Map<String, String> guidAssignments) {
        boolean ret = true;

        if (val1 == null) {
            ret = val2 == null;
        } else if (val2 == null) {
            ret = false;
        } else {
            AtlasStruct structVal1 = getStructFromValue(val1);

            if (structVal1 == null) {
                ret = false;
            } else {
                AtlasStruct structVal2 = getStructFromValue(val2);

                if (structVal2 == null) {
                    ret = false;
                } else if (!StringUtils.equalsIgnoreCase(structVal1.getTypeName(), structVal2.getTypeName())) {
                    ret = false;
                } else {
                    for (AtlasAttribute attribute : getAllAttributes().values()) {
                        Object attrValue1 = structVal1.getAttribute(attribute.getName());
                        Object attrValue2 = structVal2.getAttribute(attribute.getName());

                        if (!attribute.getAttributeType().areEqualValues(attrValue1, attrValue2, guidAssignments)) {
                            ret = false;

                            break;
                        }
                    }
                }
            }
        }

        return ret;
    }

    @Override
    public Object getNormalizedValue(Object obj) {
        Object ret = null;

        if (obj != null) {
            if (isValidValue(obj)) {
                if (obj instanceof AtlasStruct) {
                    normalizeAttributeValues((AtlasStruct) obj);

                    ret = obj;
                } else if (obj instanceof Map) {
                    normalizeAttributeValues((Map) obj);

                    ret = obj;
                }
            }
        }

        return ret;
    }

    @Override
    public boolean validateValue(Object obj, String objName, List<String> messages) {
        boolean ret = true;

        if (obj != null) {
            if (obj instanceof AtlasStruct) {
                AtlasStruct structObj = (AtlasStruct) obj;

                for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                    String         attrName  = attributeDef.getName();
                    AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

                    if (attribute != null) {
                        AtlasType dataType  = attribute.getAttributeType();
                        Object    value     = structObj.getAttribute(attrName);
                        String    fieldName = objName + "." + attrName;

                        if (value != null) {
                            ret = dataType.validateValue(value, fieldName, messages) && ret;
                        } else if (!attributeDef.getIsOptional()) {
                            // if required attribute is null, check if attribute value specified in relationship
                            if (structObj instanceof AtlasEntity) {
                                AtlasEntity entityObj = (AtlasEntity) structObj;

                                if (entityObj.getRelationshipAttribute(attrName) == null) {
                                    if (entityObj.getIsIncomplete() != null && entityObj.getIsIncomplete()) { // In case of import shell entities, avoid checking of mandatory attributes
                                        continue;
                                    }

                                    ret = false;

                                    messages.add(fieldName + ": mandatory attribute value missing in type " + getTypeName());
                                }
                            } else {
                                ret = false;

                                messages.add(fieldName + ": mandatory attribute value missing in type " + getTypeName());
                            }
                        }
                    }
                }
            } else if (obj instanceof Map) {
                Map<?, ?> attributes             = AtlasTypeUtil.toStructAttributes((Map<?, ?>) obj);
                Map<?, ?> relationshipAttributes = AtlasTypeUtil.toRelationshipAttributes((Map<?, ?>) obj);

                for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                    String         attrName  = attributeDef.getName();
                    AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

                    if (attribute != null) {
                        AtlasType dataType  = attribute.getAttributeType();
                        Object    value     = attributes.get(attrName);
                        String    fieldName = objName + "." + attrName;

                        if (value != null) {
                            ret = dataType.validateValue(value, fieldName, messages) && ret;
                        } else if (!attributeDef.getIsOptional()) {
                            // if required attribute is null, check if attribute value specified in relationship
                            if (MapUtils.isEmpty(relationshipAttributes) || !relationshipAttributes.containsKey(attrName)) {
                                ret = false;

                                messages.add(fieldName + ": mandatory attribute value missing in type " + getTypeName());
                            }
                        }
                    }
                }
            } else {
                ret = false;
                messages.add(objName + "=" + obj + ": invalid value for type " + getTypeName());
            }
        }

        return ret;
    }

    @Override
    public boolean isValidValueForUpdate(Object obj) {
        if (obj != null) {
            Map<String, Object> attributes;

            if (obj instanceof AtlasStruct) {
                AtlasStruct structObj = (AtlasStruct) obj;

                attributes = structObj.getAttributes();
            } else if (obj instanceof Map) {
                attributes = AtlasTypeUtil.toStructAttributes((Map<?, ?>) obj);
            } else {
                return false;
            }

            if (MapUtils.isNotEmpty(attributes)) {
                for (Map.Entry<String, Object> e : attributes.entrySet()) {
                    String            attrName  = e.getKey();
                    Object            attrValue = e.getValue();
                    AtlasAttributeDef attrDef   = structDef.getAttribute(attrName);

                    if (attrValue == null || attrDef == null) {
                        continue;
                    }

                    if (!isAssignableValueForUpdate(attrValue, attrDef)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    @Override
    public Object getNormalizedValueForUpdate(Object obj) {
        Object ret = null;

        if (obj != null) {
            if (isValidValueForUpdate(obj)) {
                if (obj instanceof AtlasStruct) {
                    normalizeAttributeValuesForUpdate((AtlasStruct) obj);

                    ret = obj;
                } else if (obj instanceof Map) {
                    normalizeAttributeValuesForUpdate((Map) obj);

                    ret = obj;
                }
            }
        }

        return ret;
    }

    @Override
    public boolean validateValueForUpdate(Object obj, String objName, List<String> messages) {
        boolean             ret        = true;
        Map<String, Object> attributes = null;

        if (obj != null) {
            if (obj instanceof AtlasStruct) {
                AtlasStruct structObj = (AtlasStruct) obj;

                attributes = structObj.getAttributes();
            } else if (obj instanceof Map) {
                attributes = AtlasTypeUtil.toStructAttributes((Map<?, ?>) obj);
            } else {
                ret = false;

                messages.add(objName + "=" + obj + ": invalid value for type " + getTypeName());
            }

            if (MapUtils.isNotEmpty(attributes)) {
                for (Map.Entry<String, Object> e : attributes.entrySet()) {
                    String         attrName  = e.getKey();
                    Object         attrValue = e.getValue();
                    AtlasAttribute attribute = allAttributes.get(attrName);

                    if (attrValue == null) {
                        continue;
                    }

                    if (attribute != null) {
                        AtlasType dataType  = attribute.getAttributeType();
                        String    fieldName = objName + "." + attrName;

                        ret = dataType.validateValueForUpdate(attrValue, fieldName, messages) && ret;
                    }
                }
            }
        }

        return ret;
    }

    @Override
    void resolveReferences(AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        Map<String, AtlasAttribute> a = new HashMap<>();

        for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
            AtlasType      attrType  = typeRegistry.getType(attributeDef.getTypeName());
            AtlasAttribute attribute = new AtlasAttribute(this, attributeDef, attrType);

            Cardinality cardinality = attributeDef.getCardinality();

            if (cardinality == Cardinality.LIST || cardinality == Cardinality.SET) {
                if (!(attrType instanceof AtlasArrayType)) {
                    throw new AtlasBaseException(AtlasErrorCode.INVALID_ATTRIBUTE_TYPE_FOR_CARDINALITY, getTypeName(), attributeDef.getName());
                }

                AtlasArrayType arrayType = (AtlasArrayType) attrType;

                arrayType.setMinCount(attributeDef.getValuesMinCount());
                arrayType.setMaxCount(attributeDef.getValuesMaxCount());
                arrayType.setCardinality(cardinality);
            }

            //check if attribute type is not classification
            if (attrType instanceof AtlasArrayType) {
                attrType = ((AtlasArrayType) attrType).getElementType();
            } else if (attrType instanceof AtlasMapType) {
                attrType = ((AtlasMapType) attrType).getValueType();
            }

            if (attrType instanceof AtlasClassificationType) {
                throw new AtlasBaseException(AtlasErrorCode.ATTRIBUTE_TYPE_INVALID, getTypeName(), attributeDef.getName());
            }

            if (attrType instanceof AtlasBusinessMetadataType) {
                throw new AtlasBaseException(AtlasErrorCode.ATTRIBUTE_TYPE_INVALID, getTypeName(), attributeDef.getName());
            }

            a.put(attributeDef.getName(), attribute);
        }

        resolveConstraints(typeRegistry);

        this.allAttributes  = Collections.unmodifiableMap(a);
        this.uniqAttributes = getUniqueAttributes(this.allAttributes);
    }

    @Override
    void resolveReferencesPhase2(AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        super.resolveReferencesPhase2(typeRegistry);
        for (AtlasAttribute attribute : allAttributes.values()) {
            if (attribute.getInverseRefAttributeName() == null) {
                continue;
            }

            // Set the inverse reference attribute.
            AtlasType       referencedType       = typeRegistry.getType(attribute.getAttributeDef().getTypeName());
            AtlasEntityType referencedEntityType = getReferencedEntityType(referencedType);
            AtlasAttribute  inverseReference     = referencedEntityType.getAttribute(attribute.getInverseRefAttributeName());

            attribute.setInverseRefAttribute(inverseReference);
        }
    }

    public Map<String, AtlasAttribute> getAllAttributes() {
        return allAttributes;
    }

    public Map<String, AtlasAttribute> getUniqAttributes() {
        return uniqAttributes;
    }

    public AtlasAttribute getAttribute(String attributeName) {
        AtlasAttribute ret = allAttributes.get(attributeName);

        if (ret == null) {
            ret = getSystemAttribute(attributeName);
        }

        if (ret == null) {
            ret = getBusinesAAttribute(attributeName);
        }

        return ret;
    }

    public AtlasAttribute getSystemAttribute(String attributeName) {
        return null;
    }

    public AtlasAttribute getBusinesAAttribute(String attributeName) {
        return null;
    }

    public void normalizeAttributeValues(AtlasStruct obj) {
        if (obj != null) {
            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                String attributeName = attributeDef.getName();

                if (obj.hasAttribute(attributeName)) {
                    Object attributeValue = getNormalizedValue(obj.getAttribute(attributeName), attributeDef);

                    obj.setAttribute(attributeName, attributeValue);
                } else if (!attributeDef.getIsOptional()) {
                    obj.setAttribute(attributeName, createDefaultValue(attributeDef));
                }
            }
        }
    }

    public void normalizeAttributeValuesForUpdate(AtlasStruct obj) {
        if (obj != null) {
            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                String attributeName = attributeDef.getName();

                if (obj.hasAttribute(attributeName)) {
                    Object attributeValue = getNormalizedValueForUpdate(obj.getAttribute(attributeName), attributeDef);

                    obj.setAttribute(attributeName, attributeValue);
                }
            }
        }
    }

    public void normalizeAttributeValues(Map<String, Object> obj) {
        if (obj != null) {
            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                String attributeName = attributeDef.getName();

                if (obj.containsKey(attributeName)) {
                    Object attributeValue = getNormalizedValue(obj.get(attributeName), attributeDef);

                    obj.put(attributeName, attributeValue);
                } else if (!attributeDef.getIsOptional()) {
                    obj.put(attributeName, createDefaultValue(attributeDef));
                }
            }
        }
    }

    public void normalizeAttributeValuesForUpdate(Map<String, Object> obj) {
        if (obj != null) {
            for (AtlasAttributeDef attrDef : structDef.getAttributeDefs()) {
                String attrName  = attrDef.getName();
                Object attrValue = obj.get(attrName);

                if (obj.containsKey(attrName)) {
                    attrValue = getNormalizedValueForUpdate(attrValue, attrDef);
                    obj.put(attrName, attrValue);
                }
            }
        }
    }

    public void populateDefaultValues(AtlasStruct obj) {
        if (obj != null) {
            Map<String, Object> attributes = obj.getAttributes();

            if (attributes == null) {
                attributes = new HashMap<>();
            }

            for (AtlasAttributeDef attributeDef : structDef.getAttributeDefs()) {
                if (!attributeDef.getIsOptional()) {
                    attributes.put(attributeDef.getName(), createDefaultValue(attributeDef));
                }
            }

            obj.setAttributes(attributes);
        }
    }

    public String getVertexPropertyName(String attrName) throws AtlasBaseException {
        AtlasAttribute attribute = getAttribute(attrName);

        if (attribute != null) {
            return attribute.getVertexPropertyName();
        }

        throw new AtlasBaseException(AtlasErrorCode.UNKNOWN_ATTRIBUTE, attrName, structDef.getName());
    }

    public String getQualifiedAttributePropertyKey(String attrName) throws AtlasBaseException {
        if (allAttributes.containsKey(attrName)) {
            return allAttributes.get(attrName).getVertexPropertyName();
        }

        throw new AtlasBaseException(AtlasErrorCode.UNKNOWN_ATTRIBUTE, attrName, structDef.getName());
    }

    protected Map<String, AtlasAttribute> getUniqueAttributes(Map<String, AtlasAttribute> attributes) {
        Map<String, AtlasAttribute> ret = new HashMap<>();

        if (MapUtils.isNotEmpty(attributes)) {
            for (AtlasAttribute attribute : attributes.values()) {
                if (attribute.getAttributeDef().getIsUnique()) {
                    ret.put(attribute.getName(), attribute);
                }
            }
        }

        return Collections.unmodifiableMap(ret);
    }

    private void resolveConstraints(AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        for (AtlasAttributeDef attributeDef : getStructDef().getAttributeDefs()) {
            if (CollectionUtils.isEmpty(attributeDef.getConstraints())) {
                continue;
            }

            for (AtlasConstraintDef constraint : attributeDef.getConstraints()) {
                if (constraint.isConstraintType(CONSTRAINT_TYPE_OWNED_REF)) {
                    AtlasEntityType attrType = getReferencedEntityType(typeRegistry.getType(attributeDef.getTypeName()));

                    if (attrType == null) {
                        throw new AtlasBaseException(AtlasErrorCode.CONSTRAINT_OWNED_REF_ATTRIBUTE_INVALID_TYPE, getTypeName(), attributeDef.getName(), CONSTRAINT_TYPE_OWNED_REF, attributeDef.getTypeName());
                    }
                } else if (constraint.isConstraintType(CONSTRAINT_TYPE_INVERSE_REF)) {
                    AtlasEntityType attrType = getReferencedEntityType(typeRegistry.getType(attributeDef.getTypeName()));

                    if (attrType == null) {
                        throw new AtlasBaseException(AtlasErrorCode.CONSTRAINT_INVERSE_REF_ATTRIBUTE_INVALID_TYPE, getTypeName(), attributeDef.getName(), CONSTRAINT_TYPE_INVERSE_REF, attributeDef.getTypeName());
                    }

                    String inverseRefAttrName = AtlasTypeUtil.getStringValue(constraint.getParams(), CONSTRAINT_PARAM_ATTRIBUTE);

                    if (StringUtils.isBlank(inverseRefAttrName)) {
                        throw new AtlasBaseException(AtlasErrorCode.CONSTRAINT_MISSING_PARAMS, getTypeName(), attributeDef.getName(), CONSTRAINT_PARAM_ATTRIBUTE, CONSTRAINT_TYPE_INVERSE_REF, String.valueOf(constraint.getParams()));
                    }

                    AtlasAttributeDef inverseRefAttrDef = attrType.getStructDef().getAttribute(inverseRefAttrName);

                    if (inverseRefAttrDef == null) {
                        throw new AtlasBaseException(AtlasErrorCode.CONSTRAINT_INVERSE_REF_INVERSE_ATTRIBUTE_NON_EXISTING, getTypeName(), attributeDef.getName(), CONSTRAINT_TYPE_INVERSE_REF, attrType.getTypeName(), inverseRefAttrName);
                    }

                    AtlasEntityType inverseRefAttrType = getReferencedEntityType(typeRegistry.getType(inverseRefAttrDef.getTypeName()));

                    if (inverseRefAttrType == null) {
                        throw new AtlasBaseException(AtlasErrorCode.CONSTRAINT_INVERSE_REF_INVERSE_ATTRIBUTE_INVALID_TYPE, getTypeName(), attributeDef.getName(), CONSTRAINT_TYPE_INVERSE_REF, attrType.getTypeName(), inverseRefAttrName);
                    }
                }
            }
        }
    }

    private Object createDefaultValue(AtlasAttributeDef attributeDef) {
        Object ret = null;

        if (attributeDef != null) {
            AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

            if (attribute != null) {
                AtlasType dataType = attribute.getAttributeType();

                ret = dataType.createDefaultValue(attributeDef.getDefaultValue());
            }
        }

        return ret;
    }

    private boolean isAssignableValue(Object value, AtlasAttributeDef attributeDef) {
        boolean ret = true;

        if (value != null) {
            AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

            if (attribute != null) {
                AtlasType attrType = attribute.getAttributeType();

                if (!attrType.isValidValue(value)) {
                    ret = false; // invalid value
                }
            }
        } else if (!attributeDef.getIsOptional()) {
            ret = false; // mandatory attribute not present
        }

        return ret;
    }

    private boolean isAssignableValueForUpdate(Object value, AtlasAttributeDef attributeDef) {
        boolean ret = true;

        if (value != null) {
            AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

            if (attribute != null) {
                AtlasType attrType = attribute.getAttributeType();

                if (!attrType.isValidValueForUpdate(value)) {
                    ret = false; // invalid value
                }
            }
        }

        return ret;
    }

    private Object getNormalizedValue(Object value, AtlasAttributeDef attributeDef) {
        AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

        if (attribute != null) {
            AtlasType attrType = attribute.getAttributeType();

            if (value == null) {
                if (!attributeDef.getIsOptional()) {
                    return attrType.createDefaultValue();
                }
            } else {
                return attrType.getNormalizedValue(value);
            }
        }

        return null;
    }

    private Object getNormalizedValueForUpdate(Object value, AtlasAttributeDef attributeDef) {
        AtlasAttribute attribute = allAttributes.get(attributeDef.getName());

        if (attribute != null) {
            AtlasType attrType = attribute.getAttributeType();

            if (value != null) {
                return attrType.getNormalizedValueForUpdate(value);
            }
        }

        return null;
    }

    private AtlasStruct getStructFromValue(Object val) {
        final AtlasStruct ret;

        if (val instanceof AtlasStruct) {
            ret = (AtlasStruct) val;
        } else if (val instanceof Map) {
            ret = new AtlasStruct((Map<?, ?>) val);
        } else if (val instanceof String) {
            Map<?, ?> map = AtlasType.fromJson(val.toString(), Map.class);

            if (map == null) {
                ret = null;
            } else {
                ret = new AtlasStruct(map);
            }
        } else {
            ret = null;
        }

        return ret;
    }

    public static class AtlasAttribute {
        public static final Object VERTEX_PROPERTY_PREFIX_STRING_INDEX_TYPE = "__s_";

        private static final char BRACE_OPEN_CHAR   = '(';
        private static final char BRACE_CLOSE_CHAR  = ')';
        private static final char DOUBLE_QUOTE_CHAR = '"';
        private static final char SPACE_CHAR        = ' ';

        // Keys copied from org.janusgraph.graphdb.types.system.SystemTypeManager.RESERVED_CHARS
        // JanusGraph checks that these chars are not part of any keys hence encoding
        // also including Titan reserved characters to support migrated property keys
        private static final String[][] RESERVED_CHAR_ENCODE_MAP = new String[][] {
                new String[] {"{", "_o"},
                new String[] {"}", "_c"},
                new String[] {"\"", "_q"},
                new String[] {"$", "_d"}, //titan reserved character
                new String[] {"%", "_p"}, //titan reserved characters
        };

        private final AtlasStructType   definedInType;
        private final AtlasType         attributeType;
        private final AtlasAttributeDef attributeDef;
        private final String            qualifiedName;
        private final String            vertexPropertyName;
        private final String            vertexUniquePropertyName;
        private final boolean           isOwnedRef;
        private final boolean           isObjectRef;
        private final String            inverseRefAttributeName;

        private AtlasAttribute                 inverseRefAttribute;
        private String                         relationshipName;
        private String                         relationshipEdgeLabel;
        private AtlasRelationshipEdgeDirection relationshipEdgeDirection;
        private boolean                        isLegacyAttribute;
        private String                         indexFieldName;
        private boolean                        isDynAttribute;
        private boolean                        isDynAttributeEvalTrigger;

        public AtlasAttribute(AtlasStructType definedInType, AtlasAttributeDef attrDef, AtlasType attributeType, String relationshipName, String relationshipLabel) {
            this.definedInType            = definedInType;
            this.attributeDef             = attrDef;
            this.attributeType            = attributeType.getTypeForAttribute();
            this.qualifiedName            = getQualifiedAttributeName(definedInType.getStructDef(), attributeDef.getName());
            this.vertexPropertyName       = generateVertexPropertyName(definedInType.getStructDef(), attributeDef, qualifiedName);
            this.vertexUniquePropertyName = attrDef.getIsUnique() ? encodePropertyKey(getQualifiedAttributeName(definedInType.getStructDef(), UNIQUE_ATTRIBUTE_SHADE_PROPERTY_PREFIX + attributeDef.getName())) : null;
            this.relationshipName         = relationshipName;
            this.relationshipEdgeLabel    = getRelationshipEdgeLabel(relationshipLabel);

            boolean isOwnedRef          = false;
            String  inverseRefAttribute = null;

            LOG.debug("Attribute {} will use the vertext property name {}.", qualifiedName, vertexPropertyName);

            if (CollectionUtils.isNotEmpty(attributeDef.getConstraints())) {
                for (AtlasConstraintDef constraint : attributeDef.getConstraints()) {
                    if (constraint.isConstraintType(CONSTRAINT_TYPE_OWNED_REF)) {
                        isOwnedRef = true;
                    }

                    if (constraint.isConstraintType(CONSTRAINT_TYPE_INVERSE_REF)) {
                        Object val = constraint.getParam(CONSTRAINT_PARAM_ATTRIBUTE);

                        if (val != null) {
                            inverseRefAttribute = val.toString();
                        }
                    }
                }
            }

            this.isOwnedRef                = isOwnedRef;
            this.inverseRefAttributeName   = inverseRefAttribute;
            this.relationshipEdgeDirection = AtlasRelationshipEdgeDirection.OUT;

            switch (this.attributeType.getTypeCategory()) {
                case OBJECT_ID_TYPE:
                    isObjectRef = true;
                    break;

                case MAP:
                    AtlasMapType mapType = (AtlasMapType) this.attributeType;

                    isObjectRef = mapType.getValueType().getTypeCategory() == OBJECT_ID_TYPE;
                    break;

                case ARRAY:
                    AtlasArrayType arrayType = (AtlasArrayType) this.attributeType;

                    isObjectRef = arrayType.getElementType().getTypeCategory() == OBJECT_ID_TYPE;
                    break;

                default:
                    isObjectRef = false;
                    break;
            }
        }

        public AtlasAttribute(AtlasStructType definedInType, AtlasAttributeDef attrDef, AtlasType attributeType) {
            this(definedInType, attrDef, attributeType, null, null);
        }

        public AtlasAttribute(AtlasAttribute other) {
            this.definedInType             = other.definedInType;
            this.attributeType             = other.attributeType;
            this.attributeDef              = other.attributeDef;
            this.qualifiedName             = other.qualifiedName;
            this.vertexPropertyName        = other.vertexPropertyName;
            this.vertexUniquePropertyName  = other.vertexUniquePropertyName;
            this.isOwnedRef                = other.isOwnedRef;
            this.isObjectRef               = other.isObjectRef;
            this.inverseRefAttributeName   = other.inverseRefAttributeName;
            this.inverseRefAttribute       = other.inverseRefAttribute;
            this.relationshipName          = other.relationshipName;
            this.relationshipEdgeLabel     = other.relationshipEdgeLabel;
            this.relationshipEdgeDirection = other.relationshipEdgeDirection;
            this.isLegacyAttribute         = other.isLegacyAttribute;
            this.indexFieldName            = other.indexFieldName;
            this.isDynAttribute            = false;
            this.isDynAttributeEvalTrigger = false;
        }

        public static String getEdgeLabel(String property) {
            return "__" + property;
        }

        public static String encodePropertyKey(String key) {
            if (StringUtils.isBlank(key)) {
                return key;
            }

            for (String[] strMap : RESERVED_CHAR_ENCODE_MAP) {
                key = key.replace(strMap[0], strMap[1]);
            }

            return key;
        }

        public static String decodePropertyKey(String key) {
            if (StringUtils.isBlank(key)) {
                return key;
            }

            for (String[] strMap : RESERVED_CHAR_ENCODE_MAP) {
                key = key.replace(strMap[1], strMap[0]);
            }

            return key;
        }

        public static String escapeIndexQueryValue(Collection<String> values) {
            return escapeIndexQueryValue(values, false);
        }

        public static String escapeIndexQueryValue(Collection<String> values, boolean allowWildcard) {
            StringBuilder sb = new StringBuilder();

            sb.append(BRACE_OPEN_CHAR);

            if (CollectionUtils.isNotEmpty(values)) {
                Iterator<String> iter = values.iterator();

                sb.append(escapeIndexQueryValue(iter.next(), allowWildcard));

                while (iter.hasNext()) {
                    sb.append(SPACE_CHAR).append(escapeIndexQueryValue(iter.next(), allowWildcard));
                }
            }

            sb.append(BRACE_CLOSE_CHAR);

            return sb.toString();
        }

        public static String escapeIndexQueryValue(String value) {
            return escapeIndexQueryValue(value, false, true);
        }

        public static String escapeIndexQueryValue(String value, boolean allowWildcard) {
            return escapeIndexQueryValue(value, allowWildcard, true);
        }

        public static String escapeIndexQueryValue(String value, boolean allowWildcard, boolean shouldQuote) {
            String  ret        = value;
            boolean quoteValue = false;

            if (hasIndexQueryEscapeChar(value)) {
                StringBuilder sb = new StringBuilder();

                for (int i = 0; i < value.length(); i++) {
                    char c = value.charAt(i);

                    if (!(allowWildcard && c == '*') && isIndexQueryEscapeChar(c)) {
                        sb.append('\\');
                    }

                    if (shouldQuote && !quoteValue) {
                        quoteValue = shouldQuoteIndexQueryForChar(c);
                    }

                    sb.append(c);
                }

                ret = sb.toString();
            } else if (value != null) {
                for (int i = 0; i < value.length(); i++) {
                    if (shouldQuote && shouldQuoteIndexQueryForChar(value.charAt(i))) {
                        quoteValue = true;

                        break;
                    }
                }
            }

            if (quoteValue) {
                boolean isQuoteAtStart = ret.charAt(0) == DOUBLE_QUOTE_CHAR;
                boolean isQuoteAtEnd   = ret.charAt(ret.length() - 1) == DOUBLE_QUOTE_CHAR;

                if (!isQuoteAtStart) {
                    if (!isQuoteAtEnd) {
                        ret = DOUBLE_QUOTE_CHAR + ret + DOUBLE_QUOTE_CHAR;
                    } else {
                        ret = DOUBLE_QUOTE_CHAR + ret;
                    }
                } else if (!isQuoteAtEnd) {
                    ret = ret + DOUBLE_QUOTE_CHAR;
                }
            }

            return ret;
        }

        public static boolean hastokenizeChar(String value) {
            if (StringUtils.isNotEmpty(value)) {
                for (int i = 0; i < value.length(); i++) {
                    if (hastokenizeChar(value, i)) {
                        return true;
                    } else if (hasCJKChar(value, i)) {
                        return true;
                    }
                }
            }
            return false;
        }

        public static String getQualifiedAttributeName(AtlasStructDef structDef, String attrName) {
            if (isRootType(structDef)) {
                return attrName;
            } else {
                return attrName.contains(".") ? attrName : String.format("%s.%s", structDef.getName(), attrName);
            }
        }

        public static String generateVertexPropertyName(AtlasStructDef structDef, AtlasAttributeDef attrDef, String qualifiedName) {
            String vertexPropertyName = qualifiedName;
            String attrName           = attrDef.getName();

            if (isRootType(structDef)) {
                return attrName;
            } else {
                if (!attrDef.getName().contains(".") && AtlasAttributeDef.IndexType.STRING.equals(attrDef.getIndexType()) && ATLAS_TYPE_STRING.equalsIgnoreCase(attrDef.getTypeName())) {
                    vertexPropertyName = String.format("%s.%s%s", structDef.getName(), VERTEX_PROPERTY_PREFIX_STRING_INDEX_TYPE, attrDef.getName());
                }
            }

            return encodePropertyKey(vertexPropertyName);
        }

        public AtlasStructType getDefinedInType() {
            return definedInType;
        }

        public AtlasStructDef getDefinedInDef() {
            return definedInType.getStructDef();
        }

        public AtlasType getAttributeType() {
            return attributeType;
        }

        public AtlasAttributeDef getAttributeDef() {
            return attributeDef;
        }

        public String getName() {
            return attributeDef.getName();
        }

        public String getTypeName() {
            return attributeDef.getTypeName();
        }

        public String getQualifiedName() {
            return qualifiedName;
        }

        public String getVertexPropertyName() {
            return vertexPropertyName;
        }

        public String getVertexUniquePropertyName() {
            return vertexUniquePropertyName;
        }

        public boolean isOwnedRef() {
            return isOwnedRef;
        }

        public boolean isObjectRef() {
            return isObjectRef;
        }

        public String getInverseRefAttributeName() {
            return inverseRefAttributeName;
        }

        public AtlasAttribute getInverseRefAttribute() {
            return inverseRefAttribute;
        }

        public void setInverseRefAttribute(AtlasAttribute inverseAttr) {
            inverseRefAttribute = inverseAttr;
        }

        public String getRelationshipName() {
            return relationshipName;
        }

        public void setRelationshipName(String relationshipName) {
            this.relationshipName = relationshipName;
        }

        public String getRelationshipEdgeLabel() {
            return relationshipEdgeLabel;
        }

        public void setRelationshipEdgeLabel(String relationshipEdgeLabel) {
            this.relationshipEdgeLabel = relationshipEdgeLabel;
        }

        public AtlasRelationshipEdgeDirection getRelationshipEdgeDirection() {
            return relationshipEdgeDirection;
        }

        public void setRelationshipEdgeDirection(AtlasRelationshipEdgeDirection relationshipEdgeDirection) {
            this.relationshipEdgeDirection = relationshipEdgeDirection;
        }

        public boolean isLegacyAttribute() {
            return isLegacyAttribute;
        }

        public void setLegacyAttribute(boolean legacyAttribute) {
            isLegacyAttribute = legacyAttribute;
        }

        public String getIndexFieldName() {
            return indexFieldName;
        }

        public void setIndexFieldName(String indexFieldName) {
            this.indexFieldName = indexFieldName;
        }

        public int getSearchWeight() {
            return attributeDef.getSearchWeight();
        }

        public AtlasAttributeDef.IndexType getIndexType() {
            return attributeDef.getIndexType();
        }

        public boolean getIsDynAttribute() {
            return isDynAttribute;
        }

        public void setIsDynAttribute(boolean isDynAttribute) {
            this.isDynAttribute = isDynAttribute;
        }

        public boolean getIsDynAttributeEvalTrigger() {
            return isDynAttributeEvalTrigger;
        }

        public void setIsDynAttributeEvalTrigger(boolean isDynAttributeEvalTrigger) {
            this.isDynAttributeEvalTrigger = isDynAttributeEvalTrigger;
        }

        public Set<String> getOptionSet(String optionName) {
            String      strValue = attributeDef.getOption(optionName);

            return StringUtils.isBlank(strValue) ? null : AtlasType.fromJson(strValue, Set.class);
        }

        public Integer getOptionInt(String optionName) {
            String  strValue = attributeDef.getOption(optionName);

            return StringUtils.isBlank(strValue) ? null : Integer.parseInt(strValue);
        }

        public String getOptionString(String optionName) {
            String strValue = attributeDef.getOption(optionName);

            return StringUtils.isBlank(strValue) ? null : strValue;
        }

        public AtlasEntityType getReferencedEntityType(AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
            AtlasType type = typeRegistry.getType(attributeDef.getTypeName());

            return AtlasStructType.getReferencedEntityType(type);
        }

        private static boolean hasIndexQueryEscapeChar(String value) {
            if (value != null) {
                for (int i = 0; i < value.length(); i++) {
                    if (isIndexQueryEscapeChar(value.charAt(i))) {
                        return true;
                    }
                }
            }

            return false;
        }

        private static boolean isIndexQueryEscapeChar(char c) {
            switch (c) {
                case '+':
                case '-':
                case '&':
                case '|':
                case '!':
                case '(':
                case ')':
                case '{':
                case '}':
                case '[':
                case ']':
                case '^':
                case '"':
                case '~':
                case '*':
                case '?':
                case ':':
                case '\\':
                case '/':
                case ' ':
                    return true;
            }

            return false;
        }

        private static boolean hasCJKChar(String value, int i) {
            char                   ch    = value.charAt(i);
            Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);

            return Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(block) ||
                    Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS.equals(block) ||
                    Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A.equals(block) ||
                    Character.UnicodeBlock.HIRAGANA.equals(block);
        }

        private static boolean hastokenizeChar(String value, int i) {
            char c = value.charAt(i);

            if (!Character.isLetterOrDigit(c)) {
                switch (c) {
                    case '_':
                        return false;
                    case '.':
                    case ':':
                    case '\'':
                        if (i > 0 && !Character.isAlphabetic(value.charAt(i - 1))) {
                            return true;
                        }
                        return i < value.length() - 1 && !Character.isAlphabetic(value.charAt(i + 1));
                }

                return true;
            }

            return false;
        }

        private static boolean shouldQuoteIndexQueryForChar(char c) {
            switch (c) {
                case '@':
                case ' ':
                case '+':
                case '-':
                case '&':
                case '|':
                case '!':
                case '(':
                case ')':
                case '{':
                case '}':
                case '[':
                case ']':
                case '^':
                case '"':
                case '~':
                case '?':
                case ':':
                case '\\':
                case '/':
                    return true;
            }

            return false;
        }

        private String getRelationshipEdgeLabel(String relationshipLabel) {
            return (relationshipLabel == null) ? getEdgeLabel(qualifiedName) : relationshipLabel;
        }

        private static boolean isRootType(AtlasStructDef structDef) {
            return StringUtils.equals(structDef.getName(), AtlasEntityType.ENTITY_ROOT.getTypeName()) || StringUtils.equals(structDef.getName(), AtlasClassificationType.CLASSIFICATION_ROOT.getTypeName());
        }

        public enum AtlasRelationshipEdgeDirection { IN, OUT, BOTH }
    }

    static {
        try {
            configuration = ApplicationProperties.get();
        } catch (AtlasException e) {
            throw new RuntimeException(e);
        }

        skipCheckForParentChildAttributeName = configuration.getBoolean(SKIP_CHECK_FOR_PARENT_CHILD_ATTRIBUTE_NAME, false);
    }
}
