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

import java.net.InetAddress;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

import javax.annotation.Nonnull;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation.LocalhostMachine;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.net.HasNetworkAddresses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Iterables;

/** utilities for working with MachineLocations */
public class Machines {

    private static final Logger log = LoggerFactory.getLogger(Machines.class);
    
    public static Maybe<String> getSubnetHostname(Location where) {
        // TODO Should we look at HasNetworkAddresses? But that's not a hostname.
        String hostname = null;
        if (where instanceof HasSubnetHostname) {
            hostname = ((HasSubnetHostname) where).getSubnetHostname();
        }
        if (hostname == null && where instanceof MachineLocation) {
            Maybe<String> subnetIp = getSubnetIp(where);
            if (subnetIp.isPresent()) {
                hostname = subnetIp.get();
            } else {
                InetAddress addr = ((MachineLocation) where).getAddress();
                if (addr != null) hostname = addr.getHostAddress();
            }
        }
        log.debug("computed subnet hostname {} for {}", hostname, where);
        // TODO if Maybe.absent(message) appears, could/should use that
        // TODO If no machine available, should we throw new IllegalStateException("Cannot find hostname for "+where);
        return Maybe.fromNullable(hostname);
    }

    public static Maybe<String> getSubnetIp(Location where) {
        // TODO Too much duplication between the ip and hostname methods
        String result = null;
        if (where instanceof HasSubnetHostname) {
            result = ((HasSubnetHostname) where).getSubnetIp();
        }
        if (where instanceof HasNetworkAddresses) {
            Set<String> privateAddrs = ((HasNetworkAddresses) where).getPrivateAddresses();
            if (privateAddrs.size() > 0) {
                result = Iterables.get(privateAddrs, 0);
            }
        }
        if (result == null && where instanceof MachineLocation) {
            InetAddress addr = ((MachineLocation) where).getAddress();
            if (addr != null) result = addr.getHostAddress();
        }
        log.debug("computed subnet host ip {} for {}", result, where);
        return Maybe.fromNullable(result);
    }

    @Nonnull
    @SuppressWarnings("unchecked")
    public static <T> Maybe<T> findUniqueElement(Iterable<?> items, Class<T> type) {
        if (items == null) {
            return Maybe.absent(new NullPointerException("Null iterable"));
        }
        Iterator<?> i = items.iterator();
        T result = null;
        while (i.hasNext()) {
            Object candidate = i.next();
            if (type.isInstance(candidate)) {
                if (result==null) result = (T)candidate;
                else {
                    if (log.isTraceEnabled())
                        log.trace("Multiple instances of "+type+" in "+items+"; ignoring");
                    return Maybe.absent(new IllegalStateException("Multiple instances of "+type+" in "+items+"; expected a single one"));
                }
            }
        }
        if (result==null) 
            return Maybe.absent(new IllegalStateException("No instances of "+type+" available (in "+items+")"));
        return Maybe.of(result);
    }

    /**
     * @deprecated since 0.9.0; see {@link #findUniqueMachineLocation(Iterable, Class)}, 
     *             e.g. {@code findUniqueMachineLocation(locations, SshMachineLocation.class)}
     */
    @Deprecated
    public static Maybe<SshMachineLocation> findUniqueSshMachineLocation(Iterable<? extends Location> locations) {
        return findUniqueMachineLocation(locations, SshMachineLocation.class);
    }

    public static Maybe<MachineLocation> findUniqueMachineLocation(Iterable<? extends Location> locations) {
        return findUniqueMachineLocation(locations, MachineLocation.class);
    }

    public static <T extends MachineLocation> Maybe<T> findUniqueMachineLocation(Iterable<? extends Location> locations, Class<T> clazz) {
        return findUniqueElement(locations, clazz);
    }

    public static Maybe<String> findSubnetHostname(Iterable<? extends Location> ll) {
        Maybe<MachineLocation> l = findUniqueMachineLocation(ll);
        if (!l.isPresent()) {
            return Maybe.absent();
//            throw new IllegalStateException("Cannot find hostname for among "+ll);
        }
        return Machines.getSubnetHostname(l.get());
    }

    public static Maybe<String> findSubnetHostname(Entity entity) {
        String sh = entity.getAttribute(Attributes.SUBNET_HOSTNAME);
        if (sh!=null) return Maybe.of(sh);
        return findSubnetHostname(entity.getLocations());
    }
    
    public static Maybe<String> findSubnetOrPublicHostname(Entity entity) {
        String hn = entity.getAttribute(Attributes.HOSTNAME);
        if (hn!=null) {
            // attributes already set, see if there was a SUBNET_HOSTNAME set
            // note we rely on (public) hostname being set _after_ subnet_hostname,
            // to prevent tiny possibility of races resulting in hostname being returned
            // becasue subnet is still being looked up -- see MachineLifecycleEffectorTasks
            Maybe<String> sn = findSubnetHostname(entity);
            if (sn.isPresent()) return sn;
            // short-circuit discovery if attributes have been set already
            return Maybe.of(hn);
        }
        
        Maybe<MachineLocation> l = findUniqueMachineLocation(entity.getLocations());
        if (!l.isPresent()) return Maybe.absent();
        InetAddress addr = l.get().getAddress();
        if (addr==null) return Maybe.absent();
        return Maybe.fromNullable(addr.getHostName());
    }

    public static Maybe<String> findSubnetOrPrivateIp(Entity entity) {
        // see comments in findSubnetOrPrivateHostname
        String hn = entity.getAttribute(Attributes.ADDRESS);
        if (hn!=null) {
            Maybe<String> sn = findSubnetIp(entity);
            if (sn.isPresent()) return sn;
            return Maybe.of(hn);
        }
        
        Maybe<MachineLocation> l = findUniqueMachineLocation(entity.getLocations());
        if (!l.isPresent()) return Maybe.absent();
        InetAddress addr = l.get().getAddress();
        if (addr==null) return Maybe.absent();
        return Maybe.fromNullable(addr.getHostAddress());
    }

    public static Maybe<String> findSubnetIp(Entity entity) {
        String sh = entity.getAttribute(Attributes.SUBNET_ADDRESS);
        if (sh!=null) return Maybe.of(sh);
        return findSubnetIp(entity.getLocations());
    }
    
    public static Maybe<String> findSubnetIp(Iterable<? extends Location> ll) {
        // TODO Or if can't find MachineLocation, should we throw new IllegalStateException("Cannot find hostname for among "+ll);
        Maybe<MachineLocation> l = findUniqueMachineLocation(ll);
        return (l.isPresent()) ? Machines.getSubnetIp(l.get()) : Maybe.<String>absent();
    }

    /** returns whether it is localhost (and has warned) */
    public static boolean warnIfLocalhost(Collection<? extends Location> locations, String message) {
        if (locations.size()==1) {
            Location l = locations.iterator().next();
            if (l instanceof LocalhostMachineProvisioningLocation || l instanceof LocalhostMachine) {
                log.warn(message);
                return true;
            }
        }
        return false;
    }

}
