/*
 * 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.opsworks;

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

import org.apache.tools.ant.BuildException;

import com.amazonaws.ant.AWSAntTask;
import com.amazonaws.ant.SimpleNestedElement;
import com.amazonaws.services.opsworks.AWSOpsWorksClient;
import com.amazonaws.services.opsworks.model.Architecture;
import com.amazonaws.services.opsworks.model.AutoScalingType;
import com.amazonaws.services.opsworks.model.CreateInstanceRequest;
import com.amazonaws.services.opsworks.model.RootDeviceType;
import com.amazonaws.services.opsworks.model.StartInstanceRequest;

public class CreateInstanceTask extends AWSAntTask {

    
    private List<String> layerIds = new LinkedList<String>();
    private String stackId;
    private String instanceType;
    private String os;
    private String amiId;
    private String sshKeyName;
    private String availabilityZone;
    private String virtualizationType;
    private String subnetId;
    private String propertyNameForInstanceId;
    private Architecture architecture;
    private AutoScalingType autoScalingType;
    private RootDeviceType rootDeviceType = RootDeviceType.Ebs;
    private boolean installUpdatesOnBoot = true;
    private boolean ebsOptimized;
    private boolean useProjectLayerIds = true;
    private boolean startOnCreate = true;

    /**
     * Allows you to add a proconfigured nested LayerId element. At least one
     * LayerId must be specified.
     * 
     * @param layerId
     *            A preconfigured LayerID object to add
     */
    public void addConfiguredLayerId(LayerId layerId) {
        layerIds.add(layerId.getValue());
    }

    /**
     * The ID of the stack to link to this instance. You can find the ID of your
     * stack in the opsworks console. If you create a stack earlier in this
     * task, it will be assigned to the "stackId" property. If you have already
     * set the "stackId" property, you do not need to set this attribute--it
     * will automatically search for the "stackId" attribute. You are required
     * to either set the "stackId" attribute or this parameter.
     * 
     * @param stackId
     */
    public void setStackId(String stackId) {
        this.stackId = stackId;
    }

    /**
     * Set the type of this instance, such as m1.small, t2.medium, etc.
     * Required.
     * 
     * @param instanceType
     *            The type of this instance.
     */
    public void setInstanceType(String instanceType) {
        this.instanceType = instanceType;
    }

    /**
     * Set the type of autoscaling to use for this instance. Not required.
     * 
     * @param autoScalingType
     *            The type of autoscaling to use for this instance.
     */
    public void setAutoScalingType(String autoScalingType) {
        try {
            this.autoScalingType = AutoScalingType.valueOf(autoScalingType);
        } catch (IllegalArgumentException e) {
            throw new BuildException(e.getMessage(), e);
        }
    }

    /**
     * Set the operating system for this instance. Not required, defaults to
     * Amazon Linux.
     * 
     * @param os
     *            The operating system to use for this instance.
     */
    public void setOs(String os) {
        this.os = os;
    }

    /**
     * Set the custom AMI ID to be used to create this instance. Use if you set
     * "os" to "Custom". Not required.
     * 
     * @param amiId
     *            A custom AMI ID to be used to create this instance
     */
    public void setAmiId(String amiId) {
        this.amiId = amiId;
    }

    /**
     * Set the SSH key name of this instance. Not required, but this must be set
     * if you want to SSH into this instance later.
     * 
     * @param sshKeyName
     *            The IAM SSH key name used to SSH into this instance.
     */
    public void setSshKeyName(String sshKeyName) {
        this.sshKeyName = sshKeyName;
    }

    /**
     * Set the availability zone of this instance. Not required.
     * 
     * @param availabilityZone
     *            The availability zone of this instance
     */
    public void setAvailabilityZone(String availabilityZone) {
        this.availabilityZone = availabilityZone;
    }

    /**
     * Set the architecture of this instance. Not required, defaults to x86_64.
     * 
     * @param architecture
     *            The architecture of this instance.
     */
    public void setArchitecture(String architecture) {
        try {
            this.architecture = Architecture.valueOf(architecture);
        } catch (IllegalArgumentException e) {
            throw new BuildException(e.getMessage(), e);
        }
    }

    /**
     * Set whether to install OS and package updates when this instance boots.
     * Not required, default is true. It is highly recommended that you leave
     * this as true.
     * 
     * @param installUpdatesOnBoot
     *            Whether to install OS and package updates when this instance
     *            boots.
     */
    public void setInstallUpdatesOnBoot(boolean installUpdatesOnBoot) {
        this.installUpdatesOnBoot = installUpdatesOnBoot;
    }

    /**
     * Set whether to create an Amazon EBS-Optimized instance. Not required,
     * default is false.
     * 
     * @param ebsOptimized
     *            Whether to create an Amazon EBS-Optimized instance.
     */
    public void setEbsOptimized(boolean ebsOptimized) {
        this.ebsOptimized = ebsOptimized;
    }

    /**
     * Set whether to add all LayerId project properties to the layerIds group
     * of this instance. If set to true, then all layers created earlier in this
     * build will be used as layerIds for this instance. Not required, defaults
     * to true.
     * 
     * @param useProjectLayerIds
     *            Whether to add all the IDs of all layers created earlier in
     *            this project to the layerIds group of this instance.
     */
    public void setUseProjectLayerIds(boolean useProjectLayerIds) {
        this.useProjectLayerIds = useProjectLayerIds;
    }

    /**
     * Set whether to start this instance at the end of the execution of this
     * task. Not required, defaults to true.
     * 
     * @param startOnCreate
     *            Whether to start this instance at the end of the execution of
     *            this task.
     */
    public void setStartOnCreate(boolean startOnCreate) {
        this.startOnCreate = startOnCreate;
    }

    /**
     * Set the instance's virtualization type. Not required.
     * 
     * @param virtualizationType
     *            The instances virtualization type. Should be paravirtual or
     *            hvm.
     */
    public void setVirtualizationType(String virtualizationType) {
        this.virtualizationType = virtualizationType;
    }

    /**
     * Set the root device type of this instance. Not required.
     * 
     * @param rootDeviceType
     *            The root device type of this instance
     */
    public void setRootDeviceType(String rootDeviceType) {
        try {
            this.rootDeviceType = RootDeviceType.valueOf(rootDeviceType);
        } catch (IllegalArgumentException e) {
            throw new BuildException(e.getMessage(), e);
        }
    }

    /**
     * The ID of this instance's subnet. You can use this parameter to override
     * the default, if this stack is running in a VPC.
     * 
     * @param subnetId
     *            The ID of this instance's subnet.
     */
    public void setSubnetId(String subnetId) {
        this.subnetId = subnetId;
    }

    /**
     * Set which property to assign the ID of this instance to
     * 
     * @param propertyToSet
     *            The property to assign the ID of this instance to
     */
    public void setPropertyNameForInstanceId(String propertyToSet) {
        this.propertyNameForInstanceId = propertyToSet;
    }

    public void checkParams() {
        boolean areMalformedParams = false;
        StringBuilder errors = new StringBuilder("");
        if (stackId == null) {
            if (!Boolean.TRUE.equals(getProject().getReference(Constants.STACK_ID_REFERENCE))) {
                stackId = getProject().getProperty(Constants.STACK_ID_PROPERTY);
            }
            if (stackId == null) {
                areMalformedParams = true;
                errors.append("Missing parameter: stackId is required \n");
            } else {
                System.out.println("Using " + Constants.STACK_ID_PROPERTY
                        + " property as stackId");
            }
        }
        if (useProjectLayerIds) {
            addProjectLayerIds();
        }
        if (layerIds.size() <= 0) {
            areMalformedParams = true;
            errors.append("Missing parameter: You must specify at least one LayerId \n");
        }
        if (areMalformedParams) {
            throw new BuildException(errors.toString());
        }
    }

    private void addProjectLayerIds() {
        String projectLayerIds = getProject().getProperty(Constants.LAYER_IDS_PROPERTY);
        for (String layerId : projectLayerIds.split(",")) {
            layerIds.add(layerId);
        }
    }

    /**
     * Creates an instance according to the set parameters. Also sets an
     * instanceId property. Which property will be set depends on what order
     * this instance is created in the project. If it is the first instance
     * created in this Ant build, instanceId1 is set. If it's the second,
     * instanceId2 is set, etc. The ID is also printed for you to set to your
     * own property for later use.
     */
    public void execute() {
        checkParams();
        AWSOpsWorksClient client = getOrCreateClient(AWSOpsWorksClient.class);
        CreateInstanceRequest createInstanceRequest = new CreateInstanceRequest()
                .withStackId(stackId)
                .withInstallUpdatesOnBoot(installUpdatesOnBoot)
                .withEbsOptimized(ebsOptimized).withLayerIds(layerIds)
                .withInstanceType(instanceType).withOs(os)
                .withAmiId(amiId).withSshKeyName(sshKeyName)
                .withAvailabilityZone(availabilityZone)
                .withVirtualizationType(virtualizationType)
                .withRootDeviceType(rootDeviceType).withSubnetId(subnetId);
        String instanceId;
        if(autoScalingType != null) {
            createInstanceRequest.setAutoScalingType(autoScalingType);
        }
        if(architecture != null) {
            createInstanceRequest.setArchitecture(architecture);
        }
        try {
            instanceId = client.createInstance(createInstanceRequest)
                    .getInstanceId();
            if (startOnCreate) {
                client.startInstance(new StartInstanceRequest()
                        .withInstanceId(instanceId));
                System.out.println("Starting created instance.");
            }
            Thread.sleep(10); // Wait for the instance metadata to populate
            System.out
                    .println("Created instance with instanceId "
                            + instanceId
                            + ". View the status of this instance at https://console.aws.amazon.com/opsworks/home?#/stack/"
                            + stackId + "/instances");
        } catch (Exception e) {
            throw new BuildException("Could not create Instance: "
                    + e.getMessage(), e);
        }

        if (instanceId != null) {
            if (getProject().getProperty(Constants.INSTANCE_IDS_PROPERTY) == null) {
                getProject().setProperty(Constants.INSTANCE_IDS_PROPERTY, instanceId);
            } else {
                getProject().setProperty(
                        Constants.INSTANCE_IDS_PROPERTY,
                        getProject().getProperty(Constants.INSTANCE_IDS_PROPERTY) + ","
                                + instanceId);
            }
            if (propertyNameForInstanceId != null) {
                getProject().setProperty(propertyNameForInstanceId, instanceId);
            }
        }
    }

    /**
     * A container class to use as a nested element, so you can specify any
     * number of layerIds for this instance. You can find the IDs of layers to
     * use in the OpsWorks console.
     */
    public static class LayerId extends SimpleNestedElement {
    }
}