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

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.profiling.Flags;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * API for apps to request and listen for app specific profiling.
 */
@FlaggedApi(Flags.FLAG_TELEMETRY_APIS)
public final class ProfilingManager {
    private static final String TAG = ProfilingManager.class.getSimpleName();
    private static final boolean DEBUG = false;

    /** Profiling type for {@link #requestProfiling} to request a java heap dump. */
    public static final int PROFILING_TYPE_JAVA_HEAP_DUMP = 1;

    /** Profiling type for {@link #requestProfiling} to request a heap profile. */
    public static final int PROFILING_TYPE_HEAP_PROFILE = 2;

    /** Profiling type for {@link #requestProfiling} to request a stack sample. */
    public static final int PROFILING_TYPE_STACK_SAMPLING = 3;

    /** Profiling type for {@link #requestProfiling} to request a system trace. */
    public static final int PROFILING_TYPE_SYSTEM_TRACE = 4;

    /* Begin public API defined keys. */
    /* End public API defined keys. */

    /* Begin not-public API defined keys/values. */
    /**
     * Can only be used with profiling type heap profile, stack sampling, or system trace.
     * Value of type int.
     * @hide
     */
    public static final String KEY_DURATION_MS = "KEY_DURATION_MS";

    /**
     * Can only be used with profiling type heap profile. Value of type long.
     * @hide
     */
    public static final String KEY_SAMPLING_INTERVAL_BYTES = "KEY_SAMPLING_INTERVAL_BYTES";

    /**
     * Can only be used with profiling type heap profile. Value of type boolean.
     * @hide
     */
    public static final String KEY_TRACK_JAVA_ALLOCATIONS = "KEY_TRACK_JAVA_ALLOCATIONS";

    /**
     * Can only be used with profiling type stack sampling. Value of type int.
     * @hide
     */
    public static final String KEY_FREQUENCY_HZ = "KEY_FREQUENCY_HZ";

    /**
     * Can be used with all profiling types. Value of type int.
     * @hide
     */
    public static final String KEY_SIZE_KB = "KEY_SIZE_KB";

    /**
     * Can be used with profiling type system trace.
     * Value of type int must be one of:
     * {@link VALUE_BUFFER_FILL_POLICY_DISCARD}
     * {@link VALUE_BUFFER_FILL_POLICY_RING_BUFFER}
     * @hide
     */
    public static final String KEY_BUFFER_FILL_POLICY = "KEY_BUFFER_FILL_POLICY";

    /** @hide */
    public static final int VALUE_BUFFER_FILL_POLICY_DISCARD = 1;

    /** @hide */
    public static final int VALUE_BUFFER_FILL_POLICY_RING_BUFFER = 2;
    /* End not-public API defined keys/values. */

    /**
     * @hide *
     */
    @IntDef(
        prefix = {"PROFILING_TYPE_"},
        value = {
            PROFILING_TYPE_JAVA_HEAP_DUMP,
            PROFILING_TYPE_HEAP_PROFILE,
            PROFILING_TYPE_STACK_SAMPLING,
            PROFILING_TYPE_SYSTEM_TRACE,
        })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ProfilingType {}

    private final Object mLock = new Object();
    private final Context mContext;

    /** @hide */
    @VisibleForTesting
    @GuardedBy("mLock")
    public final ArrayList<ProfilingRequestCallbackWrapper> mCallbacks = new ArrayList<>();

    /** @hide */
    @VisibleForTesting
    @GuardedBy("mLock")
    public IProfilingService mProfilingService;

    /**
     * Constructor for ProfilingManager.
     *
     * @hide
     */
    public ProfilingManager(Context context) {
        mContext = context;
    }

    /**
     * Request system profiling.
     *
     * <p class="note">
     *   Note: use of this API directly is not recommended for most use cases.
     *   Consider using the higher level wrappers provided by AndroidX that will construct the
     *   request correctly, supporting available options with simplified request parameters
     * </p>
     *
     * <p>
     *   Both a listener and an executor must be set at the time of the request for the request to
     *   be considered for fulfillment. Listener/executor pairs can be set in this method, with
     *   {@link registerForAllProfilingResults}, or both. The listener and executor must be set
     *   together, in the same call. If no listener and executor combination is set, the request
     *   will be discarded and no callback will be received.
     * </p>
     *
     * <p>
     *   Requests will be rate limited and are not guaranteed to be filled.
     * </p>
     *
     * <p>
     *   There might be a delay before profiling begins.
     *   For continuous profiling types (system tracing, stack sampling, and heap profiling),
     *   we recommend starting the collection early and stopping it with {@link cancellationSignal}
     *   immediately after the area of interest to ensure that the section you want profiled is
     *   captured.
     *   For heap dumps, we recommend testing locally to ensure that the heap dump is collected at
     *   the proper time.
     * </p>
     *
     * @param profilingType Type of profiling to collect.
     * @param parameters Bundle of request related parameters. If the bundle contains any
     *                  unrecognized parameters, the request will be fail with
     *                  {@link #ProfilingResult#ERROR_FAILED_INVALID_REQUEST}. If the values for
     *                  the parameters are out of supported range, the closest possible in range
     *                  value will be chosen.
     *                  Use of androidx wrappers is recommended over generating this directly.
     * @param tag Caller defined data to help identify the output.
     *                  The first 20 alphanumeric characters, plus dashes, will be lowercased
     *                  and included in the output filename.
     * @param cancellationSignal for caller requested cancellation.
     *                  Results will be returned if available.
     *                  If this is null, the requesting app will not be able to stop the collection.
     *                  The collection will stop after timing out with either the provided
     *                  configurations or with system defaults
     * @param executor  The executor to call back with.
     *                  Will only be used for the listener provided in this method.
     *                  If this is null, and no global executor and listener combinations are
     *                  registered at the time of the request, the request will be dropped.
     * @param listener  Listener to be triggered with result. Any global listeners registered via
     *                  {@link #registerForAllProfilingResults} will also be triggered. If this is
     *                  null, and no global listener and executor combinations are registered at
     *                  the time of the request, the request will be dropped.
     */
    public void requestProfiling(
            @ProfilingType int profilingType,
            @Nullable Bundle parameters,
            @Nullable String tag,
            @Nullable CancellationSignal cancellationSignal,
            @Nullable Executor executor,
            @Nullable Consumer<ProfilingResult> listener) {
        synchronized (mLock) {
            try {
                final UUID key = UUID.randomUUID();

                if (executor != null && listener != null) {
                    // Listeners are provided, store them.
                    mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, key));
                } else if (mCallbacks.isEmpty()) {
                    // No listeners have been registered by any path, toss the request.
                    throw new IllegalArgumentException(
                            "No listeners have been registered. Request has been discarded.");
                }
                // If neither case above was hit, app wide listeners were provided. Continue.

                final IProfilingService service = getOrCreateIProfilingServiceLocked(false);
                if (service == null) {
                    executor.execute(() -> listener.accept(
                            new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
                                "ProfilingService is not available")));
                    if (DEBUG) Log.d(TAG, "ProfilingService is not available");
                    return;
                }

                String packageName = mContext.getPackageName();
                if (packageName == null) {
                    executor.execute(() -> listener.accept(
                            new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
                                    "Failed to resolve package name")));
                    if (DEBUG) Log.d(TAG, "Failed to resolve package name.");
                    return;
                }

                // For key, use most and least significant bits so we can create an identical UUID
                // after passing over binder.
                service.requestProfiling(profilingType, parameters,
                        mContext.getFilesDir().getPath(), tag,
                        key.getMostSignificantBits(), key.getLeastSignificantBits(),
                        packageName);
                if (cancellationSignal != null) {
                    cancellationSignal.setOnCancelListener(
                            () -> {
                                synchronized (mLock) {
                                    try {
                                        service.requestCancel(key.getMostSignificantBits(),
                                                key.getLeastSignificantBits());
                                    } catch (RemoteException e) {
                                        // Ignore, request in flight already and we can't stop it.
                                    }
                                }
                            }
                    );
                }
            } catch (RemoteException e) {
                if (DEBUG) Log.d(TAG, "Binder exception processing request", e);
                executor.execute(() -> listener.accept(
                        new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
                                "Binder exception processing request")));
                throw new RuntimeException("Unable to request profiling.");
            }
        }
    }

    /**
     * Register a listener to be called for all profiling results for this uid. Listeners set here
     * will be called in addition to any provided with the request.
     *
     * <p class="note"> Note: If a callback attempt fails (for example, because your app is killed
     * while a trace is in progress) re-delivery may be attempted using a listener added via this
     * method. </p>
     *
     * @param executor The executor to call back with.
     * @param listener Listener to be triggered with result.
     */
    public void registerForAllProfilingResults(
            @NonNull Executor executor,
            @NonNull Consumer<ProfilingResult> listener) {
        synchronized (mLock) {
            // Only notify {@link mProfilingService} of a general listener being added if it already
            // exists as registering it also handles the notifying.
            boolean shouldNotifyService = mProfilingService != null;

            if (getOrCreateIProfilingServiceLocked(true) == null) {
                // If the binder object was not successfully registered then this listener will
                // not ever be triggered.
                executor.execute(() -> listener.accept(new ProfilingResult(
                        ProfilingResult.ERROR_UNKNOWN, null, null,
                        "Binder exception processing request")));
                return;
            }
            mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, null));

            if (shouldNotifyService) {
                // Notify service that a general listener was added. General listeners are also used
                // for queued callbacks if any are waiting.
                try {
                    mProfilingService.generalListenerAdded();
                } catch (RemoteException e) {
                    // Do nothing. Binder callback is already registered, but service won't know
                    // there is a general listener so queued callbacks won't occur.
                    Log.d(TAG, "Exception notifying service of general callback,"
                            + " queued callbacks will not occur.", e);
                }
            }
        }
    }

    /**
     * Unregister a listener that was to be called for all profiling results. If no listener is
     * provided, all listeners for this process that were not submitted with a profiling request
     * will be removed.
     *
     * @param listener Listener to unregister and no longer be triggered with the results.
     *                 Null to remove all global listeners for this uid.
     */
    public void unregisterForAllProfilingResults(
            @Nullable Consumer<ProfilingResult> listener) {
        synchronized (mLock) {
            if (mCallbacks.isEmpty()) {
                // No callbacks, nothing to remove.
                return;
            }

            if (listener == null) {
                // Remove all global listeners.
                ArrayList<ProfilingRequestCallbackWrapper> listenersToRemove = new ArrayList<>();
                for (int i = 0; i < mCallbacks.size(); i++) {
                    ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
                    // Only remove global listeners which are not tied to a specific request. These
                    // can be identified by checking that they do not have an associated key.
                    if (wrapper.mKey == null) {
                        listenersToRemove.add(wrapper);
                    }
                }
                mCallbacks.removeAll(listenersToRemove);
            } else {
                // Remove the provided listener only.
                for (int i = 0; i < mCallbacks.size(); i++) {
                    ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
                    if (listener.equals(wrapper.mListener)) {
                        mCallbacks.remove(i);
                        return;
                    }
                }
            }
        }
    }


    /** @hide */
    @VisibleForTesting
    @GuardedBy("mLock")
    public @Nullable IProfilingService getOrCreateIProfilingServiceLocked(
            boolean isGeneralListener) {
        // We only register the callback with registerResultsCallback once per binder object, and we
        // only create one binder object per ProfilingManager instance. If the object already exists
        // then it was successfully created and registered previously so we can just return it.
        if (mProfilingService != null) {
            return mProfilingService;
        }

        mProfilingService = IProfilingService.Stub.asInterface(
                ProfilingFrameworkInitializer.getProfilingServiceManager()
                    .getProfilingServiceRegisterer().get());
        if (mProfilingService == null) {
            // Service is not accessible, all requests will fail.
            return mProfilingService;
        }
        try {
            mProfilingService.registerResultsCallback(isGeneralListener,
                    new IProfilingResultCallback.Stub() {

                        /**
                         * Called by {@link ProfilingService} when a result is ready,
                         * both for success and failure.
                         */
                        @Override
                        public void sendResult(@Nullable String resultFile, long keyMostSigBits,
                                long keyLeastSigBits, int status, @Nullable String tag,
                                @Nullable String error) {
                            synchronized (mLock) {
                                if (mCallbacks.isEmpty()) {
                                    // This shouldn't happen - no callbacks, nowhere to report this
                                    // result.
                                    if (DEBUG) Log.d(TAG, "No callbacks");
                                    mProfilingService = null;
                                    return;
                                }

                                // This shouldn't be true, but if the file is null ensure the status
                                // represents a failure.
                                final boolean overrideStatusToError = resultFile == null
                                        && status == ProfilingResult.ERROR_NONE;

                                UUID key = new UUID(keyMostSigBits, keyLeastSigBits);
                                int removeListenerPos = -1;
                                for (int i = 0; i < mCallbacks.size(); i++) {
                                    ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
                                    if (key.equals(wrapper.mKey)) {
                                        // At most 1 listener can have a key matching this result:
                                        // the one registered with the request, remove that one
                                        // only.
                                        if (removeListenerPos == -1) {
                                            removeListenerPos = i;
                                        } else {
                                            // This should never happen.
                                            if (DEBUG) {
                                                Log.d(TAG,
                                                        "More than 1 listener with the same key");
                                            }
                                        }
                                    } else if (wrapper.mKey != null) {
                                        // If the key is not null, and doesn't matched the result
                                        // key, then this key belongs to another request and should
                                        // not be triggered.
                                        continue;
                                    }

                                    // TODO: b/337017299 - check resultFile is valid before
                                    // returning Now trigger the callback for any listener that
                                    // doesn't belong to another request.
                                    wrapper.mExecutor.execute(() -> wrapper.mListener.accept(
                                            new ProfilingResult(overrideStatusToError
                                                    ? ProfilingResult.ERROR_UNKNOWN : status,
                                                    resultFile, tag, error)));
                                }

                                // Remove the single listener that was tied to the request, if
                                // applicable.
                                if (removeListenerPos != -1) {
                                    mCallbacks.remove(removeListenerPos);
                                }
                            }
                        }

                        /**
                         * Called by {@link ProfilingService} when a trace is ready and needs to be
                         * copied to callers internal storage.
                         *
                         * This method will open a new file and pass back the FileDescriptor for
                         * ProfilingService to write to via a new binder call.
                         *
                         * Takes in key most/least significant bits which represent the key that
                         * will be used to associate this back to a profiling session which will
                         * write to the generated file.
                         */
                        @Override
                        public void generateFile(String filePathAbsolute, String fileName,
                                long keyMostSigBits, long keyLeastSigBits) {
                            synchronized (mLock) {
                                try {
                                    // Ensure the profiling directory exists. Create it if it
                                    // doesn't.
                                    final File profilingDir = new File(filePathAbsolute);
                                    if (!profilingDir.exists()) {
                                        profilingDir.mkdir();
                                    }

                                    // Create the profiling file for the output to be written to.
                                    final File profilingFile = new File(
                                            filePathAbsolute + fileName);
                                    profilingFile.createNewFile();
                                    if (!profilingFile.exists()) {
                                        // Failed to create output file. Result may be lost.
                                        if (DEBUG) Log.d(TAG, "Output file couldn't be created");
                                        return;
                                    }

                                    // Wrap the new output file in a {@link ParcelFileDescriptor} to
                                    // send back to {@link ProfilingService} to write to.
                                    ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                                            profilingFile,
                                            ParcelFileDescriptor.MODE_READ_WRITE);
                                    IProfilingService service =
                                            getOrCreateIProfilingServiceLocked(false);

                                    if (service == null) {
                                        // Unable to send file descriptor because we have nowhere to
                                        // send it to. Result may be lost. Close descriptor and
                                        // delete file.
                                        if (DEBUG) Log.d(TAG, "Unable to send file descriptor");
                                        tryToCleanupGeneratedFile(pfd, profilingFile);
                                        return;
                                    }

                                    try {
                                        // Send the file descriptor to service to write to.
                                        service.receiveFileDescriptor(pfd, keyMostSigBits,
                                                keyLeastSigBits);
                                    } catch (RemoteException e) {
                                        // If we failed to send it, try to clean it up as it won't
                                        // be used.
                                        if (DEBUG) {
                                            Log.d(TAG, "Failed sending file descriptor to service",
                                                    e);
                                        }
                                        tryToCleanupGeneratedFile(pfd, profilingFile);
                                    }
                                } catch (Exception e) {
                                    // Failure prepping output file. Result may be lost.
                                    if (DEBUG) Log.d(TAG, "Exception preparing file", e);
                                    return;
                                }
                            }
                        }

                        /**
                         * Attempt to clean up the files created for service by closing the file
                         * descriptor and deleting the file. This is intended for error cases where
                         * the descriptor could not be sent. If it was successfully sent, service
                         * will handle closing it and requesting a delete if necessary.
                         */
                        private void tryToCleanupGeneratedFile(ParcelFileDescriptor fileDescriptor,
                                File file) {
                            if (fileDescriptor != null) {
                                try {
                                    fileDescriptor.close();
                                } catch (IOException e) {
                                    // Nothing else we can do, ignore.
                                    if (DEBUG) Log.d(TAG, "Failed to cleanup file descriptor", e);
                                }
                            }

                            if (file != null) {
                                try {
                                    file.delete();
                                } catch (SecurityException e) {
                                    // Nothing else we can do, ignore.
                                    if (DEBUG) Log.d(TAG, "Failed to cleanup file", e);
                                }
                            }
                        }

                        /**
                         * Delete a file. To be used only for files created by {@link generateFile}.
                         */
                        @Override
                        public void deleteFile(String filePathAndName) {
                            try {
                                Files.delete(Path.of(filePathAndName));
                            } catch (Exception exception) {
                                if (DEBUG) Log.e(TAG, "Failed to delete file.", exception);
                            }
                        }
                    });
        } catch (RemoteException e) {
            if (DEBUG) Log.d(TAG, "Exception registering service callback", e);
            throw new RuntimeException("Unable to register profiling result callback."
                    + " All Profiling requests will fail.");
        }
        return mProfilingService;
    }

    private static final class ProfilingRequestCallbackWrapper {
        /** executor provided with callback request */
        final @NonNull Executor mExecutor;

        /** listener provided with callback request */
        final @NonNull Consumer<ProfilingResult> mListener;

        /**
         * Unique key generated with each profiling request {@link #requestProfiling}, but not with
         * requests to register a listener only {@link #registerForAllProfilingResults}.
         *
         * Key is used to match the result with the listener added with the request so that it can
         * removed after being triggered while the general registered callbacks remain active.
         */
        final @Nullable UUID mKey;

        ProfilingRequestCallbackWrapper(@NonNull Executor executor,
                @NonNull Consumer<ProfilingResult> listener,
                @Nullable UUID key) {
            mExecutor = executor;
            mListener = listener;
            mKey = key;
        }
    }
}
