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

import static com.google.common.base.Objects.equal;
import static com.google.common.base.MoreObjects.toStringHelper;
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 static com.google.common.io.BaseEncoding.base64;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Named;

import org.jclouds.http.HttpRequest;
import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping;
import org.jclouds.openstack.nova.v2_0.domain.Network;
import org.jclouds.rest.MapBinder;
import org.jclouds.rest.binders.BindToJsonPayload;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ForwardingObject;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

public class CreateServerOptions implements MapBinder {
   @Inject
   private BindToJsonPayload jsonBinder;

   static class File {
      private final String path;
      private final String contents;

      public File(String path, byte[] contents) {
         this.path = checkNotNull(path, "path");
         this.contents = base64().encode(checkNotNull(contents, "contents"));
         checkArgument(
               path.getBytes().length < 255,
               String.format("maximum length of path is 255 bytes.  Path specified %s is %d bytes", path,
                     path.getBytes().length));
      }

      public String getContents() {
         return contents;
      }

      public String getPath() {
         return path;
      }

      @Override
      public boolean equals(Object object) {
         if (this == object) {
            return true;
         }
         if (object instanceof File) {
            final File other = File.class.cast(object);
            return equal(path, other.path);
         } else {
            return false;
         }
      }

      @Override
      public int hashCode() {
         return Objects.hashCode(path);
      }

      @Override
      public String toString() {
         return toStringHelper("file").add("path", path).toString();
      }

   }

   private String keyName;
   private String adminPass;
   private Set<String> securityGroupNames = ImmutableSet.of();
   private Map<String, String> metadata = ImmutableMap.of();
   private List<File> personality = Lists.newArrayList();
   private byte[] userData;
   private String diskConfig;
   private List<String> networks = ImmutableList.of();
   private List<Network> novaNetworks = ImmutableList.of();
   private String availabilityZone;
   private boolean configDrive;
   private Set<BlockDeviceMapping> blockDeviceMappings = ImmutableSet.of();

   @Override
   public boolean equals(Object object) {
      if (this == object) {
         return true;
      }
      if (object instanceof CreateServerOptions) {
         final CreateServerOptions other = CreateServerOptions.class.cast(object);
         return equal(keyName, other.keyName) && equal(adminPass, other.adminPass)
               && equal(securityGroupNames, other.securityGroupNames) && equal(metadata, other.metadata)
               && equal(personality, other.personality)
               && equal(diskConfig, other.diskConfig)
               && equal(networks, other.networks)
               && equal(availabilityZone, other.availabilityZone)
               && equal(configDrive, other.configDrive)
               && equal(blockDeviceMappings, other.blockDeviceMappings);
      } else {
         return false;
      }
   }

   @Override
   public int hashCode() {
      return Objects.hashCode(keyName, adminPass, securityGroupNames, metadata, personality, networks, availabilityZone,
            configDrive, blockDeviceMappings);
   }

   protected String formatPossiblyGzipped(final byte [] data) {
      if (data.length > 10 && data[0] == 31 && data[1] == -117) {
         return String.format("<gzipped data (%d bytes)>", data.length);
      }
      return new String(data);
   }

   protected ToStringHelper string() {
      ToStringHelper toString = MoreObjects.toStringHelper(this);
      toString.add("keyName", keyName);
      if (!securityGroupNames.isEmpty())
         toString.add("securityGroupNames", securityGroupNames);
      if (!metadata.isEmpty())
         toString.add("metadata", metadata);
      if (!personality.isEmpty())
         toString.add("personality", personality);
      if (adminPass != null)
         toString.add("adminPassPresent", true);
      if (diskConfig != null)
         toString.add("diskConfig", diskConfig);
      toString.add("userData", userData == null ? null : formatPossiblyGzipped(userData));
      if (!networks.isEmpty())
         toString.add("networks", networks);
      toString.add("availabilityZone", availabilityZone == null ? null : availabilityZone);
      toString.add("configDrive", configDrive);
      if (!blockDeviceMappings.isEmpty())
         toString.add("blockDeviceMappings", blockDeviceMappings);
      return toString;
   }

   @Override
   public String toString() {
      return string().toString();
   }

   static class ServerRequest {
      final String name;
      final String imageRef;
      final String flavorRef;
      @Named("availability_zone")
      String availabilityZone;
      String adminPass;
      Map<String, String> metadata;
      List<File> personality;
      String key_name;
      @Named("security_groups")
      Set<NamedThingy> securityGroupNames;
      String user_data;
      @Named("OS-DCF:diskConfig")
      String diskConfig;
      List<Map<String, String>> networks;
      @Named("config_drive")
      String configDrive;
      @Named("block_device_mapping_v2")
      Set<BlockDeviceMapping> blockDeviceMappings;

      private ServerRequest(String name, String imageRef, String flavorRef) {
         this.name = name;
         this.imageRef = imageRef;
         this.flavorRef = flavorRef;
      }

   }

   @Override
   public <R extends HttpRequest> R bindToRequest(R request, Map<String, Object> postParams) {
      ServerRequest server = new ServerRequest(checkNotNull(postParams.get("name"), "name parameter not present").toString(),
            checkNotNull(postParams.get("imageRef"), "imageRef parameter not present").toString(),
            checkNotNull(postParams.get("flavorRef"), "flavorRef parameter not present").toString());
      if (!metadata.isEmpty())
         server.metadata = metadata;
      if (!personality.isEmpty())
         server.personality = personality;
      if (keyName != null)
         server.key_name = keyName;
      if (availabilityZone != null)
         server.availabilityZone = availabilityZone;
      if (userData != null)
         server.user_data = base64().encode(userData);
      if (configDrive)
         server.configDrive = "true";
      if (!securityGroupNames.isEmpty()) {
         server.securityGroupNames = Sets.newLinkedHashSet();
         for (String groupName : securityGroupNames) {
            server.securityGroupNames.add(new NamedThingy(groupName));
         }
      }
      if (adminPass != null) {
         server.adminPass = adminPass;
      }
      if (diskConfig != null) {
         server.diskConfig = diskConfig;
      }
      if (!networks.isEmpty() || !novaNetworks.isEmpty()) {
         server.networks = Lists.newArrayList(); // ensures ordering is preserved - helps testing and more intuitive for users.
         for (Network network : novaNetworks) {
            // Avoid serializing null values, which are common here.
            ImmutableMap.Builder<String, String> networkMap = new ImmutableMap.Builder<String, String>();
            if (network.getNetworkUuid() != null) {
               networkMap.put("uuid", network.getNetworkUuid());
            }
            if (network.getPortUuid() != null) {
               networkMap.put("port", network.getPortUuid());
            }
            if (network.getFixedIp() != null) {
               networkMap.put("fixed_ip", network.getFixedIp());
            }
            server.networks.add(networkMap.build());
         }
         for (String network : networks) {
            server.networks.add(ImmutableMap.of("uuid", network));
         }
      }
      if (!blockDeviceMappings.isEmpty()) {
         server.blockDeviceMappings = blockDeviceMappings;
      }

      return bindToRequest(request, (Object) ImmutableMap.of("server", server));
   }

   private static class NamedThingy extends ForwardingObject {
      private String name;

      private NamedThingy(String name) {
         this.name = name;
      }

      @Override
      protected Object delegate() {
         return name;
      }
   }

   /**
    * You may further customize a cloud server by injecting data into the file
    * system of the cloud server itself. This is useful, for example, for
    * inserting ssh keys, setting configuration files, or storing data that you
    * want to retrieve from within the instance itself. It is intended to
    * provide a minimal amount of launch-time personalization. If significant
    * customization is required, a custom image should be created. The max size
    * of the file path data is 255 bytes while the max size of the file contents
    * is determined by provider quotas(default size is 10KB). Note that the file
    * contents should be encoded as a Base64 string and the size limit refers to
    * the number of bytes in the decoded data not the number of characters in the
    * encoded data. The maximum number of file path/content pairs that can be supplied
    * is determined by provider quotas(default is 5). Any existing files that match
    * the specified file will be renamed to include the extension bak followed by a
    * time stamp.
    * For example, the file /etc/passwd will be backed
    * up as /etc/passwd.bak.1246036261.5785. All files will have root and the
    * root group as owner and group owner, respectively and will allow user and
    * group read access only (-r--r-----).
    */
   public CreateServerOptions writeFileToPath(byte[] contents, String path) {
      personality.add(new File(path, contents));
      return this;
   }

   public CreateServerOptions adminPass(String adminPass) {
      checkNotNull(adminPass, "adminPass");
      this.adminPass = adminPass;
      return this;
   }

   /**
    * Custom cloud server metadata can also be supplied at launch time. This
    * metadata is stored in the API system where it is retrievable by querying
    * the API for server status. The maximum size of the metadata key and value
    * is each 255 bytes.
    */
   public CreateServerOptions metadata(Map<String, String> metadata) {
      checkNotNull(metadata, "metadata");
      for (Entry<String, String> entry : metadata.entrySet()) {
         checkArgument(
               entry.getKey().getBytes().length < 255,
               String.format("maximum length of metadata key is 255 bytes.  Key specified %s is %d bytes",
                     entry.getKey(), entry.getKey().getBytes().length));
         checkArgument(entry.getKey().getBytes().length < 255, String.format(
               "maximum length of metadata value is 255 bytes.  Value specified for %s (%s) is %d bytes",
               entry.getKey(), entry.getValue(), entry.getValue().getBytes().length));
      }
      this.metadata = ImmutableMap.copyOf(metadata);
      return this;
   }

   /**
    * Custom user-data can be also be supplied at launch time.
    * It is retrievable by the instance and is often used for launch-time configuration
    * by instance scripts.
    * Pass userData unencoded, as the value will be base64 encoded automatically.
    */
   public CreateServerOptions userData(byte[] userData) {
      this.userData = userData;
      return this;
   }

   /**
    * Set to true to use a config drive for metadata.
    * This is a separate configuration drive that can be used separately from the metadata service.
    * This needs to be set to "true" when trying to use user data for cloud-init.
    * @see http://docs.openstack.org/grizzly/openstack-compute/admin/content/config-drive.html
    */
   public CreateServerOptions configDrive(boolean configDrive) {
      this.configDrive = configDrive;
      return this;
   }

   /**
    * A keypair name can be defined when creating a server. This key will be
    * linked to the server and used to SSH connect to the machine
    * @see #getKeyPairName()
    */
   public CreateServerOptions keyPairName(String keyName) {
      this.keyName = keyName;
      return this;
   }

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

   /**
    * @see #getSecurityGroupNames()
    */
   public CreateServerOptions securityGroupNames(String... securityGroupNames) {
      return securityGroupNames(ImmutableSet.copyOf(checkNotNull(securityGroupNames, "securityGroupNames")));
   }

   public CreateServerOptions securityGroupNames(Iterable<String> securityGroupNames) {
      for (String groupName : checkNotNull(securityGroupNames, "securityGroupNames"))
         checkNotNull(emptyToNull(groupName), "all security groups must be non-empty");
      this.securityGroupNames = ImmutableSet.copyOf(securityGroupNames);
      return this;
   }

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

   /**
    * @see #getNetworks()
    */
   public CreateServerOptions networks(Iterable<String> networks) {
      this.networks = ImmutableList.copyOf(networks);
      return this;
   }

   /**
    * @see #getNetworks()
    * Overwrites networks supplied by {@link #networks(Iterable)}
    */
   public CreateServerOptions novaNetworks(Iterable<Network> networks) {
      this.novaNetworks = ImmutableList.copyOf(networks);
      return this;
   }

   /**
    * @see #getNetworks()
    */
   public CreateServerOptions networks(String... networks) {
      return networks(ImmutableList.copyOf(networks));
   }

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

   /**
    * A keypair name can be defined when creating a server. This key will be
    * linked to the server and used to SSH connect to the machine
    */
   public String getKeyPairName() {
      return keyName;
   }

   public String getAvailabilityZone() {
      return availabilityZone;
   }

   /**
    * Security groups the user specified to run servers with.
    * <p/>
    * <h3>Note</h3>
    * <p/>
    * This requires that {@link NovaApi#getSecurityGroupExtensionApi(String)} to return
    * {@link Optional#isPresent present}
    */
   public Set<String> getSecurityGroupNames() {
      return securityGroupNames;
   }

   /**
    * Get custom networks specified for the server.
    *
    * @return A list of uuids defined by Neutron (previously Quantum)
    * @see <a href="https://wiki.openstack.org/wiki/Neutron/APIv2-specification#Network">Neutron Networks<a/>
    */
   public List<String> getNetworks() {
      return networks;
   }

   /**
    * Get custom networks specified for the server.
    *
    * @return A list of uuids defined by Neutron (previously Quantum)
    * @see <a href="https://wiki.openstack.org/wiki/Neutron/APIv2-specification#Network">Neutron Networks<a/>
    */
   public List<Network> getNovaNetworks() {
      return novaNetworks;
   }

   /**
    * When you create a server from an image with the diskConfig value set to
    * {@link Server#DISK_CONFIG_AUTO}, the server is built with a single partition that is expanded to
    * the disk size of the flavor selected. When you set the diskConfig attribute to
    * {@link Server#DISK_CONFIG_MANUAL}, the server is built by using the partition scheme and file
    * system that is in the source image.
    * <p/>
    * If the target flavor disk is larger, remaining disk space is left unpartitioned. A server inherits the diskConfig
    * attribute from the image from which it is created. However, you can override the diskConfig value when you create
    * a server. This field is only present if the Disk Config extension is installed in your OpenStack deployment.
    */
   public String getDiskConfig() {
      return diskConfig;
   }

   /**
    * Determines if a configuration drive will be attached to the server or not.
    * This can be used for cloud-init or other configuration purposes.
    */
   public boolean getConfigDrive() {
      return configDrive;
   }

   /**
    * Block devices that should be attached to the instance at boot time.
    */
   public Set<BlockDeviceMapping> getBlockDeviceMappings() {
      return blockDeviceMappings;
   }

   public static class Builder {
      /**
       * @see CreateServerOptions#writeFileToPath(byte[], String)
       */
      public static CreateServerOptions writeFileToPath(byte[] contents, String path) {
         CreateServerOptions options = new CreateServerOptions();
         return options.writeFileToPath(contents, path);
      }

      /**
       * @see CreateServerOptions#adminPass(String)
       */
      public static CreateServerOptions adminPass(String adminPass) {
         CreateServerOptions options = new CreateServerOptions();
         return options.adminPass(adminPass);
      }

      /**
       * @see CreateServerOptions#metadata(Map<String, String>)
       */
      public static CreateServerOptions metadata(Map<String, String> metadata) {
         CreateServerOptions options = new CreateServerOptions();
         return options.metadata(metadata);
      }

      /**
       * @see CreateServerOptions#keyPairName(String)
       */
      public static CreateServerOptions keyPairName(String keyName) {
         CreateServerOptions options = new CreateServerOptions();
         return options.keyPairName(keyName);
      }

      /**
       * @see CreateServerOptions#securityGroupNames(String...)
       */
      public static CreateServerOptions securityGroupNames(String... groupNames) {
         CreateServerOptions options = new CreateServerOptions();
         if (new CreateServerOptions().securityGroupNames(groupNames) == CreateServerOptions.class.cast(options.securityGroupNames(groupNames)))
            System.out.println("They are fucking equal, dump the cast!!!");
         return CreateServerOptions.class.cast(options.securityGroupNames(groupNames));
      }

      /**
       * @see CreateServerOptions#securityGroupNames(Iterable)
       */
      public static CreateServerOptions securityGroupNames(Iterable<String> groupNames) {
         return CreateServerOptions.class.cast(new CreateServerOptions().securityGroupNames(groupNames));
      }

      /**
       * @see CreateServerOptions#diskConfig(String)
       */
      public static CreateServerOptions diskConfig(String diskConfig) {
         return CreateServerOptions.class.cast(new CreateServerOptions().diskConfig(diskConfig));
      }

      /**
       * @see CreateServerOptions#networks(String...)
       */
      public static CreateServerOptions networks(String... networks) {
         return CreateServerOptions.class.cast(new CreateServerOptions().networks(networks));
      }

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

      /**
       * @see CreateServerOptions#novaNetworks(Iterable)
       */
      public static CreateServerOptions novaNetworks(Iterable<Network> networks) {
         return CreateServerOptions.class.cast(new CreateServerOptions().novaNetworks(networks));
      }

      /**
       * @see CreateServerOptions#availabilityZone(String)
       */
      public static CreateServerOptions availabilityZone(String availabilityZone) {
         return new CreateServerOptions().availabilityZone(availabilityZone);
      }

      /**
       * @see CreateServerOptions#blockDeviceMappings(Set)
       */
      public static CreateServerOptions blockDeviceMappings(Set<BlockDeviceMapping> blockDeviceMappings) {
         return new CreateServerOptions().blockDeviceMappings(blockDeviceMappings);
      }
   }

   @Override
   public <R extends HttpRequest> R bindToRequest(R request, Object input) {
      return jsonBinder.bindToRequest(request, input);
   }
}
