/*
 * 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.compute.domain.internal;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Doubles;
import org.jclouds.collect.Memoized;
import org.jclouds.compute.domain.ComputeMetadata;
import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.OperatingSystem;
import org.jclouds.compute.domain.OsFamily;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.domain.TemplateBuilder;
import org.jclouds.compute.domain.TemplateBuilderSpec;
import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.compute.predicates.ImagePredicates;
import org.jclouds.compute.reference.ComputeServiceConstants;
import org.jclouds.compute.suppliers.ImageCacheSupplier;
import org.jclouds.domain.Location;
import org.jclouds.logging.Logger;

import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.find;
import static com.google.common.collect.Iterables.size;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Iterables.tryFind;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.format;
import static org.jclouds.compute.util.ComputeServiceUtils.getCores;
import static org.jclouds.compute.util.ComputeServiceUtils.getCoresAndSpeed;
import static org.jclouds.compute.util.ComputeServiceUtils.getSpace;

public class TemplateBuilderImpl implements TemplateBuilder {
   @Resource
   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
   protected Logger logger = Logger.NULL;

   protected final ImageCacheSupplier images;
   protected final Supplier<Set<? extends Hardware>> hardwares;
   protected final Supplier<Set<? extends Location>> locations;
   protected final Supplier<Location> defaultLocation;
   protected final Provider<TemplateOptions> optionsProvider;
   protected final Provider<TemplateBuilder> defaultTemplateProvider;

   @VisibleForTesting
   protected Location location;
   @VisibleForTesting
   protected String imageId;
   @VisibleForTesting
   protected String hardwareId;
   @VisibleForTesting
   protected String hypervisor;
   @VisibleForTesting
   protected String imageVersion;
   @VisibleForTesting
   protected OsFamily osFamily;
   @VisibleForTesting
   protected String osVersion;
   @VisibleForTesting
   protected Boolean os64Bit;
   @VisibleForTesting
   protected String osName;
   @VisibleForTesting
   protected String osDescription;
   @VisibleForTesting
   protected String osArch;
   @VisibleForTesting
   protected String imageName;
   @VisibleForTesting
   protected String imageDescription;
   @VisibleForTesting
   protected Predicate<Image> imagePredicate;
   @VisibleForTesting
   protected Function<Iterable<? extends Image>, Image> imageChooser;
   @VisibleForTesting
   protected double minCores;
   @VisibleForTesting
   protected int minRam;
   @VisibleForTesting
   protected double minDisk;
   @VisibleForTesting
   protected boolean biggest;
   @VisibleForTesting
   protected boolean fastest;
   @VisibleForTesting
   protected TemplateOptions options;
   @VisibleForTesting
   protected Boolean forceCacheReload;

   @Inject
   protected TemplateBuilderImpl(@Memoized Supplier<Set<? extends Location>> locations,
         @Memoized Supplier<Set<? extends Image>> images, @Memoized Supplier<Set<? extends Hardware>> hardwares,
         Supplier<Location> defaultLocation, @Named("DEFAULT") Provider<TemplateOptions> optionsProvider,
         @Named("DEFAULT") Provider<TemplateBuilder> defaultTemplateProvider) {
      this.locations = checkNotNull(locations, "locations");
      checkArgument(images instanceof ImageCacheSupplier, "an instance of the ImageCacheSupplier is needed");
      this.images = ImageCacheSupplier.class.cast(images);
      this.hardwares = checkNotNull(hardwares, "hardwares");
      this.defaultLocation = checkNotNull(defaultLocation, "defaultLocation");
      this.optionsProvider = checkNotNull(optionsProvider, "optionsProvider");
      this.defaultTemplateProvider = checkNotNull(defaultTemplateProvider, "defaultTemplateProvider");
   }

   static Predicate<Hardware> supportsImagesPredicate(final Iterable<? extends Image> images) {
      return new Predicate<Hardware>() {
         @Override
         public boolean apply(final Hardware hardware) {
            return Iterables.any(images, new Predicate<Image>() {

               @Override
               public boolean apply(Image input) {
                  return hardware.supportsImage().apply(input);
               }

               @Override
               public String toString() {
                  return "hardware(" + hardware + ").supportsImage()";
               }

            });

         }

      };
   }

   final Predicate<ComputeMetadata> locationPredicate = new NullEqualToIsParentOrIsGrandparentOfCurrentLocation(new Supplier<Location>() {

      @Override
      public Location get() {
         return location;
      }

   });

   private final Predicate<OperatingSystem> osFamilyPredicate = new Predicate<OperatingSystem>() {

      @Override
      public boolean apply(OperatingSystem input) {
         boolean returnVal = true;
         if (osFamily != null)
            returnVal = osFamily.equals(input.getFamily());
         return returnVal;
      }

      @Override
      public String toString() {
         return "osFamily(" + osFamily + ")";
      }
   };

   private final Predicate<OperatingSystem> osNamePredicate = new Predicate<OperatingSystem>() {
      @Override
      public boolean apply(OperatingSystem input) {
         boolean returnVal = true;
         if (osName != null) {
            if (input.getName() == null)
               returnVal = false;
            else
               returnVal = input.getName().contains(osName) || input.getName().matches(osName);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "osName(" + osName + ")";
      }
   };

   private final Predicate<OperatingSystem> osDescriptionPredicate = new Predicate<OperatingSystem>() {
      @Override
      public boolean apply(OperatingSystem input) {
         boolean returnVal = true;
         if (osDescription != null) {
            if (input.getDescription() == null)
               returnVal = false;
            else
               returnVal = input.getDescription().contains(osDescription)
                     || input.getDescription().matches(osDescription);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "osDescription(" + osDescription + ")";
      }
   };

   private final Predicate<OperatingSystem> osVersionPredicate = new Predicate<OperatingSystem>() {
      @Override
      public boolean apply(OperatingSystem input) {
         boolean returnVal = true;
         if (osVersion != null) {
            if (input.getVersion() == null)
               returnVal = false;
            else
               returnVal = input.getVersion().contains(osVersion) || input.getVersion().matches(osVersion);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "osVersion(" + osVersion + ")";
      }
   };

   private final Predicate<OperatingSystem> os64BitPredicate = new Predicate<OperatingSystem>() {
      @Override
      public boolean apply(OperatingSystem input) {
         boolean returnVal = true;
         if (os64Bit != null) {
            if (os64Bit)
               return input.is64Bit();
            else
               return !input.is64Bit();
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "os64Bit(" + os64Bit + ")";
      }
   };

   private final Predicate<OperatingSystem> osArchPredicate = new Predicate<OperatingSystem>() {
      @Override
      public boolean apply(OperatingSystem input) {
         boolean returnVal = true;
         if (osArch != null) {
            if (input.getArch() == null)
               returnVal = false;
            else
               returnVal = input.getArch().contains(osArch) || input.getArch().matches(osArch);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "osArch(" + osArch + ")";
      }
   };

   private final Predicate<Image> imageVersionPredicate = new Predicate<Image>() {
      @Override
      public boolean apply(Image input) {
         boolean returnVal = true;
         if (imageVersion != null) {
            if (input.getVersion() == null)
               returnVal = false;
            else
               returnVal = input.getVersion().contains(imageVersion) || input.getVersion().matches(imageVersion);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "imageVersion(" + imageVersion + ")";
      }
   };

   private final Predicate<Image> imageNamePredicate = new Predicate<Image>() {
      @Override
      public boolean apply(Image input) {
         boolean returnVal = true;
         if (imageName != null) {
            if (input.getName() == null)
               returnVal = false;
            else
               returnVal = input.getName().equals(imageName) || input.getName().contains(imageName)
                        || input.getName().matches(imageName);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "imageName(" + imageName + ")";
      }
   };

   private final Predicate<Image> imageDescriptionPredicate = new Predicate<Image>() {
      @Override
      public boolean apply(Image input) {
         boolean returnVal = true;
         if (imageDescription != null) {
            if (input.getDescription() == null)
               returnVal = false;
            else
               returnVal = input.getDescription().equals(imageDescription)
                     || input.getDescription().contains(imageDescription)
                     || input.getDescription().matches(imageDescription);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "imageDescription(" + imageDescription + ")";
      }
   };
   private final Predicate<Hardware> hardwareIdPredicate = new Predicate<Hardware>() {
      @Override
      public boolean apply(Hardware input) {
         boolean returnVal = true;
         if (hardwareId != null) {
            returnVal = hardwareId.equals(input.getId());
            // match our input params so that the later predicates pass.
            if (returnVal) {
               fromHardware(input);
            }
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "hardwareId(" + hardwareId + ")";
      }
   };

   private final Predicate<Hardware> hypervisorPredicate = new Predicate<Hardware>() {
      @Override
      public boolean apply(Hardware input) {
         boolean returnVal = true;
         if (hypervisor != null) {
            if (input.getHypervisor() == null)
               returnVal = false;
            else
               returnVal = input.getHypervisor().contains(hypervisor)
                     || input.getHypervisor().matches(hypervisor);
         }
         return returnVal;
      }

      @Override
      public String toString() {
         return "hypervisorMatches(" + hypervisor + ")";
      }
   };

   private final Predicate<Hardware> hardwareCoresPredicate = new Predicate<Hardware>() {
      @Override
      public boolean apply(Hardware input) {
         double cores = getCores(input);
         return cores >= TemplateBuilderImpl.this.minCores;
      }

      @Override
      public String toString() {
         return "minCores(" + minCores + ")";
      }
   };

   private final Predicate<Hardware> hardwareDiskPredicate = new Predicate<Hardware>() {
      @Override
      public boolean apply(Hardware input) {
         return getSpace(input) >= TemplateBuilderImpl.this.minDisk;
      }

      @Override
      public String toString() {
         return "minDisk(" + minDisk + ")";
      }
   };

   private final Predicate<Hardware> hardwareRamPredicate = new Predicate<Hardware>() {
      @Override
      public boolean apply(Hardware input) {
         return input.getRam() >= TemplateBuilderImpl.this.minRam;
      }

      @Override
      public String toString() {
         return "minRam(" + minRam + ")";
      }
   };

   private Predicate<Hardware> buildHardwarePredicate() {
      List<Predicate<Hardware>> predicates = newArrayList();
      if (location != null)
         predicates.add(new Predicate<Hardware>() {

            @Override
            public boolean apply(Hardware input) {
               return locationPredicate.apply(input);
            }

            @Override
            public String toString() {
               return locationPredicate.toString();
            }
         });
      if (hypervisor != null)
         predicates.add(hypervisorPredicate);
      predicates.add(hardwareCoresPredicate);
      predicates.add(hardwareRamPredicate);
      predicates.add(hardwareDiskPredicate);

      // looks verbose, but explicit <Hardware> type needed for this to compile
      // properly
      Predicate<Hardware> hardwarePredicate = predicates.size() == 1 ? Iterables.<Predicate<Hardware>> get(predicates, 0)
            : Predicates.<Hardware> and(predicates);
      return hardwarePredicate;
   }

   static final Ordering<Hardware> DEFAULT_SIZE_ORDERING = new Ordering<Hardware>() {
      public int compare(Hardware left, Hardware right) {
         return ComparisonChain.start().compare(getCores(left), getCores(right)).compare(left.getRam(), right.getRam())
               .compare(getSpace(left), getSpace(right)).result();
      }
   };
   static final Ordering<Hardware> BY_CORES_ORDERING = new Ordering<Hardware>() {
      public int compare(Hardware left, Hardware right) {
         return Doubles.compare(getCoresAndSpeed(left), getCoresAndSpeed(right));
      }
   };
   static final Ordering<Hardware> NOT_DEPRECATED_ORDERING = new Ordering<Hardware>() {
      public int compare(Hardware left, Hardware right) {
         // we take max so deprecated items come first
         return ComparisonChain.start().compareTrueFirst(left.isDeprecated(), right.isDeprecated()).result();
      }
   };
   static final Ordering<Image> DEFAULT_IMAGE_ORDERING = new Ordering<Image>() {
      public int compare(Image left, Image right) {
         /* This currently, and for some time, has *preferred* images whose fields are null,
          * and prefers those which come last alphabetically.
          * It seems preferable to take images whose fields are *not* null, ie nullsFirst;
          * and to use something like the AlphaNum Algorithm then take the last
          * (so "Ubuntu 13.04" would be preferred over "Ubuntu 9.10").
          * However not changing it now as people may be surprised if the images they get back start changing.
          */
         return ComparisonChain.start()
               .compare(left.getName(), right.getName(), Ordering.<String> natural().nullsLast())
               .compare(left.getVersion(), right.getVersion(), Ordering.<String> natural().nullsLast())
               .compare(left.getDescription(), right.getDescription(), Ordering.<String> natural().nullsLast())
               .compare(left.getOperatingSystem().getName(), right.getOperatingSystem().getName(),
                     Ordering.<String> natural().nullsLast())
               .compare(left.getOperatingSystem().getVersion(), right.getOperatingSystem().getVersion(),
                     Ordering.<String> natural().nullsLast())
               .compare(left.getOperatingSystem().getDescription(), right.getOperatingSystem().getDescription(),
                     Ordering.<String> natural().nullsLast())
               .compare(left.getOperatingSystem().getArch(), right.getOperatingSystem().getArch(),
                     Ordering.<String> natural().nullsLast()).result();
      }
   };

   @VisibleForTesting
   // non-static for logging
   final Function<Iterable<? extends Image>, Image> imageChooserFromOrdering(final Ordering<Image> ordering) {
      return new Function<Iterable<? extends Image>, Image>() {
         @Override
         public Image apply(Iterable<? extends Image> input) {
            List<? extends Image> maxImages = multiMax(ordering, input);
            if (logger.isTraceEnabled())
               logger.trace("<<   best images(%s)", transform(maxImages, imageToId));
            return maxImages.get(maxImages.size() - 1);
         }
      };
   }

   @VisibleForTesting
   Function<Iterable<? extends Image>, Image> defaultImageChooser() {
       return imageChooserFromOrdering(DEFAULT_IMAGE_ORDERING);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder fromTemplate(Template template) {
      location = template.getLocation();
      fromHardware(template.getHardware());
      fromImage(template.getImage());
      options(template.getOptions());
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder fromHardware(Hardware hardware) {
      if (currentLocationWiderThan(hardware.getLocation()))
         this.location = hardware.getLocation();
      this.minCores = getCores(hardware);
      this.minRam = hardware.getRam();
      this.minDisk = getSpace(hardware);
      this.hypervisor = hardware.getHypervisor();
      return this;
   }

   private boolean currentLocationWiderThan(Location location) {
      return this.location == null || (location != null && this.location.getScope().compareTo(location.getScope()) < 0);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder fromImage(Image image) {
      if (currentLocationWiderThan(image.getLocation()))
         this.location = image.getLocation();
      if (image.getOperatingSystem().getFamily() != null)
         this.osFamily = image.getOperatingSystem().getFamily();
      if (image.getName() != null)
         this.imageName = image.getName();
      if (image.getDescription() != null)
         this.imageDescription = String.format("^%s$", Pattern.quote(image.getDescription()));
      if (image.getOperatingSystem().getName() != null)
         this.osName = image.getOperatingSystem().getName();
      if (image.getOperatingSystem().getDescription() != null)
         this.osDescription = image.getOperatingSystem().getDescription();
      if (image.getVersion() != null)
         this.imageVersion = String.format("^%s$", Pattern.quote(image.getVersion()));
      if (image.getOperatingSystem().getVersion() != null)
         this.osVersion = image.getOperatingSystem().getVersion();
      this.os64Bit = image.getOperatingSystem().is64Bit();
      if (image.getOperatingSystem().getArch() != null)
         this.osArch = image.getOperatingSystem().getArch();
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder smallest() {
      this.biggest = false;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder biggest() {
      this.biggest = true;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder fastest() {
      this.fastest = true;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder locationId(final String locationId) {
      Set<? extends Location> locations = this.locations.get();
      try {
         this.location = find(locations, new Predicate<Location>() {

            @Override
            public boolean apply(Location input) {
               return input.getId().equals(locationId);
            }

            @Override
            public String toString() {
               return "locationId(" + locationId + ")";
            }

         });
      } catch (NoSuchElementException e) {
         throw new NoSuchElementException(format("location id %s not found in: %s", locationId, locations));
      }
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder osFamily(OsFamily os) {
      this.osFamily = os;
      return this;
   }

   private static final Function<Image, String> imageToId = new Function<Image, String>() {

      @Override
      public String apply(Image arg0) {
         return arg0.getId();
      }

   };


   private static final Function<Hardware, String> hardwareToId = new Function<Hardware, String>() {

      @Override
      public String apply(Hardware arg0) {
         return arg0.getId();
      }

   };
   /**
    * {@inheritDoc}
    */
   @Override
   public Template build() {
      if (nothingChangedExceptOptions()) {
         TemplateBuilder defaultTemplate = defaultTemplateProvider.get();
         if (options != null)
            defaultTemplate.options(options);
         return defaultTemplate.build();
      }

      if (options == null)
         options = optionsProvider.get();
      logger.debug(">> searching params(%s)", this);
      Set<? extends Image> images = getImages();
      checkState(!images.isEmpty(), "no images present!");
      Set<? extends Hardware> hardwaresToSearch = hardwares.get();
      checkState(!hardwaresToSearch.isEmpty(), "no hardware profiles present!");

      Image image = null;
      if (imageId != null) {
         image = loadImageWithId(images);
         if (currentLocationWiderThan(image.getLocation()))
            this.location = image.getLocation();
      }

      Hardware hardware = null;
      if (hardwareId != null) {
         hardware = findHardwareWithId(hardwaresToSearch);
         if (currentLocationWiderThan(hardware.getLocation()))
            this.location = hardware.getLocation();
      }

      // if the user hasn't specified a location id, or an image or hardware
      // with location, let's search scoped to the implicit one
      if (location == null)
         location = defaultLocation.get();

      if (image == null) {
         Iterable<? extends Image> supportedImages = findSupportedImages(images);
         if (hardware == null)
            hardware = resolveHardware(hardwaresToSearch, supportedImages);
         image = resolveImage(hardware, supportedImages);
      } else {
         if (hardware == null)
            hardware = resolveHardware(hardwaresToSearch, ImmutableSet.of(image));
      }

      logger.debug("<<   matched image(%s) hardware(%s) location(%s)", image.getId(), hardware.getId(),
            location.getId());
      return new TemplateImpl(image, hardware, location, options);
   }

   private Iterable<? extends Image> findSupportedImages(Set<? extends Image> images) {
      Predicate<Image> imagePredicate = buildImagePredicate();
      Iterable<? extends Image> supportedImages = filter(images, imagePredicate);
      if (size(supportedImages) == 0) {
         throw throwNoSuchElementExceptionAfterLoggingImageIds(
               format("no image matched predicate: %s", imagePredicate), images);
      }
      return supportedImages;
   }

   private Image loadImageWithId(Iterable<? extends Image> images) {
      Optional<? extends Image> image = tryFind(images, ImagePredicates.idEquals(imageId));
      if (!image.isPresent()) {
         image = this.images.get(imageId); // Load the image from the cache, and refresh if missing
         if (!image.isPresent()) {
            throw throwNoSuchElementExceptionAfterLoggingImageIds(format("imageId(%s) not found", imageId), images);
         }
      }
      fromImage(image.get());
      return image.get();
   }

   protected Hardware findHardwareWithId(Set<? extends Hardware> hardwaresToSearch) {
      Hardware hardware;
      // TODO: switch to GetHardwareStrategy in version 1.5
      hardware = tryFind(hardwaresToSearch, hardwareIdPredicate).orNull();
      if (hardware == null)
         throw throwNoSuchElementExceptionAfterLoggingHardwareIds(format("%s not found", hardwareIdPredicate),
               hardwaresToSearch);
      return hardware;
   }

   protected NoSuchElementException throwNoSuchElementExceptionAfterLoggingImageIds(String message, Iterable<? extends Image> images) {
      NoSuchElementException exception = new NoSuchElementException(message);
      if (logger.isTraceEnabled())
         logger.warn(exception, "image ids that didn't match: %s", transform(images, imageToId));
      throw exception;
   }

   protected NoSuchElementException throwNoSuchElementExceptionAfterLoggingHardwareIds(String message, Iterable<? extends Hardware> hardwares) {
      NoSuchElementException exception = new NoSuchElementException(message);
      if (logger.isTraceEnabled())
         logger.warn(exception, "hardware ids that didn't match: %s", transform(hardwares, hardwareToId));
      throw exception;
   }

   protected Hardware resolveHardware(Set<? extends Hardware> hardwarel, final Iterable<? extends Image> images) {
      Ordering<Hardware> hardwareOrdering = hardwareSorter();

      Iterable<Predicate<Image>> supportsImagePredicates = Iterables.transform(hardwarel,
               new Function<Hardware, Predicate<Image>>() {

                  @Override
                  public Predicate<Image> apply(Hardware input) {
                     return input.supportsImage();
                  }

               });

      Predicate<Image> supportsImagePredicate = Iterables.size(supportsImagePredicates) == 1 ? Iterables
               .getOnlyElement(supportsImagePredicates) : Predicates.<Image>or(supportsImagePredicates);

      if (!Iterables.any(images, supportsImagePredicate)) {
         String message = format("no hardware profiles support images matching params: %s", supportsImagePredicate);
         throw throwNoSuchElementExceptionAfterLoggingHardwareIds(message, hardwarel);
      }

      Iterable<? extends Hardware> hardwareCompatibleWithOurImages = filter(hardwarel, supportsImagesPredicate(images));
      Predicate<Hardware> hardwarePredicate = buildHardwarePredicate();
      Hardware hardware;
      try {
         hardware = hardwareOrdering.max(filter(hardwareCompatibleWithOurImages, hardwarePredicate));
      } catch (NoSuchElementException exception) {
         String message = format("no hardware profiles match params: %s", hardwarePredicate);
         throw throwNoSuchElementExceptionAfterLoggingHardwareIds(message, hardwareCompatibleWithOurImages);
      }
      logger.trace("<<   matched hardware(%s)", hardware.getId());
      return hardware;
   }

   protected Function<Iterable<? extends Image>, Image> imageChooser() {
      if (imageChooser != null) return imageChooser;
      return defaultImageChooser();
   }

   protected Ordering<Hardware> hardwareSorter() {
      Ordering<Hardware> hardwareOrdering = DEFAULT_SIZE_ORDERING;
      if (!biggest)
         hardwareOrdering = hardwareOrdering.reverse();
      if (fastest)
         hardwareOrdering = Ordering.compound(ImmutableList.of(BY_CORES_ORDERING, hardwareOrdering));
      hardwareOrdering = Ordering.compound(ImmutableList.of(NOT_DEPRECATED_ORDERING, hardwareOrdering));
      return hardwareOrdering;
   }

   /**
    *
    * @param hardware
    * @param supportedImages
    * @throws NoSuchElementException
    *            if there's no image that matches the predicate
    */
   protected Image resolveImage(final Hardware hardware, Iterable<? extends Image> supportedImages) {
      Predicate<Image> imagePredicate = new Predicate<Image>() {

         @Override
         public boolean apply(Image arg0) {
            return hardware.supportsImage().apply(arg0);
         }

         @Override
         public String toString() {
            return "hardware(" + hardware + ").supportsImage()";
         }
      };

      try {
         Iterable<? extends Image> matchingImages = filter(supportedImages, imagePredicate);
         if (logger.isTraceEnabled())
            logger.trace("<<   matched images(%s)", transform(matchingImages, imageToId));
         return imageChooser().apply(matchingImages);
      } catch (NoSuchElementException exception) {
         throw throwNoSuchElementExceptionAfterLoggingImageIds(format("no image matched params: %s", toString()),
               supportedImages);
      }
   }

   /**
    * Like Ordering, but handle the case where there are multiple valid maximums
    */
   @SuppressWarnings("unchecked")
   @VisibleForTesting
   static <T, E extends T> List<E> multiMax(Comparator<T> ordering, Iterable<E> iterable) {
      Iterator<E> iterator = iterable.iterator();
      List<E> maxes = newArrayList(iterator.next());
      E maxSoFar = maxes.get(0);
      while (iterator.hasNext()) {
         E current = iterator.next();
         int comparison = ordering.compare(maxSoFar, current);
         if (comparison == 0) {
            maxes.add(current);
         } else if (comparison < 0) {
            maxes = newArrayList(current);
            maxSoFar = current;
         }
      }
      return maxes;
   }
   protected Set<? extends Image> getImages() {
      return forceCacheReload != null && forceCacheReload ? images.rebuildCache() : images.get();
   }

   private Predicate<Image> buildImagePredicate() {
      List<Predicate<Image>> predicates = newArrayList();
      if (location != null)
         predicates.add(new Predicate<Image>() {

            @Override
            public boolean apply(Image input) {
               return locationPredicate.apply(input);
            }

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

      final List<Predicate<OperatingSystem>> osPredicates = newArrayList();
      if (osFamily != null)
         osPredicates.add(osFamilyPredicate);
      if (osName != null)
         osPredicates.add(osNamePredicate);
      if (osDescription != null)
         osPredicates.add(osDescriptionPredicate);
      if (osVersion != null)
         osPredicates.add(osVersionPredicate);
      if (os64Bit != null)
         osPredicates.add(os64BitPredicate);
      if (osArch != null)
         osPredicates.add(osArchPredicate);
      if (!osPredicates.isEmpty())
         predicates.add(new Predicate<Image>() {

            @Override
            public boolean apply(Image input) {
               return Predicates.and(osPredicates).apply(input.getOperatingSystem());
            }

            @Override
            public String toString() {
               return Predicates.and(osPredicates).toString();
            }

         });
      if (imageVersion != null)
         predicates.add(imageVersionPredicate);
      if (imageName != null)
         predicates.add(imageNamePredicate);
      if (imageDescription != null)
         predicates.add(imageDescriptionPredicate);
      if (imagePredicate != null)
         predicates.add(imagePredicate);

      // looks verbose, but explicit <Image> type needed for this to compile
      // properly
      Predicate<Image> imagePredicate = predicates.size() == 1 ? Iterables.<Predicate<Image>> get(predicates, 0)
            : Predicates.<Image> and(predicates);
      return imagePredicate;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder imageId(String imageId) {
      this.imageId = imageId;
      this.imageName = null;
      this.imageDescription = null;
      this.imagePredicate = null;
      this.imageVersion = null;
      this.osFamily = null;
      this.osName = null;
      this.osDescription = null;
      this.osVersion = null;
      this.os64Bit = null;
      this.osArch = null;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder imageNameMatches(String nameRegex) {
      this.imageName = nameRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder imageDescriptionMatches(String descriptionRegex) {
      this.imageDescription = descriptionRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder imageMatches(Predicate<Image> condition) {
      this.imagePredicate = condition;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilderImpl imageChooser(Function<Iterable<? extends Image>, Image> imageChooser) {
      this.imageChooser = imageChooser;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder imageVersionMatches(String imageVersionRegex) {
      this.imageVersion = imageVersionRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder osVersionMatches(String osVersionRegex) {
      this.osVersion = osVersionRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder osArchMatches(String osArchitectureRegex) {
      this.osArch = osArchitectureRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder minCores(double minCores) {
      this.minCores = minCores;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder minRam(int megabytes) {
      this.minRam = megabytes;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder minDisk(double gigabytes) {
      this.minDisk = gigabytes;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder osNameMatches(String osNameRegex) {
      this.osName = osNameRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder osDescriptionMatches(String osDescriptionRegex) {
      this.osDescription = osDescriptionRegex;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder hardwareId(String hardwareId) {
      this.hardwareId = hardwareId;
      this.hypervisor = null;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder hypervisorMatches(String hypervisor) {
      this.hypervisor = hypervisor;
      return this;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder options(TemplateOptions options) {
      this.options = optionsProvider.get();
      checkNotNull(options, "options").copyTo(this.options);
      return this;
   }

   @VisibleForTesting
   boolean nothingChangedExceptOptions() {
      return osFamily == null && location == null && imageId == null && hardwareId == null && hypervisor == null
            && osName == null && imagePredicate == null && imageChooser == null && osDescription == null
            && imageVersion == null && osVersion == null && osArch == null && os64Bit == null && imageName == null
            && imageDescription == null && minCores == 0 && minRam == 0 && minDisk == 0 && !biggest && !fastest;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public TemplateBuilder any() {
      return defaultTemplateProvider.get();
   }

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

   /**
    * @since 1.5
    */
   protected ToStringHelper string() {
      ToStringHelper toString = MoreObjects.toStringHelper("").omitNullValues();
      if (biggest)
         toString.add("biggest", biggest);
      if (fastest)
         toString.add("fastest", fastest);
      toString.add("imageName", imageName);
      toString.add("imageDescription", imageDescription);
      toString.add("imageId", imageId);
      toString.add("imagePredicate", imagePredicate);
      toString.add("imageChooser", imageChooser);
      toString.add("imageVersion", imageVersion);
      if (location != null)
         toString.add("locationId", location.getId());
      if (minCores > 0) //TODO: make non-primitive
         toString.add("minCores", minCores);
      if (minRam > 0) //TODO: make non-primitive
         toString.add("minRam", minRam);
      if (minRam > 0) //TODO: make non-primitive
         toString.add("minRam", minRam);
      if (minDisk > 0) //TODO: make non-primitive
         toString.add("minDisk", minDisk);
      toString.add("osFamily", osFamily);
      toString.add("osName", osName);
      toString.add("osDescription", osDescription);
      toString.add("osVersion", osVersion);
      toString.add("osArch", osArch);
      toString.add("os64Bit", os64Bit);
      toString.add("hardwareId", hardwareId);
      toString.add("hypervisor", hypervisor);
      toString.add("forceCacheReload", forceCacheReload);
      return toString;
   }

   @Override
   public TemplateBuilder os64Bit(boolean is64Bit) {
      this.os64Bit = is64Bit;
      return this;
   }

   @Override
   public TemplateBuilder from(TemplateBuilderSpec spec) {
      return spec.copyTo(this, options != null ? options : (options = optionsProvider.get()));
   }

   @Override
   public TemplateBuilder from(String spec) {
      return from(TemplateBuilderSpec.parse(spec));
   }

   @Override
   public TemplateBuilder forceCacheReload() {
      this.forceCacheReload = true;
      return this;
   }

   
}
