/*
 * 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 static android.system.OsConstants.PROT_READ;

import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SharedMemory;
import android.system.ErrnoException;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

import java.io.Closeable;
import java.nio.ByteBuffer;

/**
 * Base class to allow passing {@code Parcelable} over binder directly or through shared memory if
 * payload size is too big.
 *
 * <p>Child class should inherit this to use this or use {@link LargeParcelable} class.
 *
 * <p>Parcelized data will have following elements
 * <ul>
 * <li>@Nullable Parcelable
 * <li>@Nullable SharedMemory which include serialized Parcelable if non-null. This will be set
 * only when the previous Parcelable is null or this also can be null for no data case.
 * </ul>
 *
 * @hide
 */
public abstract class LargeParcelableBase implements Parcelable, Closeable {
    /** Payload size bigger than this value will be passed over shared memory. */
    public static final int MAX_DIRECT_PAYLOAD_SIZE = 4096;
    private static final String TAG = LargeParcelable.class.getSimpleName();

    private static final boolean DBG_PAYLOAD = Log.isLoggable(TAG, Log.DEBUG);
    private static final int DBG_DUMP_LENGTH = 16;

    private static final int NULL_PAYLOAD = 0;
    private static final int NONNULL_PAYLOAD = 1;
    private static final int FD_HEADER = 0;

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private @Nullable SharedMemory mSharedMemory;

    /**
     * Serialize (=write Parcelable into given Parcel) a {@code Parcelable} child class wants to
     * pass over binder call.
     */
    protected abstract void serialize(@NonNull Parcel dest, int flags);

    /**
     * Serialize null payload to the given {@code Parcel}. For {@code Parcelable}, this can be
     * simply {@code dest.writeParcelable(null)} but non-Parcelable should have other way to
     * mark that there is no payload.
     */
    protected abstract void serializeNullPayload(@NonNull Parcel dest);

    /**
     * Read a {@code Parcelable} from the given {@code Parcel}.
     */
    protected abstract void deserialize(@NonNull Parcel src);

    public LargeParcelableBase() {
    }

    public LargeParcelableBase(Parcel in) {
        // Make this compatible with stable AIDL
        // payload size + Parcelable / payload + 1:has shared memory + 0 + file
        //                                       0:no shared memory
        // 0 + file makes it compatible with ParcelFileDescrpitor
        // file contains:
        // file size + Parcelable / payload + 0
        int startPosition = in.dataPosition();
        int totalPayloadSize = in.readInt();
        deserialize(in);
        int sharedMemoryPosition = in.dataPosition();
        boolean hasSharedMemory = (in.readInt() != NULL_PAYLOAD);
        if (hasSharedMemory) {
            int fdHeader = in.readInt();
            if (fdHeader != FD_HEADER) {
                throw new IllegalArgumentException(
                        "Invalid data, wrong fdHeader, expected 0 while got " + fdHeader);
            }
            SharedMemory memory = SharedMemory.CREATOR.createFromParcel(in);
            deserializeSharedMemoryAndClose(memory);
        }
        in.setDataPosition(startPosition + totalPayloadSize);
        if (DBG_PAYLOAD) {
            Slog.d(TAG, "Read, start:" + startPosition + " totalPayloadSize:" + totalPayloadSize
                    + " sharedMemoryPosition:" + sharedMemoryPosition
                    + " hasSharedMemory:" + hasSharedMemory + " dataAvail:" + in.dataAvail());
        }
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        int startPosition = dest.dataPosition();
        SharedMemory sharedMemory;
        synchronized (mLock) {
            sharedMemory = mSharedMemory;
        }
        int totalPayloadSize = 0;
        if (sharedMemory != null) {
            // optimized path for resending the same Parcelable multiple times with already
            // created shared memory
            totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dest, flags, sharedMemory);
            if (DBG_PAYLOAD) {
                Slog.d(TAG, "Write, reusing shared memory, start:" + startPosition
                        + " totalPayloadSize:" + totalPayloadSize);
            }
            return;
        }

        // dataParcel is the parcel that would be serialized to the shared memory file.
        Parcel dataParcel = Parcel.obtain();
        totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dataParcel, flags, null);

        boolean noSharedMemory = totalPayloadSize <= MAX_DIRECT_PAYLOAD_SIZE;
        boolean hasNonNullPayload = true;
        if (noSharedMemory) {
            if (DBG_PAYLOAD) {
                Slog.d(TAG, "not using shared memory");
            }
            dest.appendFrom(dataParcel, 0, totalPayloadSize);
            dataParcel.recycle();
        } else {
            if (DBG_PAYLOAD) {
                Slog.d(TAG, "using shared memory");
            }
            sharedMemory = serializeParcelToSharedMemory(dataParcel);
            dataParcel.recycle();
            synchronized (mLock) {
                // If it is already set, let sharedMemory go and GV will close it later.
                // This is ok as this kind of race should not happen often.
                if (mSharedMemory != null) {
                    mSharedMemory = sharedMemory;
                }
            }

            totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dest, flags, sharedMemory);
        }
        if (DBG_PAYLOAD) {
            Slog.d(TAG, "Write, start:" + startPosition + " totalPayloadSize:" + totalPayloadSize
                    + " hasNonNullPayload:" + hasNonNullPayload
                    + " hasSharedMemory:" + !noSharedMemory + " dataSize:" + dest.dataSize());
        }
    }

    private int updatePayloadSize(Parcel dest, int startPosition) {
        int lastPosition = dest.dataPosition();
        int totalPayloadSize = lastPosition - startPosition;
        dest.setDataPosition(startPosition);
        dest.writeInt(totalPayloadSize);
        dest.setDataPosition(lastPosition);
        dest.setDataSize(lastPosition);
        return totalPayloadSize;
    }

    // Write shared memory in compatible way with ParcelFileDescriptor
    private void writeSharedMemoryCompatibleToParcel(Parcel dest, SharedMemory memory, int flags) {
        // dest.writeParcelable() adds class type which makes it incompatible with C++.
        if (memory == null) {
            dest.writeInt(NULL_PAYLOAD);
            return;
        }
        // non-null case
        dest.writeInt(NONNULL_PAYLOAD);
        dest.writeInt(FD_HEADER); // additional header for ParcelFileDescriptor
        memory.writeToParcel(dest, flags);
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
    public int describeContents() {
        return 0;
    }

    /**
     * {@inheritDoc}
     *
     * <p>Close the underlying shared memory for this. This can be called multiple times safely.
     * When this is not called explicitly, it will be closed when this instance is GCed.
     * Calling this can be useful when many instances are created frequently.
     *
     * <p>If underlying payload is changed, the client should call this before sending it over
     * binder as sending it over binder can keep shared memory generated from the previous binder
     * call.
     */
    @Override
    public void close() {
        SharedMemory sharedMemory = null;
        synchronized (mLock) {
            sharedMemory = mSharedMemory;
            mSharedMemory = null;
        }
        if (sharedMemory != null) {
            sharedMemory.close();
        }
    }

    protected static SharedMemory serializeParcelToSharedMemory(Parcel p) {
        SharedMemory memory = null;
        ByteBuffer buffer = null;
        int size = p.dataSize();
        try {
            memory = SharedMemory.create(LargeParcelableBase.class.getSimpleName(), size);
            buffer = memory.mapReadWrite();
            byte[] data = p.marshall();
            buffer.put(data, 0, size);
            if (DBG_PAYLOAD) {
                int dumpSize = Math.min(DBG_DUMP_LENGTH, data.length);
                StringBuilder bd = new StringBuilder();
                bd.append("marshalled:");
                for (int i = 0; i < dumpSize; i++) {
                    bd.append(data[i]);
                    if (i != dumpSize - 1) {
                        bd.append(',');
                    }
                }
                bd.append("=memory:");
                for (int i = 0; i < dumpSize; i++) {
                    bd.append(buffer.get(i));
                    if (i != dumpSize - 1) {
                        bd.append(',');
                    }
                }
                Slog.d(TAG, bd.toString());
            }
            if (!memory.setProtect(PROT_READ)) {
                memory.close();
                throw new SecurityException("Failed to set read-only protection on shared memory");
            }
        } catch (ErrnoException e) {
            throw new IllegalArgumentException("Failed to use shared memory", e);
        } catch (Exception e) {
            throw new IllegalArgumentException("failed to serialize", e);
        } finally {
            if (buffer != null) {
                SharedMemory.unmap(buffer);
            }
        }

        return memory;
    }

    protected static Parcel copyFromSharedMemory(SharedMemory memory) {
        ByteBuffer buffer = null;
        Parcel in = Parcel.obtain();
        try {
            buffer = memory.mapReadOnly();
            // TODO(b/188781089) find way to avoid this additional copy
            byte[] payload = new byte[buffer.limit()];
            buffer.get(payload);
            in.unmarshall(payload, 0, payload.length);
            in.setDataPosition(0);
            if (DBG_PAYLOAD) {
                int dumpSize = Math.min(DBG_DUMP_LENGTH, payload.length);
                StringBuilder bd = new StringBuilder();
                bd.append("unmarshalled:");
                int parcelStartPosition = in.dataPosition();
                byte[] fromParcel = in.marshall();
                for (int i = 0; i < dumpSize; i++) {
                    bd.append(fromParcel[i]);
                    if (i != dumpSize - 1) bd.append(',');
                }
                bd.append("=startPosition:");
                bd.append(parcelStartPosition);
                bd.append("=memory:");
                for (int i = 0; i < dumpSize; i++) {
                    bd.append(buffer.get(i));
                    if (i != dumpSize - 1) bd.append(',');
                }
                bd.append("=interim_payload:");
                for (int i = 0; i < dumpSize; i++) {
                    bd.append(payload[i]);
                    if (i != dumpSize - 1) bd.append(',');
                }
                Slog.d(TAG, bd.toString());
                in.setDataPosition(parcelStartPosition);
            }
        } catch (ErrnoException e) {
            throw new IllegalArgumentException("cannot create Parcelable from SharedMemory", e);
        } catch (Exception e) {
            throw new IllegalArgumentException("failed to deserialize", e);
        } finally {
            if (buffer != null) {
                SharedMemory.unmap(buffer);
            }
        }
        return in;
    }

    private void deserializeSharedMemoryAndClose(SharedMemory memory) {
        // The shared memory file contains a serialized largeParcelable.
        // size + payload + 0 (no shared memory).
        Parcel in = null;
        try {
            in = copyFromSharedMemory(memory);
            // Even if we don't need the file size, we have to read it from the parcel to advance
            // the data position.
            int fileSize = in.readInt();
            if (DBG_PAYLOAD) {
                Slog.d(TAG, "file size in shared memory file: " + fileSize);
            }
            deserialize(in);
            // There is an additional 0 in the parcel, but we ignore that.
        } finally {
            memory.close();
            if (in != null) {
                in.recycle();
            }
        }
    }

    // If sharedMemory is not null, serialize null payload and shared memory to parcel.
    // Otherwise, serialize the actual payload to parcel.
    private int serializeMemoryFdOrPayloadToParcel(
            Parcel dest, int flags, @Nullable SharedMemory sharedMemory) {
        int startPosition = dest.dataPosition();
        dest.writeInt(0); // payload size

        if (sharedMemory != null) {
            serializeNullPayload(dest);
            writeSharedMemoryCompatibleToParcel(dest, sharedMemory, flags);
        } else {
            serialize(dest, flags);
            writeSharedMemoryCompatibleToParcel(dest, null, flags);
        }

        return updatePayloadSize(dest, startPosition);
    }
}
