/*
 * 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.keyguard.KeyguardStatusView;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcherListView;

/**
 * Utility class to calculate the clock position and top padding of notifications on Keyguard.
 */
public class KeyguardClockPositionAlgorithm {
    /**
     * How much the clock height influences the shade position.
     * 0 means nothing, 1 means move the shade up by the height of the clock
     * 0.5f means move the shade up by half of the size of the clock.
     */
    private static float CLOCK_HEIGHT_WEIGHT = 0.7f;

    /**
     * Margin between the bottom of the clock and the notification shade.
     */
    private int mClockNotificationsMargin;

    /**
     * Height of the parent view - display size in px.
     */
    private int mHeight;

    /**
     * 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 clock.
     */
    private int mClockPreferredY;

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

    /**
     * Whether or not there is a custom clock face on keyguard.
     */
    private boolean mHasCustomClock;

    /**
     * Whether or not the NSSL contains any visible notifications.
     */
    private boolean mHasVisibleNotifs;

    /**
     * Height of notification stack: Sum of height of each notification.
     */
    private int mNotificationStackHeight;

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

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

    /**
     * Maximum bottom padding to avoid overlap with {@link KeyguardBottomAreaView} or
     * the ambient indication.
     */
    private int mMaxShadeBottom;

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

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

    /**
     * Burn-in prevention x translation.
     */
    private int mBurnInPreventionOffsetX;

    /**
     * Burn-in prevention y translation.
     */
    private int mBurnInPreventionOffsetY;

    /**
     * Burn-in prevention y translation for large clock layouts.
     */
    private int mBurnInPreventionOffsetYLargeClock;

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

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

    private float mEmptyDragAmount;

    /**
     * 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 int mLockScreenMode;

    /**
     * Refreshes the dimension values.
     */
    public void loadDimens(Resources res) {
        mClockNotificationsMargin = res.getDimensionPixelSize(
                R.dimen.keyguard_clock_notifications_margin);

        mContainerTopPadding =
                res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) / 2;
        mBurnInPreventionOffsetX = res.getDimensionPixelSize(
                R.dimen.burn_in_prevention_offset_x);
        mBurnInPreventionOffsetY = res.getDimensionPixelSize(
                R.dimen.burn_in_prevention_offset_y);
        mBurnInPreventionOffsetYLargeClock = res.getDimensionPixelSize(
                R.dimen.burn_in_prevention_offset_y_large_clock);
    }

    /**
     * Sets up algorithm values.
     */
    public void setup(int keyguardStatusBarHeaderHeight, int maxShadeBottom,
            int notificationStackHeight, float panelExpansion, int parentHeight,
            int keyguardStatusHeight, int userSwitchHeight, int clockPreferredY,
            int userSwitchPreferredY, boolean hasCustomClock, boolean hasVisibleNotifs, float dark,
            float emptyDragAmount, boolean bypassEnabled, int unlockedStackScrollerPadding,
            float qsExpansion, int cutoutTopInset) {
        mMinTopMargin = keyguardStatusBarHeaderHeight + Math.max(mContainerTopPadding,
                userSwitchHeight);
        mMaxShadeBottom = maxShadeBottom;
        mNotificationStackHeight = notificationStackHeight;
        mPanelExpansion = panelExpansion;
        mHeight = parentHeight;
        mKeyguardStatusHeight = keyguardStatusHeight;
        mUserSwitchHeight = userSwitchHeight;
        mClockPreferredY = clockPreferredY;
        mUserSwitchPreferredY = userSwitchPreferredY;
        mHasCustomClock = hasCustomClock;
        mHasVisibleNotifs = hasVisibleNotifs;
        mDarkAmount = dark;
        mEmptyDragAmount = emptyDragAmount;
        mBypassEnabled = bypassEnabled;
        mUnlockedStackScrollerPadding = unlockedStackScrollerPadding;
        mQsExpansion = qsExpansion;
        mCutoutTopInset = cutoutTopInset;
    }

    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 = mBypassEnabled ? mUnlockedStackScrollerPadding
                : y + mKeyguardStatusHeight;
        result.stackScrollerPaddingExpanded = mBypassEnabled ? mUnlockedStackScrollerPadding
                : getClockY(1.0f, mDarkAmount) + mKeyguardStatusHeight;
        result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
        result.clockScale = interpolate(getBurnInScale(), 1.0f, 1.0f - mDarkAmount);
    }

    /**
     * Update lock screen mode for testing different layouts
     */
    public void onLockScreenModeChanged(int mode) {
        mLockScreenMode = mode;
    }

    public float getMinStackScrollerPadding() {
        return mBypassEnabled ? mUnlockedStackScrollerPadding
                : mMinTopMargin + mKeyguardStatusHeight + mClockNotificationsMargin;
    }

    private int getMaxClockY() {
        return mHeight / 2 - mKeyguardStatusHeight - mClockNotificationsMargin;
    }

    private int getPreferredClockY() {
        return mClockPreferredY;
    }

    private int getExpandedPreferredClockY() {
        if (mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) {
            return mMinTopMargin + mUserSwitchHeight;
        }
        return (mHasCustomClock && (!mHasVisibleNotifs || mBypassEnabled)) ? getPreferredClockY()
                : getExpandedClockPosition();
    }

    /**
     * Vertically align the clock and the shade in the available space considering only
     * a percentage of the clock height defined by {@code CLOCK_HEIGHT_WEIGHT}.
     * @return Clock Y in pixels.
     */
    public int getExpandedClockPosition() {
        final int availableHeight = mMaxShadeBottom - mMinTopMargin;
        final int containerCenter = mMinTopMargin + availableHeight / 2;

        float y = containerCenter
                - (mKeyguardStatusHeight + mUserSwitchHeight) * CLOCK_HEIGHT_WEIGHT
                - mClockNotificationsMargin - mNotificationStackHeight / 2;
        if (y < mMinTopMargin) {
            y = mMinTopMargin;
        }

        // Don't allow the clock base to be under half of the screen
        final float maxClockY = getMaxClockY();
        if (y > maxClockY) {
            y = maxClockY;
        }

        return (int) y;
    }

    private int getClockY(float panelExpansion, float darkAmount) {
        // Dark: Align the bottom edge of the clock at about half of the screen:
        float clockYDark = (mHasCustomClock ? getPreferredClockY() : getMaxClockY())
                + burnInPreventionOffsetY();
        clockYDark = MathUtils.max(0, clockYDark);

        float clockYRegular = getExpandedPreferredClockY();
        float clockYBouncer = -mKeyguardStatusHeight;

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

        darkAmount = mBypassEnabled && !mHasCustomClock ? 1.0f : darkAmount;

        if (mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) {
            // This will keep the clock at the top but out of the cutout area
            float shift = 0;
            if (clockY - mBurnInPreventionOffsetYLargeClock < mCutoutTopInset) {
                shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYLargeClock);
            }
            clockYDark = clockY + burnInPreventionOffsetY() + shift;
        }
        return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mEmptyDragAmount);
    }

    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 + mEmptyDragAmount);
    }

    /**
     * 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)));
        alphaKeyguard *= (1f - mQsExpansion);
        alphaKeyguard = Interpolators.ACCELERATE.getInterpolation(alphaKeyguard);
        return MathUtils.lerp(alphaKeyguard, 1f, mDarkAmount);
    }

    private float burnInPreventionOffsetY() {
        int offset = mBurnInPreventionOffsetY;
        if (mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) {
            offset = mBurnInPreventionOffsetYLargeClock;
        }

        return getBurnInOffset(offset * 2, false /* xAxis */) - offset;
    }

    private float burnInPreventionOffsetX() {
        if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) {
            return getBurnInOffset(mBurnInPreventionOffsetX * 2, true /* xAxis */)
                    - mBurnInPreventionOffsetX;
        }
        return getBurnInOffset(mBurnInPreventionOffsetX, 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;
    }
}
