/*
 * 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.profiling;

import static android.os.profiling.ProfilingService.TracingState;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.QueuedResultsWrapper;
import android.util.Log;

import java.util.UUID;

/**
 * Represents a single in progress tracing session and all necessary data to manage and process it.
 */
public final class TracingSession {
    private static final String TAG = TracingSession.class.getSimpleName();

    // LINT.IfChange(persisted_params)
    // Persisted params
    private final int mProfilingType;
    private final int mTriggerType;
    private final int mUid;
    @NonNull private final String mPackageName;
    @Nullable private final String mTag;
    private final long mKeyMostSigBits;
    private final long mKeyLeastSigBits;
    @Nullable private String mFileName = null;
    @Nullable private String mRedactedFileName = null;
    @NonNull private TracingState mState;
    private int mRetryCount = 0;
    @Nullable private String mErrorMessage = null;
    // Expected to be populated with ProfilingResult.ERROR_* values.
    private int mErrorStatus = -1; // Default to invalid value.
    private long mProfilingStartTimeMs;
    // LINT.ThenChange(:from_proto)

    // Non-persisted params
    @Nullable private final Bundle mParams;
    @Nullable private Process mActiveTrace;
    @Nullable private Process mActiveRedaction;
    @Nullable private Runnable mProcessResultRunnable;
    @Nullable private String mKey = null;
    @Nullable private String mDestinationFileName = null;
    private long mRedactionStartTimeMs;
    private int mMaxProfilingTimeAllowedMs = 0;

    public TracingSession(int profilingType,  int uid, String packageName, int triggerType) {
        this(
                profilingType,
                null,
                uid,
                packageName,
                null,
                0L,
                0L,
                triggerType);
    }

    public TracingSession(int profilingType, Bundle params, int uid, String packageName, String tag,
            long keyMostSigBits, long keyLeastSigBits, int triggerType) {
        mProfilingType = profilingType;
        mTriggerType = triggerType;
        mParams = params;
        mUid = uid;
        mPackageName = packageName;
        mTag = tag;
        mKeyMostSigBits = keyMostSigBits;
        mKeyLeastSigBits = keyLeastSigBits;
        mState = TracingState.REQUESTED;
    }

    // LINT.IfChange(from_proto)
    public TracingSession(QueuedResultsWrapper.TracingSession sessionProto) {
        mProfilingType = sessionProto.getProfilingType();
        mUid = sessionProto.getUid();
        mPackageName = sessionProto.getPackageName();
        mTag = sessionProto.getTag();
        mKeyMostSigBits = sessionProto.getKeyMostSigBits();
        mKeyLeastSigBits = sessionProto.getKeyLeastSigBits();
        if (sessionProto.hasFileName()) {
            mFileName = sessionProto.getFileName();
        }
        if (sessionProto.hasRedactedFileName()) {
            mRedactedFileName = sessionProto.getRedactedFileName();
        }
        mState = TracingState.of(sessionProto.getTracingState());
        mRetryCount = sessionProto.getRetryCount();
        if (sessionProto.hasErrorMessage()) {
            mErrorMessage = sessionProto.getErrorMessage();
        }
        mErrorStatus = sessionProto.getErrorStatus();
        mTriggerType = sessionProto.getTriggerType();
        mProfilingStartTimeMs = sessionProto.getProfilingStartTime();

        // params is not persisted because we cannot guarantee that it does not contain some large
        // store of data, and because we don't need it anymore once the request has gotten to the
        // point of being persisted.
        mParams = null;

        if (mState == null || mState.getValue() < TracingState.PROFILING_FINISHED.getValue()) {
            // This should never happen. If state is null, then we can't know what to do next. If
            // the state is earlier than PROFILING_FINISHED then it should not have been in the
            // queue and therefore should not have been persisted. Either way, update the state to
            // indicate that the caller was already notified (because we can't know what to notify),
            // this will ensure that all that's remaining is cleanup.
            mState = TracingState.NOTIFIED_REQUESTER;
            Log.e(TAG, "Attempting to load a queued session with an invalid state.");
        }
    }
    // LINT.ThenChange(:to_proto)

    /** Generates the config for this request and converts to bytes. */
    public byte[] getConfigBytes() throws IllegalArgumentException {
        return Configs.generateConfigForRequest(mProfilingType, mParams, mPackageName);
    }

    /**
     * Gets the amount of time before the system should start checking whether the profiling is
     * complete so that post processing can begin.
     */
    public int getPostProcessingScheduleDelayMs() throws IllegalArgumentException {
        return Configs.getInitialProfilingTimeMs(mProfilingType, mParams);
    }

    /**
     * Gets the maximum profiling time allowed for this TracingSession.
     * @return maximum profiling time allowed in ms.
     */
    public int getMaxProfilingTimeAllowedMs() {
        if (mMaxProfilingTimeAllowedMs != 0) {
            return mMaxProfilingTimeAllowedMs;
        }
        mMaxProfilingTimeAllowedMs =
                Configs.getMaxProfilingTimeAllowedMs(mProfilingType, mParams);
        return mMaxProfilingTimeAllowedMs;
    }

    /** Get the tracing session unique key which was provided by {@link ProfilingManager}. */
    @Nullable
    public String getKey() {
        if (mKey == null) {
            mKey = (new UUID(mKeyMostSigBits, mKeyLeastSigBits)).toString();
        }
        return mKey;
    }

    public void setActiveTrace(Process activeTrace) {
        mActiveTrace = activeTrace;
    }

    public void setActiveRedaction(Process activeRedaction) {
        mActiveRedaction = activeRedaction;
    }

    public void setProcessResultRunnable(Runnable processResultRunnable) {
        mProcessResultRunnable = processResultRunnable;
    }

    // The file set here will be the name of the file that perfetto creates regardless of the
    // type of profiling that is being done.
    public void setFileName(String fileName) {
        mFileName = fileName;
    }

    public void setRedactedFileName(String fileName) {
        mRedactedFileName = fileName;
    }

    public void setRedactionStartTimeMs(long startTime) {
        mRedactionStartTimeMs = startTime;
    }

    public void setRetryCount(int retryCount) {
        mRetryCount = retryCount;
    }

    /**
     * Do not call directly!
     * State should only be updated with {@link ProfilingService#advanceStateAndContinue}.
     */
    public void setState(TracingState state) {
        mState = state;
    }

    /** Increase retry count by 1 */
    public void incrementRetryCount() {
        mRetryCount += 1;
    }

    public void setProfilingStartTimeMs(long startTime)  {
        mProfilingStartTimeMs = startTime;
    }

    /**
     * Update error status. Also overrides error message to null as the two fields must be set
     * together to ensure they make sense.
     */
    public void setError(int status) {
        setError(status, null);
    }

    /** Update error status and message. */
    public void setError(int status, String message) {
        mErrorStatus = status;
        mErrorMessage = message;
    }

    @Nullable
    public Process getActiveTrace() {
        return mActiveTrace;
    }

    @Nullable
    public Process getActiveRedaction() {
        return mActiveRedaction;
    }

    @Nullable
    public Runnable getProcessResultRunnable() {
        return mProcessResultRunnable;
    }

    public int getProfilingType() {
        return mProfilingType;
    }

    public int getUid() {
        return mUid;
    }

    @NonNull
    public String getPackageName() {
        return mPackageName;
    }

    @Nullable
    public String getTag() {
        return mTag;
    }

    public long getKeyMostSigBits() {
        return mKeyMostSigBits;
    }

    public long getKeyLeastSigBits() {
        return mKeyLeastSigBits;
    }

    // This returns the name of the file that perfetto created during profiling. If the profiling
    // type was a trace collection it will return the unredacted trace file name.
    @Nullable
    public String getFileName() {
        return mFileName;
    }

    @Nullable
    public String getRedactedFileName() {
        return mRedactedFileName;
    }

    public long getRedactionStartTimeMs() {
        return mRedactionStartTimeMs;
    }

    public long getProfilingStartTimeMs() {
        return mProfilingStartTimeMs;
    }

    /**
     * Returns the relative path starting from apps storage dir including name of the file being
     * returned to the client.
     * @param appRelativePath relative path to app storage.
     * @return relative file path and name of file.
     */
    @Nullable
    public String getDestinationFileName(String appRelativePath) {
        if (mFileName == null) {
            return null;
        }
        if (mDestinationFileName == null) {
            mDestinationFileName = appRelativePath
                    + ((this.getRedactedFileName() == null) ? mFileName : mRedactedFileName);
        }
        return mDestinationFileName;
    }

    @NonNull
    public TracingState getState() {
        return mState;
    }

    public int getRetryCount() {
        return mRetryCount;
    }

    @Nullable
    public String getErrorMessage() {
        return mErrorMessage;
    }

    public int getErrorStatus() {
        return mErrorStatus;
    }

    public int getTriggerType() {
        return mTriggerType;
    }

    // LINT.IfChange(to_proto)
    /** Convert this session to a proto for persisting. */
    public QueuedResultsWrapper.TracingSession toProto() {
        QueuedResultsWrapper.TracingSession.Builder tracingSessionBuilder =
                QueuedResultsWrapper.TracingSession.newBuilder();

        tracingSessionBuilder.setProfilingType(mProfilingType);
        tracingSessionBuilder.setUid(mUid);
        tracingSessionBuilder.setPackageName(mPackageName);
        if (mTag != null) {
            tracingSessionBuilder.setTag(mTag);
        }
        tracingSessionBuilder.setKeyMostSigBits(mKeyMostSigBits);
        tracingSessionBuilder.setKeyLeastSigBits(mKeyLeastSigBits);
        if (mFileName != null) {
            tracingSessionBuilder.setFileName(mFileName);
        }
        if (mRedactedFileName != null) {
            tracingSessionBuilder.setRedactedFileName(mRedactedFileName);
        }
        tracingSessionBuilder.setTracingState(mState.getValue());
        tracingSessionBuilder.setRetryCount(mRetryCount);
        if (mErrorMessage != null) {
            tracingSessionBuilder.setErrorMessage(mErrorMessage);
        }
        tracingSessionBuilder.setErrorStatus(mErrorStatus);
        tracingSessionBuilder.setTriggerType(mTriggerType);
        tracingSessionBuilder.setProfilingStartTime(mProfilingStartTimeMs);

        return tracingSessionBuilder.build();
    }
    // LINT.ThenChange(/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java:equals)
}
