/*
 * Copyright (C) 2022 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.view;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.InsetsSource.Flags;
import android.view.WindowInsets.Type.InsetsType;

import java.util.Arrays;
import java.util.Objects;

/**
 * Insets provided by a window.
 *
 * The insets frame will by default as the window frame size. If the providers are set, the
 * calculation result based on the source size will be used as the insets frame.
 *
 * The InsetsFrameProvider should be self-contained. Nothing describing the window itself, such as
 * contentInsets, visibleInsets, etc. won't affect the insets providing to other windows when this
 * is set.
 * @hide
 */
public class InsetsFrameProvider implements Parcelable {

    /**
     * Uses the display frame as the source.
     */
    public static final int SOURCE_DISPLAY = 0;

    /**
     * Uses the window bounds as the source.
     */
    public static final int SOURCE_CONTAINER_BOUNDS = 1;

    /**
     * Uses the window frame as the source.
     */
    public static final int SOURCE_FRAME = 2;

    /**
     * Uses {@link #mArbitraryRectangle} as the source.
     */
    public static final int SOURCE_ARBITRARY_RECTANGLE = 3;

    private final int mId;

    /**
     * The selection of the starting rectangle to be converted into source frame.
     */
    private int mSource = SOURCE_FRAME;

    /**
     * This is used as the source frame only if SOURCE_ARBITRARY_RECTANGLE is applied.
     */
    private Rect mArbitraryRectangle;

    /**
     * Modifies the starting rectangle selected by {@link #mSource}.
     *
     * For example, when the given source frame is (0, 0) - (100, 200), and the insetsSize is null,
     * the source frame will be directly used as the final insets frame. If the insetsSize is set to
     * (0, 0, 0, 50) instead, the insets frame will be a frame starting from the bottom side of the
     * source frame with height of 50, i.e., (0, 150) - (100, 200).
     */
    private Insets mInsetsSize = null;

    /**
     * Various behavioral options/flags. Default is none.
     *
     * @see Flags
     */
    private @Flags int mFlags;

    /**
     * If null, the size set in insetsSize will be applied to all window types. If it contains
     * element of some types, the insets reported to the window with that types will be overridden.
     */
    private InsetsSizeOverride[] mInsetsSizeOverrides = null;

    /**
     * This field, if set, is indicating the insets needs to be at least the given size inside the
     * display cutout safe area. This will be compared to the insets size calculated based on other
     * attributes, and will be applied when this is larger. This is independent of the
     * PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT in LayoutParams, as this is not going to change
     * the layout of the window, but only change the insets frame. This can be applied to insets
     * calculated based on all three source frames.
     *
     * Be cautious, this will not be in effect for the window types whose insets size is overridden.
     */
    private Insets mMinimalInsetsSizeInDisplayCutoutSafe = null;

    /**
     * Indicates the bounding rectangles within the provided insets frame, in relative coordinates
     * to the source frame.
     */
    private Rect[] mBoundingRects = null;

    /**
     * Creates an InsetsFrameProvider which describes what frame an insets source should have.
     *
     * @param owner the owner of this provider. We might have multiple sources with the same type on
     *              a display, this is used to identify them.
     * @param index the index of this provider. An owner might provide multiple sources with the
     *              same type, this is used to identify them.
     *              The value must be in a range of [0, 2047].
     * @param type the {@link InsetsType}.
     * @see InsetsSource#createId(Object, int, int)
     */
    public InsetsFrameProvider(Object owner, @IntRange(from = 0, to = 2047) int index,
            @InsetsType int type) {
        mId = InsetsSource.createId(owner, index, type);
    }

    /**
     * Returns an unique integer which identifies the insets source.
     */
    public int getId() {
        return mId;
    }

    /**
     * Returns the index specified in {@link #InsetsFrameProvider(IBinder, int, int)}.
     */
    public int getIndex() {
        return InsetsSource.getIndex(mId);
    }

    /**
     * Returns the {@link InsetsType} specified in {@link #InsetsFrameProvider(IBinder, int, int)}.
     */
    public int getType() {
        return InsetsSource.getType(mId);
    }

    public InsetsFrameProvider setSource(int source) {
        mSource = source;
        return this;
    }

    public int getSource() {
        return mSource;
    }

    public InsetsFrameProvider setFlags(@Flags int flags, @Flags int mask) {
        mFlags = (mFlags & ~mask) | (flags & mask);
        return this;
    }

    public @Flags int getFlags() {
        return mFlags;
    }

    public boolean hasFlags(@Flags int mask) {
        return (mFlags & mask) == mask;
    }

    public InsetsFrameProvider setInsetsSize(Insets insetsSize) {
        mInsetsSize = insetsSize;
        return this;
    }

    public Insets getInsetsSize() {
        return mInsetsSize;
    }

    public InsetsFrameProvider setArbitraryRectangle(Rect rect) {
        mArbitraryRectangle = new Rect(rect);
        return this;
    }

    public Rect getArbitraryRectangle() {
        return mArbitraryRectangle;
    }

    public InsetsFrameProvider setInsetsSizeOverrides(InsetsSizeOverride[] insetsSizeOverrides) {
        mInsetsSizeOverrides = insetsSizeOverrides;
        return this;
    }

    public InsetsSizeOverride[] getInsetsSizeOverrides() {
        return mInsetsSizeOverrides;
    }

    public InsetsFrameProvider setMinimalInsetsSizeInDisplayCutoutSafe(
            Insets minimalInsetsSizeInDisplayCutoutSafe) {
        mMinimalInsetsSizeInDisplayCutoutSafe = minimalInsetsSizeInDisplayCutoutSafe;
        return this;
    }

    public Insets getMinimalInsetsSizeInDisplayCutoutSafe() {
        return mMinimalInsetsSizeInDisplayCutoutSafe;
    }

    /**
     * Sets the bounding rectangles within and relative to the source frame.
     */
    public InsetsFrameProvider setBoundingRects(@Nullable Rect[] boundingRects) {
        mBoundingRects = boundingRects == null ? null : boundingRects.clone();
        return this;
    }

    /**
     * Returns the arbitrary bounding rects, or null if none were set.
     */
    @Nullable
    public Rect[] getBoundingRects() {
        return mBoundingRects;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("InsetsFrameProvider: {");
        sb.append("id=#").append(Integer.toHexString(mId));
        sb.append(", index=").append(getIndex());
        sb.append(", type=").append(WindowInsets.Type.toString(getType()));
        sb.append(", source=").append(sourceToString(mSource));
        sb.append(", flags=[").append(InsetsSource.flagsToString(mFlags)).append("]");
        if (mInsetsSize != null) {
            sb.append(", insetsSize=").append(mInsetsSize);
        }
        if (mInsetsSizeOverrides != null) {
            sb.append(", insetsSizeOverrides=").append(Arrays.toString(mInsetsSizeOverrides));
        }
        if (mArbitraryRectangle != null) {
            sb.append(", mArbitraryRectangle=").append(mArbitraryRectangle.toShortString());
        }
        if (mMinimalInsetsSizeInDisplayCutoutSafe != null) {
            sb.append(", mMinimalInsetsSizeInDisplayCutoutSafe=")
                    .append(mMinimalInsetsSizeInDisplayCutoutSafe);
        }
        if (mBoundingRects != null) {
            sb.append(", mBoundingRects=").append(Arrays.toString(mBoundingRects));
        }
        sb.append("}");
        return sb.toString();
    }

    private static String sourceToString(int source) {
        switch (source) {
            case SOURCE_DISPLAY:
                return "DISPLAY";
            case SOURCE_CONTAINER_BOUNDS:
                return "CONTAINER_BOUNDS";
            case SOURCE_FRAME:
                return "FRAME";
            case SOURCE_ARBITRARY_RECTANGLE:
                return "ARBITRARY_RECTANGLE";
        }
        return "UNDEFINED";
    }

    public InsetsFrameProvider(Parcel in) {
        mId = in.readInt();
        mSource = in.readInt();
        mFlags = in.readInt();
        mInsetsSize = in.readTypedObject(Insets.CREATOR);
        mInsetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR);
        mArbitraryRectangle = in.readTypedObject(Rect.CREATOR);
        mMinimalInsetsSizeInDisplayCutoutSafe = in.readTypedObject(Insets.CREATOR);
        mBoundingRects = in.createTypedArray(Rect.CREATOR);
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mId);
        out.writeInt(mSource);
        out.writeInt(mFlags);
        out.writeTypedObject(mInsetsSize, flags);
        out.writeTypedArray(mInsetsSizeOverrides, flags);
        out.writeTypedObject(mArbitraryRectangle, flags);
        out.writeTypedObject(mMinimalInsetsSizeInDisplayCutoutSafe, flags);
        out.writeTypedArray(mBoundingRects, flags);
    }

    public boolean idEquals(InsetsFrameProvider o) {
        return mId == o.mId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final InsetsFrameProvider other = (InsetsFrameProvider) o;
        return mId == other.mId && mSource == other.mSource && mFlags == other.mFlags
                && Objects.equals(mInsetsSize, other.mInsetsSize)
                && Arrays.equals(mInsetsSizeOverrides, other.mInsetsSizeOverrides)
                && Objects.equals(mArbitraryRectangle, other.mArbitraryRectangle)
                && Objects.equals(mMinimalInsetsSizeInDisplayCutoutSafe,
                        other.mMinimalInsetsSizeInDisplayCutoutSafe)
                && Arrays.equals(mBoundingRects, other.mBoundingRects);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mId, mSource, mFlags, mInsetsSize,
                Arrays.hashCode(mInsetsSizeOverrides), mArbitraryRectangle,
                mMinimalInsetsSizeInDisplayCutoutSafe, Arrays.hashCode(mBoundingRects));
    }

    public static final @NonNull Parcelable.Creator<InsetsFrameProvider> CREATOR =
            new Parcelable.Creator<>() {
                @Override
                public InsetsFrameProvider createFromParcel(Parcel in) {
                    return new InsetsFrameProvider(in);
                }

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

    /**
     * Class to describe the insets size to be provided to window with specific window type. If not
     * used, same insets size will be sent as instructed in the insetsSize and source.
     *
     * If the insetsSize of given type is set to {@code null}, the insets source frame will be used
     * directly for that window type.
     */
    public static class InsetsSizeOverride implements Parcelable {

        private final int mWindowType;
        private final Insets mInsetsSize;

        protected InsetsSizeOverride(Parcel in) {
            mWindowType = in.readInt();
            mInsetsSize = in.readTypedObject(Insets.CREATOR);
        }

        public InsetsSizeOverride(int windowType, Insets insetsSize) {
            mWindowType = windowType;
            mInsetsSize = insetsSize;
        }
        public int getWindowType() {
            return mWindowType;
        }

        public Insets getInsetsSize() {
            return mInsetsSize;
        }

        public static final Creator<InsetsSizeOverride> CREATOR = new Creator<>() {
            @Override
            public InsetsSizeOverride createFromParcel(Parcel in) {
                return new InsetsSizeOverride(in);
            }

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

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(mWindowType);
            out.writeTypedObject(mInsetsSize, flags);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(32);
            sb.append("TypedInsetsSize: {");
            sb.append("windowType=").append(ViewDebug.intToString(
                    WindowManager.LayoutParams.class, "type", mWindowType));
            sb.append(", insetsSize=").append(mInsetsSize);
            sb.append("}");
            return sb.toString();
        }

        @Override
        public int hashCode() {
            return Objects.hash(mWindowType, mInsetsSize);
        }
    }
}

