/*
 * Copyright (C) 2019 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.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;
import android.view.accessibility.IAccessibilityEmbeddedConnection;
import android.window.WindowTokenClient;

import java.util.Objects;

/**
 * Utility class for adding a View hierarchy to a {@link SurfaceControl}. The View hierarchy
 * will render in to a root SurfaceControl, and receive input based on the SurfaceControl's
 * placement on-screen. The primary usage of this class is to embed a View hierarchy from
 * one process in to another. After the SurfaceControlViewHost has been set up in the embedded
 * content provider, we can send the {@link SurfaceControlViewHost.SurfacePackage}
 * to the host process. The host process can then attach the hierarchy to a SurfaceView within
 * its own by calling
 * {@link SurfaceView#setChildSurfacePackage}.
 */
public class SurfaceControlViewHost {
    private final static String TAG = "SurfaceControlViewHost";
    private final ViewRootImpl mViewRoot;
    private WindowlessWindowManager mWm;

    private SurfaceControl mSurfaceControl;
    private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
    private boolean mReleased = false;

    private final class ISurfaceControlViewHostImpl extends ISurfaceControlViewHost.Stub {
        @Override
        public void onConfigurationChanged(Configuration configuration) {
            if (mViewRoot == null) {
                return;
            }
            mViewRoot.mHandler.post(() -> {
                if (mWm != null) {
                    mWm.setConfiguration(configuration);
                }
                if (mViewRoot != null) {
                    mViewRoot.forceWmRelayout();
                }
            });
        }

        @Override
        public void onDispatchDetachedFromWindow() {
            if (mViewRoot == null) {
                return;
            }
            mViewRoot.mHandler.post(() -> {
                release();
            });
        }

        @Override
        public void onInsetsChanged(InsetsState state, Rect frame) {
            if (mViewRoot != null) {
                mViewRoot.mHandler.post(() -> {
                    mViewRoot.setOverrideInsetsFrame(frame);
                });
            }
            mWm.setInsetsState(state);
        }
    }

    private ISurfaceControlViewHost mRemoteInterface = new ISurfaceControlViewHostImpl();

    /**
     * Package encapsulating a Surface hierarchy which contains interactive view
     * elements. It's expected to get this object from
     * {@link SurfaceControlViewHost#getSurfacePackage} afterwards it can be embedded within
     * a SurfaceView by calling {@link SurfaceView#setChildSurfacePackage}.
     *
     * Note that each {@link SurfacePackage} must be released by calling
     * {@link SurfacePackage#release}. However, if you use the recommended flow,
     *  the framework will automatically handle the lifetime for you.
     *
     * 1. When sending the package to the remote process, return it from an AIDL method
     * or manually use FLAG_WRITE_RETURN_VALUE in writeToParcel. This will automatically
     * release the package in the local process.
     * 2. In the remote process, consume the package using SurfaceView. This way the
     * SurfaceView will take over the lifetime and call {@link SurfacePackage#release}
     * for the user.
     *
     * One final note: The {@link SurfacePackage} lifetime is totally de-coupled
     * from the lifetime of the underlying {@link SurfaceControlViewHost}. Regardless
     * of the lifetime of the package the user should still call
     * {@link SurfaceControlViewHost#release} when finished.
     */
    public static final class SurfacePackage implements Parcelable {
        private SurfaceControl mSurfaceControl;
        private final IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
        private final IBinder mInputToken;
        private final ISurfaceControlViewHost mRemoteInterface;

        SurfacePackage(SurfaceControl sc, IAccessibilityEmbeddedConnection connection,
               IBinder inputToken, ISurfaceControlViewHost ri) {
            mSurfaceControl = sc;
            mAccessibilityEmbeddedConnection = connection;
            mInputToken = inputToken;
            mRemoteInterface = ri;
        }

        /**
         * Constructs a copy of {@code SurfacePackage} with an independent lifetime.
         *
         * The caller can use this to create an independent copy in situations where ownership of
         * the {@code SurfacePackage} would be transferred elsewhere, such as attaching to a
         * {@code SurfaceView}, returning as {@code Binder} result value, etc. The caller is
         * responsible for releasing this copy when its done.
         *
         * @param other {@code SurfacePackage} to create a copy of.
         */
        public SurfacePackage(@NonNull SurfacePackage other) {
            SurfaceControl otherSurfaceControl = other.mSurfaceControl;
            if (otherSurfaceControl != null && otherSurfaceControl.isValid()) {
                mSurfaceControl = new SurfaceControl();
                mSurfaceControl.copyFrom(otherSurfaceControl, "SurfacePackage");
            }
            mAccessibilityEmbeddedConnection = other.mAccessibilityEmbeddedConnection;
            mInputToken = other.mInputToken;
            mRemoteInterface = other.mRemoteInterface;
        }

        private SurfacePackage(Parcel in) {
            mSurfaceControl = new SurfaceControl();
            mSurfaceControl.readFromParcel(in);
            mAccessibilityEmbeddedConnection = IAccessibilityEmbeddedConnection.Stub.asInterface(
                    in.readStrongBinder());
            mInputToken = in.readStrongBinder();
            mRemoteInterface = ISurfaceControlViewHost.Stub.asInterface(
                in.readStrongBinder());
        }

        /**
         * Use {@link SurfaceView#setChildSurfacePackage} or manually fix
         * accessibility (see SurfaceView implementation).
         * @hide
         */
        public @NonNull SurfaceControl getSurfaceControl() {
            return mSurfaceControl;
        }

        /**
         * Gets an accessibility embedded connection interface for this SurfaceControlViewHost.
         *
         * @return {@link IAccessibilityEmbeddedConnection} interface.
         * @hide
         */
        public IAccessibilityEmbeddedConnection getAccessibilityEmbeddedConnection() {
            return mAccessibilityEmbeddedConnection;
        }

        /**
         * @hide
         */
        public ISurfaceControlViewHost getRemoteInterface() {
            return mRemoteInterface;
        }

        /**
         * Forward a configuration to the remote SurfaceControlViewHost.
         * This will cause View#onConfigurationChanged to be invoked on the remote
         * end. This does not automatically cause the SurfaceControlViewHost
         * to be resized. The root View of a SurfaceControlViewHost
         * is more akin to a PopupWindow in that the size is user specified
         * independent of configuration width and height.
         *
         * In order to receive the configuration change via 
         * {@link View#onConfigurationChanged}, the context used with the
         * SurfaceControlViewHost and it's embedded view hierarchy must
         * be a WindowContext obtained from {@link Context#createWindowContext}.
         *
         * If a regular service context is used, then your embedded view hierarchy
         * will always perceive the global configuration.
         *
         * @param c The configuration to forward
         */
        public void notifyConfigurationChanged(@NonNull Configuration c) {
            try {
                getRemoteInterface().onConfigurationChanged(c);
            } catch (RemoteException e) {
                e.rethrowAsRuntimeException();
            }
        }

        /**
         * Tear down the remote SurfaceControlViewHost and cause
         * View#onDetachedFromWindow to be invoked on the other side.
         */
        public void notifyDetachedFromWindow() {
            try {
                getRemoteInterface().onDispatchDetachedFromWindow();
            } catch (RemoteException e) {
                e.rethrowAsRuntimeException();
            }
        }

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

        @Override
        public void writeToParcel(@NonNull Parcel out, int flags) {
            mSurfaceControl.writeToParcel(out, flags);
            out.writeStrongBinder(mAccessibilityEmbeddedConnection.asBinder());
            out.writeStrongBinder(mInputToken);
            out.writeStrongBinder(mRemoteInterface.asBinder());
        }

        /**
         * Release the {@link SurfaceControl} associated with this package.
         * It's not necessary to call this if you pass the package to
         * {@link SurfaceView#setChildSurfacePackage} as {@link SurfaceView} will
         * take ownership in that case.
         */
        public void release() {
            if (mSurfaceControl != null) {
                mSurfaceControl.release();
             }
             mSurfaceControl = null;
        }

        /**
         * Returns an input token used which can be used to request focus on the embedded surface.
         *
         * @hide
         */
        public IBinder getInputToken() {
            return mInputToken;
        }

        public static final @NonNull Creator<SurfacePackage> CREATOR
             = new Creator<SurfacePackage>() {
                     public SurfacePackage createFromParcel(Parcel in) {
                         return new SurfacePackage(in);
                     }
                     public SurfacePackage[] newArray(int size) {
                         return new SurfacePackage[size];
                     }
             };
    }

    /** @hide */
    public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
            @NonNull WindowlessWindowManager wwm) {
        mWm = wwm;
        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout());
        addConfigCallback(c, d);

        WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot);

        mAccessibilityEmbeddedConnection = mViewRoot.getAccessibilityEmbeddedConnection();
    }

    /**
     * Construct a new SurfaceControlViewHost. The root Surface will be
     * allocated internally and is accessible via getSurfacePackage().
     *
     * The {@param hostToken} parameter, primarily used for ANR reporting,
     * must be obtained from whomever will be hosting the embedded hierarchy.
     * It's accessible from {@link SurfaceView#getHostToken}.
     *
     * @param context The Context object for your activity or application.
     * @param display The Display the hierarchy will be placed on.
     * @param hostToken The host token, as discussed above.
     */
    public SurfaceControlViewHost(@NonNull Context context, @NonNull Display display,
            @Nullable IBinder hostToken) {
        mSurfaceControl = new SurfaceControl.Builder()
                .setContainerLayer()
                .setName("SurfaceControlViewHost")
                .setCallsite("SurfaceControlViewHost")
                .build();
        mWm = new WindowlessWindowManager(context.getResources().getConfiguration(),
                mSurfaceControl, hostToken);

        mViewRoot = new ViewRootImpl(context, display, mWm, new WindowlessWindowLayout());
        addConfigCallback(context, display);

        WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot);

        mAccessibilityEmbeddedConnection = mViewRoot.getAccessibilityEmbeddedConnection();
    }

    private void addConfigCallback(Context c, Display d) {
        final IBinder token = c.getWindowContextToken();
        mViewRoot.addConfigCallback((conf) -> {
            if (token instanceof WindowTokenClient) {
                final WindowTokenClient w = (WindowTokenClient)  token;
                w.onConfigurationChanged(conf, d.getDisplayId(), true);
            }
        });
    }

    /**
     * @hide
     */
    @Override
    protected void finalize() throws Throwable {
        if (mReleased) {
            return;
        }
        Log.e(TAG, "SurfaceControlViewHost finalized without being released: " + this);
        // We aren't on the UI thread here so we need to pass false to doDie
        mViewRoot.die(false /* immediate */);
        WindowManagerGlobal.getInstance().removeWindowlessRoot(mViewRoot);
    }

    /**
     * Return a SurfacePackage for the root SurfaceControl of the embedded hierarchy.
     * Rather than be directly reparented using {@link SurfaceControl.Transaction} this
     * SurfacePackage should be passed to {@link SurfaceView#setChildSurfacePackage}
     * which will not only reparent the Surface, but ensure the accessibility hierarchies
     * are linked.
     */
    public @Nullable SurfacePackage getSurfacePackage() {
        if (mSurfaceControl != null && mAccessibilityEmbeddedConnection != null) {
            return new SurfacePackage(new SurfaceControl(mSurfaceControl, "getSurfacePackage"),
                mAccessibilityEmbeddedConnection,
                mWm.getFocusGrantToken(), mRemoteInterface);
        } else {
            return null;
        }
    }

    /**
     * Set the root view of the SurfaceControlViewHost. This view will render in to
     * the SurfaceControl, and receive input based on the SurfaceControls positioning on
     * screen. It will be laid as if it were in a window of the passed in width and height.
     *
     * @param view The View to add
     * @param width The width to layout the View within, in pixels.
     * @param height The height to layout the View within, in pixels.
     */
    public void setView(@NonNull View view, int width, int height) {
        final WindowManager.LayoutParams lp =
                new WindowManager.LayoutParams(width, height,
                        WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT);
        setView(view, lp);
    }

    /**
     * @hide
     */
    @TestApi
    public void setView(@NonNull View view, @NonNull WindowManager.LayoutParams attrs) {
        Objects.requireNonNull(view);
        attrs.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        view.setLayoutParams(attrs);
        mViewRoot.setView(view, attrs, null);
    }

    /**
     * @return The view passed to setView, or null if none has been passed.
     */
    public @Nullable View getView() {
        return mViewRoot.getView();
    }

    /**
     * @return the ViewRootImpl wrapped by this host.
     * @hide
     */
    public IWindow getWindowToken() {
        return mViewRoot.mWindow;
    }

    /**
     * @return the WindowlessWindowManager instance that this host is attached to.
     * @hide
     */
    public @NonNull WindowlessWindowManager getWindowlessWM() {
        return mWm;
    }

    /**
     * @hide
     */
    @TestApi
    public void relayout(WindowManager.LayoutParams attrs) {
        relayout(attrs, SurfaceControl.Transaction::apply);
    }

    /**
     * Forces relayout and draw and allows to set a custom callback when it is finished
     * @hide
     */
    public void relayout(WindowManager.LayoutParams attrs,
            WindowlessWindowManager.ResizeCompleteCallback callback) {
        mViewRoot.setLayoutParams(attrs, false);
        mViewRoot.setReportNextDraw(true /* syncBuffer */, "scvh_relayout");
        mWm.setCompletionCallback(mViewRoot.mWindow.asBinder(), callback);
    }

    /**
     * Modify the size of the root view.
     *
     * @param width Width in pixels
     * @param height Height in pixels
     */
    public void relayout(int width, int height) {
        final WindowManager.LayoutParams lp =
                new WindowManager.LayoutParams(width, height,
                        WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT);
        relayout(lp);
    }

    /**
     * Trigger the tear down of the embedded view hierarchy and release the SurfaceControl.
     * This will result in onDispatchedFromWindow being dispatched to the embedded view hierarchy
     * and render the object unusable.
     */
    public void release() {
        // ViewRoot will release mSurfaceControl for us.
        mViewRoot.die(true /* immediate */);
        WindowManagerGlobal.getInstance().removeWindowlessRoot(mViewRoot);
        mReleased = true;
    }

    /**
     * @hide
     */
    public IBinder getFocusGrantToken() {
        return mWm.getFocusGrantToken();
    }
}
