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

import java.text.DecimalFormat;

import org.apache.jmeter.samplers.SampleResult;

/**
 * <p>
 * Running sample data container. Just instantiate a new instance of this
 * class, and then call {@link #addSample(SampleResult)} a few times, and pull
 * the stats out with whatever methods you prefer.
 * </p>
 * <p>
 * Please note that this class is not thread-safe.
 * The calling class is responsible for ensuring thread safety if required.
 * Versions prior to 2.3.2 appeared to be thread-safe but weren't as label and index were not final.
 * Also the caller needs to synchronize access in order to enure that variables are consistent.
 * </p>
 *
 */
public class RunningSample {

    private final DecimalFormat rateFormatter = new DecimalFormat("#.0"); // $NON-NLS-1$

    private final DecimalFormat errorFormatter = new DecimalFormat("#0.00%"); // $NON-NLS-1$

    private long counter;

    private long runningSum;

    private long max, min;

    private long errorCount;

    private long firstTime;

    private long lastTime;

    private final String label;

    private final int index;

    /**
     * Use this constructor to create the initial instance
     */
    public RunningSample(String label, int index) {
        this.label = label;
        this.index = index;
        init();
    }

    /**
     * Copy constructor to create a duplicate of existing instance (without the
     * disadvantages of clone()
     *
     * @param src existing RunningSample to be copied
     */
    public RunningSample(RunningSample src) {
        this.counter = src.counter;
        this.errorCount = src.errorCount;
        this.firstTime = src.firstTime;
        this.index = src.index;
        this.label = src.label;
        this.lastTime = src.lastTime;
        this.max = src.max;
        this.min = src.min;
        this.runningSum = src.runningSum;
    }

    private void init() {
        counter = 0L;
        runningSum = 0L;
        max = Long.MIN_VALUE;
        min = Long.MAX_VALUE;
        errorCount = 0L;
        firstTime = Long.MAX_VALUE;
        lastTime = 0L;
    }

    /**
     * Clear the counters (useful for differential stats)
     *
     */
    public void clear() {
        init();
    }

    /**
     * Get the elapsed time for the samples
     *
     * @return how long the samples took
     */
    public long getElapsed() {
        if (lastTime == 0) {
            return 0;// No samples collected ...
        }
        return lastTime - firstTime;
    }

    /**
     * Returns the throughput associated to this sampler in requests per second.
     * May be slightly skewed because it takes the timestamps of the first and
     * last samples as the total time passed, and the test may actually have
     * started before that start time and ended after that end time.
     */
    public double getRate() {
        if (counter == 0) {
            return 0.0; // Better behaviour when howLong=0 or lastTime=0
        }

        long howLongRunning = lastTime - firstTime;

        if (howLongRunning == 0) {
            return Double.MAX_VALUE;
        }

        return (double) counter / howLongRunning * 1000.0;
    }

    /**
     * Returns the throughput associated to this sampler in requests per min.
     * May be slightly skewed because it takes the timestamps of the first and
     * last samples as the total time passed, and the test may actually have
     * started before that start time and ended after that end time.
     */
    public double getRatePerMin() {
        if (counter == 0) {
            return 0.0; // Better behaviour when howLong=0 or lastTime=0
        }

        long howLongRunning = lastTime - firstTime;

        if (howLongRunning == 0) {
            return Double.MAX_VALUE;
        }
        return (double) counter / howLongRunning * 60000.0;
    }

    /**
     * Returns a String that represents the throughput associated for this
     * sampler, in units appropriate to its dimension:
     * <p>
     * The number is represented in requests/second or requests/minute or
     * requests/hour.
     * <p>
     * Examples: "34.2/sec" "0.1/sec" "43.0/hour" "15.9/min"
     *
     * @return a String representation of the rate the samples are being taken
     *         at.
     */
    public String getRateString() {
        double rate = getRate();

        if (rate == Double.MAX_VALUE) {
            return "N/A";
        }

        String unit = "sec";

        if (rate < 1.0) {
            rate *= 60.0;
            unit = "min";
        }
        if (rate < 1.0) {
            rate *= 60.0;
            unit = "hour";
        }

        return rateFormatter.format(rate) + "/" + unit;
    }

    public String getLabel() {
        return label;
    }

    public int getIndex() {
        return index;
    }

    /**
     * Records a sample.
     *
     */
    public void addSample(SampleResult res) {
        long aTimeInMillis = res.getTime();
        
        counter+=res.getSampleCount();
        errorCount += res.getErrorCount();

        long startTime = res.getStartTime();
        long endTime = res.getEndTime();

        if (firstTime > startTime) {
            // this is our first sample, set the start time to current timestamp
            firstTime = startTime;
        }

        // Always update the end time
        if (lastTime < endTime) {
            lastTime = endTime;
        }
        runningSum += aTimeInMillis;

        if (aTimeInMillis > max) {
            max = aTimeInMillis;
        }

        if (aTimeInMillis < min) {
            min = aTimeInMillis;
        }

    }

    /**
     * Adds another RunningSample to this one.
     * Does not check if it has the same label and index.
     */
    public void addSample(RunningSample rs) {
        this.counter += rs.counter;
        this.errorCount += rs.errorCount;
        this.runningSum += rs.runningSum;
        if (this.firstTime > rs.firstTime) {
            this.firstTime = rs.firstTime;
        }
        if (this.lastTime < rs.lastTime) {
            this.lastTime = rs.lastTime;
        }
        if (this.max < rs.max) {
            this.max = rs.max;
        }
        if (this.min > rs.min) {
            this.min = rs.min;
        }
    }

    /**
     * Returns the time in milliseconds of the quickest sample.
     *
     * @return the time in milliseconds of the quickest sample.
     */
    public long getMin() {
        long rval = 0;

        if (min != Long.MAX_VALUE) {
            rval = min;
        }
        return rval;
    }

    /**
     * Returns the time in milliseconds of the slowest sample.
     *
     * @return the time in milliseconds of the slowest sample.
     */
    public long getMax() {
        long rval = 0;

        if (max != Long.MIN_VALUE) {
            rval = max;
        }
        return rval;
    }

    /**
     * Returns the average time in milliseconds that samples ran in.
     *
     * @return the average time in milliseconds that samples ran in.
     */
    public long getAverage() {
        if (counter == 0) {
            return 0;
        }
        return runningSum / counter;
    }

    /**
     * Returns the number of samples that have been recorded by this instance of
     * the RunningSample class.
     *
     * @return the number of samples that have been recorded by this instance of
     *         the RunningSample class.
     */
    public long getNumSamples() {
        return counter;
    }

    /**
     * Returns the raw double value of the percentage of samples with errors
     * that were recorded. (Between 0.0 and 1.0) If you want a nicer return
     * format, see {@link #getErrorPercentageString()}.
     *
     * @return the raw double value of the percentage of samples with errors
     *         that were recorded.
     */
    public double getErrorPercentage() {
        double rval = 0.0;

        if (counter == 0) {
            return rval;
        }
        rval = (double) errorCount / (double) counter;
        return rval;
    }

    /**
     * Returns a String which represents the percentage of sample errors that
     * have occurred. ("0.00%" through "100.00%")
     *
     * @return a String which represents the percentage of sample errors that
     *         have occurred.
     */
    public String getErrorPercentageString() {
        double myErrorPercentage = this.getErrorPercentage();

        return errorFormatter.format(myErrorPercentage);
    }

    /**
     * For debugging purposes, mainly.
     */
    @Override
    public String toString() {
        StringBuilder mySB = new StringBuilder();

        mySB.append("Samples: " + this.getNumSamples() + "  ");
        mySB.append("Avg: " + this.getAverage() + "  ");
        mySB.append("Min: " + this.getMin() + "  ");
        mySB.append("Max: " + this.getMax() + "  ");
        mySB.append("Error Rate: " + this.getErrorPercentageString() + "  ");
        mySB.append("Sample Rate: " + this.getRateString());
        return mySB.toString();
    }

    /**
     * @return errorCount
     */
    public long getErrorCount() {
        return errorCount;
    }

}
