package com.android.server.vr;

import static android.view.Display.INVALID_DISPLAY;

import android.app.ActivityManagerInternal;
import android.app.Vr2dDisplayProperties;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.ImageReader;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.service.vr.IPersistentVrStateCallbacks;
import android.service.vr.IVrManager;
import android.util.Log;
import android.view.Surface;

import com.android.server.wm.WindowManagerInternal;

/**
 * Creates a 2D Virtual Display while VR Mode is enabled. This display will be used to run and
 * render 2D app within a VR experience. For example, bringing up the 2D calculator app in VR.
 */
class Vr2dDisplay {
    private final static String TAG = "Vr2dDisplay";
    private final static boolean DEBUG = false;

    private int mVirtualDisplayHeight;
    private int mVirtualDisplayWidth;
    private int mVirtualDisplayDpi;
    private final static int STOP_VIRTUAL_DISPLAY_DELAY_MILLIS = 2000;
    private final static String UNIQUE_DISPLAY_ID = "277f1a09-b88d-4d1e-8716-796f114d080b";
    private final static String DISPLAY_NAME = "VR 2D Display";

    private final static String DEBUG_ACTION_SET_MODE =
            "com.android.server.vr.Vr2dDisplay.SET_MODE";
    private final static String DEBUG_EXTRA_MODE_ON =
            "com.android.server.vr.Vr2dDisplay.EXTRA_MODE_ON";
    private final static String DEBUG_ACTION_SET_SURFACE =
            "com.android.server.vr.Vr2dDisplay.SET_SURFACE";
    private final static String DEBUG_EXTRA_SURFACE =
            "com.android.server.vr.Vr2dDisplay.EXTRA_SURFACE";

    /**
     * The default width of the VR virtual display
     */
    public static final int DEFAULT_VIRTUAL_DISPLAY_WIDTH = 1400;

    /**
     * The default height of the VR virtual display
     */
    public static final int DEFAULT_VIRTUAL_DISPLAY_HEIGHT = 1800;

    /**
     * The default height of the VR virtual dpi.
     */
    public static final int DEFAULT_VIRTUAL_DISPLAY_DPI = 320;

    /**
     * The minimum height, width and dpi of VR virtual display.
     */
    public static final int MIN_VR_DISPLAY_WIDTH = 1;
    public static final int MIN_VR_DISPLAY_HEIGHT = 1;
    public static final int MIN_VR_DISPLAY_DPI = 1;

    private final ActivityManagerInternal mActivityManagerInternal;
    private final WindowManagerInternal mWindowManagerInternal;
    private final DisplayManager mDisplayManager;
    private final IVrManager mVrManager;
    private final Object mVdLock = new Object();
    private final Handler mHandler = new Handler();

    /**
     * Callback implementation to receive changes to VrMode.
     **/
    private final IPersistentVrStateCallbacks mVrStateCallbacks =
            new IPersistentVrStateCallbacks.Stub() {
        @Override
        public void onPersistentVrStateChanged(boolean enabled) {
            if (enabled != mIsPersistentVrModeEnabled) {
                mIsPersistentVrModeEnabled = enabled;
                updateVirtualDisplay();
            }
        }
    };

    private VirtualDisplay mVirtualDisplay;
    private Surface mSurface;
    private ImageReader mImageReader;
    private Runnable mStopVDRunnable;
    private boolean mIsVrModeOverrideEnabled;  // debug override to set vr mode.
    private boolean mIsVirtualDisplayAllowed = true;  // Virtual-display feature toggle
    private boolean mIsPersistentVrModeEnabled;  // indicates we are in vr persistent mode.
    private boolean mBootsToVr = false;  // The device boots into VR (standalone VR device)

    public Vr2dDisplay(DisplayManager displayManager,
           ActivityManagerInternal activityManagerInternal,
           WindowManagerInternal windowManagerInternal, IVrManager vrManager) {
        mDisplayManager = displayManager;
        mActivityManagerInternal = activityManagerInternal;
        mWindowManagerInternal = windowManagerInternal;
        mVrManager = vrManager;
        mVirtualDisplayWidth = DEFAULT_VIRTUAL_DISPLAY_WIDTH;
        mVirtualDisplayHeight = DEFAULT_VIRTUAL_DISPLAY_HEIGHT;
        mVirtualDisplayDpi = DEFAULT_VIRTUAL_DISPLAY_DPI;
    }

    /**
     * Initializes the compabilitiy display by listening to VR mode changes.
     */
    public void init(Context context, boolean bootsToVr) {
        startVrModeListener();
        startDebugOnlyBroadcastReceiver(context);
        mBootsToVr = bootsToVr;
        if (mBootsToVr) {
          // If we are booting into VR, we need to start the virtual display immediately. This
          // ensures that the virtual display is up by the time Setup Wizard is started.
          updateVirtualDisplay();
        }
    }

    /**
     * Creates and Destroys the virtual display depending on the current state of VrMode.
     */
    private void updateVirtualDisplay() {
        if (DEBUG) {
            Log.i(TAG, "isVrMode: " + mIsPersistentVrModeEnabled + ", override: "
                    + mIsVrModeOverrideEnabled + ", isAllowed: " + mIsVirtualDisplayAllowed
                    + ", bootsToVr: " + mBootsToVr);
        }

        if (shouldRunVirtualDisplay()) {
            Log.i(TAG, "Attempting to start virtual display");
            // TODO: Consider not creating the display until ActivityManager needs one on
            // which to display a 2D application.
            startVirtualDisplay();
        } else {
            // Stop virtual display to test exit condition
            stopVirtualDisplay();
        }
    }

    /**
     * Creates a DEBUG-only BroadcastReceiver through which a test app can simulate VrMode and
     * set a custom Surface for the virtual display.  This allows testing of the virtual display
     * without going into full 3D.
     *
     * @param context The context.
     */
    private void startDebugOnlyBroadcastReceiver(Context context) {
        if (DEBUG) {
            IntentFilter intentFilter = new IntentFilter(DEBUG_ACTION_SET_MODE);
            intentFilter.addAction(DEBUG_ACTION_SET_SURFACE);

            context.registerReceiver(new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    final String action = intent.getAction();
                    if (DEBUG_ACTION_SET_MODE.equals(action)) {
                        mIsVrModeOverrideEnabled =
                                intent.getBooleanExtra(DEBUG_EXTRA_MODE_ON, false);
                        updateVirtualDisplay();
                    } else if (DEBUG_ACTION_SET_SURFACE.equals(action)) {
                        if (mVirtualDisplay != null) {
                            if (intent.hasExtra(DEBUG_EXTRA_SURFACE)) {
                                setSurfaceLocked(intent.getParcelableExtra(DEBUG_EXTRA_SURFACE));
                            }
                        } else {
                            Log.w(TAG, "Cannot set the surface because the VD is null.");
                        }
                    }
                }
            }, intentFilter);
        }
    }

    /**
     * Starts listening to VrMode changes.
     */
    private void startVrModeListener() {
        if (mVrManager != null) {
            try {
                mVrManager.registerPersistentVrStateListener(mVrStateCallbacks);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not register VR State listener.", e);
            }
        }
    }

    /**
     * Sets the resolution and DPI of the Vr2d virtual display used to display
     * 2D applications in VR mode.
     *
     * <p>Requires {@link android.Manifest.permission#ACCESS_VR_MANAGER} permission.</p>
     *
     * @param displayProperties Properties of the virtual display for 2D applications
     * in VR mode.
     */
    public void setVirtualDisplayProperties(Vr2dDisplayProperties displayProperties) {
        synchronized(mVdLock) {
            if (DEBUG) {
                Log.i(TAG, "VD setVirtualDisplayProperties: " +
                        displayProperties.toString());
            }

            int width = displayProperties.getWidth();
            int height = displayProperties.getHeight();
            int dpi = displayProperties.getDpi();
            boolean resized = false;

            if (width < MIN_VR_DISPLAY_WIDTH || height < MIN_VR_DISPLAY_HEIGHT ||
                    dpi < MIN_VR_DISPLAY_DPI) {
                Log.i(TAG, "Ignoring Width/Height/Dpi values of " + width + "," + height + ","
                        + dpi);
            } else {
                Log.i(TAG, "Setting width/height/dpi to " + width + "," + height + "," + dpi);
                mVirtualDisplayWidth = width;
                mVirtualDisplayHeight = height;
                mVirtualDisplayDpi = dpi;
                resized = true;
            }

            if ((displayProperties.getFlags() & Vr2dDisplayProperties.FLAG_VIRTUAL_DISPLAY_ENABLED)
                    == Vr2dDisplayProperties.FLAG_VIRTUAL_DISPLAY_ENABLED) {
                mIsVirtualDisplayAllowed = true;
            } else if ((displayProperties.getRemovedFlags() &
                    Vr2dDisplayProperties.FLAG_VIRTUAL_DISPLAY_ENABLED)
                    == Vr2dDisplayProperties.FLAG_VIRTUAL_DISPLAY_ENABLED) {
                mIsVirtualDisplayAllowed = false;
            }

            if (mVirtualDisplay != null && resized && mIsVirtualDisplayAllowed) {
                mVirtualDisplay.resize(mVirtualDisplayWidth, mVirtualDisplayHeight,
                    mVirtualDisplayDpi);
                ImageReader oldImageReader = mImageReader;
                mImageReader = null;
                startImageReader();
                oldImageReader.close();
            }

            // Start/Stop the virtual display in case the updates indicated that we should.
            updateVirtualDisplay();
        }
    }

    /**
     * Returns the virtual display ID if one currently exists, otherwise returns
     * {@link INVALID_DISPLAY_ID}.
     *
     * @return The virtual display ID.
     */
    public int getVirtualDisplayId() {
        synchronized(mVdLock) {
            if (mVirtualDisplay != null) {
                int virtualDisplayId = mVirtualDisplay.getDisplay().getDisplayId();
                if (DEBUG) {
                    Log.i(TAG, "VD id: " + virtualDisplayId);
                }
                return virtualDisplayId;
            }
        }
        return INVALID_DISPLAY;
    }

    /**
     * Starts the virtual display if one does not already exist.
     */
    private void startVirtualDisplay() {
        if (DEBUG) {
            Log.d(TAG, "Request to start VD, DM:" + mDisplayManager);
        }

        if (mDisplayManager == null) {
            Log.w(TAG, "Cannot create virtual display because mDisplayManager == null");
            return;
        }

        synchronized (mVdLock) {
            if (mVirtualDisplay != null) {
                Log.i(TAG, "VD already exists, ignoring request");
                return;
            }

            int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
            flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
            flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
            flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
            flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
            mVirtualDisplay = mDisplayManager.createVirtualDisplay(null /* projection */,
                    DISPLAY_NAME, mVirtualDisplayWidth, mVirtualDisplayHeight, mVirtualDisplayDpi,
                    null /* surface */, flags, null /* callback */, null /* handler */,
                    UNIQUE_DISPLAY_ID);

            if (mVirtualDisplay != null) {
                updateDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
                // Now create the ImageReader to supply a Surface to the new virtual display.
                startImageReader();
            } else {
                Log.w(TAG, "Virtual display id is null after createVirtualDisplay");
                updateDisplayId(INVALID_DISPLAY);
                return;
            }
        }

        Log.i(TAG, "VD created: " + mVirtualDisplay);
    }

    private void updateDisplayId(int displayId) {
        mActivityManagerInternal.setVr2dDisplayId(displayId);
        mWindowManagerInternal.setVr2dDisplayId(displayId);
    }

    /**
     * Stops the virtual display with a {@link #STOP_VIRTUAL_DISPLAY_DELAY_MILLIS} timeout.
     * The timeout prevents the virtual display from bouncing in cases where VrMode goes in and out
     * of being enabled. This can happen sometimes with our 2D test app.
     */
    private void stopVirtualDisplay() {
        if (mStopVDRunnable == null) {
           mStopVDRunnable = new Runnable() {
               @Override
               public void run() {
                    if (shouldRunVirtualDisplay()) {
                        Log.i(TAG, "Virtual Display destruction stopped: VrMode is back on.");
                    } else {
                        Log.i(TAG, "Stopping Virtual Display");
                        synchronized (mVdLock) {
                            updateDisplayId(INVALID_DISPLAY);
                            setSurfaceLocked(null); // clean up and release the surface first.
                            if (mVirtualDisplay != null) {
                                mVirtualDisplay.release();
                                mVirtualDisplay = null;
                            }
                            stopImageReader();
                        }
                    }
               }
           };
        }

        mHandler.removeCallbacks(mStopVDRunnable);
        mHandler.postDelayed(mStopVDRunnable, STOP_VIRTUAL_DISPLAY_DELAY_MILLIS);
    }

    /**
     * Set the surface to use with the virtual display.
     *
     * Code should be locked by {@link #mVdLock} before invoked.
     *
     * @param surface The Surface to set.
     */
    private void setSurfaceLocked(Surface surface) {
        // Change the surface to either a valid surface or a null value.
        if (mSurface != surface && (surface == null || surface.isValid())) {
            Log.i(TAG, "Setting the new surface from " + mSurface + " to " + surface);
            if (mVirtualDisplay != null) {
                mVirtualDisplay.setSurface(surface);
            }
            if (mSurface != null) {
                mSurface.release();
            }
            mSurface = surface;
        }
    }

    /**
     * Starts an ImageReader as a do-nothing Surface.  The virtual display will not get fully
     * initialized within surface flinger unless it has a valid Surface associated with it. We use
     * the ImageReader as the default valid Surface.
     */
    private void startImageReader() {
        if (mImageReader == null) {
            mImageReader = ImageReader.newInstance(mVirtualDisplayWidth, mVirtualDisplayHeight,
                PixelFormat.RGBA_8888, 2 /* maxImages */);
            Log.i(TAG, "VD startImageReader: res = " + mVirtualDisplayWidth + "X" +
                    mVirtualDisplayHeight + ", dpi = " + mVirtualDisplayDpi);
        }
        synchronized (mVdLock) {
            setSurfaceLocked(mImageReader.getSurface());
        }
    }

    /**
     * Cleans up the ImageReader.
     */
    private void stopImageReader() {
        if (mImageReader != null) {
            mImageReader.close();
            mImageReader = null;
        }
    }

    private boolean shouldRunVirtualDisplay() {
        // Virtual Display should run whenever:
        // * Virtual Display is allowed/enabled AND
        // (1) BootsToVr is set indicating the device never leaves VR
        // (2) VR (persistent) mode is enabled
        // (3) VR mode is overridden to be enabled.
        return mIsVirtualDisplayAllowed &&
                (mBootsToVr || mIsPersistentVrModeEnabled || mIsVrModeOverrideEnabled);
    }
}
