/*
 * 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.entity.software.base;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.PortRange;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.guava.Maybe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;

public class InboundPortsUtils {
    private static final Logger log = LoggerFactory.getLogger(InboundPortsUtils.class);

    /**
     * Returns the required open inbound ports for an Entity.
     * If {@code portsAutoInfer} is {@code true} then
     * return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
     * config key {@link PortRange} plus any ports defined with a config key matching the provided regex.
     *
     * @param entity         the Entity
     * @param portsAutoInfer if {@code true} then also return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
     *                       config key {@link PortRange} plus any ports defined with a config keys matching the provided regex
     * @param portRegex      the regex to match config keys that define inbound ports
     * @return a collection of port numbers
     */
    public static Collection<Integer> getRequiredOpenPorts(Entity entity, Boolean portsAutoInfer, String portRegex) {
        return getRequiredOpenPorts(entity, ImmutableSet.<ConfigKey<?>>of(), portsAutoInfer, portRegex);
    }

    /**
     * Returns the required open inbound ports for an Entity.
     * If {@code portsAutoInfer} is {@code true} then
     * return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
     * config key {@link PortRange} plus any ports defined with a config key matching the provided regex.
     * This method also accepts an extra set of config keys in addition to those that are defined in the EntityType of the entity itself.
     *
     * @param entity          the Entity
     * @param extraConfigKeys extra set of config key to inspect for inbound ports
     * @param portsAutoInfer  if {@code true} then return the first value for each {@link org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey}
     *                        config key {@link PortRange} plus any ports defined with a config keys matching the provided regex
     * @param portRegex       the regex to match config keys that define inbound ports
     * @return a collection of port numbers
     */
    public static Collection<Integer> getRequiredOpenPorts(Entity entity, Set<ConfigKey<?>> extraConfigKeys, Boolean portsAutoInfer, String portRegex) {
        Set<Integer> ports = MutableSet.of();

        /* TODO: This won't work if there's a port collision, which will cause the corresponding port attribute
           to be incremented until a free port is found. In that case the entity will use the free port, but the
           firewall will open the initial port instead. Mostly a problem for SameServerEntity, localhost location.
         */
        if (portsAutoInfer == null || portsAutoInfer.booleanValue()) { // auto-infer defaults to true if not specified
            Set<ConfigKey<?>> configKeys = Sets.newHashSet(extraConfigKeys);
            configKeys.addAll(entity.getEntityType().getConfigKeys());
            // Also add dynamically added config keys
            Map<ConfigKey<?>, ?> configuredConfigKeys = MutableMap.copyOf(((EntityInternal) entity).config().getBag().getAllConfigAsConfigKeyMap());
            configKeys.addAll(configuredConfigKeys.keySet());

            if (portRegex == null) {
                portRegex = ".*\\.port"; // defaults to legacy regex if not specified
            }

            Pattern portsPattern = Pattern.compile(portRegex);
            for (ConfigKey<?> k : configKeys) {
                if (isAssignableFromPortConfigKey(configuredConfigKeys, k)
                        || (portsPattern.matcher(k.getName()).matches())) {
                    Object value = entity.config().get(k);
                    Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, new TypeToken<PortRange>() {
                    });
                    if (maybePortRange.isPresentAndNonNull()) {
                        PortRange p = maybePortRange.get();
                        if (p != null && !p.isEmpty())
                            ports.add(p.iterator().next());
                    }
                }
            }
        }

        log.debug("getRequiredOpenPorts detected default {} for {}", ports, entity);
        return ports;
    }

    /**
     * Checks if a configkey can be managed as a config key {@link PortRange}. The method checks if
     * the config type or the entity config key value are assignable from the {@link PortRange}.
     *
     * @param configuredConfigKeys the configured entity config keys
     * @param configKey            the configkey that could be assigned as a {@link org.apache.brooklyn.api.location.PortRange}
     */
    private static boolean isAssignableFromPortConfigKey(Map<ConfigKey<?>, ?> configuredConfigKeys, ConfigKey<?> configKey) {
        return PortRange.class.isAssignableFrom(configKey.getType())
                || configuredConfigKeys.get(configKey) instanceof PortRange;
    }

}
