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

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.objs.HasShortName;
import org.apache.brooklyn.core.location.cloud.CloudLocationConfig;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.annotations.Beta;
import com.google.common.base.CharMatcher;

/** 
 * Implements <b>most</b> of {@link CloudMachineNamer},
 * leaving just one method -- {@link #generateNewIdOfLength(int)} --
 * for subclasses to provide.
 * <p>
 * {@link CloudLocationConfig#VM_NAME_MAX_LENGTH} is used to find the VM length, 
 * unless {@link #getCustomMaxNameLength(ConfigBag)} is overridden or
 * {@link #setDefaultMachineNameMaxLength(int)} invoked on the instance supplied.
 */
public abstract class AbstractCloudMachineNamer implements CloudMachineNamer {

    int defaultMachineNameMaxLength = CloudLocationConfig.VM_NAME_MAX_LENGTH.getDefaultValue();
    int defaultMachineNameSaltLength = CloudLocationConfig.VM_NAME_SALT_LENGTH.getDefaultValue();
    protected String separator = "-";

    @Override
    public String generateNewMachineUniqueName(ConfigBag setup) {
        return generateNewIdReservingLength(setup, 0);
    }
    
    @Override
    public String generateNewMachineUniqueNameFromGroupId(ConfigBag setup, String groupId) {
        int availSaltLength = getMaxNameLength(setup) - (groupId.length() + separator.length());
        int requestedSaltLength = getLengthForMachineUniqueNameSalt(setup, false);
        if (availSaltLength <= 0 || requestedSaltLength <= 0) {
            return groupId;
        }
            
        return sanitize(groupId + separator + Identifiers.makeRandomId(Math.min(requestedSaltLength, availSaltLength))).toLowerCase();
    }

    @Override
    public String generateNewGroupId(ConfigBag setup) {
        return sanitize(generateNewIdReservingLength(setup, getLengthForMachineUniqueNameSalt(setup, true))).toLowerCase();
    }

    protected String generateNewIdReservingLength(ConfigBag setup, int lengthToReserve) {
        int len = getMaxNameLength(setup);
        // decrement by e.g. 9 chars because jclouds adds that (dash plus 8 for hex id)
        len -= lengthToReserve;
        if (len<=0) return "";
        return Strings.maxlen(generateNewIdOfLength(setup, len), len);
    }
    
    /** Method for subclasses to provide to construct the context-specific part of an identifier,
     * for use in {@link #generateNewGroupId()} and {@link #generateNewMachineUniqueName()}.
     * 
     * @param maxLengthHint an indication of the maximum length permitted for the ID generated,
     * supplied for implementations which wish to use this information to decide what to truncate.
     * (This class will truncate any return values longer than this.) 
     */
    protected abstract String generateNewIdOfLength(ConfigBag setup, int maxLengthHint);

    /** Returns the max length of a VM name for the cloud specified in setup;
     * this value is typically decremented by 9 to make room for jclouds labels;
     * delegates to {@link #getCustomMaxNameLength()} when 
     * {@link CloudLocationConfig#VM_NAME_MAX_LENGTH} is not set */
    public int getMaxNameLength(ConfigBag setup) {
        if (setup.containsKey(CloudLocationConfig.VM_NAME_MAX_LENGTH)) {
            // if a length is set explicitly, use that (but intercept default behaviour)
            return setup.get(CloudLocationConfig.VM_NAME_MAX_LENGTH);
        }
        
        Integer custom = getCustomMaxNameLength(setup);
        if (custom!=null) return custom;
        
        // return the default
        return defaultMachineNameMaxLength;  
    }
    
    // sometimes we create salt string, sometimes jclouds does
    public int getLengthForMachineUniqueNameSalt(ConfigBag setup, boolean includeSeparator) {
        int saltLen;
        if (setup.containsKey(CloudLocationConfig.VM_NAME_SALT_LENGTH)) {
            saltLen = setup.get(CloudLocationConfig.VM_NAME_SALT_LENGTH);
        } else {
            // default value comes from key, but custom default can be set
            saltLen = defaultMachineNameSaltLength;
        }
        
        if (saltLen>0 && includeSeparator)
            saltLen += separator.length();
        
        return saltLen;
    }
    
    public AbstractCloudMachineNamer setDefaultMachineNameMaxLength(int defaultMaxLength) {
        this.defaultMachineNameMaxLength = defaultMaxLength;
        return this;
    }

    /** Number of chars to use or reserve for the machine identifier when constructing a group identifier;
     * jclouds for instance uses "-" plus 8 */
    public AbstractCloudMachineNamer setDefaultMachineNameSeparatorAndSaltLength(String separator, int defaultMachineUniqueNameSaltLength) {
        this.separator = separator;
        this.defaultMachineNameSaltLength = defaultMachineUniqueNameSaltLength;
        return this;
    }
    
    /** Method for overriding to provide custom logic when an explicit config key is not set for the machine length. */
    public Integer getCustomMaxNameLength(ConfigBag setup) {
        return null;
    }

    protected static String shortName(Object x) {
        if (x instanceof HasShortName) {
            return ((HasShortName)x).getShortName();
        }
        if (x instanceof Entity) {
            return ((Entity)x).getDisplayName();
        }
        return x.toString();
    }

    @Beta //probably won't live here long-term
    public static String sanitize(String s) {
        return CharMatcher.inRange('A', 'Z')
                .or(CharMatcher.inRange('a', 'z'))
                .or(CharMatcher.inRange('0', '9'))
                .negate()
                .trimAndCollapseFrom(s, '-');
    }
}
