package cz.fidentis.analyst.tests;

import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GLAutoDrawable;
import cz.fidentis.analyst.face.HumanFace;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.mesh.core.MeshModel;

import javax.vecmath.Vector3d;
import java.util.List;
import java.util.Map;

import static com.jogamp.opengl.GL.GL_FRONT_AND_BACK;
import static com.jogamp.opengl.GL.GL_VIEWPORT;
import static com.jogamp.opengl.GL2GL3.GL_FILL;
import static com.jogamp.opengl.GL2GL3.GL_LINE;
import static com.jogamp.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW_MATRIX;
import static com.jogamp.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION_MATRIX;
import cz.fidentis.analyst.gui.Canvas;
import cz.fidentis.analyst.gui.GeneralGLEventListener;
import cz.fidentis.analyst.mesh.core.MeshPoint;
import cz.fidentis.analyst.visitors.mesh.Curvature;
import cz.fidentis.analyst.visitors.mesh.HausdorffDistance;
import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JButton;
import javax.swing.JFrame;
import org.openide.util.Exceptions;

/**
 * Testing application displaying different heat maps in 3D.
 * 
 * @author Daniel Sokol
 * @author Radek Oslejsek
 */
public class HeatMapsTestApp extends GeneralGLEventListener {
    
    private static final JFrame TEST_FRAME = new JFrame();
    
    private Map<MeshFacet, List<Double>> values;
    private final Color minColor = Color.BLUE;
    private final Color maxColor = Color.RED;
    
    private double minDistance;
    private double maxDistance;

    
    /**
     * Constructor.
     * @param canvas Canvas
     */
    public HeatMapsTestApp(Canvas canvas) {
        super(canvas);
    }
    
    /**
     * Main method 
     * @param args Input arguments 
     * @throws IOException on IO error
     */
    public static void main(String[] args) {
        //Declaration
        JButton gaussButton = new JButton("Show Gaussian curvature");
        JButton meanButton = new JButton("Show mean curvature");
        JButton maxButton = new JButton("Show maximum principal curvature");
        JButton minButton = new JButton("Show minimum principal curvature");
        JButton hdButton1 = new JButton("Show Hausdorff distance to average boy - p2p abosult");
        JButton hdButton2 = new JButton("Show Hausdorff distance to average boy - p2p relative");

        Canvas testCanvas = new Canvas();
        HeatMapsTestApp colorListener = new HeatMapsTestApp(testCanvas);
        testCanvas.setListener(colorListener);
        
        gaussButton.addActionListener(e -> colorListener.computeGaussian());
        meanButton.addActionListener(e -> colorListener.computeMean());
        maxButton.addActionListener(e -> colorListener.computeMaxPrincipal());
        minButton.addActionListener(e -> colorListener.computeMinPrincipal());
        hdButton1.addActionListener(e -> colorListener.computeHausdorffDistanceP2pAbs());
        hdButton2.addActionListener(e -> colorListener.computeHausdorffDistanceP2pRel());

        TEST_FRAME.setLayout(new GridBagLayout());
        TEST_FRAME.setVisible(true);
        TEST_FRAME.setSize(1600, 900);

        // Adding
        GridBagConstraints c = new GridBagConstraints();
        c.fill = GridBagConstraints.BOTH;
        c.gridx = 0;
        c.gridy = 0;
        c.weighty = 1;
        c.weightx = 1;

        c.gridheight = 6;
        c.gridwidth = 4;
        c.weightx = 4;
        TEST_FRAME.add(testCanvas, c);
        c.gridheight = 1;
        c.gridwidth = 2;
        c.gridx = 4;
        c.weightx = 2;
        //testFrame.add(addButton, c);
        c.gridy = 1;
        TEST_FRAME.add(gaussButton, c);
        c.gridy = 2;
        TEST_FRAME.add(meanButton, c);
        c.gridy = 3;
        TEST_FRAME.add(maxButton, c);
        c.gridy = 4;
        TEST_FRAME.add(minButton, c);
        c.gridy = 5;
        TEST_FRAME.add(hdButton1, c);
        c.gridy = 6;
        TEST_FRAME.add(hdButton2, c);
    }
    
    @Override
    public void setModel(MeshModel model) {
        super.setModel(model);
        
        System.err.println();
        System.err.println("MODEL: " + getModel());
        
        Set<Vector3d> unique = new HashSet<>();
        for (MeshPoint p: getModel().getFacets().get(0).getVertices()) {
            unique.add(p.getPosition());
        }
        System.out.println("UNIQUE VERTICES: " + unique.size());
        /*
        for (Vector3d v: unique2) {
            for (MeshPoint p: uniqueVerts) {
                if (v.equals(p.getPosition())) {
                    System.out.println(p);
                }
            }
            System.out.println("----------------");
        }
        */
    }

    /**
     * Computes curvature
     */
    public void computeGaussian() {
        if (this.getModel() == null) {
            return;
        }
        
        long startTime = System.currentTimeMillis();
        Curvature vis = new Curvature();
        this.getModel().compute(vis);
        values = vis.getGaussianCurvatures();
        long duration =  System.currentTimeMillis() - startTime;
        System.err.println(duration + "\tmsec: Gaussian curvature");
        
        minDistance = -0.02;
        maxDistance = 0.008;
    }
    
    /**
     * Computes curvature
     */
    public void computeMean() {
        if (this.getModel() == null) {
            return;
        }
        
        long startTime = System.currentTimeMillis();
        Curvature vis = new Curvature();
        this.getModel().compute(vis);
        values = vis.getMeanCurvatures();
        long duration =  System.currentTimeMillis() - startTime;
        System.err.println(duration + "\tmsec: Mean curvature");
        
        minDistance = -0.001;
        maxDistance = 0.1;
    }
    
    /**
     * Computes curvature
     */
    public void computeMinPrincipal() {
        if (this.getModel() == null) {
            return;
        }
        
        long startTime = System.currentTimeMillis();
        Curvature vis = new Curvature();
        this.getModel().compute(vis);
        values = vis.getMinPrincipalCurvatures();
        long duration =  System.currentTimeMillis() - startTime;
        System.err.println(duration + "\tmsec: Minimum principal curvature");
        
        minDistance = -0.05;
        maxDistance = 0.03;
    }
    
    /**
     * Computes curvature
     */
    public void computeMaxPrincipal() {
        if (this.getModel() == null) {
            return;
        }
        
        long startTime = System.currentTimeMillis();
        Curvature vis = new Curvature();
        this.getModel().compute(vis);
        values = vis.getMaxPrincipalCurvatures();
        long duration =  System.currentTimeMillis() - startTime;
        System.err.println(duration + "\tmsec: Maximum principal curvature");
        
        minDistance = -0.02;
        maxDistance = 0.25;
    }
    
    /**
     * Computes absolute HD
     */
    public void computeHausdorffDistanceP2pAbs() {
        if (this.getModel() == null) {
            return;
        }
        try {
            HumanFace boy = new HumanFace(new File("src/test/resources/cz/fidentis/analyst/average_boy_17-20.obj"));
            long startTime = System.currentTimeMillis();
            HausdorffDistance vis = new HausdorffDistance(boy.getMeshModel(), Strategy.POINT_TO_POINT, false, true);
            this.getModel().compute(vis);
            values = vis.getDistances();
            long duration =  System.currentTimeMillis() - startTime;
            System.err.println(duration + "\tmsec: Hausdorff distance " + vis.getStrategy() + " " + (vis.relativeDistance() ? "RELATIVE" : "ABSOLUTE"));
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        List<Double> distanceList = values.get(getModel().getFacets().get(0));
        minDistance = distanceList.stream().mapToDouble(Double::doubleValue).min().getAsDouble();
        maxDistance = distanceList.stream().mapToDouble(Double::doubleValue).max().getAsDouble();
    }
    
    /**
     * Computes relative HD
     */
    public void computeHausdorffDistanceP2pRel() {
        if (this.getModel() == null) {
            return;
        }
        try {
            HumanFace boy = new HumanFace(new File("src/test/resources/cz/fidentis/analyst/average_boy_17-20.obj"));
            long startTime = System.currentTimeMillis();
            HausdorffDistance vis = new HausdorffDistance(boy.getMeshModel(), Strategy.POINT_TO_POINT, true, true);
            this.getModel().compute(vis);
            values = vis.getDistances();
            long duration =  System.currentTimeMillis() - startTime;
            System.err.println(duration + "\tmsec: Hausdorff distance " + vis.getStrategy() + " " + (vis.relativeDistance() ? "RELATIVE" : "ABSOLUTE"));
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        List<Double> distanceList = values.get(getModel().getFacets().get(0));
        minDistance = distanceList.stream().mapToDouble(Double::doubleValue).min().getAsDouble();
        maxDistance = distanceList.stream().mapToDouble(Double::doubleValue).max().getAsDouble();
    }
    
    @Override
    public void display(GLAutoDrawable glad) {
        wireModel = glCanvas.getDrawWired(); // is wire-frame or not

        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 (values != null) {
            drawHeatmap(this.getModel());
        } else if (getModel() != 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(getModel());
        }

        gl.glFlush();
    }

    protected void drawHeatmap(MeshModel model) {
        for (int i = 0; i < model.getFacets().size(); i++) {
            renderFaceWithHeatmap(model.getFacets().get(i), values.get(model.getFacets().get(i)), minDistance, maxDistance);
        }
    }

    protected void renderFaceWithHeatmap(MeshFacet facet, List<Double> distancesList, Double minDistance, Double maxDistance) {
        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();
            //get color of vertex
            Color c = getColor(distancesList.get(facet.getCornerTable().getRow(v).getVertexIndex()), minDistance, maxDistance);
            gl.glMaterialfv(GL2.GL_FRONT_AND_BACK, GL2.GL_DIFFUSE, c.getComponents(null), 0);

            gl.glVertex3d(vert.x, vert.y, vert.z);
        }
        gl.glEnd();
        gl.glPopAttrib();
    }

    protected Color getColor(Double currentDistance, Double minDistance, Double maxDistance) {
        double currentParameter = ((currentDistance - minDistance) / (maxDistance - minDistance));
        
        if (currentDistance > maxDistance) {
            currentParameter = 1.0;
        } else if (currentDistance < minDistance || (maxDistance - minDistance) == 0) { 
            currentParameter = 0.0;
        }
        
        float[] hsb1 = Color.RGBtoHSB(minColor.getRed(), minColor.getGreen(), minColor.getBlue(), null);
        double h1 = hsb1[0];
        double s1 = hsb1[1];
        double b1 = hsb1[2];

        float[] hsb2 = Color.RGBtoHSB(maxColor.getRed(), maxColor.getGreen(), maxColor.getBlue(), null);
        double h2 = hsb2[0];
        double s2 = hsb2[1];
        double b2 = hsb2[2];

        // determine clockwise and counter-clockwise distance between hues
        double distCCW;
        double distCW;

        if (h1 >= h2) {
            distCCW = h1 - h2;
            distCW = 1 + h2 - h1;
        } else {
            distCCW = 1 + h1 - h2;
            distCW = h2 - h1;
        }

        double hue;

        if (distCW >= distCCW) {
            hue = h1 + (distCW * currentParameter);
        } else {
            hue = h1 - (distCCW * currentParameter);
        }

        if (hue < 0) {
            hue = 1 + hue;
        }
        if (hue > 1) {
            hue = hue - 1;
        }
        
        double saturation = (1 - currentParameter) * s1 + currentParameter * s2;
        double brightness = (1 - currentParameter) * b1 + currentParameter * b2;

        //System.out.println("AAA " + hue);

        return Color.getHSBColor((float)hue, (float)saturation, (float)brightness);
    }
}
