/*
 * Copyright (C) 2019 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 com.android.server.display.whitebalance;

import android.util.Slog;

import java.io.PrintWriter;
import java.util.Arrays;

/**
 * The DisplayWhiteBalanceController uses the DisplayWhiteBalanceThrottler to decide whether the
 * display color temperature should be updated, suppressing changes that are too frequent or too
 * minor.
 */
class DisplayWhiteBalanceThrottler {

    protected static final String TAG = "DisplayWhiteBalanceThrottler";
    protected boolean mLoggingEnabled;

    private int mIncreaseDebounce;  // Milliseconds
    private int mDecreaseDebounce;  // Milliseconds
    private long mLastTime;         // Milliseconds

    private float[] mBaseThresholds;
    private float[] mIncreaseThresholds;
    private float[] mDecreaseThresholds;
    private float mIncreaseThreshold;
    private float mDecreaseThreshold;
    private float mLastValue;

    /**
     * @param increaseDebounce
     *      The debounce time for increasing (throttled if {@code time < lastTime + debounce}).
     * @param decreaseDebounce
     *      The debounce time for decreasing (throttled if {@code time < lastTime + debounce}).
     * @param baseThresholds
     *      The ambient color temperature values used to determine the threshold as the
     *      corresponding value in increaseThresholds/decreaseThresholds.
     * @param increaseThresholds
     *      The increase threshold values (throttled if {@code value < value * (1 + threshold)}).
     * @param decreaseThresholds
     *      The decrease threshold values (throttled if {@code value > value * (1 - threshold)}).
     *
     * @throws IllegalArgumentException
     *      - increaseDebounce is negative;
     *      - decreaseDebounce is negative;
     *      - baseThresholds to increaseThresholds is not a valid mapping*;
     *      - baseThresholds to decreaseThresholds is not a valid mapping*;
     *
     *      (*) The x to y mapping is valid if:
     *          - x and y are not null;
     *          - x and y are not empty;
     *          - x and y contain only non-negative numbers;
     *          - x is strictly increasing.
     */
    DisplayWhiteBalanceThrottler(int increaseDebounce, int decreaseDebounce,
            float[] baseThresholds, float[] increaseThresholds, float[] decreaseThresholds) {
        validateArguments(increaseDebounce, decreaseDebounce, baseThresholds, increaseThresholds,
                decreaseThresholds);
        mLoggingEnabled = false;
        mIncreaseDebounce = increaseDebounce;
        mDecreaseDebounce = decreaseDebounce;
        mBaseThresholds = baseThresholds;
        mIncreaseThresholds = increaseThresholds;
        mDecreaseThresholds = decreaseThresholds;
        clear();
    }

    /**
     * Check whether the ambient color temperature should be throttled.
     *
     * @param value
     *      The ambient color temperature value.
     *
     * @return Whether the ambient color temperature should be throttled.
     */
    public boolean throttle(float value) {
        if (mLastTime != -1 && (tooSoon(value) || tooClose(value))) {
            return true;
        }
        computeThresholds(value);
        mLastTime = System.currentTimeMillis();
        mLastValue = value;
        return false;
    }

    /**
     * Clears the throttler state.
     */
    public void clear() {
        mLastTime = -1;
        mIncreaseThreshold = -1.0f;
        mDecreaseThreshold = -1.0f;
        mLastValue = -1.0f;
    }

    /**
     * Enable/disable logging.
     *
     * @param loggingEnabled
     *      Whether logging is on/off.
     *
     * @return Whether the method succeeded or not.
     */
    public boolean setLoggingEnabled(boolean loggingEnabled) {
        if (mLoggingEnabled == loggingEnabled) {
            return false;
        }
        mLoggingEnabled = loggingEnabled;
        return true;
    }

    /**
     * Dump the state.
     *
     * @param writer
     *      The PrintWriter used to dump the state.
     */
    public void dump(PrintWriter writer) {
        writer.println("  DisplayWhiteBalanceThrottler");
        writer.println("    mLoggingEnabled=" + mLoggingEnabled);
        writer.println("    mIncreaseDebounce=" + mIncreaseDebounce);
        writer.println("    mDecreaseDebounce=" + mDecreaseDebounce);
        writer.println("    mLastTime=" + mLastTime);
        writer.println("    mBaseThresholds=" + Arrays.toString(mBaseThresholds));
        writer.println("    mIncreaseThresholds=" + Arrays.toString(mIncreaseThresholds));
        writer.println("    mDecreaseThresholds=" + Arrays.toString(mDecreaseThresholds));
        writer.println("    mIncreaseThreshold=" + mIncreaseThreshold);
        writer.println("    mDecreaseThreshold=" + mDecreaseThreshold);
        writer.println("    mLastValue=" + mLastValue);
    }

    private void validateArguments(float increaseDebounce, float decreaseDebounce,
            float[] baseThresholds, float[] increaseThresholds, float[] decreaseThresholds) {
        if (Float.isNaN(increaseDebounce) || increaseDebounce < 0.0f) {
            throw new IllegalArgumentException("increaseDebounce must be a non-negative number.");
        }
        if (Float.isNaN(decreaseDebounce) || decreaseDebounce < 0.0f) {
            throw new IllegalArgumentException("decreaseDebounce must be a non-negative number.");
        }
        if (!isValidMapping(baseThresholds, increaseThresholds)) {
            throw new IllegalArgumentException(
                    "baseThresholds to increaseThresholds is not a valid mapping.");
        }
        if (!isValidMapping(baseThresholds, decreaseThresholds)) {
            throw new IllegalArgumentException(
                    "baseThresholds to decreaseThresholds is not a valid mapping.");
        }
    }

    private static boolean isValidMapping(float[] x, float[] y) {
        if (x == null || y == null || x.length == 0 || y.length == 0 || x.length != y.length) {
            return false;
        }
        float prevX = -1.0f;
        for (int i = 0; i < x.length; i++) {
            if (Float.isNaN(x[i]) || Float.isNaN(y[i]) || x[i] < 0 || prevX >= x[i]) {
                return false;
            }
            prevX = x[i];
        }
        return true;
    }

    private boolean tooSoon(float value) {
        final long time = System.currentTimeMillis();
        final long earliestTime;
        if (value > mLastValue) {
            earliestTime = mLastTime + mIncreaseDebounce;
        } else { // value <= mLastValue
            earliestTime = mLastTime + mDecreaseDebounce;
        }
        final boolean tooSoon = time < earliestTime;
        if (mLoggingEnabled) {
            Slog.d(TAG, (tooSoon ? "too soon: " : "late enough: ") + time
                    + (tooSoon ? " < " : " > ") + earliestTime);
        }
        return tooSoon;
    }

    private boolean tooClose(float value) {
        final float threshold;
        final boolean tooClose;
        if (value > mLastValue) {
            threshold = mIncreaseThreshold;
            tooClose = value < threshold;
        } else { // value <= mLastValue
            threshold = mDecreaseThreshold;
            tooClose = value > threshold;
        }
        if (mLoggingEnabled) {
            Slog.d(TAG, (tooClose ? "too close: " : "far enough: ") + value
                    + (value > threshold ? " > " : " < ") + threshold);
        }
        return tooClose;
    }

    private void computeThresholds(float value) {
        final int index = getHighestIndexBefore(value, mBaseThresholds);
        mIncreaseThreshold = value * (1.0f + mIncreaseThresholds[index]);
        mDecreaseThreshold = value * (1.0f - mDecreaseThresholds[index]);
    }

    private int getHighestIndexBefore(float value, float[] values) {
        for (int i = 0; i < values.length; i++) {
            if (values[i] >= value) {
                return i;
            }
        }
        return values.length - 1;
    }

}
