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

import android.annotation.IdRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.biometrics.SensorLocationInternal;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Build;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.widget.FrameLayout;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.res.R;

/**
 * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical
 * under-display fingerprint sensor (UDFPS).
 */
public class UdfpsDialogMeasureAdapter {
    private static final String TAG = "UdfpsDialogMeasurementAdapter";
    private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG;

    @NonNull private final ViewGroup mView;
    @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
    @Nullable private WindowManager mWindowManager;
    private int mBottomSpacerHeight;

    public UdfpsDialogMeasureAdapter(
            @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) {
        mView = view;
        mSensorProps = sensorProps;
        mWindowManager = mView.getContext().getSystemService(WindowManager.class);
    }

    @NonNull
    FingerprintSensorPropertiesInternal getSensorProps() {
        return mSensorProps;
    }

    @NonNull
    public AuthDialog.LayoutParams onMeasureInternal(
            int width, int height, @NonNull AuthDialog.LayoutParams layoutParams,
            float scaleFactor) {

        final int displayRotation = mView.getDisplay().getRotation();
        switch (displayRotation) {
            case Surface.ROTATION_0:
                return onMeasureInternalPortrait(width, height, scaleFactor);
            case Surface.ROTATION_90:
            case Surface.ROTATION_270:
                return onMeasureInternalLandscape(width, height, scaleFactor);
            default:
                Log.e(TAG, "Unsupported display rotation: " + displayRotation);
                return layoutParams;
        }
    }

    /**
     * @return the actual (and possibly negative) bottom spacer height. If negative, this indicates
     * that the UDFPS sensor is too low. Our current xml and custom measurement logic is very hard
     * too cleanly support this case. So, let's have the onLayout code translate the sensor location
     * instead.
     */
    public int getBottomSpacerHeight() {
        return mBottomSpacerHeight;
    }

    /**
     * @return sensor diameter size as scaleFactor
     */
    public int getSensorDiameter(float scaleFactor) {
        return (int) (scaleFactor * mSensorProps.getLocation().sensorRadius * 2);
    }

    @NonNull
    private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height,
            float scaleFactor) {
        final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();

        // Figure out where the bottom of the sensor anim should be.
        final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
        final int buttonBarHeight = getViewHeightPx(R.id.button_bar);
        final int dialogMargin = getDialogMarginPx();
        final int displayHeight = getMaximumWindowBounds(windowMetrics).height();
        final Insets navbarInsets = getNavbarInsets(windowMetrics);
        mBottomSpacerHeight = calculateBottomSpacerHeightForPortrait(
                mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight,
                dialogMargin, navbarInsets.bottom, scaleFactor);

        // Go through each of the children and do the custom measurement.
        int totalHeight = 0;
        final int numChildren = mView.getChildCount();
        final int sensorDiameter = getSensorDiameter(scaleFactor);
        for (int i = 0; i < numChildren; i++) {
            final View child = mView.getChildAt(i);
            if (child.getId() == R.id.biometric_icon_frame) {
                final FrameLayout iconFrame = (FrameLayout) child;
                final View icon = iconFrame.getChildAt(0);
                // Create a frame that's exactly the height of the sensor circle.
                iconFrame.measure(
                        MeasureSpec.makeMeasureSpec(
                                child.getLayoutParams().width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));

                // Ensure that the icon is never larger than the sensor.
                icon.measure(
                        MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
                        MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
            } else if (child.getId() == R.id.space_above_icon) {
                child.measure(
                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(
                                child.getLayoutParams().height, MeasureSpec.EXACTLY));
            } else if (child.getId() == R.id.button_bar) {
                child.measure(
                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
                                MeasureSpec.EXACTLY));
            } else if (child.getId() == R.id.space_below_icon) {
                // Set the spacer height so the fingerprint icon is on the physical sensor area
                final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
                child.measure(
                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
            } else if (child.getId() == R.id.description) {
                //skip description view and compute later
                continue;
            } else {
                child.measure(
                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
            }

            if (child.getVisibility() != View.GONE) {
                totalHeight += child.getMeasuredHeight();
            }
        }

        //re-calculate the height of description
        View description = mView.findViewById(R.id.description);
        if (description != null && description.getVisibility() != View.GONE) {
            totalHeight += measureDescription(description, displayHeight, width, totalHeight);
        }

        return new AuthDialog.LayoutParams(width, totalHeight);
    }

    private int measureDescription(View description, int displayHeight, int currWidth,
                                   int currHeight) {
        //description view should be measured in AuthBiometricFingerprintView#onMeasureInternal
        //so we could getMeasuredHeight in onMeasureInternalPortrait directly.
        int newHeight = description.getMeasuredHeight() + currHeight;
        int limit = (int) (displayHeight * 0.75);
        if (newHeight > limit) {
            description.measure(
                    MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
        }
        return description.getMeasuredHeight();
    }

    @NonNull
    private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height,
            float scaleFactor) {
        final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();

        // Find the spacer height needed to vertically align the icon with the sensor.
        final int titleHeight = getViewHeightPx(R.id.title);
        final int subtitleHeight = getViewHeightPx(R.id.subtitle);
        final int descriptionHeight = getViewHeightPx(R.id.description);
        final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon);
        final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
        final int buttonBarHeight = getViewHeightPx(R.id.button_bar);

        final Insets navbarInsets = getNavbarInsets(windowMetrics);
        final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight,
                subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight,
                buttonBarHeight, navbarInsets.bottom);

        // Find the spacer width needed to horizontally align the icon with the sensor.
        final int displayWidth = getMaximumWindowBounds(windowMetrics).width();
        final int dialogMargin = getDialogMarginPx();
        final int horizontalInset = navbarInsets.left + navbarInsets.right;
        final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape(
                mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor);

        final int sensorDiameter = getSensorDiameter(scaleFactor);
        final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth;

        int remeasuredHeight = 0;
        final int numChildren = mView.getChildCount();
        for (int i = 0; i < numChildren; i++) {
            final View child = mView.getChildAt(i);
            if (child.getId() == R.id.biometric_icon_frame) {
                final FrameLayout iconFrame = (FrameLayout) child;
                final View icon = iconFrame.getChildAt(0);
                // Create a frame that's exactly the height of the sensor circle.
                iconFrame.measure(
                        MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));

                // Ensure that the icon is never larger than the sensor.
                icon.measure(
                        MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
                        MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
            } else if (child.getId() == R.id.space_above_icon) {
                // Adjust the width and height of the top spacer if necessary.
                final int newTopSpacerHeight = child.getLayoutParams().height
                        - Math.min(bottomSpacerHeight, 0);
                child.measure(
                        MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY));
            } else if (child.getId() == R.id.button_bar) {
                // Adjust the width of the button bar while preserving its height.
                child.measure(
                        MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(
                                child.getLayoutParams().height, MeasureSpec.EXACTLY));
            } else if (child.getId() == R.id.space_below_icon) {
                // Adjust the bottom spacer height to align the fingerprint icon with the sensor.
                final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0);
                child.measure(
                        MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY));
            } else {
                // Use the remeasured width for all other child views.
                child.measure(
                        MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
            }

            if (child.getVisibility() != View.GONE) {
                remeasuredHeight += child.getMeasuredHeight();
            }
        }

        return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight);
    }

    private int getViewHeightPx(@IdRes int viewId) {
        final View view = mView.findViewById(viewId);
        return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0;
    }

    private int getDialogMarginPx() {
        return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding);
    }

    @NonNull
    private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) {
        return windowMetrics != null
                ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars())
                : Insets.NONE;
    }

    @NonNull
    private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) {
        return windowMetrics != null ? windowMetrics.getBounds() : new Rect();
    }

    /**
     * For devices in portrait orientation where the sensor is too high up, calculates the amount of
     * padding necessary to center the biometric icon within the sensor's physical location.
     */
    @VisibleForTesting
    static int calculateBottomSpacerHeightForPortrait(
            @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx,
            int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx,
            int navbarBottomInsetPx, float scaleFactor) {
        final SensorLocationInternal location = sensorProperties.getLocation();
        final int sensorDistanceFromBottom = displayHeightPx
                - (int) (scaleFactor * location.sensorLocationY)
                - (int) (scaleFactor * location.sensorRadius);

        final int spacerHeight = sensorDistanceFromBottom
                - textIndicatorHeightPx
                - buttonBarHeightPx
                - dialogMarginPx
                - navbarBottomInsetPx;

        if (DEBUG) {
            Log.d(TAG, "Display height: " + displayHeightPx
                    + ", Distance from bottom: " + sensorDistanceFromBottom
                    + ", Bottom margin: " + dialogMarginPx
                    + ", Navbar bottom inset: " + navbarBottomInsetPx
                    + ", Bottom spacer height (portrait): " + spacerHeight
                    + ", Scale Factor: " + scaleFactor);
        }

        return spacerHeight;
    }

    /**
     * For devices in landscape orientation where the sensor is too high up, calculates the amount
     * of padding necessary to center the biometric icon within the sensor's physical location.
     */
    @VisibleForTesting
    static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx,
            int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx,
            int buttonBarHeightPx, int navbarBottomInsetPx) {

        final int dialogHeightAboveIcon = titleHeightPx
                + subtitleHeightPx
                + descriptionHeightPx
                + topSpacerHeightPx;

        final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx;

        final int bottomSpacerHeight = dialogHeightAboveIcon
                - dialogHeightBelowIcon
                - navbarBottomInsetPx;

        if (DEBUG) {
            Log.d(TAG, "Title height: " + titleHeightPx
                    + ", Subtitle height: " + subtitleHeightPx
                    + ", Description height: " + descriptionHeightPx
                    + ", Top spacer height: " + topSpacerHeightPx
                    + ", Text indicator height: " + textIndicatorHeightPx
                    + ", Button bar height: " + buttonBarHeightPx
                    + ", Navbar bottom inset: " + navbarBottomInsetPx
                    + ", Bottom spacer height (landscape): " + bottomSpacerHeight);
        }

        return bottomSpacerHeight;
    }

    /**
     * For devices in landscape orientation where the sensor is too left/right, calculates the
     * amount of padding necessary to center the biometric icon within the sensor's physical
     * location.
     */
    @VisibleForTesting
    static int calculateHorizontalSpacerWidthForLandscape(
            @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx,
            int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) {
        final SensorLocationInternal location = sensorProperties.getLocation();
        final int sensorDistanceFromEdge = displayWidthPx
                - (int) (scaleFactor * location.sensorLocationY)
                - (int) (scaleFactor * location.sensorRadius);

        final int horizontalPadding = sensorDistanceFromEdge
                - dialogMarginPx
                - navbarHorizontalInsetPx;

        if (DEBUG) {
            Log.d(TAG, "Display width: " + displayWidthPx
                    + ", Distance from edge: " + sensorDistanceFromEdge
                    + ", Dialog margin: " + dialogMarginPx
                    + ", Navbar horizontal inset: " + navbarHorizontalInsetPx
                    + ", Horizontal spacer width (landscape): " + horizontalPadding
                    + ", Scale Factor: " + scaleFactor);
        }

        return horizontalPadding;
    }
}
