/*
 * Copyright (C) 2021 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.car.internal;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.builtin.os.SharedMemoryHelper;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.SharedMemory;
import android.util.Slog;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;

/**
 * Utility to pass any {@code Parcelable} through binder with automatic conversion into shared
 * memory when data size is too big.
 *
 * <p> This class can work by itself but child class can be useful to use a custom class to
 * interface with C++ world. For such usage, child class will only add its own {@code CREATOR} impl
 * and a constructor taking {@code Parcel in}.
 *
 * <p>For stable AIDL, this class also provides two methods for serialization
 * {@link #toLargeParcelable} and deserialization {@link #reconstructStableAIDLParcelable}.
 * Please check included test for the usage.
 *
 * <p>If the caller sends this class through binder, the caller must close this class after writing
 * to parcel, unless this class is used as the return value for a binder call. If this is used as
 * return value, the stored shared memory will be lost unless caller make a copy of the shared
 * memory file descriptor.
 *
 * <p>If the caller receives this class through binder, the caller must close this after reading the
 * data.
 */
public class LargeParcelable extends LargeParcelableBase {
    /**
     * Stable AIDL Parcelable should have this member with {@code ParcelFileDescriptor} to support
     * bigger payload passing over shared memory.
     */
    public static final String STABLE_AIDL_SHARED_MEMORY_MEMBER = "sharedMemoryFd";
    /**
     * Stable AIDL Parcelable has {@code readFromParcel(Parcel)} public method.
     */
    public static final String STABLE_AIDL_PARCELABLE_READ_FROM_PARCEL = "readFromParcel";

    private static final String TAG = LargeParcelable.class.getSimpleName();
    private static final boolean DBG_PAYLOAD = false;
    private static final boolean DBG_STABLE_AIDL_CLASS = false;

    // cannot set this final even if it is set only once in constructor as set is done in
    // deserialize call.
    private @Nullable Parcelable mParcelable;

    // This is shared across thread. As this is per class, use volatile to avoid adding
    // separate lock. If additional static volatile is added, a lock should be added.
    private static volatile WeakReference<ClassLoader> sClassLoader = null;

    /**
     * Sets {@code ClassLoader} for loading the {@Code Parcelable}. This should be done before
     * getting binder call. Default classloader may not recognize the Parcelable passed and relevant
     * classloader like package classloader should be set before getting any binder data.
     */
    public static void setClassLoader(ClassLoader loader) {
        sClassLoader = new WeakReference<>(loader);
    }

    public LargeParcelable(Parcel in) {
        super(in);
    }

    public LargeParcelable(Parcelable parcelable) {
        mParcelable = parcelable;
    }

    /**
     * Returns {@code Parcelable} carried by this instance.
     */
    public Parcelable getParcelable() {
        return mParcelable;
    }

    @Override
    protected void serialize(@NonNull Parcel dest, int flags) {
        int startPosition;
        if (DBG_PAYLOAD) {
            startPosition = dest.dataPosition();
        }
        dest.writeParcelable(mParcelable, flags);
        if (DBG_PAYLOAD) {
            Slog.d(TAG, "serialize-payload, start:" + startPosition
                    + " size:" + (dest.dataPosition() - startPosition));
        }
    }

    @Override
    protected void serializeNullPayload(@NonNull Parcel dest) {
        int startPosition;
        if (DBG_PAYLOAD) {
            startPosition = dest.dataPosition();
        }
        dest.writeParcelable(null, 0);
        if (DBG_PAYLOAD) {
            Slog.d(TAG, "serializeNullPayload-payload, start:" + startPosition
                    + " size:" + (dest.dataPosition() - startPosition));
        }
    }

    @Override
    protected void deserialize(@NonNull Parcel src) {
        // default class loader does not work as it may not be in boot class path.
        ClassLoader loader = (sClassLoader == null) ? null : sClassLoader.get();
        int startPosition;
        if (DBG_PAYLOAD) {
            startPosition = src.dataPosition();
        }
        mParcelable = src.readParcelable(loader);
        if (DBG_PAYLOAD) {
            Slog.d(TAG, "deserialize-payload, start:" + startPosition
                    + " size:" + (src.dataPosition() - startPosition)
                    + " mParcelable:" + mParcelable);
        }
    }

    public static final @NonNull Parcelable.Creator<LargeParcelable> CREATOR =
            new Parcelable.Creator<LargeParcelable>() {
                @Override
                public LargeParcelable[] newArray(int size) {
                    return new LargeParcelable[size];
                }

                @Override
                public LargeParcelable createFromParcel(@NonNull Parcel in) {
                    return new LargeParcelable(in);
                }
            };

    /**
     * A helper method to close a nullable {@link ParcelFileDescriptor}.
     *
     * Caller can use this method to close the {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} field
     * returned from {@link #toLargeParcelable}.
     *
     * For example:
     *
     * ```
     * var largeParcelable = toLargeParcelable(origParcelable);
     * try {
     *   sendData(largeParcelable);
     * } finally {
     *   closeFd(largeParcelable.sharedMemoryFd);
     * }
     * ```
     */
    public static void closeFd(@Nullable ParcelFileDescriptor fd) {
        if (fd == null) {
            return;
        }
        try {
            fd.close();
        } catch (IOException e) {
            Slog.e(TAG, "Failed to close shared memory fd from large parcelable", e);
        }
    }

    /**
     * @see #toLargeParcelable(Parcelable, Callable<Parcelable>)
     *
     * <p>The returned Parcelable may contain a {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} field that
     * must be closed after send over binder thread.
     *
     * <p>If the returned Parcelable is the return value for a binder call, then binder library will
     * close the sharedMemoryFd field when writing to Parcel and the caller does not need (and does
     * not have a way) to close it.
     */
    @Nullable
    public static Parcelable toLargeParcelable(@Nullable Parcelable p) {
        return toLargeParcelable(p, null);
    }

    /**
     * Prepare a {@code Parcelable} defined from Stable AIDL to be able to sent through binder.
     *
     * <p>The {@code Parcelable} should have a public member having name of
     * {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} with {@code ParcelFileDescriptor} type.
     *
     * <p>If payload size is big, the input would be serialized to a shared memory file and a
     * an empty {@code Parcelable} with only the file descriptor set would be returned. If the
     * payload size is small enough to be sent across binder or the input already contains a shared
     * memory file, the original input would be returned.
     *
     * <p>The returned Parcelable may contain a {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} field that
     * must be closed after send over binder thread.
     *
     * <p>If the returned Parcelable is the return value for a binder call, then binder library will
     * close the sharedMemoryFd field when writing to Parcel and the caller does not need (and does
     * not have a way) to close it.
     *
     * @param p {@code Parcelable} the input to convert that might contain large data.
     * @param constructEmptyParcelable a callable to create an empty Parcelable with the same type
     *      as input. If this is null, the default initializer for the input type would be used.
     * @return a {@code Parcelable} that could be sent through binder despite memory limitation.
     */
    @Nullable
    public static Parcelable toLargeParcelable(
            @Nullable Parcelable p, @Nullable Callable<Parcelable> constructEmptyParcelable) {
        if (p == null) {
            return null;
        }
        Class parcelableClass = p.getClass();
        if (DBG_STABLE_AIDL_CLASS) {
            Slog.d(TAG, "toLargeParcelable stable AIDL Parcelable:"
                    + parcelableClass.getSimpleName());
        }
        Field field;
        ParcelFileDescriptor sharedMemoryFd;
        try {
            field = parcelableClass.getField(STABLE_AIDL_SHARED_MEMORY_MEMBER);
            sharedMemoryFd = (ParcelFileDescriptor) field.get(p);
        } catch (Exception e) {
            throw new IllegalArgumentException("Cannot access " + STABLE_AIDL_SHARED_MEMORY_MEMBER,
                    e);
        }
        if (sharedMemoryFd != null) {
            return p;
        }
        Parcel dataParcel = Parcel.obtain();
        p.writeToParcel(dataParcel, 0);
        int payloadSize = dataParcel.dataSize();
        if (payloadSize <= LargeParcelableBase.MAX_DIRECT_PAYLOAD_SIZE) {
            // direct path, no re-write to shared memory.
            if (DBG_PAYLOAD) {
                Slog.d(TAG, "toLargeParcelable send directly, payload size:" + payloadSize);
            }
            return p;
        }

        try (SharedMemory memory = LargeParcelableBase.serializeParcelToSharedMemory(dataParcel)) {
            dataParcel.recycle();
            sharedMemoryFd = SharedMemoryHelper.createParcelFileDescriptor(memory);
        } catch (IOException e) {
            throw new IllegalArgumentException("unable to duplicate shared memory fd", e);
        }

        Parcelable emptyPayload;
        if (constructEmptyParcelable != null) {
            try {
                emptyPayload = constructEmptyParcelable.call();
            } catch (Exception e) {
                throw new IllegalArgumentException("Cannot use Parcelable constructor", e);
            }
        } else {
            try {
                emptyPayload = (Parcelable) parcelableClass.newInstance();
            } catch (Exception e) {
                throw new IllegalArgumentException("Cannot access Parcelable constructor", e);
            }
        }

        try {
            field.set(emptyPayload, sharedMemoryFd);
        } catch (Exception e) {
            throw new IllegalArgumentException("Cannot access Parcelable member for FD", e);
        }

        return emptyPayload;
    }

    /**
     * Reconstructs {@code Parcelable} defined from Stable AIDL. It will create
     * a new {@code Parcelable} if the shared memory fd is not null. If the shared memory fd is
     * null, it will return the original {@code Parcelable p} as it is.
     *
     * <p>The sharedMemoryFd field in the input will be closed inside this function,
     *
     * <p>This version is faster than {@link #reconstructStableAIDLParcelable(Parcelable, boolean)}
     * by avoid using reflection.
     *
     * @param p                 Original {@code Parcelable} containing the payload.
     * @param sharedMemoryFd    The file descriptor to the shared memory, this is the
     * {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} field inside the parcelable.
     * @param parcelableCreator A creator used to create the parcelable from a parcel.
     * @param <T>               The parcelable class.
     * @return a new {@code Parcelable} if shared memory fd is not null or the original parcelable.
     */
    public static <T extends Parcelable> @Nullable T reconstructStableAIDLParcelable(@Nullable T p,
            @Nullable ParcelFileDescriptor sharedMemoryFd,
            Parcelable.Creator<T> parcelableCreator) {
        return reconstructStableAIDLParcelable(p, sharedMemoryFd, parcelableCreator,
                /* sharedMemorySetter= */ null);
    }

    /**
     * Reconstructs {@code Parcelable} defined from Stable AIDL. It should have a {@link
     * ParcelFileDescriptor} member named {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} and will create
     * a new {@code Parcelable} if the shared memory portion is not null. If there is no shared
     * memory, it will return the original {@code Parcelable p} as it is.
     *
     * <p>The {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} field, if exists, will be closed inside
     * this function.
     *
     * <p>If keepSharedMemory is true, the caller must close the returned shared memory after using.
     *
     * <p>This method uses reflection to find the fields inside the input parcelable so must not
     * be used for performance critical task.
     *
     * @param p                Original {@code Parcelable} containing the payload.
     * @param keepSharedMemory Whether to keep created shared memory in the returned {@code
     *                         Parcelable}. Set to {@code true} if this {@code Parcelable} is sent
     *                         across binder repeatedly.
     * @param <T>              The parcelable class.
     * @return a new {@code Parcelable} if payload read from shared memory or old one if payload
     * is small enough.
     */
    public static <T extends Parcelable> @Nullable T reconstructStableAIDLParcelable(
            @Nullable T p, boolean keepSharedMemory) {
        if (p == null) {
            return null;
        }
        ParcelFileDescriptor sharedMemoryFd = null;
        Field fieldSharedMemory;
        try {
            fieldSharedMemory = p.getClass().getField(STABLE_AIDL_SHARED_MEMORY_MEMBER);
            sharedMemoryFd = (ParcelFileDescriptor) fieldSharedMemory.get(p);
        } catch (Exception e) {
            throw new IllegalArgumentException("Cannot access " + STABLE_AIDL_SHARED_MEMORY_MEMBER,
                    e);
        }
        ReflectionStableParcelableCreator<T> creator;
        try {
            creator = new ReflectionStableParcelableCreator(p);
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(
                    "The input parcelable is not a stabke parcelable, cannot find readFromParcel "
                    + "method", e);
        }
        ReflectionSharedMemorySetter<T> sharedMemorySetter = null;
        if (keepSharedMemory) {
            sharedMemorySetter = new ReflectionSharedMemorySetter(fieldSharedMemory);
        }
        return reconstructStableAIDLParcelable(p, sharedMemoryFd, creator, sharedMemorySetter);
    }

    private static ParcelFileDescriptor dupFd(ParcelFileDescriptor fd) {
        try {
            return fd.dup();
        } catch (IOException e) {
            throw new IllegalStateException("Failed to duplicate shared memory fd", e);
        }
    }

    private static <T extends Parcelable> @Nullable T reconstructStableAIDLParcelable(@Nullable T p,
            @Nullable ParcelFileDescriptor sharedMemoryFd, Parcelable.Creator<T> parcelableCreator,
            @Nullable SharedMemorySetter sharedMemorySetter) {
        if (sharedMemoryFd == null) {
            if (DBG_PAYLOAD) {
                Slog.d(TAG, "reconstructStableAIDLParcelable null shared memory");
            }
            return p;
        }
        if (p == null) {
            return null;
        }
        if (DBG_STABLE_AIDL_CLASS) {
            Slog.d(TAG, "reconstructStableAIDLParcelable stable AIDL Parcelable:"
                    + p.getClass().getSimpleName());
        }
        Parcel in = null;
        T retParcelable = null;
        try {
            // SharedMemory.fromFileDescriptor take ownership, so we need to dupe to keep
            // sharedMemoryFd the Parcelable valid.
            try (SharedMemory memory = SharedMemory.fromFileDescriptor(dupFd(sharedMemoryFd))) {
                in = LargeParcelableBase.copyFromSharedMemory(memory);
            }

            retParcelable = parcelableCreator.createFromParcel(in);
            if (sharedMemorySetter != null) {
                sharedMemorySetter.set(retParcelable, dupFd(sharedMemoryFd));
            }

            if (DBG_PAYLOAD) {
                Slog.d(TAG, "reconstructStableAIDLParcelable read shared memory, data size:"
                        + in.dataPosition());
            }
        } finally {
            closeFd(sharedMemoryFd);
            if (in != null) {
                in.recycle();
            }
        }
        return retParcelable;
    }

    /**
     * An implementation for {@code createFromParcel} by using reflection to get the
     * {@link STABLE_AIDL_PARCELABLE_READ_FROM_PARCEL} method from the parcelable and inoke it.
     */
    private static final class ReflectionStableParcelableCreator<T extends Parcelable>
            implements Parcelable.Creator<T> {
        private final Class<?> mParcelableClass;
        private final Method mReadMethod;

        ReflectionStableParcelableCreator(T p) throws NoSuchMethodException {
            mParcelableClass = p.getClass();
            mReadMethod = mParcelableClass.getMethod(
                    STABLE_AIDL_PARCELABLE_READ_FROM_PARCEL, new Class[]{Parcel.class});
        }

        @Override
        public T createFromParcel(Parcel source) {
            try {
                T p = (T) mParcelableClass.getDeclaredConstructor().newInstance();
                // runs p.readFromParcel(source). readFromParcel is a method implemented
                // by every stable Parcelable class, but not exposed directly from Parcelable.
                mReadMethod.invoke(p, source);
                return p;
            } catch (IllegalAccessException | InvocationTargetException | InstantiationException
                    | NoSuchMethodException e) {
                throw new IllegalArgumentException(
                        "Cannot access the input Parcelable's readFromParcel/constructor method",
                        e);
            }
        }

        @Override
        public T[] newArray(int size) {
            throw new UnsupportedOperationException("This must never be used");
        }
    }

    /**
     * An interface to set the 'sharedMemoryFd' field inside a parcelable.
     */
    private interface SharedMemorySetter<T extends Parcelable> {
        void set(T parcelable, ParcelFileDescriptor sharedMemoryFd);
    }

    /**
     * An implementation for SharedMemorySetter using reflection to find the field.
     */
    private static final class ReflectionSharedMemorySetter<T extends Parcelable>
            implements SharedMemorySetter<T> {
        private final Field mFieldSharedMemory;

        ReflectionSharedMemorySetter(Field fieldSharedMemory) {
            mFieldSharedMemory = fieldSharedMemory;
        }

        @Override
        public void set(T parcelable, ParcelFileDescriptor sharedMemoryFd) {
            try {
                mFieldSharedMemory.set(parcelable, sharedMemoryFd);
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException(
                        "Cannot access the input Parcelable's sharedMemoryFd field", e);
            }
        }
    }
}
