/*
 * 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 com.android.wm.shell.common;

import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;

import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Region;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.MergedConfiguration;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DragEvent;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindow;
import android.view.IWindowManager;
import android.view.IWindowSession;
import android.view.IWindowSessionCallback;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.ScrollCaptureResponse;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.SurfaceSession;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.ClientWindowFrames;

import com.android.internal.os.IResultReceiver;

import java.util.HashMap;

/**
 * Represents the "windowing" layer of the WM Shell. This layer allows shell components to place and
 * manipulate windows without talking to WindowManager.
 */
public class SystemWindows {
    private static final String TAG = "SystemWindows";

    private final SparseArray<PerDisplay> mPerDisplay = new SparseArray<>();
    private final HashMap<View, SurfaceControlViewHost> mViewRoots = new HashMap<>();
    private final DisplayController mDisplayController;
    private final IWindowManager mWmService;
    private IWindowSession mSession;

    private final DisplayController.OnDisplaysChangedListener mDisplayListener =
            new DisplayController.OnDisplaysChangedListener() {
                @Override
                public void onDisplayAdded(int displayId) { }

                @Override
                public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
                    PerDisplay pd = mPerDisplay.get(displayId);
                    if (pd == null) {
                        return;
                    }
                    pd.updateConfiguration(newConfig);
                }

                @Override
                public void onDisplayRemoved(int displayId) { }
            };

    public SystemWindows(DisplayController displayController, IWindowManager wmService) {
        mWmService = wmService;
        mDisplayController = displayController;
        mDisplayController.addDisplayWindowListener(mDisplayListener);
        try {
            mSession = wmService.openSession(
                    new IWindowSessionCallback.Stub() {
                        @Override
                        public void onAnimatorScaleChanged(float scale) {}
                    });
        } catch (RemoteException e) {
            Slog.e(TAG, "Unable to create layer", e);
        }
    }

    /**
     * Adds a view to system-ui window management.
     */
    public void addView(View view, WindowManager.LayoutParams attrs, int displayId,
            @WindowManager.ShellRootLayer int shellRootLayer) {
        PerDisplay pd = mPerDisplay.get(displayId);
        if (pd == null) {
            pd = new PerDisplay(displayId);
            mPerDisplay.put(displayId, pd);
        }
        pd.addView(view, attrs, shellRootLayer);
    }

    /**
     * Removes a view from system-ui window management.
     * @param view
     */
    public void removeView(View view) {
        SurfaceControlViewHost root = mViewRoots.remove(view);
        root.release();
    }

    /**
     * Updates the layout params of a view.
     */
    public void updateViewLayout(@NonNull View view, ViewGroup.LayoutParams params) {
        SurfaceControlViewHost root = mViewRoots.get(view);
        if (root == null || !(params instanceof WindowManager.LayoutParams)) {
            return;
        }
        view.setLayoutParams(params);
        root.relayout((WindowManager.LayoutParams) params);
    }

    /**
     * Sets the accessibility window for the given {@param shellRootLayer}.
     */
    public void setShellRootAccessibilityWindow(int displayId,
            @WindowManager.ShellRootLayer int shellRootLayer, View view) {
        PerDisplay pd = mPerDisplay.get(displayId);
        if (pd == null) {
            return;
        }
        pd.setShellRootAccessibilityWindow(shellRootLayer, view);
    }

    /**
     * Sets the touchable region of a view's window. This will be cropped to the window size.
     * @param view
     * @param region
     */
    public void setTouchableRegion(@NonNull View view, Region region) {
        SurfaceControlViewHost root = mViewRoots.get(view);
        if (root == null) {
            return;
        }
        WindowlessWindowManager wwm = root.getWindowlessWM();
        if (!(wwm instanceof SysUiWindowManager)) {
            return;
        }
        ((SysUiWindowManager) wwm).setTouchableRegionForWindow(view, region);
    }

    /**
     * Get the IWindow token for a specific root.
     *
     * @param windowType A window type from {@link WindowManager}.
     */
    IWindow getWindow(int displayId, int windowType) {
        PerDisplay pd = mPerDisplay.get(displayId);
        if (pd == null) {
            return null;
        }
        return pd.getWindow(windowType);
    }

    /**
     * Gets the SurfaceControl associated with a root view. This is the same surface that backs the
     * ViewRootImpl.
     */
    public SurfaceControl getViewSurface(View rootView) {
        for (int i = 0; i < mPerDisplay.size(); ++i) {
            for (int iWm = 0; iWm < mPerDisplay.valueAt(i).mWwms.size(); ++iWm) {
                SurfaceControl out = mPerDisplay.valueAt(i).mWwms.valueAt(iWm)
                        .getSurfaceControlForWindow(rootView);
                if (out != null) {
                    return out;
                }
            }
        }
        return null;
    }

    private class PerDisplay {
        final int mDisplayId;
        private final SparseArray<SysUiWindowManager> mWwms = new SparseArray<>();

        PerDisplay(int displayId) {
            mDisplayId = displayId;
        }

        public void addView(View view, WindowManager.LayoutParams attrs,
                @WindowManager.ShellRootLayer int shellRootLayer) {
            SysUiWindowManager wwm = addRoot(shellRootLayer);
            if (wwm == null) {
                Slog.e(TAG, "Unable to create systemui root");
                return;
            }
            final Display display = mDisplayController.getDisplay(mDisplayId);
            SurfaceControlViewHost viewRoot =
                    new SurfaceControlViewHost(
                            view.getContext(), display, wwm, true /* useSfChoreographer */);
            attrs.flags |= FLAG_HARDWARE_ACCELERATED;
            viewRoot.setView(view, attrs);
            mViewRoots.put(view, viewRoot);
            setShellRootAccessibilityWindow(shellRootLayer, view);
        }

        SysUiWindowManager addRoot(@WindowManager.ShellRootLayer int shellRootLayer) {
            SysUiWindowManager wwm = mWwms.get(shellRootLayer);
            if (wwm != null) {
                return wwm;
            }
            SurfaceControl rootSurface = null;
            ContainerWindow win = new ContainerWindow();
            try {
                rootSurface = mWmService.addShellRoot(mDisplayId, win, shellRootLayer);
            } catch (RemoteException e) {
            }
            if (rootSurface == null) {
                Slog.e(TAG, "Unable to get root surfacecontrol for systemui");
                return null;
            }
            Context displayContext = mDisplayController.getDisplayContext(mDisplayId);
            wwm = new SysUiWindowManager(mDisplayId, displayContext, rootSurface, win);
            mWwms.put(shellRootLayer, wwm);
            return wwm;
        }

        IWindow getWindow(int windowType) {
            SysUiWindowManager wwm = mWwms.get(windowType);
            if (wwm == null) {
                return null;
            }
            return wwm.mContainerWindow;
        }

        void setShellRootAccessibilityWindow(@WindowManager.ShellRootLayer int shellRootLayer,
                View view) {
            SysUiWindowManager wwm = mWwms.get(shellRootLayer);
            if (wwm == null) {
                return;
            }
            try {
                mWmService.setShellRootAccessibilityWindow(mDisplayId, shellRootLayer,
                        view != null ? mViewRoots.get(view).getWindowToken() : null);
            } catch (RemoteException e) {
                Slog.e(TAG, "Error setting accessibility window for " + mDisplayId + ":"
                        + shellRootLayer, e);
            }
        }

        void updateConfiguration(Configuration configuration) {
            for (int i = 0; i < mWwms.size(); ++i) {
                mWwms.valueAt(i).updateConfiguration(configuration);
            }
        }
    }

    /**
     * A subclass of WindowlessWindowManager that provides insets to its viewroots.
     */
    public class SysUiWindowManager extends WindowlessWindowManager {
        final int mDisplayId;
        ContainerWindow mContainerWindow;
        final HashMap<IBinder, SurfaceControl> mLeashForWindow =
                new HashMap<IBinder, SurfaceControl>();
        public SysUiWindowManager(int displayId, Context ctx, SurfaceControl rootSurface,
                ContainerWindow container) {
            super(ctx.getResources().getConfiguration(), rootSurface, null /* hostInputToken */);
            mContainerWindow = container;
            mDisplayId = displayId;
        }

        void updateConfiguration(Configuration configuration) {
            setConfiguration(configuration);
        }

        SurfaceControl getSurfaceControlForWindow(View rootView) {
            synchronized (this) {
                return mLeashForWindow.get(getWindowBinder(rootView));
            }
        }

        protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
            SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession())
                  .setContainerLayer()
                  .setName("SystemWindowLeash")
                  .setHidden(false)
                  .setParent(mRootSurface)
                  .setCallsite("SysUiWIndowManager#attachToParentSurface").build();
            synchronized (this) {
                mLeashForWindow.put(window.asBinder(), leash);
            }
            b.setParent(leash);
        }

        @Override
        public void remove(android.view.IWindow window) throws RemoteException {
            super.remove(window);
            synchronized(this) {
                IBinder token = window.asBinder();
                new SurfaceControl.Transaction().remove(mLeashForWindow.get(token))
                    .apply();
                mLeashForWindow.remove(token);
            }
        }

        void setTouchableRegionForWindow(View rootView, Region region) {
            IBinder token = rootView.getWindowToken();
            if (token == null) {
                return;
            }
            setTouchRegion(token, region);
        }
    }

    static class ContainerWindow extends IWindow.Stub {
        ContainerWindow() {}

        @Override
        public void resized(ClientWindowFrames frames, boolean reportDraw,
                MergedConfiguration newMergedConfiguration, boolean forceLayout,
                boolean alwaysConsumeSystemBars, int displayId) {}

        @Override
        public void locationInParentDisplayChanged(Point offset) {}

        @Override
        public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {}

        @Override
        public void insetsControlChanged(InsetsState insetsState,
                InsetsSourceControl[] activeControls, boolean willMove, boolean willResize) {}

        @Override
        public void showInsets(int types, boolean fromIme) {}

        @Override
        public void hideInsets(int types, boolean fromIme) {}

        @Override
        public void moved(int newX, int newY) {}

        @Override
        public void dispatchAppVisibility(boolean visible) {}

        @Override
        public void dispatchGetNewSurface() {}

        @Override
        public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {}

        @Override
        public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {}

        @Override
        public void closeSystemDialogs(String reason) {}

        @Override
        public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep,
                float zoom, boolean sync) {}

        @Override
        public void dispatchWallpaperCommand(String action, int x, int y,
                int z, Bundle extras, boolean sync) {}

        /* Drag/drop */
        @Override
        public void dispatchDragEvent(DragEvent event) {}

        @Override
        public void updatePointerIcon(float x, float y) {}

        @Override
        public void dispatchWindowShown() {}

        @Override
        public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {}

        @Override
        public void requestScrollCapture(IScrollCaptureResponseListener listener) {
            try {
                listener.onScrollCaptureResponse(
                        new ScrollCaptureResponse.Builder()
                                .setDescription("Not Implemented")
                                .build());

            } catch (RemoteException ex) {
                // ignore
            }
        }
    }
}
