/*
 * Copyright (C) 2022 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 android.window;

import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.view.Surface.ROTATION_0;
import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;

import android.annotation.NonNull;
import android.app.ResourcesManager;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.InsetsState;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.WindowMetrics;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;

/**
 * A controller to handle {@link android.view.WindowMetrics} related APIs, which are
 * <ol>
 *     <li>{@link WindowManager#getCurrentWindowMetrics()}</li>
 *     <li>{@link WindowManager#getMaximumWindowMetrics()}</li>
 *     <li>{@link WindowManager#getPossibleMaximumWindowMetrics(int)}</li>
 * </ol>
 *
 * @hide
 */
public final class WindowMetricsController {
    private final Context mContext;

    public WindowMetricsController(@NonNull Context context) {
        mContext = context;
    }

    /** @see WindowManager#getCurrentWindowMetrics() */
    public WindowMetrics getCurrentWindowMetrics() {
        return getWindowMetricsInternal(false /* isMaximum */);
    }

    /** @see WindowManager#getMaximumWindowMetrics() */
    public WindowMetrics getMaximumWindowMetrics() {
        return getWindowMetricsInternal(true /* isMaximum */);
    }

    /**
     * The core implementation to obtain {@link WindowMetrics}
     *
     * @param isMaximum {@code true} to obtain {@link WindowManager#getCurrentWindowMetrics()}.
     *                  {@code false} to obtain {@link WindowManager#getMaximumWindowMetrics()}.
     */
    private WindowMetrics getWindowMetricsInternal(boolean isMaximum) {
        final Rect bounds;
        final float density;
        final boolean isScreenRound;
        final int activityType;
        synchronized (ResourcesManager.getInstance()) {
            final Configuration config = mContext.getResources().getConfiguration();
            final WindowConfiguration winConfig = config.windowConfiguration;
            bounds = (isMaximum) ? winConfig.getMaxBounds() : winConfig.getBounds();
            // Multiply default density scale because WindowMetrics provide the density value with
            // the scaling factor for the Density Independent Pixel unit, which is the same unit
            // as DisplayMetrics#density
            density = config.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
            isScreenRound = config.isScreenRound();
            activityType = winConfig.getActivityType();
        }
        final IBinder token = Context.getToken(mContext);
        final Supplier<WindowInsets> insetsSupplier = () -> getWindowInsetsFromServerForDisplay(
                mContext.getDisplayId(), token, bounds, isScreenRound, activityType);
        return new WindowMetrics(new Rect(bounds), insetsSupplier, density);
    }

    /**
     * Retrieves WindowInsets for the given context and display, given the window bounds.
     *
     * @param displayId the ID of the logical display to calculate insets for
     * @param token the token of Activity or WindowContext
     * @param bounds the window bounds to calculate insets for
     * @param isScreenRound if the display identified by displayId is round
     * @param activityType the activity type of the window to calculate insets for
     * @return WindowInsets calculated for the given window bounds, on the given display
     */
    private static WindowInsets getWindowInsetsFromServerForDisplay(int displayId, IBinder token,
            Rect bounds, boolean isScreenRound, int activityType) {
        try {
            final InsetsState insetsState = new InsetsState();
            WindowManagerGlobal.getWindowManagerService().getWindowInsets(
                    displayId, token, insetsState);
            final float overrideInvScale = CompatibilityInfo.getOverrideInvertedScale();
            if (overrideInvScale != 1f) {
                insetsState.scale(overrideInvScale);
            }
            return insetsState.calculateInsets(bounds, null /* ignoringVisibilityState */,
                    isScreenRound, SOFT_INPUT_ADJUST_NOTHING, 0 /* flags */, SYSTEM_UI_FLAG_VISIBLE,
                    WindowManager.LayoutParams.INVALID_WINDOW_TYPE, activityType,
                    null /* idSideMap */);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @see WindowManager#getPossibleMaximumWindowMetrics(int) */
    @NonNull
    public Set<WindowMetrics> getPossibleMaximumWindowMetrics(int displayId) {
        List<DisplayInfo> possibleDisplayInfos;
        try {
            possibleDisplayInfos = WindowManagerGlobal.getWindowManagerService()
                    .getPossibleDisplayInfo(displayId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        Set<WindowMetrics> maxMetrics = new HashSet<>();
        WindowInsets windowInsets;
        DisplayInfo currentDisplayInfo;
        for (int i = 0; i < possibleDisplayInfos.size(); i++) {
            currentDisplayInfo = possibleDisplayInfos.get(i);

            // Calculate max bounds for natural rotation and state.
            Rect maxBounds = new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
                    currentDisplayInfo.getNaturalHeight());

            // Calculate insets for the natural max bounds.
            final boolean isScreenRound = (currentDisplayInfo.flags & Display.FLAG_ROUND) != 0;
            // Initialize insets based on Surface.ROTATION_0. Note any window-provided insets
            // will not be set.
            windowInsets = getWindowInsetsFromServerForDisplay(
                    currentDisplayInfo.displayId, null /* token */,
                    new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
                            currentDisplayInfo.getNaturalHeight()), isScreenRound,
                    ACTIVITY_TYPE_UNDEFINED);
            // Set the hardware-provided insets. Always with the ROTATION_0 result.
            DisplayCutout cutout = currentDisplayInfo.displayCutout;
            if (cutout != null && currentDisplayInfo.rotation != ROTATION_0) {
                cutout = cutout.getRotated(
                        currentDisplayInfo.logicalWidth, currentDisplayInfo.logicalHeight,
                        currentDisplayInfo.rotation, ROTATION_0);
            }
            windowInsets = new WindowInsets.Builder(windowInsets).setRoundedCorners(
                    currentDisplayInfo.roundedCorners).setDisplayCutout(cutout).build();

            // Multiply default density scale because WindowMetrics provide the density value with
            // the scaling factor for the Density Independent Pixel unit, which is the same unit
            // as DisplayMetrics#density
            final float density = currentDisplayInfo.logicalDensityDpi
                    * DisplayMetrics.DENSITY_DEFAULT_SCALE;
            maxMetrics.add(new WindowMetrics(maxBounds, windowInsets, density));
        }
        return maxMetrics;
    }
}