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

import java.io.Serializable;
import java.net.InetAddress;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.AddressableLocation;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.core.location.AbstractLocation;
import org.apache.brooklyn.core.location.LocationConfigKeys;
import org.apache.brooklyn.util.core.ClassLoaderUtils;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.internal.BrooklynSystemProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;

/**
 * Encapsulates geo-IP information for a given host.
 */
public class HostGeoInfo implements Serializable {
    
    private static final long serialVersionUID = -5866759901535266181L;

    public static final Logger log = LoggerFactory.getLogger(HostGeoInfo.class);

    /** the IP address */
    public final String address;
    
    public final String displayName;
    
    public final double latitude;
    public final double longitude;
    
    private static Maybe<HostGeoLookup> cachedLookup = null;

    public static HostGeoInfo create(String address, String displayName, double latitude, double longitude) {
        return new HostGeoInfo(address, displayName, latitude, longitude);
    }
    
    public static HostGeoInfo fromIpAddress(InetAddress address) {
        try {
            HostGeoLookup lookup = getDefaultLookup();
            if (lookup!=null)
                return lookup.getHostGeoInfo(address);
        } catch (Exception e) {
            if (log.isDebugEnabled())
                log.debug("unable to look up geo DNS info for "+address, e);
        }
        return null;
    }

    @Nullable
    public static HostGeoLookup getDefaultLookup() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        if (cachedLookup==null) {
            cachedLookup = Maybe.of(findHostGeoLookupImpl());
        }                
        return cachedLookup.get();
    }
    
    public static void clearCachedLookup() {
        cachedLookup = null;
    }
    
    /** returns null if cannot be set */
    public static HostGeoInfo fromLocation(Location l) {
        if (l==null) return null;
        
        Location la = l;
        HostGeoInfo resultFromLocation = null;
        while (la!=null) {
            if (la instanceof HasHostGeoInfo) {
                resultFromLocation = ((HasHostGeoInfo)l).getHostGeoInfo();
                if (resultFromLocation!=null) break;
            }
            la = la.getParent();
        }
        if (resultFromLocation!=null && l==la) {
            // from the location
            return resultFromLocation;
        }
        // resultFromLocation may be inherited, in which case we will copy it later
        
        InetAddress address = findIpAddress(l);
        Object latitude = l.getConfig(LocationConfigKeys.LATITUDE);
        Object longitude = l.getConfig(LocationConfigKeys.LONGITUDE);

        if (resultFromLocation!=null && (latitude == null || longitude == null)) {
            latitude = resultFromLocation.latitude;
            longitude = resultFromLocation.longitude;            
        }
        if (address!=null && (latitude == null || longitude == null)) {
            HostGeoInfo geo = fromIpAddress(address);
            if (geo==null) return null;
            latitude = geo.latitude;
            longitude = geo.longitude;
        }
        
        if (latitude==null || longitude==null)
            return null;
        
        Exception error=null;
        try {
            latitude = TypeCoercions.castPrimitive(latitude, Double.class);
            longitude = TypeCoercions.castPrimitive(longitude, Double.class);
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            error = e;
        }
        if (error!=null || !(latitude instanceof Double) || !(longitude instanceof Double))
            throw new IllegalArgumentException("Location "+l+" specifies invalid type of lat/long: " +
                    "lat="+latitude+" (type "+(latitude==null ? null : latitude.getClass())+"); " +
                    "lon="+longitude+" (type "+(longitude==null ? null : longitude.getClass())+")", error);
        
        HostGeoInfo result = new HostGeoInfo(address!=null ? address.getHostAddress() : null, l.getDisplayName(), (Double) latitude, (Double) longitude);
        if (l instanceof AbstractLocation) {
            ((AbstractLocation)l).setHostGeoInfo(result);
        }
        return result;
    }

    @Deprecated
    private static boolean warnedLegacy = false;
    
    private static HostGeoLookup findHostGeoLookupImpl() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        String type = BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getValue();
        if (type==null) {
            type = BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL_LEGACY.getValue();
            if (type!=null && !warnedLegacy) {
                warnedLegacy = true;
                log.warn("Using deprecated host-geo-lookup property "+BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL_LEGACY+"; "
                    + "set "+BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL+" instead");
            }
        }
        /* utrace seems more accurate than geobytes, and it gives a report of how many tokens are left;
         * but maxmind if it's installed locally is even better (does not require remote lookup),
         * so use it if available */
        if (type==null) {
            if (MaxMind2HostGeoLookup.getDatabaseReader()!=null)
                return new MaxMind2HostGeoLookup();
            log.debug("Using Utrace remote for geo lookup because MaxMind2 is not available");
            return new UtraceHostGeoLookup();
        }
        if (type.isEmpty()) return null;
        return (HostGeoLookup) new ClassLoaderUtils(HostGeoInfo.class).loadClass(type).newInstance();
    }

    public static HostGeoInfo fromEntity(Entity e) {
        for (Location l : e.getLocations()) {
            HostGeoInfo hgi = fromLocation(l);
            if (hgi != null)
                return hgi;
        }
        return null;
    }
    
    public static InetAddress findIpAddress(Location l) {
        if (l == null)
            return null;
        if (l instanceof AddressableLocation)
            return ((AddressableLocation) l).getAddress();
        return findIpAddress(l.getParent());
    }
    
    public HostGeoInfo(String address, String displayName, double latitude, double longitude) {
        this.address = address;
        this.displayName = displayName==null ? "" : displayName;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public String getAddress() {
        return address;
    }
    
    @Override
    public String toString() {
        return "HostGeoInfo["+displayName+": "+(address!=null ? address : "(no-address)")+" at ("+latitude+","+longitude+")]";
    }
    
    @Override
    public boolean equals(Object o) {
        // Slight cheat: only includes the address + displayName field (displayName to allow overloading localhost etc)
        return (o instanceof HostGeoInfo) && Objects.equal(address, ((HostGeoInfo) o).address)
                && Objects.equal(displayName, ((HostGeoInfo) o).displayName);
    }
    
    @Override
    public int hashCode() {
        // Slight cheat: only includes the address + displayName field (displayName to allow overloading localhost etc)
        return Objects.hashCode(address, displayName);
    }
    
}
