/*
 * 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 android.app.appsearch;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Wrapper class to provide implementation for readBlob/writeBlob for all API levels.
 *
 * @hide
 */
public class ParcelableUtil {
    private static final String TAG = "AppSearchParcel";
    private static final String TEMP_FILE_PREFIX = "AppSearchSerializedBytes";
    private static final String TEMP_FILE_SUFFIX = ".tmp";
    // Same as IBinder.MAX_IPC_LIMIT. Limit that should be placed on IPC sizes to keep them safely
    // under the transaction buffer limit.
    private static final int DOCUMENT_SIZE_LIMIT_IN_BYTES = 64 * 1024;

    /**
     * IntDef for how a {@link android.app.appsearch.aidl.AppSearchBatchResultParcelV2} or {@link
     * android.app.appsearch.aidl.AppSearchResultParcelV2} write to or read from {@link Parcel}.
     */
    @IntDef(
            prefix = "WRITE_PARCEL_MODE_",
            value = {
                WRITE_PARCEL_MODE_MARSHALL_WRITE_IN_BLOB,
                WRITE_PARCEL_MODE_DIRECTLY_WRITE_TO_PARCEL
            })
    @Retention(RetentionPolicy.SOURCE)
    public @interface WriteParcelMode {}

    /**
     * We could use Parcel.writeBlob() and Parcel.readBlob(). Parcel.writeBlob() API could automatic
     * use Android Shared Memory it detects the data size is larger than 16 KiB. The data must be
     * marshalled to byte array to use that API. This could help us to avoid exceed binder
     * transaction limit when sending large objects.
     *
     * <p>Never use this mode if you need to marshall the parcel, the outcome of writeBlob contains
     * a fd to Android Shared Memory and cannot be marshalled. Only use this mode on the end parcel
     * object.
     */
    public static final int WRITE_PARCEL_MODE_MARSHALL_WRITE_IN_BLOB = 1;

    /**
     * Directly write this object to parcel. We cannot marshall binder object and FDs. If a result
     * contains such objects, we should always directly write it to and read it from parcel.
     */
    public static final int WRITE_PARCEL_MODE_DIRECTLY_WRITE_TO_PARCEL = 2;

    // TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
    @SuppressLint("ObsoleteSdkInt")
    public static void writeBlob(@NonNull Parcel parcel, @NonNull byte[] bytes) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Since parcel.writeBlob was added in API level 33, it is not available
            // on lower API levels.
            parcel.writeBlob(bytes);
        } else {
            writeToParcelForSAndBelow(parcel, bytes);
        }
    }

    private static void writeToParcelForSAndBelow(Parcel parcel, byte[] bytes) {
        try {
            parcel.writeInt(bytes.length);
            if (bytes.length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
                parcel.writeByteArray(bytes);
            } else {
                ParcelFileDescriptor parcelFileDescriptor = writeDataToTempFileAndUnlinkFile(bytes);
                parcel.writeFileDescriptor(parcelFileDescriptor.getFileDescriptor());
            }
        } catch (IOException e) {
            // TODO(b/232805516): Add abstraction to handle the exception based on environment.
            Log.w(TAG, "Couldn't write to unlinked file.", e);
        }
    }

    /** Calls parcel#readBlob on T+ and uses byte array or PFD on lower API levels. */
    @Nullable
    // TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
    @SuppressLint("ObsoleteSdkInt")
    public static byte[] readBlob(Parcel parcel) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Since parcel.readBlob was added in API level 33, it is not available
            // on lower API levels.
            return parcel.readBlob();
        } else {
            return readFromParcelForSAndBelow(parcel);
        }
    }

    @Nullable
    private static byte[] readFromParcelForSAndBelow(Parcel parcel) {
        try {
            int length = parcel.readInt();
            if (length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
                byte[] documentByteArray = new byte[length];
                parcel.readByteArray(documentByteArray);
                return documentByteArray;
            } else {
                ParcelFileDescriptor pfd = parcel.readFileDescriptor();
                return getDataFromFd(pfd, length);
            }
        } catch (IOException e) {
            // TODO(b/232805516): Add abstraction to handle the exception based on environment.
            Log.w(TAG, "Couldn't read from unlinked file.", e);
            return null;
        }
    }

    /**
     * Reads data bytes from file using provided FileDescriptor. It also closes the PFD so that will
     * delete the underlying file if it's the only reference left.
     *
     * @param pfd ParcelFileDescriptor for the file to read.
     * @param length Number of bytes to read from the file.
     */
    private static byte[] getDataFromFd(ParcelFileDescriptor pfd, int length) throws IOException {
        try (DataInputStream in =
                new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd))) {
            byte[] data = new byte[length];
            in.read(data);
            return data;
        }
    }

    /**
     * Writes to a temp file owned by the caller, then unlinks/deletes it, and returns an FD which
     * is the only remaining reference to the tmp file.
     *
     * @param data Data in the form of byte array to write to the file.
     */
    private static ParcelFileDescriptor writeDataToTempFileAndUnlinkFile(byte[] data)
            throws IOException {
        // TODO(b/232959004):  Update directory to app-specific cache dir instead of null.
        File unlinkedFile =
                File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX, /* directory= */ null);
        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(unlinkedFile))) {
            out.write(data);
            out.flush();
        }
        ParcelFileDescriptor parcelFileDescriptor =
                ParcelFileDescriptor.open(
                        unlinkedFile,
                        ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE);
        unlinkedFile.delete();
        return parcelFileDescriptor;
    }

    private ParcelableUtil() {}
}
