/*
 * Part of the PapARt project - https://project.inria.fr/papart/
 *
 * Copyright (C) 2014-2016 Inria
 * Copyright (C) 2011-2013 Bordeaux University
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation, version 2.1.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; If not, see
 * <http://www.gnu.org/licenses/>.
 */
package fr.inria.papart.procam;

import com.jogamp.newt.opengl.GLWindow;
import fr.inria.papart.calibration.MultiSimpleCalibrator;
import fr.inria.papart.procam.camera.Camera;
import fr.inria.papart.calibration.files.CameraConfiguration;
import fr.inria.papart.calibration.files.HomographyCalibration;
import fr.inria.papart.calibration.files.PlanarTouchCalibration;
import fr.inria.papart.procam.display.BaseDisplay;
import fr.inria.papart.procam.display.ARDisplay;
import org.bytedeco.javacpp.freenect;
import fr.inria.papart.calibration.files.PlaneAndProjectionCalibration;
import fr.inria.papart.calibration.files.PlaneCalibration;
import fr.inria.papart.calibration.files.ScreenConfiguration;
import fr.inria.papart.depthcam.devices.Kinect360;
import fr.inria.papart.depthcam.analysis.DepthAnalysisImpl;
import fr.inria.papart.depthcam.devices.DepthCameraDevice;
import fr.inria.papart.depthcam.devices.KinectOne;
import fr.inria.papart.depthcam.devices.NectarOpenNI;
import fr.inria.papart.depthcam.devices.OpenNI2;
import fr.inria.papart.depthcam.devices.RealSense;
import fr.inria.papart.multitouch.TouchInput;
import fr.inria.papart.multitouch.TUIOTouchInput;
import fr.inria.papart.multitouch.DepthTouchInput;
import fr.inria.papart.multitouch.detection.BlinkTracker;
import fr.inria.papart.multitouch.detection.CalibratedColorTracker;
import fr.inria.papart.multitouch.detection.ColorTracker;
import fr.inria.papart.utils.LibraryUtils;
import fr.inria.papart.procam.camera.CameraFactory;
import fr.inria.papart.procam.camera.CameraNectar;
import fr.inria.papart.procam.camera.CameraOpenKinect;
import fr.inria.papart.procam.camera.CameraRGBIRDepth;
import fr.inria.papart.procam.camera.CameraRealSense;
import fr.inria.papart.procam.camera.CannotCreateCameraException;
import fr.inria.papart.procam.camera.SubCamera;
import fr.inria.papart.tracking.DetectedMarker;
import fr.inria.papart.utils.MathUtils;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bytedeco.javacv.CameraDevice;
import org.reflections.Reflections;
import processing.core.PApplet;
import processing.core.PFont;
import processing.core.PMatrix3D;
import processing.core.PVector;
import processing.event.KeyEvent;

/**
 *
 * @author Jeremy Laviole
 */
public class Papart {

    public final static String folder = LibraryUtils.getPapartDataFolder() + "/";
    public final static String calibrationFolder = folder + "calibration/";
    public final static String markerFolder = folder + "markers/";

    public static boolean isInria = false;
    public static boolean isReality = true;
    public static String calibrationFileName = "A4-calib.svg";

    public static String cameraCalibName = "camera.yaml";
    public static String projectorCalibName = "projector.yaml";

    public static String cameraCalib = calibrationFolder + cameraCalibName;
    public static String projectorCalib = calibrationFolder + projectorCalibName;

    public static String camCalibARtoolkit = calibrationFolder + "camera-projector.cal";
    public static String kinectIRCalib = calibrationFolder + "calibration-kinect-IR.yaml";
    public static String SR300IRCalib = calibrationFolder + "calibration-SR300-IR.yaml";
    public static String kinectRGBCalib = calibrationFolder + "calibration-kinect-RGB.yaml";
    public static String kinectStereoCalib = calibrationFolder + "calibration-kinect-Stereo.xml";

    public static String AstraSDepthCalib = calibrationFolder + "calibration-AstraS-depth.yaml";
    public static String AstraSRGBCalib = calibrationFolder + "calibration-AstraS-rgb.yaml";
    public static String AstraSIRCalib = calibrationFolder + "calibration-AstraS-ir.yaml";
    public static String AstraSStereoCalib = calibrationFolder + "calibration-AstraS-stereo.xml";

    public static String kinectTrackingCalib = "kinectTracking.xml";
    public static String cameraProjExtrinsics = "camProjExtrinsics.xml";
    public static String cameraProjHomography = "camProjHomography.xml";

    public static String screenConfig = calibrationFolder + "screenConfiguration.xml";
    public static String cameraConfig = calibrationFolder + "cameraConfiguration.xml";
    public static String depthCameraConfig = calibrationFolder + "depthCameraConfiguration.xml";

    public static String colorThresholds = calibrationFolder + "colorThreshold";
    public static String redThresholds = calibrationFolder + "redThresholds.txt";
    public static String blueThresholds = calibrationFolder + "blueThresholds.txt";
    public static String blinkThresholds = calibrationFolder + "blinkThresholds.txt";

    public static String tablePosition = calibrationFolder + "tablePosition.xml";
    public static String planeCalib = calibrationFolder + "PlaneCalibration.xml";
    public static String homographyCalib = calibrationFolder + "HomographyCalibration.xml";
    public static String planeAndProjectionCalib = calibrationFolder + "PlaneProjectionCalibration.xml";
    public static String touchColorCalib = calibrationFolder + "TouchColorCalibration.xml";
    public static String touchBlinkCalib = calibrationFolder + "TouchBlinkCalibration.xml";

    public static String touchCalib = calibrationFolder + "Touch2DCalibration.xml";
    public static String touchCalib3D = calibrationFolder + "Touch3DCalibration.xml";

    public static String touchCalibrations[];

    public int defaultFontSize = 12;

    protected static Papart singleton = null;

    protected float zNear = 10;
    protected float zFar = 6000;

    private final PApplet applet;
    private final Class appletClass;

    private boolean displayInitialized;
    private boolean cameraInitialized;
    private boolean touchInitialized;

    private BaseDisplay display;
    private ARDisplay arDisplay;


    private Camera cameraTracking;
    private DepthAnalysisImpl depthAnalysis;

    private TouchInput touchInput;
    private PVector frameSize = new PVector();
    private boolean isWithoutCamera = false;

    private DepthCameraDevice depthCameraDevice;

    /**
     * Create the main PapARt object, look at the examples for how to use it.
     *
     * @param applet
     */
    public Papart(Object applet) {
        this.displayInitialized = false;
        this.cameraInitialized = false;
        this.touchInitialized = false;
        this.applet = (PApplet) applet;

        this.appletClass = applet.getClass();
        // TODO: singleton -> Better implementation.
        if (Papart.singleton == null) {
            Papart.singleton = this;

            Papart.touchCalibrations = new String[3];
            for (int i = 0; i < touchCalibrations.length; i++) {
                touchCalibrations[i] = calibrationFolder + "TouchCalibration" + i + ".xml";
            }

            fr.inria.papart.utils.DrawUtils.applet = (PApplet) applet;
        }
    }

    /**
     * Load the default CameraConfiguration, to start the default camera.
     *
     * @param applet
     * @return
     */
    public static CameraConfiguration getDefaultCameraConfiguration(PApplet applet) {
        CameraConfiguration config = new CameraConfiguration();
        config.loadFrom(applet, cameraConfig);
        return config;
    }

    /**
     * Load the default detph camera configuration, to start the default depth
     * camera.
     *
     * @param applet
     * @return
     */
    public static CameraConfiguration getDefaultDepthCameraConfiguration(PApplet applet) {
        CameraConfiguration config = new CameraConfiguration();
        config.loadFrom(applet, depthCameraConfig);
        return config;
    }

    /**
     * Load the default screen configuration. The screen configuration is not
     * used in the current releases.
     *
     * @param applet
     * @return
     */
    public static ScreenConfiguration getDefaultScreenConfiguration(PApplet applet) {
        ScreenConfiguration config = new ScreenConfiguration();
        config.loadFrom(applet, screenConfig);
        return config;
    }

    public MultiSimpleCalibrator multiCalibrator;

    public void multiCalibration() {

        try {

            if (multiCalibrator == null) {
                multiCalibrator = new MultiSimpleCalibrator();
            } else {
                if (multiCalibrator.isActive()) {
                    multiCalibrator.stopCalib();
                } else {
                    multiCalibrator.startCalib();
                }
//            calibrationPopup.hide();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

       /**
     * Start the default camera and a CameraDisplay. You still need to enable
     * the tracking and start the camera. The window will resize itself to the
     * camera size.
     *
     * @param applet
     * @return
     */
    public static Papart seeThrough(PApplet applet) {
        return seeThrough(applet, 1);
    }

    /**
     * Start the default camera and a CameraDisplay. You still need to enable
     * the tracking and start the camera. The window will resize itself to the
     * camera size.
     *
     * @param applet
     * @param quality the quality can upscale or downscale the CameraDisplay.
     * Increase (2.0) for better quality. If you have a 640 width display, and 2
     * quality the rendered image width will be 1280 pixels.
     * @return
     */
    public static Papart seeThrough(PApplet applet, float quality) {

        ProjectiveDeviceP pdp = null;
        try {
            pdp = ProjectiveDeviceP.loadCameraDevice(applet, cameraCalib);
        } catch (Exception ex) {
            Logger.getLogger(Papart.class.getName()).log(Level.SEVERE, null, ex);
        }

        Papart papart = new Papart(applet);

        try {
            papart.initCamera();
        } catch (CannotCreateCameraException ex) {
            throw new RuntimeException("Cannot start the default camera: " + ex);
        }
        papart.initARDisplay(quality);
        papart.checkInitialization();

        return papart;
    }

    /**
     * Start a fullscreen sketch.
     *
     * @param applet
     * @return
     */
    public static Papart projection2D(PApplet applet) {

        ScreenConfiguration screenConfiguration = getDefaultScreenConfiguration(applet);

        removeFrameBorder(applet);

        Papart papart = new Papart(applet);

        papart.frameSize.set(screenConfiguration.getProjectionScreenWidth(),
                screenConfiguration.getProjectionScreenHeight());
        papart.shouldSetWindowLocation = true;
        papart.shouldSetWindowSize = true;
        papart.registerPost();

        return papart;
    }

    private boolean shouldSetWindowLocation = false;
    private boolean shouldSetWindowSize = false;

    /**
     * Register the "post" method, this is used to change the window location.
     */
    private void registerPost() {
        applet.registerMethod("post", this);
    }

    private void registerKey() {
        applet.registerMethod("keyEvent", this);
    }

    /**
     * KeyEvent to handle keys. No global keys analysed for now.
     *
     * @param e
     */
    public void keyEvent(KeyEvent e) {
//        if (e.getKey() == 'c') {
//            calibration();
//        }
    }

    /**
     * Force the size of the sketch to the cameraTracking size. This is used for
     * SeeThrough augmented reality.
     */
    public void forceCameraSize() {
        forceWindowSize(cameraTracking.width(),
                cameraTracking.height());
    }

    /**
     * Force the size of the sketch to the depthCamera size. This is used for
     * SeeThrough augmented reality with depth cameras.
     */
    public void forceDepthCameraSize() {
        forceWindowSize(depthCameraDevice.getDepthCamera().width(),
                depthCameraDevice.getDepthCamera().height());
    }

    /**
     * Force a custom window size.
     *
     * @param w width in pixels.
     * @param h height in pixels.
     */
    public void forceWindowSize(int w, int h) {

        Papart papart = Papart.getPapart();

        papart.shouldSetWindowLocation = true;
        papart.shouldSetWindowSize = true;
        papart.registerPost();

        frameSize.set(w, h);
//        this.shouldSetWindowSize = true;
//        registerPost();
//
//        GLWindow window = (GLWindow) applet.getSurface().getNative();
//        window.setUndecorated(false);
//        window.setSize(w, h);
    }

    /**
     * Force a fullscreen size (for projectors). This call removes the window
     * decoration: menu bars, and makets it fullscreen.
     *
     * @param w width in pixels.
     * @param h hegiht in pixels.
     * @param px location in pixels (from left).
     * @param py location in pixels (from top).
     */
    public void forceProjectorSize(int w, int h, int px, int py) {
        frameSize.set(w, h);
//        this.shouldSetWindowSize = true;
//        registerPost();

        GLWindow window = (GLWindow) applet.getSurface().getNative();
        window.setUndecorated(true);
        window.setSize(w, h);
        window.setPosition(px, py);
    }

    /**
     * Places the window at the correct location if required, according to the
     * configuration. Do not call directly, it may crash the application.
     * Register the method "post" and it will call this method.
     *
     */
    public static void checkWindowLocation() {
        Papart papart = getPapart();

        if (papart == null) {
            System.err.println("Cannot update window location without a Papart object.");
            return;
        }
        if (papart.shouldSetWindowLocation) {
            papart.defaultFrameLocation();
        }
        if (papart.shouldSetWindowSize) {
            papart.setFrameSize();
        }
        papart.shouldSetWindowLocation = false;
        papart.shouldSetWindowSize = false;
    }

    /**
     * Does not draw anything, it used only to check the window location. This
     * is called once then unregistered.
     */
    public void post() {
        checkWindowLocation();
        applet.unregisterMethod("post", this);
    }

    /**
     * Set the frame to default location given by the screenConfiguration.
     */
    public void defaultFrameLocation() {
        ScreenConfiguration screenConfiguration = getDefaultScreenConfiguration(this.applet);
        this.applet.frame.setLocation(screenConfiguration.getProjectionScreenOffsetX(),
                screenConfiguration.getProjectionScreenOffsetY());

        GLWindow window = (GLWindow) applet.getSurface().getNative();
        window.setPosition(screenConfiguration.getProjectionScreenOffsetX(),
                screenConfiguration.getProjectionScreenOffsetY());
    }

    /**
     * Update the applet size to the current frameSize. The current frameSize
     * depends on which type of rendering is used.
     */
    public void setFrameSize() {
        this.applet.getSurface().setSize((int) frameSize.x, (int) frameSize.y);
    }

    protected static void removeFrameBorder(PApplet applet) {
        if (!applet.g.isGL()) {
            applet.frame.removeNotify();
            applet.frame.setUndecorated(true);
            applet.frame.addNotify();
        }
    }

    /**
     * Get the Papart singleton.
     *
     * @return
     */
    public static Papart getPapart() {
        return Papart.singleton;
    }

    /**
     * Save a PMatrix3D to the Papart calibration folder. This can be used to
     * communicate 3D locations between sketches. The calibration folder is
     * this: sketchbook/libraries/PapARt/data/calibration.
     *
     * @param fileName
     * @param mat
     */
    public void saveCalibration(String fileName, PMatrix3D mat) {
        HomographyCalibration.saveMatTo(applet, mat, Papart.calibrationFolder + fileName);
    }

    /**
     * Saves a 3D transformation matrix (such as a paper location).
     *
     * @param fileName
     * @param mat
     */
    public void saveLocationTo(String fileName, PMatrix3D mat) {
        HomographyCalibration.saveMatTo(applet, mat, fileName);
    }

    /**
     * Load a PMatrix3D to the Papart calibration folder. This can be used to
     * communicate 3D locations between sketches. The calibration folder is
     * this: sketchbook/libraries/PapARt/data/calibration.
     *
     * @param fileName
     * @return null if the file does not exists.
     */
    public PMatrix3D loadCalibration(String fileName) {

        File f = new File(Papart.calibrationFolder + fileName);
        if (f.exists()) {
            return HomographyCalibration.getMatFrom(applet, Papart.calibrationFolder + fileName);
        } else {
            return null;
        }
    }

    /**
     * Save the position of a paperScreen as the default table location.
     *
     * @param paperScreen
     */
    public void setTableLocation(PaperScreen paperScreen) {
        HomographyCalibration.saveMatTo(applet, paperScreen.getLocation(), tablePosition);
    }

    /**
     * Save the position of a matrix the default table location.
     *
     * @param mat
     */
    public void setTableLocation(PMatrix3D mat) {
        HomographyCalibration.saveMatTo(applet, mat, tablePosition);
    }


    /**
     * The location of the table, warning it must be set once by
     * setTablePosition.
     *
     * @return
     */
    public PMatrix3D getTableLocation() {
        return HomographyCalibration.getMatFrom(applet, tablePosition);
    }

    /**
     * Work in progress function
     *
     * @return
     */
    public PlaneCalibration getTablePlane() {
        return PlaneCalibration.CreatePlaneCalibrationFrom(HomographyCalibration.getMatFrom(applet, tablePosition),
                new PVector(100, 100));
    }

    /**
     * Move a PaperScreen to the table location. After this, the paperScreen
     * location is not updated anymore. To activate the tracking again use :
     * paperScreen.useManualLocation(false); You can move the paperScreen
     * according to its current location with the paperScreen.setLocation()
     * methods.
     *
     * @param paperScreen
     */
    public void moveToTablePosition(PaperScreen paperScreen) {
        paperScreen.useManualLocation(true, HomographyCalibration.getMatFrom(applet, tablePosition));
//        paperScreen.markerBoard.setFakeLocation(getPublicCameraTracking(), HomographyCalibration.getMatFrom(applet, tablePosition));
    }

    @Deprecated
    public void initNoCamera(int quality) {
        this.isWithoutCamera = true;
        initNoCameraDisplay(quality);
    }

    /**
     * Load a BaseDisplay, used for debug. This call replaces the projector or
     * seeThrough.
     */
    public void initDebug() {
        this.isWithoutCamera = true;
        initDebugDisplay();
    }

    private void tryLoadExtrinsics() {
        PMatrix3D extrinsics = loadCalibration(cameraProjExtrinsics);
        if (extrinsics == null) {
            System.out.println("loading default extrinsics. Could not find " + cameraProjExtrinsics + " .");
        } else {
            arDisplay.setExtrinsics(extrinsics);
        }
    }

    /**
     * Initialize the default calibrated camera for object tracking.
     *
     * @throws fr.inria.papart.procam.camera.CannotCreateCameraException
     */
    public void initCamera() throws CannotCreateCameraException {
        CameraConfiguration cameraConfiguration = getDefaultCameraConfiguration(applet);
        initCamera(cameraConfiguration);
    }

    /**
     * Initialize a camera for object tracking.
     *
     * @param cameraConfiguration
     * @throws fr.inria.papart.procam.camera.CannotCreateCameraException
     *
     */
    public void initCamera(CameraConfiguration cameraConfiguration) throws CannotCreateCameraException {
        initCamera(cameraConfiguration.getCameraName(),
                cameraConfiguration.getCameraType(),
                cameraConfiguration.getCameraFormat());
    }

    /**
     * Initialize a camera for object tracking.
     *
     */
    public void initCamera(String cameraNo, Camera.Type cameraType, String cameraFormat) throws CannotCreateCameraException {
        assert (!cameraInitialized);

        cameraTracking = CameraFactory.createCamera(cameraType, cameraNo, cameraFormat);
        cameraTracking.setParent(applet);
        cameraTracking.setCalibration(cameraCalib);
    }

    /**
     * Initialize the default ARDisplay from the cameraTracking.
     *
     * @param quality
     */
    private void initARDisplay(float quality) {
        assert (this.cameraTracking != null && this.applet != null);

        arDisplay = new ARDisplay(this.applet, getPublicCameraTracking());
        arDisplay.setZNearFar(zNear, zFar);
        arDisplay.setQuality(quality);
        arDisplay.init();
        this.display = arDisplay;
        frameSize.set(arDisplay.getWidth(), arDisplay.getHeight());
        displayInitialized = true;
    }

    @Deprecated
    private void initNoCameraDisplay(float quality) {
        initDebugDisplay();
    }

    /**
     * Create a BaseDisplay.
     */
    private void initDebugDisplay() {
        display = new BaseDisplay();
        display.setFrameSize(applet.width, applet.height);
        display.setDrawingSize(applet.width, applet.height);
        display.init();
        displayInitialized = true;
    }

    private void checkInitialization() {
        assert (cameraTracking != null);
//        this.applet.registerMethod("dispose", this);
//        this.applet.registerMethod("stop", this);
    }

    /**
     * Create the default touchInput, using a depthCamera. This call loads the
     * depthCamera and the TouchInput.
     */
    public void loadTouchInput() {
        try {
            // HACK load also the main camera... :[
            if (this.cameraTracking == null) {
                initCamera();
            }

            loadDefaultDepthCamera();
            loadDefaultDepthTouchInput();
        } catch (CannotCreateCameraException cce) {
            throw new RuntimeException("Cannot start the depth camera");
        }
        updateDepthCameraDeviceExtrinsics();
    }

    /**
     * Get the extrinsics from a depth camera.
     */
    private void updateDepthCameraDeviceExtrinsics() {
        // Check if depthCamera is the same as the camera !
        if (cameraTracking instanceof CameraRGBIRDepth
                && cameraTracking == depthCameraDevice.getMainCamera()) {

            // No extrinsic used, it is already in the camera... 
            depthCameraDevice.getDepthCamera().setExtrinsics(depthCameraDevice.getStereoCalibration());

            // Specific
            // Important to use it for now ! Used in KinectTouchInput.projectPointToScreen
            ((DepthTouchInput) this.touchInput).useRawDepth();

//            System.out.println("Papart: Using Touchextrinsics from the device.");
        } else {
            // Two different cameras  
            // setExtrinsics must after the kinect stereo calibration is loaded
            PMatrix3D extr = (Papart.getPapart()).loadCalibration(Papart.kinectTrackingCalib);
            extr.invert();
            depthCameraDevice.getDepthCamera().setExtrinsics(extr);
//            System.out.println("Papart: Using Touchextrinsics from the calibrated File.");
        }
    }


    /**
     * Initialize the default depth camera. You still need to start the camera.
     *
     * @return @throws CannotCreateCameraException
     */
    public DepthCameraDevice loadDefaultDepthCamera() throws CannotCreateCameraException {

        // Two cases, either the other camera running of the same type
        CameraConfiguration depthCamConfiguration = Papart.getDefaultDepthCameraConfiguration(applet);

        // If the camera is not instanciated, we use depth + color from the camera.
//        if (cameraTracking == null) {
//            System.err.println("You must choose a camera to create a DepthCamera.");
//        }
        if (depthCamConfiguration.getCameraType() == Camera.Type.REALSENSE) {
            depthCameraDevice = new RealSense(applet, cameraTracking);
        }

        if (depthCamConfiguration.getCameraType() == Camera.Type.OPEN_KINECT) {
            depthCameraDevice = new Kinect360(applet, cameraTracking);
        }

        if (depthCamConfiguration.getCameraType() == Camera.Type.OPEN_KINECT_2) {
            depthCameraDevice = new KinectOne(applet, cameraTracking);
        }
        if (depthCamConfiguration.getCameraType() == Camera.Type.OPENNI2) {
            depthCameraDevice = new OpenNI2(applet, cameraTracking);
        }
        if (depthCamConfiguration.getCameraType() == Camera.Type.NECTAR) {
            depthCameraDevice = new NectarOpenNI(applet, cameraTracking);
        }

        if (depthCameraDevice == null) {
            System.err.println("Could not load the depth camera !" + "Camera Type " + depthCamConfiguration.getCameraType());
        }

        // At this point, cameraTracking & depth Camera are ready. 
        return depthCameraDevice;
    }

    /**
     * Initialize the default touch input. You need to create the depth camera
     * first.
     */
    private void loadDefaultDepthTouchInput() {
        depthAnalysis = new DepthAnalysisImpl(this.applet, depthCameraDevice);

        PlaneAndProjectionCalibration calibration = new PlaneAndProjectionCalibration();
        calibration.loadFrom(this.applet, planeAndProjectionCalib);

        DepthTouchInput depthTouchInput
                = new DepthTouchInput(this.applet,
                        depthCameraDevice,
                        depthAnalysis, calibration);

        depthCameraDevice.setTouch(depthTouchInput);

        for (int i = 0; i < 3; i++) {
            depthTouchInput.setTouchDetectionCalibration(i, getTouchCalibration(i));
        }
        depthTouchInput.setSimpleTouchDetectionCalibration(getPapart().getDefaultTouchCalibration());

        this.touchInput = depthTouchInput;
        touchInitialized = true;
    }

    public PlanarTouchCalibration getDefaultColorTouchCalibration() {
        PlanarTouchCalibration calib = new PlanarTouchCalibration();
        calib.loadFrom(applet, Papart.touchColorCalib);
        return calib;
    }

    public PlanarTouchCalibration getDefaultBlinkTouchCalibration() {
        PlanarTouchCalibration calib = new PlanarTouchCalibration();
        calib.loadFrom(applet, Papart.touchBlinkCalib);
        return calib;
    }

    public PlanarTouchCalibration getDefaultTouchCalibration() {
        PlanarTouchCalibration calib = new PlanarTouchCalibration();
        calib.loadFrom(applet, Papart.touchCalib);
        return calib;
    }

    public PlanarTouchCalibration getDefaultTouchCalibration3D() {
        PlanarTouchCalibration calib = new PlanarTouchCalibration();
        calib.loadFrom(applet, Papart.touchCalib3D);
        return calib;
    }

    public PlanarTouchCalibration getTouchCalibration(int id) {
        PlanarTouchCalibration calib = new PlanarTouchCalibration();
        calib.loadFrom(applet, Papart.touchCalibrations[id]);
        return calib;
    }

    public void loadTouchInputTUIO() {
        touchInput = new TUIOTouchInput(this.applet, 3333);
        this.touchInitialized = true;
    }

    /**
     * Find all the PaperScreen and PaperTouchScreen classes and create an
     * instance of it.
     *
     * @return a list of all the created instances.
     */
    public ArrayList<PaperScreen> loadSketches() {
        // Sketches are not within a package.
        Reflections reflections = new Reflections("");

        ArrayList<PaperScreen> instances = new ArrayList<>();
        Set<Class<? extends PaperTouchScreen>> paperTouchScreenClasses = reflections.getSubTypesOf(PaperTouchScreen.class
        );
        for (Class<? extends PaperTouchScreen> klass : paperTouchScreenClasses) {
            try {
                Class[] ctorArgs2 = new Class[1];
                ctorArgs2[0] = this.appletClass;
                Constructor<? extends PaperTouchScreen> constructor = klass.getDeclaredConstructor(ctorArgs2);
                System.out.println("Starting a PaperTouchScreen. " + klass.getName());
                PaperTouchScreen newInstance = constructor.newInstance(this.appletClass.cast(this.applet));
                instances.add(newInstance);
            } catch (Exception ex) {
                System.out.println("Error loading PapartTouchApp : " + klass.getName() + ex);
                ex.printStackTrace();
            }
        }

        Set<Class<? extends PaperScreen>> paperScreenClasses = reflections.getSubTypesOf(PaperScreen.class);

        // Add them once.
        paperScreenClasses.removeAll(paperTouchScreenClasses);
        for (Class<? extends PaperScreen> klass : paperScreenClasses) {
            try {
                Class[] ctorArgs2 = new Class[1];
                ctorArgs2[0] = this.appletClass;
                Constructor<? extends PaperScreen> constructor = klass.getDeclaredConstructor(ctorArgs2);
                System.out.println("Starting a PaperScreen. " + klass.getName());
                PaperScreen newInstance = constructor.newInstance(this.appletClass.cast(this.applet));
                instances.add(newInstance);
            } catch (Exception ex) {
                System.out.println("Error loading PapartApp : " + klass.getName());
                ex.printStackTrace();
            }
        }

        return instances;
    }

    /**
     * Start the tracking without a thread.
     */
    public void startTrackingWithoutThread() {
        if (this.cameraTracking == null) {
            System.err.println("Start Tracking requires a Camera...");
            return;
        }
        this.getPublicCameraTracking().trackSheets(true);
    }

    /**
     * Start the camera thread, and the tracking. it calls automatically
     * startCameraThread().
     */
    public void startTracking() {
        if (this.cameraTracking == null) {
            System.err.println("Start Tracking requires a Camera...");
            return;
        }
        this.getPublicCameraTracking().trackSheets(true);
        startCameraThread();
    }

    /**
     * Start the camera(s) in a thread. This call also starts the depth camera
     * when needed.
     */
    public void startCameraThread() {
        cameraTracking.start();

        if (depthCameraDevice != null) {
            depthCameraDevice.loadDataFromDevice();
        }

        // Calibration might be loaded from the device and require an update. 
        if (arDisplay != null) {
            arDisplay.reloadCalibration();
        }

        cameraTracking.setThread();

        if (depthCameraDevice != null
                && cameraTracking != depthCameraDevice.getMainCamera()) {
            depthCameraDevice.getMainCamera().start();
            depthCameraDevice.getMainCamera().setThread();
        }
    }

    public void startDepthCameraThread() {
        depthCameraDevice.getMainCamera().start();
        depthCameraDevice.getMainCamera().setThread();
    }

    /**
     * Get the Position of a marker. MarkerTracking must be enabled with at
     * least one SVG marker to find.
     *
     * @param markerID id of the marker to find.
     * @param markerWidth size of square marker in mm.
     * @return null is the marker is not found.
     */
    public PMatrix3D getMarkerMatrix(int markerID, float markerWidth) {
        if (cameraTracking == null
                || cameraTracking.getDetectedMarkers() == null) {
            return null;
        }

        for (DetectedMarker marker : cameraTracking.getDetectedMarkers()) {
            if (marker.id == markerID) {
                return MathUtils.compute3DPos(marker, markerWidth, cameraTracking);
            }
        }
        return null;
    }

    /**
     * Get the Position of a marker. MarkerTracking must be enabled with at
     * least one SVG marker to find.
     *
     * @return null is the marker is not found.
     */
    public DetectedMarker[] getMarkerList() {
        if (cameraTracking == null
                || cameraTracking.getDetectedMarkers() == null) {
            return new DetectedMarker[0];
        }
        return cameraTracking.getDetectedMarkers();
    }

    /**
     * Return the x,y positions of a 3D location projected onto a given
     * reference. The Z axis is the angle (in radians) given by the rotation of
     * the positionToFind.
     *
     * @param positionToFind
     * @param reference
     * @return
     */
    public PVector projectPositionTo2D(PMatrix3D positionToFind,
            PMatrix3D reference,
            float referenceHeight) {
        PMatrix3D referenceInv = reference.get();
        referenceInv.invert();
        PMatrix3D relative = positionToFind.get();
        relative.preApply(referenceInv);

        PMatrix3D positionToFind2 = positionToFind.get();
        positionToFind2.translate(100, 0, 0);
        PMatrix3D relative2 = positionToFind2.get();
        relative2.preApply(referenceInv);

        PVector out = new PVector();
        float x = relative.m03 - relative2.m03;
        float y = relative.m13 - relative2.m13;
        out.z = PApplet.atan2(x, y);

        out.x = relative.m03;
        out.y = referenceHeight - relative.m13;

        return out;
    }

    /**
     * Return the x,y positions of a 3D location projected onto a given
     * reference. The Z axis is the angle (in radians) given by the rotation of
     * the positionToFind.
     *
     * @param positionToFind
     * @param reference
     * @return
     */
    public PVector projectPositionTo(PMatrix3D positionToFind,
            PaperScreen reference) {
        PMatrix3D referenceInv = reference.getLocation().get();
        referenceInv.invert();
        PMatrix3D relative = positionToFind.get();
        relative.preApply(referenceInv);

        PMatrix3D positionToFind2 = positionToFind.get();
        positionToFind2.translate(100, 0, 0);
        PMatrix3D relative2 = positionToFind2.get();
        relative2.preApply(referenceInv);

        PVector out = new PVector();
        float x = relative.m03 - relative2.m03;
        float y = relative.m13 - relative2.m13;
        out.z = PApplet.atan2(x, y);

        out.x = relative.m03;
        out.y = reference.drawingSize.y - relative.m13;

        return out;
    }

    // NOTE: camera can dispose themselves now... 
//    public void dispose() {
//        if (touchInitialized && depthCameraDevice != null) {
//            depthCameraDevice.close();
//        }
//        if (cameraInitialized && cameraTracking != null) {
//            try {
//                cameraTracking.close();
//            } catch (Exception e) {
//                System.err.println("Error closing the tracking camera" + e);
//            }
//        }
//        System.out.println("Cameras closed.");
//    }
    /**
     * Create a red ColorTracker for a PaperScreen.
     *
     * @param screen PaperScreen to set the location of the tracking.
     * @param quality capture quality in px/mm. lower (0.5f) for higher
     * performance.
     * @return
     */
    public CalibratedColorTracker initAllTracking(PaperScreen screen, float quality) {
        CalibratedColorTracker colorTracker = new CalibratedColorTracker(screen, quality);
        return colorTracker;
    }

    /**
     * Create a red ColorTracker for a PaperScreen.
     *
     * @param screen PaperScreen to set the location of the tracking.
     * @param quality capture quality in px/mm. lower (0.5f) for higher
     * performance.
     * @return
     */
    public ColorTracker initRedTracking(PaperScreen screen, float quality) {
        return initColorTracking("red", redThresholds, screen, quality);
    }

    /**
     * Create a blue ColorTracker for a PaperScreen.
     *
     * @param screen PaperScreen to set the location of the tracking.
     * @param quality capture quality in px/mm. lower (0.5f) for higher
     * performance.
     * @return
     */
    public ColorTracker initBlueTracking(PaperScreen screen, float quality) {
        return initColorTracking("blue", blueThresholds, screen, quality);
    }

    /**
     * Create a blue ColorTracker for a PaperScreen.
     *
     * @param screen PaperScreen to set the location of the tracking.
     * @param quality capture quality in px/mm. lower (0.5f) for higher
     * performance.
     * @param freq
     * @return
     */
    public BlinkTracker initXTracking(PaperScreen screen, float quality, float freq) {
        BlinkTracker blinkTracker = new BlinkTracker(screen, getDefaultBlinkTouchCalibration(), quality);
        String[] list = applet.loadStrings(blinkThresholds);
        for (String data : list) {
            blinkTracker.loadParameter(data);
        }
        blinkTracker.setName("x");
        blinkTracker.setFreq(freq);
        return blinkTracker;
    }

    private ColorTracker initColorTracking(String name, String calibFile, PaperScreen screen, float quality) {
        ColorTracker colorTracker = new ColorTracker(screen, getDefaultColorTouchCalibration(), quality);
        String[] list = applet.loadStrings(calibFile);
        for (int i = 0; i < list.length; i++) {
            String data = list[i];
            colorTracker.loadParameter(data);
        }
        colorTracker.setName(name);
        return colorTracker;
    }

    public BaseDisplay getDisplay() {
//        assert (displayInitialized);
        return this.display;
    }

    public void setDisplay(BaseDisplay display) {
        // todo check this .
        displayInitialized = true;
        this.display = display;
    }

    public void setTrackingCamera(Camera camera) {
        this.cameraTracking = camera;
        if (camera == null) {
            setNoTrackingCamera();
        }
    }

    public void setNoTrackingCamera() {
        this.isWithoutCamera = true;
    }

    public ARDisplay getARDisplay() {
//        assert (displayInitialized);
        return this.arDisplay;
    }

    public Camera getCameraTracking() {
        return this.cameraTracking;
    }

    public Camera getPublicCameraTracking() {
        if (cameraTracking instanceof CameraRGBIRDepth) {
            if (((CameraRGBIRDepth) cameraTracking).getActingCamera() == null) {
                throw new RuntimeException("Papart: Impossible to use the mainCamera, use a subCamera or set the ActAsX methods.");
            }
            return ((CameraRGBIRDepth) cameraTracking).getActingCamera();
        }
        return this.cameraTracking;
    }

    public boolean isTouchInitialized(){
        return this.touchInitialized;
    }
    
    public TouchInput getTouchInput() {
        assert (touchInitialized);
        return this.touchInput;
    }

    public PVector getFrameSize() {
        assert (this.frameSize != null);
        return this.frameSize.get();
    }

    public boolean isWithoutCamera() {
        return this.isWithoutCamera;
    }

    public Camera getKinectCamera() {
        return this.depthCameraDevice.getColorCamera();
    }

    public DepthCameraDevice getDepthCameraDevice() {
        return depthCameraDevice;
    }

    public DepthAnalysisImpl getKinectAnalysis() {
        return this.depthAnalysis;
    }

    public Camera.Type getDepthCameraType() {
        if (depthCameraDevice == null) {
            return Camera.Type.FAKE;
        }
        return depthCameraDevice.type();
    }

    public PApplet getApplet() {
        return applet;
    }

}