/*
 * 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.internal.display;

import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.MathUtils;
import android.util.Slog;
import android.view.Display;

import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;

/**
 * BrightnessSynchronizer helps convert between the int (old) system and float
 * (new) system for storing the brightness. It has methods to convert between the two and also
 * observes for when one of the settings is changed and syncs this with the other.
 */
public class BrightnessSynchronizer {
    private static final String TAG = "BrightnessSynchronizer";

    private static final boolean DEBUG = false;
    private static final Uri BRIGHTNESS_URI =
            Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);

    private static final long WAIT_FOR_RESPONSE_MILLIS = 200;

    private static final int MSG_RUN_UPDATE = 1;

    // The tolerance within which we consider brightness values approximately equal to eachother.
    // This value is approximately 1/3 of the smallest possible brightness value.
    public static final float EPSILON = 0.001f;

    private static int sBrightnessUpdateCount = 1;

    private final Context mContext;
    private final BrightnessSyncObserver mBrightnessSyncObserver;
    private final Clock mClock;
    private final Handler mHandler;

    private DisplayManager mDisplayManager;
    private int mLatestIntBrightness;
    private float mLatestFloatBrightness;
    private BrightnessUpdate mCurrentUpdate;
    private BrightnessUpdate mPendingUpdate;

    public BrightnessSynchronizer(Context context) {
        this(context, Looper.getMainLooper(), SystemClock::uptimeMillis);
    }

    @VisibleForTesting
    public BrightnessSynchronizer(Context context, Looper looper, Clock clock) {
        mContext = context;
        mClock = clock;
        mBrightnessSyncObserver = new BrightnessSyncObserver();
        mHandler = new BrightnessSynchronizerHandler(looper);
    }

    /**
     * Starts brightnessSyncObserver to ensure that the float and int brightness values stay
     * in sync.
     * This also ensures that values are synchronized at system start up too.
     * So we force an update to the int value, since float is the source of truth. Fallback to int
     * value, if float is invalid. If both are invalid, use default float value from config.
     */
    public void startSynchronizing() {
        if (mDisplayManager == null) {
            mDisplayManager = mContext.getSystemService(DisplayManager.class);
        }
        if (mBrightnessSyncObserver.isObserving()) {
            Slog.wtf(TAG, "Brightness sync observer requesting synchronization a second time.");
            return;
        }
        mLatestFloatBrightness = getScreenBrightnessFloat();
        mLatestIntBrightness = getScreenBrightnessInt();
        Slog.i(TAG, "Initial brightness readings: " + mLatestIntBrightness + "(int), "
                + mLatestFloatBrightness + "(float)");

        if (!Float.isNaN(mLatestFloatBrightness)) {
            mPendingUpdate = new BrightnessUpdate(BrightnessUpdate.TYPE_FLOAT,
                    mLatestFloatBrightness);
        } else if (mLatestIntBrightness != PowerManager.BRIGHTNESS_INVALID) {
            mPendingUpdate = new BrightnessUpdate(BrightnessUpdate.TYPE_INT,
                    mLatestIntBrightness);
        } else {
            final float defaultBrightness = mContext.getResources().getFloat(
                    com.android.internal.R.dimen.config_screenBrightnessSettingDefaultFloat);
            mPendingUpdate = new BrightnessUpdate(BrightnessUpdate.TYPE_FLOAT, defaultBrightness);
            Slog.i(TAG, "Setting initial brightness to default value of: " + defaultBrightness);
        }

        mBrightnessSyncObserver.startObserving(mHandler);
        mHandler.sendEmptyMessageAtTime(MSG_RUN_UPDATE, mClock.uptimeMillis());
    }

    /**
     * Prints data on dumpsys.
     */
    public void dump(PrintWriter pw) {
        pw.println("BrightnessSynchronizer");
        pw.println("  mLatestIntBrightness=" + mLatestIntBrightness);
        pw.println("  mLatestFloatBrightness=" + mLatestFloatBrightness);
        pw.println("  mCurrentUpdate=" + mCurrentUpdate);
        pw.println("  mPendingUpdate=" + mPendingUpdate);
    }

    /**
     * Converts between the int brightness system and the float brightness system.
     */
    public static float brightnessIntToFloat(int brightnessInt) {
        if (brightnessInt == PowerManager.BRIGHTNESS_OFF) {
            return PowerManager.BRIGHTNESS_OFF_FLOAT;
        } else if (brightnessInt == PowerManager.BRIGHTNESS_INVALID) {
            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
        } else {
            final float minFloat = PowerManager.BRIGHTNESS_MIN;
            final float maxFloat = PowerManager.BRIGHTNESS_MAX;
            final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
            final float maxInt = PowerManager.BRIGHTNESS_ON;
            return MathUtils.constrainedMap(minFloat, maxFloat, minInt, maxInt, brightnessInt);
        }
    }

    /**
     * Converts between the float brightness system and the int brightness system.
     */
    public static int brightnessFloatToInt(float brightnessFloat) {
        return Math.round(brightnessFloatToIntRange(brightnessFloat));
    }

    /**
     * Translates specified value from the float brightness system to the int brightness system,
     * given the min/max of each range. Accounts for special values such as OFF and invalid values.
     * Value returned as a float primitive (to preserve precision), but is a value within the
     * int-system range.
     */
    public static float brightnessFloatToIntRange(float brightnessFloat) {
        if (floatEquals(brightnessFloat, PowerManager.BRIGHTNESS_OFF_FLOAT)) {
            return PowerManager.BRIGHTNESS_OFF;
        } else if (Float.isNaN(brightnessFloat)) {
            return PowerManager.BRIGHTNESS_INVALID;
        } else {
            final float minFloat = PowerManager.BRIGHTNESS_MIN;
            final float maxFloat = PowerManager.BRIGHTNESS_MAX;
            final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
            final float maxInt = PowerManager.BRIGHTNESS_ON;
            return MathUtils.constrainedMap(minInt, maxInt, minFloat, maxFloat, brightnessFloat);
        }
    }

    /**
     * Consumes a brightness change event for the float-based brightness.
     *
     * @param brightness Float brightness.
     */
    private void handleBrightnessChangeFloat(float brightness) {
        mLatestFloatBrightness = brightness;
        handleBrightnessChange(BrightnessUpdate.TYPE_FLOAT, brightness);
    }

    /**
     * Consumes a brightness change event for the int-based brightness.
     *
     * @param brightness Int brightness.
     */
    private void handleBrightnessChangeInt(int brightness) {
        mLatestIntBrightness = brightness;
        handleBrightnessChange(BrightnessUpdate.TYPE_INT, brightness);
    }

    /**
     * Consumes a brightness change event.
     *
     * @param type Type of the brightness change (int/float)
     * @param brightness brightness.
     */
    private void handleBrightnessChange(int type, float brightness) {
        boolean swallowUpdate = mCurrentUpdate != null
                && mCurrentUpdate.swallowUpdate(type, brightness);
        BrightnessUpdate prevUpdate = null;
        if (!swallowUpdate) {
            prevUpdate = mPendingUpdate;
            mPendingUpdate = new BrightnessUpdate(type, brightness);
        }
        runUpdate();

        // If we created a new update and it is still pending after the update, add a log.
        if (!swallowUpdate && mPendingUpdate != null) {
            Slog.i(TAG, "New PendingUpdate: " + mPendingUpdate + ", prev=" + prevUpdate);
        }
    }

    /**
     * Runs updates for current and pending BrightnessUpdates.
     */
    private void runUpdate() {
        if (DEBUG) {
            Slog.d(TAG, "Running update mCurrent="  + mCurrentUpdate
                    + ", mPending=" + mPendingUpdate);
        }

        // do-while instead of while to allow mCurrentUpdate to get set if there's a pending update.
        do {
            if (mCurrentUpdate != null) {
                mCurrentUpdate.update();
                if (mCurrentUpdate.isRunning()) {
                    break; // current update is still running, nothing to do.
                } else if (mCurrentUpdate.isCompleted()) {
                    if (mCurrentUpdate.madeUpdates()) {
                        Slog.i(TAG, "Completed Update: " + mCurrentUpdate);
                    }
                    mCurrentUpdate = null;
                }
            }
            // No current update any more, lets start the next update if there is one.
            if (mCurrentUpdate == null && mPendingUpdate != null) {
                mCurrentUpdate = mPendingUpdate;
                mPendingUpdate = null;
            }
        } while (mCurrentUpdate != null);
    }

    /**
     * Gets the stored screen brightness float value from the display brightness setting.
     * @return brightness
     */
    private float getScreenBrightnessFloat() {
        return mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY);
    }

    /**
     * Gets the stored screen brightness int from the system settings.
     * @return brightness
     */
    private int getScreenBrightnessInt() {
        return Settings.System.getIntForUser(mContext.getContentResolver(),
                Settings.System.SCREEN_BRIGHTNESS, PowerManager.BRIGHTNESS_INVALID,
                UserHandle.USER_CURRENT);
    }

    /**
     * Tests whether two brightness float values are within a small enough tolerance
     * of each other.
     * @param a first float to compare
     * @param b second float to compare
     * @return whether the two values are within a small enough tolerance value
     */
    public static boolean floatEquals(float a, float b) {
        if (a == b) {
            return true;
        } else if (Float.isNaN(a) && Float.isNaN(b)) {
            return true;
        } else if (Math.abs(a - b) < EPSILON) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Encapsulates a brightness change event and contains logic for synchronizing the appropriate
     * settings for the specified brightness change.
     */
    @VisibleForTesting
    public class BrightnessUpdate {
        static final int TYPE_INT = 0x1;
        static final int TYPE_FLOAT = 0x2;

        private static final int STATE_NOT_STARTED = 1;
        private static final int STATE_RUNNING = 2;
        private static final int STATE_COMPLETED = 3;

        private final int mSourceType;
        private final float mBrightness;

        private long mTimeUpdated;
        private int mState;
        private int mUpdatedTypes;
        private int mConfirmedTypes;
        private int mId;

        BrightnessUpdate(int sourceType, float brightness) {
            mId = sBrightnessUpdateCount++;
            mSourceType = sourceType;
            mBrightness = brightness;
            mTimeUpdated = 0;
            mUpdatedTypes = 0x0;
            mConfirmedTypes = 0x0;
            mState = STATE_NOT_STARTED;
        }

        @Override
        public String toString() {
            return "{[" + mId + "] " + toStringLabel(mSourceType, mBrightness)
                    + ", mUpdatedTypes=" + mUpdatedTypes + ", mConfirmedTypes=" + mConfirmedTypes
                    + ", mTimeUpdated=" + mTimeUpdated + "}";
        }

        /**
         * Runs the synchronization process, moving forward through the internal state machine.
         */
        void update() {
            if (mState == STATE_NOT_STARTED) {
                mState = STATE_RUNNING;

                // check if we need to update int
                int brightnessInt = getBrightnessAsInt();
                if (mLatestIntBrightness != brightnessInt) {
                    Settings.System.putIntForUser(mContext.getContentResolver(),
                            Settings.System.SCREEN_BRIGHTNESS, brightnessInt,
                            UserHandle.USER_CURRENT);
                    mLatestIntBrightness = brightnessInt;
                    mUpdatedTypes |= TYPE_INT;
                }

                // check if we need to update float
                float brightnessFloat = getBrightnessAsFloat();
                if (!floatEquals(mLatestFloatBrightness, brightnessFloat)) {
                    mDisplayManager.setBrightness(Display.DEFAULT_DISPLAY, brightnessFloat);
                    mLatestFloatBrightness = brightnessFloat;
                    mUpdatedTypes |= TYPE_FLOAT;
                }

                // If we made updates, lets wait for responses.
                if (mUpdatedTypes != 0x0) {
                    // Give some time for our updates to return a confirmation response. If they
                    // don't return by that time, MSG_RUN_UPDATE will get sent and we will stop
                    // listening for responses and mark this update as complete.
                    if (DEBUG) {
                        Slog.d(TAG, "Sending MSG_RUN_UPDATE for "
                                + toStringLabel(mSourceType, mBrightness));
                    }
                    Slog.i(TAG, "[" + mId + "] New Update "
                            + toStringLabel(mSourceType, mBrightness) + " set brightness values: "
                            + toStringLabel(mUpdatedTypes & TYPE_FLOAT, brightnessFloat) + " "
                            + toStringLabel(mUpdatedTypes & TYPE_INT, brightnessInt));

                    mHandler.sendEmptyMessageAtTime(MSG_RUN_UPDATE,
                            mClock.uptimeMillis() + WAIT_FOR_RESPONSE_MILLIS);
                }
                mTimeUpdated = mClock.uptimeMillis();
            }

            if (mState == STATE_RUNNING) {
                // If we're not waiting on any more confirmations or the time has expired, move to
                // completed state.
                if (mConfirmedTypes == mUpdatedTypes
                        || (mTimeUpdated + WAIT_FOR_RESPONSE_MILLIS) < mClock.uptimeMillis()) {
                    mState = STATE_COMPLETED;
                }
            }
        }

        /**
         * Attempts to consume the specified brightness change if it is determined that the change
         * is a notification of a change previously made by this class.
         *
         * @param type The type of change (int|float)
         * @param brightness The brightness value.
         * @return True if the change was caused by this class, thus swallowed.
         */
        boolean swallowUpdate(int type, float brightness) {
            if ((mUpdatedTypes & type) != type || (mConfirmedTypes & type) != 0x0) {
                // It's either a type we didn't update, or one we've already confirmed.
                return false;
            }

            final boolean floatUpdateConfirmed =
                    type == TYPE_FLOAT && floatEquals(getBrightnessAsFloat(), brightness);
            final boolean intUpdateConfirmed =
                    type == TYPE_INT && getBrightnessAsInt() == (int) brightness;

            if (floatUpdateConfirmed || intUpdateConfirmed) {
                mConfirmedTypes |= type;
                Slog.i(TAG, "Swallowing update of " + toStringLabel(type, brightness)
                        + " by update: " + this);
                return true;
            }
            return false;
        }

        boolean isRunning() {
            return mState == STATE_RUNNING;
        }

        boolean isCompleted() {
            return mState == STATE_COMPLETED;
        }

        boolean madeUpdates() {
            return mUpdatedTypes != 0x0;
        }

        private int getBrightnessAsInt() {
            if (mSourceType == TYPE_INT) {
                return (int) mBrightness;
            }
            return brightnessFloatToInt(mBrightness);
        }

        private float getBrightnessAsFloat() {
            if (mSourceType == TYPE_FLOAT) {
                return mBrightness;
            }
            return brightnessIntToFloat((int) mBrightness);
        }

        private String toStringLabel(int type, float brightness) {
            return (type == TYPE_INT) ? ((int) brightness) + "(i)"
                    : ((type == TYPE_FLOAT) ? brightness + "(f)"
                    : "");
        }
    }

    /** Functional interface for providing time. */
    @VisibleForTesting
    public interface Clock {
        /** @return system uptime in milliseconds. */
        long uptimeMillis();
    }

    class BrightnessSynchronizerHandler extends Handler {
        BrightnessSynchronizerHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_RUN_UPDATE:
                    if (DEBUG) {
                        Slog.d(TAG, "MSG_RUN_UPDATE");
                    }
                    runUpdate();
                    break;
                default:
                    super.handleMessage(msg);
            }

        }
    };

    private class BrightnessSyncObserver {
        private boolean mIsObserving;

        private final DisplayListener mListener = new DisplayListener() {
            @Override
            public void onDisplayAdded(int displayId) {}

            @Override
            public void onDisplayRemoved(int displayId) {}

            @Override
            public void onDisplayChanged(int displayId) {
                handleBrightnessChangeFloat(getScreenBrightnessFloat());
            }
        };

        private ContentObserver createBrightnessContentObserver(Handler handler) {
            return new ContentObserver(handler) {
                @Override
                public void onChange(boolean selfChange, Uri uri) {
                    if (selfChange) {
                        return;
                    }
                    if (BRIGHTNESS_URI.equals(uri)) {
                        handleBrightnessChangeInt(getScreenBrightnessInt());
                    }
                }
            };
        }

        boolean isObserving() {
            return mIsObserving;
        }

        void startObserving(Handler handler) {
            final ContentResolver cr = mContext.getContentResolver();
            cr.registerContentObserver(BRIGHTNESS_URI, false,
                    createBrightnessContentObserver(handler), UserHandle.USER_ALL);
            mDisplayManager.registerDisplayListener(mListener, handler,
                    DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
            mIsObserving = true;
        }
    }
}
