/*
 * Copyright (c) 2009-2012 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.blender.meshes;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.objects.Properties;

/**
 * A class that is used in mesh calculations.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public class MeshHelper extends AbstractBlenderHelper {
    private static final Logger LOGGER                   = Logger.getLogger(MeshHelper.class.getName());

    /** A type of UV data layer in traditional faced mesh (triangles or quads). */
    public static final int     UV_DATA_LAYER_TYPE_FMESH = 5;
    /** A type of UV data layer in bmesh type. */
    public static final int     UV_DATA_LAYER_TYPE_BMESH = 16;

    /** A material used for single lines and points. */
    private Material            blackUnshadedMaterial;

    /**
     * This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
     * versions.
     * 
     * @param blenderVersion
     *            the version read from the blend file
     * @param blenderContext
     *            the blender context
     */
    public MeshHelper(String blenderVersion, BlenderContext blenderContext) {
        super(blenderVersion, blenderContext);
    }

    /**
     * Converts the mesh structure into temporal mesh.
     * The temporal mesh is stored in blender context and here always a clone is being returned because the mesh might
     * be modified by modifiers.
     * 
     * @param meshStructure
     *            the mesh structure
     * @param blenderContext
     *            the blender context
     * @return temporal mesh read from the given structure
     * @throws BlenderFileException
     *             an exception is thrown when problems with reading blend file occur
     */
    public TemporalMesh toTemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException {
        LOGGER.log(Level.FINE, "Loading temporal mesh named: {0}.", meshStructure.getName());
        TemporalMesh temporalMesh = (TemporalMesh) blenderContext.getLoadedFeature(meshStructure.getOldMemoryAddress(), LoadedDataType.TEMPORAL_MESH);
        if (temporalMesh != null) {
            LOGGER.fine("The mesh is already loaded. Returning its clone.");
            return temporalMesh.clone();
        }

        if ("ID".equals(meshStructure.getType())) {
            LOGGER.fine("Loading mesh from external blend file.");
            return (TemporalMesh) this.loadLibrary(meshStructure);
        }

        String name = meshStructure.getName();
        LOGGER.log(Level.FINE, "Reading mesh: {0}.", name);
        temporalMesh = new TemporalMesh(meshStructure, blenderContext);

        LOGGER.fine("Loading materials.");
        MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
        temporalMesh.setMaterials(materialHelper.getMaterials(meshStructure, blenderContext));

        LOGGER.fine("Reading custom properties.");
        Properties properties = this.loadProperties(meshStructure, blenderContext);
        temporalMesh.setProperties(properties);

        blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, meshStructure);
        blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.TEMPORAL_MESH, temporalMesh);
        return temporalMesh.clone();
    }

    /**
     * Tells if the given mesh structure supports BMesh.
     * 
     * @param meshStructure
     *            the mesh structure
     * @return <b>true</b> if BMesh is supported and <b>false</b> otherwise
     */
    public boolean isBMeshCompatible(Structure meshStructure) {
        Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop");
        Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly");
        return pMLoop != null && pMPoly != null && pMLoop.isNotNull() && pMPoly.isNotNull();
    }

    /**
     * This method returns the vertices.
     * 
     * @param meshStructure
     *            the structure containing the mesh data
     * @return a list of two - element arrays, the first element is the vertex and the second - its normal
     * @throws BlenderFileException
     *             this exception is thrown when the blend file structure is somehow invalid or corrupted
     */
    @SuppressWarnings("unchecked")
    public void loadVerticesAndNormals(Structure meshStructure, List<Vector3f> vertices, List<Vector3f> normals) throws BlenderFileException {
        LOGGER.log(Level.FINE, "Loading vertices and normals from mesh: {0}.", meshStructure.getName());
        int count = ((Number) meshStructure.getFieldValue("totvert")).intValue();
        if (count > 0) {
            Pointer pMVert = (Pointer) meshStructure.getFieldValue("mvert");
            List<Structure> mVerts = pMVert.fetchData();
            Vector3f co = null, no = null;
            if (fixUpAxis) {
                for (int i = 0; i < count; ++i) {
                    DynamicArray<Number> coordinates = (DynamicArray<Number>) mVerts.get(i).getFieldValue("co");
                    co = new Vector3f(coordinates.get(0).floatValue(), coordinates.get(2).floatValue(), -coordinates.get(1).floatValue());
                    vertices.add(co);

                    DynamicArray<Number> norm = (DynamicArray<Number>) mVerts.get(i).getFieldValue("no");
                    no = new Vector3f(norm.get(0).shortValue() / 32767.0f, norm.get(2).shortValue() / 32767.0f, -norm.get(1).shortValue() / 32767.0f);
                    normals.add(no);
                }
            } else {
                for (int i = 0; i < count; ++i) {
                    DynamicArray<Number> coordinates = (DynamicArray<Number>) mVerts.get(i).getFieldValue("co");
                    co = new Vector3f(coordinates.get(0).floatValue(), coordinates.get(1).floatValue(), coordinates.get(2).floatValue());
                    vertices.add(co);

                    DynamicArray<Number> norm = (DynamicArray<Number>) mVerts.get(i).getFieldValue("no");
                    no = new Vector3f(norm.get(0).shortValue() / 32767.0f, norm.get(1).shortValue() / 32767.0f, norm.get(2).shortValue() / 32767.0f);
                    normals.add(no);
                }
            }
        }
        LOGGER.log(Level.FINE, "Loaded {0} vertices and normals.", vertices.size());
    }

    /**
     * This method returns the vertices colors. Each vertex is stored in byte[4] array.
     * 
     * @param meshStructure
     *            the structure containing the mesh data
     * @param blenderContext
     *            the blender context
     * @return a list of vertices colors, each color belongs to a single vertex or empty list of colors are not specified
     * @throws BlenderFileException
     *             this exception is thrown when the blend file structure is somehow invalid or corrupted
     */
    public List<byte[]> loadVerticesColors(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException {
        LOGGER.log(Level.FINE, "Loading vertices colors from mesh: {0}.", meshStructure.getName());
        MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
        Pointer pMCol = (Pointer) meshStructure.getFieldValue(meshHelper.isBMeshCompatible(meshStructure) ? "mloopcol" : "mcol");
        List<byte[]> verticesColors = new ArrayList<byte[]>();
        // it was likely a bug in blender untill version 2.63 (the blue and red factors were misplaced in their structure)
        // so we need to put them right
        boolean useBGRA = blenderContext.getBlenderVersion() < 263;
        if (pMCol.isNotNull()) {
            List<Structure> mCol = pMCol.fetchData();
            for (Structure color : mCol) {
                byte r = ((Number) color.getFieldValue("r")).byteValue();
                byte g = ((Number) color.getFieldValue("g")).byteValue();
                byte b = ((Number) color.getFieldValue("b")).byteValue();
                byte a = ((Number) color.getFieldValue("a")).byteValue();
                verticesColors.add(useBGRA ? new byte[] { b, g, r, a } : new byte[] { r, g, b, a });
            }
        }
        return verticesColors;
    }

    /**
     * The method loads the UV coordinates. The result is a map where the key is the user's UV set name and the values are UV coordinates.
     * But depending on the mesh type (triangle/quads or bmesh) the lists in the map have different meaning.
     * For bmesh they are enlisted just like they are stored in the blend file (in loops).
     * For traditional faces every 4 UV's should be assigned for a single face.
     * @param meshStructure
     *            the mesh structure
     * @return a map that sorts UV coordinates between different UV sets
     * @throws BlenderFileException
     *             an exception is thrown when problems with blend file occur
     */
    @SuppressWarnings("unchecked")
    public LinkedHashMap<String, List<Vector2f>> loadUVCoordinates(Structure meshStructure) throws BlenderFileException {
        LOGGER.log(Level.FINE, "Loading UV coordinates from mesh: {0}.", meshStructure.getName());
        LinkedHashMap<String, List<Vector2f>> result = new LinkedHashMap<String, List<Vector2f>>();
        if (this.isBMeshCompatible(meshStructure)) {
            // in this case the UV's are assigned to vertices (an array is the same length as the vertex array)
            Structure loopData = (Structure) meshStructure.getFieldValue("ldata");
            Pointer pLoopDataLayers = (Pointer) loopData.getFieldValue("layers");
            List<Structure> loopDataLayers = pLoopDataLayers.fetchData();
            for (Structure structure : loopDataLayers) {
                Pointer p = (Pointer) structure.getFieldValue("data");
                if (p.isNotNull() && ((Number) structure.getFieldValue("type")).intValue() == MeshHelper.UV_DATA_LAYER_TYPE_BMESH) {
                    String uvSetName = structure.getFieldValue("name").toString();
                    List<Structure> uvsStructures = p.fetchData();
                    List<Vector2f> uvs = new ArrayList<Vector2f>(uvsStructures.size());
                    for (Structure uvStructure : uvsStructures) {
                        DynamicArray<Number> loopUVS = (DynamicArray<Number>) uvStructure.getFieldValue("uv");
                        uvs.add(new Vector2f(loopUVS.get(0).floatValue(), loopUVS.get(1).floatValue()));
                    }
                    result.put(uvSetName, uvs);
                }
            }
        } else {
            // in this case UV's are assigned to faces (the array has the same legnth as the faces count)
            Structure facesData = (Structure) meshStructure.getFieldValue("fdata");
            Pointer pFacesDataLayers = (Pointer) facesData.getFieldValue("layers");
            if (pFacesDataLayers.isNotNull()) {
                List<Structure> facesDataLayers = pFacesDataLayers.fetchData();
                for (Structure structure : facesDataLayers) {
                    Pointer p = (Pointer) structure.getFieldValue("data");
                    if (p.isNotNull() && ((Number) structure.getFieldValue("type")).intValue() == MeshHelper.UV_DATA_LAYER_TYPE_FMESH) {
                        String uvSetName = structure.getFieldValue("name").toString();
                        List<Structure> uvsStructures = p.fetchData();
                        List<Vector2f> uvs = new ArrayList<Vector2f>(uvsStructures.size());
                        for (Structure uvStructure : uvsStructures) {
                            DynamicArray<Number> mFaceUVs = (DynamicArray<Number>) uvStructure.getFieldValue("uv");
                            uvs.add(new Vector2f(mFaceUVs.get(0).floatValue(), mFaceUVs.get(1).floatValue()));
                            uvs.add(new Vector2f(mFaceUVs.get(2).floatValue(), mFaceUVs.get(3).floatValue()));
                            uvs.add(new Vector2f(mFaceUVs.get(4).floatValue(), mFaceUVs.get(5).floatValue()));
                            uvs.add(new Vector2f(mFaceUVs.get(6).floatValue(), mFaceUVs.get(7).floatValue()));
                        }
                        result.put(uvSetName, uvs);
                    }
                }
            }
        }
        return result;
    }

    /**
     * Loads all vertices groups.
     * @param meshStructure
     *            the mesh structure
     * @return a list of vertex groups for every vertex in the mesh
     * @throws BlenderFileException
     *             an exception is thrown when problems with blend file occur
     */
    public List<Map<String, Float>> loadVerticesGroups(Structure meshStructure) throws BlenderFileException {
        LOGGER.log(Level.FINE, "Loading vertices groups from mesh: {0}.", meshStructure.getName());
        List<Map<String, Float>> result = new ArrayList<Map<String, Float>>();

        Structure parent = blenderContext.peekParent();
        if(parent != null) {
        	// the mesh might be saved without its parent (it is then unused)
        	Structure defbase = (Structure) parent.getFieldValue("defbase");
            List<String> groupNames = new ArrayList<String>();
            List<Structure> defs = defbase.evaluateListBase();
            for (Structure def : defs) {
                groupNames.add(def.getFieldValue("name").toString());
            }

            Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
            if (pDvert.isNotNull()) {// assigning weights and bone indices
                List<Structure> dverts = pDvert.fetchData();
                for (Structure dvert : dverts) {
                    Map<String, Float> weightsForVertex = new HashMap<String, Float>();
                    Pointer pDW = (Pointer) dvert.getFieldValue("dw");
                    if (pDW.isNotNull()) {
                        List<Structure> dw = pDW.fetchData();
                        for (Structure deformWeight : dw) {
                            int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue();
                            float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
                            String groupName = groupNames.get(groupIndex);

                            weightsForVertex.put(groupName, weight);
                        }
                    }
                    result.add(weightsForVertex);
                }
            }
        }
        return result;
    }

    /**
     * Selects the proper subsets of UV coordinates for the given sublist of indexes.
     * @param face
     *            the face with the original UV sets
     * @param indexesSublist
     *            the sub list of indexes
     * @return a map of UV coordinates subsets
     */
    public Map<String, List<Vector2f>> selectUVSubset(Face face, Integer... indexesSublist) {
        Map<String, List<Vector2f>> result = null;
        if (face.getUvSets() != null) {
            result = new HashMap<String, List<Vector2f>>();
            for (Entry<String, List<Vector2f>> entry : face.getUvSets().entrySet()) {
                List<Vector2f> uvs = new ArrayList<Vector2f>(indexesSublist.length);
                for (Integer index : indexesSublist) {
                    uvs.add(entry.getValue().get(face.getIndexes().indexOf(index)));
                }
                result.put(entry.getKey(), uvs);
            }
        }
        return result;
    }

    /**
     * Selects the proper subsets of vertex colors for the given sublist of indexes.
     * @param face
     *            the face with the original vertex colors
     * @param indexesSublist
     *            the sub list of indexes
     * @return a sublist of vertex colors
     */
    public List<byte[]> selectVertexColorSubset(Face face, Integer... indexesSublist) {
        List<byte[]> result = null;
        List<byte[]> vertexColors = face.getVertexColors();
        if (vertexColors != null) {
            result = new ArrayList<byte[]>(indexesSublist.length);
            for (Integer index : indexesSublist) {
                result.add(vertexColors.get(face.getIndexes().indexOf(index)));
            }
        }
        return result;
    }

    /**
     * Returns the black unshaded material. It is used for lines and points because that is how blender
     * renders it.
     * @param blenderContext
     *            the blender context
     * @return black unshaded material
     */
    public synchronized Material getBlackUnshadedMaterial(BlenderContext blenderContext) {
        if (blackUnshadedMaterial == null) {
            blackUnshadedMaterial = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
            blackUnshadedMaterial.setColor("Color", ColorRGBA.Black);
        }
        return blackUnshadedMaterial;
    }
}
