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

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

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.location.MachineProvisioningLocation;
import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.location.AbstractLocation;
import org.apache.brooklyn.core.location.cloud.AbstractAvailabilityZoneExtension;
import org.apache.brooklyn.core.location.cloud.AvailabilityZoneExtension;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.annotations.Beta;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;

/** A location which consists of multiple locations stitched together to form availability zones.
 * The first location will be used by default, but if an {@link AvailabilityZoneExtension}-aware entity
 * is used, it may stripe across each of the locations.  See notes at {@link AvailabilityZoneExtension}. */
public class MultiLocation<T extends MachineLocation> extends AbstractLocation implements MachineProvisioningLocation<T> {

    /**
     * The subLocationSpecs allows {@link MultiLocationResolver} to work with just LocationSpecs, rather than 
     * having to instantiate the sub-locations immediately (which would be bad, because the caller
     * might not use the spec and thus might not unmanage the sub-location instances).
     *
     * We clear the subLocationSpecs in init, so they will never be persisted. This will help with
     * backwards compatibility if we change how this is done.
     *
     * By the end of init(), the {@link #SUB_LOCATIONS} will contain the full list of locations; it 
     * will never contain a {@link LocationSpec}.
     */
    @Beta
    @SuppressWarnings("serial")
    public static final ConfigKey<List<LocationSpec<?>>> SUB_LOCATION_SPECS = ConfigKeys.newConfigKey(
            new TypeToken<List<LocationSpec<?>>>() {},
            "subLocationSpecs", 
            "Specs of sub-locations that should be immediatly instantiated on init");
    
    @SuppressWarnings("serial")
    public static final ConfigKey<List<MachineProvisioningLocation<?>>> SUB_LOCATIONS = ConfigKeys.newConfigKey(
            new TypeToken<List<MachineProvisioningLocation<?>>>() {},
            "subLocations", 
            "The sub-locations that this location can delegate to");

    @Override
    public void init() {
        super.init();
        
        List<MachineProvisioningLocation<?>> subLocs = MutableList.copyOf(getConfig(SUB_LOCATIONS));
        
        List<LocationSpec<?>> subLocSpecs = getConfig(SUB_LOCATION_SPECS);
        if (subLocSpecs != null) {
            for (LocationSpec<?> spec : subLocSpecs) {
                if (spec.getParent() == null) {
                    // use a copy with us set as parent
                    spec = LocationSpec.create(spec).parent(this);
                }
                MachineProvisioningLocation<?> subLoc = (MachineProvisioningLocation<?>) getManagementContext().getLocationManager().createLocation(spec);
                subLocs.add(subLoc);
            }
        }
        checkState(subLocs.size() >= 1, "sub-locations must not be empty");
        config().set(SUB_LOCATIONS, subLocs);
        config().set(SUB_LOCATION_SPECS, (List<LocationSpec<?>>) null);

        AvailabilityZoneExtension azExtension = new AvailabilityZoneExtensionImpl(getManagementContext(), subLocs);
        addExtension(AvailabilityZoneExtension.class, azExtension);
    }

    public T obtain() throws NoMachinesAvailableException {
        return obtain(MutableMap.of());
    }
    
    /** finds (creates) and returns a {@link MachineLocation}; 
     * this always tries the first sub-location, moving on the second and subsequent if the first throws {@link NoMachinesAvailableException}.
     * (if you want striping across locations, see notes in {@link AvailabilityZoneExtension}.) */
    @SuppressWarnings("unchecked")
    @Override
    public T obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
        List<MachineProvisioningLocation<?>> sublocsList = getSubLocations();
        Iterator<MachineProvisioningLocation<?>> sublocs = sublocsList.iterator();
        List<NoMachinesAvailableException> errors = MutableList.of();
        while (sublocs.hasNext()) {
            try {
                return (T) sublocs.next().obtain(flags);
            } catch (NoMachinesAvailableException e) {
                errors.add(e);
            }
        }
        Exception wrapped;
        String msg;
        if (errors.size()>1) {
            wrapped = new CompoundRuntimeException(errors.size()+" sublocation exceptions, including: "+
                Exceptions.collapseText(errors.get(0)), errors);
            msg = Exceptions.collapseText(wrapped);
        } else if (errors.size()==1) {
            wrapped = errors.get(0);
            msg = wrapped.getMessage();
            if (Strings.isBlank(msg)) msg = wrapped.toString();
        } else {
            msg = "no sub-locations set for this multi-location";
            wrapped = null;
        }
        throw new NoMachinesAvailableException("No machines available in any of the "+sublocsList.size()+" location"+Strings.s(sublocsList.size())+
            " configured here: "+msg, wrapped);
    }

    public List<MachineProvisioningLocation<?>> getSubLocations() {
        return getRequiredConfig(SUB_LOCATIONS);
    }

    @SuppressWarnings("unchecked")
    @Override
    public MachineProvisioningLocation<T> newSubLocation(Map<?, ?> newFlags) {
        // TODO shouldn't have to copy config bag as it should be inherited (but currently it is not used inherited everywhere; just most places)
        return getManagementContext().getLocationManager().createLocation(LocationSpec.create(getClass())
                .parent(this)
                .configure(config().getLocalBag().getAllConfig())  // FIXME Should this just be inherited?
                .configure(newFlags));
    }

    @SuppressWarnings("unchecked")
    @Override
    public void release(T machine) {
        ((MachineProvisioningLocation<T>)machine.getParent()).release(machine);
    }

    @Override
    public Map<String, Object> getProvisioningFlags(Collection<String> tags) {
        return Maps.<String,Object>newLinkedHashMap();
    }

    @SuppressWarnings("unchecked")
    protected MachineProvisioningLocation<T> firstSubLoc() {
        return (MachineProvisioningLocation<T>) Iterables.get(getSubLocations(), 0);
    }

    protected <K> K getRequiredConfig(ConfigKey<K> key) {
        return checkNotNull(getConfig(key), key.getName());
    }

    public static class AvailabilityZoneExtensionImpl extends AbstractAvailabilityZoneExtension implements AvailabilityZoneExtension {

        private final List<MachineProvisioningLocation<?>> subLocations;
        
        public AvailabilityZoneExtensionImpl(ManagementContext managementContext, List<MachineProvisioningLocation<?>> subLocations) {
            super(managementContext);
            this.subLocations = ImmutableList.copyOf(subLocations);
        }
        
        @Override
        protected List<Location> doGetAllSubLocations() {
            return ImmutableList.<Location>copyOf(subLocations);
        }
        
        @Override
        protected boolean isNameMatch(Location loc, Predicate<? super String> namePredicate) {
            return namePredicate.apply(loc.getDisplayName());
        }
    }
}
