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

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.brooklyn.util.time.Duration;

import com.google.common.collect.ImmutableList;

/**
 * Keeps a list of timestamped values that are in the given time-period (millis).
 * It also guarantees to keep the given minimum number of values in the list (even if old),
 * and to keep the given number of out-of-date values.
 * 
 * For example, this is useful if we want to determine if a metric has been consistently high.
 */
public class TimeWindowedList<T> {
    private final LinkedList<TimestampedValue<T>> values = new LinkedList<TimestampedValue<T>>();
    private volatile Duration timePeriod;
    private final int minVals;
    private final int minExpiredVals;

    public TimeWindowedList(Duration timePeriod) {
        this.timePeriod = timePeriod;
        minVals = 0;
        minExpiredVals = 0;
    }
    
    public TimeWindowedList(Map<String,?> flags) {
        if (!flags.containsKey("timePeriod")) throw new IllegalArgumentException("Must define timePeriod");
        timePeriod = Duration.of(flags.get("timePeriod"));
        
        if (flags.containsKey("minVals")) {
            minVals = ((Number)flags.get("minVals")).intValue();
        } else {
            minVals = 0;
        }
        if (flags.containsKey("minExpiredVals")) {
            minExpiredVals = ((Number)flags.get("minExpiredVals")).intValue();
        } else {
            minExpiredVals = 0;
        }
    }
    
    public void setTimePeriod(Duration newTimePeriod) {
        timePeriod = newTimePeriod;
    }
    
    public synchronized T getLatestValue() {
        return (values.isEmpty()) ? null : values.get(values.size()-1).getValue();
    }
    
    public List<TimestampedValue<T>> getValues() {
        return getValues(System.currentTimeMillis());
    }
    
    public synchronized List<TimestampedValue<T>> getValues(long now) {
        pruneValues(now);
        return ImmutableList.copyOf(values);
    }
    
    public synchronized List<TimestampedValue<T>> getValuesInWindow(long now, Duration subTimePeriod) {
        long startTime = now - subTimePeriod.toMilliseconds();
        List<TimestampedValue<T>> result = new LinkedList<TimestampedValue<T>>();
        TimestampedValue<T> mostRecentExpired = null;
        for (TimestampedValue<T> val : values) {
            if (val.getTimestamp() < startTime) {
                // discard; but remember most recent too-old value so we include that as the "initial"
                mostRecentExpired = val;
            } else {
                result.add(val);
            }
        }
        if (minExpiredVals > 0 && mostRecentExpired != null) {
            result.add(0, mostRecentExpired);
        }
        
        if (result.size() < minVals) {
            int minIndex = Math.max(0, values.size()-minVals);
            return ImmutableList.copyOf(values.subList(minIndex, values.size()));
        } else {
            return result;
        }
    }
    
    public void add(T val) {
        add(val, System.currentTimeMillis());
    }
    
    public synchronized void add(T val, long timestamp) {
        values.add(values.size(), new TimestampedValue<T>(val, timestamp));
        pruneValues(timestamp);
    }
    
    public synchronized void pruneValues(long now) {
        long startTime = now - timePeriod.toMilliseconds();
        int expiredValsCount = 0;
        if (timePeriod.equals(Duration.ZERO)) {
            expiredValsCount = values.size();
        } else {
            for (TimestampedValue<T> val : values) {
                if (val.getTimestamp() < startTime) {
                    expiredValsCount++;
                } else {
                    break;
                }
            }
        }
        int numToPrune = Math.min(expiredValsCount - minExpiredVals, values.size()-minVals);
        for (int i = 0; i < numToPrune; i++) {
            values.removeFirst();
        }
    }
    
    @Override
    public String toString() {
        return "timePeriod="+timePeriod+", vals="+values;
    }
}
