/*
 * Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazonaws.ant.cloudformation;

import java.util.LinkedList;
import java.util.List;

import org.apache.tools.ant.BuildException;

import com.amazonaws.ant.AWSAntTask;
import com.amazonaws.ant.KeyValueNestedElement;
import com.amazonaws.ant.SimpleNestedElement;
import com.amazonaws.services.cloudformation.AmazonCloudFormationClient;
import com.amazonaws.services.cloudformation.model.Capability;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.UpdateStackRequest;

public class UpdateStackTask extends AWSAntTask {
    private String stackName;
    private String stackPolicyBody;
    private String stackPolicyURL;
    private String stackPolicyDuringUpdateBody;
    private String stackPolicyDuringUpdateURL;
    private String templateBody;
    private String templateURL;

    private List<String> capabilities = new LinkedList<String>();
    private List<String> notificationArns = new LinkedList<String>();
    private List<Parameter> parameters = new LinkedList<Parameter>();

    private Boolean usePreviousTemplate;

    /**
     * Allows you to add any number of nested preconfigured Capability elements.
     * Will warn you if the Capability is not supported by our model, but will
     * still try to execute.
     * 
     * @param capability
     *            a preconfigured Capability object.
     */
    public void addConfiguredCapability(StackCapability capability) {
        String toAdd = capability.getValue();
        try {
            Capability.fromValue(toAdd);
        } catch (IllegalArgumentException e) {
            System.out
                    .println("The capability "
                            + toAdd
                            + " does not seem to be in our model. If this build fails, this may be why.");
        } finally {
            capabilities.add(toAdd);
        }
    }

    /**
     * Allows you to add any number of nested preconfigured NotificationArn
     * elements.
     * 
     * @param notificationArn
     *            a preconfigured NotificationArn object.
     */
    public void addConfiguredNotificationArn(NotificationArn notificationArn) {
        notificationArns.add(notificationArn.getValue());
    }

    /**
     * Allows you to add any number of nested preconfigured StackParameter
     * elements.
     * 
     * @param stackParameter
     *            a preconfigured StackParameter object.
     */
    public void addConfiguredStackParameter(StackParameter parameter) {
        parameters.add(new Parameter().withParameterKey(parameter.getKey())
                .withParameterValue(parameter.getValue())
                .withUsePreviousValue(parameter.getUsePreviousValue()));
    }

    /**
     * Set the name of this stack. Required.
     * 
     * @param stackName
     *            The stack name
     */
    public void setStackName(String stackName) {
        this.stackName = stackName;
    }

    /**
     * Set the body of a stack policy to apply. Must be well-formed, properly
     * escaped JSON if specified. If this is set, stackPolicyURL cannot be set.
     * If stackPolicyURL is set, this cannot be set. Not required.
     * 
     * @param stackPolicyBody
     *            Well formed, properly escaped JSON specifying a stack policy.
     */
    public void setStackPolicyBody(String stackPolicyBody) {
        this.stackPolicyBody = stackPolicyBody;
    }

    /**
     * Set the URL leading to the body of a stack policy to apply. If this is
     * set, stackPolicyBody cannot be set. If stackPolicyBody is set, this
     * cannot be set. Not required.
     * 
     * @param stackPolicyURL
     *            A valid URL pointing to a JSON object specifying a stack
     *            policy.
     */
    public void setStackPolicyURL(String stackPolicyURL) {
        this.stackPolicyURL = stackPolicyURL;
    }

    /**
     * Set the body of a stack policy to apply during the update only,
     * overriding the currently operational policy until the update completes.
     * Must be well-formed, properly escaped JSON if specified. If this is set,
     * stackPolicyDuringUpdateURL cannot be set. If stackPolicyDuringUpdateURL
     * is set, this cannot be set. Not required.
     * 
     * @param stackPolicyDuringUpdateBody
     *            Well formed, properly escaped JSON specifying a stack policy.
     */
    public void setStackPolicyDuringUpdateBody(
            String stackPolicyDuringUpdateBody) {
        this.stackPolicyDuringUpdateBody = stackPolicyDuringUpdateBody;
    }

    /**
     * Set the URL leading to the body of a stack policy to apply during the
     * update only, overriding the currently operational policy until the update
     * completes. If this is set, stackPolicyDuringUpdateBody cannot be set. If
     * stackPolicyDuringUpdateBody is set, this cannot be set. Not required.
     * 
     * @param stackPolicyDuringUpdateURL
     *            A valid URL pointing to a JSON object specifying a stack
     *            policy.
     */
    public void setStackPolicyDuringUpdateURL(String stackPolicyDuringUpdateURL) {
        this.stackPolicyDuringUpdateURL = stackPolicyDuringUpdateURL;
    }

    /**
     * Set the body of the template to use for this stack. Must be well-formed,
     * properly escaped JSON if specified. If this is set, templateURL cannot be
     * set. If templateURL is set, this cannot be set. It is required that this
     * or templateURL be set, or that usePreviousTemplate be set to true. If
     * usePreviousTemplate is true, this should not be set.
     * 
     * @param templateBody
     *            Well formed, properly escaped JSON specifying a template.
     */
    public void setTemplateBody(String templateBody) {
        this.templateBody = templateBody;
    }

    /**
     * Set the URL leading to the body of the template to use for this stack. If
     * this is set, templateBody cannot be set. If templateBody is set, this
     * cannot be set. It is required that this or templateBody be set, or that
     * usePreviousTemplate be set to true. If usePreviousTemplate is true, this
     * should not be set.
     * 
     * @param templateURL
     *            A valid URL pointing to a JSON object specifying a template.
     */
    public void setTemplateURL(String templateURL) {
        this.templateURL = templateURL;
    }

    /**
     * Set whether to just use the previous template during this update. If this
     * is set, templateURL and templateBody should not be set. Not required.
     * 
     * @param usePreviousTemplate
     *            Whether to use the previous template during this update.
     */
    public void setUsePreviousTemplate(boolean usePreviousTemplate) {
        this.usePreviousTemplate = usePreviousTemplate;
    }

    private void checkParams() {
        boolean areMalformedParams = false;
        StringBuilder errors = new StringBuilder("");

        if (stackName == null) {
            areMalformedParams = true;
            errors.append("Missing parameter: stackName is required. \n");
        }
        if (stackPolicyBody != null && stackPolicyURL != null) {
            areMalformedParams = true;
            errors.append("Error in parameter configuration: You can set either stackPolicyBody or stackPolicyURL, but not both \n");
        }

        if (!Boolean.TRUE.equals(usePreviousTemplate)
                && (templateBody == null) == (templateURL == null)) {
            areMalformedParams = true;
            errors.append("Error in parameter configuration: You must set either templateBody or templateURL (But not both), "
                    + "or set usePreviousTemplate to true. \n");
        }

        if (stackPolicyDuringUpdateBody != null
                && stackPolicyDuringUpdateURL != null) {
            areMalformedParams = true;
            errors.append("You can set stackPolicyDuringUpdateBody or stackPolicyDuringUpdateURL, but not both \n");
        }

        if (areMalformedParams) {
            throw new BuildException(errors.toString());
        }
    }

    public void execute() {
        checkParams();
        AmazonCloudFormationClient client = getOrCreateClient(AmazonCloudFormationClient.class);
        UpdateStackRequest request = new UpdateStackRequest()
                .withStackName(stackName).withStackPolicyBody(stackPolicyBody)
                .withStackPolicyURL(stackPolicyURL)
                .withTemplateBody(templateBody).withTemplateURL(templateURL)
                .withStackPolicyDuringUpdateBody(stackPolicyDuringUpdateBody)
                .withStackPolicyDuringUpdateURL(stackPolicyDuringUpdateURL)
                .withUsePreviousTemplate(usePreviousTemplate);

        if (capabilities.size() > 0) {
            request.setCapabilities(capabilities);
        }
        if (parameters.size() > 0) {
            request.setParameters(parameters);
        }
        if (notificationArns.size() > 0) {
            request.setNotificationARNs(notificationArns);
        }

        try {
            client.updateStack(request);
            System.out.println("Update stack " + stackName
                    + " request submitted.");
        } catch (Exception e) {
            throw new BuildException("Could not update stack: "
                    + e.getMessage(), e);
        }
    }

    /**
     * Nested element for specifying a Capability. Set the value to the
     * Capability you want to add to the satck.
     */
    public static class StackCapability extends SimpleNestedElement {
    }

    /**
     * Nested element for specifying a NotificationArn. Set the value to the
     * NotificationArn you want to add to the stack.
     */
    public static class NotificationArn extends SimpleNestedElement {
    }

    /**
     * Nested element for specifying a Parameter. Set the key to the name of the
     * parameter, and the value to the value of the parameter.
     */
    public static class StackParameter extends KeyValueNestedElement {
        private boolean usePreviousValue;

        public void setUsePreviousValue(boolean usePreviousValue) {
            this.usePreviousValue = usePreviousValue;
        }

        public boolean getUsePreviousValue() {
            return usePreviousValue;
        }
    }
}