/*
 * 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 com.android.server.art;

import static com.android.server.art.model.ArtFlags.PriorityClassApi;

import android.annotation.NonNull;
import android.annotation.StringDef;
import android.annotation.SystemApi;
import android.os.Build;
import android.os.SystemProperties;
import android.text.TextUtils;

import androidx.annotation.RequiresApi;

import com.android.server.art.model.ArtFlags;
import com.android.server.pm.PackageManagerLocal;

import dalvik.system.DexFile;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Set;

/**
 * Maps a compilation reason to a compiler filter and a priority class.
 *
 * @hide
 */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class ReasonMapping {
    private ReasonMapping() {}

    // Keep this in sync with `ArtShellCommand.printHelp` except for 'inactive'.

    /** Dexopting apps on the first boot after flashing or factory resetting the device. */
    public static final String REASON_FIRST_BOOT = "first-boot";
    /** Dexopting apps on the next boot after an OTA. */
    public static final String REASON_BOOT_AFTER_OTA = "boot-after-ota";
    /** Dexopting apps on the next boot after a mainline update. */
    public static final String REASON_BOOT_AFTER_MAINLINE_UPDATE = "boot-after-mainline-update";
    /** Installing an app after user presses the "install"/"update" button. */
    public static final String REASON_INSTALL = "install";
    /** Dexopting apps in the background. */
    public static final String REASON_BG_DEXOPT = "bg-dexopt";
    /** Invoked by cmdline. */
    public static final String REASON_CMDLINE = "cmdline";
    /** Downgrading the compiler filter when an app is not used for a long time. */
    public static final String REASON_INACTIVE = "inactive";

    // Reasons for Play Install Hints (go/install-hints).
    public static final String REASON_INSTALL_FAST = "install-fast";
    public static final String REASON_INSTALL_BULK = "install-bulk";
    public static final String REASON_INSTALL_BULK_SECONDARY = "install-bulk-secondary";
    public static final String REASON_INSTALL_BULK_DOWNGRADED = "install-bulk-downgraded";
    public static final String REASON_INSTALL_BULK_SECONDARY_DOWNGRADED =
            "install-bulk-secondary-downgraded";

    /** @hide */
    public static final Set<String> REASONS_FOR_INSTALL = Set.of(REASON_INSTALL,
            REASON_INSTALL_FAST, REASON_INSTALL_BULK, REASON_INSTALL_BULK_SECONDARY,
            REASON_INSTALL_BULK_DOWNGRADED, REASON_INSTALL_BULK_SECONDARY_DOWNGRADED);

    // Keep this in sync with `ArtShellCommand.printHelp`.
    /** @hide */
    public static final Set<String> BATCH_DEXOPT_REASONS = Set.of(REASON_FIRST_BOOT,
            REASON_BOOT_AFTER_OTA, REASON_BOOT_AFTER_MAINLINE_UPDATE, REASON_BG_DEXOPT);

    /**
     * Reasons for {@link ArtManagerLocal#dexoptPackages}.
     *
     * @hide
     */
    // clang-format off
    @StringDef(prefix = "REASON_", value = {
        REASON_FIRST_BOOT,
        REASON_BOOT_AFTER_OTA,
        REASON_BOOT_AFTER_MAINLINE_UPDATE,
        REASON_BG_DEXOPT,
    })
    // clang-format on
    @Retention(RetentionPolicy.SOURCE)
    public @interface BatchDexoptReason {}

    /**
     * Reasons for {@link ArtManagerLocal#onBoot(String, Executor, Consumer<OperationProgress>)}.
     *
     * @hide
     */
    // clang-format off
    @StringDef(prefix = "REASON_", value = {
        REASON_FIRST_BOOT,
        REASON_BOOT_AFTER_OTA,
        REASON_BOOT_AFTER_MAINLINE_UPDATE,
    })
    // clang-format on
    @Retention(RetentionPolicy.SOURCE)
    public @interface BootReason {}

    /**
     * Loads the compiler filter from the system property for the given reason and checks for
     * validity.
     *
     * @throws IllegalArgumentException if the reason is invalid
     * @throws IllegalStateException if the system property value is invalid
     *
     * @hide
     */
    @NonNull
    public static String getCompilerFilterForReason(@NonNull String reason) {
        String value = SystemProperties.get("pm.dexopt." + reason);
        if (TextUtils.isEmpty(value)) {
            throw new IllegalArgumentException("No compiler filter for reason '" + reason + "'");
        }
        if (!Utils.isValidArtServiceCompilerFilter(value)) {
            throw new IllegalStateException(
                    "Got invalid compiler filter '" + value + "' for reason '" + reason + "'");
        }
        return value;
    }

    /**
     * Loads the compiler filter from the system property for:
     * - shared libraries
     * - apps used by other apps without a dex metadata file
     *
     * @throws IllegalStateException if the system property value is invalid
     *
     * @hide
     */
    @NonNull
    public static String getCompilerFilterForShared() {
        // "shared" is technically not a compilation reason, but the compiler filter is defined as a
        // system property as if "shared" is a reason.
        String value = getCompilerFilterForReason("shared");
        if (DexFile.isProfileGuidedCompilerFilter(value)) {
            throw new IllegalStateException(
                    "Compiler filter for 'shared' must not be profile guided, got '" + value + "'");
        }
        return value;
    }

    /**
     * Returns the priority for the given reason.
     *
     * @throws IllegalArgumentException if the reason is invalid
     * @see PriorityClassApi
     *
     * @hide
     */
    public static @PriorityClassApi byte getPriorityClassForReason(@NonNull String reason) {
        switch (reason) {
            case REASON_FIRST_BOOT:
            case REASON_BOOT_AFTER_OTA:
            case REASON_BOOT_AFTER_MAINLINE_UPDATE:
                return ArtFlags.PRIORITY_BOOT;
            case REASON_INSTALL_FAST:
                return ArtFlags.PRIORITY_INTERACTIVE_FAST;
            case REASON_INSTALL:
            case REASON_CMDLINE:
                return ArtFlags.PRIORITY_INTERACTIVE;
            case REASON_BG_DEXOPT:
            case REASON_INACTIVE:
            case REASON_INSTALL_BULK:
            case REASON_INSTALL_BULK_SECONDARY:
            case REASON_INSTALL_BULK_DOWNGRADED:
            case REASON_INSTALL_BULK_SECONDARY_DOWNGRADED:
                return ArtFlags.PRIORITY_BACKGROUND;
            default:
                throw new IllegalArgumentException("No priority class for reason '" + reason + "'");
        }
    }

    /**
     * Loads the concurrency from the system property, for batch dexopt ({@link
     * ArtManagerLocal#dexoptPackages}), or 1 if the system property is not found or cannot be
     * parsed.
     *
     * @hide
     */
    public static int getConcurrencyForReason(@NonNull @BatchDexoptReason String reason) {
        return SystemProperties.getInt("pm.dexopt." + reason + ".concurrency", 1 /* def */);
    }
}
