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

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey.Builder;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.feed.ConfigToAttributes;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.reflect.TypeToken;

/**
* A {@link Sensor} describing an attribute that can be configured with inputs that are used to derive the final value.
* <p>
* The {@link ConfigKey} will have the same name and description as the sensor but not necessarily the same type.
* Conversion to set the sensor value from the config key must be supplied in a subclass.
* <p>
* {@link ConfigToAttributes#apply(Entity, AttributeSensorAndConfigKey)} is useful to set the attribute from the sensor.
*/
public abstract class AttributeSensorAndConfigKey<ConfigType,SensorType> extends BasicAttributeSensor<SensorType> 
        implements ConfigKey.HasConfigKey<ConfigType> {
    
    private static final long serialVersionUID = -3103809215973264600L;
    private static final Logger log = LoggerFactory.getLogger(AttributeSensorAndConfigKey.class);

    private ConfigKey<ConfigType> configKey;

    public AttributeSensorAndConfigKey(Class<ConfigType> configType, Class<SensorType> sensorType, String name) {
        this(configType, sensorType, name, name, null);
    }
    
    public AttributeSensorAndConfigKey(Class<ConfigType> configType, Class<SensorType> sensorType, String name, String description) {
        this(TypeToken.of(configType), TypeToken.of(sensorType), name, description, null);
    }
    
    public AttributeSensorAndConfigKey(Class<ConfigType> configType, Class<SensorType> sensorType, String name, String description, Object defaultValue) {
        this(TypeToken.of(configType), TypeToken.of(sensorType), name, description, defaultValue);
    }

    public AttributeSensorAndConfigKey(TypeToken<ConfigType> configType, TypeToken<SensorType> sensorType, String name) {
        this(configType, sensorType, name, null);
    }

    public AttributeSensorAndConfigKey(TypeToken<ConfigType> configType, TypeToken<SensorType> sensorType, String name, String description) {
        this(configType, sensorType, name, description, null);
    }

    public AttributeSensorAndConfigKey(TypeToken<ConfigType> configType, TypeToken<SensorType> sensorType, String name, String description, Object defaultValue) {
        super(sensorType, name, description);
        ConfigType defaultValueTyped;
        try {
            defaultValueTyped = TypeCoercions.coerce(defaultValue, configType);
        } catch (Exception e) {
            log.warn("Invalid default value '"+defaultValue+"' for "+name+" (rethrowing: "+e, e);
            throw Exceptions.propagate(e);
        }
        configKey = new BasicConfigKey<ConfigType>(configType, name, description, defaultValueTyped);
    }

    public AttributeSensorAndConfigKey(AttributeSensorAndConfigKey<ConfigType,SensorType> orig, ConfigType defaultValue) {
        super(orig.getTypeToken(), orig.getName(), orig.getDescription());
        configKey = ConfigKeys.newConfigKeyWithDefault(orig.configKey, 
                TypeCoercions.coerce(defaultValue, orig.configKey.getTypeToken()));
    }
    public AttributeSensorAndConfigKey(Builder<ConfigType,?> configKeyBuilder, TypeToken<SensorType> sensorType) {
        super(sensorType, configKeyBuilder.getName(), configKeyBuilder.getDescription());
        configKey = new BasicConfigKey<ConfigType>(configKeyBuilder);
    }

    @Override
    public ConfigKey<ConfigType> getConfigKey() { return configKey; }
    
    /** returns the sensor value for this attribute on the given entity, if present,
     * otherwise works out what the sensor value should be based on the config key's value
     * <p>
     * calls to this may allocate resources (e.g. ports) so should be called only once and 
     * then (if non-null) assigned as the sensor's value
     * <p>
     * <b>(for this reason this method should generally not be invoked by callers except in tests and by the framework,
     * and similarly should not be overridden; implement {@link #convertConfigToSensor(Object, Entity)} instead for single-execution calls.
     * the framework calls this from {@link AbstractEntity#setAttribute(AttributeSensorAndConfigKey)} 
     * typically via {@link ConfigToAttributes#apply(Entity)} e.g. from SoftwareProcessImpl.preStart().)
     * </b> 
     */
    public SensorType getAsSensorValue(Entity e) {
        SensorType sensorValue = e.getAttribute(this);
        if (sensorValue!=null) return sensorValue;
        
        ConfigType v = e.config().get(this);
        try {
            return convertConfigToSensor(v, e);
        } catch (Throwable t) {
            throw new IllegalArgumentException("Cannot convert config value "+v+" for sensor "+this+": "+t, t);
        }
    }

    /**
     * @see {@link #getAsSensorValue(Entity)}
     * 
     * Differs in that the config value is converted based on just the management context, rather
     * than for a specific entity. For example, useful if using {@link BrooklynConfigKeys} in BrooklynWebServer.
     * </b> 
     */
    public SensorType getAsSensorValue(ManagementContext managementContext) {
        ConfigType v = managementContext.getConfig().getConfig(this);
        try {
            return convertConfigToSensor(v, managementContext);
        } catch (Throwable t) {
            throw new IllegalArgumentException("Cannot convert config value "+v+" for sensor "+this+": "+t, t);
        }
    }

    /** converts the given ConfigType value to the corresponding SensorType value, 
     * with respect to the given entity
     * <p>
     * this is invoked after checks whether the entity already has a value for the sensor,
     * and the entity-specific config value is passed for convenience if set, 
     * otherwise the config key default value is passed for convenience
     * <p>
     * this message should be allowed to return null if the conversion cannot be completed at this time */
    protected abstract SensorType convertConfigToSensor(ConfigType value, Entity entity);

    /**
     * @see {@link #convertConfigToSensor(Object, Entity)}
     */
    protected abstract SensorType convertConfigToSensor(ConfigType value, ManagementContext entity);
}
