package com.jme3.app;

/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.input.vr.VRAPI;
import com.jme3.input.vr.VRInputAPI;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.post.PreNormalCaching;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.system.AppSettings;
import com.jme3.util.VRGUIPositioningMode;
import com.jme3.util.VRGuiManager;
import com.jme3.util.VRMouseManager;
import com.jme3.util.VRViewManager;

import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A JMonkey app state dedicated to Virtual Reality. 
 * An application that want to use VR devices (HTC vive, ...) has to use this app state.<br>
 * As this app state and the main {@link Application application} have to share {@link AppSettings application settings}, 
 * the common way to use this app state is:<br>
 * <ul>
 * <li>To create {@link AppSettings application settings} and set the VR related settings (see {@link VRConstants}).
 * <li>To instantiate this app state with the created settings.
 * <li>To instantiate the main {@link Application application} and to attach it to the created settings (with {@link Application#setSettings(AppSettings) setSettings(AppSettings)}).
 * <li>To start the main {@link Application application}.
 * </ul>
 * Attaching an instance of this app state to an already started application may cause crashes.
 * @author Julien Seinturier - JOrigin project - <a href="http://www.jorigin.org">http:/www.jorigin.org</a>
 */
public class VRAppState extends AbstractAppState {

    private static final Logger logger = Logger.getLogger(VRAppState.class.getName());
    
    /**
     * Is the application has not to start within VR mode (default is <code>false</code>).
     */
    public boolean DISABLE_VR = false;
    


    private float fFar  = 1000f;
    private float fNear = 0.1f;
    private int xWin    = 1920;
    private int yWin    = 1080;
    
    private float resMult = 1f;
 
    /*
     where is the headset pointing, after all rotations are combined?
     depends on observer rotation, if any
     */
    private Quaternion tempq = new Quaternion();
    
    private Application application      = null;
    private AppStateManager stateManager = null;
    private AppSettings settings         = null;
    
    private VREnvironment environment    = null;
    
    /**
     * Create a new default VR app state that relies on the given {@link VREnvironment VR environment}.
     * @param environment the {@link VREnvironment VR environment} that this app state is using.
     */
    public VRAppState(VREnvironment environment) {
      super();

      this.environment = environment; 
      
      this.setSettings(environment.getSettings());
     }
    
    /**
     * Create a new VR app state with given settings. The app state relies on the the given {@link VREnvironment VR environment}.
     * @param settings the settings to use.
     * @param environment the {@link VREnvironment VR environment} that this app state is using.
     */
    public VRAppState(AppSettings settings, VREnvironment environment){
      this(environment);
      this.settings = settings;
      processSettings(settings);
    }

    
    /**
     * Simple update of the app state, this method should contains any spatial updates.
     * This method is called by the {@link #update(float) update()} method and should not be called manually.
     * @param tpf the application time.
     */
    public void simpleUpdate(float tpf) {
    	return;
    }
    
    /**
     * Rendering callback of the app state. This method is called by the {@link #update(float) update()} method and should not be called manually.
     * @param renderManager the {@link RenderManager render manager}.
     */
    public void simpleRender(RenderManager renderManager) {
        PreNormalCaching.resetCache(environment.isInVR());
    }

    /**
     * Set the frustrum values for the application.
     * @param near the frustrum near value.
     * @param far the frustrum far value.
     */
    public void setFrustrumNearFar(float near, float far) {
        fNear = near;
        fFar = far;
    }
    
    /**
     * Set the mirror window size in pixel.
     * @param width the width of the mirror window in pixel.
     * @param height the height of the mirror window in pixel.
     */
    public void setMirrorWindowSize(int width, int height) {
        xWin = width;
        yWin = height;
    }
    
    /**
     * Set the resolution multiplier.
     * @param val the resolution multiplier.
     */
    public void setResolutionMultiplier(float val) {
        resMult = val;
        if( environment.getVRViewManager() != null ){
        	environment.getVRViewManager().setResolutionMultiplier(resMult);
        }
    }
   
	
    /**
     * Move filters from the main scene into the eye's.
     * This removes filters from the main scene.
     */
    public void moveScreenProcessingToVR() {
      environment.getVRViewManager().moveScreenProcessingToEyes();
    }
    
    /**
     * Get the observer final rotation within the scene.
     * @return the observer final rotation within the scene.
     * @see #getFinalObserverPosition()
     */
    public Quaternion getFinalObserverRotation() {
        if( environment.getVRViewManager() == null ) {
            if( environment.getObserver() == null ) {
                return environment.getCamera().getRotation();
            } else {
            	return ((Spatial)environment.getObserver()).getWorldRotation();
            }
        }  
        
        if( environment.getObserver() == null ) {
            tempq.set(environment.getDummyCamera().getRotation());
        } else {
            tempq.set(((Spatial)environment.getObserver()).getWorldRotation());
        }
        return tempq.multLocal(environment.getVRHardware().getOrientation());
    }
    
    /**
     * Get the observer final position within the scene.
     * @return the observer position.
     * @see #getFinalObserverRotation()
     */
    public Vector3f getFinalObserverPosition() {
        if( environment.getVRViewManager() == null ) {
            if( environment.getObserver() == null ) {
                return environment.getCamera().getLocation();
            } else{
            	return ((Spatial)environment.getObserver()).getWorldTranslation();            
            }
        }
        
        Vector3f pos = environment.getVRHardware().getPosition();
        if( environment.getObserver() == null ) {
        	environment.getDummyCamera().getRotation().mult(pos, pos);
            return pos.addLocal(environment.getDummyCamera().getLocation());
        } else {
        	((Spatial)environment.getObserver()).getWorldRotation().mult(pos, pos);
            return pos.addLocal(((Spatial)environment.getObserver()).getWorldTranslation());
        }
    }
    
    /**
     * Get the VR headset left viewport.
     * @return the VR headset left viewport.
     * @see #getRightViewPort()
     */
    public ViewPort getLeftViewPort() {
        if( environment.getVRViewManager() == null ){
        	return application.getViewPort();
        }
        
        return environment.getVRViewManager().getLeftViewport();
    }
    
    /**
     * Get the VR headset right viewport.
     * @return the VR headset right viewport.
     * @see #getLeftViewPort()
     */
    public ViewPort getRightViewPort() {
        if( environment.getVRViewManager() == null ){
        	return application.getViewPort();
        }
        return environment.getVRViewManager().getRightViewport();
    }
    
    /**
     * Set the background color for both left and right view ports.
     * @param clr the background color.
     */
    public void setBackgroundColors(ColorRGBA clr) {
        if( environment.getVRViewManager() == null ) {
            application.getViewPort().setBackgroundColor(clr);
        } else if( environment.getVRViewManager().getLeftViewport() != null ) {
        	
        	environment.getVRViewManager().getLeftViewport().setBackgroundColor(clr);
            
        	if( environment.getVRViewManager().getRightViewport() != null ){
            	environment.getVRViewManager().getRightViewport().setBackgroundColor(clr);
            }
        }
    }
    
    /**
     * Get the {@link Application} to which this app state is attached.
     * @return the {@link Application} to which this app state is attached.
     * @see #getStateManager()
     */
    public Application getApplication(){
    	return application;
    }
    
    /**
     * Get the {@link AppStateManager state manager} to which this app state is attached.
     * @return the {@link AppStateManager state manager} to which this app state is attached.
     * @see #getApplication()
     */
    public AppStateManager getStateManager(){
    	return stateManager;
    }
    
    /**
     * Get the scene observer. If no observer has been set, this method return the application {@link #getCamera() camera}.
     * @return the scene observer. 
     * @see #setObserver(Spatial)
     */
    public Object getObserver() {
        return environment.getObserver();
    }
    
    /**
     * Set the scene observer. The VR headset will be linked to it. If no observer is set, the VR headset is linked to the the application {@link #getCamera() camera}.
     * @param observer the scene observer.
     */
    public void setObserver(Spatial observer) {
       environment.setObserver(observer);
    }
    
    /**
     * Check if the rendering is instanced (see <a href="https://en.wikipedia.org/wiki/Geometry_instancing">Geometry instancing</a>).
     * @return <code>true</code> if the rendering is instanced and <code>false</code> otherwise.
     */
    public boolean isInstanceRendering() {
        return environment.isInstanceRendering();
    }
    
    /**
     * Return the {@link VREnvironment VR environment} on which this app state relies. 
     * @return the {@link VREnvironment VR environment} on which this app state relies. 
     */
    public VREnvironment getVREnvironment(){
    	return environment;
    }
    
	/**
	 * Get the VR underlying hardware.
	 * @return the VR underlying hardware.
	 */
	public VRAPI getVRHardware() {
	    return getVREnvironment().getVRHardware();
	}
	
	/**
	 * Get the VR dedicated input.
	 * @return the VR dedicated input.
	 */
	public VRInputAPI getVRinput() {
	    if( getVREnvironment().getVRHardware() == null ){
	    	return null;
	    }
	    
	    return getVREnvironment().getVRHardware().getVRinput();
	}
	
	/**
	 * Get the VR view manager.
	 * @return the VR view manager.
	 */
	public VRViewManager getVRViewManager() {
	    return getVREnvironment().getVRViewManager();
	}
	
	/**
	 * Get the GUI manager attached to this app state.
	 * @return the GUI manager attached to this app state.
	 */
	public VRGuiManager getVRGUIManager(){
		return getVREnvironment().getVRGUIManager();
	}
	
	/**
	 * Get the VR mouse manager attached to this app state.
	 * @return the VR mouse manager attached to this application.
	 */
	public VRMouseManager getVRMouseManager(){
		return getVREnvironment().getVRMouseManager();
	}
    
	/**
	 * Get the {@link AppSettings settings} attached to this app state.
	 * @return the {@link AppSettings settings} attached to this app state.
	 * @see #setSettings(AppSettings)
	 */
	public AppSettings getSettings(){
		return settings;
	}
	
	/**
	 * Set the {@link AppSettings settings} attached to this app state.
	 * @param settings the {@link AppSettings settings} attached to this app state.
	 * @see #getSettings()
	 */
	public void setSettings(AppSettings settings){
		this.settings = settings;
		processSettings(settings);
	}
	
    @Override
    public void update(float tpf) {    
        
        // update VR pose & cameras
        if( environment.getVRViewManager() != null ) {
        	environment.getVRViewManager().update(tpf);    
        } else if( environment.getObserver() != null ) {
            environment.getCamera().setFrame(((Spatial)environment.getObserver()).getWorldTranslation(), ((Spatial)environment.getObserver()).getWorldRotation());
        }
        
        //FIXME: check if this code is necessary.
        // Updates scene and gui states.
        Iterator<Spatial> spatialIter = application.getViewPort().getScenes().iterator();
        Spatial spatial = null;
        while(spatialIter.hasNext()){
        	spatial = spatialIter.next();
        	spatial.updateLogicalState(tpf);
        	spatial.updateGeometricState();
        }        
        
        if( environment.isInVR() == false || environment.getVRGUIManager().getPositioningMode() == VRGUIPositioningMode.MANUAL ) {
            // only update geometric state here if GUI is in manual mode, or not in VR
            // it will get updated automatically in the viewmanager update otherwise
        	spatialIter = application.getGuiViewPort().getScenes().iterator();
            spatial = null;
            while(spatialIter.hasNext()){
            	spatial = spatialIter.next();
            	spatial.updateGeometricState();
            }    
        }
        
        // use the analog control on the first tracked controller to push around the mouse
        environment.getVRMouseManager().updateAnalogAsMouse(0, null, null, null, tpf);
    }

    @Override
    public void postRender() {
        super.postRender();
        
        // update compositor
        if( environment.getVRViewManager() != null ) {
        	environment.getVRViewManager().postRender();
        }
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        
        this.application  = app;
        this.stateManager = stateManager;
        
        // disable annoying warnings about GUI stuff being updated, which is normal behavior
        // for late GUI placement for VR purposes
        Logger.getLogger("com.jme3").setLevel(Level.SEVERE);     
        
        app.getCamera().setFrustumFar(fFar);
        app.getCamera().setFrustumNear(fNear);

        if( environment.isInVR() ) {
        	
        	logger.config("VR mode enabled.");
        	
            if( environment.getVRHardware() != null ) {
            	environment.getVRHardware().initVRCompositor(environment.compositorAllowed());
            } else {
            	logger.warning("No VR system found.");
            }
            
            
            environment.getVRViewManager().setResolutionMultiplier(resMult);
            //inputManager.addMapping(RESET_HMD, new KeyTrigger(KeyInput.KEY_F9));
            //setLostFocusBehavior(LostFocusBehavior.Disabled);
        } else {
        	logger.config("VR mode disabled.");
            //viewPort.attachScene(rootNode);
            //guiViewPort.attachScene(guiNode);
        }
        
        if( environment.getVRViewManager() != null ) {
        	environment.getVRViewManager().initialize();
        }
    }
    
    @Override
    public void stateAttached(AppStateManager stateManager) {
        super.stateAttached(stateManager); //To change body of generated methods, choose Tools | Templates.
        
        if (settings == null) {
            settings = new AppSettings(true);
            logger.config("Using default settings.");
        } else {
        	logger.config("Using given settings.");
        }
   
        // Attach VR environment to the application
        if (!environment.isInitialized()){
        	environment.initialize();
        }
        
        if (environment.isInitialized()){
        	environment.atttach(this, stateManager.getApplication());
        } else {
        	logger.severe("Cannot attach VR environment to the VR app state as its not initialized.");
        }

        GraphicsDevice defDev = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
                                    
        if( environment.isInVR() && !environment.compositorAllowed() ) {
            // "easy extended" mode
            // setup experimental JFrame on external device
            // first, find the VR device
            GraphicsDevice VRdev = null;
            GraphicsDevice[] devs = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
            // pick the display that isn't the default one
            for(GraphicsDevice gd : devs) {
                if( gd != defDev ) {
                    VRdev = gd;
                    break;
                }
            }

            // did we get the VR device?
            if( VRdev != null ) {
                // set properties for VR acceleration
                try {   
                    java.awt.DisplayMode useDM = null;
                    int max = 0;
                    for(java.awt.DisplayMode dm : VRdev.getDisplayModes()) {
                        int check = dm.getHeight() + dm.getWidth() + dm.getRefreshRate() + dm.getBitDepth();
                        if( check > max ) {
                            max = check;
                            useDM = dm;
                        }
                    }
                    
                    // create a window for the VR device
                    settings.setWidth(useDM.getWidth());
                    settings.setHeight(useDM.getHeight());
                    settings.setBitsPerPixel(useDM.getBitDepth());
                    settings.setFrequency(useDM.getRefreshRate());
                    settings.setSwapBuffers(true);
                    settings.setVSync(true); // allow vsync on this display
                    stateManager.getApplication().setSettings(settings);
                    logger.config("Updated underlying application settings.");
                    
                    //VRdev.setFullScreenWindow(VRwindow);
                    // make sure we are in the right display mode
                    if( VRdev.getDisplayMode().equals(useDM) == false ) {
                        VRdev.setDisplayMode(useDM);
                    }
                    
                    return;
                } catch(Exception e) { 
                    logger.log(Level.SEVERE, e.getMessage(), e);
                }
            } else {
            	logger.config("Cannot access to external screen.");
            }
        } else {
        	if (!environment.isInVR()){
        	  logger.config("Cannot switch to VR mode (VR disabled by user).");
        	} else if (!environment.compositorAllowed()){
        	  logger.warning("Cannot switch to VR mode (VR not supported).");
        	}
        }
        
        if( !environment.isInVR() ) {
        	
        	//FIXME: Handling GLFW workaround on MacOS
        	boolean macOs = false;
            if (macOs) {
                // GLFW workaround on macs
                settings.setFrequency(defDev.getDisplayMode().getRefreshRate());
                settings.setDepthBits(24);
                settings.setVSync(true);
                // try and read resolution from file in local dir
                File resfile = new File("resolution.txt");
                if( resfile.exists() ) {
                    try {
                        BufferedReader br = new BufferedReader(new FileReader(resfile));
                        settings.setWidth(Integer.parseInt(br.readLine()));
                        settings.setHeight(Integer.parseInt(br.readLine()));
                        try {
                            settings.setFullscreen(br.readLine().toLowerCase(Locale.ENGLISH).contains("full"));
                        } catch(Exception e) {
                            settings.setFullscreen(false);
                        }
                        br.close();
                    } catch(Exception e) {
                        settings.setWidth(1280);
                        settings.setHeight(720);
                    }
                } else {
                    settings.setWidth(1280);
                    settings.setHeight(720);
                    settings.setFullscreen(false);
                }
                settings.setResizable(false);
            }
            settings.setSwapBuffers(true);
        } else {
            // use basic mirroring window, skip settings window
            settings.setSamples(1);
            settings.setWidth(xWin);
            settings.setHeight(yWin);
            settings.setBitsPerPixel(32);     
            settings.setFrameRate(0);
            settings.setFrequency(environment.getVRHardware().getDisplayFrequency());
            settings.setFullscreen(false);
            settings.setVSync(false); // stop vsyncing on primary monitor!
            settings.setSwapBuffers(environment.isSwapBuffers());
        }

        // Updating application settings
        stateManager.getApplication().setSettings(settings);
        logger.config("Updated underlying application settings.");
        
    }

    @Override
    public void cleanup() {
        if( environment.getVRHardware() != null ) {
        	environment.getVRHardware().destroy();
        }        
        
        this.application  = null;
        this.stateManager = null;
    }
    
    @Override
    public void stateDetached(AppStateManager stateManager) {
      super.stateDetached(stateManager);
    }
    
    /**
     * Process the attached settings and apply changes to this app state.
     * @param settings the app settings to process.
     */
    protected void processSettings(AppSettings settings){
    	if (settings != null){

            if (settings.get(VRConstants.SETTING_DISABLE_VR) != null){
                DISABLE_VR = settings.getBoolean(VRConstants.SETTING_DISABLE_VR);
    		}
    	}
    }
}