package cz.fidentis.analyst.gui;

import cz.fidentis.analyst.mesh.core.MeshModel;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import com.jogamp.opengl.GL;
import static com.jogamp.opengl.GL.GL_DEPTH_TEST;
import static com.jogamp.opengl.GL.GL_FRONT_AND_BACK;
import static com.jogamp.opengl.GL.GL_VIEWPORT;
import com.jogamp.opengl.GL2;
import static com.jogamp.opengl.GL2GL3.GL_FILL;
import static com.jogamp.opengl.GL2GL3.GL_LINE;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLEventListener;
import static com.jogamp.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW_MATRIX;
import static com.jogamp.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION_MATRIX;
import com.jogamp.opengl.glu.GLU;
import cz.fidentis.analyst.symmetry.Config;
import cz.fidentis.analyst.symmetry.SignificantPoints;
import cz.fidentis.analyst.symmetry.SymmetryEstimator;

import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

/**
 *
 * @author Natália Bebjaková
 */
public class GeneralGLEventListener implements GLEventListener {  
    
    /**
     * MeshModel that is displayed
     */
    private MeshModel model = new MeshModel();
    
    /**
     * MeshModel of the symmetry plane
     */
    private MeshFacet symmetryPlaneFacet = null;
    
    /**
     * GLCanvas which listener belongs to 
     */
    protected Canvas glCanvas;  
    
    /**
    * GLU object.
    */
    protected GLU glu;
    
    /**
     * Usage of openGL version 2
     */
    protected GL2 gl;
    
    /**
     * The last viewport.
     */
    protected int[] viewport = new int[4];
     
    /**
     * The last model matrix.
     */
    protected float[] modelViewMatrix = new float[16];
    
    /**
     * The last projection matrix.
     */
    protected float[] projectionMatrix = new float[16];  
    
    /**
     * The X coordinate of the last known mouse position during the scene rotation.
     */
    int mouseX = 0;
    
    /**
     * The Y coordinate of the last know mouse position during the scene rotation.
     */
    int mouseY = 0;
    
    protected Vector3f defaultPosition = new Vector3f(0, 0, 300);
    protected Vector3f currentPosition = new Vector3f(0, 0, 300);

    protected double zCenter = 0;
    protected double xCenter = 0;
    protected double yCenter = 0;

    protected double zCameraPosition;
    protected double xCameraPosition;
    protected double yCameraPosition;

    protected double zUpPosition = 0;
    protected double xUpPosition = 0;
    protected double yUpPosition = 1;
    
    /**
     * Decides if model is displayed as wire-frame
     */  
    protected boolean wireModel = false;

    /**
     * decides if the symmetryPlane is displayed
     */
    protected static boolean symmetryPlane;

    /**
     * Decides if the background will be white
     */
    protected boolean whiteBackround = false;
    
    /**
     * 
     * @return is background white or not
     */
    public boolean isWhiteBackround() {
        return whiteBackround;
    }
    
    /**
     * 
     * @return Matrix for model view
     */
    public float[] getModelViewMatrix() {
        return modelViewMatrix;
    }

    /**
     * 
     * @return Matrix for projection
     */
    public float[] getProjectionMatrix() {
        return projectionMatrix;
    }
    
    /**
     * 
     * @return GlCanvas for displaying
     */
    public Canvas getGlCanvas() {
        return glCanvas;
    }
    
    /**
     * 
     * @param drawWire Decides if model is displayed as wire-frame
     */
    public void setWireMode(boolean drawWire) {
        wireModel = drawWire;
    }

    /**
     *
     * @param drawSymmetry Decides if model is displayed as wire-frame
     */
    public void setSymmetryPlane(boolean drawSymmetry) {
        symmetryPlane = drawSymmetry;
    }

    /**
     * 
     * @param whiteBackround Is backround white or not
     */
    public void setWhiteBackround(boolean whiteBackround) {
        this.whiteBackround = whiteBackround;
    }

    /**
     * Creates new EventListener
     * @param canvas GLCanvas which listener belongs to
     */
    public GeneralGLEventListener(Canvas canvas) {
        this.glCanvas = canvas;
    }
    
    /**
     *
     * @param model Set model to be displayed
     */
    public void setModel(MeshModel model) {
        this.model = model;
    }

    /**
     *
     * @param symmetryPlaneFacet Set model of the symmetry plane
     */
    public void setSymmetryPlaneFacet(MeshFacet symmetryPlaneFacet) {
        this.symmetryPlaneFacet = symmetryPlaneFacet;
    }

    /**
     *
     * @return Returns displayed model 
     */
    public MeshModel getModel() {
        return model;
    }

    
    /**
     * Invoked when main frame is created
     * @param glad Glad object
     */
    @Override
    public void init(GLAutoDrawable glad) {
        System.out.println("OLD GeneralGLEventListener.init");
        
        this.gl = (GL2) glad.getGL();
        glu = new GLU();
      
        gl.setSwapInterval(1);
        gl.glEnable(GL2.GL_LIGHTING);
        gl.glEnable(GL2.GL_LIGHT0);
        gl.glEnable(GL2.GL_DEPTH_TEST);
        gl.glClearColor(0,0,0,0);     // background for GLCanvas
        gl.glClear(GL2.GL_COLOR_BUFFER_BIT);
        
        gl.glShadeModel(GL2.GL_SMOOTH);    // use smooth shading

        gl.glDepthFunc(GL2.GL_LESS);
        gl.glDepthRange(0.0, 1.0);
        gl.glEnable(GL_DEPTH_TEST); 

        gl.glEnable(GL2.GL_NORMALIZE);
        gl.glDisable(GL2.GL_CULL_FACE);
    }

    /**
     * Invoked when main frame is closed
     * @param glad Glad object
     */
    @Override
    public void dispose(GLAutoDrawable glad) {
    }
    
    /**
     * Invoked every frame.
     * @param glad Glad object
     */
    @Override
    public void display(GLAutoDrawable glad) {
        wireModel = glCanvas.getDrawWired(); // is wire-frame or not
        symmetryPlane = glCanvas.getDrawSymmetryPlane();

        //Generate SymmetryPlane first time we request to see it
        if (symmetryPlane && symmetryPlaneFacet == null) {
            SymmetryEstimator estimator = new SymmetryEstimator(Config.getDefault(), SignificantPoints.CurvatureAlg.GAUSSIAN);
            model.getFacets().get(0).accept(estimator);
            symmetryPlaneFacet = estimator.getSymmetryPlaneMesh();

            //Set calculated planes for the loaded Face
            glCanvas.loadedFace.setSymmetryPlane(estimator.getSymmetryPlane());
            glCanvas.loadedFace.setCuttingPlane(symmetryPlaneFacet);
        }

        if (whiteBackround) {
            gl.glClearColor(0.9f,0.9f,0.9f,0); 
        } else {
            gl.glClearColor(0.25f,0.25f,0.25f,0); 
        }
        // background for GLCanvas       
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
        gl.glLoadIdentity();

        // sets model to proper position
        glu.gluLookAt(xCameraPosition, yCameraPosition, zCameraPosition, xCenter, yCenter, zCenter, xUpPosition, yUpPosition, zUpPosition);

        gl.glShadeModel(GL2.GL_SMOOTH);
        gl.glGetIntegerv(GL_VIEWPORT, viewport, 0);
        gl.glGetFloatv(GL_MODELVIEW_MATRIX, modelViewMatrix, 0);
        gl.glGetFloatv(GL_PROJECTION_MATRIX, projectionMatrix, 0);
        
        //if there is any model, draw 
        if (model != null) {
            if (wireModel) {
                gl.glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); //drawn as wire-frame
            } else {
                gl.glPolygonMode( GL_FRONT_AND_BACK, GL_FILL); // drawn as full traingles
            }

            drawWithoutTextures(model);
            if (symmetryPlane) {
                renderFacet(symmetryPlaneFacet);
            }
        } 
            
        //gl.glPopMatrix();
        gl.glFlush();
    }
    
    /**
     * Loops through the facets and render each of them 
     * 
     * @param model model of the face
     */
    public void drawWithoutTextures(MeshModel model) {
        for (int i = 0; i < model.getFacets().size(); i++) {
            renderFacet(model.getFacets().get(i));
        }
    }
    
    /**
     * Loops through the facet and render all the vertices as they are stored in corner table
     * 
     * @param facet facet of model
     */
    public void renderFacet(MeshFacet facet) {
        gl.glBegin(GL2.GL_TRIANGLES); //vertices are rendered as triangles
     
        // get the normal and tex coords indicies for face i  
        for (int v = 0; v < facet.getCornerTable().getSize(); v++) { 
            // render the normals
            Vector3d norm = facet.getVertices().get(facet.getCornerTable().getRow(v).getVertexIndex()).getNormal(); 
            if(norm != null) {
                gl.glNormal3d(norm.x, norm.y, norm.z);
            }
            // render the vertices
            Vector3d vert = facet.getVertices().get(facet.getCornerTable().getRow(v).getVertexIndex()).getPosition(); 
            gl.glVertex3d(vert.x, vert.y, vert.z);
        }
        gl.glEnd();

    }

    /**
     *
     * @param glad Glad object
     * @param x x
     * @param y y
     * @param width New width
     * @param height New height
     */
    @Override
    public void reshape(GLAutoDrawable glad, int x, int y, int width, int height) {

        if (height == 0) {
            height = 1;    // to avoid division by 0 in aspect ratio below
        }
        gl.glViewport(x, y, width, height);  // size of drawing area

        float h = (float) height / (float) width;

        gl.glMatrixMode(GL2.GL_PROJECTION);
        gl.glLoadIdentity();

        glu.gluPerspective(65, width / (float) height, 5.0f, 1500.0f);
        gl.glMatrixMode(GL2.GL_MODELVIEW);
        gl.glLoadIdentity();

        gl.glTranslatef(0.0f, 0.0f, -40.0f);
    }

    /**
     *
     * @param x New x position
     * @param y New y position
     * @param z New z position
     */
    public void setCameraPosition(float x, float y, float z) {
        currentPosition.set(x, y, z);

        setNewCameraPosition(currentPosition);
        zCameraPosition = defaultPosition.z;
        xCameraPosition = defaultPosition.x;
        yCameraPosition = defaultPosition.y;
    }    

    /**
     *
     * @param position New position of camera
     */
    public void setNewCameraPosition(Vector3f position) {
        xCameraPosition = position.x;
        yCameraPosition = position.y;
        zCameraPosition = position.z;
    }

    /**
     *
     * @param degree degree of rotation
     */
    public void rotateUp(double degree) {
        rotate(-degree, 0);
    }

    /**
     *
     * @param degree degree of rotation
     */
    public void rotateDown(double degree) {
        rotate(degree, 0);
    }

    /**
     *
     * @param degree degree of rotation
     */
    public void rotateLeft(double degree) {
        rotate(0, degree);
    }

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

    /**
     * 
     * @return X axis
     */
    private Vector3f getXaxis() {
        Vector3f xAxis = new Vector3f(
                (float) ((yCameraPosition - yCenter) *zUpPosition - (zCameraPosition - zCenter) * yUpPosition),
                (float) ((zCameraPosition - zCenter) * xUpPosition - (xCameraPosition - xCenter) * zUpPosition),
                (float) ((xCameraPosition - xCenter) * yUpPosition - xUpPosition * (yCameraPosition - yCenter)));
         
        float length = (float) Math.sqrt(xAxis.x * xAxis.x +
                xAxis.y * xAxis.y + xAxis.z * xAxis.z);
        xAxis.set(xAxis.x / length, xAxis.y / length, xAxis.z / length);
        return xAxis;
    }

    /**
     * 
     * @return Y axis
     */
    private Vector3f getYaxis() {
         Vector3f yAxis = new Vector3f((float) xUpPosition, (float) yUpPosition, (float) zUpPosition);
        float length = (float) Math.sqrt(yAxis.x * yAxis.x +
                yAxis.y * yAxis.y + yAxis.z * yAxis.z);
        yAxis.set((yAxis.x / length), (yAxis.y / length), (yAxis.z / length));
        return yAxis;
    }

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

        Vector3f point = new Vector3f((float) xCameraPosition,
                (float) yCameraPosition, (float) zCameraPosition);

        Vector3f camera = rotateAroundAxe(point, xAxis, Math.toRadians(xAngle));
        camera = rotateAroundAxe(camera, yAxis, Math.toRadians(yAngle));

        point = new Vector3f((float) xUpPosition, (float) yUpPosition, (float) zUpPosition);

        Vector3f up = rotateAroundAxe(point, xAxis, Math.toRadians(xAngle));
        up = rotateAroundAxe(up, yAxis, Math.toRadians(yAngle));

        xUpPosition = up.x;
        yUpPosition = up.y;
        zUpPosition = up.z;

        setNewCameraPosition(camera);
    }

    /**
     * 
     * @param xShift xShift
     * @param yShift yShift
     */
    public void move(double xShift, double yShift) {
        Vector3f xAxis = getXaxis();
        Vector3f yAxis = getYaxis();

        Vector3f shift = new Vector3f((float) (xAxis.x * xShift + yAxis.x * yShift),
                (float) (xAxis.y * xShift + yAxis.y * yShift), (float) (xAxis.z * xShift + yAxis.z * yShift));
        Vector3f camera = new Vector3f((float) xCameraPosition + shift.x, (float) yCameraPosition + shift.y,
                (float) zCameraPosition + shift.z);
        xCenter += shift.x;
        yCenter += shift.y;
        zCenter += shift.z;

        setNewCameraPosition(camera);
    }

    /**
     * Calculate the new position f point from given angle and rotation axe.
     *
     * @param point original position
     * @param u vector of rotation axe
     * @param angle angle of rotation
     * @return new position
     */
    public Vector3f rotateAroundAxe(Vector3f point, Vector3f u, double angle) {
        Vector3f p;
        float x = (float) ((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);
        float y = (float) ((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);
        float z = (float) ((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);
        p = new Vector3f(x, y, z);

        return p;
    }

    /**
     * Sets model to the starting position
     */
    public void rotationAndSizeRestart() {
        xUpPosition = 0;
        yUpPosition = 1;
        zUpPosition = 0;

        setNewCameraPosition(defaultPosition);
        xCenter = 0;
        yCenter = 0;
        zCenter = 0;
    }

    /**
     *
     * @param distance Distance to be zoom in
     */
    public void zoomIn(double distance) {
        double x = xCameraPosition - xCenter;
        double y = yCameraPosition - yCenter;
        double z = zCameraPosition - zCenter;
        double sqrt = Math.sqrt(x * x + y * y + z * z);

        if (sqrt > 0) {
            xCameraPosition = xCenter + ((sqrt - distance) * x / sqrt);
            yCameraPosition = yCenter + ((sqrt - distance) * y / sqrt);
            zCameraPosition = zCenter + ((sqrt - distance) * z / sqrt);
        }
    }

    /**
     *
     * @param distance Distance to be zoom out
     */
    public void zoomOut(double distance) {
        double x = xCameraPosition - xCenter;
        double y = yCameraPosition - yCenter;
        double z = zCameraPosition - zCenter;
        double sqrt = Math.sqrt(x * x + y * y + z * z);

        if (sqrt == 0) {
            sqrt = 1;
        }
        xCameraPosition = xCenter + ((sqrt + distance) * x / sqrt);
        yCameraPosition = yCenter + ((sqrt + distance) * y / sqrt);
        zCameraPosition = zCenter + ((sqrt + distance) * z / sqrt);
    }
}
