/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 org.jclouds.openstack.nova.v2_0.compute.options;

import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;

import java.util.Arrays;
import java.util.Map;
import java.util.Set;

import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.openstack.nova.v2_0.NovaApi;
import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping;
import org.jclouds.openstack.nova.v2_0.domain.Network;
import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions;
import org.jclouds.scriptbuilder.domain.Statement;

import com.google.common.base.Objects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;

/**
 * Contains options supported in the {@code ComputeService#runNode} operation on the
 * "openstack-nova" provider. <h2>Usage</h2> The recommended way to instantiate a
 * NovaTemplateOptions object is to statically import NovaTemplateOptions.* and invoke a static
 * creation method followed by an instance mutator (if needed):
 * <p/>
 * <code>
 * import static org.jclouds.aws.ec2.compute.options.NovaTemplateOptions.Builder.*;
 * <p/>
 * ComputeService api = // get connection
 * templateBuilder.options(inboundPorts(22, 80, 8080, 443));
 * Set<? extends NodeMetadata> set = api.createNodesInGroup(tag, 2, templateBuilder.build());
 * <code>
 */
public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
   @Override
   public NovaTemplateOptions clone() {
      NovaTemplateOptions options = new NovaTemplateOptions();
      copyTo(options);
      return options;
   }

   @Override
   public void copyTo(TemplateOptions to) {
      super.copyTo(to);
      if (to instanceof NovaTemplateOptions) {
         NovaTemplateOptions eTo = NovaTemplateOptions.class.cast(to);
         eTo.autoAssignFloatingIp(shouldAutoAssignFloatingIp());
         if (getFloatingIpPoolNames().isPresent())
            eTo.floatingIpPoolNames(getFloatingIpPoolNames().get());
         eTo.generateKeyPair(shouldGenerateKeyPair());
         eTo.keyPairName(getKeyPairName());
         if (getUserData() != null) {
            eTo.userData(getUserData());
         }
         if (getDiskConfig() != null) {
            eTo.diskConfig(getDiskConfig());
         }

         eTo.configDrive(getConfigDrive());
         eTo.novaNetworks(getNovaNetworks());
         eTo.availabilityZone(getAvailabilityZone());
         eTo.blockDeviceMappings(getBlockDeviceMappings());
      }
   }

   protected boolean autoAssignFloatingIp = false;
   protected Optional<Set<String>> floatingIpPoolNames = Optional.absent();
   protected boolean generateKeyPair = false;
   protected String keyPairName;
   protected byte[] userData;
   protected String diskConfig;
   protected boolean configDrive;
   protected Set<Network> novaNetworks;
   protected String availabilityZone;
   // TODO move up to TemplateOptions as SoftLayer also have something similar?
   protected Set<BlockDeviceMapping> blockDeviceMappings = ImmutableSet.of();

   @Override
   public boolean equals(Object o) {
      if (this == o)
         return true;
      if (o == null || getClass() != o.getClass())
         return false;
      NovaTemplateOptions that = NovaTemplateOptions.class.cast(o);
      return super.equals(that) && equal(this.autoAssignFloatingIp, that.autoAssignFloatingIp)
            && equal(this.floatingIpPoolNames, that.floatingIpPoolNames)
            && equal(this.generateKeyPair, that.generateKeyPair)
            && equal(this.keyPairName, that.keyPairName)
            && Arrays.equals(this.userData, that.userData)
            && equal(this.diskConfig, that.diskConfig)
            && equal(this.configDrive, that.configDrive)
            && equal(this.novaNetworks, that.novaNetworks)
            && equal(this.availabilityZone, that.availabilityZone)
            && equal(this.blockDeviceMappings, that.blockDeviceMappings);
   }

   @Override
   public int hashCode() {
      return Objects.hashCode(super.hashCode(), autoAssignFloatingIp, floatingIpPoolNames, generateKeyPair, keyPairName, 
              Arrays.hashCode(userData), diskConfig, configDrive, novaNetworks, availabilityZone, blockDeviceMappings);
   }

   @Override
   public ToStringHelper string() {
      ToStringHelper toString = super.string();
      if (!autoAssignFloatingIp)
         toString.add("autoAssignFloatingIp", autoAssignFloatingIp);
      if (floatingIpPoolNames.isPresent())
         toString.add("floatingIpPoolNames", floatingIpPoolNames.get());
      if (generateKeyPair)
         toString.add("generateKeyPair", generateKeyPair);
      toString.add("keyPairName", keyPairName);
      toString.add("userData", userData);
      toString.add("diskConfig", diskConfig);
      toString.add("configDrive", configDrive);
      toString.add("novaNetworks", novaNetworks);
      toString.add("availabilityZone", availabilityZone);
      toString.add("blockDeviceMappings", blockDeviceMappings);
      return toString;
   }

   /**
    * @see #getFloatingIpPoolNames()
    */
   public NovaTemplateOptions autoAssignFloatingIp(boolean enable) {
      this.autoAssignFloatingIp = enable;
      return this;
   }

   /**
    * @see #getFloatingIpPoolNames()
    */
   public NovaTemplateOptions floatingIpPoolNames(String... floatingIpPoolNames) {
      return floatingIpPoolNames(ImmutableSet.copyOf(checkNotNull(floatingIpPoolNames, "floatingIpPoolNames")));
   }

   /**
    * @see #getFloatingIpPoolNames()
    */
   public NovaTemplateOptions floatingIpPoolNames(Iterable<String> floatingIpPoolNames) {
      for (String groupName : checkNotNull(floatingIpPoolNames, "floatingIpPoolNames"))
        checkNotNull(emptyToNull(groupName), "all floating-ip-pool-names must be non-empty");
      this.floatingIpPoolNames = Optional.<Set<String>> of(ImmutableSet.copyOf(floatingIpPoolNames));
      return this;
   }

   /**
    * @see #shouldGenerateKeyPair()
    */
   public NovaTemplateOptions generateKeyPair(boolean enable) {
      this.generateKeyPair = enable;
      return this;
   }

   /**
    * @see #shouldGenerateKeyPair()
    */
   public NovaTemplateOptions keyPairName(String keyPairName) {
      this.keyPairName = keyPairName;
      return this;
   }

   /**
    * <h3>Note</h3>
    *
    * This requires that {@link NovaApi#getExtensionApi(String)} to return
    * {@link Optional#isPresent present}
    *
    * @return true if auto assignment of a floating ip to each vm is enabled
    */
   public boolean shouldAutoAssignFloatingIp() {
      return autoAssignFloatingIp;
   }

   /**
    * @see CreateServerOptions#getAvailabilityZone()
    */
   public NovaTemplateOptions availabilityZone(String availabilityZone) {
      this.availabilityZone = availabilityZone;
      return this;
   }

   /**
    * @see #getBlockDeviceMappings()
    */
   public NovaTemplateOptions blockDeviceMappings(BlockDeviceMapping... blockDeviceMappings) {
      return blockDeviceMappings(ImmutableSet.copyOf(checkNotNull(blockDeviceMappings, "blockDeviceMappings")));
   }

   /**
    * @see #getBlockDeviceMappings()
    */
   public NovaTemplateOptions blockDeviceMappings(Iterable<BlockDeviceMapping> blockDeviceMappings) {
      this.blockDeviceMappings = ImmutableSet.copyOf(blockDeviceMappings);
      return this;
   }

   /**
    * The floating IP pool name(s) to use when allocating a FloatingIP. Applicable
    * only if #shouldAutoAssignFloatingIp() returns true. If not set will attempt to
    * use whatever FloatingIP(s) can be found regardless of which pool they originated
    * from
    *
    * @return floating-ip-pool names to use
    */
   public Optional<Set<String>> getFloatingIpPoolNames() {
      return floatingIpPoolNames;
   }

   /**
    * Specifies the keypair used to run instances with
    * @return the keypair to be used
    */
   public String getKeyPairName() {
      return keyPairName;
   }

   /**
    * <h3>Note</h3>
    *
    * This requires that {@link NovaApi#getKeyPairApi(String)} to return
    * {@link Optional#isPresent present}
    *
    * @return true if auto generation of keypairs is enabled
    */
   public boolean shouldGenerateKeyPair() {
      return generateKeyPair;
   }

   public byte[] getUserData() {
      return userData;
   }

   /**
    * @see CreateServerOptions#getDiskConfig()
    */
   public String getDiskConfig() {
      return diskConfig;
   }

   /**
    * @see CreateServerOptions#getConfigDrive()
    */
   public boolean getConfigDrive() {
      return configDrive;
   }

   /**
    * @see CreateServerOptions#getNetworks()
    */
   public Set<Network> getNovaNetworks() {
      return novaNetworks;
   }

   /**
    * @see CreateServerOptions#getAvailabilityZone()
    */
   public String getAvailabilityZone() {
      return availabilityZone;
   }

   public Set<BlockDeviceMapping> getBlockDeviceMappings() {
      return blockDeviceMappings;
   }

   public static class Builder {

      /**
       * @see NovaTemplateOptions#shouldAutoAssignFloatingIp()
       */
      public static NovaTemplateOptions autoAssignFloatingIp(boolean enable) {
         return new NovaTemplateOptions().autoAssignFloatingIp(enable);
      }

      /**
       * @see #getFloatingIpPoolNames()
       */
      public NovaTemplateOptions floatingIpPoolNames(String... floatingIpPoolNames) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.floatingIpPoolNames(floatingIpPoolNames));
      }

      /**
       * @see #getFloatingIpPoolNames()
       */
      public NovaTemplateOptions floatingIpPoolNames(Iterable<String> floatingIpPoolNames) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.floatingIpPoolNames(floatingIpPoolNames));
      }

      /**
       * @see NovaTemplateOptions#shouldGenerateKeyPair()
       */
      public static NovaTemplateOptions generateKeyPair(boolean enable) {
         return new NovaTemplateOptions().generateKeyPair(enable);
      }

      /**
       * @see NovaTemplateOptions#getKeyPairName()
       */
      public static NovaTemplateOptions keyPairName(String keyPairName) {
         return new NovaTemplateOptions().keyPairName(keyPairName);
      }

      // methods that only facilitate returning the correct object type

      /**
       * @see TemplateOptions#inboundPorts
       */
      public static NovaTemplateOptions inboundPorts(int... ports) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.inboundPorts(ports));
      }

      /**
       * @see TemplateOptions#port
       */
      public static NovaTemplateOptions blockOnPort(int port, int seconds) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.blockOnPort(port, seconds));
      }

      /**
       * @see TemplateOptions#installPrivateKey
       */
      public static NovaTemplateOptions installPrivateKey(String rsaKey) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.installPrivateKey(rsaKey));
      }

      /**
       * @see TemplateOptions#authorizePublicKey
       */
      public static NovaTemplateOptions authorizePublicKey(String rsaKey) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.authorizePublicKey(rsaKey));
      }

      /**
       * @see TemplateOptions#userMetadata
       */
      public static NovaTemplateOptions userMetadata(Map<String, String> userMetadata) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.userMetadata(userMetadata));
      }

      /**
       * @see TemplateOptions#nodeNames(Iterable)
       */
      public static NovaTemplateOptions nodeNames(Iterable<String> nodeNames) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.nodeNames(nodeNames));
      }

      /**
       * @see TemplateOptions#networks(Iterable)
       */
      public static NovaTemplateOptions networks(Iterable<String> networks) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.networks(networks));
      }

      /**
       * @see TemplateOptions#overrideLoginUser
       */
      public static NovaTemplateOptions overrideLoginUser(String user) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.overrideLoginUser(user);
      }

      /**
       * @see TemplateOptions#overrideLoginPassword
       */
      public static NovaTemplateOptions overrideLoginPassword(String password) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.overrideLoginPassword(password);
      }

      /**
       * @see TemplateOptions#overrideLoginPrivateKey
       */
      public static NovaTemplateOptions overrideLoginPrivateKey(String privateKey) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.overrideLoginPrivateKey(privateKey);
      }

      /**
       * @see TemplateOptions#overrideAuthenticateSudo
       */
      public static NovaTemplateOptions overrideAuthenticateSudo(boolean authenticateSudo) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.overrideAuthenticateSudo(authenticateSudo);
      }

      /**
       * @see TemplateOptions#overrideLoginCredentials
       */
      public static NovaTemplateOptions overrideLoginCredentials(LoginCredentials credentials) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.overrideLoginCredentials(credentials);
      }

      /**
       * @see TemplateOptions#blockUntilRunning
       */
      public static NovaTemplateOptions blockUntilRunning(boolean blockUntilRunning) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.blockUntilRunning(blockUntilRunning);
      }

      /**
       * @see NovaTemplateOptions#userData
       */
      public static NovaTemplateOptions userData(byte[] userData) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.userData(userData));
      }

      /**
       * @see org.jclouds.openstack.nova.v2_0.options.CreateServerOptions#getDiskConfig()
       */
      public static NovaTemplateOptions diskConfig(String diskConfig) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.diskConfig(diskConfig));
      }

      /**
       * @see org.jclouds.openstack.nova.v2_0.options.CreateServerOptions#getConfigDrive()
       */
      public static NovaTemplateOptions configDrive(boolean configDrive) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.configDrive(configDrive));
      }

      /**
       * @see org.jclouds.openstack.nova.v2_0.options.CreateServerOptions#getNetworks()
       */
      public static NovaTemplateOptions novaNetworks(Set<Network> novaNetworks) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return NovaTemplateOptions.class.cast(options.novaNetworks(novaNetworks));
      }

      /**
       * @see NovaTemplateOptions#getAvailabilityZone()
       */
      public static NovaTemplateOptions availabilityZone(String availabilityZone) {
         NovaTemplateOptions options = new NovaTemplateOptions();
         return options.availabilityZone(availabilityZone);
      }
   }

   // methods that only facilitate returning the correct object type

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions blockOnPort(int port, int seconds) {
      return NovaTemplateOptions.class.cast(super.blockOnPort(port, seconds));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions inboundPorts(int... ports) {
      return NovaTemplateOptions.class.cast(super.inboundPorts(ports));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions authorizePublicKey(String publicKey) {
      return NovaTemplateOptions.class.cast(super.authorizePublicKey(publicKey));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions installPrivateKey(String privateKey) {
      return NovaTemplateOptions.class.cast(super.installPrivateKey(privateKey));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions blockUntilRunning(boolean blockUntilRunning) {
      return NovaTemplateOptions.class.cast(super.blockUntilRunning(blockUntilRunning));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions dontAuthorizePublicKey() {
      return NovaTemplateOptions.class.cast(super.dontAuthorizePublicKey());
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions nameTask(String name) {
      return NovaTemplateOptions.class.cast(super.nameTask(name));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions runAsRoot(boolean runAsRoot) {
      return NovaTemplateOptions.class.cast(super.runAsRoot(runAsRoot));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions runScript(Statement script) {
      return NovaTemplateOptions.class.cast(super.runScript(script));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions overrideLoginCredentials(LoginCredentials overridingCredentials) {
      return NovaTemplateOptions.class.cast(super.overrideLoginCredentials(overridingCredentials));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions overrideLoginPassword(String password) {
      return NovaTemplateOptions.class.cast(super.overrideLoginPassword(password));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions overrideLoginPrivateKey(String privateKey) {
      return NovaTemplateOptions.class.cast(super.overrideLoginPrivateKey(privateKey));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions overrideLoginUser(String loginUser) {
      return NovaTemplateOptions.class.cast(super.overrideLoginUser(loginUser));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions overrideAuthenticateSudo(boolean authenticateSudo) {
      return NovaTemplateOptions.class.cast(super.overrideAuthenticateSudo(authenticateSudo));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions userMetadata(Map<String, String> userMetadata) {
      return NovaTemplateOptions.class.cast(super.userMetadata(userMetadata));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions userMetadata(String key, String value) {
      return NovaTemplateOptions.class.cast(super.userMetadata(key, value));
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public NovaTemplateOptions nodeNames(Iterable<String> nodeNames) {
      return NovaTemplateOptions.class.cast(super.nodeNames(nodeNames));
   }

   /**
    * <br>Ensures NovaTemplateOptions can work with networks specified as Strings.
    * Also provides for compatibility with the abstraction layer.
    */
   @Override
   public NovaTemplateOptions networks(Iterable<String> networks) {
      return NovaTemplateOptions.class.cast(super.networks(networks));
   }

   /**
    * <br>Ensures NovaTemplateOptions can work with networks specified as Strings.
    * Also provides for compatibility with the abstraction layer.
    */
   @Override
   public NovaTemplateOptions networks(String... networks) {
      return NovaTemplateOptions.class.cast(super.networks(networks));
   }

   /**
    * User data as bytes (not base64-encoded)
    */
   public NovaTemplateOptions userData(byte[] userData) {
      // This limit may not be needed for nova
      checkArgument(checkNotNull(userData, "userData").length <= 16 * 1024,
            "userData cannot be larger than 16kb");
      this.userData = userData;
      return this;
   }

   /**
    * @see CreateServerOptions#getDiskConfig()
    */
   public NovaTemplateOptions diskConfig(String diskConfig) {
      this.diskConfig = diskConfig;
      return this;
   }

   /**
    * OpenStack can be configured to write metadata to a special configuration drive that will be
    * attached to the instance when it boots. The instance can retrieve any information that would
    * normally be available through the metadata service by mounting this disk and reading files from it.
    * To enable the config drive, set this parameter to "true".
    * This has to be enabled for user data cases.
    * @see CreateServerOptions#getConfigDrive()
    */
   public NovaTemplateOptions configDrive(boolean configDrive) {
      this.configDrive = configDrive;
      return this;
   }

   /**
    * @param novaNetworks The list of network declarations.
    * Nova-specific network declarations allow for specifying network UUIDs, port UUIDs, and fixed IPs.
    * Unline {@link #networks(Iterable)} this supports setting additional network parameters and not just network UUIDs.
    * @see CreateServerOptions#getNetworks()
    */
   public NovaTemplateOptions novaNetworks(Set<Network> novaNetworks) {
      this.novaNetworks = novaNetworks;
      return this;
   }
}
