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

import java.util.concurrent.TimeUnit;

import org.apache.brooklyn.api.catalog.Catalog;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.EnricherSpec;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.enricher.stock.AbstractTypeTransformingEnricher;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Converts an absolute measure of time into a fraction of time, based on the delta between consecutive values 
 * and the elapsed time between those values.
 * 
 * For example, if the values are for ProcessCpuTime (i.e. microseconds of CPU time for the given process), then 
 * this would convert it into a fraction (i.e. 100th of the percentage): (newVal-prevVal)/(newTimestamp/prevTimestamp).
 * 
 * It also configured with the time units for the values.
 */
@Catalog(name="Time-fraction Delta", description="Converts an absolute measure of time into a fraction of time, "
        + "based on the delta between consecutive values and the elapsed time between those values.")
public class TimeFractionDeltaEnricher<T extends Number> extends AbstractTypeTransformingEnricher<T,Double> {
    private static final Logger LOG = LoggerFactory.getLogger(TimeFractionDeltaEnricher.class);
    
    /**
     * @deprecated since 0.12.0; use {@link #durationPerOrigUnit}
     */
    //Kept for backwards compatibility with persisted state.
    @SetFromFlag
    @Deprecated
    private Long nanosPerOrigUnit;
    
    @SetFromFlag
    private Duration durationPerOrigUnit;
    
    protected Number lastValue;
    protected long lastTimestamp = -1;

    public TimeFractionDeltaEnricher() { // for rebinding
    }
    
    /**
     * @deprecated since 0.12.0; use {@link EnricherSpec}
     */
    @Deprecated
    public TimeFractionDeltaEnricher(Entity producer, Sensor<T> source, Sensor<Double> target, TimeUnit origUnits) {
        this(producer, source, target, origUnits.toNanos(1));
    }
    
    /**
     * @deprecated since 0.12.0; use {@link EnricherSpec}
     */
    @Deprecated
    public TimeFractionDeltaEnricher(Entity producer, Sensor<T> source, Sensor<Double> target, long nanosPerOrigUnit) {
        super(producer, source, target);
        this.nanosPerOrigUnit = nanosPerOrigUnit;
        
        if (source!=null && target!=null)
            this.uniqueTag = JavaClassNames.simpleClassName(getClass())+":"+source.getName()+"*"+Duration.nanos(nanosPerOrigUnit)+"->"+target.getName();
    }
    
    @Override
    public void init() {
        super.init();
        if (durationPerOrigUnit == null) {
            if (nanosPerOrigUnit != null) {
                durationPerOrigUnit = Duration.nanos(nanosPerOrigUnit);
            } else {
                durationPerOrigUnit = Duration.nanos(1);
            }
        }
        nanosPerOrigUnit = null;
        
        if (uniqueTag == null && source != null && target != null) {
            uniqueTag = JavaClassNames.simpleClassName(getClass())+":"+source.getName()+"*"+durationPerOrigUnit+"->"+target.getName();
        }
    }
    
    @Override
    public void rebind() {
        super.rebind();
        
        if (durationPerOrigUnit == null) {
            if (nanosPerOrigUnit != null) {
                durationPerOrigUnit = Duration.nanos(nanosPerOrigUnit);
            } else {
                durationPerOrigUnit = Duration.nanos(1);
            }
        }
        nanosPerOrigUnit = null;
    }
    
    @Override
    public void onEvent(SensorEvent<T> event) {
        onEvent(event, event.getTimestamp());
    }
    
    public void onEvent(SensorEvent<T> event, long eventTimestamp) {
        Number current = event.getValue();
        
        if (current == null) {
            // Can't compute a delta; 
            // don't assume current=zero because then things like requestCount->requestsPerSecond is negative!
            // instead don't publish anything
            if (LOG.isTraceEnabled()) LOG.trace("ignoring null value in {}, at {}", new Object[] {this, eventTimestamp});
            return;
        }
        
        if (eventTimestamp > lastTimestamp) {
            if (lastValue == null) {
                // cannot calculate delta with a single value
                if (LOG.isTraceEnabled()) LOG.trace("{} received event but no last value so will not emit, null -> {} at {}", 
                        new Object[] {this, current, eventTimestamp}); 
            } else if (lastTimestamp < 0) {
                LOG.warn("{} has lastValue {} but last timestamp {}; new value is {} at {}; not publishing", 
                        new Object[] {this, lastValue, lastTimestamp, current, eventTimestamp});
            } else {
                long duration = eventTimestamp - lastTimestamp;
                double fraction = toNanos(current.doubleValue() - lastValue.doubleValue(), durationPerOrigUnit.nanos()) / TimeUnit.MILLISECONDS.toNanos(duration);
                entity.sensors().set((AttributeSensor<Double>)target, fraction);
                if (LOG.isTraceEnabled()) LOG.trace("set {} to {}, {} -> {} at {} (previous at {})", 
                        new Object[] {this, fraction, lastValue, current, eventTimestamp, lastTimestamp}); 
            }
            lastValue = current;
            lastTimestamp = eventTimestamp;
        }
    }
    
    private double toNanos(double val, long nanosPerOrigUnit) {
        return val*nanosPerOrigUnit;
    }
}
