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

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.contains;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.find;
import static com.google.common.io.BaseEncoding.base16;
import static org.jclouds.compute.util.ComputeServiceUtils.metadataAndTagsAsCommaDelimitedValue;
import static org.jclouds.util.Predicates2.retry;

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

import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.jclouds.Constants;
import org.jclouds.collect.Memoized;
import org.jclouds.compute.ComputeServiceAdapter;
import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.HardwareBuilder;
import org.jclouds.compute.domain.Processor;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.domain.Volume;
import org.jclouds.compute.domain.internal.VolumeImpl;
import org.jclouds.compute.predicates.ImagePredicates;
import org.jclouds.compute.reference.ComputeServiceConstants;
import org.jclouds.compute.reference.ComputeServiceConstants.Timeouts;
import org.jclouds.domain.Location;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.glesys.GleSYSApi;
import org.jclouds.glesys.compute.options.GleSYSTemplateOptions;
import org.jclouds.glesys.domain.AllowedArgumentsForCreateServer;
import org.jclouds.glesys.domain.OSTemplate;
import org.jclouds.glesys.domain.Server;
import org.jclouds.glesys.domain.ServerDetails;
import org.jclouds.glesys.domain.ServerSpec;
import org.jclouds.glesys.options.CreateServerOptions;
import org.jclouds.glesys.options.DestroyServerOptions;
import org.jclouds.location.predicates.LocationPredicates;
import org.jclouds.logging.Logger;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListeningExecutorService;

/**
 * defines the connection between the {@link GleSYSApi} implementation and
 * the jclouds {@link ComputeService}
 */
@Singleton
public class GleSYSComputeServiceAdapter implements ComputeServiceAdapter<ServerDetails, Hardware, OSTemplate, String> {

   @Resource
   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
   protected Logger logger = Logger.NULL;

   private final GleSYSApi api;
   private final ListeningExecutorService userExecutor;
   private final Timeouts timeouts;
   private final Supplier<Set<? extends Location>> locations;

   @Inject
   public GleSYSComputeServiceAdapter(GleSYSApi api,
         @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, Timeouts timeouts,
         @Memoized Supplier<Set<? extends Location>> locations) {
      this.api = checkNotNull(api, "api");
      this.userExecutor = checkNotNull(userExecutor, "userExecutor");
      this.timeouts = checkNotNull(timeouts, "timeouts");
      this.locations = checkNotNull(locations, "locations");
   }

   @Override
   public NodeAndInitialCredentials<ServerDetails> createNodeWithGroupEncodedIntoName(String group, String name,
         Template template) {
      checkNotNull(template, "template was null");
      checkNotNull(template.getOptions(), "template options was null");
      checkArgument(template.getOptions().getClass().isAssignableFrom(GleSYSTemplateOptions.class),
            "options class %s should have been assignable from GleSYSTemplateOptions", template.getOptions().getClass());

      GleSYSTemplateOptions templateOptions = template.getOptions().as(GleSYSTemplateOptions.class);

      CreateServerOptions createServerOptions = new CreateServerOptions();
      createServerOptions.ip(templateOptions.getIp());
      template.getOptions().userMetadata(ComputeServiceConstants.NODE_GROUP_KEY, group);

      Map<String, String> md = metadataAndTagsAsCommaDelimitedValue(template.getOptions());
      if (!md.isEmpty()) {
         String description = Joiner.on('\n').withKeyValueSeparator("=").join(md);
         // TODO: get glesys to stop stripping out equals and commas!
         createServerOptions.description(base16().lowerCase().encode(description.getBytes(UTF_8)));
      }
      ServerSpec.Builder<?> builder = ServerSpec.builder();
      builder.datacenter(template.getLocation().getId());
      builder.templateName(template.getImage().getId());
      builder.platform(template.getHardware().getHypervisor());
      builder.memorySizeMB(template.getHardware().getRam());
      builder.diskSizeGB(Math.round(template.getHardware().getVolumes().get(0).getSize()));
      builder.cpuCores((int) template.getHardware().getProcessors().get(0).getCores());
      builder.transferGB(templateOptions.getTransferGB());
      ServerSpec spec = builder.build();


      // use random root password unless one was provided via template options
      String password = templateOptions.hasRootPassword() ? templateOptions.getRootPassword() : getRandomPassword();

      logger.debug(">> creating new Server spec(%s) name(%s) options(%s)", spec, name, createServerOptions);
      ServerDetails result = api.getServerApi().createWithHostnameAndRootPassword(spec, name, password,
            createServerOptions);
      logger.trace("<< server(%s)", result.getId());

      return new NodeAndInitialCredentials<ServerDetails>(result, result.getId() + "", LoginCredentials.builder()
            .password(password).build());
   }

   /**
    * @return a generated random password string
    */
   private String getRandomPassword() {
      return UUID.randomUUID().toString().replace("-", "");
   }

   @Override
   public Iterable<Hardware> listHardwareProfiles() {
      Set<? extends Location> locationsSet = locations.get();
      ImmutableSet.Builder<Hardware> hardwareToReturn = ImmutableSet.builder();

      // do this loop after dupes are filtered, else OOM
      Set<OSTemplate> images = listImages();

      for (Entry<String, AllowedArgumentsForCreateServer> platformToArgs : api.getServerApi()
            .getAllowedArgumentsForCreateByPlatform().entrySet())
         for (String datacenter : platformToArgs.getValue().getDataCenters())
            for (int diskSizeGB : platformToArgs.getValue().getDiskSizesInGB().getAllowedUnits())
               for (int cpuCores : platformToArgs.getValue().getCpuCoreOptions().getAllowedUnits())
                  for (int memorySizeMB : platformToArgs.getValue().getMemorySizesInMB().getAllowedUnits()) {
                     ImmutableSet.Builder<String> templatesSupportedBuilder = ImmutableSet.builder();
                     for (OSTemplate template : images) {
                        if (template.getPlatform().equals(platformToArgs.getKey())
                              && diskSizeGB >= template.getMinDiskSize() && memorySizeMB >= template.getMinMemSize())
                           templatesSupportedBuilder.add(template.getName());
                     }
                     ImmutableSet<String> templatesSupported = templatesSupportedBuilder.build();
                     if (!templatesSupported.isEmpty())
                        hardwareToReturn.add(new HardwareBuilder()
                              .ids(String.format(
                                    "datacenter(%s)platform(%s)cpuCores(%d)memorySizeMB(%d)diskSizeGB(%d)", datacenter,
                                    platformToArgs.getKey(), cpuCores, memorySizeMB, diskSizeGB)).ram(memorySizeMB)
                              .processors(ImmutableList.of(new Processor(cpuCores, 1.0)))
                              .volumes(ImmutableList.<Volume> of(new VolumeImpl((float) diskSizeGB, true, true)))
                              .hypervisor(platformToArgs.getKey())
                              .location(find(locationsSet, LocationPredicates.idEquals(datacenter)))
                              .supportsImage(ImagePredicates.idIn(templatesSupported)).build());
                  }

      return hardwareToReturn.build();
   }

   @Override
   public Set<OSTemplate> listImages() {
      return api.getServerApi().listTemplates().toSet();
   }

   // cheat until we have a getTemplate command
   @Override
   public OSTemplate getImage(final String id) {
      return find(listImages(), new Predicate<OSTemplate>() {

         @Override
         public boolean apply(OSTemplate input) {
            return input.getName().equals(id);
         }

      }, null);
   }

   @Override
   public Iterable<ServerDetails> listNodes() {
      return api.getServerApi().list().transform(new Function<Server, ServerDetails>() {
         public ServerDetails apply(Server from) {
            return api.getServerApi().get(from.getId());
         }
      });
   }

   @Override
   public Iterable<ServerDetails> listNodesByIds(final Iterable<String> ids) {
      return filter(listNodes(), new Predicate<ServerDetails>() {

            @Override
            public boolean apply(ServerDetails server) {
               return contains(ids, server.getId());
            }
         });
   }

   @Override
   public Set<String> listLocations() {
      return FluentIterable.from(api.getServerApi().getAllowedArgumentsForCreateByPlatform().values())
            .transformAndConcat(new Function<AllowedArgumentsForCreateServer, Set<String>>() {
               @Override
               public Set<String> apply(AllowedArgumentsForCreateServer arg0) {
                  return arg0.getDataCenters();
               }
            }).toSet();
   }

   @Override
   public ServerDetails getNode(String id) {
      return api.getServerApi().get(id);
   }

   @Override
   public void destroyNode(String id) {
      retry(new Predicate<String>() {
         public boolean apply(String arg0) {
            try {
               api.getServerApi().destroy(arg0, DestroyServerOptions.Builder.discardIp());
               return true;
            } catch (IllegalStateException e) {
               return false;
            }
         }
      }, timeouts.nodeTerminated).apply(id);
   }

   @Override
   public void rebootNode(String id) {
      api.getServerApi().reboot(id);
   }

   @Override
   public void resumeNode(String id) {
      api.getServerApi().start(id);
   }

   @Override
   public void suspendNode(String id) {
      api.getServerApi().stop(id);
   }
}
