/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.cli.handlers;

import static org.wildfly.common.Assert.checkNotNullParam;
import static org.wildfly.common.Assert.checkNotEmptyParam;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.as.cli.ArgumentValueConverter;
import org.jboss.as.cli.CliEvent;
import org.jboss.as.cli.CommandArgument;
import org.jboss.as.cli.CommandContext;
import org.jboss.as.cli.CommandFormatException;
import org.jboss.as.cli.CommandHandler;
import org.jboss.as.cli.CommandLineCompleter;
import org.jboss.as.cli.CommandLineException;
import org.jboss.as.cli.ModelNodeFormatter;
import org.jboss.as.cli.OperationCommand;
import org.jboss.as.cli.Util;
import org.jboss.as.cli.impl.ArgumentWithValue;
import org.jboss.as.cli.impl.ArgumentWithoutValue;
import org.jboss.as.cli.impl.DefaultCompleter;
import org.jboss.as.cli.impl.DefaultCompleter.CandidatesProvider;
import org.jboss.as.cli.operation.OperationFormatException;
import org.jboss.as.cli.operation.OperationRequestAddress;
import org.jboss.as.cli.operation.ParsedCommandLine;
import org.jboss.as.cli.operation.impl.DefaultOperationRequestAddress;
import org.jboss.as.cli.operation.impl.DefaultOperationRequestBuilder;
import org.jboss.as.cli.util.SimpleTable;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.dmr.Property;


/**
 *
 * @author Alexey Loubyansky
 */
public class GenericTypeOperationHandler extends BatchModeCommandHandler {

    private static final int DASH_OFFSET = 22;

    protected final String commandName;
    protected final String idProperty;
    protected final String nodeType;
    protected final ArgumentWithValue profile;
    protected final ArgumentWithValue name;
    protected final ArgumentWithValue operation;

    protected final Set<String> excludedOps;

    // help arguments
    protected final ArgumentWithoutValue helpProperties;
    protected final ArgumentWithoutValue helpCommands;

    // these are caching vars
    private final Map<String, CommandArgument> staticArgs = new HashMap<String, CommandArgument>();

    private Map<String, ArgumentValueConverter> propConverters;
    private Map<String, CommandLineCompleter> valueCompleters;

    private Map<String, OperationCommandWithDescription> customHandlers;

    private WritePropertyHandler writePropHandler;
    private Map<String, OperationCommand> opHandlers;

    private final boolean isChildNode;

    /**
     * Generic command constructor.
     *
     * @param ctx The CommandContext.
     * @param nodeType The resource type. This type is used to name the command.
     * @param idProperty The property name that identify the resource.
     */
    public GenericTypeOperationHandler(CommandContext ctx, String nodeType, String idProperty) {
        this(null, ctx, nodeType, idProperty);
    }

    /**
     * Generic command constructor.
     *
     * @param commandName The name of the command. If null, nodeType is used.
     * @param ctx The CommandContext.
     * @param nodeType The resource type.
     * @param idProperty The property name that identify the resource.
     */
    public GenericTypeOperationHandler(String commandName, CommandContext ctx, String nodeType, String idProperty) {
        this(commandName, ctx, nodeType, idProperty, false);
    }

    /**
     * Generic command constructor.
     *
     * @param commandName The name of the command. If null, nodeType is used.
     * @param ctx The CommandContext.
     * @param nodeType The resource type.
     * @param idProperty The property name that identify the resource.
     * @param isChildNode True is the path references a child node, false if the
     * path references a type node.
     */
    public GenericTypeOperationHandler(String commandName, CommandContext ctx, String nodeType, String idProperty, boolean isChildNode) {
        this(commandName, ctx, nodeType, idProperty, isChildNode, "read-attribute", "read-children-names", "read-children-resources",
                "read-children-types", "read-operation-description", "read-operation-names",
                "read-resource-description", "validate-address", "write-attribute", "undefine-attribute", "whoami");
    }

    /**
     * Generic command constructor.
     *
     * @param ctx The CommandContext.
     * @param nodeType The resource type. This type is used to name the command.
     * @param idProperty The property name that identify the resource.
     * @param excludeOperations The list of operations to exclude.
     */
    public GenericTypeOperationHandler(CommandContext ctx, String nodeType, String idProperty, String... excludeOperations) {
        this(null, ctx, nodeType, idProperty, excludeOperations);
    }

    /**
     * Generic command constructor.
     *
     * @param commandName The name of the command. If null, nodeType is used.
     * @param ctx The CommandContext.
     * @param nodeType The resource type.
     * @param idProperty The property name that identify the resource.
     * @param excludeOperations The list of operations to exclude.
     */
    public GenericTypeOperationHandler(String commandName, CommandContext ctx, String nodeType, String idProperty, String... excludeOperations) {
        this(commandName, ctx, nodeType, idProperty, false, excludeOperations);
    }

    /**
     * Generic command constructor.
     *
     * @param commandName The name of the command. If null, nodeType is used.
     * @param ctx The CommandContext.
     * @param nodeType The resource type.
     * @param isChildNode True is the path references a child node, false if the
     * path references a type node.
     * @param idProperty The property name that identify the resource.
     * @param excludeOperations The list of operations to exclude.
     */
    public GenericTypeOperationHandler(String commandName, CommandContext ctx, String nodeType, String idProperty, boolean isChildNode, String... excludeOperations) {

        super(ctx, "generic-type-operation", true);

        this.isChildNode = isChildNode;

        checkNotEmptyParam("nodeType", nodeType);

        if(nodeType.startsWith("/profile=") || nodeType.startsWith("profile=")) {
            int nextSep = nodeType.indexOf('/', 7);
            if(nextSep < 0) {
                throw new IllegalArgumentException("Failed to determine the path after the profile in '" + nodeType + "'.");
            }
            nodeType = nodeType.substring(nextSep);
            this.nodeType = nodeType;
        } else {
            this.nodeType = nodeType;
        }

        helpArg = new ArgumentWithoutValue(this, "--help", "-h");

        addRequiredPath(nodeType);

        String cmdName = commandName == null ? getRequiredType() : commandName;

        if (isChildNode) {
            if (cmdName == null) {
                cmdName = requiredAddress.getNodeName();
            } else {
                // It means that the getRequiredType returned a value, wrong for child node.
                if (commandName == null) {
                    throw new IllegalArgumentException("The child node path appears to end on a type: '" + nodeType + "'");
                }
            }
        }

        this.commandName = cmdName;
        if (this.commandName == null) {
            throw new IllegalArgumentException("The node path doesn't end on a type: '" + nodeType + "'");
        }

        this.idProperty = idProperty;

        if(excludeOperations != null) {
            this.excludedOps = new HashSet<String>(Arrays.asList(excludeOperations));
            if (isChildNode) {
                this.excludedOps.add(Util.ADD);
            }
        } else {
            if (isChildNode) {
                excludedOps = new HashSet<>();
                excludedOps.add(Util.ADD);
            } else {
                excludedOps = Collections.emptySet();
            }
        }

        profile = new ArgumentWithValue(this, new DefaultCompleter(new CandidatesProvider(){
            @Override
            public List<String> getAllCandidates(CommandContext ctx) {
                return Util.getNodeNames(ctx.getModelControllerClient(), null, Util.PROFILE);
            }}), "--profile") {
            @Override
            public boolean canAppearNext(CommandContext ctx) throws CommandFormatException {
                if(!isDependsOnProfile()) {
                    return false;
                }
                if(!ctx.isDomainMode()) {
                    return false;
                }
                return super.canAppearNext(ctx);
            }
        };
        //profile.addCantAppearAfter(helpArg);

        operation = new ArgumentWithValue(this, new DefaultCompleter(new CandidatesProvider(){
                @Override
                public Collection<String> getAllCandidates(CommandContext ctx) {
                    DefaultOperationRequestAddress address = new DefaultOperationRequestAddress();
                    if(isDependsOnProfile() && ctx.isDomainMode()) {
                        final String profileName = profile.getValue(ctx.getParsedCommandLine());
                        if(profileName == null) {
                            return Collections.emptyList();
                        }
                        address.toNode(Util.PROFILE, profileName);
                    }
                    for(OperationRequestAddress.Node node : getRequiredAddress()) {
                        address.toNode(node.getType(), node.getName());
                    }
                    if (!isChildNode) {
                        address.toNode(getRequiredType(), "?");
                    }
                    Collection<String> ops = Util.getOperationNames(ctx, address);
                    ops.removeAll(excludedOps);
                    if(customHandlers != null) {
                        if(ops.isEmpty()) {
                            ops = customHandlers.keySet();
                        } else {
                            ops = new HashSet<String>(ops);
                            for(Map.Entry<String,OperationCommandWithDescription> entry : customHandlers.entrySet()) {
                                if(entry.getValue().isAvailable(ctx)) {
                                    ops.add(entry.getKey());
                                } else {
                                    ops.remove(entry.getKey()); // in case custom handler overrides the default op
                                }
                            }
                        }
                    }
                    return ops;
                }}), 0, "--operation") {
            @Override
            public boolean canAppearNext(CommandContext ctx) throws CommandFormatException {
                if(isDependsOnProfile() && ctx.isDomainMode() && !profile.isValueComplete(ctx.getParsedCommandLine())) {
                    return false;
                }
                return super.canAppearNext(ctx);
            }
        };
        operation.addCantAppearAfter(helpArg);

        name = new ArgumentWithValue(this, new DefaultCompleter(new DefaultCompleter.CandidatesProvider() {
            @Override
            public List<String> getAllCandidates(CommandContext ctx) {
                ModelControllerClient client = ctx.getModelControllerClient();
                if (client == null || isChildNode) {
                    return Collections.emptyList();
                }
                DefaultOperationRequestAddress address = new DefaultOperationRequestAddress();
                if(isDependsOnProfile() && ctx.isDomainMode()) {
                    if(profile == null) {
                        return Collections.emptyList();
                    }
                    final String profileName = profile.getValue(ctx.getParsedCommandLine());
                    address.toNode(Util.PROFILE, profileName);
                }
                for(OperationRequestAddress.Node node : getRequiredAddress()) {
                    address.toNode(node.getType(), node.getName());
                }
                return Util.getNodeNames(ctx.getModelControllerClient(), address, getRequiredType());
                }
            }), (idProperty == null ? "--name" : "--" + idProperty)) {
            @Override
            public boolean canAppearNext(CommandContext ctx) throws CommandFormatException {
                if ((isDependsOnProfile() && ctx.isDomainMode() && !profile.isValueComplete(ctx.getParsedCommandLine())) || isChildNode) {
                    return false;
                }
                return super.canAppearNext(ctx);
            }
        };
        name.addCantAppearAfter(helpArg);

        helpArg.addCantAppearAfter(name);

        helpProperties = new ArgumentWithoutValue(this, "--properties");
        helpProperties.addRequiredPreceding(helpArg);
        helpProperties.addCantAppearAfter(operation);

        helpCommands = new ArgumentWithoutValue(this, "--commands");
        helpCommands.addRequiredPreceding(helpArg);
        helpCommands.addCantAppearAfter(operation);
        helpCommands.addCantAppearAfter(helpProperties);
        helpProperties.addCantAppearAfter(helpCommands);


        ///
        staticArgs.put(helpArg.getFullName(), helpArg);
        staticArgs.put(helpCommands.getFullName(), helpCommands);
        staticArgs.put(helpProperties.getFullName(), helpProperties);
        staticArgs.put(profile.getFullName(), profile);
        staticArgs.put(name.getFullName(), name);
        staticArgs.put(operation.getFullName(), operation);
    }

    public String getCommandName() {
        return commandName;
    }

    public void addValueConverter(String propertyName, ArgumentValueConverter converter) {
        if(propConverters == null) {
            propConverters = new HashMap<String, ArgumentValueConverter>();
        }
        propConverters.put(propertyName, converter);
    }

    public void addValueCompleter(String propertyName, CommandLineCompleter completer) {
        if(valueCompleters == null) {
            valueCompleters = new HashMap<String, CommandLineCompleter>();
        }
        valueCompleters.put(propertyName, completer);
    }

    public void addHandler(String name, OperationCommandWithDescription handler) {
        if(customHandlers == null) {
            customHandlers = Collections.singletonMap(name, handler);
        } else {
            if (customHandlers.size() == 1) {
                final Map<String, OperationCommandWithDescription> tmp = customHandlers;
                customHandlers = new HashMap<String, OperationCommandWithDescription>();
                customHandlers.putAll(tmp);
            }
            customHandlers.put(name, handler);

        }
    }

    @Override
    public CommandArgument getArgument(CommandContext ctx, String name) {
        final ParsedCommandLine args = ctx.getParsedCommandLine();
        try {
            if(!this.name.isValueComplete(args)) {
                return staticArgs.get(name);
            }
        } catch (CommandFormatException e) {
            return null;
        }
        final String op = operation.getValue(args);
        OperationCommand handler;
        try {
            handler = getHandler(ctx, op);
        } catch (CommandLineException e) {
            return null;
        }
        return handler.getArgument(ctx, name);
    }

    @Override
    public Collection<CommandArgument> getArguments(CommandContext ctx) {
        final ParsedCommandLine args = ctx.getParsedCommandLine();
        List<CommandArgument> arguments = new ArrayList<>();
        try {
            if (isChildNode) {
                arguments.addAll(staticArgs.values());
            } else {
                if (!name.isValueComplete(args)) {
                    return staticArgs.values();
                }
            }
        } catch (CommandFormatException e) {
            return Collections.emptyList();
        }
        final String op = operation.getValue(args);
        OperationCommand handler;
        try {
            handler = getHandler(ctx, op);
        } catch (CommandLineException e) {
            // Return the static arguments in case of child node.
            return arguments;
        }
        arguments.addAll(handler.getArguments(ctx));
        return arguments;
    }

    private OperationCommand getHandler(CommandContext ctx, String op) throws CommandLineException {
        if(op == null) {
            if(writePropHandler == null) {
                writePropHandler = new WritePropertyHandler();
                Iterator<AttributeDescription> props = getNodeProperties(ctx);
                while(props.hasNext()) {
                    final AttributeDescription prop = props.next();
                    if(prop.isWriteAllowed()) {
                        CommandLineCompleter valueCompleter = null;
                        ArgumentValueConverter valueConverter = null;
                        if(propConverters != null) {
                            valueConverter = propConverters.get(prop.getName());
                        }
                        if(valueCompleters != null) {
                            valueCompleter = valueCompleters.get(prop.getName());
                        }
                        if(valueConverter == null) {
                            valueConverter = ArgumentValueConverter.DEFAULT;
                            final ModelType propType = prop.getType();
                            if(propType != null) {
                                if(ModelType.BOOLEAN == propType) {
                                    if(valueCompleter == null) {
                                        valueCompleter = SimpleTabCompleter.BOOLEAN;
                                    }
                                } else if(ModelType.STRING == propType) {
                                    valueConverter = ArgumentValueConverter.NON_OBJECT;
                                } else if(prop.getName().endsWith("properties")) { // TODO this is bad but can't rely on proper descriptions
                                    valueConverter = ArgumentValueConverter.PROPERTIES;
                                } else if(ModelType.LIST == propType) {
                                    if(asType(prop.getProperty(Util.VALUE_TYPE)) == ModelType.PROPERTY) {
                                        valueConverter = ArgumentValueConverter.PROPERTIES;
                                    } else {
                                        valueConverter = ArgumentValueConverter.LIST;
                                    }
                                }
                            }
                        }
                        final CommandArgument arg = new ArgumentWithValue(GenericTypeOperationHandler.this, valueCompleter, valueConverter, "--" + prop.getName());
                        writePropHandler.addArgument(arg);
                    }
                }
            }
            return writePropHandler;
        } else {
            if(customHandlers != null && customHandlers.containsKey(op)) {
                final OperationCommand opHandler = customHandlers.get(op);
                if(opHandler != null) {
                    return opHandler;
                }
            }

            if(opHandlers != null) {
                OperationCommand opHandler = opHandlers.get(op);
                if(opHandler != null) {
                    return opHandler;
                }
            }

            final ModelNode descr = getOperationDescription(ctx, op);
            if(opHandlers == null) {
                opHandlers = new HashMap<String, OperationCommand>();
            }
            final OpHandler opHandler = new OpHandler(op);
            opHandlers.put(op, opHandler);
            opHandler.addArgument(this.headers);

            if(descr != null && descr.has(Util.REQUEST_PROPERTIES)) {
                final List<Property> propList = descr.get(Util.REQUEST_PROPERTIES).asPropertyList();
                for (Property prop : propList) {
                    final ModelNode propDescr = prop.getValue();
                    CommandLineCompleter valueCompleter = null;
                    ArgumentValueConverter valueConverter = null;
                    if(propConverters != null) {
                        valueConverter = propConverters.get(prop.getName());
                    }
                    if(valueCompleters != null) {
                        valueCompleter = valueCompleters.get(prop.getName());
                    }
                    if(valueConverter == null) {
                        valueConverter = ArgumentValueConverter.DEFAULT;
                        if(propDescr.has(Util.TYPE)) {
                          final ModelType type = propDescr.get(Util.TYPE).asType();
                            if(ModelType.BOOLEAN == type) {
                                if(valueCompleter == null) {
                                    valueCompleter = SimpleTabCompleter.BOOLEAN;
                                }
                            } else if(ModelType.STRING == type) {
                                valueConverter = ArgumentValueConverter.NON_OBJECT;
                            } else if(prop.getName().endsWith("properties")) { // TODO this is bad but can't rely on proper descriptions
                                valueConverter = ArgumentValueConverter.PROPERTIES;
                            } else if(ModelType.LIST == type) {
                                if(propDescr.hasDefined(Util.VALUE_TYPE) && asType(propDescr.get(Util.VALUE_TYPE)) == ModelType.PROPERTY) {
                                    valueConverter = ArgumentValueConverter.PROPERTIES;
                                } else {
                                    valueConverter = ArgumentValueConverter.LIST;
                                }
                            }
                        }
                    }
                    final CommandArgument arg = new ArgumentWithValue(GenericTypeOperationHandler.this, valueCompleter, valueConverter, "--" + prop.getName());
                    opHandler.addArgument(arg);
                }
            }
            return opHandler;
        }
    }

    protected ModelType asType(ModelNode type) {
        if(type == null) {
            return null;
        }
        try {
            return type.asType();
        } catch(IllegalArgumentException e) {
            // the value type is a structure
            return null;
        }
    }

    @Override
    public boolean hasArgument(CommandContext ctx, String name) {
        return true;
    }

    @Override
    public boolean hasArgument(CommandContext ctx, int index) {
        return true;
    }

    @Override
    public void addArgument(CommandArgument arg) {
    }

    @Override
    protected void recognizeArguments(CommandContext ctx) throws CommandFormatException {
        // argument validation is performed during request construction
    }

    @Override
    public ModelNode buildRequestWithoutHeaders(CommandContext ctx) throws CommandFormatException {
        final String operation = this.operation.getValue(ctx.getParsedCommandLine());
        OperationCommand opHandler;
        try {
            opHandler = getHandler(ctx, operation);
        } catch (CommandFormatException e) {
            throw e;
        } catch (CommandLineException e) {
            throw new CommandFormatException("Command is not supported or unavailable in the current context", e);
        }
        return opHandler.buildRequest(ctx);
    }

    @Override
    public void cliEvent(CliEvent event, CommandContext ctx) {
        super.cliEvent(event, ctx);
        if(event == CliEvent.DISCONNECTED) {
            this.opHandlers = null;
            this.writePropHandler = null;
        }
    }

    @Override
    protected void handleResponse(CommandContext ctx, ModelNode opResponse, boolean composite) throws CommandFormatException {
        //System.out.println(opResponse);
        if (!Util.isSuccess(opResponse)) {
            throw new CommandFormatException(Util.getFailureDescription(opResponse));
        }
        final StringBuilder buf = formatResponse(ctx, opResponse, composite, null);
        if(buf != null) {
            ctx.printLine(buf.toString());
        }
    }

    protected StringBuilder formatResponse(CommandContext ctx, ModelNode opResponse, boolean composite, StringBuilder buf) throws CommandFormatException {
        if(opResponse.hasDefined(Util.RESULT)) {
            final ModelNode result = opResponse.get(Util.RESULT);
            if(composite) {
                final Set<String> keys;
                try {
                    keys = result.keys();
                } catch(Exception e) {
                    throw new CommandFormatException("Failed to get step results from a composite operation response " + opResponse);
                }
                for(String key : keys) {
                    final ModelNode stepResponse = result.get(key);
                    buf = formatResponse(ctx, stepResponse, false, buf); // TODO nested composite ops aren't expected for now
                }
            } else {
                final ModelNodeFormatter formatter = ModelNodeFormatter.Factory.forType(result.getType());
                if(buf == null) {
                    buf = new StringBuilder();
                }
                formatter.format(buf, 0, result);
            }
        }
        if(opResponse.hasDefined(Util.RESPONSE_HEADERS)) {
            final ModelNode headers = opResponse.get(Util.RESPONSE_HEADERS);
            final Set<String> keys = headers.keys();
            final SimpleTable table = new SimpleTable(2, ctx.getTerminalWidth());
            for(String key : keys) {
                table.addLine(new String[]{key + ':', headers.get(key).asString()});
            }
            if(buf == null) {
                buf = new StringBuilder();
            } else {
                buf.append(Util.LINE_SEPARATOR);
            }
            table.append(buf, false);
        }
        return buf;
    }

    @Override
    protected void printHelp(CommandContext ctx) throws CommandLineException {

        ParsedCommandLine args = ctx.getParsedCommandLine();
        if(helpProperties.isPresent(args)) {
            printProperties(ctx, getNodeProperties(ctx));
            return;
        }

        if(helpCommands.isPresent(args)) {
            printSupportedCommands(ctx);
            return;
        }

        final String operationName = operation.getValue(args);
        if(operationName == null) {
            printNodeDescription(ctx);
            return;
        }

/*        if(customHandlers != null && customHandlers.containsKey(operationName)) {
            OperationCommand operationCommand = customHandlers.get(operationName);
            operationCommand.handle(ctx);
            return;
        }
*/
        final ModelNode result = getOperationDescription(ctx, operationName);
        if(!result.hasDefined(Util.DESCRIPTION)) {
            throw new CommandLineException("Operation description is not available.");
        }

        ctx.printLine("\nDESCRIPTION:\n");
        formatText(ctx, result.get(Util.DESCRIPTION).asString(), 2);

        if(result.hasDefined(Util.REQUEST_PROPERTIES)) {
            printProperties(ctx, getAttributeIterator(result.get(Util.REQUEST_PROPERTIES).asPropertyList(), null));
        } else {
            printProperties(ctx, Collections.<AttributeDescription>emptyIterator());
        }
    }

    protected void printProperties(CommandContext ctx, Iterator<AttributeDescription> props) {
        final Map<String, StringBuilder> requiredProps = new LinkedHashMap<String,StringBuilder>();
        requiredProps.put(this.name.getFullName(), new StringBuilder().append("Required argument in commands which identifies the instance to execute the command against."));
        final Map<String, StringBuilder> optionalProps = new LinkedHashMap<String, StringBuilder>();

        String accessType = null;
        while(props.hasNext()) {
            AttributeDescription attr = props.next();
            //final ModelNode value = attr.getValue();

            // filter metrics
            accessType = attr.getAccess();
//            if("metric".equals(accessType)) {
//                continue;
//            }

            final boolean required = attr.getBooleanProperty(Util.REQUIRED);
            final StringBuilder descr = new StringBuilder();

            final ModelType modelType = attr.getType();
            final String type = modelType == null? "no type info" : modelType.toString();
            final String attrDescr = attr.getDescription();
            if (attrDescr != null) {
                descr.append('(');
                descr.append(type);
                if(accessType != null) {
                    descr.append(',').append(accessType);
                }
                descr.append(") ");
                descr.append(attrDescr);
            } else if(descr.length() == 0) {
                descr.append("no description.");
            }

            if(required) {
                if(idProperty != null && idProperty.equals(attr.getName())) {
                    if(descr.charAt(descr.length() - 1) != '.') {
                        descr.append('.');
                    }
                    requiredProps.get(this.name.getFullName()).insert(0, ' ').insert(0, descr.toString());
                } else {
                    requiredProps.put("--" + attr.getName(), descr);
                }
            } else {
                optionalProps.put("--" + attr.getName(), descr);
            }
        }

        ctx.printLine("\n");
        if(accessType == null) {
            ctx.printLine("REQUIRED ARGUMENTS:\n");
        }
        for(String argName : requiredProps.keySet()) {
            formatProperty(ctx, argName, requiredProps.get(argName));
        }

        if(!optionalProps.isEmpty()) {
            if(accessType == null ) {
                ctx.printLine("\n\nOPTIONAL ARGUMENTS:\n");
            }
            for(String argName : optionalProps.keySet()) {
                formatProperty(ctx, argName, optionalProps.get(argName));
            }
        }
    }

    public void printDescription(CommandContext ctx) throws CommandFormatException {
        printNodeDescription(ctx);
    }

    protected void printNodeDescription(CommandContext ctx) throws CommandFormatException {

        int offset = 2;

        ctx.printLine("\nSYNOPSIS\n");

        final StringBuilder buf = new StringBuilder();
        buf.append("  ").append(commandName).append(" --help [--properties | --commands] |\n");
        if(isDependsOnProfile() && ctx.isDomainMode()) {
            for(int i = 0; i <= commandName.length() + offset; ++i) {
                buf.append(' ');
            }
            buf.append("--profile=<profile_name>\n");
        }
        for(int i = 0; i <= commandName.length() + offset; ++i) {
            buf.append(' ');
        }
        buf.append('(').append(name.getFullName()).append("=<resource_id> (--<property>=<value>)*) |\n");
        for(int i = 0; i <= commandName.length() + offset; ++i) {
            buf.append(' ');
        }
        buf.append("(<command> ").append(name.getFullName()).append("=<resource_id> (--<parameter>=<value>)*)");

        buf.append('\n');
        for(int i = 0; i <= commandName.length() + offset; ++i) {
            buf.append(' ');
        }
        buf.append("[--headers={<operation_header> (;<operation_header>)*}]");
        ctx.printLine(buf.toString());

        ctx.printLine("\n\nDESCRIPTION\n");

        buf.setLength(0);
        buf.append("The command is used to manage resources of type ");
        buf.append(this.nodeType);
        buf.append(".");
        formatText(ctx, buf, offset);

        ctx.printLine("\n\nRESOURCE DESCRIPTION\n");

        if(isDependsOnProfile() && ctx.isDomainMode() && profile.getValue(ctx.getParsedCommandLine()) == null) {
            buf.setLength(0);
            buf.append("(Execute '");
            buf.append(commandName).append(" --profile=<profile_name> --help' to include the resource description here.)");
            formatText(ctx, buf, offset);
        } else if(ctx.getModelControllerClient() == null) {
            buf.setLength(0);
            buf.append("(Connection to the controller is required to be able to load the resource description)");
            formatText(ctx, buf, offset);
        } else {
            ModelNode request = initRequest(ctx);
            if(request == null) {
                return;
            }
            request.get(Util.OPERATION).set(Util.READ_RESOURCE_DESCRIPTION);
            ModelNode result = null;
            try {
                result = ctx.getModelControllerClient().execute(request);
                if(!result.hasDefined(Util.RESULT)) {
                    throw new CommandFormatException("Node description is not available.");
                }
                result = result.get(Util.RESULT);
                if(!result.hasDefined(Util.DESCRIPTION)) {
                    throw new CommandFormatException("Node description is not available.");
                }
            } catch (Exception e) {
            }

            buf.setLength(0);
            if(result != null) {
                buf.append(result.get(Util.DESCRIPTION).asString());
            } else {
                buf.append("N/A. Please, open a jira issue at https://issues.jboss.org/browse/WFLY to get this fixed. Thanks!");
            }
            formatText(ctx, buf, offset);
        }

        ctx.printLine("\n\nARGUMENTS\n");

        formatProperty(ctx, "--help", "prints this content.");

        formatProperty(ctx, "--help --properties",
                "prints the list of the resource properties including their access-type " +
                "(read/write/metric), value type, and the description.");

        formatProperty(ctx, "--help --commands",
                "prints the list of the commands available for the resource." +
                " To get the complete description of a specific command (including its parameters, " +
                "their types and descriptions), execute " + commandName + " <command> --help.");


        if(isDependsOnProfile() && ctx.isDomainMode()) {
            formatProperty(ctx, "--profile", "the name of the profile the target resource belongs to.");
        }

        buf.setLength(0);
        if(idProperty == null) {
            buf.append("is the name of the resource that completes the path ").append(nodeType).append(" and ");
        } else {
            buf.append("corresponds to a property of the resource which ");
        }
        buf.append("is used to identify the resource against which the command should be executed.");
        formatProperty(ctx, name.getFullName(), buf);

        formatProperty(ctx, "<property>",
                "property name of the resource whose value should be updated. " +
                "For a complete list of available property names, their types and descriptions, execute " +
                commandName + " --help --properties.");

        formatProperty(ctx, "<command>",
                "command name provided by the resource. For a complete list of available commands execute " +
                commandName + " --help --commands.");

        formatProperty(ctx, "<parameter>",
                "parameter name of the <command> provided by the resource. " +
                "For a complete list of available parameter names of a specific <command>, " +
                "their types and descriptions execute " + commandName + " <command> --help.");

        formatProperty(ctx, "--headers",
                "a list of operation headers separated by a semicolon. For the list of supported " +
                "headers, please, refer to the domain management documentation or use tab-completion.");
    }

    protected void formatText(CommandContext ctx, CharSequence text, int offset) {
        int terminalWidth = ctx.getTerminalWidth();
        if(terminalWidth <= 0) {
            terminalWidth = 80;
        }
        final StringBuilder target = new StringBuilder();
        if(offset >= terminalWidth) {
            target.append(text);
        } else {
            int startIndex = 0;
            while(startIndex < text.length()) {
                if(startIndex > 0) {
                    target.append(Util.LINE_SEPARATOR);
                }
                for(int i = 0; i < offset; ++i) {
                    target.append(' ');
                }
                int endIndex = startIndex + terminalWidth - offset;
                if(endIndex > text.length()) {
                    endIndex = text.length();
                    target.append(text.subSequence(startIndex, endIndex));
                    startIndex = endIndex;
                } else {
                    while(endIndex >= startIndex && !Character.isWhitespace(text.charAt(endIndex))) {
                        --endIndex;
                    }
                    if(endIndex <= startIndex) {
                        endIndex = startIndex + terminalWidth - 2;
                        target.append(text.subSequence(startIndex, endIndex));
                        startIndex = endIndex;
                    } else {
                        target.append(text.subSequence(startIndex, endIndex));
                        startIndex = endIndex + 1;
                    }
                }
            }
        }
        ctx.printLine(target.toString());
    }

    protected void formatProperty(CommandContext ctx, String argName, final CharSequence descr) {

        final StringBuilder prop = new StringBuilder();
        prop.append(' ').append(argName);
        int spaces = DASH_OFFSET - prop.length();
        do {
            prop.append(' ');
            --spaces;
        } while(spaces >= 0);

        int terminalWidth = ctx.getTerminalWidth();
        if(terminalWidth <= 0) {
            terminalWidth = 80;
        }

        int dashIndex = prop.length();
        int textOffset = dashIndex + 3;
        int textLength = terminalWidth - textOffset;
        prop.append(" - ");

        if(descr.length() <= textLength) {
            prop.append(descr);
            prop.append(Util.LINE_SEPARATOR);
        } else {
            int lineStart = 0;
            int lineNo = 1;
            while(lineStart < descr.length()) {
                prop.ensureCapacity(terminalWidth);
                if(lineStart > 0) {
                    if(lineNo == 3 && dashIndex > DASH_OFFSET) {
                        textOffset = DASH_OFFSET + 2;
                        textLength = terminalWidth - textOffset;
                    }
                    for(int i = 0; i < textOffset; ++i) {
                        prop.append(' ');
                    }
                }
                int lastCharIndex = lineStart + textLength;

                if(lastCharIndex >= descr.length()) {
                    lastCharIndex = descr.length();
                    prop.append(descr.subSequence(lineStart, lastCharIndex));
                    lineStart = lastCharIndex;
                } else {
                    while(lastCharIndex >= lineStart && !Character.isWhitespace(descr.charAt(lastCharIndex))) {
                        --lastCharIndex;
                    }
                    if(lastCharIndex <= lineStart) {
                        lastCharIndex = lineStart + textLength;
                        prop.append(descr.subSequence(lineStart, lastCharIndex));
                        lineStart = lastCharIndex;
                    } else {
                        prop.append(descr.subSequence(lineStart, lastCharIndex));
                        lineStart = lastCharIndex + 1;
                    }
                }
                prop.append(Util.LINE_SEPARATOR);
                ++lineNo;
            }
        }
        ctx.printLine(prop.toString());
    }

    protected void printSupportedCommands(CommandContext ctx) throws CommandLineException {
        final List<String> list = getSupportedCommands(ctx);
        list.add("To read the description of a specific command execute '" + this.commandName + " command_name --help'.");
        for(String name : list) {
            ctx.printLine(name);
        }
    }

    protected List<String> getSupportedCommands(CommandContext ctx) throws CommandLineException {
        final ModelNode request = initRequest(ctx);
        request.get(Util.OPERATION).set(Util.READ_OPERATION_NAMES);
        if(ctx.getConfig().isAccessControl()) {
            request.get(Util.ACCESS_CONTROL).set(true);
        }
        ModelNode result;
        try {
            result = ctx.getModelControllerClient().execute(request);
        } catch (IOException e) {
            throw new CommandLineException("Failed to load a list of commands.", e);
        }
        if (!result.hasDefined(Util.RESULT)) {
            throw new CommandLineException("Operation names aren't available.");
        }
        final List<ModelNode> nodeList = result.get(Util.RESULT).asList();
        final List<String> supportedCommands = new ArrayList<String>(nodeList.size());
        if(!nodeList.isEmpty()) {
            for(ModelNode node : nodeList) {
                final String opName = node.asString();
                if(!excludedOps.contains(opName) && (customHandlers == null || !customHandlers.containsKey(opName))) {
                    supportedCommands.add(opName);
                }
            }
        }
        if(customHandlers != null) {
            supportedCommands.addAll(customHandlers.keySet());
        }
        Collections.sort(supportedCommands);
        return supportedCommands;
    }

    protected Iterator<AttributeDescription> getNodeProperties(CommandContext ctx) throws CommandLineException {

        if(ctx.getModelControllerClient() == null) {
            throw new CommandLineException("Failed to load target attributes: not connected");
        }
        ModelNode request = initRequest(ctx);
        if(request == null) {
            return Collections.emptyIterator();
        }
        request.get(Util.OPERATION).set(Util.READ_RESOURCE_DESCRIPTION);
        if(ctx.getConfig().isAccessControl()) {
            request.get(Util.ACCESS_CONTROL).set(Util.COMBINED_DESCRIPTIONS);
        }
        ModelNode result;
        try {
            result = ctx.getModelControllerClient().execute(request);
        } catch (IOException e) {
            return Collections.emptyIterator();
        }
        if(!result.hasDefined(Util.RESULT)) {
            return Collections.emptyIterator();
        }
        result = result.get(Util.RESULT);
        if(!result.hasDefined(Util.ATTRIBUTES)) {
            return Collections.emptyIterator();
        }

        final ModelNode accessControl;
        if(ctx.getConfig().isAccessControl()) {
            if(result.has(Util.ACCESS_CONTROL)) {
                accessControl = result.get(Util.ACCESS_CONTROL);
            } else {
                accessControl = null;
            }
        } else {
            accessControl = null;
        }

        return getAttributeIterator(result.get(Util.ATTRIBUTES).asPropertyList(), accessControl);
    }

    protected Iterator<AttributeDescription> getAttributeIterator(final List<Property> props, ModelNode accessControl) {
        final ModelNode attrAccessControl;
        if(accessControl != null) {
            if(accessControl.has(Util.DEFAULT)) {
                final ModelNode def = accessControl.get(Util.DEFAULT);
                if(def.has(Util.ATTRIBUTES)) {
                    attrAccessControl = def.get(Util.ATTRIBUTES);
                } else {
                    attrAccessControl = null;
                }
            } else {
                attrAccessControl = null;
            }
        } else {
            attrAccessControl = null;
        }
        return new Iterator<AttributeDescription>() {

            final Iterator<Property> properties = props.iterator();
            private Property current;
            private AttributeDescription descr = new AttributeDescription() {

                @Override
                public String getName() {
                    return current.getName();
                }

                @Override
                public ModelType getType() {
                    final ModelNode value = getProperty(Util.TYPE);
                    return value == null ? null : value.asType();
                }

                @Override
                public String getAccess() {
                    if(attrAccessControl != null && attrAccessControl.has(current.getName())) {
                        final ModelNode accessSpec = attrAccessControl.get(current.getName());
                        final StringBuilder buf = new StringBuilder();
                        if(accessSpec.get(Util.READ).asBoolean()) {
                            buf.append(Util.READ).append('-');
                        }
                        if(accessSpec.get(Util.WRITE).asBoolean()) {
                            buf.append(Util.WRITE);
                            if(buf.length() == 5) {
                                buf.append("-only");
                            }
                        } else {
                            buf.append("only");
                        }
                        return buf.toString();
                    } else {
                        final ModelNode value = getProperty(Util.ACCESS_TYPE);
                        return value == null ? null : value.asString();
                    }
                }

                @Override
                public boolean isWriteAllowed() {
                    if(attrAccessControl != null && attrAccessControl.has(current.getName())) {
                        final ModelNode accessSpec = attrAccessControl.get(current.getName());
                        if(accessSpec.get(Util.WRITE).asBoolean()) {
                            return true;
                        }
                        return false;
                    }
                    final ModelNode value = getProperty(Util.ACCESS_TYPE);
                    if(value == null) {
                        return false;
                    }
                    return Util.READ_WRITE.equals(value.asString());
                }

                @Override
                public String getDescription() {
                    final ModelNode value = getProperty(Util.DESCRIPTION);
                    return value == null ? null : value.asString();
                }

                @Override
                public ModelNode getProperty(String name) {
                    if(current.getValue().has(name)) {
                        return current.getValue().get(name);
                    }
                    return null;
                }

                @Override
                public boolean getBooleanProperty(String name) {
                    if(current.getValue().has(name)) {
                        return current.getValue().get(name).asBoolean();
                    }
                    return false;
                }
            };
            @Override
            public boolean hasNext() {
                return properties.hasNext();
            }

            @Override
            public AttributeDescription next() {
                current = properties.next();
                return descr;
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    interface AttributeDescription {
        String getName();
        ModelType getType();
        String getAccess();
        boolean isWriteAllowed();
        String getDescription();
        ModelNode getProperty(String name);
        boolean getBooleanProperty(String name);
    }

    protected ModelNode getOperationDescription(CommandContext ctx, String operationName) throws CommandLineException {
        if(customHandlers != null) {
            final OperationCommandWithDescription handler = customHandlers.get(operationName);
            if(handler != null) {
                return handler.getOperationDescription(ctx);
            }
        }
        if(ctx.getModelControllerClient() == null) {
            throw new CommandLineException("Failed to load operation description: not connected");
        }

        ModelNode request = initRequest(ctx);
        if(request == null) {
            return null;
        }
        request.get(Util.OPERATION).set(Util.READ_OPERATION_DESCRIPTION);
        request.get(Util.NAME).set(operationName);
        ModelNode result;
        try {
            result = ctx.getModelControllerClient().execute(request);
        } catch (IOException e) {
            throw new CommandFormatException("Failed to execute read-operation-description.", e);
        }
        if (!result.hasDefined(Util.RESULT)) {
            String msg = Util.getFailureDescription(result);
            if(msg == null) {
                msg = "Failed to load description for '" + operationName + "': " + result;
            }
            throw new CommandLineException(msg);
        }
        return result.get(Util.RESULT);
    }

    protected ModelNode initRequest(CommandContext ctx) throws CommandFormatException {
        ModelNode request = new ModelNode();
        ModelNode address = request.get(Util.ADDRESS);
        if(isDependsOnProfile() && ctx.isDomainMode()) {
            final String profileName = profile.getValue(ctx.getParsedCommandLine());
            if(profileName == null) {
                throw new CommandFormatException("WARNING: --profile argument is required for the complete description.");
            }
            address.add(Util.PROFILE, profileName);
        }
        for(OperationRequestAddress.Node node : getRequiredAddress()) {
            address.add(node.getType(), node.getName());
        }
        if (!isChildNode) {
            address.add(getRequiredType(), "?");
        }
        return request;
    }

    private abstract class ActionHandler implements CommandHandler, OperationCommand {

        protected Map<String, CommandArgument> args = Collections.emptyMap();

        void addArgument(CommandArgument arg) {
            checkNotNullParam("arg", arg);
            if(args.isEmpty()) {
                args = new HashMap<String, CommandArgument>();
            }
            args.put(arg.getFullName(), arg);
        }
        @Override
        public boolean isAvailable(CommandContext ctx) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isBatchMode(CommandContext ctx) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void handle(CommandContext ctx) throws CommandLineException {
            throw new UnsupportedOperationException();
        }

        @Override
        public CommandArgument getArgument(CommandContext ctx, String name) {
            return args.get(name);
        }

        @Override
        public boolean hasArgument(CommandContext ctx, String name) {
            return args.containsKey(name);
        }

        @Override
        public boolean hasArgument(CommandContext ctx, int index) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Collection<CommandArgument> getArguments(CommandContext ctx) {
            return args.values();
        }
    }

    private class WritePropertyHandler extends ActionHandler {

        @Override
        public ModelNode buildRequest(CommandContext ctx) throws CommandFormatException {

            final String name = GenericTypeOperationHandler.this.name.getValue(ctx.getParsedCommandLine(), false);
            if (!isChildNode && name == null) {
                throw new CommandFormatException("Required argument '" + GenericTypeOperationHandler.this.name.getFullName() + "' is missing.");
            }
            final ModelNode composite = new ModelNode();
            composite.get(Util.OPERATION).set(Util.COMPOSITE);
            composite.get(Util.ADDRESS).setEmptyList();
            final ModelNode steps = composite.get(Util.STEPS);

            final ParsedCommandLine args = ctx.getParsedCommandLine();

            final String profile;
            if(isDependsOnProfile() && ctx.isDomainMode()) {
                profile = GenericTypeOperationHandler.this.profile.getValue(args);
                if(profile == null) {
                    throw new OperationFormatException("--profile argument value is missing.");
                }
            } else {
                profile = null;
            }

            final Map<String,CommandArgument> nodeProps = this.args;
            for(String argName : args.getPropertyNames()) {
                if(isDependsOnProfile() && argName.equals("--profile") || GenericTypeOperationHandler.this.name.getFullName().equals(argName)) {
                    continue;
                }

                final ArgumentWithValue arg = (ArgumentWithValue) nodeProps.get(argName);
                if(arg == null) {
                    throw new CommandFormatException("Unrecognized argument name '" + argName + "'");
                }

                DefaultOperationRequestBuilder builder = new DefaultOperationRequestBuilder();
                if (profile != null) {
                    builder.addNode(Util.PROFILE, profile);
                }

                for(OperationRequestAddress.Node node : getRequiredAddress()) {
                    builder.addNode(node.getType(), node.getName());
                }
                builder.addNode(getRequiredType(), name);
                builder.setOperationName(Util.WRITE_ATTRIBUTE);
                final String propName;
                if(argName.charAt(1) == '-') {
                    propName = argName.substring(2);
                } else {
                    propName = argName.substring(1);
                }
                builder.addProperty(Util.NAME, propName);

//                final String valueString = args.getPropertyValue(argName);
//                ModelNode nodeValue = arg.getValueConverter().fromString(ctx, valueString);
                final ModelNode nodeValue = arg.toModelNode(ctx);
                builder.getModelNode().get(Util.VALUE).set(nodeValue);

                steps.add(builder.buildRequest());
            }

            return composite;
        }
    }

    class OpHandler extends ActionHandler {

        private final String opName;

        OpHandler(String opName) {
            super();
            this.opName = checkNotEmptyParam("opName", opName);
        }

        @Override
        public ModelNode buildRequest(CommandContext ctx) throws CommandFormatException {
            final ParsedCommandLine parsedArgs = ctx.getParsedCommandLine();

            final ModelNode request = new ModelNode();
            final ModelNode address = request.get(Util.ADDRESS);
            if(isDependsOnProfile() && ctx.isDomainMode()) {
                final String profile = GenericTypeOperationHandler.this.profile.getValue(parsedArgs);
                if(profile == null) {
                    throw new OperationFormatException("Required argument --profile is missing.");
                }
                address.add(Util.PROFILE, profile);
            }

            for (OperationRequestAddress.Node node : getRequiredAddress()) {
                address.add(node.getType(), node.getName());
            }
            if (!isChildNode) {
                final String name = GenericTypeOperationHandler.this.name.getValue(ctx.getParsedCommandLine(), true);
                address.add(getRequiredType(), name);
            }
            request.get(Util.OPERATION).set(opName);

            for(String argName : parsedArgs.getPropertyNames()) {
                if(isDependsOnProfile() && argName.equals("--profile")) {
                    continue;
                }

                if(this.args.isEmpty()) {
                    if(argName.equals(GenericTypeOperationHandler.this.name.getFullName())) {
                        continue;
                    }
                    throw new CommandFormatException("Command '" + operation + "' is not expected to have arguments other than " + GenericTypeOperationHandler.this.name.getFullName() + ".");
                }

                final ArgumentWithValue arg = (ArgumentWithValue) this.args.get(argName);
                if(arg == null) {
                    if(argName.equals(GenericTypeOperationHandler.this.name.getFullName())) {
                        continue;
                    }
                    throw new CommandFormatException("Unrecognized argument " + argName + " for command '" + opName + "'.");
                }

                final String propName;
                if(argName.charAt(1) == '-') {
                    propName = argName.substring(2);
                } else {
                    propName = argName.substring(1);
                }

//                final String valueString = parsedArgs.getPropertyValue(argName);
//                ModelNode nodeValue = arg.getValueConverter().fromString(ctx, valueString);
                final ModelNode nodeValue = arg.toModelNode(ctx);
                request.get(propName).set(nodeValue);
            }
            return request;
        }
    }
}
