/*
 * 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.Collection;
import java.util.Map;
import java.util.NoSuchElementException;

import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationRegistry;
import org.apache.brooklyn.api.location.LocationResolver;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.location.BasicLocationRegistry;
import org.apache.brooklyn.core.location.LocationConfigKeys;
import org.apache.brooklyn.core.location.LocationConfigUtils;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@SuppressWarnings("rawtypes")
public class JcloudsLocationResolver implements LocationResolver {

    public static final Logger log = LoggerFactory.getLogger(JcloudsLocationResolver.class);
    
    public static final String JCLOUDS = "jclouds";
    
    public static final Collection<String> AWS_REGIONS = Arrays.asList(
            // from http://docs.amazonwebservices.com/general/latest/gr/rande.html as of Apr 2012.
            // it is suggested not to maintain this list here, instead to require aws-ec2 explicitly named.
            "eu-west-1","us-east-1","us-west-1","us-west-2","ap-southeast-1","ap-northeast-1","sa-east-1");
    
    static {
        JcloudsTypeCoercions.init();
    }

    private ManagementContext managementContext;

    @Override
    public void init(ManagementContext managementContext) {
        this.managementContext = checkNotNull(managementContext, "managementContext");
    }
    
    @Override
    public boolean isEnabled() {
        return LocationConfigUtils.isResolverPrefixEnabled(managementContext, getPrefix());
    }
    
    protected class JcloudsSpecParser {
        String providerOrApi;
        String parameter;
        
        public JcloudsSpecParser parse(String spec, boolean dryrun) {
            JcloudsSpecParser result = new JcloudsSpecParser();
            int split = spec.indexOf(':');
            if (split<0) {
                if (spec.equalsIgnoreCase(getPrefix())) {
                    if (dryrun) return null;
                    throw new IllegalArgumentException("Cannot use '"+spec+"' as a location ID; it is insufficient. "+
                           "Try jclouds:aws-ec2 (for example).");
                }
                result.providerOrApi = spec;
                result.parameter = null;
            } else {
                result.providerOrApi = spec.substring(0, split);
                result.parameter = spec.substring(split+1);
                int numJcloudsPrefixes = 0;
                while (result.providerOrApi.equalsIgnoreCase(getPrefix())) {
                    //strip any number of jclouds: prefixes, for use by static "resolve" method
                    numJcloudsPrefixes++;
                    result.providerOrApi = result.parameter;
                    result.parameter = null;
                    split = result.providerOrApi.indexOf(':');
                    if (split>=0) {
                        result.parameter = result.providerOrApi.substring(split+1);
                        result.providerOrApi = result.providerOrApi.substring(0, split);
                    }
                }
                if (!dryrun && numJcloudsPrefixes > 1) {
                    log.warn("Use of deprecated location spec '"+spec+"'; in future use a single \"jclouds\" prefix");
                }
            }
            
            if (result.parameter==null && AWS_REGIONS.contains(result.providerOrApi)) {
                // treat amazon as a default
                result.parameter = result.providerOrApi;
                result.providerOrApi = "aws-ec2";
                if (!dryrun)
                    log.warn("Use of deprecated location '"+result.parameter+"'; in future refer to with explicit " +
                            "provider '"+result.providerOrApi+":"+result.parameter+"'");
            }
            
            return result;
        }

        public boolean isProvider() {
            return JcloudsProviderAndApiLoader.isProvider(providerOrApi);
        }

        public boolean isApi() {
            return JcloudsProviderAndApiLoader.isApi(providerOrApi);
        }
        
        public String getProviderOrApi() {
            return providerOrApi;
        }
        
        public String getParameter() {
            return parameter;
        }
    }
    
    @Override
    public LocationSpec<? extends Location> newLocationSpecFromString(String spec, Map<?, ?> locationFlags, LocationRegistry registry) {
        Map globalProperties = registry.getProperties();

        JcloudsSpecParser details = new JcloudsSpecParser().parse(spec, false);
        String namedLocation = (String) locationFlags.get(LocationInternal.NAMED_SPEC_NAME.getName());

        boolean isProvider = details.isProvider();
        String providerOrApi = details.providerOrApi;
        // gce claims to be an api ... perhaps just a bug? email sent to jclouds dev list, 28 mar 2014
        isProvider = isProvider || "google-compute-engine".equals(providerOrApi);
        
        if (Strings.isEmpty(providerOrApi)) {
            throw new IllegalArgumentException("Cloud provider/API type not specified in spec \""+spec+"\"");
        }
        if (!isProvider && !details.isApi()) {
            throw new NoSuchElementException("Cloud provider/API type "+providerOrApi+" is not supported by jclouds");
        }
        
        // For everything in brooklyn.properties, only use things with correct prefix (and remove that prefix).
        // But for everything passed in via locationFlags, pass those as-is.
        // TODO Should revisit the locationFlags: where are these actually used? Reason accepting properties without
        //      full prefix is that the map's context is explicitly this location, rather than being generic properties.
        Map<String,Object> allProperties = getAllProperties(registry, globalProperties);
        String regionOrEndpoint = details.parameter;
        if (regionOrEndpoint==null && isProvider) regionOrEndpoint = (String)locationFlags.get(LocationConfigKeys.CLOUD_REGION_ID.getName());
        Map<Object,Object> jcloudsProperties = MutableMap.<Object,Object>copyOf(new JcloudsPropertiesFromBrooklynProperties().getJcloudsProperties(providerOrApi, regionOrEndpoint, namedLocation, allProperties));
        jcloudsProperties.putAll(locationFlags);
        
        if (regionOrEndpoint!=null) {
            // apply the regionOrEndpoint (e.g. from the parameter) as appropriate -- but only if it has not been overridden
            if (isProvider) {
                // providers from ServiceLoader take a location (endpoint already configured), and optionally a region name
                // NB blank might be supplied if spec string is "mycloud:" -- that should be respected, 
                // whereas no parameter/regionName ie null value -- "mycloud" -- means don't set
                if (Strings.isBlank(Strings.toString(jcloudsProperties.get(JcloudsLocationConfig.CLOUD_REGION_ID.getName()))))
                    jcloudsProperties.put(JcloudsLocationConfig.CLOUD_REGION_ID.getName(), regionOrEndpoint);
            } else {
                // other "providers" are APIs so take an _endpoint_ (but not a location);
                // see note above re null here
                if (Strings.isBlank(Strings.toString(jcloudsProperties.get(JcloudsLocationConfig.CLOUD_ENDPOINT.getName()))))
                    jcloudsProperties.put(JcloudsLocationConfig.CLOUD_ENDPOINT.getName(), regionOrEndpoint);
            }
        }
        
        return LocationSpec.create(getLocationClass())
                .configure(LocationConfigUtils.finalAndOriginalSpecs(spec, jcloudsProperties, globalProperties, namedLocation))
                .configure(jcloudsProperties);
    }

    @SuppressWarnings("unchecked")
    private Map<String,Object> getAllProperties(LocationRegistry registry, Map properties) {
        Map<String,Object> allProperties = MutableMap.of();
        if (registry!=null) allProperties.putAll(registry.getProperties());
        allProperties.putAll(properties);
        return allProperties;
    }
    
    @Override
    public String getPrefix() {
        return JCLOUDS;
    }

    protected Class<? extends JcloudsLocation> getLocationClass() {
        return JcloudsLocation.class;
    }
    
    @Override
    public boolean accepts(String spec, LocationRegistry registry) {
        if (BasicLocationRegistry.isResolverPrefixForSpec(this, spec, true)) return true;
        JcloudsSpecParser details = new JcloudsSpecParser().parse(spec, true);
        if (details==null) return false;
        if (details.isProvider() || details.isApi()) return true;
        return false;
    }

}
