/**
 * SimpleFPSCamera.java
 *
 * Copyright (c) 2013-2016, F(X)yz
 * 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 F(X)yz, any associated website, 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 F(X)yz 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.
 */ 

package org.fxyz3d.scene;

import javafx.animation.AnimationTimer;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.util.Callback;
import org.fxyz3d.geometry.MathUtils;

/**
 * A self initializing First Person Shooter camera
 *
 * @author Jason Pollastrini aka jdub1581
 */
public class SimpleFPSCamera extends Parent {

    public SimpleFPSCamera() {
        initialize();
    }

    private void update() {
        updateControls();
    }

    private void updateControls() {
        if (fwd && !back) {
            moveForward();
        }
        if (strafeL) {
            strafeLeft();
        }
        if (strafeR) {
            strafeRight();
        }
        if (back && !fwd) {
            moveBack();
        }
        if (up && !down) {
            moveUp();
        }
        if (down && !up) {
            moveDown();
        }
    }
    /*==========================================================================
     Initialization
     */
    private final Group root = new Group();
    private final Affine affine = new Affine();
    private final Translate t = new Translate(0, 0, 0);
    private final Rotate rotateX = new Rotate(0, Rotate.X_AXIS),
            rotateY = new Rotate(0, Rotate.Y_AXIS),
            rotateZ = new Rotate(0, Rotate.Z_AXIS);

    private boolean fwd, strafeL, strafeR, back, up, down, shift;

    private double mouseSpeed = 1.0, mouseModifier = 0.1;
    private double moveSpeed = 10.0;
    private double mousePosX;
    private double mousePosY;
    private double mouseOldX;
    private double mouseOldY;
    private double mouseDeltaX;
    private double mouseDeltaY;

    private void initialize() {
        getChildren().add(root);
        getTransforms().add(affine);
        initializeCamera();
        startUpdateThread();
    }

    public void loadControlsForSubScene(SubScene scene) {
        sceneProperty().addListener(l -> {
            if (getScene() != null) {
                getScene().addEventHandler(KeyEvent.ANY, ke -> {
                    if (ke.getEventType() == KeyEvent.KEY_PRESSED) {
                        switch (ke.getCode()) {
                            case Q:
                                up = true;
                                break;
                            case E:
                                down = true;
                                break;
                            case W:
                                fwd = true;
                                break;
                            case S:
                                back = true;
                                break;
                            case A:
                                strafeL = true;
                                break;
                            case D:
                                strafeR = true;
                                break;
                            case SHIFT:
                                shift = true;
                                moveSpeed = 20;
                                break;
                        }
                    } else if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
                        switch (ke.getCode()) {
                            case Q:
                                up = false;
                                break;
                            case E:
                                down = false;
                                break;
                            case W:
                                fwd = false;
                                break;
                            case S:
                                back = false;
                                break;
                            case A:
                                strafeL = false;
                                break;
                            case D:
                                strafeR = false;
                                break;
                            case SHIFT:
                                moveSpeed = 10;
                                shift = false;
                                break;
                        }
                    }
                    ke.consume();
                });
            }
        });
        scene.addEventHandler(MouseEvent.ANY, me -> {
            if (me.getEventType().equals(MouseEvent.MOUSE_PRESSED)) {
                mousePosX = me.getSceneX();
                mousePosY = me.getSceneY();
                mouseOldX = me.getSceneX();
                mouseOldY = me.getSceneY();

            } else if (me.getEventType().equals(MouseEvent.MOUSE_DRAGGED)) {
                mouseOldX = mousePosX;
                mouseOldY = mousePosY;
                mousePosX = me.getSceneX();
                mousePosY = me.getSceneY();
                mouseDeltaX = (mousePosX - mouseOldX);
                mouseDeltaY = (mousePosY - mouseOldY);

                mouseSpeed = 1.0;
                mouseModifier = 0.1;

                if (me.isPrimaryButtonDown()) {
                    if (me.isControlDown()) {
                        mouseSpeed = 0.1;
                    }
                    if (me.isShiftDown()) {
                        mouseSpeed = 1.0;
                    }
                    t.setX(getPosition().getX());
                    t.setY(getPosition().getY());
                    t.setZ(getPosition().getZ());

                    affine.setToIdentity();

                    rotateY.setAngle(
                            MathUtils.clamp(-360, ((rotateY.getAngle() + mouseDeltaX * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 360)
                    ); // horizontal                
                    rotateX.setAngle(
                            MathUtils.clamp(-45, ((rotateX.getAngle() - mouseDeltaY * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 35)
                    ); // vertical
                    affine.prepend(t.createConcatenation(rotateY.createConcatenation(rotateX)));

                } else if (me.isSecondaryButtonDown()) {
                    /*
                     init zoom?
                     */
                } else if (me.isMiddleButtonDown()) {
                    /*
                     init panning?
                     */
                }
            }
        });

        scene.addEventHandler(ScrollEvent.ANY, se -> {

            if (se.getEventType().equals(ScrollEvent.SCROLL_STARTED)) {

            } else if (se.getEventType().equals(ScrollEvent.SCROLL)) {

            } else if (se.getEventType().equals(ScrollEvent.SCROLL_FINISHED)) {

            }
        });
    }

    public void loadControlsForScene(Scene scene) {
        scene.addEventHandler(KeyEvent.ANY, ke -> {
            if (ke.getEventType() == KeyEvent.KEY_PRESSED) {
                switch (ke.getCode()) {
                    case Q:
                        up = true;
                        break;
                    case E:
                        down = true;
                        break;
                    case W:
                        fwd = true;
                        break;
                    case S:
                        back = true;
                        break;
                    case A:
                        strafeL = true;
                        break;
                    case D:
                        strafeR = true;
                        break;
                    case SHIFT:
                        shift = true;
                        moveSpeed = 20;
                        break;
                }
            } else if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
                switch (ke.getCode()) {
                    case Q:
                        up = false;
                        break;
                    case E:
                        down = false;
                        break;
                    case W:
                        fwd = false;
                        break;
                    case S:
                        back = false;
                        break;
                    case A:
                        strafeL = false;
                        break;
                    case D:
                        strafeR = false;
                        break;
                    case SHIFT:
                        moveSpeed = 10;
                        shift = false;
                        break;
                }
            }
            ke.consume();
        });
        scene.addEventHandler(MouseEvent.ANY, me -> {
            if (me.getEventType().equals(MouseEvent.MOUSE_PRESSED)) {
                mousePosX = me.getSceneX();
                mousePosY = me.getSceneY();
                mouseOldX = me.getSceneX();
                mouseOldY = me.getSceneY();

            } else if (me.getEventType().equals(MouseEvent.MOUSE_DRAGGED)) {
                mouseOldX = mousePosX;
                mouseOldY = mousePosY;
                mousePosX = me.getSceneX();
                mousePosY = me.getSceneY();
                mouseDeltaX = (mousePosX - mouseOldX);
                mouseDeltaY = (mousePosY - mouseOldY);

                mouseSpeed = 1.0;
                mouseModifier = 0.1;

                if (me.isPrimaryButtonDown()) {
                    if (me.isControlDown()) {
                        mouseSpeed = 0.1;
                    }
                    if (me.isShiftDown()) {
                        mouseSpeed = 1.0;
                    }
                    t.setX(getPosition().getX());
                    t.setY(getPosition().getY());
                    t.setZ(getPosition().getZ());

                    affine.setToIdentity();

                    rotateY.setAngle(
                            MathUtils.clamp(-360, ((rotateY.getAngle() + mouseDeltaX * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 360)
                    ); // horizontal                
                    rotateX.setAngle(
                            MathUtils.clamp(-45, ((rotateX.getAngle() - mouseDeltaY * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 35)
                    ); // vertical
                    affine.prepend(t.createConcatenation(rotateY.createConcatenation(rotateX)));

                } else if (me.isSecondaryButtonDown()) {
                    /*
                     init zoom?
                     */
                } else if (me.isMiddleButtonDown()) {
                    /*
                     init panning?
                     */
                }
            }
        });

        scene.addEventHandler(ScrollEvent.ANY, se -> {

            if (se.getEventType().equals(ScrollEvent.SCROLL_STARTED)) {

            } else if (se.getEventType().equals(ScrollEvent.SCROLL)) {

            } else if (se.getEventType().equals(ScrollEvent.SCROLL_FINISHED)) {

            }
        });
    }

    private void initializeCamera() {
        getCamera().setNearClip(0.1);
        getCamera().setFarClip(100000);
        getCamera().setFieldOfView(42);
        getCamera().setVerticalFieldOfView(true);
        root.getChildren().add(getCamera());
    }

    private void startUpdateThread() {
        new AnimationTimer() {
            @Override
            public void handle(long now) {
                update();
            }
        }.start();
    }
    /*==========================================================================
     Movement
     */

    private void moveForward() {
        affine.setTx(getPosition().getX() + moveSpeed * getN().getX());
        affine.setTy(getPosition().getY() + moveSpeed * getN().getY());
        affine.setTz(getPosition().getZ() + moveSpeed * getN().getZ());
    }

    private void strafeLeft() {
        affine.setTx(getPosition().getX() + moveSpeed * -getU().getX());
        affine.setTy(getPosition().getY() + moveSpeed * -getU().getY());
        affine.setTz(getPosition().getZ() + moveSpeed * -getU().getZ());
    }

    private void strafeRight() {
        affine.setTx(getPosition().getX() + moveSpeed * getU().getX());
        affine.setTy(getPosition().getY() + moveSpeed * getU().getY());
        affine.setTz(getPosition().getZ() + moveSpeed * getU().getZ());
    }

    private void moveBack() {
        affine.setTx(getPosition().getX() + moveSpeed * -getN().getX());
        affine.setTy(getPosition().getY() + moveSpeed * -getN().getY());
        affine.setTz(getPosition().getZ() + moveSpeed * -getN().getZ());
    }

    private void moveUp() {
        affine.setTx(getPosition().getX() + moveSpeed * -getV().getX());
        affine.setTy(getPosition().getY() + moveSpeed * -getV().getY());
        affine.setTz(getPosition().getZ() + moveSpeed * -getV().getZ());
    }

    private void moveDown() {
        affine.setTx(getPosition().getX() + moveSpeed * getV().getX());
        affine.setTy(getPosition().getY() + moveSpeed * getV().getY());
        affine.setTz(getPosition().getZ() + moveSpeed * getV().getZ());
    }

    /*==========================================================================
     Properties
     */
    private final ReadOnlyObjectWrapper<PerspectiveCamera> camera = new ReadOnlyObjectWrapper<>(this, "camera", new PerspectiveCamera(true));

    public final PerspectiveCamera getCamera() {
        return camera.get();
    }

    public ReadOnlyObjectProperty cameraProperty() {
        return camera.getReadOnlyProperty();
    }

    /*==========================================================================
     Callbacks    
     | R | Up| F |  | P|
     U |mxx|mxy|mxz|  |tx|
     V |myx|myy|myz|  |ty|
     N |mzx|mzy|mzz|  |tz|
    
     */
    //Forward / look direction    
    private final Callback<Transform, Point3D> F = (a) -> {
        return new Point3D(a.getMzx(), a.getMzy(), a.getMzz());
    };
    private final Callback<Transform, Point3D> N = (a) -> {
        return new Point3D(a.getMxz(), a.getMyz(), a.getMzz());
    };
    // up direction
    private final Callback<Transform, Point3D> UP = (a) -> {
        return new Point3D(a.getMyx(), a.getMyy(), a.getMyz());
    };
    private final Callback<Transform, Point3D> V = (a) -> {
        return new Point3D(a.getMxy(), a.getMyy(), a.getMzy());
    };
    // right direction
    private final Callback<Transform, Point3D> R = (a) -> {
        return new Point3D(a.getMxx(), a.getMxy(), a.getMxz());
    };
    private final Callback<Transform, Point3D> U = (a) -> {
        return new Point3D(a.getMxx(), a.getMyx(), a.getMzx());
    };
    //position
    private final Callback<Transform, Point3D> P = (a) -> {
        return new Point3D(a.getTx(), a.getTy(), a.getTz());
    };

    private Point3D getF() {
        return F.call(getLocalToSceneTransform());
    }

    public Point3D getLookDirection() {
        return getF();
    }

    private Point3D getN() {
        return N.call(getLocalToSceneTransform());
    }

    public Point3D getLookNormal() {
        return getN();
    }

    private Point3D getR() {
        return R.call(getLocalToSceneTransform());
    }

    private Point3D getU() {
        return U.call(getLocalToSceneTransform());
    }

    private Point3D getUp() {
        return UP.call(getLocalToSceneTransform());
    }

    private Point3D getV() {
        return V.call(getLocalToSceneTransform());
    }

    public final Point3D getPosition() {
        return P.call(getLocalToSceneTransform());
    }

}
