/*
 * JBoss, Home of Professional Open Source
 * Copyright 2016, Red Hat, Inc., and individual contributors as indicated
 * by the @authors tag.
 *
 * Licensed 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.wildfly.legacy.util;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ACCESS_TYPE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ALTERNATIVES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ATTRIBUTES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CHILDREN;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEFAULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEPRECATED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.EXPRESSIONS_ALLOWED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MANAGEMENT_MAJOR_VERSION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MANAGEMENT_MICRO_VERSION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MANAGEMENT_MINOR_VERSION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MODEL_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NILLABLE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATIONS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REPLY_PROPERTIES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REQUEST_PROPERTIES;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STORAGE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.TYPE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.UNIT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE_TYPE;

import java.io.IOException;
import java.io.PrintStream;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.jboss.as.controller.ModelVersion;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.registry.AttributeAccess.Storage;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.Property;

/**
 * Compares the current running server with the passed in version. The passed in version
 * must have model and resource definition dumps in {@code src/test/resources/legacy-models} as
 * outlined in {@link DumpStandaloneResourceDefinitionUtil}, {@link DumpDomainResourceDefinitionUtil} and {@link GrabModelVersionsUtil}.
 * <p>
 * To run this a big heap size is needed, e.g. -Xmx1024m
 *
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
public class CompareModelVersionsUtil {

    private final boolean compareDifferentVersions;
    private final boolean compareRuntime;
    private final String targetVersion;
    private final ModelNode legacyModelVersions;
    private final ModelNode legacyResourceDefinitions;
    private final ModelNode currentModelVersions;
    private final ModelNode currentResourceDefinitions;
    private final Set<String> subsystems;
    private final boolean compareDeprecated;
    private final PrintStream out;

    private CompareModelVersionsUtil(boolean compareDifferentVersions,
                                     boolean compareRuntime,
                                     boolean compareDeprecated,
                                     String targetVersion,
                                     ModelNode legacyModelVersions,
                                     ModelNode legacyResourceDefinitions,
                                     ModelNode currentModelVersions,
                                     ModelNode currentResourceDefinitions,
                                     Set<String> subsystems) throws Exception {
        this.compareDifferentVersions = compareDifferentVersions;
        this.compareRuntime = compareRuntime;
        this.targetVersion = targetVersion;
        this.legacyModelVersions = legacyModelVersions;
        this.legacyResourceDefinitions = legacyResourceDefinitions;
        this.currentModelVersions = currentModelVersions;
        this.currentResourceDefinitions = currentResourceDefinitions;
        this.subsystems = subsystems;
        this.compareDeprecated = compareDeprecated;
        this.out = System.out;
    }

    public static void main(String[] args) throws Exception {

        if (CompareModelVersionsUtil.class.getProtectionDomain().getCodeSource().getLocation().toString().endsWith(".jar")) {
            throw new Exception("This currently does not work as a jar. Please import a clone of https://github.com/kabir/wildfly-legacy-test into your IDE");
        }

        String version = System.getProperty("jboss.as.compare.version", null);
        String fromTgt = System.getProperty("jboss.as.compare.from.target", null);
        String differentVersions = System.getProperty("jboss.as.compare.different.versions", null);
        String type = System.getProperty("jboss.as.compare.type", null);
        String runtime = System.getProperty("jboss.as.compare.runtime", null);
        String deprecated = System.getProperty("jboss.as.compare.deprecated", null);
        Set<String> subsystems = parseList(System.getProperty("jboss.as.compare.subsystems", null));

        if (version == null) {
            System.out.print("Enter legacy EAP version: ");
            version = readInput(null);
        }
        System.out.println("Using target model: " + version);

        if (type == null) {
            System.out.print("Enter type [S](standalone)/H(host)/D(domain)/F(domain + host):");
            type = readInput("S");
        }
        final ResourceType[] resourceTypes;
        if (ResourceType.STANDALONE.toString().startsWith(type.toUpperCase())) {
            resourceTypes = new ResourceType[]{ResourceType.STANDALONE};
        } else if (ResourceType.HOST.toString().startsWith(type.toUpperCase())) {
            resourceTypes = new ResourceType[]{ResourceType.HOST};
        } else if (ResourceType.DOMAIN.toString().startsWith(type.toUpperCase())) {
            resourceTypes = new ResourceType[]{ResourceType.DOMAIN};
        } else if (type.toUpperCase().equals("F")) {
            resourceTypes = new ResourceType[]{ResourceType.DOMAIN, ResourceType.HOST};
        } else {
            throw new IllegalArgumentException("Could not determine type for: '" + type + "'");
        }

        if (fromTgt == null) {
            System.out.print("Read from target directory or from the legacy-models directory - t/[l]:");
            fromTgt = readInput("l");
        }

        final Path fromDirectory;
        if (fromTgt.equals("l")) {
            //File projectDir = Tools.getProjectDirectory();
            URL legacyModels = Thread.currentThread().getContextClassLoader().getResource("legacy-models");
            fromDirectory = Paths.get(legacyModels.toURI());
        } else if (fromTgt.equals("t")) {
            fromDirectory = Tools.getProjectDirectory().resolve("target");
        } else {
            throw new IllegalArgumentException("Please enter 'l' for legacy-models directory or 't' for target directory");
        }

        if (differentVersions == null) {
            System.out.print("Report on differences in the model when the management versions are different? [y]/n: ");
            differentVersions = readInput("y").toLowerCase();
        }
        boolean compareDifferentVersions;
        if (differentVersions.equals("n")) {
            System.out.println("Not reporting on differences in the model when the management versions are different");
            compareDifferentVersions = false;
        } else if (differentVersions.equals("y")) {
            System.out.println("Reporting on differences in the model when the management versions are different");
            compareDifferentVersions = true;
        } else {
            throw new IllegalArgumentException("Please enter 'y' or 'n'");
        }

        if (runtime == null) {
            System.out.print("Report on differences in the model of runtime resources/attributes? y/[n]: ");
            runtime = readInput("n").toLowerCase();
        }
        boolean compareRuntime;
        if (runtime.equals("n")) {
            System.out.println("Not reporting on differences in the model for runtime resources/attributes.");
            compareRuntime = false;
        } else if (runtime.equals("y")) {
            System.out.println("Reporting on differences in the model for runtime resources/attributes.");
            compareRuntime = true;
        } else {
            throw new IllegalArgumentException("Please enter 'y' or 'n'");
        }

        if (deprecated == null) {
            System.out.print("Report on differences of deprecation versions for resources/attributes? y/[n]: ");
            deprecated = readInput("n").toLowerCase();
        }
        boolean compareDeprecated;
        if (deprecated.equals("n")) {
            System.out.println("Not reporting on differences of deprecation versions for resources/attributes.");
            compareDeprecated = false;
        } else if (differentVersions.equals("y")) {
            System.out.println("Reporting on differences of deprecation versions for resources/attributes.");
            compareDeprecated = true;
        } else {
            throw new IllegalArgumentException("Please enter 'y' or 'n'");
        }

        if (subsystems == null) {
            System.out.print("If you only want to report on differences of certain subsystems enter their names as a comma-separated list (e.g. 'ejb3,jmx'): ");
            subsystems = parseList(readInput(""));
        }

        System.out.println("Loading legacy model versions for " + version + "....");
        ModelNode legacyModelVersions = Tools.loadModelNodeFromFile(fromDirectory.resolve("standalone-model-versions-" + version + ".dmr"));
        System.out.println("Loaded legacy model versions");

        System.out.println("Loading model versions for currently running server...");
        ModelNode currentModelVersions = Tools.getCurrentModelVersions();
        System.out.println("Loaded current model versions");

        for (ResourceType resourceType : resourceTypes) {
            doCompare(resourceType, fromDirectory, compareDifferentVersions, compareRuntime, compareDeprecated, version, legacyModelVersions, currentModelVersions, subsystems);
        }
    }

    private static void doCompare(ResourceType resourceType,
                                  Path fromDirectory,
                                  boolean compareDifferentVersions,
                                  boolean compareRuntime,
                                  boolean compareDeprecated,
                                  String targetVersion,
                                  ModelNode legacyModelVersions,
                                  ModelNode currentModelVersions,
                                  Set<String> subsystems) throws Exception {
        System.out.println("Loading legacy resource descriptions for " + targetVersion + "....");
        ModelNode legacyResourceDefinitions = Tools.loadModelNodeFromFile(fromDirectory.resolve(resourceType.toString().toLowerCase() + "-resource-definition-" + targetVersion + ".dmr"));
        System.out.println("Loaded legacy resource descriptions");


        System.out.println("Loading resource descriptions for currently running " + resourceType + "...");
        final ModelNode currentResourceDefinitions;
        if (resourceType == ResourceType.STANDALONE) {
            currentResourceDefinitions = Tools.getCurrentRunningResourceDefinition(PathAddress.EMPTY_ADDRESS);
        } else if (resourceType == ResourceType.DOMAIN) {
            currentResourceDefinitions = Tools.getCurrentRunningDomainResourceDefinition();
        } else {
            currentResourceDefinitions = Tools.getCurrentRunningResourceDefinition(PathAddress.pathAddress(PathElement.pathElement(HOST, "master")));
        }
        System.out.println("Loaded current resource descriptions");

        CompareModelVersionsUtil compareModelVersionsUtil = new CompareModelVersionsUtil(compareDifferentVersions, compareRuntime,
                compareDeprecated, targetVersion, legacyModelVersions, legacyResourceDefinitions, currentModelVersions,
                currentResourceDefinitions, subsystems);

        System.out.println("Starting comparison of the current....\n");
        compareModelVersionsUtil.compareModels();
        System.out.println("\nDone comparison of " + resourceType + "!");
    }

    private static String readInput(String defaultAnswer) throws IOException {
        StringBuilder sb = new StringBuilder();
        char c = (char) System.in.read();
        while (c != '\n') {
            sb.append(c);
            c = (char) System.in.read();
        }
        String s = sb.toString().trim();
        if (s.equals("")) {
            if (defaultAnswer != null) {
                return defaultAnswer;
            }
            throw new IllegalArgumentException("Please enter a valid answer");
        }
        return s;
    }

    private void compareModels() {
        if (subsystems == null) {
            compareCoreModels();
        }
        compareSubsystemModels();
    }

    private void compareCoreModels() {
        out.println("====== Comparing core models ======");
        ResourceDefinition currentDefinition = new ResourceDefinition(trimSubsystem(currentResourceDefinitions), currentModelVersions);
        ResourceDefinition legacyDefinition = new ResourceDefinition(trimSubsystem(legacyResourceDefinitions), legacyModelVersions);
        CompareContext context = new CompareContext(PathAddress.EMPTY_ADDRESS, PathAddress.EMPTY_ADDRESS, true, currentDefinition, legacyDefinition);
        if (!context.continuteWithCheck()) {
            return;
        }
        compareModel(context);
    }

    private void compareSubsystemModels() {
        out.println("====== Comparing subsystem models ======");
        ResourceDefinition rootCurrentDefinition = new ResourceDefinition(trimNonSubsystem(currentResourceDefinitions), currentModelVersions);
        ResourceDefinition rootLegacyDefinition = new ResourceDefinition(trimNonSubsystem(legacyResourceDefinitions), legacyModelVersions);
        Map<String, ModelNode> currentSubsystems = rootCurrentDefinition.getChildren(SUBSYSTEM);
        Map<String, ModelNode> legacySubsystems = rootLegacyDefinition.getChildren(SUBSYSTEM);

        CompareContext context = new CompareContext(PathAddress.EMPTY_ADDRESS, PathAddress.EMPTY_ADDRESS, true, rootCurrentDefinition, rootLegacyDefinition);
        compareKeySetsAndRemoveMissing(context, "subsystems", currentSubsystems, legacySubsystems);

        for (Map.Entry<String, ModelNode> legacyEntry : legacySubsystems.entrySet()) {
            if (subsystems != null && !subsystems.contains(legacyEntry.getKey())) {
                continue;
            }
            PathAddress subsystemAddress = PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, legacyEntry.getKey()));
            ResourceDefinition currentDefinition = new ResourceDefinition(currentSubsystems.get(legacyEntry.getKey()), currentModelVersions);
            ResourceDefinition legacyDefinition = new ResourceDefinition(legacyEntry.getValue(), legacyModelVersions);
            context = new CompareContext(subsystemAddress, subsystemAddress, false, currentDefinition, legacyDefinition);
            if (!context.continuteWithCheck()) {
                continue;
            }
            compareModel(context);
        }
    }


    private ModelNode trimSubsystem(ModelNode definition) {
        ModelNode def = definition.clone();
        def.get(CHILDREN).remove(SUBSYSTEM);
        return def;
    }

    private ModelNode trimNonSubsystem(ModelNode definition) {
        ModelNode def = definition.clone();
        for (String key : def.get(CHILDREN).keys()) {
            if (!key.equals(SUBSYSTEM)) {
                def.remove(key);
            }
        }
        return def;
    }

    private void compareModel(CompareContext context) {
        //out.println("---->  " + context.getPathAddress());
        if (!compareRuntime) {
            if (context.getCurrentDefinition().isRuntime() && context.getLegacyDefinition().isRuntime()) {
                return;
            }
        }
        compareAttributes(context);
        compareOperations(context);
        compareChildren(context);
    }

    private void compareAttributes(CompareContext context) {
        Map<String, ModelNode> legacyAttributes = context.getLegacyDefinition().getAttributes();
        Map<String, ModelNode> currentAttributes = context.getCurrentDefinition().getAttributes();

        compareKeySetsAndRemoveMissing(context, ATTRIBUTES, currentAttributes, legacyAttributes);
        //TODO compare types, expressions etc.

        for (Map.Entry<String, ModelNode> legacyEntry : legacyAttributes.entrySet()) {
            ModelNode legacyAttribute = legacyEntry.getValue();
            ModelNode currentAttribute = currentAttributes.get(legacyEntry.getKey());

            String id = "attribute '" + legacyEntry.getKey() + "'";
            compareAttributeOrOperationParameter(context, id, currentAttribute, legacyAttribute);
            compareAccessType(context, id, currentAttribute, legacyAttribute);
            compareStorage(context, id, currentAttribute, legacyAttribute);
            compareDefault(context, id, currentAttribute, legacyAttribute);
            compareUnit(context, id, currentAttribute, legacyAttribute);
        }
    }

    private void compareOperations(CompareContext context) {
        Map<String, ModelNode> legacyOperations = context.getLegacyDefinition().getOperations();
        Map<String, ModelNode> currentOperations = context.getCurrentDefinition().getOperations();

        compareKeySetsAndRemoveMissing(context, "operations", currentOperations, legacyOperations);

        for (Map.Entry<String, ModelNode> legacyOpEntry : legacyOperations.entrySet()) {
            String operationName = legacyOpEntry.getKey();
            ModelNode legacyOperation = legacyOpEntry.getValue();
            ModelNode currentOperation = currentOperations.get(operationName);

            Map<String, ModelNode> legacyParameters = context.getLegacyDefinition().getOperationParameters(operationName);
            Map<String, ModelNode> currentParameters = context.getCurrentDefinition().getOperationParameters(operationName);

            compareKeySetsAndRemoveMissing(context, "parameters for operation '" + operationName + "'", currentParameters, legacyParameters);

            for (Map.Entry<String, ModelNode> legacyParamEntry : legacyParameters.entrySet()) {
                ModelNode legacyParameter = legacyParamEntry.getValue();
                ModelNode currentParameter = currentParameters.get(legacyParamEntry.getKey());

                String id = "parameter '" + legacyParamEntry.getKey() + "' of operation '" + operationName + "'";
                compareAttributeOrOperationParameter(context, id, currentParameter, legacyParameter);
            }

            ModelNode legacyReply = legacyOperation.get(REPLY_PROPERTIES);
            ModelNode currentReply = currentOperation.get(REPLY_PROPERTIES);
            compareAttributeOrOperationParameter(context, "'reply-properties' for operation '" + operationName + "'", currentReply, legacyReply);
//            if (!currentReply.equals(legacyReply)) {
//                context.println("Different 'reply-properties' for operation '" + operationName + "'. Current: " + currentReply + "; legacy: " + legacyReply);
//            }
        }
    }

    private void compareAttributeOrOperationParameter(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        compareType(context, id, current, legacy);
        compareValueType(context, id, current, legacy);
        compareNillable(context, id, current, legacy);
        compareExpressionsAllowed(context, id, current, legacy);
        compareAlternatives(context, id, current, legacy);
        if (compareDeprecated) {
            compareDeprecated(context, id, current, legacy);
        }
        //TODO compare anything else?
    }

    private void compareType(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(TYPE).equals(legacy.get(TYPE))) {
            context.println("Different 'type' for " + id + ". Current: " + current.get(TYPE) + "; legacy: " + legacy.get(TYPE));
        }
    }

    private void compareValueType(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        ModelNode currentValueType = current.get(VALUE_TYPE);
        ModelNode legacyValueType = legacy.get(VALUE_TYPE);
        if (!currentValueType.isDefined() && !legacyValueType.isDefined()) {
            return;
        }
        if (isType(legacyValueType) || isType(currentValueType)) {
            if (!currentValueType.equals(legacyValueType)) {
                context.println("Different 'value-type' for " + id + ". Current: " + current.get(VALUE_TYPE) + "; legacy: " + legacy.get(VALUE_TYPE));
            }
        } else {
            Map<String, ModelNode> legacyValueTypes = createMapIndexedByKey(legacyValueType);
            Map<String, ModelNode> currentValueTypes = createMapIndexedByKey(currentValueType);

            compareKeySetsAndRemoveMissing(context, "value-type for " + id, currentValueTypes, legacyValueTypes);
            for (Map.Entry<String, ModelNode> entry : currentValueTypes.entrySet()) {
                ModelNode currentEntry = entry.getValue();
                ModelNode legacyEntry = legacyValueTypes.get(entry.getKey());
                compareAttributeOrOperationParameter(context, "value-type key '" + entry.getKey() + "' for " + id, currentEntry, legacyEntry);
            }
        }
    }

    private void compareNillable(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        boolean currentNillable = current.get(NILLABLE).asBoolean(false);
        boolean legacyNillable = legacy.get(NILLABLE).asBoolean(false);
        boolean hasAlternativeCurrent = current.hasDefined(ALTERNATIVES);
        
        if (currentNillable != legacyNillable && !hasAlternativeCurrent) {
            if (currentNillable){//than legacy wasn't
                context.print("* ");
            }
            context.println("Different 'nillable' for " + id + ". Current: " + currentNillable + "; legacy: " + legacyNillable);
        }
    }

    private void compareExpressionsAllowed(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        boolean currentNillable = current.get(EXPRESSIONS_ALLOWED).asBoolean(false);
        boolean legacyNillable = legacy.get(EXPRESSIONS_ALLOWED).asBoolean(false);
        if (currentNillable != legacyNillable) {
            context.println("Different 'expressions-allowed' for " + id + ". Current: " + currentNillable + "; legacy: " + legacyNillable);
        }
    }

    private void compareAccessType(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(ACCESS_TYPE).equals(legacy.get(ACCESS_TYPE))) {
            context.println("Different 'access-type' for " + id + ". Current: " + current.get(ACCESS_TYPE) + "; legacy: " + legacy.get(ACCESS_TYPE));
        }
    }

    private void compareStorage(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(STORAGE).equals(legacy.get(STORAGE))) {
            context.println("Different 'storage' for " + id + ". Current: " + current.get(STORAGE) + "; legacy: " + legacy.get(STORAGE));
        }
    }

    private void compareDefault(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(DEFAULT).equals(legacy.get(DEFAULT))) {
            if (legacy.hasDefined(DEFAULT)){
                context.print("* ");
            }
            context.println("Different 'default' for " + id + ". Current: " + current.get(DEFAULT) + "; legacy: " + legacy.get(DEFAULT));
        }
    }

    private void compareDeprecated(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(DEPRECATED).equals(legacy.get(DEPRECATED))) {
            context.println("Different 'deprecated' for " + id + ". Current: " + current.get(DEPRECATED) + "; legacy: " + legacy.get(DEPRECATED));
        }
    }

    private void compareUnit(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(UNIT).equals(legacy.get(UNIT))) {
            context.println("Different 'unit' for " + id + ". Current: " + current.get(UNIT) + "; legacy: " + legacy.get(UNIT));
        }
    }

    private boolean isType(ModelNode node) {
        if (!node.isDefined()) {
            return false;
        }
        try {
            node.asType();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private Map<String, ModelNode> createMapIndexedByKey(ModelNode node) {
        Map<String, ModelNode> map = new HashMap<>();
        if (!node.isDefined()) {
            return map;
        }
        for (Property prop : node.asPropertyList()) {
            map.put(prop.getName(), prop.getValue());
        }
        return map;
    }

    private void compareAlternatives(CompareContext context, String id, ModelNode current, ModelNode legacy) {
        if (!current.get(ALTERNATIVES).equals(legacy.get(ALTERNATIVES))) {
            context.println("Different 'alternatives' for " + id + ". Current: " + current.get(ALTERNATIVES) + "; legacy: " + legacy.get(ALTERNATIVES));
        }
    }

    private void compareChildren(CompareContext context) {
        Set<String> legacyChildTypes = context.getLegacyDefinition().getChildTypes();
        Set<String> currentChildTypes = context.getCurrentDefinition().getChildTypes();

        compareSetsAndRemoveMissing(context, "child types", currentChildTypes, legacyChildTypes);

        for (String type : legacyChildTypes) {
            Map<String, ModelNode> legacyChildren = context.getLegacyDefinition().getChildren(type);
            Map<String, ModelNode> currentChildren = context.getCurrentDefinition().getChildren(type);

            compareKeySetsAndRemoveMissing(context, "child names for type=" + type, currentChildren, legacyChildren);

            for (Map.Entry<String, ModelNode> legacyChildEntry : legacyChildren.entrySet()) {
                String name = legacyChildEntry.getKey();
                ModelNode legacyChildDescription = legacyChildEntry.getValue();
                ModelNode currentChildDescription = currentChildren.get(name);

                CompareContext childContext;
                try {
                    childContext = new CompareContext(
                            context.getRootAddress(),
                            context.getPathAddress().append(PathElement.pathElement(type, name)),
                            context.isCore(),
                            new ResourceDefinition(currentChildDescription, currentModelVersions),
                            new ResourceDefinition(legacyChildDescription, legacyModelVersions));
                } catch (RuntimeException e) {
                    out.println(context.getPathAddress() + " + " + type + "=" + name);
                    throw e;
                }
                compareModel(childContext);
            }
        }
    }

    private void compareKeySetsAndRemoveMissing(CompareContext context, String type, Map<String, ModelNode> currentMap, Map<String, ModelNode> legacyMap) {
        compareSetsAndRemoveMissing(context, type, currentMap.keySet(), legacyMap.keySet());
    }

    private void compareSetsAndRemoveMissing(CompareContext context, String type, Set<String> currentSet, Set<String> legacySet) {
        Set<String> extraInLegacy = getMissingNames(context, legacySet, currentSet);
        Set<String> extraInCurrent = getMissingNames(context, currentSet, legacySet);

        Set<String> extraInLegacyAfterRuntime = new HashSet<>(extraInLegacy);
        Set<String> extraInCurrentAfterRuntime = new HashSet<>(extraInCurrent);
        if (extraInLegacy.size() > 0 || extraInCurrent.size() > 0) {
            if (!compareRuntime && type.equals(ATTRIBUTES)) {
                extraInLegacyAfterRuntime = trimRuntimeAttributes(context.getLegacyDefinition().getAttributes(), extraInLegacyAfterRuntime);
                extraInCurrentAfterRuntime = trimRuntimeAttributes(context.getCurrentDefinition().getAttributes(), extraInCurrentAfterRuntime);
            }
        }

        if (extraInLegacyAfterRuntime.size() > 0 || extraInCurrentAfterRuntime.size() > 0) {
            context.println("* Missing " + type +
                    " in current: " + extraInLegacyAfterRuntime + "; missing in legacy " + extraInCurrentAfterRuntime);
        }

        if (extraInCurrent.size() > 0) {
            currentSet.removeAll(extraInCurrent);
        }
        if (extraInLegacy.size() > 0) {
            legacySet.removeAll(extraInLegacy);
        }
    }

    private Set<String> trimRuntimeAttributes(Map<String, ModelNode> attributes, Set<String> set) {
        Set<String> runtime = new HashSet<>(set);
        for (String name : set) {
            ModelNode desc = attributes.get(name);
            if (desc.hasDefined(STORAGE) && desc.get(STORAGE).asString().equals(Storage.RUNTIME.toString())) {
                runtime.remove(name);
            }
        }
        return runtime;
    }

    private Set<String> getMissingNames(CompareContext context, Set<String> possiblyMissing, Set<String> names) {
        Set<String> missing = new HashSet<>(possiblyMissing);
        for (String name : names) {
            missing.remove(name);
        }

        //7.1.2 did not have MANAGEMENT_MICRO_VERSION don't bother reporting that
        if (context.isVersionLevel() && missing.contains(MANAGEMENT_MICRO_VERSION) && names.contains(MANAGEMENT_MAJOR_VERSION) && names.contains(MANAGEMENT_MINOR_VERSION)) {
            missing.remove(MANAGEMENT_MICRO_VERSION);
        }

        return missing;
    }

    private class CompareContext {
        final PathAddress rootAddress;
        final PathAddress pathAddress;
        final boolean core;
        final ResourceDefinition legacyDefinition;
        final ResourceDefinition currentDefinition;
        boolean outputPath;

        CompareContext(PathAddress rootAddress, PathAddress pathAddress, boolean core, ResourceDefinition currentDefinition, ResourceDefinition legacyDefinition) {
            this.rootAddress = rootAddress;
            this.pathAddress = pathAddress;
            this.core = core;
            this.currentDefinition = currentDefinition;
            this.legacyDefinition = legacyDefinition;
        }

        PathAddress getRootAddress() {
            return rootAddress;
        }


        PathAddress getPathAddress() {
            return pathAddress;
        }

        boolean isVersionLevel() {
            return rootAddress.equals(pathAddress);
        }

        boolean isCore() {
            return core;
        }

        ResourceDefinition getLegacyDefinition() {
            return legacyDefinition;
        }

        ResourceDefinition getCurrentDefinition() {
            return currentDefinition;
        }

        boolean continuteWithCheck() {
            if (!isVersionLevel()) {
                return true;
            }
            ModelVersion currentVersion = getModelVersion(currentDefinition);
            ModelVersion legacyVersion = getModelVersion(legacyDefinition);
            out.println("====== Resource root address: " + formatAddressOneLine(pathAddress) + " - Current version: " + currentVersion + "; legacy version: " + legacyVersion + " =======");

            if (!legacyVersion.equals(currentVersion) && compareDifferentVersions) {
                return true;
            } else if (legacyVersion.equals(currentVersion)) {
                return true;
            } else {
                out.println("Skipping check of resource and children");
                return false;
            }
        }

        private ModelVersion getModelVersion(ResourceDefinition definition) {
            if (core) {
                return definition.getCoreModelVersion();
            } else {
                return definition.getSubsystemVersion(pathAddress);
            }
        }

        private String formatAddressOneLine(PathAddress addr) {
            StringBuilder sb = new StringBuilder("[");
            boolean first = true;
            for (PathElement element : addr) {
                if (first) {
                    first = false;
                } else {
                    sb.append(",");
                }
                sb.append(element);
            }
            sb.append("]");
            return sb.toString();
        }

        void print(String msg) {
            out.print(msg);
        }
        void println(String msg) {
            if (!outputPath) {
                outputPath = true;
                PathAddress relative = pathAddress.subAddress(rootAddress.size());
                out.println("--- Problems for relative address to root " + formatAddressOneLine(relative) + ":");
            }
            out.println(msg);
        }
    }


    private static class ResourceDefinition {
        final ModelNode description;
        final ModelNode versions;

        ResourceDefinition(ModelNode description, ModelNode versions) {
            this.description = description;
            this.versions = versions;
        }


        Map<String, ModelNode> getAttributes() {
            return getSortedEntryMap(description, ATTRIBUTES);
        }

        Map<String, ModelNode> getOperations() {
            return getSortedEntryMap(description, OPERATIONS);
        }

        Set<String> getChildTypes() {
            return getSortedEntryMap(description, CHILDREN).keySet();
        }

        Map<String, ModelNode> getChildren(String type) {
            return getSortedEntryMap(description.get(CHILDREN, type), MODEL_DESCRIPTION);
        }

        Map<String, ModelNode> getOperationParameters(String opName) {
            return getSortedEntryMap(description.get(OPERATIONS, opName), REQUEST_PROPERTIES);
        }

        private Map<String, ModelNode> getSortedEntryMap(ModelNode parent, String name) {
            if (!parent.hasDefined(name)) {
                return Collections.emptyMap();
            }
            Map<String, ModelNode> sorted = new TreeMap<>();
            for (Property prop : parent.get(name).asPropertyList()) {
                sorted.put(prop.getName(), prop.getValue());
            }
            return sorted;
        }

        private ModelVersion getCoreModelVersion() {
            return Tools.createModelVersion(versions.get(Tools.CORE, Tools.STANDALONE));
        }

        private ModelVersion getSubsystemVersion(PathAddress address) {
            for (PathElement element : address) {
                if (element.getKey().equals(SUBSYSTEM)) {
                    return Tools.createModelVersion(versions.get(SUBSYSTEM, element.getValue()));
                }
            }
            throw new IllegalArgumentException("Could not find subsystem version for " + address);
        }

        boolean isRuntime() {
            return description.hasDefined(STORAGE) && description.get(STORAGE).asString().equals("runtime");
        }
    }

    private static Set<String> parseList(String s) {
        if (s == null) {
            return null;
        }
        String[] elements = s.trim().split(",");
        Set<String> set = null;
        for (String element : elements) {
            element = element.trim();
            if (element.length() > 0) {
                if (set == null) {
                    set = new HashSet<>();
                }
                set.add(element.trim());
            }
        }
        return set;
    }

}
