/*
 * Copyright (C) 2020 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.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.util.MathUtils;

/**
 * ParcelableHolder is a Parcelable which can contain another Parcelable.
 * The main use case of ParcelableHolder is to make a Parcelable extensible.
 * For example, an AOSP-defined Parcelable <code>AospDefinedParcelable</code>
 * is expected to be extended by device implementers for their value-add features.
 * Previously without ParcelableHolder, the device implementers had to
 * directly modify the Parcelable to add more fields:
 * <pre> {@code
 * parcelable AospDefinedParcelable {
 *   int a;
 *   String b;
 *   String x; // added by a device implementer
 *   int[] y; // added by a device implementer
 * }}</pre>
 *
 * This practice is very error-prone because the fields added by the device implementer
 * might have a conflict when the Parcelable is revisioned in the next releases of Android.
 *
 * Using ParcelableHolder, one can define an extension point in a Parcelable.
 * <pre> {@code
 * parcelable AospDefinedParcelable {
 *   int a;
 *   String b;
 *   ParcelableHolder extension;
 * }}</pre>
 * Then the device implementers can define their own Parcelable for their extension.
 *
 * <pre> {@code
 * parcelable OemDefinedParcelable {
 *   String x;
 *   int[] y;
 * }}</pre>
 * Finally, the new Parcelable can be attached to the original Parcelable via
 * the ParcelableHolder field.
 *
 * <pre> {@code
 * AospDefinedParcelable ap = ...;
 * OemDefinedParcelable op = new OemDefinedParcelable();
 * op.x = ...;
 * op.y = ...;
 * ap.extension.setParcelable(op);}</pre>
 *
 * <p class="note">ParcelableHolder is <strong>not</strong> thread-safe.</p>
 *
 * @hide
 */
@SystemApi
public final class ParcelableHolder implements Parcelable {
    /**
     * This is set by {@link #setParcelable}.
     * {@link #mParcelable} and {@link #mParcel} are mutually exclusive
     * if {@link ParcelableHolder} contains value, otherwise, both are null.
     */
    private Parcelable mParcelable;
    /**
     * This is set by {@link #readFromParcel}.
     * {@link #mParcelable} and {@link #mParcel} are mutually exclusive
     * if {@link ParcelableHolder} contains value, otherwise, both are null.
     */
    private Parcel mParcel;
    private @Parcelable.Stability int mStability = Parcelable.PARCELABLE_STABILITY_LOCAL;

    public ParcelableHolder(@Parcelable.Stability int stability) {
        mStability = stability;
    }

    private ParcelableHolder() {

    }

    /**
     * {@link ParcelableHolder}'s stability is determined by the parcelable
     * which contains this ParcelableHolder.
     * For more detail refer to {@link Parcelable#getStability}.
     */
    @Override
    public @Parcelable.Stability int getStability() {
        return mStability;
    }

    @NonNull
    public static final Parcelable.Creator<ParcelableHolder> CREATOR =
            new Parcelable.Creator<ParcelableHolder>() {
                @NonNull
                @Override
                public ParcelableHolder createFromParcel(@NonNull Parcel parcel) {
                    ParcelableHolder parcelable = new ParcelableHolder();
                    parcelable.readFromParcel(parcel);
                    return parcelable;
                }

                @NonNull
                @Override
                public ParcelableHolder[] newArray(int size) {
                    return new ParcelableHolder[size];
                }
            };


    /**
     * Write a parcelable into ParcelableHolder, the previous parcelable will be removed.
     * (@link #setParcelable} and (@link #getParcelable} are not thread-safe.
     * @throws BadParcelableException if the parcelable's stability is more unstable
     *         ParcelableHolder.
     */
    public void setParcelable(@Nullable Parcelable p) {
        // A ParcelableHolder can only hold things at its stability or higher.
        if (p != null && this.getStability() > p.getStability()) {
            throw new BadParcelableException(
                "A ParcelableHolder can only hold things at its stability or higher. "
                + "The ParcelableHolder's stability is " + this.getStability()
                + ", but the parcelable's stability is " + p.getStability());
        }
        mParcelable = p;
        if (mParcel != null) {
            mParcel.recycle();
            mParcel = null;
        }
    }

    /**
     * Read a parcelable from ParcelableHolder.
     * (@link #setParcelable} and (@link #getParcelable} are not thread-safe.
     * @return the parcelable that was written by {@link #setParcelable} or {@link #readFromParcel},
     *         or {@code null} if the parcelable has not been written.
     * @throws BadParcelableException if T is different from the type written by
     *         (@link #setParcelable}.
     */
    @Nullable
    public <T extends Parcelable> T getParcelable(@NonNull Class<T> clazz) {
        if (mParcel == null) {
            if (mParcelable != null && !clazz.isInstance(mParcelable)) {
                throw new BadParcelableException(
                    "The ParcelableHolder has " + mParcelable.getClass().getName()
                    + ", but the requested type is " + clazz.getName());
            }
            return (T) mParcelable;
        }

        mParcel.setDataPosition(0);

        T parcelable = mParcel.readParcelable(clazz.getClassLoader());
        if (parcelable != null && !clazz.isInstance(parcelable)) {
            throw new BadParcelableException(
                    "The ParcelableHolder has " + parcelable.getClass().getName()
                    + ", but the requested type is " + clazz.getName());
        }
        mParcelable = parcelable;

        mParcel.recycle();
        mParcel = null;
        return parcelable;
    }

    /**
     * Read ParcelableHolder from a parcel.
     */
    public void readFromParcel(@NonNull Parcel parcel) {
        this.mStability = parcel.readInt();

        mParcelable = null;

        int dataSize = parcel.readInt();
        if (dataSize < 0) {
            throw new IllegalArgumentException("dataSize from parcel is negative");
        } else if (dataSize == 0) {
            if (mParcel != null) {
                mParcel.recycle();
                mParcel = null;
            }
            return;
        }
        if (mParcel == null) {
            mParcel = Parcel.obtain();
        }
        mParcel.setDataPosition(0);
        mParcel.setDataSize(0);
        int dataStartPos = parcel.dataPosition();

        mParcel.appendFrom(parcel, dataStartPos, dataSize);
        parcel.setDataPosition(MathUtils.addOrThrow(dataStartPos, dataSize));
    }

    @Override
    public void writeToParcel(@NonNull Parcel parcel, int flags) {
        parcel.writeInt(this.mStability);

        if (mParcel != null) {
            parcel.writeInt(mParcel.dataSize());
            parcel.appendFrom(mParcel, 0, mParcel.dataSize());
            return;
        }

        if (mParcelable == null) {
            parcel.writeInt(0);
            return;
        }

        int sizePos = parcel.dataPosition();
        parcel.writeInt(0);
        int dataStartPos = parcel.dataPosition();
        parcel.writeParcelable(mParcelable, 0);
        int dataSize = parcel.dataPosition() - dataStartPos;

        parcel.setDataPosition(sizePos);
        parcel.writeInt(dataSize);
        parcel.setDataPosition(MathUtils.addOrThrow(parcel.dataPosition(), dataSize));
    }

    @Override
    public int describeContents() {
        if (mParcel != null) {
            return mParcel.hasFileDescriptors() ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
        }
        if (mParcelable != null) {
            return mParcelable.describeContents();
        }
        return 0;
    }
}
