/*
 * 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.apache.brooklyn.location.jclouds;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.OperatingSystem;
import org.jclouds.compute.domain.OsFamily;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.math.DoubleMath;

@Beta
/** NB: subclasses must implement {@link #clone()} */
public class BrooklynImageChooser implements Cloneable {

    private static final Logger log = LoggerFactory.getLogger(BrooklynImageChooser.class);
    
    protected ComputeService computeService;
    protected ConfigBag config;
    protected String cloudProviderName;
    
    protected static int compare(double left, double right) {
        return DoubleMath.fuzzyCompare(left, right, 0.00000001);
    }
    
    protected static boolean imageNameContains(Image img, String pattern) {
        if (img.getName()==null) return false;
        return img.getName().contains(pattern);
    }
    
    protected static boolean imageNameContainsCaseInsensitive(Image img, String pattern) {
        if (img.getName()==null) return false;
        return img.getName().toLowerCase().contains(pattern.toLowerCase());
    }
    
    protected static boolean imageNameContainsWordCaseInsensitive(Image img, String pattern) {
        if (img.getName()==null) return false;
        return img.getName().toLowerCase().matches("(.*[^a-z])?"+pattern.toLowerCase()+"([^a-z].*)?");
    }
    
    protected double punishmentForOldOsVersions(Image img, OsFamily family, double minVersion) {
        OperatingSystem os = img.getOperatingSystem();
        if (os!=null && family.equals(os.getFamily())) {
            String v = os.getVersion();
            if (v!=null) {
                try {
                    double vd = Double.parseDouble(v);
                    // punish older versions, with a -log function (so 0.5 version behind is -log(1.5)=-0.5 and 2 versions behind is -log(3)=-1.2  
                    if (vd < minVersion) return -Math.log(1+(minVersion - vd));
                } catch (Exception e) {
                    /* ignore unparseable versions */
                }
            }
        }
        return 0;
    }
    
    public List<String> blackListedImageIds() {
        return Arrays.asList(
                // bad natty image - causes 403 on attempts to apt-get; https://bugs.launchpad.net/ubuntu/+bug/987182
                "us-east-1/ami-1cb30875",
                // wrong login user advertised, causes "Error Invalid packet: indicated length 1349281121 too large"
                // from sshj due to message coming back "Plea"(se log in as another user), according to https://github.com/jclouds/legacy-jclouds/issues/748
                "us-east-1/ami-08faa660"
            );
    }

    public List<String> whilelistedImageIds() {
        return Arrays.asList(
        // these are the ones we recommend in brooklyn.properties, but now autodetection should be more reliable
//                "us-east-1/ami-d0f89fb9",
//                "us-west-1/ami-fe002cbb",
//                "us-west-2/ami-70f96e40",
//                "eu-west-1/ami-ce7b6fba",
//                "sa-east-1/ami-a3da00be",
//                "ap-southeast-1/ami-64084736",
//                "ap-southeast-2/ami-04ea7a3e",
//                "ap-northeast-1/ami-fe6ceeff"
            );
    }
    
    public double score(Image img) {
        double score = 0;

        if (blackListedImageIds().contains(img.getId()))
            score -= 50;

        if (whilelistedImageIds().contains(img.getId()))
            // NB: this should be less than deprecated punishment to catch deprecation of whitelisted items
            score += 20;

        score += punishmentForDeprecation(img);

    
        // prefer these guys, in stock brooklyn provisioning

        OperatingSystem os = img.getOperatingSystem();
        if (os!=null) {
            if (os.getFamily()!=null) {
                // preference for these open, popular OS (but only wrt versions above) 
                if (os.getFamily().equals(OsFamily.CENTOS)) {
                    score += punishmentForOldOsVersions(img, OsFamily.CENTOS, 7);
                    score += 3;

                }
                else if (os.getFamily().equals(OsFamily.UBUNTU)) {
                    score += punishmentForOldOsVersions(img, OsFamily.UBUNTU, 12);
                    score += 2;

                    // prefer these LTS releases slightly above others
                    // (but note in AWS Virginia, at least, version is empty for the 14.04 images for some reason, as of Aug 2014)
                    if ("18.04".equals(os.getVersion())) score += 0.4;
                    else if ("16.04".equals(os.getVersion())) score += 0.3;
                    else if ("14.04".equals(os.getVersion())) score += 0.2;
                    else if ("12.04".equals(os.getVersion())) score += 0.1;

                    // NB some 13.10 images take 20m+ before they are sshable on AWS
                    // with "vesafb: module verification error" showing in the AWS system log
                }

                // slight preference for these 
                else if (os.getFamily().equals(OsFamily.RHEL)) score += 1;
                else if (os.getFamily().equals(OsFamily.AMZN_LINUX)) score += 1;
                else if (os.getFamily().equals(OsFamily.DEBIAN)) score += 1;

                // prefer to take our chances with unknown / unlabelled linux than something explicitly windows
                else if (os.getFamily().equals(OsFamily.WINDOWS)) score -= 1;
                
                if ("softlayer".equals(cloudProviderName)) {
                    // on softlayer, prefer images where family is part of the image id
                    // (this is the only way to identiy official images; but in other clouds
                    // it can cause not-so-good images to get selected!)
                    if (img.getId().toLowerCase().contains(os.getFamily().toString().toLowerCase()))
                        score += 0.5;
                }
            }
            
            // prefer 64-bit
            if (os.is64Bit()) score += 0.5;

            // prefer 'normal' releases of the OS, especially on Azure for 'OpenLogic' images.
            // See https://issues.apache.org/jira/browse/BROOKLYN-607
            // e.g. {id=northeurope/OpenLogic/CentOS-HPC/7.4, providerId=OpenLogic, name=CentOS-HPC, location={scope=REGION, id=northeurope, description=North Europe, parent=azurecompute-arm, iso3166Codes=[IE]}, os={family=centos, version=7.4, description=7.4, is64Bit=true}, description=7.4, version=7.4, status=AVAILABLE, loginUser=jclouds}
            //      {id=northeurope/OpenLogic/CentOS-SRIOV/7.3-SRIOV, providerId=OpenLogic, name=CentOS-SRIOV, location={scope=REGION, id=northeurope, description=North Europe, parent=azurecompute-arm, iso3166Codes=[IE]}, os={family=centos, version=7.3-SRIOV, description=7.3-SRIOV, is64Bit=true}, description=7.3-SRIOV, version=7.3-SRIOV, status=AVAILABLE, loginUser=jclouds}
            //      {id=northeurope/OpenLogic/CentOS/7.6, providerId=OpenLogic, name=CentOS, location={scope=REGION, id=northeurope, description=North Europe, parent=azurecompute-arm, iso3166Codes=[IE]}, os={family=centos, version=7.6, description=7.6, is64Bit=true}, description=7.6, version=7.6, status=AVAILABLE, loginUser=jclouds}
            if (img.getName() != null) {
                String name = img.getName();
                if (name.contains("-SRIOV")) {
                    score -= 0.05;
                } else if (name.contains("-CI")) {
                    score -= 0.05;
                } else if (name.contains("-HPC")) {
                    score -= 0.05;
                } else if (name.contains("-LVM")) {
                    score -= 0.05;
                }
            }
        }

        // TODO prefer known providerIds

        if (log.isTraceEnabled())
            log.trace("initial score "+score+" for "+img);
        
        return score;
    }

    protected double punishmentForDeprecation(Image img) {
        // google deprecation strategy
        //        userMetadata={deprecatedState=DEPRECATED}}
        String deprecated = img.getUserMetadata().get("deprecatedState");
        if (deprecated!=null) {
            if ("deprecated".equalsIgnoreCase(deprecated))
                return -30;
            if ("obsolete".equalsIgnoreCase(deprecated))
                return -40;
            log.warn("Unrecognised 'deprecatedState' value '"+deprecated+"' when scoring "+img+"; ignoring that metadata");
        }
        
        // common strategies
        if (imageNameContainsWordCaseInsensitive(img, "deprecated")) return -30;
        if (imageNameContainsWordCaseInsensitive(img, "alpha")) return -10;
        if (imageNameContainsWordCaseInsensitive(img, "beta")) return -5;
        if (imageNameContainsWordCaseInsensitive(img, "testing")) return -5;
        if (imageNameContainsWordCaseInsensitive(img, "rc")) return -3;

        // no indication this is deprecated
        return 0;
    }

    @Override
    public BrooklynImageChooser clone() {
        return new BrooklynImageChooser();
    }
    
    protected void use(ComputeService service) {
        if (this.computeService!=null && !this.computeService.equals(service))
            throw new IllegalStateException("ImageChooser must be cloned to set a compute service");
        this.computeService = service;
        if (computeService!=null) {
            cloudProviderName = computeService.getContext().unwrap().getId();
        }
    }
    
    protected void use(ConfigBag config) {
        if (this.config !=null && !this.config.equals(config))
            throw new IllegalStateException("ImageChooser must be cloned to set config");
        this.config = config;
    }
    
    public BrooklynImageChooser cloneFor(ComputeService service) {
        BrooklynImageChooser result = clone();
        result.use(service);
        return result;
    }
    
    public BrooklynImageChooser cloneFor(ConfigBag config) {
        BrooklynImageChooser result = clone();
        result.use(config);
        return result;
    }
    
    public static class OrderingScoredWithoutDefaults extends Ordering<Image> implements ComputeServiceAwareChooser<OrderingScoredWithoutDefaults>,
            ConfigAwareChooser<OrderingScoredWithoutDefaults>{
        private BrooklynImageChooser chooser;
        public OrderingScoredWithoutDefaults(BrooklynImageChooser chooser) {
            this.chooser = chooser;
        }
        @Override
        public int compare(Image left, Image right) {
            return BrooklynImageChooser.compare(chooser.score(left), chooser.score(right));
        }
        @Override
        public OrderingScoredWithoutDefaults cloneFor(ComputeService service) {
            return new OrderingScoredWithoutDefaults(chooser.cloneFor(service));
        }        
        @Override
        public OrderingScoredWithoutDefaults cloneFor(ConfigBag config) {
            return new OrderingScoredWithoutDefaults(chooser.cloneFor(config));
        }        
    }
    
    public Ordering<Image> orderingScoredWithoutDefaults() {
        return new OrderingScoredWithoutDefaults(this);
    }
    
    /** @deprecated since 0.7.0; kept for persisted state backwards compatibility */
    @Deprecated
    @SuppressWarnings("unused")
    private Ordering<Image> orderingScoredWithoutDefaultsDeprecated() {
        return new Ordering<Image>() {
            @Override
            public int compare(Image left, Image right) {
                return BrooklynImageChooser.compare(score(left), score(right));
            }
        };
    }
    
    public static class OrderingWithDefaults extends Ordering<Image> implements ComputeServiceAwareChooser<OrderingWithDefaults>,
            ConfigAwareChooser<OrderingWithDefaults> {
        Ordering<Image> primaryOrdering;
        public OrderingWithDefaults(final Ordering<Image> primaryOrdering) {
            this.primaryOrdering = primaryOrdering;
        }
        @Override
        public int compare(Image left, Image right) {
            return ComparisonChain.start()
                .compare(left, right, primaryOrdering)
                // fall back to default strategy otherwise, except preferring *non*-null values
                // TODO use AlphaNum string comparator
                .compare(left.getName(), right.getName(), Ordering.<String> natural().nullsFirst())
                .compare(left.getVersion(), right.getVersion(), Ordering.<String> natural().nullsFirst())
                .compare(left.getDescription(), right.getDescription(), Ordering.<String> natural().nullsFirst())
                .compare(left.getOperatingSystem().getName(), right.getOperatingSystem().getName(), Ordering.<String> natural().nullsFirst())
                .compare(left.getOperatingSystem().getVersion(), right.getOperatingSystem().getVersion(), Ordering.<String> natural().nullsFirst())
                .compare(left.getOperatingSystem().getDescription(), right.getOperatingSystem().getDescription(), Ordering.<String> natural().nullsFirst())
                .compare(left.getOperatingSystem().getArch(), right.getOperatingSystem().getArch(), Ordering.<String> natural().nullsFirst()).result();
        }    
        @Override
        public OrderingWithDefaults cloneFor(ComputeService service) {
            if (primaryOrdering instanceof ComputeServiceAwareChooser) {
                return new OrderingWithDefaults( BrooklynImageChooser.cloneFor(primaryOrdering, service) );
            }
            return this;
        }        
        @Override
        public OrderingWithDefaults cloneFor(ConfigBag config) {
            if (primaryOrdering instanceof ConfigAwareChooser) {
                return new OrderingWithDefaults( BrooklynImageChooser.cloneFor(primaryOrdering, config) );
            }
            return this;
        }        
    }
    
    public static Ordering<Image> orderingWithDefaults(final Ordering<Image> primaryOrdering) {
        return new OrderingWithDefaults(primaryOrdering);
    }
    
    /** @deprecated since 0.7.0; kept for persisted state backwards compatibility */
    @Deprecated
    @SuppressWarnings("unused")
    private static Ordering<Image> orderingWithDefaultsDeprecated(final Ordering<Image> primaryOrdering) {
        return new Ordering<Image>() {
            @Override
            public int compare(Image left, Image right) {
                return ComparisonChain.start()
                    .compare(left, right, primaryOrdering)
                    // fall back to default strategy otherwise, except preferring *non*-null values
                    // TODO suggest to use NaturalOrderComparator (so 10>9) then order by:
                    // 1) `name.replaceAll("([^0-9]+)", " ")` 
                    // 2) shortest non-empty name
                    // 3) version (NaturalOrderComparator, prefer last)
                    // 4) name (NaturalOrderComparator, prefer last)
                    // 5) other fields (NaturalOrderComparator, prefer last)
                    .compare(left.getName(), right.getName(), Ordering.<String> natural().nullsFirst())
                    .compare(left.getVersion(), right.getVersion(), Ordering.<String> natural().nullsFirst())
                    .compare(left.getDescription(), right.getDescription(), Ordering.<String> natural().nullsFirst())
                    .compare(left.getOperatingSystem().getName(), right.getOperatingSystem().getName(), Ordering.<String> natural().nullsFirst())
                    .compare(left.getOperatingSystem().getVersion(), right.getOperatingSystem().getVersion(), Ordering.<String> natural().nullsFirst())
                    .compare(left.getOperatingSystem().getDescription(), right.getOperatingSystem().getDescription(), Ordering.<String> natural().nullsFirst())
                    .compare(left.getOperatingSystem().getArch(), right.getOperatingSystem().getArch(), Ordering.<String> natural().nullsFirst()).result();
            }
        };
    }
    
    public static class ImageChooserFromOrdering implements Function<Iterable<? extends Image>, Image>, 
            ComputeServiceAwareChooser<ImageChooserFromOrdering>, ConfigAwareChooser<ImageChooserFromOrdering> {
        final List<Ordering<? super Image>> orderings;
        
        public ImageChooserFromOrdering(final Ordering<Image> ordering) {
            this(ImmutableList.of(ordering));
        }
        public ImageChooserFromOrdering(Iterable<? extends Ordering<? super Image>> orderings) {
            this.orderings = ImmutableList.copyOf(checkNotNull(orderings, "orderings"));
        }
        @Override
        public Image apply(Iterable<? extends Image> input) {
            List<? extends Image> maxImages = multiMax(Ordering.compound(orderings), input);
            return maxImages.get(maxImages.size() - 1);
        }
        @Override
        public ImageChooserFromOrdering cloneFor(ComputeService service) {
            if (Iterables.tryFind(orderings, Predicates.instanceOf(ComputeServiceAwareChooser.class)).isPresent()) {
                List<Ordering<? super Image>> clonedOrderings = Lists.newArrayList();
                for (Ordering<? super Image> ordering : orderings) {
                    if (ordering instanceof ComputeServiceAwareChooser) {
                        clonedOrderings.add(BrooklynImageChooser.cloneFor(ordering, service));
                    } else {
                        clonedOrderings.add(ordering);
                    }
                }
                return new ImageChooserFromOrdering(clonedOrderings);
            } else {
                return this;
            }
        }        
        @Override
        public ImageChooserFromOrdering cloneFor(ConfigBag config) {
            if (Iterables.tryFind(orderings, Predicates.instanceOf(ConfigAwareChooser.class)).isPresent()) {
                List<Ordering<? super Image>> clonedOrderings = Lists.newArrayList();
                for (Ordering<? super Image> ordering : orderings) {
                    if (ordering instanceof ConfigAwareChooser) {
                        clonedOrderings.add(BrooklynImageChooser.cloneFor(ordering, config));
                    } else {
                        clonedOrderings.add(ordering);
                    }
                }
                return new ImageChooserFromOrdering(clonedOrderings);
            } else {
                return this;
            }
        }

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

    public static Function<Iterable<? extends Image>, Image> imageChooserFromOrdering(final Ordering<Image> ordering) {
        return new ImageChooserFromOrdering(ordering);
    }
    
    public static Function<Iterable<? extends Image>, Image> imageChooserFromOrderings(Iterable<? extends Ordering<? super Image>> orderings) {
        return new ImageChooserFromOrdering(orderings);
    }

    /** @deprecated since 0.7.0; kept for persisted state backwards compatibility */
    @Deprecated
    @SuppressWarnings("unused")
    private static Function<Iterable<? extends Image>, Image> imageChooserFromOrderingDeprecated(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);
                return maxImages.get(maxImages.size() - 1);
            }
        };
    }
    
    protected interface ComputeServiceAwareChooser<T> {
        public T cloneFor(ComputeService service);
    }

    protected interface ConfigAwareChooser<T> {
        public T cloneFor(ConfigBag config);
    }

    /**
     * Attempts to clone the given item for use with the given {@link ComputeService} and/or the
     * given config, if the item is {@link ComputeServiceAwareChooser} or {@link ConfigAwareChooser}; 
     * otherwise it returns the item unchanged.
     */
    public static <T> T cloneFor(T item, ComputeService service, ConfigBag config) {
        T result = cloneFor(item, service);
        return cloneFor(result, config);
    }

    /**
     * Attempts to clone the given item for use with the given {@link ComputeService}, if
     * the item is {@link ComputeServiceAwareChooser}; otherwise it returns the item unchanged.
     */
    @SuppressWarnings("unchecked")
    public static <T> T cloneFor(T item, ComputeService service) {
        if (item instanceof ComputeServiceAwareChooser) {
            return ((ComputeServiceAwareChooser<T>)item).cloneFor(service);
        }
        return item;
    }

    /**
     * Attempts to clone the given item for use with the given {@link ConfigBag}, if
     * the item is {@link ConfigAwareChooser}; otherwise it returns the item unchanged.
     */
    @SuppressWarnings("unchecked")
    public static <T> T cloneFor(T item, ConfigBag service) {
        if (item instanceof ConfigAwareChooser) {
            return ((ConfigAwareChooser<T>)item).cloneFor(service);
        }
        return item;
    }

    // from jclouds
    static <T, E extends T> List<E> multiMax(Comparator<T> ordering, Iterable<E> iterable) {
        Iterator<E> iterator = iterable.iterator();
        List<E> maxes = MutableList.of(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 = MutableList.of(current);
              maxSoFar = current;
           }
        }
        return maxes;
     }
    
    public Ordering<Image> ordering() {
        return orderingWithDefaults(orderingScoredWithoutDefaults());
    }

    public Function<Iterable<? extends Image>,Image> chooser() {
        return imageChooserFromOrdering(ordering());
    }

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

}
