/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.as.cli.ArgumentValueConverter;
import org.jboss.as.cli.CommandArgument;
import org.jboss.as.cli.CommandContext;
import org.jboss.as.cli.CommandFormatException;
import org.jboss.as.cli.CommandLineCompleter;
import org.jboss.as.cli.CommandLineException;
import org.jboss.as.cli.Util;
import org.jboss.as.cli.accesscontrol.AccessRequirementBuilder;
import org.jboss.as.cli.accesscontrol.AccessRequirementBuilder.RequirementSetBuilder;
import org.jboss.as.cli.impl.ArgumentWithValue;
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.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.dmr.Property;

/**
 * A command that is composed of several operations
 * performed against the same resource.
 *
 * @author Alexey Loubyansky
 */
public class ResourceCompositeOperationHandler extends BaseOperationCommand {

//    private final String commandName;
    private final String[] ops;

    private final Map<String, Map<String, ArgumentWithValue>> opArgs = new HashMap<String, Map<String, ArgumentWithValue>>();

    protected final String idProperty;

    protected final ArgumentWithValue name;
    protected final ArgumentWithValue profile;

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

    private final Map<String, CommandArgument> staticArgs = new HashMap<String, CommandArgument>();

    public ResourceCompositeOperationHandler(CommandContext ctx,
            String command,
            String nodeType,
            String idProperty,
            String... operations) {
        super(ctx, command, true);

        checkNotNullParam("command", command);
  //      this.commandName = command;

        ops = checkNotEmptyParam("operations", operations);

        this.idProperty = idProperty;

        addRequiredPath(nodeType);

        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);
            }
        };

        name = new ArgumentWithValue(this, new DefaultCompleter(new DefaultCompleter.CandidatesProvider() {
            @Override
            public List<String> getAllCandidates(CommandContext ctx) {
                ModelControllerClient client = ctx.getModelControllerClient();
                if (client == null) {
                    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())) {
                    return false;
                }
                return super.canAppearNext(ctx);
            }
        };
        name.addCantAppearAfter(helpArg);
        helpArg.addCantAppearAfter(name);

        staticArgs.put(helpArg.getFullName(), helpArg);
        staticArgs.put(profile.getFullName(), profile);
        staticArgs.put(name.getFullName(), name);

        final RequirementSetBuilder reqBuilder = AccessRequirementBuilder.Factory.create(ctx).any();
        final RequirementSetBuilder domainReq = this.isDependsOnProfile() ? reqBuilder.domain().all() : null;
        final RequirementSetBuilder standaloneReq = reqBuilder.standalone().all();
        for(String op : ops) {
            if(domainReq != null) {
                domainReq.profileOperation(nodeType + "=?", op);
            }
            standaloneReq.operation(nodeType + "=?", op);
        }
        this.accessRequirement = reqBuilder.build();
    }

    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);
    }

    @Override
    protected ModelNode buildRequestWithoutHeaders(CommandContext ctx) throws CommandFormatException {

        final ModelNode address = buildOperationAddress(ctx);

        final ModelNode composite = new ModelNode();
        composite.get(Util.OPERATION).set(Util.COMPOSITE);
        composite.get(Util.ADDRESS).setEmptyList();
        final ModelNode steps = composite.get(Util.STEPS);

        for(String opName : this.ops) {
            final ModelNode req = new ModelNode();
            req.get(Util.OPERATION).set(opName);
            req.get(Util.ADDRESS).set(address);

            Map<String, ArgumentWithValue> opArgs;
            try {
                opArgs = getOperationArguments(ctx, opName);
            } catch (CommandFormatException e) {
                throw e;
            } catch (CommandLineException e) {
                throw new CommandFormatException("Failed to read " + opName + " arguments.", e);
            }
            for(ArgumentWithValue arg : opArgs.values()) {

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

//                final String valueString = arg.getOriginalValue(parsedArgs, false);
//                if(valueString != null) {
//                    ModelNode nodeValue = arg.getValueConverter().fromString(ctx, valueString);
                final ModelNode nodeValue = arg.toModelNode(ctx);
                if(nodeValue != null) {
                    req.get(propName).set(nodeValue);
                }
            }
            steps.add(req);
        }

        return composite;
    }

    protected ModelNode buildOperationAddress(CommandContext ctx) throws CommandFormatException {

        final String name = ResourceCompositeOperationHandler.this.name.getValue(ctx.getParsedCommandLine(), true);

        ModelNode address = new ModelNode();
        if(isDependsOnProfile() && ctx.isDomainMode()) {
            final String profile = ResourceCompositeOperationHandler.this.profile.getValue(ctx.getParsedCommandLine());
            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());
        }
        address.add(getRequiredType(), name);
        return address;
    }

    @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;
        }
        return getAllArguments(ctx).get(name);
    }

    @Override
    public Collection<CommandArgument> getArguments(CommandContext ctx) {

        final ParsedCommandLine args = ctx.getParsedCommandLine();
        try {
            if(!this.name.isValueComplete(args)) {
                return staticArgs.values();
            }
        } catch (CommandFormatException e) {
            return null;
        }
        return getAllArguments(ctx).values();
    }

    @Override
    protected void recognizeArguments(CommandContext ctx) throws CommandFormatException {
        final Map<String, CommandArgument> allArgs = getAllArguments(ctx);
        if(!allArgs.keySet().containsAll(ctx.getParsedCommandLine().getPropertyNames())) {
            final Set<String> unrecognized = new HashSet<String>(ctx.getParsedCommandLine().getPropertyNames());
            unrecognized.removeAll(allArgs.keySet());
            throw new CommandFormatException("Unrecognized arguments: " + unrecognized);
        }
    }

    private Map<String,CommandArgument> allArgs;
    protected Map<String, CommandArgument> getAllArguments(CommandContext ctx) {
        if(allArgs == null) {
            allArgs = loadArguments(ctx);
            allArgs.putAll(staticArgs);
        }
        return allArgs;
    }

    protected Map<String, CommandArgument> loadArguments(CommandContext ctx) {
        final Map<String, CommandArgument> allArgs = new HashMap<String, CommandArgument>();
        for(String opName : ops) {
            try {
                allArgs.putAll(getOperationArguments(ctx, opName));
            } catch (CommandLineException e) {
                return Collections.emptyMap();
            }
        }
        return allArgs;
    }

    protected Map<String, ArgumentWithValue> getOperationArguments(CommandContext ctx, String opName) throws CommandLineException {
        Map<String, ArgumentWithValue> args = opArgs.get(opName);
        if(args != null) {
            return args;
        }

        final ModelNode descr = getOperationDescription(ctx, opName);
        if(descr.has(Util.REQUEST_PROPERTIES)) {
            args = new HashMap<String,ArgumentWithValue>();
            final List<Property> propList = descr.get(Util.REQUEST_PROPERTIES).asPropertyList();
            for (Property prop : propList) {
                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 = getType(prop.getValue());
                    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(descr.get(Util.VALUE_TYPE)) == ModelType.PROPERTY) {
                                valueConverter = ArgumentValueConverter.PROPERTIES;
                            } else {
                                valueConverter = ArgumentValueConverter.LIST;
                            }
                        }
                    }
                }
                final ArgumentWithValue arg = new ArgumentWithValue(ResourceCompositeOperationHandler.this, valueCompleter, valueConverter, "--" + prop.getName());
                args.put(arg.getFullName(), arg);
            }
        } else {
            args = Collections.emptyMap();
        }
        opArgs.put(opName, args);
        return args;
    }

    protected ModelNode getOperationDescription(CommandContext ctx, String operationName) throws CommandLineException {
        final ModelNode request = initRequest(ctx);
        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 CommandLineException("Failed to execute read-operation-description.", e);
        }
        if (!result.hasDefined(Util.RESULT)) {
            throw new CommandLineException("Operation description received no result.");
        }
        return result.get(Util.RESULT);
    }

    protected ModelNode initRequest(CommandContext ctx) throws CommandLineException {
        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 CommandLineException("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());
        }
        address.add(getRequiredType(), "?");
        return request;
    }

    protected ModelType getType(ModelNode descr) {
        if(!descr.has(Util.TYPE)) {
            return null;
        }
        try {
            return descr.get(Util.TYPE).asType();
        } catch(IllegalArgumentException e) {
            // the value type is a structure
            return null;
        }
    }

    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;
        }
    }
}
