package com.android.server.wm;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Pools.SynchronizedPool;
import android.view.DisplayInfo;
import android.view.IDisplayMagnificationController;
import android.view.IDisplayMagnificationMediator;
import android.view.MagnificationSpec;
import android.view.Surface;
import android.view.WindowManager;
import android.view.WindowManagerPolicy;

import com.android.internal.os.SomeArgs;
import com.android.internal.policy.impl.PhoneWindowManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

final class DisplayMagnificationMediator extends IDisplayMagnificationMediator.Stub
        implements DisplayListener {
    private static final String LOG_TAG = DisplayMagnificationMediator.class.getSimpleName();

    private static final boolean DEBUG_WINDOW_TRANSITIONS = false;
    private static final boolean DEBUG_ROTATION = false;
    private static final boolean DEBUG_LAYERS = false;
    private static final boolean DEBUG_RECTANGLE_REQUESTED = false;

    private static final int MESSAGE_NOTIFY_MAGNIFIED_FRAME_CHANGED = 1;
    private static final int MESSAGE_NOTIFY_RECTANGLE_ON_SCREEN_REQUESTED = 2;
    private static final int MESSAGE_NOTIFY_USER_CONTEXT_CHANGED = 3;
    private static final int MESSAGE_NOTIFY_ROTATION_CHANGED = 4;

    private static final String METHOD_SIGNATURE_ADD_CONTROLLER =
            "addController(int, IDisplayMagnificationController)";
    private static final String METHOD_SIGNATURE_REMOVE_CONTROLLER =
            "removeController(IDisplayMagnificationController, int)";
    private static final String METHOD_SIGNATURE_SET_MAGNIFICATION_SPEC =
            "setMagnificationSpec(IDisplayMagnificationController, MagnificationSpec)";

    private final Rect mTempRect = new Rect();
    private final Rect mTempRect1 = new Rect();
    private final Region mTempRegion = new Region();

    private final SparseArray<DisplayState> mDisplayStates =
            new SparseArray<DisplayMagnificationMediator.DisplayState>();

    private final Context mContext;
    private final WindowManagerService mWindowManagerService;

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message message) {
            switch (message.what) {
                case MESSAGE_NOTIFY_MAGNIFIED_FRAME_CHANGED: {
                    SomeArgs args = (SomeArgs) message.obj;
                    IDisplayMagnificationController client =
                            (IDisplayMagnificationController) args.arg1;
                    final int left = args.argi1;
                    final int top = args.argi2;
                    final int right = args.argi3;
                    final int bottom = args.argi4;
                    try {
                        client.onMagnifedFrameChanged(left, top, right, bottom);
                    } catch (RemoteException re) {
                        /* ignore */
                    } finally {
                        args.recycle();
                    }
                } break;
                case MESSAGE_NOTIFY_RECTANGLE_ON_SCREEN_REQUESTED: {
                    SomeArgs args = (SomeArgs) message.obj;
                    IDisplayMagnificationController client =
                            (IDisplayMagnificationController) args.arg1;
                    final int left = args.argi1;
                    final int top = args.argi2;
                    final int right = args.argi3;
                    final int bottom = args.argi4;
                    try {
                        client.onRectangleOnScreenRequested(left, top, right, bottom);
                    } catch (RemoteException re) {
                        /* ignore */
                    } finally {
                        args.recycle();
                    }
                } break;
                case MESSAGE_NOTIFY_USER_CONTEXT_CHANGED: {
                    IDisplayMagnificationController client =
                            (IDisplayMagnificationController) message.obj;
                    try {
                        client.onUserContextChanged();
                    } catch (RemoteException re) {
                        /* ignore */
                    }
                } break;
                case MESSAGE_NOTIFY_ROTATION_CHANGED: {
                    IDisplayMagnificationController client =
                            (IDisplayMagnificationController) message.obj;
                    final int rotation = message.arg1;
                    try {
                        client.onRotationChanged(rotation);
                    } catch (RemoteException re) {
                        /* ignore */
                    }
                } break;
            }
        }
    };

    public DisplayMagnificationMediator(WindowManagerService windowManagerService) {
        mContext = windowManagerService.mContext;
        mWindowManagerService = windowManagerService;
        DisplayManager displayManager = (DisplayManager)
                mContext.getSystemService(Context.DISPLAY_SERVICE);
        displayManager.registerDisplayListener(this, mHandler);
    }

    @Override
    public void addController(int displayId, IDisplayMagnificationController controller) {
        enforceCallingPermission(Manifest.permission.MAGNIFY_DISPLAY,
                METHOD_SIGNATURE_ADD_CONTROLLER);
        synchronized (mWindowManagerService.mWindowMap) {
            DisplayState displayState = mDisplayStates.get(displayId);
            if (displayState != null) {
                displayState.clearLw();
            }
            mDisplayStates.remove(displayId);
            mDisplayStates.put(displayId, new DisplayState(displayId, controller));
        }
    }

    @Override
    public void removeController(IDisplayMagnificationController controller) {
        enforceCallingPermission(Manifest.permission.MAGNIFY_DISPLAY,
                METHOD_SIGNATURE_REMOVE_CONTROLLER);
        synchronized (mWindowManagerService.mWindowMap) {
            final int displayStateCount = mDisplayStates.size();
            for (int i = 0; i < displayStateCount; i++) {
                DisplayState displayState = mDisplayStates.valueAt(i);
                if (displayState.mClient.asBinder() == controller.asBinder()) {
                    displayState.clearLw();
                    mDisplayStates.removeAt(i);
                    return;
                }
            }
        }
    }

    @Override
    public void setMagnificationSpec(IDisplayMagnificationController controller,
            MagnificationSpec spec) {
        enforceCallingPermission(Manifest.permission.MAGNIFY_DISPLAY,
                METHOD_SIGNATURE_SET_MAGNIFICATION_SPEC);
        synchronized (mWindowManagerService.mWindowMap) {
            DisplayState displayState = null;
            final int displayStateCount = mDisplayStates.size();
            for (int i = 0; i < displayStateCount; i++) {
                DisplayState candidate = mDisplayStates.valueAt(i);
                if (candidate.mClient.asBinder() == controller.asBinder()) {
                    displayState = candidate;
                    break;
                }
            }
            if (displayState == null) {
                Slog.e(LOG_TAG, "Setting magnification spec for unregistered controller "
                        + controller);
                return;
            }
            displayState.mMagnificationSpec.initialize(spec.scale, spec.offsetX,
                    spec.offsetY);
            spec.recycle();
            mWindowManagerService.scheduleAnimationLocked();
        }
    }

    @Override
    public MagnificationSpec getCompatibleMagnificationSpec(IBinder windowToken) {
        synchronized (mWindowManagerService.mWindowMap) {
            WindowState windowState = mWindowManagerService.mWindowMap.get(windowToken);
            if (windowState == null) {
                return null;
            }
            MagnificationSpec spec = getMagnificationSpecLw(windowState);
            if ((spec == null || spec.isNop()) && windowState.mGlobalScale == 1.0f) {
                return null;
            }
            if (spec == null) {
                spec = MagnificationSpec.obtain();
            } else {
                spec = MagnificationSpec.obtain(spec);
            }
            spec.scale *= windowState.mGlobalScale;
            return spec;
        }
    }

    @Override
    public void onDisplayAdded(int displayId) {
        /* do nothing */
    }

    @Override
    public void onDisplayRemoved(int displayId) {
        synchronized (mWindowManagerService.mWindowMap) {
            DisplayState displayState = mDisplayStates.get(displayId);
            if (displayState != null) {
                displayState.clearLw();
                mDisplayStates.remove(displayId);
            }
        }
    }

    @Override
    public void onDisplayChanged(int displayId) {
        /* do nothing */
    }

    public void onRectangleOnScreenRequestedLw(WindowState windowState, Rect rectangle,
            boolean immediate) {
        DisplayState displayState = mDisplayStates.get(windowState.getDisplayId());
        if (displayState == null) {
            return;
        }
        if (DEBUG_RECTANGLE_REQUESTED) {
            Slog.i(LOG_TAG, "Rectangle on screen requested: " + rectangle
                    + " displayId: " + windowState.getDisplayId());
        }
        if (!displayState.isMagnifyingLw()) {
            return;
        }
        Rect magnifiedRegionBounds = mTempRect1;
        displayState.getMagnifiedFrameInContentCoordsLw(magnifiedRegionBounds);
        if (magnifiedRegionBounds.contains(rectangle)) {
            return;
        }
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = displayState.mClient;
        args.argi1 = rectangle.left;
        args.argi2 = rectangle.top;
        args.argi3 = rectangle.right;
        args.argi4 = rectangle.bottom;
        mHandler.obtainMessage(MESSAGE_NOTIFY_RECTANGLE_ON_SCREEN_REQUESTED,
                args).sendToTarget();
    }

    public void onWindowLayersChangedLw(int displayId) {
        DisplayState displayState = mDisplayStates.get(displayId);
        if (displayState == null) {
            return;
        }
        if (DEBUG_LAYERS) {
            Slog.i(LOG_TAG, "Layers changed displayId: " + displayId);
        }
        displayState.mViewport.recomputeBoundsLw();
    }

    public void onRotationChangedLw(int displayId, int rotation) {
        DisplayState displayState = mDisplayStates.get(displayId);
        if (displayState == null) {
            return;
        }
        if (DEBUG_ROTATION) {
            Slog.i(LOG_TAG, "Rotaton: " + Surface.rotationToString(rotation)
                    + " displayId: " + displayId);
        }
        displayState.mViewport.recomputeBoundsLw();
        mHandler.obtainMessage(MESSAGE_NOTIFY_ROTATION_CHANGED, rotation, 0,
                displayState.mClient).sendToTarget();
    }

    public void onWindowTransitionLw(WindowState windowState, int transition) {
        DisplayState displayState = mDisplayStates.get(windowState.getDisplayId());
        if (displayState == null) {
            return;
        }
        if (DEBUG_WINDOW_TRANSITIONS) {
            Slog.i(LOG_TAG, "Window transition: "
                    + PhoneWindowManager.windowTransitionToString(transition)
                    + " displayId: " + windowState.getDisplayId());
        }
        final boolean magnifying = displayState.isMagnifyingLw();
        if (magnifying) {
            switch (transition) {
                case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN:
                case WindowManagerPolicy.TRANSIT_TASK_OPEN:
                case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT:
                case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN:
                case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE:
                case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
                    mHandler.obtainMessage(MESSAGE_NOTIFY_USER_CONTEXT_CHANGED,
                            displayState.mClient).sendToTarget();
                }
            }
        }
        final int type = windowState.mAttrs.type;
        if (type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
                || type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
                || type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG
                || type == WindowManager.LayoutParams.TYPE_KEYGUARD
                || type == WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) {
            switch (transition) {
                case WindowManagerPolicy.TRANSIT_ENTER:
                case WindowManagerPolicy.TRANSIT_SHOW:
                case WindowManagerPolicy.TRANSIT_EXIT:
                case WindowManagerPolicy.TRANSIT_HIDE: {
                    displayState.mViewport.recomputeBoundsLw();
                } break;
            }
        }
        switch (transition) {
            case WindowManagerPolicy.TRANSIT_ENTER:
            case WindowManagerPolicy.TRANSIT_SHOW: {
                if (!magnifying) {
                    break;
                }
                switch (type) {
                    case WindowManager.LayoutParams.TYPE_APPLICATION:
                    case WindowManager.LayoutParams.TYPE_APPLICATION_PANEL:
                    case WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA:
                    case WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL:
                    case WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG:
                    case WindowManager.LayoutParams.TYPE_SEARCH_BAR:
                    case WindowManager.LayoutParams.TYPE_PHONE:
                    case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT:
                    case WindowManager.LayoutParams.TYPE_TOAST:
                    case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY:
                    case WindowManager.LayoutParams.TYPE_PRIORITY_PHONE:
                    case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG:
                    case WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG:
                    case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR:
                    case WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY:
                    case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL:
                    case WindowManager.LayoutParams.TYPE_RECENTS_OVERLAY: {
                        Rect magnifiedRegionBounds = mTempRect1;
                        displayState.getMagnifiedFrameInContentCoordsLw(magnifiedRegionBounds);
                        Rect touchableRegionBounds = mTempRect;
                        windowState.getTouchableRegion(mTempRegion);
                        mTempRegion.getBounds(touchableRegionBounds);
                        if (!magnifiedRegionBounds.intersect(touchableRegionBounds)) {
                            SomeArgs args = SomeArgs.obtain();
                            args.arg1 = displayState.mClient;
                            args.argi1 = touchableRegionBounds.left;
                            args.argi2 = touchableRegionBounds.top;
                            args.argi3 = touchableRegionBounds.right;
                            args.argi4 = touchableRegionBounds.bottom;
                            mHandler.obtainMessage(MESSAGE_NOTIFY_RECTANGLE_ON_SCREEN_REQUESTED,
                                    args).sendToTarget();
                        }
                    } break;
                } break;
            }
        }
    }

    public MagnificationSpec getMagnificationSpecLw(WindowState windowState) {
        DisplayState displayState = mDisplayStates.get(windowState.getDisplayId());
        if (displayState == null) {
            return null;
        }
        MagnificationSpec spec = displayState.mMagnificationSpec;
        if (spec != null && !spec.isNop()) {
            if (windowState.mAttachedWindow != null) {
                if (!canMagnifyWindow(windowState.mAttachedWindow.mAttrs.type)) {
                    return null;
                }
            }
            if (!canMagnifyWindow(windowState.mAttrs.type)) {
                return null;
            }
        }
        return spec;
    }

    private void enforceCallingPermission(String permission, String function) {
        if (Process.myPid() == Binder.getCallingPid()) {
            return;
        }
        if (mContext.checkCallingPermission(permission) != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("The caller does not have " + permission
                    + " required to call " + function);
        }
    }

    private static boolean canMagnifyWindow(int type) {
        switch (type) {
            case WindowManager.LayoutParams.TYPE_INPUT_METHOD:
            case WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG:
            case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR:
            case WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY: {
                return false;
            }
        }
        return true;
    }

    private static final class Viewport {

        private final ArrayList<WindowStateInfo> mTempWindowStateInfoList =
                new ArrayList<WindowStateInfo>();

        private final Rect mTempRect1 = new Rect();
        private final Rect mTempRect2 = new Rect();
        private final Rect mTempRect3 = new Rect();

        private final Rect mBounds = new Rect();
        private final Handler mHandler;
        private final IDisplayMagnificationController mClient;
        private final WindowManagerService mWindowManagerService;
        private final DisplayInfo mDisplayInfo;

        private final int mDisplayId;

        public Viewport(Context context, int displayId, Handler handler,
                IDisplayMagnificationController client, WindowManagerService windowManagerService) {
            mDisplayId = displayId;
            mHandler = handler;
            mWindowManagerService = windowManagerService;
            mDisplayInfo = mWindowManagerService.getDisplayContentLocked(displayId)
                    .getDisplayInfo();
            mClient = client;
            recomputeBoundsLw();
        }

        private final Comparator<WindowStateInfo> mWindowInfoInverseComparator =
                new Comparator<WindowStateInfo>() {
            @Override
            public int compare(WindowStateInfo lhs, WindowStateInfo rhs) {
                if (lhs.mWindowState.mLayer != rhs.mWindowState.mLayer) {
                    return rhs.mWindowState.mLayer - lhs.mWindowState.mLayer;
                }
                if (lhs.mTouchableRegion.top != rhs.mTouchableRegion.top) {
                    return rhs.mTouchableRegion.top - lhs.mTouchableRegion.top;
                }
                if (lhs.mTouchableRegion.left != rhs.mTouchableRegion.left) {
                    return rhs.mTouchableRegion.left - lhs.mTouchableRegion.left;
                }
                if (lhs.mTouchableRegion.right != rhs.mTouchableRegion.right) {
                    return rhs.mTouchableRegion.right - lhs.mTouchableRegion.right;
                }
                if (lhs.mTouchableRegion.bottom != rhs.mTouchableRegion.bottom) {
                    return rhs.mTouchableRegion.bottom - lhs.mTouchableRegion.bottom;
                }
                return 0;
            }
        };

        public void recomputeBoundsLw() {
            Rect magnifiedFrame = mBounds;
            magnifiedFrame.set(0, 0, 0, 0);

            Rect oldmagnifiedFrame = mTempRect3;
            oldmagnifiedFrame.set(magnifiedFrame);

            Rect availableFrame = mTempRect1;
            availableFrame.set(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);

            ArrayList<WindowStateInfo> visibleWindows = mTempWindowStateInfoList;
            visibleWindows.clear();
            getVisibleWindowsLw(visibleWindows);

            Collections.sort(visibleWindows, mWindowInfoInverseComparator);

            final int visibleWindowCount = visibleWindows.size();
            for (int i = 0; i < visibleWindowCount; i++) {
                WindowStateInfo info = visibleWindows.get(i);
                if (info.mWindowState.mAttrs.type == WindowManager
                        .LayoutParams.TYPE_MAGNIFICATION_OVERLAY) {
                    continue;
                }
                Rect windowFrame = mTempRect2;
                windowFrame.set(info.mTouchableRegion);
                if (canMagnifyWindow(info.mWindowState.mAttrs.type)) {
                    magnifiedFrame.union(windowFrame);
                    magnifiedFrame.intersect(availableFrame);
                } else {
                    subtract(windowFrame, magnifiedFrame);
                    subtract(availableFrame, windowFrame);
                }
                if (availableFrame.equals(magnifiedFrame)) {
                    break;
                }
            }
            for (int i = visibleWindowCount - 1; i >= 0; i--) {
                visibleWindows.remove(i).recycle();
            }

            final int displayWidth = mDisplayInfo.logicalWidth;
            final int displayHeight = mDisplayInfo.logicalHeight;
            magnifiedFrame.intersect(0, 0, displayWidth, displayHeight);

            if (!oldmagnifiedFrame.equals(magnifiedFrame)) {
                SomeArgs args = SomeArgs.obtain();
                args.arg1 = mClient;
                args.argi1 = magnifiedFrame.left;
                args.argi2 = magnifiedFrame.top;
                args.argi3 = magnifiedFrame.right;
                args.argi4 = magnifiedFrame.bottom;
                mHandler.obtainMessage(MESSAGE_NOTIFY_MAGNIFIED_FRAME_CHANGED, args)
                        .sendToTarget();
            }
        }

        private void getVisibleWindowsLw(ArrayList<WindowStateInfo> outWindowStates) {
            DisplayContent displayContent = mWindowManagerService.getDisplayContentLocked(
                    mDisplayId);
            WindowList windowList = displayContent.getWindowList();
            final int windowCount = windowList.size();
            for (int i = 0; i < windowCount; i++) {
                WindowState windowState = windowList.get(i);
                if (windowState.isVisibleLw() || windowState.mAttrs.type == WindowManager
                        .LayoutParams.TYPE_UNIVERSE_BACKGROUND) {
                    outWindowStates.add(WindowStateInfo.obtain(windowState));
                }
            }
        }

        public Rect getBoundsLw() {
            return mBounds;
        }

        private static boolean subtract(Rect lhs, Rect rhs) {
            if (lhs.right < rhs.left || lhs.left  > rhs.right
                    || lhs.bottom < rhs.top || lhs.top > rhs.bottom) {
                return false;
            }
            if (lhs.left < rhs.left) {
                lhs.right = rhs.left;
            }
            if (lhs.top < rhs.top) {
                lhs.bottom = rhs.top;
            }
            if (lhs.right > rhs.right) {
                lhs.left = rhs.right;
            }
            if (lhs.bottom > rhs.bottom) {
                lhs.top = rhs.bottom;
            }
            return true;
        }

        private static final class WindowStateInfo {
            private static final int MAX_POOL_SIZE = 30;

            private static final SynchronizedPool<WindowStateInfo> sPool =
                    new SynchronizedPool<WindowStateInfo>(MAX_POOL_SIZE);

            private static final Region mTempRegion = new Region();

            public WindowState mWindowState;
            public final Rect mTouchableRegion = new Rect();

            public static WindowStateInfo obtain(WindowState windowState) {
                WindowStateInfo info = sPool.acquire();
                if (info == null) {
                    info = new WindowStateInfo();
                }
                info.mWindowState = windowState;
                windowState.getTouchableRegion(mTempRegion);
                mTempRegion.getBounds(info.mTouchableRegion);
                return info;
            }

            public void recycle() {
                mWindowState = null;
                mTouchableRegion.setEmpty();
                sPool.release(this);
            }
        }
    }

    private final class DisplayState {
        final int mDisplayId;
        final MagnificationSpec mMagnificationSpec;
        final Viewport mViewport;
        final IDisplayMagnificationController mClient;

        DisplayState(int displayId, IDisplayMagnificationController client) {
            mDisplayId = displayId;
            mClient = client;
            mMagnificationSpec = MagnificationSpec.obtain();
            mViewport = new Viewport(mContext, mDisplayId, mHandler,
                    mClient, mWindowManagerService);
        }

        public boolean isMagnifyingLw() {
            return mMagnificationSpec.scale > 1.0f;
        }

        private void getMagnifiedFrameInContentCoordsLw(Rect rect) {
            MagnificationSpec spec = mMagnificationSpec;
            rect.set(mViewport.getBoundsLw());
            rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
            rect.scale(1.0f / spec.scale);
        }

        public void clearLw() {
            mMagnificationSpec.recycle();
        }
    }
}
