package cz.fidentis.analyst.scene;

import javax.vecmath.Vector3d;

/**
 * OpenGL code related to the camera.
 * 
 * @author Natalia Bebjakova 
 * @author Radek Oslejsek
 */
public class Camera {
    
    public static final Vector3d DEFAULT_POSITION = new Vector3d(0, 0, 300);
    
    private final Vector3d center = new Vector3d(0, 0, 0);
    private final Vector3d currentPosition = new Vector3d(0, 0, 300);
    private final Vector3d upDirection = new Vector3d(0, 1, 0);
    
    /**
     * Sets model to the initial position and orientation
     */
    public void initLocation() {
        this.upDirection.set(0, 1, 0);
        this.currentPosition.set(this.DEFAULT_POSITION);
        this.center.set(0, 0, 0);
    }

    /**
     * Returns center of the scene.
     * 
     * @return center of the scene.
     */
    public Vector3d getCenter() {
        return new Vector3d(center);
    }
    
    /**
     * Returns camera position.
     * 
     * @return camera position.
     */
    public Vector3d getPosition() {
        return new Vector3d(currentPosition);
    }
    
    /**
     * Returns camera orientation (the up direction).
     * 
     * @return camera orientation (the up direction).
     */
    public Vector3d getUpDirection() {
        return new Vector3d(upDirection);
    }
    
    /**
     * Rotates the camera up.
     * 
     * @param degree degree of rotation
     */
    public void rotateUp(double degree) {
        rotate(-degree, 0);
    }

    /**
     * Rotates the camera down.
     * 
     * @param degree degree of rotation
     */
    public void rotateDown(double degree) {
        rotate(degree, 0);
    }

    /**
     * Rotates the camera left.
     * 
     * @param degree degree of rotation
     */
    public void rotateLeft(double degree) {
        rotate(0, degree);
    }

    /**
     * Rotates the camera right.
     * @param degree degree of rotation
     */
    public void rotateRight(double degree) {
        rotate(0, -degree);
    }

    /**
     * Returns normalized direction of the X axis.
     * 
     * @return normalized direction of the X axis.
     */
    private Vector3d getXaxis() {
        Vector3d axis = new Vector3d(
                (currentPosition.y - center.y) * upDirection.z - (currentPosition.z - center.z) * upDirection.y,
                (currentPosition.z - center.z) * upDirection.x - (currentPosition.x - center.x) * upDirection.z,
                (currentPosition.x - center.x) * upDirection.y - (currentPosition.y - center.y) * upDirection.x);
        axis.normalize();
        return axis;
    }

    /**
     * Returns normalized direction of the Y axis.
     * 
     * @return normalized direction of the Y axis.
     */
    private Vector3d getYaxis() {
        Vector3d axis = new Vector3d(upDirection.x, upDirection.y, upDirection.z);
        //axis.normalize();
        return axis;
    }

    /**
     * Rotates object around axes that appear as horizontal and vertical axes on
     * the screen (parallel to the screen edges), intersecting at the center of
     * the screen (i.e., head center).
     *
     * @param xAngle angle around vertical axis on the screen
     * @param yAngle angle around horizontal axis on the screen
     */
    public void rotate(double xAngle, double yAngle) {
        Vector3d xAxis = getXaxis();
        Vector3d yAxis = getYaxis();

        Vector3d point = new Vector3d(currentPosition);
        Vector3d camera = rotateAroundAxe(point, xAxis, Math.toRadians(xAngle));
        camera = rotateAroundAxe(camera, yAxis, Math.toRadians(yAngle));

        point = new Vector3d(upDirection);
        Vector3d up = rotateAroundAxe(point, xAxis, Math.toRadians(xAngle));
        up = rotateAroundAxe(up, yAxis, Math.toRadians(yAngle));
        
        this.upDirection.set(up);
        this.currentPosition.set(camera);

        //setNewCameraPosition(camera);
    }

    /**
     * Moves the camera.
     * 
     * @param xShift xShift
     * @param yShift yShift
     */
    public void move(double xShift, double yShift) {
        Vector3d yAxis = getYaxis();
        yAxis.scale(yShift);
        
        Vector3d shift = getXaxis();
        shift.scale(xShift);
        shift.add(yAxis);
        
        Vector3d camera = new Vector3d(currentPosition);
        camera.add(shift);
        
        this.center.add(shift);
        this.currentPosition.set(camera);
    }

    /**
     * Calculate the new position f point from given angle and rotation axe.
     *
     * @param point original position
     * @param u vector of rotation axis
     * @param angle angle of rotation
     * @return new position
     */
    public Vector3d rotateAroundAxe(Vector3d point, Vector3d u, double angle) {
        double x = ((Math.cos(angle) + u.x * u.x * (1 - Math.cos(angle))) * point.x
                + (u.x * u.y * (1 - Math.cos(angle)) - u.z * Math.sin(angle)) * point.y
                + (u.x * u.z * (1 - Math.cos(angle)) + u.y * Math.sin(angle)) * point.z);
        double y = ((u.x * u.y * (1 - Math.cos(angle)) + u.z * Math.sin(angle)) * point.x
                + (Math.cos(angle) + u.y * u.y * (1 - Math.cos(angle))) * point.y
                + (u.y * u.z * (1 - Math.cos(angle)) - u.x * Math.sin(angle)) * point.z);
        double z = ((u.x * u.z * (1 - Math.cos(angle)) - u.y * Math.sin(angle)) * point.x
                + (u.y * u.z * (1 - Math.cos(angle)) + u.x * Math.sin(angle)) * point.y
                + (Math.cos(angle) + u.z * u.z * (1 - Math.cos(angle))) * point.z);
        return new Vector3d(x, y, z);
    }

    /**
     * Zooms in.
     * 
     * @param distance Distance to be zoom in
     */
    public void zoomIn(double distance) {
        Vector3d v = new Vector3d(currentPosition);
        v.sub(center);
        double len = v.length();
        if (len > 0) {
            this.currentPosition.set(v);
            this.currentPosition.scale((len - distance) / len);
            this.currentPosition.add(center);
        }
    }

    /**
     * Zooms out.
     * 
     * @param distance Distance to be zoom out
     */
    public void zoomOut(double distance) {
        Vector3d v = new Vector3d(currentPosition);
        v.sub(center);
        double len = v.length();
        if (len == 0) {
            len = 1;
        }
        
        this.currentPosition.set(v);
        this.currentPosition.scale((len + distance) / len);
        this.currentPosition.add(center);
    }
}
