/*
 * 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.annotation.NonNull;
import android.util.Slog;
import android.util.Spline;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
import com.android.server.display.utils.History;

import java.io.PrintWriter;

/**
 * The DisplayWhiteBalanceController drives display white-balance (automatically correcting the
 * display color temperature depending on the ambient color temperature).
 *
 * The DisplayWhiteBalanceController:
 * - Uses the AmbientColorTemperatureSensor to detect changes in the ambient color temperature;
 * - Uses the AmbientColorTemperatureFilter to average these changes over time, filter out the
 *   noise, and arrive at an estimate of the actual ambient color temperature;
 * - Uses the DisplayWhiteBalanceThrottler to decide whether the display color tempearture should
 *   be updated, suppressing changes that are too frequent or too minor.
 */
public class DisplayWhiteBalanceController implements
        AmbientSensor.AmbientBrightnessSensor.Callbacks,
        AmbientSensor.AmbientColorTemperatureSensor.Callbacks {

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

    private boolean mEnabled;

    // To decouple the DisplayPowerController from the DisplayWhiteBalanceController, the DPC
    // implements Callbacks and passes itself to the DWBC so it can call back into it without
    // knowing about it.
    private Callbacks mCallbacks;

    private AmbientSensor.AmbientBrightnessSensor mBrightnessSensor;

    @VisibleForTesting
    AmbientFilter mBrightnessFilter;
    private AmbientSensor.AmbientColorTemperatureSensor mColorTemperatureSensor;

    @VisibleForTesting
    AmbientFilter mColorTemperatureFilter;
    private DisplayWhiteBalanceThrottler mThrottler;

    private final float mLowLightAmbientColorTemperature;
    private final float mHighLightAmbientColorTemperature;

    private float mAmbientColorTemperature;

    @VisibleForTesting
    float mPendingAmbientColorTemperature;
    private float mLastAmbientColorTemperature;

    private ColorDisplayServiceInternal mColorDisplayServiceInternal;

    // The most recent ambient color temperature values are kept for debugging purposes.
    private static final int HISTORY_SIZE = 50;
    private History mAmbientColorTemperatureHistory;

    // Override the ambient color temperature for debugging purposes.
    private float mAmbientColorTemperatureOverride;

    // A piecewise linear relationship between ambient and display color temperatures.
    private Spline.LinearSpline mAmbientToDisplayColorTemperatureSpline;

    // In very low or very high brightness conditions ambient EQ should to set to a default
    // instead of using mAmbientToDisplayColorTemperatureSpline. However, setting ambient EQ
    // based on thresholds can cause the display to rapidly change color temperature. To solve
    // this, mLowLightAmbientBrightnessToBiasSpline and mHighLightAmbientBrightnessToBiasSpline
    // are used to smoothly interpolate from ambient color temperature to the defaults.
    // A piecewise linear relationship between low light brightness and low light bias.
    private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSpline;

    // A piecewise linear relationship between high light brightness and high light bias.
    private Spline.LinearSpline mHighLightAmbientBrightnessToBiasSpline;

    private float mLatestAmbientColorTemperature;
    private float mLatestAmbientBrightness;
    private float mLatestLowLightBias;
    private float mLatestHighLightBias;

    /**
     * @param brightnessSensor
     *      The sensor used to detect changes in the ambient brightness.
     * @param brightnessFilter
     *      The filter used to avergae ambient brightness changes over time, filter out the noise
     *      and arrive at an estimate of the actual ambient brightness.
     * @param colorTemperatureSensor
     *      The sensor used to detect changes in the ambient color temperature.
     * @param colorTemperatureFilter
     *      The filter used to average ambient color temperature changes over time, filter out the
     *      noise and arrive at an estimate of the actual ambient color temperature.
     * @param throttler
     *      The throttler used to determine whether the new display color temperature should be
     *      updated or not.
     * @param lowLightAmbientBrightnesses
     *      The ambient brightness used to map the ambient brightnesses to the biases used to
     *      interpolate to lowLightAmbientColorTemperature.
     * @param lowLightAmbientBiases
     *      The biases used to map the ambient brightnesses to the biases used to interpolate to
     *      lowLightAmbientColorTemperature.
     * @param lowLightAmbientColorTemperature
     *      The ambient color temperature to which we interpolate to based on the low light curve.
     * @param highLightAmbientBrightnesses
     *      The ambient brightness used to map the ambient brightnesses to the biases used to
     *      interpolate to highLightAmbientColorTemperature.
     * @param highLightAmbientBiases
     *      The biases used to map the ambient brightnesses to the biases used to interpolate to
     *      highLightAmbientColorTemperature.
     * @param highLightAmbientColorTemperature
     *      The ambient color temperature to which we interpolate to based on the high light curve.
     * @param ambientColorTemperatures
     *      The ambient color tempeartures used to map the ambient color temperature to the display
     *      color temperature (or null if no mapping is necessary).
     * @param displayColorTemperatures
     *      The display color temperatures used to map the ambient color temperature to the display
     *      color temperature (or null if no mapping is necessary).
     *
     * @throws NullPointerException
     *      - brightnessSensor is null;
     *      - brightnessFilter is null;
     *      - colorTemperatureSensor is null;
     *      - colorTemperatureFilter is null;
     *      - throttler is null.
     */
    public DisplayWhiteBalanceController(
            @NonNull AmbientSensor.AmbientBrightnessSensor brightnessSensor,
            @NonNull AmbientFilter brightnessFilter,
            @NonNull AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor,
            @NonNull AmbientFilter colorTemperatureFilter,
            @NonNull DisplayWhiteBalanceThrottler throttler,
            float[] lowLightAmbientBrightnesses, float[] lowLightAmbientBiases,
            float lowLightAmbientColorTemperature,
            float[] highLightAmbientBrightnesses, float[] highLightAmbientBiases,
            float highLightAmbientColorTemperature,
            float[] ambientColorTemperatures, float[] displayColorTemperatures) {
        validateArguments(brightnessSensor, brightnessFilter, colorTemperatureSensor,
                colorTemperatureFilter, throttler);
        mLoggingEnabled = false;
        mEnabled = false;
        mCallbacks = null;
        mBrightnessSensor = brightnessSensor;
        mBrightnessFilter = brightnessFilter;
        mColorTemperatureSensor = colorTemperatureSensor;
        mColorTemperatureFilter = colorTemperatureFilter;
        mThrottler = throttler;
        mLowLightAmbientColorTemperature = lowLightAmbientColorTemperature;
        mHighLightAmbientColorTemperature = highLightAmbientColorTemperature;
        mAmbientColorTemperature = -1.0f;
        mPendingAmbientColorTemperature = -1.0f;
        mLastAmbientColorTemperature = -1.0f;
        mAmbientColorTemperatureHistory = new History(HISTORY_SIZE);
        mAmbientColorTemperatureOverride = -1.0f;

        try {
            mLowLightAmbientBrightnessToBiasSpline = new Spline.LinearSpline(
                    lowLightAmbientBrightnesses, lowLightAmbientBiases);
        } catch (Exception e) {
            Slog.e(TAG, "failed to create low light ambient brightness to bias spline.", e);
            mLowLightAmbientBrightnessToBiasSpline = null;
        }
        if (mLowLightAmbientBrightnessToBiasSpline != null) {
            if (mLowLightAmbientBrightnessToBiasSpline.interpolate(0.0f) != 0.0f ||
                    mLowLightAmbientBrightnessToBiasSpline.interpolate(Float.POSITIVE_INFINITY)
                    != 1.0f) {
                Slog.d(TAG, "invalid low light ambient brightness to bias spline, "
                        + "bias must begin at 0.0 and end at 1.0.");
                mLowLightAmbientBrightnessToBiasSpline = null;
            }
        }

        try {
            mHighLightAmbientBrightnessToBiasSpline = new Spline.LinearSpline(
                    highLightAmbientBrightnesses, highLightAmbientBiases);
        } catch (Exception e) {
            Slog.e(TAG, "failed to create high light ambient brightness to bias spline.", e);
            mHighLightAmbientBrightnessToBiasSpline = null;
        }
        if (mHighLightAmbientBrightnessToBiasSpline != null) {
            if (mHighLightAmbientBrightnessToBiasSpline.interpolate(0.0f) != 0.0f ||
                    mHighLightAmbientBrightnessToBiasSpline.interpolate(Float.POSITIVE_INFINITY)
                    != 1.0f) {
                Slog.d(TAG, "invalid high light ambient brightness to bias spline, "
                        + "bias must begin at 0.0 and end at 1.0.");
                mHighLightAmbientBrightnessToBiasSpline = null;
            }
        }

        if (mLowLightAmbientBrightnessToBiasSpline != null &&
                mHighLightAmbientBrightnessToBiasSpline != null) {
            if (lowLightAmbientBrightnesses[lowLightAmbientBrightnesses.length - 1] >
                    highLightAmbientBrightnesses[0]) {
                Slog.d(TAG, "invalid low light and high light ambient brightness to bias spline "
                        + "combination, defined domains must not intersect.");
                mLowLightAmbientBrightnessToBiasSpline = null;
                mHighLightAmbientBrightnessToBiasSpline = null;
            }
        }

        try {
            mAmbientToDisplayColorTemperatureSpline = new Spline.LinearSpline(
                    ambientColorTemperatures, displayColorTemperatures);
        } catch (Exception e) {
            Slog.e(TAG, "failed to create ambient to display color temperature spline.", e);
            mAmbientToDisplayColorTemperatureSpline = null;
        }

        mColorDisplayServiceInternal = LocalServices.getService(ColorDisplayServiceInternal.class);
    }

    /**
     * Enable/disable the controller.
     *
     * @param enabled
     *      Whether the controller should be on/off.
     *
     * @return Whether the method succeeded or not.
     */
    public boolean setEnabled(boolean enabled) {
        if (enabled) {
            return enable();
        } else {
            return disable();
        }
    }

    /**
     * Set an object to call back to when the display color temperature should be updated.
     *
     * @param callbacks
     *      The object to call back to.
     *
     * @return Whether the method succeeded or not.
     */
    public boolean setCallbacks(Callbacks callbacks) {
        if (mCallbacks == callbacks) {
            return false;
        }
        mCallbacks = callbacks;
        return true;
    }

    /**
     * Enable/disable logging.
     *
     * @param loggingEnabled
     *      Whether logging should be on/off.
     *
     * @return Whether the method succeeded or not.
     */
    public boolean setLoggingEnabled(boolean loggingEnabled) {
        if (mLoggingEnabled == loggingEnabled) {
            return false;
        }
        mLoggingEnabled = loggingEnabled;
        mBrightnessSensor.setLoggingEnabled(loggingEnabled);
        mBrightnessFilter.setLoggingEnabled(loggingEnabled);
        mColorTemperatureSensor.setLoggingEnabled(loggingEnabled);
        mColorTemperatureFilter.setLoggingEnabled(loggingEnabled);
        mThrottler.setLoggingEnabled(loggingEnabled);
        return true;
    }

    /**
     * Set the ambient color temperature override.
     *
     * This is only applied when the ambient color temperature changes or is updated (in which case
     * it overrides the ambient color temperature estimate); in other words, it doesn't necessarily
     * change the display color temperature immediately.
     *
     * @param ambientColorTemperatureOverride
     *      The ambient color temperature override.
     *
     * @return Whether the method succeeded or not.
     */
    public boolean setAmbientColorTemperatureOverride(float ambientColorTemperatureOverride) {
        if (mAmbientColorTemperatureOverride == ambientColorTemperatureOverride) {
            return false;
        }
        mAmbientColorTemperatureOverride = ambientColorTemperatureOverride;
        return true;
    }

    /**
     * Dump the state.
     *
     * @param writer
     *      The writer used to dump the state.
     */
    public void dump(PrintWriter writer) {
        writer.println("DisplayWhiteBalanceController");
        writer.println("  mLoggingEnabled=" + mLoggingEnabled);
        writer.println("  mEnabled=" + mEnabled);
        writer.println("  mCallbacks=" + mCallbacks);
        mBrightnessSensor.dump(writer);
        mBrightnessFilter.dump(writer);
        mColorTemperatureSensor.dump(writer);
        mColorTemperatureFilter.dump(writer);
        mThrottler.dump(writer);
        writer.println("  mLowLightAmbientColorTemperature=" + mLowLightAmbientColorTemperature);
        writer.println("  mHighLightAmbientColorTemperature=" + mHighLightAmbientColorTemperature);
        writer.println("  mAmbientColorTemperature=" + mAmbientColorTemperature);
        writer.println("  mPendingAmbientColorTemperature=" + mPendingAmbientColorTemperature);
        writer.println("  mLastAmbientColorTemperature=" + mLastAmbientColorTemperature);
        writer.println("  mAmbientColorTemperatureHistory=" + mAmbientColorTemperatureHistory);
        writer.println("  mAmbientColorTemperatureOverride=" + mAmbientColorTemperatureOverride);
        writer.println("  mAmbientToDisplayColorTemperatureSpline="
                + mAmbientToDisplayColorTemperatureSpline);
        writer.println("  mLowLightAmbientBrightnessToBiasSpline="
                + mLowLightAmbientBrightnessToBiasSpline);
        writer.println("  mHighLightAmbientBrightnessToBiasSpline="
                + mHighLightAmbientBrightnessToBiasSpline);
    }

    @Override // AmbientSensor.AmbientBrightnessSensor.Callbacks
    public void onAmbientBrightnessChanged(float value) {
        final long time = System.currentTimeMillis();
        mBrightnessFilter.addValue(time, value);
        updateAmbientColorTemperature();
    }

    @Override // AmbientSensor.AmbientColorTemperatureSensor.Callbacks
    public void onAmbientColorTemperatureChanged(float value) {
        final long time = System.currentTimeMillis();
        mColorTemperatureFilter.addValue(time, value);
        updateAmbientColorTemperature();
    }

    /**
     * Updates the ambient color temperature.
     */
    public void updateAmbientColorTemperature() {
        final long time = System.currentTimeMillis();
        float ambientColorTemperature = mColorTemperatureFilter.getEstimate(time);
        mLatestAmbientColorTemperature = ambientColorTemperature;

        if (mAmbientToDisplayColorTemperatureSpline != null && ambientColorTemperature != -1.0f) {
            ambientColorTemperature =
                mAmbientToDisplayColorTemperatureSpline.interpolate(ambientColorTemperature);
        }

        float ambientBrightness = mBrightnessFilter.getEstimate(time);
        mLatestAmbientBrightness = ambientBrightness;

        if (ambientColorTemperature != -1.0f &&
                mLowLightAmbientBrightnessToBiasSpline != null) {
            float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
            ambientColorTemperature =
                    bias * ambientColorTemperature + (1.0f - bias)
                    * mLowLightAmbientColorTemperature;
            mLatestLowLightBias = bias;
        }
        if (ambientColorTemperature != -1.0f &&
                mHighLightAmbientBrightnessToBiasSpline != null) {
            float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
            ambientColorTemperature =
                    (1.0f - bias) * ambientColorTemperature + bias
                    * mHighLightAmbientColorTemperature;
            mLatestHighLightBias = bias;
        }

        if (mAmbientColorTemperatureOverride != -1.0f) {
            if (mLoggingEnabled) {
                Slog.d(TAG, "override ambient color temperature: " + ambientColorTemperature
                        + " => " + mAmbientColorTemperatureOverride);
            }
            ambientColorTemperature = mAmbientColorTemperatureOverride;
        }

        // When the display color temperature needs to be updated, we call DisplayPowerController to
        // call our updateColorTemperature. The reason we don't call it directly is that we want
        // all changes to the system to happen in a predictable order in DPC's main loop
        // (updatePowerState).
        if (ambientColorTemperature == -1.0f || mThrottler.throttle(ambientColorTemperature)) {
            return;
        }

        if (mLoggingEnabled) {
            Slog.d(TAG, "pending ambient color temperature: " + ambientColorTemperature);
        }
        mPendingAmbientColorTemperature = ambientColorTemperature;
        if (mCallbacks != null) {
            mCallbacks.updateWhiteBalance();
        }
    }

    /**
     * Updates the display color temperature.
     */
    public void updateDisplayColorTemperature() {
        float ambientColorTemperature = -1.0f;

        // If both the pending and the current ambient color temperatures are -1, it means the DWBC
        // was just enabled, and we use the last ambient color temperature until new sensor events
        // give us a better estimate.
        if (mAmbientColorTemperature == -1.0f && mPendingAmbientColorTemperature == -1.0f) {
            ambientColorTemperature = mLastAmbientColorTemperature;
        }

        // Otherwise, we use the pending ambient color temperature, but only if it's non-trivial
        // and different than the current one.
        if (mPendingAmbientColorTemperature != -1.0f
                && mPendingAmbientColorTemperature != mAmbientColorTemperature) {
            ambientColorTemperature = mPendingAmbientColorTemperature;
        }

        if (ambientColorTemperature == -1.0f) {
            return;
        }

        mAmbientColorTemperature = ambientColorTemperature;
        if (mLoggingEnabled) {
            Slog.d(TAG, "ambient color temperature: " + mAmbientColorTemperature);
        }
        mPendingAmbientColorTemperature = -1.0f;
        mAmbientColorTemperatureHistory.add(mAmbientColorTemperature);
        Slog.d(TAG, "Display cct: " + mAmbientColorTemperature
                + " Latest ambient cct: " + mLatestAmbientColorTemperature
                + " Latest ambient lux: " + mLatestAmbientBrightness
                + " Latest low light bias: " + mLatestLowLightBias
                + " Latest high light bias: " + mLatestHighLightBias);
        mColorDisplayServiceInternal.setDisplayWhiteBalanceColorTemperature(
                (int) mAmbientColorTemperature);
        mLastAmbientColorTemperature = mAmbientColorTemperature;
    }

    /**
     * The DisplayWhiteBalanceController decouples itself from its parent (DisplayPowerController)
     * by providing this interface to implement (and a method to set its callbacks object), and
     * calling these methods.
     */
    public interface Callbacks {

        /**
         * Called whenever the display white-balance state has changed.
         *
         * Usually, this means the estimated ambient color temperature has changed enough, and the
         * display color temperature should be updated; but it is also called if settings change.
         */
        void updateWhiteBalance();
    }

    private void validateArguments(AmbientSensor.AmbientBrightnessSensor brightnessSensor,
            AmbientFilter brightnessFilter,
            AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor,
            AmbientFilter colorTemperatureFilter,
            DisplayWhiteBalanceThrottler throttler) {
        Preconditions.checkNotNull(brightnessSensor, "brightnessSensor must not be null");
        Preconditions.checkNotNull(brightnessFilter, "brightnessFilter must not be null");
        Preconditions.checkNotNull(colorTemperatureSensor,
                "colorTemperatureSensor must not be null");
        Preconditions.checkNotNull(colorTemperatureFilter,
                "colorTemperatureFilter must not be null");
        Preconditions.checkNotNull(throttler, "throttler cannot be null");
    }

    private boolean enable() {
        if (mEnabled) {
            return false;
        }
        if (mLoggingEnabled) {
            Slog.d(TAG, "enabling");
        }
        mEnabled = true;
        mBrightnessSensor.setEnabled(true);
        mColorTemperatureSensor.setEnabled(true);
        return true;
    }

    private boolean disable() {
        if (!mEnabled) {
            return false;
        }
        if (mLoggingEnabled) {
            Slog.d(TAG, "disabling");
        }
        mEnabled = false;
        mBrightnessSensor.setEnabled(false);
        mBrightnessFilter.clear();
        mColorTemperatureSensor.setEnabled(false);
        mColorTemperatureFilter.clear();
        mThrottler.clear();
        mAmbientColorTemperature = -1.0f;
        mPendingAmbientColorTemperature = -1.0f;
        mColorDisplayServiceInternal.resetDisplayWhiteBalanceColorTemperature();
        return true;
    }

}
