/*
 * 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.CreateStackRequest;
import com.amazonaws.services.cloudformation.model.Capability;
import com.amazonaws.services.cloudformation.model.OnFailure;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.Tag;

public class CreateStackTask extends AWSAntTask {

    private static final String CREATE_COMPLETE = "CREATE_COMPLETE";
    private String onFailure;
    private String stackName;
    private String stackPolicyBody;
    private String stackPolicyURL;
    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 List<Tag> tags = new LinkedList<Tag>();

    private Boolean disableRollback;
    private boolean waitForCreation = false;

    private Integer timeoutInMinutes;

    /**
     * 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 stackParameter) {
        parameters.add(new Parameter()
                .withParameterKey(stackParameter.getKey())
                .withParameterValue(stackParameter.getValue())
                .withUsePreviousValue(stackParameter.getUsePreviousValue()));
    }

    /**
     * Allows you to add any number of nested preconfigured StackTag elements.
     * 
     * @param stackTag
     *            a preconfigured StackTag object.
     */
    public void addConfiguredStackTag(StackTag stackTag) {
        tags.add(new Tag().withKey(stackTag.getKey()).withValue(
                stackTag.getValue()));
    }

    /**
     * Set an action to execute upon failure. If this element is set,
     * disableRollback should not be set. If disableRollback is set, this should
     * not be set. Will print out a warning if your argument is not supported by
     * our model, but will still try to execute. Not required.
     * 
     * @param onFailure
     *            An action to execute on failure.
     */
    public void setOnFailure(String onFailure) {
        try {
            OnFailure.fromValue(onFailure);
        } catch (IllegalArgumentException e) {
            System.out
                    .println("The OnFailure "
                            + onFailure
                            + " does not seem to be in our model. If this build fails, this may be why.");
        } finally {
            this.onFailure = onFailure;
        }
    }

    /**
     * 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 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.
     * 
     * @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.
     * 
     * @param templateURL
     *            A valid URL pointing to a JSON object specifying a template.
     */
    public void setTemplateURL(String templateURL) {
        this.templateURL = templateURL;
    }

    /**
     * Set whether to disable rollback if stack creation fails. If onFailure is
     * set, this cannot be set. If this is set, onFailure cannot be set. Not
     * required, default is false.
     * 
     * @param disableRollback
     *            Whether to disable rollback if stack creation fails.
     */
    public void setDisableRollback(boolean disableRollback) {
        this.disableRollback = disableRollback;
    }
    
    /**
     * Set whether to block the build until this stack successfully finishes
     * creation. Not required, default is false.
     * 
     * @param waitForCreation
     *            Whether to block the build until this stack successfully
     *            finishes creation
     */
    public void setWaitForCreation(boolean waitForCreation) {
        this.waitForCreation = waitForCreation;
    }

    /**
     * Set the amount of time to allow the stack to take to create. Required,
     * and should be greater than 0.
     * 
     * @param timeoutInMinutes
     *            The amount of time to allow the stack to take to create before
     *            failing.
     */
    public void setTimeoutInMinutes(int timeoutInMinutes) {
        this.timeoutInMinutes = timeoutInMinutes;
    }

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

        if (stackName == null) {
            areMalformedParams = true;
            errors.append("Missing parameter: stackName is required. \n");
        }

        if (timeoutInMinutes == null || timeoutInMinutes.intValue() <= 0) {
            areMalformedParams = true;
            errors.append("Missing parameter: timeoutInMinutes is required and cannot be 0 \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 ((templateBody == null) == (templateURL == null)) {
            areMalformedParams = true;
            errors.append("Error in parameter configuration: You must set either templateBody or templateURL (But not both) \n");
        }

        if (disableRollback != null && onFailure != null) {
            areMalformedParams = true;
            errors.append("Error in parameter configuration :You can specify disableRollback or onFailure, but not both \n");
        }

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

    }

    public void execute() {
        checkParams();
        AmazonCloudFormationClient client = getOrCreateClient(AmazonCloudFormationClient.class);
        CreateStackRequest createStackRequest = new CreateStackRequest()
                .withDisableRollback(disableRollback).withOnFailure(onFailure)
                .withStackName(stackName).withStackPolicyBody(stackPolicyBody)
                .withStackPolicyURL(stackPolicyURL)
                .withTemplateBody(templateBody).withTemplateURL(templateURL)
                .withTimeoutInMinutes(timeoutInMinutes);

        if (capabilities.size() > 0) {
            createStackRequest.setCapabilities(capabilities);
        }
        if (parameters.size() > 0) {
            createStackRequest.setParameters(parameters);
        }
        if (tags.size() > 0) {
            createStackRequest.setTags(tags);
        }
        try {
            client.createStack(createStackRequest);
            System.out.println("Create stack " + stackName
                    + " request submitted.");
            if(waitForCreation) {
                WaitForStackToReachStateTask.waitForCloudFormationStackToReachStatus(client, stackName, CREATE_COMPLETE);
            }
        } catch (Exception e) {
            throw new BuildException(
                    "Could not create stack " + e.getMessage(), e);
        }
    }

    /**
     * Nested element for specifying a Capability. Set the value to the
     * Capability you want to add to the stack.
     */
    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;

        /**
         * Set whether to use the previous value when setting a parameter that
         * has already been set.
         * 
         * @param usePreviousValue
         *            Whether to use the previous value of this parameter.
         */
        public void setUsePreviousValue(boolean usePreviousValue) {
            this.usePreviousValue = usePreviousValue;
        }

        public boolean getUsePreviousValue() {
            return usePreviousValue;
        }
    }

    /**
     * Nested element for specifying a Tag (Key-value pair to associate with the
     * Stack).
     */
    public static class StackTag extends KeyValueNestedElement {
    }
}