/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed 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 android.telephony;

import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Parcelable class to store Telephony histogram.
 * @hide
 */
@SystemApi
public final class TelephonyHistogram implements Parcelable {
    // Type of Telephony histogram Eg: RIL histogram will have all timing data associated with
    // RIL calls. Similarly we can have any other Telephony histogram.
    private final int mCategory;

    // Unique Id identifying a sample within particular category of histogram
    private final int mId;

    // Min time taken in ms
    private int mMinTimeMs;

    // Max time taken in ms
    private int mMaxTimeMs;

    // Average time taken in ms
    private int mAverageTimeMs;

    // Total count of samples
    private int mSampleCount;

    // Array storing time taken for first #RANGE_CALCULATION_COUNT samples of histogram.
    private int[] mInitialTimings;

    // Total number of time ranges expected (must be greater than 1)
    private final int mBucketCount;

    // Array storing endpoints of range buckets. Calculated based on values of minTime & maxTime
    // after totalTimeCount is #RANGE_CALCULATION_COUNT.
    private final int[] mBucketEndPoints;

    // Array storing counts for each time range starting from smallest value range
    private final int[] mBucketCounters;

    /**
     * Constant for Telephony category
     */
    public static final int TELEPHONY_CATEGORY_RIL = 1;

    // Count of Histogram samples after which time buckets are created.
    private static final int RANGE_CALCULATION_COUNT = 10;


    // Constant used to indicate #initialTimings is null while parceling
    private static final int ABSENT = 0;

    // Constant used to indicate #initialTimings is not null while parceling
    private static final int PRESENT = 1;

    // Throws exception if #totalBuckets is not greater than one.
    public TelephonyHistogram (int category, int id, int bucketCount) {
        if (bucketCount <= 1) {
            throw new IllegalArgumentException("Invalid number of buckets");
        }
        mCategory = category;
        mId = id;
        mMinTimeMs = Integer.MAX_VALUE;
        mMaxTimeMs = 0;
        mAverageTimeMs = 0;
        mSampleCount = 0;
        mInitialTimings = new int[RANGE_CALCULATION_COUNT];
        mBucketCount = bucketCount;
        mBucketEndPoints = new int[bucketCount - 1];
        mBucketCounters = new int[bucketCount];
    }

    public TelephonyHistogram(TelephonyHistogram th) {
        mCategory = th.getCategory();
        mId = th.getId();
        mMinTimeMs = th.getMinTime();
        mMaxTimeMs = th.getMaxTime();
        mAverageTimeMs = th.getAverageTime();
        mSampleCount = th.getSampleCount();
        mInitialTimings = th.getInitialTimings();
        mBucketCount = th.getBucketCount();
        mBucketEndPoints = th.getBucketEndPoints();
        mBucketCounters = th.getBucketCounters();
    }

    public int getCategory() {
        return mCategory;
    }

    public int getId() {
        return mId;
    }

    public int getMinTime() {
        return mMinTimeMs;
    }

    public int getMaxTime() {
        return mMaxTimeMs;
    }

    public int getAverageTime() {
        return mAverageTimeMs;
    }

    public int getSampleCount () {
        return mSampleCount;
    }

    private int[] getInitialTimings() {
        return mInitialTimings;
    }

    public int getBucketCount() {
        return mBucketCount;
    }

    public int[] getBucketEndPoints() {
        if (mSampleCount > 1 && mSampleCount < 10) {
            int[] tempEndPoints = new int[mBucketCount - 1];
            calculateBucketEndPoints(tempEndPoints);
            return tempEndPoints;
        } else {
            return getDeepCopyOfArray(mBucketEndPoints);
        }
    }

    public int[] getBucketCounters() {
        if (mSampleCount > 1 && mSampleCount < 10) {
            int[] tempEndPoints = new int[mBucketCount - 1];
            int[] tempBucketCounters = new int[mBucketCount];
            calculateBucketEndPoints(tempEndPoints);
            for (int j = 0; j < mSampleCount; j++) {
                addToBucketCounter(tempEndPoints, tempBucketCounters, mInitialTimings[j]);
            }
            return tempBucketCounters;
        } else {
            return getDeepCopyOfArray(mBucketCounters);
        }
    }

    private int[] getDeepCopyOfArray(int[] array) {
        int[] clone = new int[array.length];
        System.arraycopy(array, 0, clone, 0, array.length);
        return clone;
    }

    private void addToBucketCounter(int[] bucketEndPoints, int[] bucketCounters, int time) {
        int i;
        for (i = 0; i < bucketEndPoints.length; i++) {
            if (time <= bucketEndPoints[i]) {
                bucketCounters[i]++;
                return;
            }
        }
        bucketCounters[i]++;
    }

    private void calculateBucketEndPoints(int[] bucketEndPoints) {
        for (int i = 1; i < mBucketCount; i++) {
            int endPt = mMinTimeMs + (i * (mMaxTimeMs - mMinTimeMs)) / mBucketCount;
            bucketEndPoints[i - 1] = endPt;
        }
    }

    // Add new value of time taken
    // This function updates minTime, maxTime, averageTime & totalTimeCount every time it is
    // called. initialTimings[] is updated if totalTimeCount <= #RANGE_CALCULATION_COUNT. When
    // totalTimeCount = RANGE_CALCULATION_COUNT, based on the min, max time & the number of buckets
    // expected, bucketEndPoints[] would be calculated. Then bucketCounters[] would be filled up
    // using values stored in initialTimings[]. Thereafter bucketCounters[] will always be updated.
    public void addTimeTaken(int time) {
        // Initialize all fields if its first entry or if integer overflow is going to occur while
        // trying to calculate averageTime
        if (mSampleCount == 0 || (mSampleCount == Integer.MAX_VALUE)) {
            if (mSampleCount == 0) {
                mMinTimeMs = time;
                mMaxTimeMs = time;
                mAverageTimeMs = time;
            } else {
                mInitialTimings = new int[RANGE_CALCULATION_COUNT];
            }
            mSampleCount = 1;
            Arrays.fill(mInitialTimings, 0);
            mInitialTimings[0] = time;
            Arrays.fill(mBucketEndPoints, 0);
            Arrays.fill(mBucketCounters, 0);
        } else {
            if (time < mMinTimeMs) {
                mMinTimeMs = time;
            }
            if (time > mMaxTimeMs) {
                mMaxTimeMs = time;
            }
            long totalTime = ((long)mAverageTimeMs) * mSampleCount + time;
            mAverageTimeMs = (int)(totalTime/++mSampleCount);

            if (mSampleCount < RANGE_CALCULATION_COUNT) {
                mInitialTimings[mSampleCount - 1] = time;
            } else if (mSampleCount == RANGE_CALCULATION_COUNT) {
                mInitialTimings[mSampleCount - 1] = time;

                // Calculate bucket endpoints based on bucketCount expected
                calculateBucketEndPoints(mBucketEndPoints);

                // Use values stored in initialTimings[] to update bucketCounters
                for (int j = 0; j < RANGE_CALCULATION_COUNT; j++) {
                    addToBucketCounter(mBucketEndPoints, mBucketCounters, mInitialTimings[j]);
                }
                mInitialTimings = null;
            } else {
                addToBucketCounter(mBucketEndPoints, mBucketCounters, time);
            }

        }
    }

    public String toString() {
        String basic = " Histogram id = " + mId + " Time(ms): min = " + mMinTimeMs + " max = "
                + mMaxTimeMs + " avg = " + mAverageTimeMs + " Count = " + mSampleCount;
        if (mSampleCount < RANGE_CALCULATION_COUNT) {
            return basic;
        } else {
            StringBuffer intervals = new StringBuffer(" Interval Endpoints:");
            for (int i = 0; i < mBucketEndPoints.length; i++) {
                intervals.append(" " + mBucketEndPoints[i]);
            }
            intervals.append(" Interval counters:");
            for (int i = 0; i < mBucketCounters.length; i++) {
                intervals.append(" " + mBucketCounters[i]);
            }
            return basic + intervals;
        }
    }

    public static final Parcelable.Creator<TelephonyHistogram> CREATOR =
            new Parcelable.Creator<TelephonyHistogram> () {

                @Override
                public TelephonyHistogram createFromParcel(Parcel in) {
                    return new TelephonyHistogram(in);
                }

                @Override
                public TelephonyHistogram[] newArray(int size) {
                    return new TelephonyHistogram[size];
                }
            };

    public TelephonyHistogram(Parcel in) {
        mCategory = in.readInt();
        mId = in.readInt();
        mMinTimeMs = in.readInt();
        mMaxTimeMs = in.readInt();
        mAverageTimeMs = in.readInt();
        mSampleCount = in.readInt();
        if (in.readInt() == PRESENT) {
            mInitialTimings = new int[RANGE_CALCULATION_COUNT];
            in.readIntArray(mInitialTimings);
        }
        mBucketCount = in.readInt();
        mBucketEndPoints = new int[mBucketCount - 1];
        in.readIntArray(mBucketEndPoints);
        mBucketCounters = new int[mBucketCount];
        in.readIntArray(mBucketCounters);
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mCategory);
        out.writeInt(mId);
        out.writeInt(mMinTimeMs);
        out.writeInt(mMaxTimeMs);
        out.writeInt(mAverageTimeMs);
        out.writeInt(mSampleCount);
        if (mInitialTimings == null) {
            out.writeInt(ABSENT);
        } else {
            out.writeInt(PRESENT);
            out.writeIntArray(mInitialTimings);
        }
        out.writeInt(mBucketCount);
        out.writeIntArray(mBucketEndPoints);
        out.writeIntArray(mBucketCounters);
    }

    @Override
    public int describeContents() {
        return 0;
    }
}
