/*
 * Copyright (C) 2023 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.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.util.DisplayUtils;
import android.util.Log;
import android.util.RotationUtils;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.Surface;

import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
import com.android.systemui.shared.biometrics.R;

/** Utility class for working with udfps. */
public class UdfpsUtils {
    private static final String TAG = "UdfpsUtils";

    /**
     * Gets the scale factor representing the user's current resolution / the stable (default)
     * resolution.
     *
     * @param displayInfo The display information.
     */
    public float getScaleFactor(DisplayInfo displayInfo) {
        Display.Mode maxDisplayMode =
                DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes);
        float scaleFactor =
                DisplayUtils.getPhysicalPixelDisplaySizeRatio(
                        maxDisplayMode.getPhysicalWidth(),
                        maxDisplayMode.getPhysicalHeight(),
                        displayInfo.getNaturalWidth(),
                        displayInfo.getNaturalHeight()
                );
        return (scaleFactor == Float.POSITIVE_INFINITY) ? 1f : scaleFactor;
    }

    /**
     * Gets the touch in native coordinates. Map the touch to portrait mode if the device is in
     * landscape mode.
     *
     * @param idx                The pointer identifier.
     * @param event              The MotionEvent object containing full information about the event.
     * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
     * @return The mapped touch event.
     */
    public Point getTouchInNativeCoordinates(int idx, MotionEvent event,
            UdfpsOverlayParams udfpsOverlayParams) {
        Point portraitTouch = getPortraitTouch(idx, event, udfpsOverlayParams);

        // Scale the coordinates to native resolution.
        float scale = udfpsOverlayParams.getScaleFactor();
        portraitTouch.x = (int) (portraitTouch.x / scale);
        portraitTouch.y = (int) (portraitTouch.y / scale);
        return portraitTouch;
    }

    /**
     * @param idx                The pointer identifier.
     * @param event              The MotionEvent object containing full information about the event.
     * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
     * @return Whether the touch event is within sensor area.
     */
    public boolean isWithinSensorArea(int idx, MotionEvent event,
            UdfpsOverlayParams udfpsOverlayParams) {
        Point portraitTouch = getPortraitTouch(idx, event, udfpsOverlayParams);
        return udfpsOverlayParams.getSensorBounds().contains(portraitTouch.x, portraitTouch.y);
    }

    /**
     * This function computes the angle of touch relative to the sensor and maps the angle to a list
     * of help messages which are announced if accessibility is enabled.
     *
     * @return Whether the announcing string is null
     */
    public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled, Context context,
            int scaledTouchX, int scaledTouchY, UdfpsOverlayParams udfpsOverlayParams) {
        if (!touchExplorationEnabled) {
            return null;
        }

        Resources resources = context.getResources();
        String[] touchHints = new String[] {
                resources.getString(R.string.udfps_accessibility_touch_hints_left),
                resources.getString(R.string.udfps_accessibility_touch_hints_down),
                resources.getString(R.string.udfps_accessibility_touch_hints_right),
                resources.getString(R.string.udfps_accessibility_touch_hints_up),
        };

        // Scale the coordinates to native resolution.
        float scale = udfpsOverlayParams.getScaleFactor();
        float scaledSensorX = udfpsOverlayParams.getSensorBounds().centerX() / scale;
        float scaledSensorY = udfpsOverlayParams.getSensorBounds().centerY() / scale;
        String theStr =
                onTouchOutsideOfSensorAreaImpl(
                        touchHints,
                        scaledTouchX,
                        scaledTouchY,
                        scaledSensorX,
                        scaledSensorY,
                        udfpsOverlayParams.getRotation()
                );
        Log.v(TAG, "Announcing touch outside : $theStr");
        return theStr;
    }

    /**
     * This function computes the angle of touch relative to the sensor and maps the angle to a list
     * of help messages which are announced if accessibility is enabled.
     *
     * There are 4 quadrants of the circle (90 degree arcs)
     *
     * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" [45, 135) ->
     * touchHints[1] = "Move Fingerprint down" And so on.
     */
    private String onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX,
            float touchY, float sensorX, float sensorY, int rotation) {
        float xRelativeToSensor = touchX - sensorX;
        // Touch coordinates are with respect to the upper left corner, so reverse
        // this calculation
        float yRelativeToSensor = sensorY - touchY;
        double angleInRad = Math.atan2(yRelativeToSensor, xRelativeToSensor);
        // If the radians are negative, that means we are counting clockwise.
        // So we need to add 360 degrees
        if (angleInRad < 0.0) {
            angleInRad += 2.0 * Math.PI;
        }
        // rad to deg conversion
        double degrees = Math.toDegrees(angleInRad);
        double degreesPerBucket = 360.0 / touchHints.length;
        double halfBucketDegrees = degreesPerBucket / 2.0;
        // The mapping should be as follows
        // [315, 360] && [0, 45] -> 0
        // [45, 135]             -> 1
        int index = (int) ((degrees + halfBucketDegrees) % 360 / degreesPerBucket);
        index %= touchHints.length;

        // A rotation of 90 degrees corresponds to increasing the index by 1.
        if (rotation == Surface.ROTATION_90) {
            index = (index + 1) % touchHints.length;
        }
        if (rotation == Surface.ROTATION_270) {
            index = (index + 3) % touchHints.length;
        }
        return touchHints[index];
    }

    /**
     * Map the touch to portrait mode if the device is in landscape mode.
     */
    private Point getPortraitTouch(int idx, MotionEvent event,
            UdfpsOverlayParams udfpsOverlayParams) {
        Point portraitTouch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
        int rot = udfpsOverlayParams.getRotation();
        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
            RotationUtils.rotatePoint(
                    portraitTouch,
                    RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
                    udfpsOverlayParams.getLogicalDisplayWidth(),
                    udfpsOverlayParams.getLogicalDisplayHeight()
            );
        }
        return portraitTouch;
    }
}
