/**
 * Copyright 2014 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */
package jogamp.opengl.oculusvr;

import com.jogamp.math.FovHVHalves;
import com.jogamp.math.Vec3f;
import com.jogamp.math.geom.Frustum;
import com.jogamp.nativewindow.util.Dimension;
import com.jogamp.nativewindow.util.DimensionImmutable;
import com.jogamp.nativewindow.util.PointImmutable;
import com.jogamp.nativewindow.util.Rectangle;
import com.jogamp.nativewindow.util.RectangleImmutable;
import com.jogamp.oculusvr.OVR;
import com.jogamp.oculusvr.OvrHmdContext;
import com.jogamp.oculusvr.ovrEyeRenderDesc;
import com.jogamp.oculusvr.ovrFovPort;
import com.jogamp.oculusvr.ovrHmdDesc;
import com.jogamp.oculusvr.ovrSizei;
import com.jogamp.oculusvr.ovrTrackingState;
import com.jogamp.opengl.util.stereo.LocationSensorParameter;
import com.jogamp.opengl.util.stereo.StereoDevice;
import com.jogamp.opengl.util.stereo.StereoDeviceFactory;
import com.jogamp.opengl.util.stereo.StereoDeviceRenderer;
import com.jogamp.opengl.util.stereo.StereoUtil;

public class OVRStereoDevice implements StereoDevice {
    /** 1.6 up, 5 forward */
    private static final Vec3f DEFAULT_EYE_POSITION_OFFSET = new Vec3f( 0.0f, 1.6f, -5.0f );

    private final StereoDeviceFactory factory;
    public final int deviceIndex;
    private final FovHVHalves[] defaultEyeFov;

    public ovrHmdDesc hmdDesc;
    public OvrHmdContext handle;

    private final int supportedSensorBits;
    private int usedSensorBits;
    private boolean sensorsStarted = false;

    private final int[] eyeRenderOrder;
    private final int supportedDistortionBits, recommendedDistortionBits, minimumDistortionBits;

    private final String deviceName;
    private final DimensionImmutable resolution;
    private final int requiredRotation;
    private final PointImmutable position;
    private final int dkVersion;

    private final LocationSensorParameter locationSensorParams;

    public OVRStereoDevice(final StereoDeviceFactory factory, final ovrHmdDesc hmdDesc, final int deviceIndex) {
        if( null == hmdDesc ) {
            throw new IllegalArgumentException("Passed null hmdDesc");
        }
        final OvrHmdContext nativeContext = hmdDesc.getHandle();
        if( null == nativeContext ) {
            throw new IllegalArgumentException("hmdDesc has null OvrHmdContext");
        }
        this.factory = factory;
        this.handle = nativeContext;
        this.deviceIndex = deviceIndex;
        this.hmdDesc = hmdDesc;

        final ovrFovPort[] defaultOVREyeFov = hmdDesc.getDefaultEyeFov(0, new ovrFovPort[ovrHmdDesc.getEyeRenderOrderElemCount()], 0, ovrHmdDesc.getEyeRenderOrderElemCount());
        defaultEyeFov = new FovHVHalves[defaultOVREyeFov.length];
        for(int i=0; i<defaultEyeFov.length; i++) {
            defaultEyeFov[i] = OVRUtil.getFovHV(defaultOVREyeFov[i]);
        }
        eyeRenderOrder = new int[ovrHmdDesc.getEyeRenderOrderElemCount()];
        hmdDesc.getEyeRenderOrder(0, eyeRenderOrder, 0, eyeRenderOrder.length);
        supportedDistortionBits = OVRUtil.ovrDistCaps2DistBits(hmdDesc.getDistortionCaps());
        recommendedDistortionBits = supportedDistortionBits; //  & ~StereoDeviceRenderer.DISTORTION_TIMEWARP;
        minimumDistortionBits = StereoDeviceRenderer.DISTORTION_BARREL;

        usedSensorBits = 0;
        supportedSensorBits = OVRUtil.ovrTrackingCaps2SensorBits(hmdDesc.getTrackingCaps());

        LocationSensorParameter _locationSensorParams = null;
        if( StereoUtil.usesPositionSensor(supportedSensorBits)) {
            try {
                final FovHVHalves posFov = FovHVHalves.byRadians(hmdDesc.getCameraFrustumHFovInRadians(),
                                                                 hmdDesc.getCameraFrustumVFovInRadians());
                final float posZNear = hmdDesc.getCameraFrustumNearZInMeters();
                final float posZFar = hmdDesc.getCameraFrustumFarZInMeters();
                _locationSensorParams = new LocationSensorParameter(new Frustum.FovDesc(posFov, posZNear, posZFar));
            } catch (final IllegalArgumentException iae) {
                // probably zNear/zFar issue ..
                System.err.println(iae.getMessage());
            }
        }
        locationSensorParams = _locationSensorParams;

        // DK1 delivers unrotated resolution in target orientation
        // DK2 delivers rotated resolution in target orientation, monitor screen is rotated 90deg clockwise
        deviceName = hmdDesc.getDisplayDeviceNameAsString();
        final ovrSizei res = hmdDesc.getResolution();
        resolution = new Dimension(res.getW(), res.getH());
        final int hmdType = hmdDesc.getType();
        switch( hmdType ) {
            case OVR.ovrHmd_DKHD:             // 4
            case 5:                           // OVR.ovrHmd_CrystalCoveProto:
            case OVR.ovrHmd_DK2:              // 6
                dkVersion = 2;
                requiredRotation = 90;
                break;
            default:
                dkVersion = 1;
                requiredRotation = 0;
                break;
        }
        position = OVRUtil.getVec2iAsPoint(hmdDesc.getWindowsPos());
    }

    @Override
    public final StereoDeviceFactory getFactory() { return factory; }

    @Override
    public final String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("OVRStereoDevice[product "+hmdDesc.getProductNameAsString());
        sb.append(", vendor "+hmdDesc.getManufacturerAsString());
        sb.append(", device "+deviceName);
        sb.append(", DK "+dkVersion);
        sb.append(", surfaceSize "+getSurfaceSize()+", reqRotation "+requiredRotation+" ccw-deg");
        sb.append(", surfacePos "+getPosition());
        sb.append(", distortionBits[supported ["+StereoUtil.distortionBitsToString(getSupportedDistortionBits())+
                      "], recommended ["+StereoUtil.distortionBitsToString(getRecommendedDistortionBits())+
                      "], minimum ["+StereoUtil.distortionBitsToString(getMinimumDistortionBits())+"]]");
        sb.append(", sensorBits[supported ["+StereoUtil.sensorBitsToString(getSupportedSensorBits())+
                      "], enabled ["+StereoUtil.sensorBitsToString(getEnabledSensorBits())+"]]");
        sb.append(", "+locationSensorParams+"]");
        return sb.toString();
    }

    @Override
    public final void dispose() {
       if( isValid() ) {
           stopSensors();
           OVR.ovrHmd_Destroy(hmdDesc);
           hmdDesc = null;
           handle = null;
       }
    }

    @Override
    public boolean isValid() {
        return null != hmdDesc && null != handle;
    }

    @Override
    public final PointImmutable getPosition() { return position; }

    @Override
    public final DimensionImmutable getSurfaceSize() { return resolution; }
    @Override
    public int getRequiredRotation() { return requiredRotation; }

    @Override
    public Vec3f getDefaultEyePositionOffset() { return DEFAULT_EYE_POSITION_OFFSET; }

    @Override
    public final FovHVHalves[] getDefaultFOV() { return defaultEyeFov; }

    @Override
    public final LocationSensorParameter getLocationSensorParams() { return locationSensorParams; }

    @Override
    public final void resetLocationSensorOrigin() {
        if( isValid() && sensorsStarted && StereoUtil.usesPositionSensor(supportedSensorBits)) {
            OVR.ovrHmd_RecenterPose(hmdDesc);
        }
    }

    /* pp */ void updateUsedSensorBits(final ovrTrackingState trackingState) {
        final int pre = usedSensorBits;
        if( sensorsStarted && null != trackingState ) {
            usedSensorBits = StereoDevice.SENSOR_ORIENTATION |
                             OVRUtil.ovrTrackingStats2SensorBits(trackingState.getStatusFlags());
        } else {
            usedSensorBits = 0;
        }
        if( StereoDevice.DEBUG ) {
            if( pre != usedSensorBits ) {
                System.err.println("XXX: Sensor Change: "+
                        ": pre["+StereoUtil.sensorBitsToString(pre)+"]"+
                        " -> now["+StereoUtil.sensorBitsToString(usedSensorBits)+"]");
            }
        }
    }

    @Override
    public final boolean startSensors(final int desiredSensorBits, final int requiredSensorBits) {
        if( isValid() && !sensorsStarted ) {
            if( requiredSensorBits != ( supportedSensorBits & requiredSensorBits ) ) {
                // required sensors not available
                if( StereoDevice.DEBUG ) {
                    System.err.println("XXX: startSensors failed: n/a required sensors ["+StereoUtil.sensorBitsToString(requiredSensorBits)+"]");
                }
                return false;
            }
            if( 0 == ( supportedSensorBits & ( requiredSensorBits | desiredSensorBits ) ) ) {
                // no sensors available
                if( StereoDevice.DEBUG ) {
                    System.err.println("XXX: startSensors failed: n/a any sensors");
                }
                return false;
            }
            // Start the sensor which provides the Rift’s pose and motion.
            final int requiredTrackingCaps = OVRUtil.sensorBits2TrackingCaps(requiredSensorBits);
            final int desiredTrackingCaps = requiredTrackingCaps | OVRUtil.sensorBits2TrackingCaps(desiredSensorBits);
            final boolean res;
            if( OVR.ovrHmd_ConfigureTracking(hmdDesc, desiredTrackingCaps, requiredTrackingCaps) ) {
                sensorsStarted = true;
                updateUsedSensorBits(OVR.ovrHmd_GetTrackingState(hmdDesc, 0.0));
                res = true;
            } else {
                res = false;
            }
            if( StereoDevice.DEBUG ) {
                System.err.println("XXX: startSensors: "+res+", started "+sensorsStarted+
                        ": required["+StereoUtil.sensorBitsToString(requiredSensorBits)+"]"+
                        ", desired["+StereoUtil.sensorBitsToString(desiredSensorBits)+"]"+
                        ", enabled["+StereoUtil.sensorBitsToString(usedSensorBits)+"]");
            }
            return res;
        } else {
            // No state change -> Success
            return true;
        }
    }
    @Override
    public final boolean stopSensors() {
        if( isValid() && sensorsStarted ) {
            OVR.ovrHmd_ConfigureTracking(hmdDesc, 0, 0); // STOP
            sensorsStarted = false;
            usedSensorBits = 0;
            return true;
        } else {
            // No state change -> Success
            return true;
        }
    }
    @Override
    public final boolean getSensorsStarted() { return sensorsStarted; }

    @Override
    public final int getSupportedSensorBits() {
        return supportedSensorBits;
    }

    @Override
    public final int getEnabledSensorBits() {
        return usedSensorBits;
    }

    @Override
    public final int[] getEyeRenderOrder() {
        return eyeRenderOrder;
    }

    @Override
    public final int getSupportedDistortionBits() {
        return supportedDistortionBits;
    };

    @Override
    public final int getRecommendedDistortionBits() {
        return recommendedDistortionBits;
    }

    @Override
    public final int getMinimumDistortionBits() {
        return minimumDistortionBits;
    }

    @Override
    public final StereoDeviceRenderer createRenderer(final int distortionBits,
                                                     final int textureCount, final Vec3f eyePositionOffset,
                                                     final FovHVHalves[] eyeFov, final float pixelsPerDisplayPixel, final int textureUnit) {
        final ovrFovPort ovrEyeFov0 = OVRUtil.getOVRFovPort(eyeFov[0]);
        final ovrFovPort ovrEyeFov1 = OVRUtil.getOVRFovPort(eyeFov[1]);

        final ovrEyeRenderDesc[] eyeRenderDesc = new ovrEyeRenderDesc[2];
        eyeRenderDesc[0] = OVR.ovrHmd_GetRenderDesc(hmdDesc, OVR.ovrEye_Left, ovrEyeFov0);
        eyeRenderDesc[1] = OVR.ovrHmd_GetRenderDesc(hmdDesc, OVR.ovrEye_Right, ovrEyeFov1);
        if( StereoDevice.DEBUG ) {
            System.err.println("XXX: eyeRenderDesc[0] "+OVRUtil.toString(eyeRenderDesc[0]));
            System.err.println("XXX: eyeRenderDesc[1] "+OVRUtil.toString(eyeRenderDesc[1]));
        }

        final DimensionImmutable eye0TextureSize = OVRUtil.getOVRSizei(OVR.ovrHmd_GetFovTextureSize(hmdDesc, OVR.ovrEye_Left,  eyeRenderDesc[0].getFov(), pixelsPerDisplayPixel));
        final DimensionImmutable eye1TextureSize = OVRUtil.getOVRSizei(OVR.ovrHmd_GetFovTextureSize(hmdDesc, OVR.ovrEye_Right, eyeRenderDesc[1].getFov(), pixelsPerDisplayPixel));
        if( StereoDevice.DEBUG ) {
            System.err.println("XXX: recommenedTex0Size "+eye0TextureSize);
            System.err.println("XXX: recommenedTex1Size "+eye1TextureSize);
        }
        // final int maxWidth = Math.max(eye0TextureSize.getWidth(), eye1TextureSize.getWidth());
        final int maxHeight = Math.max(eye0TextureSize.getHeight(), eye1TextureSize.getHeight());

        final DimensionImmutable[] eyeTextureSizes = new DimensionImmutable[] { eye0TextureSize, eye1TextureSize };
        final DimensionImmutable totalTextureSize = new Dimension(eye0TextureSize.getWidth() + eye1TextureSize.getWidth(), maxHeight);
        if( StereoDevice.DEBUG ) {
            System.err.println("XXX: textureSize Single "+eyeTextureSizes);
            System.err.println("XXX: textureSize Total  "+totalTextureSize);
        }

        final RectangleImmutable[] eyeViewports = new RectangleImmutable[2];
        if( 1 == textureCount ) { // validated in ctor below!
            // one big texture/FBO, viewport to target space
            eyeViewports[0] = new Rectangle(0, 0,
                                            eye0TextureSize.getWidth(),
                                            maxHeight);
            eyeViewports[1] = new Rectangle(eye0TextureSize.getWidth(), 0,
                                            eye1TextureSize.getWidth(),
                                            maxHeight);
        } else {
            // two textures/FBOs w/ postprocessing, which renders textures/FBOs into target space
            eyeViewports[0] = new Rectangle(0, 0,
                                            eye0TextureSize.getWidth(),
                                            eye0TextureSize.getHeight());
            eyeViewports[1] = new Rectangle(0, 0,
                                            eye1TextureSize.getWidth(),
                                            eye1TextureSize.getHeight());
        }
        return new OVRStereoDeviceRenderer(this, distortionBits, textureCount, eyePositionOffset,
                                           eyeRenderDesc, eyeTextureSizes, totalTextureSize, eyeViewports, textureUnit);
    }
}