/*
 * 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.logging.log4j.core.config.composite;

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

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.config.AbstractConfiguration;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
import org.apache.logging.log4j.core.config.plugins.util.PluginType;
import org.apache.logging.log4j.core.filter.CompositeFilter;

/**
 * The default merge strategy for composite configurations.
 * <p>
 * The default merge strategy performs the merge according to the following rules:
 * <ol>
 * <li>Aggregates the global configuration attributes with those in later configurations replacing those in previous
 * configurations with the exception that the highest status level and the lowest monitorInterval greater than 0 will
 * be used.</li>
 * <li>Properties from all configurations are aggregated. Duplicate properties replace those in previous
 * configurations.</li>
 * <li>Filters are aggregated under a CompositeFilter if more than one Filter is defined. Since Filters are not named
 * duplicates may be present.</li>
 * <li>Scripts and ScriptFile references are aggregated. Duplicate definitions replace those in previous
 * configurations.</li>
 * <li>Appenders are aggregated. Appenders with the same name are replaced by those in later configurations, including
 * all of the Appender's subcomponents.</li>
 * <li>Loggers are all aggregated. Logger attributes are individually merged with duplicates being replaced by those
 * in later configurations. Appender references on a Logger are aggregated with duplicates being replaced by those in
 * later configurations. Filters on a Logger are aggregated under a CompositeFilter if more than one Filter is defined.
 * Since Filters are not named duplicates may be present. Filters under Appender references included or discarded
 * depending on whether their parent Appender reference is kept or discarded.</li>
 * </ol>
 */
public class DefaultMergeStrategy implements MergeStrategy {

    private static final String APPENDERS = "appenders";
    private static final String PROPERTIES = "properties";
    private static final String LOGGERS = "loggers";
    private static final String SCRIPTS = "scripts";
    private static final String FILTERS = "filters";
    private static final String STATUS = "status";
    private static final String NAME = "name";
    private static final String REF = "ref";

    /**
     * Merge the root properties.
     * @param rootNode The composite root node.
     * @param configuration The configuration to merge.
     */
    @Override
    public void mergeRootProperties(final Node rootNode, final AbstractConfiguration configuration) {
        for (final Map.Entry<String, String> attribute : configuration.getRootNode().getAttributes().entrySet()) {
            boolean isFound = false;
            for (final Map.Entry<String, String> targetAttribute : rootNode.getAttributes().entrySet()) {
                if (targetAttribute.getKey().equalsIgnoreCase(attribute.getKey())) {
                    if (attribute.getKey().equalsIgnoreCase(STATUS)) {
                        final Level targetLevel = Level.getLevel(targetAttribute.getValue().toUpperCase());
                        final Level sourceLevel = Level.getLevel(attribute.getValue().toUpperCase());
                        if (targetLevel != null && sourceLevel != null) {
                            if (sourceLevel.isLessSpecificThan(targetLevel)) {
                                targetAttribute.setValue(attribute.getValue());
                            }
                        } else
                            if (sourceLevel != null) {
                                targetAttribute.setValue(attribute.getValue());
                            }
                    } else {
                        if (attribute.getKey().equalsIgnoreCase("monitorInterval")) {
                            final int sourceInterval = Integer.parseInt(attribute.getValue());
                            final int targetInterval = Integer.parseInt(targetAttribute.getValue());
                            if (targetInterval == 0 || sourceInterval < targetInterval) {
                                targetAttribute.setValue(attribute.getValue());
                            }
                        } else {
                            targetAttribute.setValue(attribute.getValue());
                        }
                    }
                    isFound = true;
                }
            }
            if (!isFound) {
                rootNode.getAttributes().put(attribute.getKey(), attribute.getValue());
            }
        }
    }

    /**
     * Merge the source Configuration into the target Configuration.
     *
     * @param target        The target node to merge into.
     * @param source        The source node.
     * @param pluginManager The PluginManager.
     */
    @Override
    public void mergConfigurations(final Node target, final Node source, final PluginManager pluginManager) {
        for (final Node sourceChildNode : source.getChildren()) {
            final boolean isFilter = isFilterNode(sourceChildNode);
            boolean isMerged = false;
            for (final Node targetChildNode : target.getChildren()) {
                if (isFilter) {
                    if (isFilterNode(targetChildNode)) {
                        updateFilterNode(target, targetChildNode, sourceChildNode, pluginManager);
                        isMerged = true;
                        break;
                    }
                    continue;
                }

                if (!targetChildNode.getName().equalsIgnoreCase(sourceChildNode.getName())) {
                    continue;
                }

                switch (targetChildNode.getName().toLowerCase()) {
                    case PROPERTIES:
                    case SCRIPTS:
                    case APPENDERS: {
                        for (final Node node : sourceChildNode.getChildren()) {
                            for (final Node targetNode : targetChildNode.getChildren()) {
                                if (Objects.equals(targetNode.getAttributes().get(NAME), node.getAttributes().get(NAME))) {
                                    targetChildNode.getChildren().remove(targetNode);
                                    break;
                                }
                            }
                            targetChildNode.getChildren().add(node);
                        }
                        isMerged = true;
                        break;
                    }
                    case LOGGERS: {
                        final Map<String, Node> targetLoggers = new HashMap<>();
                        for (final Node node : targetChildNode.getChildren()) {
                            targetLoggers.put(node.getName(), node);
                        }
                        for (final Node node : sourceChildNode.getChildren()) {
                            final Node targetNode = getLoggerNode(targetChildNode, node.getAttributes().get(NAME));
                            final Node loggerNode = new Node(targetChildNode, node.getName(), node.getType());
                            if (targetNode != null) {
                                targetNode.getAttributes().putAll(node.getAttributes());
                                for (final Node sourceLoggerChild : node.getChildren()) {
                                    if (isFilterNode(sourceLoggerChild)) {
                                        boolean foundFilter = false;
                                        for (final Node targetChild : targetNode.getChildren()) {
                                            if (isFilterNode(targetChild)) {
                                                updateFilterNode(loggerNode, targetChild, sourceLoggerChild,
                                                        pluginManager);
                                                foundFilter = true;
                                                break;
                                            }
                                        }
                                        if (!foundFilter) {
                                            final Node childNode = new Node(loggerNode, sourceLoggerChild.getName(),
                                                    sourceLoggerChild.getType());
                                            childNode.getAttributes().putAll(sourceLoggerChild.getAttributes());
                                            childNode.getChildren().addAll(sourceLoggerChild.getChildren());
                                            targetNode.getChildren().add(childNode);
                                        }
                                    } else {
                                        final Node childNode = new Node(loggerNode, sourceLoggerChild.getName(),
                                                sourceLoggerChild.getType());
                                        childNode.getAttributes().putAll(sourceLoggerChild.getAttributes());
                                        childNode.getChildren().addAll(sourceLoggerChild.getChildren());
                                        if (childNode.getName().equalsIgnoreCase("AppenderRef")) {
                                            for (final Node targetChild : targetNode.getChildren()) {
                                                if (isSameReference(targetChild, childNode)) {
                                                    targetNode.getChildren().remove(targetChild);
                                                    break;
                                                }
                                            }
                                        } else {
                                            for (final Node targetChild : targetNode.getChildren()) {
                                                if (isSameName(targetChild, childNode)) {
                                                    targetNode.getChildren().remove(targetChild);
                                                    break;
                                                }
                                            }
                                        }

                                        targetNode.getChildren().add(childNode);
                                    }
                                }
                            } else {
                                loggerNode.getAttributes().putAll(node.getAttributes());
                                loggerNode.getChildren().addAll(node.getChildren());
                                targetChildNode.getChildren().add(loggerNode);
                            }
                        }
                        isMerged = true;
                        break;
                    }
                    default: {
                        targetChildNode.getChildren().addAll(sourceChildNode.getChildren());
                        isMerged = true;
                        break;
                    }

                }
            }
            if (!isMerged) {
                if (sourceChildNode.getName().equalsIgnoreCase("Properties")) {
                    target.getChildren().add(0, sourceChildNode);
                } else {
                    target.getChildren().add(sourceChildNode);
                }
            }
        }
    }

    private Node getLoggerNode(final Node parentNode, final String name) {
        for (final Node node : parentNode.getChildren()) {
            final String nodeName = node.getAttributes().get(NAME);
            if (name == null && nodeName == null) {
                return node;
            }
            if (nodeName != null && nodeName.equals(name)) {
                return node;
            }
        }
        return null;
    }

    private void updateFilterNode(final Node target, final Node targetChildNode, final Node sourceChildNode,
            final PluginManager pluginManager) {
        if (CompositeFilter.class.isAssignableFrom(targetChildNode.getType().getPluginClass())) {
            final Node node = new Node(targetChildNode, sourceChildNode.getName(), sourceChildNode.getType());
            node.getChildren().addAll(sourceChildNode.getChildren());
            node.getAttributes().putAll(sourceChildNode.getAttributes());
            targetChildNode.getChildren().add(node);
        } else {
            final PluginType pluginType = pluginManager.getPluginType(FILTERS);
            final Node filtersNode = new Node(targetChildNode, FILTERS, pluginType);
            final Node node = new Node(filtersNode, sourceChildNode.getName(), sourceChildNode.getType());
            node.getAttributes().putAll(sourceChildNode.getAttributes());
            final List<Node> children = filtersNode.getChildren();
            children.add(targetChildNode);
            children.add(node);
            final List<Node> nodes = target.getChildren();
            nodes.remove(targetChildNode);
            nodes.add(filtersNode);
        }
    }

    private boolean isFilterNode(final Node node) {
        return Filter.class.isAssignableFrom(node.getType().getPluginClass());
    }

    private boolean isSameName(final Node node1, final Node node2) {
        final String value = node1.getAttributes().get(NAME);
        return value != null && value.toLowerCase().equals(node2.getAttributes().get(NAME).toLowerCase());
    }

    private boolean isSameReference(final Node node1, final Node node2) {
        final String value = node1.getAttributes().get(REF);
        return value != null && value.toLowerCase().equals(node2.getAttributes().get(REF).toLowerCase());
    }
}
