/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.controller.transform.description;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.transform.TransformationContext;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.dmr.Property;

/**
 * Checks whether an attribute should be rejected or not
 *
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
public interface RejectAttributeChecker {
    /**
     * Determines whether the given operation parameter value is not understandable by the target process and needs
     * to be rejected.
     *
     * @param address        the address of the operation
     * @param attributeName  the name of the attribute
     * @param attributeValue the value of the attribute
     * @param operation      the operation executed. This is unmodifiable.
     * @param context        the context of the transformation
     * @return {@code true} if the parameter value is not understandable by the target process and so needs to be rejected, {@code false} otherwise.
     */
    boolean rejectOperationParameter(PathAddress address, String attributeName, ModelNode attributeValue, ModelNode operation, TransformationContext context);

    /**
     * Gets whether the given resource attribute value is not understandable by the target process and needs
     * to be rejected.
     *
     * @param address        the address of the resource
     * @param attributeName  the name of the attribute
     * @param attributeValue the value of the attribute
     * @param context        the context of the transformation
     * @return {@code true} if the attribute value is not understandable by the target process and so needs to be rejected, {@code false} otherwise.
     */
    boolean rejectResourceAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context);

    /**
     * Returns the log message id used by this checker. This is used to group it so that all attributes failing a type of rejction
     * end up in the same error message
     *
     * @return the log message id
     */
    String getRejectionLogMessageId();

    /**
     * Gets the log message if the attribute failed rejection
     *
     * @param attributes a map of all attributes failed in this checker and their values
     * @return the formatted log message
     */
    String getRejectionLogMessage(Map<String, ModelNode> attributes);

    /**
     * A standard implementation of RejectAttributeChecker.
     */
    public abstract class DefaultRejectAttributeChecker implements RejectAttributeChecker {
        private volatile String logMessageId;

        /**
         * Constructor
         */
        protected DefaultRejectAttributeChecker() {
        }

        /**
         * {@inheritDoc}
         */
        public boolean rejectOperationParameter(PathAddress address, String attributeName, ModelNode attributeValue, ModelNode operation, TransformationContext context) {
            return rejectAttribute(address, attributeName, attributeValue, context);
        }

        /**
         * {@inheritDoc}
         */
        public boolean rejectResourceAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {
            return rejectAttribute(address, attributeName, attributeValue, context);
        }

        /**
         * Gets called by the default implementations of {@link #rejectOperationParameter(PathAddress, String, ModelNode, ModelNode, TransformationContext)}
         * and {@link #rejectResourceAttribute(PathAddress, String, ModelNode, TransformationContext)}.
         *
         * @param address        the address of the operation
         * @param attributeName  the name of the attribute
         * @param attributeValue the value of the attribute
         * @param context        the context of the transformation
         * @return {@code true} if the attribute or parameter value is not understandable by the target process and so needs to be rejected, {@code false} otherwise.
         */
        protected abstract boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context);

        /**
         * Returns the log message id used by this checker. This is used to group it so that all attributes failing a type of rejction
         * end up in the same error message. This default implementation uses the formatted log message with an empty attribute map as the id.
         *
         * @return the log message id
         */
        public String getRejectionLogMessageId() {
            String id = logMessageId;
            if (id == null) {
                id = getRejectionLogMessage(Collections.<String, ModelNode>emptyMap());
            }
            logMessageId = id;
            return logMessageId;
        }
    }


    /**
     * Checks a simple attribute for expressions
     */
    RejectAttributeChecker SIMPLE_EXPRESSIONS = new DefaultRejectAttributeChecker() {

        /** {@inheritDoc} */
        @Override
        protected boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {
            return checkForExpression(attributeValue);
        }

        /**
         * Check an attribute for expressions.
         *
         * @param node the attribute value
         * @return whether an expression was found or not
         */
        private boolean checkForExpression(final ModelNode node) {
            if (!node.isDefined()) {
                return false;
            }

            final ModelNode resolved = node.clone();
            if (node.getType() == ModelType.EXPRESSION || node.getType() == ModelType.STRING) {
                return checkForExpression(resolved.asString());
            } else if (node.getType() == ModelType.OBJECT) {
                for (Property prop : resolved.asPropertyList()) {
                    if (checkForExpression(prop.getValue())) {
                        return true;
                    }
                }
            } else if (node.getType() == ModelType.LIST) {
                for (ModelNode current : resolved.asList()) {
                    if (checkForExpression(current)) {
                        return true;
                    }
                }
            } else if (node.getType() == ModelType.PROPERTY) {
                return checkForExpression(resolved.asProperty().getValue());
            }
            return false;
        }

        private boolean checkForExpression(final String value) {
            return ExpressionPattern.EXPRESSION_PATTERN.matcher(value).matches();
        }

        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return ControllerLogger.ROOT_LOGGER.attributesDoNotSupportExpressions(attributes.keySet());
        }
    };


    /**
     * A RejectAttributeChecker for {@link ModelType#LIST} attribute values
     */
    public class ListRejectAttributeChecker implements RejectAttributeChecker {

        private final RejectAttributeChecker elementChecker;

        /**
         * Constructor
         *
         * @param elementChecker the checker to check the list elements
         */
        public ListRejectAttributeChecker(RejectAttributeChecker elementChecker) {
            this.elementChecker = elementChecker;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean rejectOperationParameter(PathAddress address, String attributeName, ModelNode attributeValue, ModelNode operation, TransformationContext context) {
            if (attributeValue.isDefined()) {
                for (ModelNode element : attributeValue.asList()) {
                    if (elementChecker.rejectOperationParameter(address, attributeName, element, operation, context)) {
                        return true;
                    }
                }
            }
            return false;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean rejectResourceAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {
            if (attributeValue.isDefined()) {
                for (ModelNode element : attributeValue.asList()) {
                    if (elementChecker.rejectResourceAttribute(address, attributeName, element, context)) {
                        return true;
                    }
                }
            }
            return false;
        }

        public String getRejectionLogMessageId() {
            return elementChecker.getRejectionLogMessageId();
        }

        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return elementChecker.getRejectionLogMessage(attributes);
        }
    }

    /**
     * A RejectAttributeChecker for {@link ModelType#OBJECT} attribute values
     */
    public class ObjectFieldsRejectAttributeChecker implements RejectAttributeChecker {

        private final Map<String, RejectAttributeChecker> fields = new HashMap<String, RejectAttributeChecker>();

        /**
         * Constructor
         *
         * @param fields map of keys in the object type and the RejectAttributeChecker to use to check the entries
         */
        public ObjectFieldsRejectAttributeChecker(Map<String, RejectAttributeChecker> fields) {
            assert fields != null : "Null fields";
            assert fields.size() > 0 : "Empty fields";
            this.fields.putAll(fields);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean rejectOperationParameter(PathAddress address, String attributeName, ModelNode attributeValue, ModelNode operation, TransformationContext context) {

            for (Map.Entry<String, RejectAttributeChecker> entry : fields.entrySet()) {
                ModelNode fieldValue = attributeValue.hasDefined(entry.getKey()) ? attributeValue.get(entry.getKey()) : new ModelNode();
                if (entry.getValue().rejectOperationParameter(address, attributeName, fieldValue, operation, context)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean rejectResourceAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {

            for (Map.Entry<String, RejectAttributeChecker> entry : fields.entrySet()) {
                ModelNode fieldValue = attributeValue.hasDefined(entry.getKey()) ? attributeValue.get(entry.getKey()) : new ModelNode();
                if (entry.getValue().rejectResourceAttribute(address, attributeName, fieldValue, context)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String getRejectionLogMessageId() {
            return fields.entrySet().iterator().next().getValue().getRejectionLogMessageId();
        }

        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return fields.entrySet().iterator().next().getValue().getRejectionLogMessage(attributes);
        }
    }

    RejectAttributeChecker DEFINED = new DefaultRejectAttributeChecker() {
        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return ControllerLogger.ROOT_LOGGER.attributesAreNotUnderstoodAndMustBeIgnored(attributes.keySet());
        }

        @Override
        protected boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue,
                                          TransformationContext context) {
            return attributeValue.isDefined();
        }
    };


    RejectAttributeChecker UNDEFINED = new DefaultRejectAttributeChecker() {
        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return ControllerLogger.ROOT_LOGGER.attributesMustBeDefined(attributes.keySet());
        }

        @Override
        protected boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue,
                                          TransformationContext context) {
            return !attributeValue.isDefined();
        }
    };


    RejectAttributeChecker ALL = new DefaultRejectAttributeChecker() {
        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return ControllerLogger.ROOT_LOGGER.attributesMustBeDefined(attributes.keySet());
        }

        @Override
        protected boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue,
                                          TransformationContext context) {
            return true;
        }
    };

    /**
     * Rejects the attribute if the value is equal to the specified value.
     */
    public static class SimpleRejectAttributeChecker extends DefaultRejectAttributeChecker {
        private final ModelNode value;

        public SimpleRejectAttributeChecker(ModelNode value) {
            this.value = value;
        }

        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return ControllerLogger.ROOT_LOGGER.attributesMustNotBeDefinedAs(this.value, attributes.keySet());
        }

        @Override
        protected boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {
            return this.value.equals(attributeValue);
        }
    }

    /**
     * Rejects an attribute if the value is anything other than the specified value..
     */
    public static class SimpleAcceptAttributeChecker extends DefaultRejectAttributeChecker {
        private final ModelNode value;

        public SimpleAcceptAttributeChecker(ModelNode value) {
            this.value = value;
        }

        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return ControllerLogger.ROOT_LOGGER.attributesMustBeDefinedAs(this.value, attributes.keySet());
        }

        @Override
        protected boolean rejectAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {
            return !this.value.equals(attributeValue);
        }
    }
}
