/*
 * Copyright 2018 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.hardware.display;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;

import com.android.internal.util.Preconditions;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Objects;

/**
 * AmbientBrightnessDayStats stores and manipulates brightness stats over a single day.
 * {@see DisplayManager.getAmbientBrightnessStats()}
 *
 * @hide
 */
@SystemApi
@TestApi
public final class AmbientBrightnessDayStats implements Parcelable {

    /** The localdate for which brightness stats are being tracked */
    private final LocalDate mLocalDate;

    /** Ambient brightness values for creating bucket boundaries from */
    private final float[] mBucketBoundaries;

    /** Stats of how much time (in seconds) was spent in each of the buckets */
    private final float[] mStats;

    /**
     * Initialize day stats from the given state. The time spent in each of the bucket is
     * initialized to 0.
     *
     * @param localDate        The date for which stats are being tracked
     * @param bucketBoundaries Bucket boundaries used from creating the buckets from
     * @hide
     */
    public AmbientBrightnessDayStats(@NonNull LocalDate localDate,
            @NonNull float[] bucketBoundaries) {
        this(localDate, bucketBoundaries, null);
    }

    /**
     * Initialize day stats from the given state
     *
     * @param localDate        The date for which stats are being tracked
     * @param bucketBoundaries Bucket boundaries used from creating the buckets from
     * @param stats            Time spent in each of the buckets (in seconds)
     * @hide
     */
    public AmbientBrightnessDayStats(@NonNull LocalDate localDate,
            @NonNull float[] bucketBoundaries, float[] stats) {
        Objects.requireNonNull(localDate);
        Objects.requireNonNull(bucketBoundaries);
        Preconditions.checkArrayElementsInRange(bucketBoundaries, 0, Float.MAX_VALUE,
                "bucketBoundaries");
        if (bucketBoundaries.length < 1) {
            throw new IllegalArgumentException("Bucket boundaries must contain at least 1 value");
        }
        checkSorted(bucketBoundaries);
        if (stats == null) {
            stats = new float[bucketBoundaries.length];
        } else {
            Preconditions.checkArrayElementsInRange(stats, 0, Float.MAX_VALUE, "stats");
            if (bucketBoundaries.length != stats.length) {
                throw new IllegalArgumentException(
                        "Bucket boundaries and stats must be of same size.");
            }
        }
        mLocalDate = localDate;
        mBucketBoundaries = bucketBoundaries;
        mStats = stats;
    }

    /**
     * @return The {@link LocalDate} for which brightness stats are being tracked.
     */
    public LocalDate getLocalDate() {
        return mLocalDate;
    }

    /**
     * @return Aggregated stats of time spent (in seconds) in various buckets.
     */
    public float[] getStats() {
        return mStats;
    }

    /**
     * Returns the bucket boundaries (in lux) used for creating buckets. For eg., if the bucket
     * boundaries array is {b1, b2, b3}, the buckets will be [b1, b2), [b2, b3), [b3, inf).
     *
     * @return The list of bucket boundaries.
     */
    public float[] getBucketBoundaries() {
        return mBucketBoundaries;
    }

    private AmbientBrightnessDayStats(Parcel source) {
        mLocalDate = LocalDate.parse(source.readString());
        mBucketBoundaries = source.createFloatArray();
        mStats = source.createFloatArray();
    }

    public static final @android.annotation.NonNull Creator<AmbientBrightnessDayStats> CREATOR =
            new Creator<AmbientBrightnessDayStats>() {

                @Override
                public AmbientBrightnessDayStats createFromParcel(Parcel source) {
                    return new AmbientBrightnessDayStats(source);
                }

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

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        AmbientBrightnessDayStats other = (AmbientBrightnessDayStats) obj;
        return mLocalDate.equals(other.mLocalDate) && Arrays.equals(mBucketBoundaries,
                other.mBucketBoundaries) && Arrays.equals(mStats, other.mStats);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = result * prime + mLocalDate.hashCode();
        result = result * prime + Arrays.hashCode(mBucketBoundaries);
        result = result * prime + Arrays.hashCode(mStats);
        return result;
    }

    @NonNull
    @Override
    public String toString() {
        StringBuilder bucketBoundariesString = new StringBuilder();
        StringBuilder statsString = new StringBuilder();
        for (int i = 0; i < mBucketBoundaries.length; i++) {
            if (i != 0) {
                bucketBoundariesString.append(", ");
                statsString.append(", ");
            }
            bucketBoundariesString.append(mBucketBoundaries[i]);
            statsString.append(mStats[i]);
        }
        return new StringBuilder()
                .append(mLocalDate).append(" ")
                .append("{").append(bucketBoundariesString).append("} ")
                .append("{").append(statsString).append("}").toString();
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mLocalDate.toString());
        dest.writeFloatArray(mBucketBoundaries);
        dest.writeFloatArray(mStats);
    }

    /**
     * Updates the stats by incrementing the time spent for the appropriate bucket based on ambient
     * brightness reading.
     *
     * @param ambientBrightness Ambient brightness reading (in lux)
     * @param durationSec       Time spent with the given reading (in seconds)
     * @hide
     */
    public void log(float ambientBrightness, float durationSec) {
        int bucketIndex = getBucketIndex(ambientBrightness);
        if (bucketIndex >= 0) {
            mStats[bucketIndex] += durationSec;
        }
    }

    private int getBucketIndex(float ambientBrightness) {
        if (ambientBrightness < mBucketBoundaries[0]) {
            return -1;
        }
        int low = 0;
        int high = mBucketBoundaries.length - 1;
        while (low < high) {
            int mid = (low + high) / 2;
            if (mBucketBoundaries[mid] <= ambientBrightness
                    && ambientBrightness < mBucketBoundaries[mid + 1]) {
                return mid;
            } else if (mBucketBoundaries[mid] < ambientBrightness) {
                low = mid + 1;
            } else if (mBucketBoundaries[mid] > ambientBrightness) {
                high = mid - 1;
            }
        }
        return low;
    }

    private static void checkSorted(float[] values) {
        if (values.length <= 1) {
            return;
        }
        float prevValue = values[0];
        for (int i = 1; i < values.length; i++) {
            Preconditions.checkState(prevValue < values[i]);
            prevValue = values[i];
        }
        return;
    }
}
