/*
 * 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.systemui.statusbar.phone;

import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInScale;
import static com.android.systemui.statusbar.notification.NotificationUtils.interpolate;

import android.content.res.Resources;
import android.util.MathUtils;

import com.android.app.animation.Interpolators;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.keyguard.KeyguardStatusView;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.core.Logger;
import com.android.systemui.log.dagger.KeyguardClockLog;
import com.android.systemui.res.R;
import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcherListView;

import javax.inject.Inject;

/**
 * Utility class to calculate the clock position and top padding of notifications on Keyguard.
 */
public class KeyguardClockPositionAlgorithm {
    private static final String TAG = "KeyguardClockPositionAlgorithm";

    /**
     * Margin between the bottom of the status view and the notification shade.
     */
    private int mStatusViewBottomMargin;

    /**
     * Height of {@link KeyguardStatusView}.
     */
    private int mKeyguardStatusHeight;

    /**
     * Height of user avatar used by the multi-user switcher. This could either be the
     * {@link KeyguardUserSwitcherListView} when it is closed and only the current user's icon is
     * visible, or it could be height of the avatar used by the
     * {@link com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController}.
     */
    private int mUserSwitchHeight;

    /**
     * Preferred Y position of user avatar used by the multi-user switcher.
     */
    private int mUserSwitchPreferredY;

    /**
     * Minimum top margin to avoid overlap with status bar or multi-user switcher avatar.
     */
    private int mMinTopMargin;

    /**
     * Minimum top inset (in pixels) to avoid overlap with any display cutouts.
     */
    private int mCutoutTopInset = 0;

    /**
     * Recommended distance from the status bar.
     */
    private int mContainerTopPadding;

    /**
     * Top margin of notifications introduced by presence of split shade header / status bar
     */
    private int mSplitShadeTopNotificationsMargin;

    /**
     * Target margin for notifications and clock from the top of the screen in split shade
     */
    private int mSplitShadeTargetTopMargin;

    /**
     * @see ShadeViewController#getExpandedFraction()
     */
    private float mPanelExpansion;

    /**
     * Max burn-in prevention x translation.
     */
    private int mMaxBurnInPreventionOffsetX;

    /**
     * Max burn-in prevention y translation for clock layouts.
     */
    private int mMaxBurnInPreventionOffsetYClock;

    /**
     * Current burn-in prevention y translation.
     */
    private float mCurrentBurnInOffsetY;

    /**
     * Doze/AOD transition amount.
     */
    private float mDarkAmount;

    /**
     * How visible the quick settings panel is.
     */
    private float mQsExpansion;

    private float mOverStretchAmount;

    /**
     * Setting if bypass is enabled. If true the clock should always be positioned like it's dark
     * and other minor adjustments.
     */
    private boolean mBypassEnabled;

    /**
     * The stackscroller padding when unlocked
     */
    private int mUnlockedStackScrollerPadding;

    private boolean mIsSplitShade;

    /**
     * Top location of the udfps icon. This includes the worst case (highest) burn-in
     * offset that would make the top physically highest on the screen.
     *
     * Set to -1 if udfps is not enrolled on the device.
     */
    private float mUdfpsTop;

    /**
     * Bottom y-position of the currently visible clock
     */
    private float mClockBottom;

    /**
     * If true, try to keep clock aligned to the top of the display. Else, assume the clock
     * is center aligned.
     */
    private boolean mIsClockTopAligned;

    private Logger mLogger;

    @Inject
    public KeyguardClockPositionAlgorithm(@KeyguardClockLog LogBuffer logBuffer) {
        mLogger = new Logger(logBuffer, TAG);
    }

    /**
     * Refreshes the dimension values.
     */
    public void loadDimens(Resources res) {
        mStatusViewBottomMargin = res.getDimensionPixelSize(
                R.dimen.keyguard_status_view_bottom_margin);
        mSplitShadeTopNotificationsMargin =
                res.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
        mSplitShadeTargetTopMargin =
                res.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin);

        mContainerTopPadding =
                res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
        mMaxBurnInPreventionOffsetX = res.getDimensionPixelSize(
                R.dimen.burn_in_prevention_offset_x);
        mMaxBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
                R.dimen.burn_in_prevention_offset_y_clock);
    }

    /**
     * Sets up algorithm values.
     */
    public void setup(int keyguardStatusBarHeaderHeight, float panelExpansion,
            int keyguardStatusHeight, int userSwitchHeight, int userSwitchPreferredY,
            float dark, float overStretchAmount, boolean bypassEnabled,
            int unlockedStackScrollerPadding, float qsExpansion, int cutoutTopInset,
            boolean isSplitShade, float udfpsTop, float clockBottom, boolean isClockTopAligned) {
        mMinTopMargin = keyguardStatusBarHeaderHeight + Math.max(mContainerTopPadding,
                userSwitchHeight);
        mPanelExpansion = BouncerPanelExpansionCalculator
                .getKeyguardClockScaledExpansion(panelExpansion);
        mKeyguardStatusHeight = keyguardStatusHeight + mStatusViewBottomMargin;
        mUserSwitchHeight = userSwitchHeight;
        mUserSwitchPreferredY = userSwitchPreferredY;
        mDarkAmount = dark;
        mOverStretchAmount = overStretchAmount;
        mBypassEnabled = bypassEnabled;
        mUnlockedStackScrollerPadding = unlockedStackScrollerPadding;
        mQsExpansion = qsExpansion;
        mCutoutTopInset = cutoutTopInset;
        mIsSplitShade = isSplitShade;
        mUdfpsTop = udfpsTop;
        mClockBottom = clockBottom;
        mIsClockTopAligned = isClockTopAligned;
    }

    public void run(Result result) {
        final int y = getClockY(mPanelExpansion, mDarkAmount);
        result.clockY = y;
        result.userSwitchY = getUserSwitcherY(mPanelExpansion);
        result.clockYFullyDozing = getClockY(
                1.0f /* panelExpansion */, 1.0f /* darkAmount */);
        result.clockAlpha = getClockAlpha(y);
        result.stackScrollerPadding = getStackScrollerPadding(y);
        result.stackScrollerPaddingExpanded = getStackScrollerPaddingExpanded();
        result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
        result.clockScale = interpolate(getBurnInScale(), 1.0f, 1.0f - mDarkAmount);
    }

    private int getStackScrollerPaddingExpanded() {
        if (mBypassEnabled) {
            return mUnlockedStackScrollerPadding;
        } else if (mIsSplitShade) {
            return getClockY(1.0f, mDarkAmount) + mUserSwitchHeight;
        } else {
            return getClockY(1.0f, mDarkAmount) + mKeyguardStatusHeight;
        }
    }

    private int getStackScrollerPadding(int clockYPosition) {
        if (mBypassEnabled) {
            return (int) (mUnlockedStackScrollerPadding + mOverStretchAmount);
        } else if (mIsSplitShade) {
            // mCurrentBurnInOffsetY is subtracted to make notifications not follow clock adjustment
            // for burn-in. It can make pulsing notification go too high and it will get clipped
            return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight
                    - (int) mCurrentBurnInOffsetY;
        } else {
            return clockYPosition + mKeyguardStatusHeight;
        }
    }

    /**
     * @param nsslTop NotificationStackScrollLayout top, which is below top of the srceen.
     * @return Distance from nsslTop to top of the first view in the lockscreen shade.
     */
    public float getLockscreenNotifPadding(float nsslTop) {
        if (mBypassEnabled) {
            return mUnlockedStackScrollerPadding - nsslTop;
        } else if (mIsSplitShade) {
            return mSplitShadeTargetTopMargin + mUserSwitchHeight - nsslTop;
        } else {
            // Non-bypass portrait shade already uses values from nsslTop
            // so we don't need to subtract it here.
            return mMinTopMargin + mKeyguardStatusHeight;
        }
    }

    /**
     * give the static topMargin, used for lockscreen clocks to get the initial translationY
     * to do counter translation
     */
    public int getExpandedPreferredClockY() {
        if (mIsSplitShade) {
            return mSplitShadeTargetTopMargin;
        } else {
            return mMinTopMargin;
        }
    }

    public int getLockscreenStatusViewHeight() {
        return mKeyguardStatusHeight;
    }

    private int getClockY(float panelExpansion, float darkAmount) {
        float clockYRegular = getExpandedPreferredClockY();

        // Dividing the height creates a smoother transition when the user swipes up to unlock
        float clockYBouncer = -mKeyguardStatusHeight / 3.0f;

        // Move clock up while collapsing the shade
        float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(panelExpansion);
        float clockY = MathUtils.lerp(clockYBouncer, clockYRegular, shadeExpansion);

        // This will keep the clock at the top but out of the cutout area
        float shift = 0;
        if (clockY - mMaxBurnInPreventionOffsetYClock < mCutoutTopInset) {
            shift = mCutoutTopInset - (clockY - mMaxBurnInPreventionOffsetYClock);
        }

        int burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; // requested offset
        final boolean hasUdfps = mUdfpsTop > -1;
        if (hasUdfps && !mIsClockTopAligned) {
            // ensure clock doesn't overlap with the udfps icon
            if (mUdfpsTop < mClockBottom) {
                // sometimes the clock textView extends beyond udfps, so let's just use the
                // space above the KeyguardStatusView/clock as our burn-in offset
                burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2;
                if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
                    burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
                }
                shift = -burnInPreventionOffsetY;
            } else {
                float upperSpace = clockY - mCutoutTopInset;
                float lowerSpace = mUdfpsTop - mClockBottom;
                // center the burn-in offset within the upper + lower space
                burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2;
                if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
                    burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
                }
                shift = (lowerSpace - upperSpace) / 2;
            }
        }

        float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
        float clockYDark = clockY
                + fullyDarkBurnInOffset
                + shift;
        mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
        final String inputs = "panelExpansion: " + panelExpansion + " darkAmount: " + darkAmount;
        final String outputs = "clockY: " + clockY
                + " burnInPreventionOffsetY: " + burnInPreventionOffsetY
                + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset
                + " shift: " + shift
                + " mOverStretchAmount: " + mOverStretchAmount
                + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY;
        mLogger.i(msg -> {
            return msg.getStr1() + " -> " + msg.getStr2();
        }, msg -> {
            msg.setStr1(inputs);
            msg.setStr2(outputs);
            return kotlin.Unit.INSTANCE;
        });
        return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
    }

    private int getUserSwitcherY(float panelExpansion) {
        float userSwitchYRegular = mUserSwitchPreferredY;
        float userSwitchYBouncer = -mKeyguardStatusHeight - mUserSwitchHeight;

        // Move user-switch up while collapsing the shade
        float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(panelExpansion);
        float userSwitchY = MathUtils.lerp(userSwitchYBouncer, userSwitchYRegular, shadeExpansion);

        return (int) (userSwitchY + mOverStretchAmount);
    }

    /**
     * We might want to fade out the clock when the user is swiping up.
     * One exception is when the bouncer will become visible, in this cause the clock
     * should always persist.
     *
     * @param y Current clock Y.
     * @return Alpha from 0 to 1.
     */
    private float getClockAlpha(int y) {
        float alphaKeyguard = Math.max(0, y / Math.max(1f, getClockY(1f, mDarkAmount)));
        if (!mIsSplitShade) {
            // in split shade QS are always expanded so this factor shouldn't apply
            float qsAlphaFactor = MathUtils.saturate(mQsExpansion / 0.3f);
            qsAlphaFactor = 1f - qsAlphaFactor;
            alphaKeyguard *= qsAlphaFactor;
        }
        alphaKeyguard = Interpolators.ACCELERATE.getInterpolation(alphaKeyguard);
        return MathUtils.lerp(alphaKeyguard, 1f, mDarkAmount);
    }

    private float burnInPreventionOffsetY(int offset) {
        return getBurnInOffset(offset * 2, false /* xAxis */) - offset;
    }

    private float burnInPreventionOffsetX() {
        return getBurnInOffset(mMaxBurnInPreventionOffsetX, true /* xAxis */);
    }

    public static class Result {

        /**
         * The x translation of the clock.
         */
        public int clockX;

        /**
         * The y translation of the clock.
         */
        public int clockY;

        /**
         * The y translation of the multi-user switch.
         */
        public int userSwitchY;

        /**
         * The y translation of the clock when we're fully dozing.
         */
        public int clockYFullyDozing;

        /**
         * The alpha value of the clock.
         */
        public float clockAlpha;

        /**
         * Amount to scale the large clock (0.0 - 1.0)
         */
        public float clockScale;

        /**
         * The top padding of the stack scroller, in pixels.
         */
        public int stackScrollerPadding;

        /**
         * The top padding of the stack scroller, in pixels when fully expanded.
         */
        public int stackScrollerPaddingExpanded;
    }
}
