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

import org.apache.atlas.entitytransform.BaseEntityHandler.AtlasTransformableEntity;
import org.apache.atlas.model.impexp.AtlasExportRequest;
import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.model.instance.AtlasObjectId;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public abstract class Condition {
    private static final Logger LOG = LoggerFactory.getLogger(Condition.class);

    private static final String CONDITION_DELIMITER                    = ":";
    private static final String CONDITION_ENTITY_OBJECT_ID             = "OBJECTID";
    private static final String CONDITION_ENTITY_TOP_LEVEL             = "TOPLEVEL";
    private static final String CONDITION_ENTITY_ALL                   = "ALL";
    private static final String CONDITION_NAME_EQUALS                  = "EQUALS";
    private static final String CONDITION_NAME_EQUALS_IGNORE_CASE      = "EQUALS_IGNORE_CASE";
    private static final String CONDITION_NAME_STARTS_WITH             = "STARTS_WITH";
    private static final String CONDITION_NAME_STARTS_WITH_IGNORE_CASE = "STARTS_WITH_IGNORE_CASE";
    private static final String CONDITION_NAME_HAS_VALUE               = "HAS_VALUE";

    protected final EntityAttribute attribute;

    protected Condition(EntityAttribute attribute) {
        this.attribute = attribute;
    }

    public static Condition createCondition(String key, String value, TransformerContext context) {
        LOG.debug("==> Condition.createCondition(key={}, value={})", key, value);

        final Condition ret;

        int    idxConditionDelim = value == null ? -1 : value.indexOf(CONDITION_DELIMITER);
        String conditionName     = idxConditionDelim == -1 ? CONDITION_NAME_EQUALS : value.substring(0, idxConditionDelim);
        String conditionValue    = idxConditionDelim == -1 ? value : value.substring(idxConditionDelim + CONDITION_DELIMITER.length());

        conditionName  = StringUtils.trim(conditionName);
        conditionValue = StringUtils.trim(conditionValue);
        value          = StringUtils.trim(value);

        EntityAttribute attribute = new EntityAttribute(StringUtils.trim(key), context);

        switch (conditionName.toUpperCase()) {
            case CONDITION_ENTITY_ALL:
                ret = new ObjectIdEquals(attribute, CONDITION_ENTITY_ALL, context);
                break;

            case CONDITION_ENTITY_TOP_LEVEL:
                ret = new ObjectIdEquals(attribute, CONDITION_ENTITY_TOP_LEVEL, context);
                break;

            case CONDITION_ENTITY_OBJECT_ID:
                ret = new ObjectIdEquals(attribute, conditionValue, context);
                break;

            case CONDITION_NAME_EQUALS:
                ret = new EqualsCondition(attribute, conditionValue);
                break;

            case CONDITION_NAME_EQUALS_IGNORE_CASE:
                ret = new EqualsIgnoreCaseCondition(attribute, conditionValue);
                break;

            case CONDITION_NAME_STARTS_WITH:
                ret = new StartsWithCondition(attribute, conditionValue);
                break;

            case CONDITION_NAME_STARTS_WITH_IGNORE_CASE:
                ret = new StartsWithIgnoreCaseCondition(attribute, conditionValue);
                break;

            case CONDITION_NAME_HAS_VALUE:
                ret = new HasValueCondition(attribute);
                break;

            default:
                ret = new EqualsCondition(attribute, value); // treat unspecified/unknown condition as 'EQUALS'
                break;
        }

        LOG.debug("<== Condition.createCondition(key={}, value={}): actionName={}, actionValue={}, ret={}", key, value, conditionName, conditionValue, ret);

        return ret;
    }

    public EntityAttribute getAttribute() {
        return attribute;
    }

    public abstract boolean matches(AtlasTransformableEntity entity);

    public static class EqualsCondition extends Condition {
        protected final String attributeValue;

        public EqualsCondition(EntityAttribute attribute, String attributeValue) {
            super(attribute);

            this.attributeValue = attributeValue;
        }

        @Override
        public boolean matches(AtlasTransformableEntity entity) {
            Object attributeValue = entity != null ? entity.getAttribute(attribute) : null;

            return attributeValue != null && StringUtils.equals(attributeValue.toString(), this.attributeValue);
        }
    }

    public static class EqualsIgnoreCaseCondition extends Condition {
        protected final String attributeValue;

        public EqualsIgnoreCaseCondition(EntityAttribute attribute, String attributeValue) {
            super(attribute);

            this.attributeValue = attributeValue;
        }

        @Override
        public boolean matches(AtlasTransformableEntity entity) {
            Object attributeValue = entity != null ? entity.getAttribute(attribute) : null;

            return attributeValue != null && StringUtils.equalsIgnoreCase(attributeValue.toString(), this.attributeValue);
        }
    }

    public static class StartsWithCondition extends Condition {
        protected final String prefix;

        public StartsWithCondition(EntityAttribute attribute, String prefix) {
            super(attribute);

            this.prefix = prefix;
        }

        @Override
        public boolean matches(AtlasTransformableEntity entity) {
            Object attributeValue = entity != null ? entity.getAttribute(attribute) : null;

            return attributeValue != null && StringUtils.startsWith(attributeValue.toString(), this.prefix);
        }
    }

    public static class StartsWithIgnoreCaseCondition extends Condition {
        protected final String prefix;

        public StartsWithIgnoreCaseCondition(EntityAttribute attribute, String prefix) {
            super(attribute);

            this.prefix = prefix;
        }

        @Override
        public boolean matches(AtlasTransformableEntity entity) {
            Object attributeValue = entity != null ? entity.getAttribute(attribute) : null;

            return attributeValue != null && StringUtils.startsWithIgnoreCase(attributeValue.toString(), this.prefix);
        }
    }

    static class ObjectIdEquals extends Condition {
        private final boolean             isMatchAll;
        private final List<AtlasObjectId> objectIds;

        public ObjectIdEquals(EntityAttribute attribute, String scope, TransformerContext context) {
            super(attribute);

            this.isMatchAll = StringUtils.equals(scope, CONDITION_ENTITY_ALL);
            this.objectIds  = new ArrayList<>();

            if (!isMatchAll && context != null && context.getExportRequest() != null) {
                AtlasExportRequest request = context.getExportRequest();

                for (AtlasObjectId objectId : request.getItemsToExport()) {
                    addObjectId(objectId);
                }
            }
        }

        @Override
        public boolean matches(AtlasTransformableEntity entity) {
            if (isMatchAll) {
                return true;
            } else {
                for (AtlasObjectId objectId : objectIds) {
                    if (isMatch(objectId, entity.getEntity())) {
                        return true;
                    }
                }

                return false;
            }
        }

        void addObjectId(AtlasObjectId objId) {
            this.objectIds.add(objId);
        }

        private boolean isMatch(AtlasObjectId objectId, AtlasEntity entity) {
            if (!StringUtils.isEmpty(objectId.getGuid())) {
                return Objects.equals(objectId.getGuid(), entity.getGuid());
            }

            boolean ret = Objects.equals(objectId.getTypeName(), entity.getTypeName());

            if (ret) {
                for (Map.Entry<String, Object> entry : objectId.getUniqueAttributes().entrySet()) {
                    ret = ret && Objects.equals(entity.getAttribute(entry.getKey()), entry.getValue());

                    if (!ret) {
                        break;
                    }
                }
            }

            return ret;
        }
    }

    public static class HasValueCondition extends Condition {
        public HasValueCondition(EntityAttribute attribute) {
            super(attribute);
        }

        @Override
        public boolean matches(AtlasTransformableEntity entity) {
            Object attributeValue = entity != null ? entity.getAttribute(attribute) : null;

            return attributeValue != null && StringUtils.isNotEmpty(attributeValue.toString());
        }
    }
}
