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

import java.util.List;

import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.TimeWindowedList;
import org.apache.brooklyn.util.collections.TimestampedValue;
import org.apache.brooklyn.util.time.Duration;

import com.google.common.base.Objects;

/**
 * Using a {@link TimeWindowedList}, tracks the recent history of values to allow a summary of 
 * those values to be obtained. 
 *   
 * @author aled
 */
public class SizeHistory {

    public static class WindowSummary {
        /** The most recent value (or -1 if there has been no value) */
        public final long latest;
        
        /** The minimum vaule within the given time period */
        public final long min;
        
        /** The maximum vaule within the given time period */
        public final long max;
        
        /** true if, since that max value, there have not been any higher values */
        public final boolean stableForGrowth;
        
        /** true if, since that low value, there have not been any lower values */
        public final boolean stableForShrinking;
        
        public WindowSummary(long latest, long min, long max, boolean stableForGrowth, boolean stableForShrinking) {
            this.latest = latest;
            this.min = min;
            this.max = max;
            this.stableForGrowth = stableForGrowth;
            this.stableForShrinking = stableForShrinking;
        }
        
        @Override
        public String toString() {
            return Objects.toStringHelper(this).add("latest", latest).add("min", min).add("max", max)
                    .add("stableForGrowth", stableForGrowth).add("stableForShrinking", stableForShrinking).toString();
        }
    }
    
    private final TimeWindowedList<Number> recentDesiredResizes;
    
    public SizeHistory(long windowSize) {
        recentDesiredResizes = new TimeWindowedList<Number>(MutableMap.of("timePeriod", windowSize, "minExpiredVals", 1));
    }

    public void add(final int val) {
        recentDesiredResizes.add(val);
    }

    public void setWindowSize(Duration newWindowSize) {
        recentDesiredResizes.setTimePeriod(newWindowSize);
    }
    
    /**
     * Summarises the history of values in this time window, with a few special things:
     * <ul>
     *   <li>If entire time-window is not covered by the given values, then min is Integer.MIN_VALUE and max is Integer.MAX_VALUE 
     *   <li>If no values, then latest is -1
     *   <li>If no recent values, then keeps last-seen value (no matter how old), to use that
     *   <li>"stable for growth" means that since that max value, there have not been any higher values
     *   <li>"stable for shrinking" means that since that low value, there have not been any lower values
     * </ul>
     */
    public WindowSummary summarizeWindow(Duration windowSize) {
        long now = System.currentTimeMillis();
        List<TimestampedValue<Number>> windowVals = recentDesiredResizes.getValuesInWindow(now, windowSize);
        
        Number latestObj = latestInWindow(windowVals);
        long latest = (latestObj == null) ? -1: latestObj.longValue();
        long max = maxInWindow(windowVals, windowSize).longValue();
        long min = minInWindow(windowVals, windowSize).longValue();
        
        // TODO Could do more sophisticated "stable" check; this is the easiest code - correct but not most efficient
        // in terms of the caller having to schedule additional stability checks.
        boolean stable = (min == max);
        
        return new WindowSummary(latest, min, max, stable, stable);
    }
    
    /**
     * If the entire time-window is not covered by the given values, then returns Integer.MAX_VALUE.
     */
    private <T extends Number> T maxInWindow(List<TimestampedValue<T>> vals, Duration timeWindow) {
        // TODO bad casting from Integer default result to T
        long now = System.currentTimeMillis();
        long epoch = now - timeWindow.toMilliseconds();
        T result = null;
        double resultAsDouble = Integer.MAX_VALUE;
        for (TimestampedValue<T> val : vals) {
            T valAsNum = val.getValue();
            double valAsDouble = (valAsNum != null) ? valAsNum.doubleValue() : 0;
            if (result == null && val.getTimestamp() > epoch) {
                result = withDefault(null, Integer.MAX_VALUE);
                resultAsDouble = result.doubleValue();
            }
            if (result == null || (valAsNum != null && valAsDouble > resultAsDouble)) {
                result = valAsNum;
                resultAsDouble = valAsDouble;
            }
        }
        return withDefault(result, Integer.MAX_VALUE);
    }
    
    /**
     * If the entire time-window is not covered by the given values, then returns Integer.MIN_VALUE
     */
    private <T extends Number> T minInWindow(List<TimestampedValue<T>> vals, Duration timeWindow) {
        long now = System.currentTimeMillis();
        long epoch = now - timeWindow.toMilliseconds();
        T result = null;
        double resultAsDouble = Integer.MIN_VALUE;
        for (TimestampedValue<T> val : vals) {
            T valAsNum = val.getValue();
            double valAsDouble = (valAsNum != null) ? valAsNum.doubleValue() : 0;
            if (result == null && val.getTimestamp() > epoch) {
                result = withDefault(null, Integer.MIN_VALUE);
                resultAsDouble = result.doubleValue();
            }
            if (result == null || (val.getValue() != null && valAsDouble < resultAsDouble)) {
                result = valAsNum;
                resultAsDouble = valAsDouble;
            }
        }
        return withDefault(result, Integer.MIN_VALUE);
    }

    @SuppressWarnings("unchecked")
    private <T> T withDefault(T result, Integer defaultValue) {
        return result!=null ? result : (T) defaultValue;
    }
    /**
     * @return null if empty, or the most recent value
     */
    private <T extends Number> T latestInWindow(List<TimestampedValue<T>> vals) {
        return vals.isEmpty() ? null : vals.get(vals.size()-1).getValue();
    }
}
