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

import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.car.Car;
import android.car.builtin.util.Slogf;
import android.car.builtin.view.SurfaceControlHelper;
import android.car.builtin.view.TouchableInsetsProvider;
import android.car.builtin.view.ViewHelper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.DeadObjectException;
import android.os.RemoteException;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A {@link SurfaceView} that can embed a Task inside of it. The task management is done remotely
 * in a process that has registered a TaskOrganizer with the system server.
 * Usually this process is the Car System UI.
 *
 * @hide
 */
public abstract class RemoteCarTaskView extends SurfaceView {
    private static final String TAG = RemoteCarTaskView.class.getSimpleName();

    private final TouchableInsetsProvider mTouchableInsetsProvider;
    private final SurfaceCallbackHandler mSurfaceCallbackHandler = new SurfaceCallbackHandler();
    private final Rect mTmpRect = new Rect();
    private final AtomicBoolean mReleased = new AtomicBoolean(false);
    private boolean mInitialized = false;
    boolean mSurfaceCreated = false;
    private Region mObscuredTouchRegion;
    private ICarTaskViewHost mICarTaskViewHost;

    RemoteCarTaskView(Context context) {
        super(context);
        mTouchableInsetsProvider = new TouchableInsetsProvider(this);
        getHolder().addCallback(mSurfaceCallbackHandler);
    }

    /** Brings the embedded task to the front. Does nothing if there is no task. */
    @RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
    @MainThread
    public void showEmbeddedTask() {
        try {
            mICarTaskViewHost.showEmbeddedTask();
        } catch (RemoteException e) {
            Slogf.e(TAG, "exception in showEmbeddedTask", e);
        }
    }

    /**
     * Sets the visibility of the embedded task.
     *
     * @hide
     */
    @RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
    @MainThread
    public void setTaskVisibility(boolean visibility) {
        try {
            mICarTaskViewHost.setTaskVisibility(visibility);
        } catch (RemoteException e) {
            Slogf.e(TAG, "exception in setTaskVisibility", e);
        }
    }

    /**
     * Reorders the embedded task.
     *
     * @hide
     */
    @RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
    @MainThread
    public void reorderTask(boolean onTop) {
        try {
            mICarTaskViewHost.reorderTask(onTop);
        } catch (RemoteException e) {
            Slogf.e(TAG, "exception in reorderTask for task", e);
        }
    }

    /**
     * Updates the WM bounds for the underlying task as per the current view bounds. Does nothing
     * if there is no task.
     */
    @RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
    @MainThread
    public void updateWindowBounds() {
        ViewHelper.getBoundsOnScreen(RemoteCarTaskView.this, mTmpRect);
        try {
            mICarTaskViewHost.setWindowBounds(mTmpRect);
        } catch (RemoteException e) {
            Slogf.e(TAG, "exception in setWindowBounds", e);
        }
    }

    /**
     * Sets the bounds of the window for the underlying Task.
     *
     * @param bounds the new bounds in screen coordinates.
     *
     * @hide
     */
    public void setWindowBounds(Rect bounds) {
        try {
            mICarTaskViewHost.setWindowBounds(bounds);
        } catch (RemoteException e) {
            Slogf.e(TAG, "exception in setWindowBounds", e);
        }
    }

    /**
     * Indicates a region of the view that is not touchable.
     *
     * @param obscuredRect the obscured region of the view.
     */
    @MainThread
    public void setObscuredTouchRect(@NonNull Rect obscuredRect) {
        mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null;
        mTouchableInsetsProvider.setObscuredTouchRegion(mObscuredTouchRegion);
    }

    /**
     * Indicates a region of the view that is not touchable.
     *
     * @param obscuredRegion the obscured region of the view.
     */
    @MainThread
    public void setObscuredTouchRegion(@NonNull Region obscuredRegion) {
        mObscuredTouchRegion = obscuredRegion;
        mTouchableInsetsProvider.setObscuredTouchRegion(mObscuredTouchRegion);
    }

    /**
     * @return the {@link android.app.ActivityManager.RunningTaskInfo} of the task currently
     * running in the TaskView.
     */
    @MainThread
    @Nullable public abstract ActivityManager.RunningTaskInfo getTaskInfo();

    /**
     * @return true, if the task view is initialized.
     */
    @MainThread
    public boolean isInitialized() {
        return mInitialized;
    }

    /**
     * @return true, if the task view is released.
     *
     * @hide
     */
    @MainThread
    public boolean isReleased() {
        return mReleased.get();
    }

    /**
     * Adds the given insets on the Task.
     *
     * The given frame for the insets type are applied to the underlying task right away.
     * If a rectangle for an insets type was added previously, it will be replaced with the
     * new value.
     * If a rectangle for a insets type was already added, but is not specified currently in
     * {@code insets}, it will remain applied to the task. Clients should explicitly call
     * {@link #removeInsets(int, int)} to remove the rectangle for that insets type from
     * the underlying task.
     *
     * @param index An owner might add multiple insets sources with the same type.
     *              This identifies them.
     * @param type  The insets type of the insets source.
     * @param frame The rectangle area of the insets source.
     */
    @RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
    @MainThread
    public void addInsets(int index, int type, @NonNull Rect frame) {
        try {
            mICarTaskViewHost.addInsets(index, type, frame);
        } catch (RemoteException e) {
            Slog.e(TAG, "exception in addInsets", e);
        }
    }

    /**
     * Removes the given insets from the Task.
     *
     * Note: This will only remove the insets that were added using
     * {@link #addInsets(int, int, Rect)}
     *
     * @param index An owner might add multiple insets sources with the same type.
     *              This identifies them.
     * @param type  The insets type of the insets source. This doesn't accept the composite types.
     */
    @RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
    public void removeInsets(int index, int type) {
        try {
            mICarTaskViewHost.removeInsets(index, type);
        } catch (RemoteException e) {
            Slog.e(TAG, "exception in removeInsets", e);
        }
    }

    void setRemoteHost(@NonNull ICarTaskViewHost carTaskViewHost) {
        mICarTaskViewHost = carTaskViewHost;

        if (mSurfaceCreated) {
            if (!mInitialized) {
                onInitialized();
                mInitialized = true;
            }
        }
    }

    /**
     * Starts the activity from the given {@code PendingIntent}
     *
     * @param pendingIntent Intent used to launch an activity.
     * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()}
     * @param options options for the activity.
     * @param launchBounds the bounds (window size and position) that the activity should be
     *                      launched in, in pixels and in screen coordinates.
     */
    void startActivity(
            @NonNull PendingIntent pendingIntent,
            @Nullable Intent fillInIntent,
            @NonNull ActivityOptions options,
            @Nullable Rect launchBounds) {
        try {
            mICarTaskViewHost.startActivity(
                    pendingIntent, fillInIntent, options.toBundle(), launchBounds);
        } catch (RemoteException exception) {
            Slogf.e(TAG, "exception in startActivity", exception);
        }
    }

    void createRootTask(int displayId) {
        try {
            mICarTaskViewHost.createRootTask(displayId);
        } catch (RemoteException exception) {
            Slogf.e(TAG, "exception in createRootTask", exception);
        }
    }

    void createLaunchRootTask(int displayId, boolean embedHomeTask, boolean embedRecentsTask,
            boolean embedAssistantTask) {
        try {
            mICarTaskViewHost.createLaunchRootTask(displayId, embedHomeTask, embedRecentsTask,
                    embedAssistantTask);
        } catch (RemoteException exception) {
            Slogf.e(TAG, "exception in createRootTask", exception);
        }
    }

    /** Release the resources associated with this task view. */
    @MainThread
    public void release() {
        getHolder().removeCallback(mSurfaceCallbackHandler);
        try {
            mReleased.set(true);
            mICarTaskViewHost.release();
        } catch (DeadObjectException e) {
            Slogf.w(TAG, "TaskView's host has already died", e);
        } catch (RemoteException e) {
            Slogf.e(TAG, "exception in release", e);
        }
        onReleased();
    }

    /**
     * Called when the task view is initialized. It is called only once for the lifetime of
     * taskview.
     */
    abstract void onInitialized();

    /**
     * Called when the task view is released. It is only called once for the lifetime of task view.
     */
    abstract void onReleased();

    /**
     * Called when the task has appeared in the taskview.
     *
     * @param taskInfo the taskInfo of the task that has appeared.
     * @param leash the suface control for the task surface.
     */
    void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
    }

    /**
     * Called when the task's info has changed.
     *
     * @param taskInfo the taskInfo of the task that has a change in info.
     */
    void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
    }

    /**
     * Called when the task has vanished.
     *
     * @param taskInfo the taskInfo of the task that has vanished.
     */
    void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        mTouchableInsetsProvider.addToViewTreeObserver();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mTouchableInsetsProvider.removeFromViewTreeObserver();
    }

    private class SurfaceCallbackHandler implements SurfaceHolder.Callback {
        @Override
        public void surfaceCreated(@NonNull SurfaceHolder holder) {
            if (mICarTaskViewHost != null) {
                if (!mInitialized) {
                    onInitialized();
                    mInitialized = true;
                }
            }
            mSurfaceCreated = true;
            try {
                mICarTaskViewHost.notifySurfaceCreated(
                        SurfaceControlHelper.copy(getSurfaceControl()));
            } catch (RemoteException e) {
                Slogf.e(TAG, "exception in notifySurfaceCreated", e);
            }
        }

        @Override
        public void surfaceChanged(
                @NonNull SurfaceHolder holder, int format, int width, int height) {
            try {
                ViewHelper.getBoundsOnScreen(RemoteCarTaskView.this, mTmpRect);
                mICarTaskViewHost.setWindowBounds(mTmpRect);
            } catch (RemoteException e) {
                Slogf.e(TAG, "exception in setWindowBounds", e);
            }
        }

        @Override
        public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
            mSurfaceCreated = false;
            try {
                mICarTaskViewHost.notifySurfaceDestroyed();
            } catch (RemoteException e) {
                Slogf.e(TAG, "exception in notifySurfaceDestroyed", e);
            }
        }
    }
}
