/*
 * Copyright (C) 2014 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.wm;

import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DIM_LAYER;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SURFACE_TRACE;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_SURFACE_ALLOC;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.Slog;
import android.view.DisplayInfo;
import android.view.SurfaceControl;

import java.io.PrintWriter;

public class DimLayer {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "DimLayer" : TAG_WM;
    private final WindowManagerService mService;

    /** Actual surface that dims */
    private SurfaceControl mDimSurface;

    /** Last value passed to mDimSurface.setAlpha() */
    private float mAlpha = 0;

    /** Last value passed to mDimSurface.setLayer() */
    private int mLayer = -1;

    /** Next values to pass to mDimSurface.setPosition() and mDimSurface.setSize() */
    private final Rect mBounds = new Rect();

    /** Last values passed to mDimSurface.setPosition() and mDimSurface.setSize() */
    private final Rect mLastBounds = new Rect();

    /** True after mDimSurface.show() has been called, false after mDimSurface.hide(). */
    private boolean mShowing = false;

    /** Value of mAlpha when beginning transition to mTargetAlpha */
    private float mStartAlpha = 0;

    /** Final value of mAlpha following transition */
    private float mTargetAlpha = 0;

    /** Time in units of SystemClock.uptimeMillis() at which the current transition started */
    private long mStartTime;

    /** Time in milliseconds to take to transition from mStartAlpha to mTargetAlpha */
    private long mDuration;

    private boolean mDestroyed = false;

    private final int mDisplayId;


    /** Interface implemented by users of the dim layer */
    interface DimLayerUser {
        /** Returns true if the user of the dim layer is fullscreen. */
        boolean isFullscreen();
        /** Returns the display info. of the dim layer user. */
        DisplayInfo getDisplayInfo();
        /** Gets the bounds of the dim layer user. */
        void getDimBounds(Rect outBounds);
        String toShortString();
    }
    /** The user of this dim layer. */
    private final DimLayerUser mUser;

    private final String mName;

    DimLayer(WindowManagerService service, DimLayerUser user, int displayId, String name) {
        mUser = user;
        mDisplayId = displayId;
        mService = service;
        mName = name;
        if (DEBUG_DIM_LAYER) Slog.v(TAG, "Ctor: displayId=" + displayId);
    }

    private void constructSurface(WindowManagerService service) {
        SurfaceControl.openTransaction();
        try {
            if (DEBUG_SURFACE_TRACE) {
                mDimSurface = new WindowSurfaceController.SurfaceTrace(service.mFxSession,
                    "DimSurface",
                    16, 16, PixelFormat.OPAQUE,
                    SurfaceControl.FX_SURFACE_DIM | SurfaceControl.HIDDEN);
            } else {
                mDimSurface = new SurfaceControl(service.mFxSession, mName,
                    16, 16, PixelFormat.OPAQUE,
                    SurfaceControl.FX_SURFACE_DIM | SurfaceControl.HIDDEN);
            }
            if (SHOW_TRANSACTIONS || SHOW_SURFACE_ALLOC) Slog.i(TAG,
                    "  DIM " + mDimSurface + ": CREATE");
            mDimSurface.setLayerStack(mDisplayId);
            adjustBounds();
            adjustAlpha(mAlpha);
            adjustLayer(mLayer);
        } catch (Exception e) {
            Slog.e(TAG_WM, "Exception creating Dim surface", e);
        } finally {
            SurfaceControl.closeTransaction();
        }
    }

    /** Return true if dim layer is showing */
    boolean isDimming() {
        return mTargetAlpha != 0;
    }

    /** Return true if in a transition period */
    boolean isAnimating() {
        return mTargetAlpha != mAlpha;
    }

    float getTargetAlpha() {
        return mTargetAlpha;
    }

    void setLayer(int layer) {
        if (mLayer == layer) {
            return;
        }
        mLayer = layer;
        adjustLayer(layer);
    }

    private void adjustLayer(int layer) {
        if (mDimSurface != null) {
            mDimSurface.setLayer(layer);
        }
    }

    int getLayer() {
        return mLayer;
    }

    private void setAlpha(float alpha) {
        if (mAlpha == alpha) {
            return;
        }
        mAlpha = alpha;
        adjustAlpha(alpha);
    }

    private void adjustAlpha(float alpha) {
        if (DEBUG_DIM_LAYER) Slog.v(TAG, "setAlpha alpha=" + alpha);
        try {
            if (mDimSurface != null) {
                mDimSurface.setAlpha(alpha);
            }
            if (alpha == 0 && mShowing) {
                if (DEBUG_DIM_LAYER) Slog.v(TAG, "setAlpha hiding");
                if (mDimSurface != null) {
                    mDimSurface.hide();
                    mShowing = false;
                }
            } else if (alpha > 0 && !mShowing) {
                if (DEBUG_DIM_LAYER) Slog.v(TAG, "setAlpha showing");
                if (mDimSurface != null) {
                    mDimSurface.show();
                    mShowing = true;
                }
            }
        } catch (RuntimeException e) {
            Slog.w(TAG, "Failure setting alpha immediately", e);
        }
    }

    /**
     * NOTE: Must be called with Surface transaction open.
     */
    private void adjustBounds() {
        final int dw, dh;
        final float xPos, yPos;
        if (!mUser.isFullscreen()) {
            dw = mBounds.width();
            dh = mBounds.height();
            xPos = mBounds.left;
            yPos = mBounds.top;
        } else {
            // Set surface size to screen size.
            final DisplayInfo info = mUser.getDisplayInfo();
            // Multiply by 1.5 so that rotating a frozen surface that includes this does not expose
            // a corner.
            dw = (int) (info.logicalWidth * 1.5);
            dh = (int) (info.logicalHeight * 1.5);
            // back off position so 1/4 of Surface is before and 1/4 is after.
            xPos = -1 * dw / 6;
            yPos = -1 * dh / 6;
        }

        if (mDimSurface != null) {
            mDimSurface.setPosition(xPos, yPos);
            mDimSurface.setSize(dw, dh);
        }

        mLastBounds.set(mBounds);
    }

    /** @param bounds The new bounds to set */
    void setBounds(Rect bounds) {
        mBounds.set(bounds);
        if (isDimming() && !mLastBounds.equals(bounds)) {
            try {
                SurfaceControl.openTransaction();
                adjustBounds();
            } catch (RuntimeException e) {
                Slog.w(TAG, "Failure setting size", e);
            } finally {
                SurfaceControl.closeTransaction();
            }
        }
    }

    /**
     * @param duration The time to test.
     * @return True if the duration would lead to an earlier end to the current animation.
     */
    private boolean durationEndsEarlier(long duration) {
        return SystemClock.uptimeMillis() + duration < mStartTime + mDuration;
    }

    /** Jump to the end of the animation.
     * NOTE: Must be called with Surface transaction open. */
    void show() {
        if (isAnimating()) {
            if (DEBUG_DIM_LAYER) Slog.v(TAG, "show: immediate");
            show(mLayer, mTargetAlpha, 0);
        }
    }

    /**
     * Begin an animation to a new dim value.
     * NOTE: Must be called with Surface transaction open.
     *
     * @param layer The layer to set the surface to.
     * @param alpha The dim value to end at.
     * @param duration How long to take to get there in milliseconds.
     */
    void show(int layer, float alpha, long duration) {
        if (DEBUG_DIM_LAYER) Slog.v(TAG, "show: layer=" + layer + " alpha=" + alpha
                + " duration=" + duration + ", mDestroyed=" + mDestroyed);
        if (mDestroyed) {
            Slog.e(TAG, "show: no Surface");
            // Make sure isAnimating() returns false.
            mTargetAlpha = mAlpha = 0;
            return;
        }

        if (mDimSurface == null) {
            constructSurface(mService);
        }

        if (!mLastBounds.equals(mBounds)) {
            adjustBounds();
        }
        setLayer(layer);

        long curTime = SystemClock.uptimeMillis();
        final boolean animating = isAnimating();
        if ((animating && (mTargetAlpha != alpha || durationEndsEarlier(duration)))
                || (!animating && mAlpha != alpha)) {
            if (duration <= 0) {
                // No animation required, just set values.
                setAlpha(alpha);
            } else {
                // Start or continue animation with new parameters.
                mStartAlpha = mAlpha;
                mStartTime = curTime;
                mDuration = duration;
            }
        }
        mTargetAlpha = alpha;
        if (DEBUG_DIM_LAYER) Slog.v(TAG, "show: mStartAlpha=" + mStartAlpha + " mStartTime="
                + mStartTime + " mTargetAlpha=" + mTargetAlpha);
    }

    /** Immediate hide.
     * NOTE: Must be called with Surface transaction open. */
    void hide() {
        if (mShowing) {
            if (DEBUG_DIM_LAYER) Slog.v(TAG, "hide: immediate");
            hide(0);
        }
    }

    /**
     * Gradually fade to transparent.
     * NOTE: Must be called with Surface transaction open.
     *
     * @param duration Time to fade in milliseconds.
     */
    void hide(long duration) {
        if (mShowing && (mTargetAlpha != 0 || durationEndsEarlier(duration))) {
            if (DEBUG_DIM_LAYER) Slog.v(TAG, "hide: duration=" + duration);
            show(mLayer, 0, duration);
        }
    }

    /**
     * Advance the dimming per the last #show(int, float, long) call.
     * NOTE: Must be called with Surface transaction open.
     *
     * @return True if animation is still required after this step.
     */
    boolean stepAnimation() {
        if (mDestroyed) {
            Slog.e(TAG, "stepAnimation: surface destroyed");
            // Ensure that isAnimating() returns false;
            mTargetAlpha = mAlpha = 0;
            return false;
        }
        if (isAnimating()) {
            final long curTime = SystemClock.uptimeMillis();
            final float alphaDelta = mTargetAlpha - mStartAlpha;
            float alpha = mStartAlpha + alphaDelta * (curTime - mStartTime) / mDuration;
            if (alphaDelta > 0 && alpha > mTargetAlpha ||
                    alphaDelta < 0 && alpha < mTargetAlpha) {
                // Don't exceed limits.
                alpha = mTargetAlpha;
            }
            if (DEBUG_DIM_LAYER) Slog.v(TAG, "stepAnimation: curTime=" + curTime + " alpha=" + alpha);
            setAlpha(alpha);
        }

        return isAnimating();
    }

    /** Cleanup */
    void destroySurface() {
        if (DEBUG_DIM_LAYER) Slog.v(TAG, "destroySurface.");
        if (mDimSurface != null) {
            mDimSurface.destroy();
            mDimSurface = null;
        }
        mDestroyed = true;
    }

    public void printTo(String prefix, PrintWriter pw) {
        pw.print(prefix); pw.print("mDimSurface="); pw.print(mDimSurface);
                pw.print(" mLayer="); pw.print(mLayer);
                pw.print(" mAlpha="); pw.println(mAlpha);
        pw.print(prefix); pw.print("mLastBounds="); pw.print(mLastBounds.toShortString());
                pw.print(" mBounds="); pw.println(mBounds.toShortString());
        pw.print(prefix); pw.print("Last animation: ");
                pw.print(" mDuration="); pw.print(mDuration);
                pw.print(" mStartTime="); pw.print(mStartTime);
                pw.print(" curTime="); pw.println(SystemClock.uptimeMillis());
        pw.print(prefix); pw.print(" mStartAlpha="); pw.print(mStartAlpha);
                pw.print(" mTargetAlpha="); pw.println(mTargetAlpha);
    }
}
