/*
 * Copyright (c) 2009-2015 jMonkeyEngine
 * 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 'jMonkeyEngine' 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 THE COPYRIGHT OWNER OR
 * CONTRIBUTORS 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 com.jme3.scene.plugins;

import com.jme3.math.Vector4f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.scene.mesh.IndexIntBuffer;
import com.jme3.scene.mesh.IndexShortBuffer;
import com.jme3.util.BufferUtils;
import com.jme3.util.IntMap;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

public final class IrUtils {
    
    private static final Logger logger = Logger.getLogger(IrUtils.class.getName());
    
    private IrUtils() { }
    
    private static IrPolygon[] quadToTri(IrPolygon quad) {
        if (quad.vertices.length == 3) {
            throw new IllegalStateException("Already a triangle");
        } 
        
        IrPolygon[] t = new IrPolygon[]{ new IrPolygon(), new IrPolygon() };
        t[0].vertices = new IrVertex[3];
        t[1].vertices = new IrVertex[3];
        
        IrVertex v0 = quad.vertices[0];
        IrVertex v1 = quad.vertices[1];
        IrVertex v2 = quad.vertices[2];
        IrVertex v3 = quad.vertices[3];
        
        // find the pair of verticies that is closest to each over
        // v0 and v2
        // OR
        // v1 and v3
        float d1 = v0.pos.distanceSquared(v2.pos);
        float d2 = v1.pos.distanceSquared(v3.pos);
        if (d1 < d2) {
            // v0 is close to v2
            // put an edge in v0, v2
            t[0].vertices[0] = v0;
            t[0].vertices[1] = v1;
            t[0].vertices[2] = v3;

            t[1].vertices[0] = v1;
            t[1].vertices[1] = v2;
            t[1].vertices[2] = v3;
        } else {
            // put an edge in v1, v3
            t[0].vertices[0] = v0;
            t[0].vertices[1] = v1;
            t[0].vertices[2] = v2;

            t[1].vertices[0] = v0;
            t[1].vertices[1] = v2;
            t[1].vertices[2] = v3;
        }
        
        return t;
    }
    
    /**
     * Applies smoothing groups to vertex normals.
     */
    public static IrMesh applySmoothingGroups(IrMesh mesh) {
        return null;
    }
    
    private static void toTangentsWithParity(IrVertex vertex) {
        if (vertex.tang != null && vertex.bitang != null) {
            float wCoord = vertex.norm.cross(vertex.tang).dot(vertex.bitang) < 0f ? -1f : 1f;
            vertex.tang4d = new Vector4f(vertex.tang.x, vertex.tang.y, vertex.tang.z, wCoord);
            vertex.tang = null;
            vertex.bitang = null;
        }
    }
    
    public static void toTangentsWithParity(IrMesh mesh) {
        for (IrPolygon polygon : mesh.polygons) {
            for (IrVertex vertex : polygon.vertices) {
                toTangentsWithParity(vertex);
            }
        }
    }
    
    private static void trimBoneWeights(IrVertex vertex) {
        if (vertex.boneWeightsIndices == null) {
            return;
        }
        
        IrBoneWeightIndex[] boneWeightsIndices = vertex.boneWeightsIndices;
        
        if (boneWeightsIndices.length <= 4) {
            return;
        }
        
        // Sort by weight
        boneWeightsIndices = Arrays.copyOf(boneWeightsIndices, boneWeightsIndices.length);
        Arrays.sort(boneWeightsIndices);
        
        // Trim to four weights at most
        boneWeightsIndices = Arrays.copyOf(boneWeightsIndices, 4);
        
        // Renormalize weights
        float sum = 0;
        
        for (int i = 0; i < boneWeightsIndices.length; i++) {
            sum += boneWeightsIndices[i].boneWeight;
        }
        
        if (sum != 1f) {
            float sumToB = sum == 0 ? 0 : 1f / sum;
            for (int i = 0; i < boneWeightsIndices.length; i++) {
                IrBoneWeightIndex original = boneWeightsIndices[i];
                boneWeightsIndices[i] = new IrBoneWeightIndex(original.boneIndex, original.boneWeight * sumToB);
            }
        }
        
        vertex.boneWeightsIndices = boneWeightsIndices;
    }
    
    /**
     * Removes low bone weights from mesh, leaving only 4 bone weights at max.
     */
    public static void trimBoneWeights(IrMesh mesh) {
        for (IrPolygon polygon : mesh.polygons) {
            for (IrVertex vertex : polygon.vertices) {
                trimBoneWeights(vertex);
            }
        }
    }
    
    /**
     * Convert mesh from quads / triangles to triangles only.
     */
    public static void triangulate(IrMesh mesh) {
        List<IrPolygon> newPolygons = new ArrayList<IrPolygon>(mesh.polygons.length);
        for (IrPolygon inputPoly : mesh.polygons) {
            if (inputPoly.vertices.length == 4) {
                IrPolygon[] tris = quadToTri(inputPoly);
                newPolygons.add(tris[0]);
                newPolygons.add(tris[1]);
            } else if (inputPoly.vertices.length == 3) {
                newPolygons.add(inputPoly);
            } else {
                // N-gon. We have to ignore it..
                logger.log(Level.WARNING, "N-gon encountered, ignoring. "
                                        + "The mesh may not appear correctly. "
                                        + "Triangulate your model prior to export.");
            }
        }
        mesh.polygons = new IrPolygon[newPolygons.size()];
        newPolygons.toArray(mesh.polygons);
    }
    
    /**
     * Separate mesh with multiple materials into multiple meshes each with 
     * one material each.
     * 
     * Polygons without a material will be added to key = -1.
     */
    public static IntMap<IrMesh> splitByMaterial(IrMesh mesh) {
        IntMap<List<IrPolygon>> materialToPolyList = new IntMap<List<IrPolygon>>();
        for (IrPolygon polygon : mesh.polygons) {
            int materialIndex = -1;
            for (IrVertex vertex : polygon.vertices) {
                if (vertex.material == null) {
                    continue;
                }
                if (materialIndex == -1) {
                    materialIndex = vertex.material;
                } else if (materialIndex != vertex.material) {
                    throw new UnsupportedOperationException("Multiple materials "
                                                 + "assigned to the same polygon");
                }
            }
            List<IrPolygon> polyList = materialToPolyList.get(materialIndex);
            if (polyList == null) {
                polyList = new ArrayList<IrPolygon>();
                materialToPolyList.put(materialIndex, polyList);
            }
            polyList.add(polygon);
        }
        IntMap<IrMesh> materialToMesh = new IntMap<IrMesh>();
        for (IntMap.Entry<List<IrPolygon>> entry : materialToPolyList) {
            int key = entry.getKey();
            List<IrPolygon> polygons = entry.getValue();
            if (polygons.size() > 0) {
                IrMesh newMesh = new IrMesh();
                newMesh.polygons = new IrPolygon[polygons.size()];
                polygons.toArray(newMesh.polygons);
                materialToMesh.put(key, newMesh);
            }
        }
        return materialToMesh;
    }
     
    /**
     * Convert IrMesh to jME3 mesh.
     */
    public static Mesh convertIrMeshToJmeMesh(IrMesh mesh) {
        Map<IrVertex, Integer> vertexToVertexIndex = new HashMap<IrVertex, Integer>();
        List<IrVertex> vertices = new ArrayList<IrVertex>();
        List<Integer> indexes = new ArrayList<Integer>();
        
        int vertexIndex = 0;
        for (IrPolygon polygon : mesh.polygons) {
            if (polygon.vertices.length != 3) {
                throw new UnsupportedOperationException("IrMesh must be triangulated first");
            }
            for (IrVertex vertex : polygon.vertices) {
                // Is this vertex already indexed?
                Integer existingIndex = vertexToVertexIndex.get(vertex);
                if (existingIndex == null) {
                    // Not indexed yet, allocate index.
                    indexes.add(vertexIndex);
                    vertexToVertexIndex.put(vertex, vertexIndex);
                    vertices.add(vertex);
                    vertexIndex++;
                } else {
                    // Index already allocated for this vertex, reuse it.
                    indexes.add(existingIndex);
                }
            }
        }
        
        Mesh jmeMesh = new Mesh();
        jmeMesh.setMode(Mesh.Mode.Triangles);
        
        FloatBuffer posBuf = null;
        FloatBuffer normBuf = null;
        FloatBuffer tangBuf = null;
        FloatBuffer uv0Buf = null;
        FloatBuffer uv1Buf = null;
        ByteBuffer colorBuf = null;
        ByteBuffer boneIndices = null;
        FloatBuffer boneWeights = null;
        IndexBuffer indexBuf = null;
        
        IrVertex inspectionVertex = vertices.get(0);
        if (inspectionVertex.pos != null) {
            posBuf = BufferUtils.createVector3Buffer(vertices.size());
            jmeMesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
        }
        if (inspectionVertex.norm != null) {
            normBuf = BufferUtils.createVector3Buffer(vertices.size());
            jmeMesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
        }
        if (inspectionVertex.tang4d != null) {
            tangBuf = BufferUtils.createFloatBuffer(vertices.size() * 4);
            jmeMesh.setBuffer(VertexBuffer.Type.Tangent, 4, tangBuf);
        }
        if (inspectionVertex.tang != null || inspectionVertex.bitang != null) {
            throw new IllegalStateException("Mesh is using 3D tangents, must be converted to 4D tangents first.");
        }
        if (inspectionVertex.uv0 != null) {
            uv0Buf = BufferUtils.createVector2Buffer(vertices.size());
            jmeMesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv0Buf);
        }
        if (inspectionVertex.uv1 != null) {
            uv1Buf = BufferUtils.createVector2Buffer(vertices.size());
            jmeMesh.setBuffer(VertexBuffer.Type.TexCoord2, 2, uv1Buf);
        }
        if (inspectionVertex.color != null) {
            colorBuf = BufferUtils.createByteBuffer(vertices.size() * 4);
            jmeMesh.setBuffer(VertexBuffer.Type.Color, 4, colorBuf);
            jmeMesh.getBuffer(VertexBuffer.Type.Color).setNormalized(true);
        }
        if (inspectionVertex.boneWeightsIndices != null) {
            boneIndices = BufferUtils.createByteBuffer(vertices.size() * 4);
            boneWeights = BufferUtils.createFloatBuffer(vertices.size() * 4);
            jmeMesh.setBuffer(VertexBuffer.Type.BoneIndex,  4, boneIndices);
            jmeMesh.setBuffer(VertexBuffer.Type.BoneWeight, 4, boneWeights);
            
            //creating empty buffers for HW skinning 
            //the buffers will be setup if ever used.
            VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
            VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
            //setting usage to cpuOnly so that the buffer is not send empty to the GPU
            indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
            weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
            
            jmeMesh.setBuffer(weightsHW);
            jmeMesh.setBuffer(indicesHW);
        }
        if (vertices.size() >= 65536) {
            // too many verticies: use intbuffer instead of shortbuffer
            IntBuffer ib = BufferUtils.createIntBuffer(indexes.size());
            jmeMesh.setBuffer(VertexBuffer.Type.Index, 3, ib);
            indexBuf = new IndexIntBuffer(ib);
        } else {
            ShortBuffer sb = BufferUtils.createShortBuffer(indexes.size());
            jmeMesh.setBuffer(VertexBuffer.Type.Index, 3, sb);
            indexBuf = new IndexShortBuffer(sb);
        }
        
        jmeMesh.setStatic();
        
        int maxBonesPerVertex = -1;
        
        for (IrVertex vertex : vertices) {
            if (posBuf != null) {
                posBuf.put(vertex.pos.x).put(vertex.pos.y).put(vertex.pos.z);
            }
            if (normBuf != null) {
                normBuf.put(vertex.norm.x).put(vertex.norm.y).put(vertex.norm.z);
            }
            if (tangBuf != null) {
                tangBuf.put(vertex.tang4d.x).put(vertex.tang4d.y).put(vertex.tang4d.z).put(vertex.tang4d.w);
            }
            if (uv0Buf != null) {
                uv0Buf.put(vertex.uv0.x).put(vertex.uv0.y);
            }
            if (uv1Buf != null) {
                uv1Buf.put(vertex.uv1.x).put(vertex.uv1.y);
            }
            if (colorBuf != null) {
                colorBuf.putInt(vertex.color.asIntABGR());
            }
            if (boneIndices != null) {
                if (vertex.boneWeightsIndices != null) {
                    if (vertex.boneWeightsIndices.length > 4) {
                        throw new UnsupportedOperationException("Mesh uses more than 4 weights per bone. " +
                                                                "Call trimBoneWeights() to allieviate this");
                    }
                    for (int i = 0; i < vertex.boneWeightsIndices.length; i++) {
                        boneIndices.put((byte) (vertex.boneWeightsIndices[i].boneIndex & 0xFF));
                        boneWeights.put(vertex.boneWeightsIndices[i].boneWeight);
                    }
                    for (int i = 0; i < 4 - vertex.boneWeightsIndices.length; i++) {
                        boneIndices.put((byte)0);
                        boneWeights.put(0f);
                    }
                } else {
                    boneIndices.putInt(0);
                    boneWeights.put(0f).put(0f).put(0f).put(0f);
                }
                
                maxBonesPerVertex = Math.max(maxBonesPerVertex, vertex.boneWeightsIndices.length);
            }
        }
        
        for (int i = 0; i < indexes.size(); i++) {
            indexBuf.put(i, indexes.get(i));
        }
        
        jmeMesh.updateCounts();
        jmeMesh.updateBound();
        
        if (boneIndices != null) {
            jmeMesh.setMaxNumWeights(maxBonesPerVertex);
            jmeMesh.prepareForAnim(true);
            jmeMesh.generateBindPose(true);
        }
        
        return jmeMesh;
    }
}
