/**
 * 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.repository.store.graph.v2;

import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.instance.AtlasClassification;
import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.repository.graphdb.AtlasVertex;
import org.apache.atlas.type.AtlasEntityType;
import org.apache.atlas.type.AtlasStructType.AtlasAttribute;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.atlas.utils.AtlasEntityUtil;
import org.apache.commons.collections.MapUtils;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.apache.atlas.repository.graph.GraphHelper.getCustomAttributes;

public class AtlasEntityComparator {
    private final AtlasTypeRegistry    typeRegistry;
    private final EntityGraphRetriever entityRetriever;
    private final Map<String, String>  guidRefMap;
    private final boolean              skipClassificationCompare;
    private final boolean              skipBusinessAttributeCompare;

    public AtlasEntityComparator(AtlasTypeRegistry typeRegistry, EntityGraphRetriever entityRetriever, Map<String, String> guidRefMap, boolean skipClassificationCompare, boolean skipBusinessAttributeCompare) {
        this.typeRegistry                 = typeRegistry;
        this.entityRetriever              = entityRetriever;
        this.guidRefMap                   = guidRefMap;
        this.skipClassificationCompare    = skipClassificationCompare;
        this.skipBusinessAttributeCompare = skipBusinessAttributeCompare;
    }

    public AtlasEntityDiffResult getDiffResult(AtlasEntity updatedEntity, AtlasEntity storedEntity, boolean findOnlyFirstDiff) throws AtlasBaseException {
        return getDiffResult(updatedEntity, storedEntity, null, findOnlyFirstDiff);
    }

    public AtlasEntityDiffResult getDiffResult(AtlasEntity updatedEntity, AtlasVertex storedVertex, boolean findOnlyFirstDiff) throws AtlasBaseException {
        return getDiffResult(updatedEntity, null, storedVertex, findOnlyFirstDiff);
    }

    private AtlasEntityDiffResult getDiffResult(AtlasEntity updatedEntity, AtlasEntity storedEntity, AtlasVertex storedVertex, boolean findOnlyFirstDiff) throws AtlasBaseException {
        AtlasEntity                              diffEntity                       = new AtlasEntity(updatedEntity.getTypeName());
        AtlasEntityType                          entityType                       = typeRegistry.getEntityTypeByName(updatedEntity.getTypeName());
        Map<String, AtlasAttribute>              entityTypeAttributes             = entityType.getAllAttributes();
        Map<String, Map<String, AtlasAttribute>> entityTypeRelationshipAttributes = entityType.getRelationshipAttributes();

        int     sectionsWithDiff                = 0;
        boolean hasDiffInAttributes             = false;
        boolean hasDiffInRelationshipAttributes = false;
        boolean hasDiffInCustomAttributes       = false;
        boolean hasDiffInBusinessAttributes     = false;

        diffEntity.setGuid(updatedEntity.getGuid());

        if (MapUtils.isNotEmpty(updatedEntity.getAttributes())) { // check for attribute value change
            for (Map.Entry<String, Object> entry : updatedEntity.getAttributes().entrySet()) {
                String         attrName  = entry.getKey();
                AtlasAttribute attribute = entityTypeAttributes.get(attrName);

                if (attribute == null) { // no such attribute
                    continue;
                }

                Object newVal  = entry.getValue();
                Object currVal = (storedEntity != null) ? storedEntity.getAttribute(attrName) : entityRetriever.getEntityAttribute(storedVertex, attribute);

                if (!attribute.getAttributeType().areEqualValues(currVal, newVal, guidRefMap)) {
                    hasDiffInAttributes = true;

                    diffEntity.setAttribute(attrName, newVal);

                    if (findOnlyFirstDiff) {
                        return new AtlasEntityDiffResult(diffEntity, true, false, false);
                    }
                }
            }

            if (hasDiffInAttributes) {
                sectionsWithDiff++;
            }
        }

        if (MapUtils.isNotEmpty(updatedEntity.getRelationshipAttributes())) { // check for relationship-attribute value change
            for (Map.Entry<String, Object> entry : updatedEntity.getRelationshipAttributes().entrySet()) {
                String attrName = entry.getKey();

                if (!entityTypeRelationshipAttributes.containsKey(attrName)) {  // no such attribute
                    continue;
                }

                Object         newVal           = entry.getValue();
                String         relationshipType = AtlasEntityUtil.getRelationshipType(newVal);
                AtlasAttribute attribute        = entityType.getRelationshipAttribute(attrName, relationshipType);
                Object         currVal          = (storedEntity != null) ? storedEntity.getRelationshipAttribute(attrName) : entityRetriever.getEntityAttribute(storedVertex, attribute);

                if (!attribute.getAttributeType().areEqualValues(currVal, newVal, guidRefMap)) {
                    hasDiffInRelationshipAttributes = true;

                    diffEntity.setRelationshipAttribute(attrName, newVal);

                    if (findOnlyFirstDiff) {
                        return new AtlasEntityDiffResult(diffEntity, true, false, false);
                    }
                }
            }

            if (hasDiffInRelationshipAttributes) {
                sectionsWithDiff++;
            }
        }

        if (!skipClassificationCompare) {
            List<AtlasClassification> newVal  = updatedEntity.getClassifications();
            List<AtlasClassification> currVal = (storedEntity != null) ? storedEntity.getClassifications() : entityRetriever.getAllClassifications(storedVertex);

            if (!Objects.equals(currVal, newVal)) {
                diffEntity.setClassifications(newVal);

                sectionsWithDiff++;

                if (findOnlyFirstDiff) {
                    return new AtlasEntityDiffResult(diffEntity, true, false, false);
                }
            }
        }

        if (updatedEntity.getCustomAttributes() != null) {
            // event coming from hook does not have custom attributes, such events must not remove existing attributes
            // UI sends empty object in case of of intended removal.
            Map<String, String> newCustomAttributes  = updatedEntity.getCustomAttributes();
            Map<String, String> currCustomAttributes = (storedEntity != null) ? storedEntity.getCustomAttributes() : getCustomAttributes(storedVertex);

            if (!Objects.equals(currCustomAttributes, newCustomAttributes)) {
                diffEntity.setCustomAttributes(newCustomAttributes);

                hasDiffInCustomAttributes = true;
                sectionsWithDiff++;

                if (findOnlyFirstDiff && sectionsWithDiff > 1) {
                    return new AtlasEntityDiffResult(diffEntity, true, false, false);
                }
            }
        }

        if (!skipBusinessAttributeCompare) {
            Map<String, Map<String, Object>> newBusinessMetadata  = updatedEntity.getBusinessAttributes();
            Map<String, Map<String, Object>> currBusinessMetadata = (storedEntity != null) ? storedEntity.getBusinessAttributes() : entityRetriever.getBusinessMetadata(storedVertex);

            if (!Objects.equals(currBusinessMetadata, newBusinessMetadata)) {
                diffEntity.setBusinessAttributes(newBusinessMetadata);

                hasDiffInBusinessAttributes = true;

                sectionsWithDiff++;

                if (findOnlyFirstDiff && sectionsWithDiff > 1) {
                    return new AtlasEntityDiffResult(diffEntity, true, false, false);
                }
            }
        }

        return new AtlasEntityDiffResult(diffEntity, sectionsWithDiff > 0, sectionsWithDiff == 1 && hasDiffInCustomAttributes, sectionsWithDiff == 1 && hasDiffInBusinessAttributes);
    }

    public static class AtlasEntityDiffResult {
        private final AtlasEntity diffEntity;
        private final boolean     hasDifference;
        private final boolean     hasDifferenceOnlyInCustomAttributes;
        private final boolean     hasDifferenceOnlyInBusinessAttributes;

        AtlasEntityDiffResult(AtlasEntity diffEntity, boolean hasDifference, boolean hasDifferenceOnlyInCustomAttributes, boolean hasDifferenceOnlyInBusinessAttributes) {
            this.diffEntity                            = diffEntity;
            this.hasDifference                         = hasDifference;
            this.hasDifferenceOnlyInCustomAttributes   = hasDifferenceOnlyInCustomAttributes;
            this.hasDifferenceOnlyInBusinessAttributes = hasDifferenceOnlyInBusinessAttributes;
        }

        public AtlasEntity getDiffEntity() {
            return diffEntity;
        }

        public boolean hasDifference() {
            return hasDifference;
        }

        public boolean hasDifferenceOnlyInCustomAttributes() {
            return hasDifferenceOnlyInCustomAttributes;
        }

        public boolean hasDifferenceOnlyInBusinessAttributes() {
            return hasDifferenceOnlyInBusinessAttributes;
        }
    }
}
