/*
 * www.javagl.de - JglTF
 *
 * Copyright 2015-2016 Marco Hutter - http://www.javagl.de
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package de.javagl.jgltf.obj.v2;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import de.javagl.jgltf.impl.v2.Accessor;
import de.javagl.jgltf.impl.v2.Asset;
import de.javagl.jgltf.impl.v2.Buffer;
import de.javagl.jgltf.impl.v2.BufferView;
import de.javagl.jgltf.impl.v2.GlTF;
import de.javagl.jgltf.impl.v2.Material;
import de.javagl.jgltf.impl.v2.Mesh;
import de.javagl.jgltf.impl.v2.MeshPrimitive;
import de.javagl.jgltf.impl.v2.Node;
import de.javagl.jgltf.impl.v2.Scene;
import de.javagl.jgltf.model.Accessors;
import de.javagl.jgltf.model.GltfConstants;
import de.javagl.jgltf.model.Optionals;
import de.javagl.jgltf.model.impl.creation.BufferStructure;
import de.javagl.jgltf.model.impl.creation.BufferStructureBuilder;
import de.javagl.jgltf.model.impl.creation.BufferStructureGltfV2;
import de.javagl.jgltf.model.impl.creation.BufferStructures;
import de.javagl.jgltf.model.io.Buffers;
import de.javagl.jgltf.model.io.GltfReference;
import de.javagl.jgltf.model.io.GltfReferenceResolver;
import de.javagl.jgltf.model.io.IO;
import de.javagl.jgltf.model.io.UriResolvers;
import de.javagl.jgltf.model.io.v2.GltfAssetV2;
import de.javagl.jgltf.obj.BufferStrategy;
import de.javagl.jgltf.obj.IntBuffers;
import de.javagl.jgltf.obj.ObjNormals;
import de.javagl.obj.Mtl;
import de.javagl.obj.MtlReader;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjData;
import de.javagl.obj.ObjGroup;
import de.javagl.obj.ObjReader;
import de.javagl.obj.ObjSplitting;
import de.javagl.obj.ObjUtils;
import de.javagl.obj.ReadableObj;

/**
 * A class for creating {@link GltfAssetV2} objects from OBJ files 
 */
public class ObjGltfAssetCreatorV2
{
    // TODO Obviously, there's a lot of code duplication between
    // here and ObjGltfAssetCreatorV1. Try to refactor this.
    
    /**
     * The logger used in this class
     */
    private static final Logger logger = 
        Logger.getLogger(ObjGltfAssetCreatorV2.class.getName());
    
    /**
     * The log level
     */
    private static final Level level = Level.FINE;
    
    /**
     * The {@link GlTF} that is currently being built
     */
    private GlTF gltf;

    /**
     * The {@link BufferStrategy} that should be used
     */
    private BufferStrategy bufferStrategy;
    
    /**
     * The {@link BufferStructureBuilder}
     */
    private BufferStructureBuilder bufferStructureBuilder;
    
    /**
     * The {@link MtlMaterialHandlerV2}
     */
    private MtlMaterialHandlerV2 mtlMaterialHandler;
    
    /**
     * The component type for the indices of the resulting glTF. 
     * For glTF 1.0, this may at most be GL_UNSIGNED_SHORT.  
     */
    private int indicesComponentType = GltfConstants.GL_UNSIGNED_SHORT;
    
    /**
     * A function that will receive all OBJ groups that should be processed,
     * and may (or may not) split them into multiple parts. 
     */
    private Function<? super ReadableObj, List<? extends ReadableObj>> 
        objSplitter;
    
    /**
     * For testing: Assign a material with a random color to each 
     * mesh primitive that was created during the splitting process
     */
    private boolean assigningRandomColorsToParts = true;
    
    /**
     * Default constructor
     */
    public ObjGltfAssetCreatorV2()
    {
        this(BufferStrategy.BUFFER_PER_FILE);
    }

    /**
     * Creates a new glTF model creator
     * 
     * @param bufferStrategy The {@link BufferStrategy} to use
     */
    public ObjGltfAssetCreatorV2(BufferStrategy bufferStrategy)
    {
        this.bufferStrategy = bufferStrategy;
        setIndicesComponentType(GltfConstants.GL_UNSIGNED_SHORT);
    }
    
    /**
     * Set the {@link BufferStrategy} to use
     * 
     * @param bufferStrategy The {@link BufferStrategy}
     */
    public void setBufferStrategy(BufferStrategy bufferStrategy)
    {
        this.bufferStrategy = bufferStrategy;
    }
    
    /**
     * Set the component type for the indices of the {@link MeshPrimitive}s
     * 
     * @param indicesComponentType The component type
     * @throws IllegalArgumentException If the given type is not
     * GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT or GL_UNSIGNED_INT
     */
    public void setIndicesComponentType(int indicesComponentType)
    {
        List<Integer> validTypes = Arrays.asList(
            GltfConstants.GL_UNSIGNED_BYTE,
            GltfConstants.GL_UNSIGNED_SHORT,
            GltfConstants.GL_UNSIGNED_INT);
        if (!validTypes.contains(indicesComponentType))
        {
            throw new IllegalArgumentException(
                "The indices component type must be GL_UNSIGNED_BYTE," + 
                "GL_UNSIGNED_SHORT or GL_UNSIGNED_INT, but is " +
                GltfConstants.stringFor(indicesComponentType));
        }
        this.indicesComponentType = indicesComponentType;

        if (indicesComponentType == GltfConstants.GL_UNSIGNED_INT)
        {
            this.objSplitter = obj -> Collections.singletonList(obj);
        }
        else if (indicesComponentType == GltfConstants.GL_UNSIGNED_SHORT)
        {
            int maxNumVertices = 65536 - 3;
            this.objSplitter = 
                obj -> ObjSplitting.splitByMaxNumVertices(obj, maxNumVertices);
        }
        else if (indicesComponentType == GltfConstants.GL_UNSIGNED_BYTE)
        {
            int maxNumVertices = 256 - 3;
            this.objSplitter = 
                obj -> ObjSplitting.splitByMaxNumVertices(obj, maxNumVertices);
        }
    }
    
    /**
     * For testing and debugging: Assign random colors to the parts that
     * are created when splitting the OBJ data
     * 
     * @param assigningRandomColorsToParts The flag
     */
    public void setAssigningRandomColorsToParts(
        boolean assigningRandomColorsToParts)
    {
        this.assigningRandomColorsToParts = assigningRandomColorsToParts;
    }
    
    /**
     * Create a {@link GltfAssetV2} from the OBJ file with the given URI
     * 
     * @param objUri The OBJ URI
     * @return The {@link GltfAssetV2}
     * @throws IOException If an IO error occurs
     */
    public GltfAssetV2 create(URI objUri) throws IOException
    {
        logger.log(level, "Creating glTF with " + bufferStrategy + 
            " buffer strategy");
        
        // Obtain the relative path information and file names
        logger.log(level, "Resolving paths from " + objUri);
        URI baseUri = IO.getParent(objUri);
        String objFileName = IO.extractFileName(objUri);
        String baseName = stripFileNameExtension(objFileName);
        String mtlFileName = baseName + ".mtl";
        URI mtlUri = IO.makeAbsolute(baseUri, mtlFileName);
        
        // Read the input data
        Obj obj = readObj(objUri);
        Map<String, Mtl> mtls = Collections.emptyMap();
        if (IO.existsUnchecked(mtlUri))
        {
            mtls = readMtls(mtlUri);
        }
        return convert(obj, mtls, baseName, baseUri);
    }
    
    /**
     * Read the OBJ from the given URI, and return it as a "renderable" OBJ,
     * which contains only triangles, has unique vertex coordinates and
     * normals, and is single-indexed 
     * 
     * @param objUri The OBJ URI
     * @return The OBJ
     * @throws IOException If an IO error occurs
     */
    private static Obj readObj(URI objUri) throws IOException
    {
        logger.log(level, "Reading OBJ from " + objUri);
        
        try (InputStream objInputStream =  objUri.toURL().openStream())
        {
            Obj obj = ObjReader.read(objInputStream);
            return ObjUtils.convertToRenderable(obj);
        }
    }

    /**
     * Read a mapping from material names to MTL objects from the given URI
     * 
     * @param mtlUri The MTL URI
     * @return The mapping
     * @throws IOException If an IO error occurs
     */
    private static Map<String, Mtl> readMtls(URI mtlUri) throws IOException
    {
        logger.log(level, "Reading MTL from " + mtlUri);

        try (InputStream mtlInputStream = mtlUri.toURL().openStream())
        {
            List<Mtl> mtlList = MtlReader.read(mtlInputStream);
            Map<String, Mtl> mtls = mtlList.stream().collect(
                LinkedHashMap::new, 
                (map, mtl) -> map.put(mtl.getName(), mtl), 
                (map0, map1) -> map0.putAll(map1));
            return mtls;
        }
    }
    
    
    /**
     * Convert the given OBJ into a {@link GltfAssetV2}.
     *   
     * @param obj The OBJ
     * @param mtlsMap The mapping from material names to MTL instances
     * @param baseName The base name for the glTF
     * @param baseUri The base URI to resolve shaders and textures against
     * @return The {@link GltfAssetV2}
     */
    public GltfAssetV2 convert(
        ReadableObj obj, Map<String, Mtl> mtlsMap, String baseName, URI baseUri)
    {
        // Basic setup 
        gltf = new GlTF();
        gltf.setAsset(createAsset());
        mtlMaterialHandler = new MtlMaterialHandlerV2(gltf);
        bufferStructureBuilder = new BufferStructureBuilder();
        
        // Create the MeshPrimitives from the OBJ and MTL data
        Map<String, Mtl> mtls = 
            mtlsMap == null ? Collections.emptyMap() : mtlsMap;
        List<MeshPrimitive> meshPrimitives = 
            createMeshPrimitives(obj, mtls);
        
        if (bufferStrategy == BufferStrategy.BUFFER_PER_FILE)
        {
            int bufferCounter = bufferStructureBuilder.getNumBufferModels();
            String bufferName = "buffer" + bufferCounter;
            String uri = bufferName + ".bin";
            bufferStructureBuilder.createBufferModel(bufferName, uri);
        }
        
        // Transfer the data from the buffer structure into the glTF
        BufferStructure bufferStructure = bufferStructureBuilder.build();
        
        List<Accessor> accessors = 
            BufferStructureGltfV2.createAccessors(bufferStructure);
        gltf.setAccessors(accessors);

        List<BufferView> bufferViews = 
            BufferStructureGltfV2.createBufferViews(bufferStructure);
        gltf.setBufferViews(bufferViews);

        List<Buffer> buffers = 
            BufferStructureGltfV2.createBuffers(bufferStructure);
        gltf.setBuffers(buffers);
        
        
        // Create the basic scene graph structure, consisting of a single Mesh 
        // (containing the MeshPrimitives) in a single Node in a single Scene
        Mesh mesh = new Mesh();
        mesh.setPrimitives(meshPrimitives);
        int meshIndex = Optionals.of(gltf.getMeshes()).size();
        gltf.addMeshes(mesh);

        Node node = new Node();
        node.setMesh(meshIndex);
        int nodeIndex = Optionals.of(gltf.getNodes()).size();
        gltf.addNodes(node);
        
        Scene scene = new Scene();
        scene.setNodes(Collections.singletonList(nodeIndex));
        int sceneIndex = Optionals.of(gltf.getScenes()).size();
        gltf.addScenes(scene);
        
        gltf.setScene(sceneIndex);

        // Create the GltfAsset from the glTF
        GltfAssetV2 gltfAsset = new GltfAssetV2(gltf, null);
        
        // For each Buffer reference in the GltfAsset, obtain the data from the 
        // BufferModel instance that has been created by the 
        // BufferStructureBuilder.
        List<GltfReference> bufferReferences = gltfAsset.getBufferReferences();
        BufferStructures.resolve(bufferReferences, bufferStructure);

        if (baseUri != null)
        {
            // Resolve the image data, by reading the data from the URIs in
            // the Images, resolved against the root path of the input OBJ
            logger.log(level, "Resolving Image data");
            Function<String, ByteBuffer> externalUriResolver = 
                UriResolvers.createBaseUriResolver(baseUri);
            List<GltfReference> imageReferences = 
                gltfAsset.getImageReferences();
            GltfReferenceResolver.resolveAll(
                imageReferences, externalUriResolver);
        }
        
        return gltfAsset;
    }
    
    
    /**
     * Create the {@link Asset} for the generated {@link GlTF} 
     * 
     * @return The {@link Asset}
     */
    private static Asset createAsset()
    {
        Asset asset = new Asset();
        asset.setGenerator("jgltf-obj from https://github.com/javagl/JglTF");
        asset.setVersion("2.0");
        return asset;
    }

    /**
     * Create the {@link MeshPrimitive}s for the given OBJ- and MTL data
     * 
     * @param obj The OBJ
     * @param mtls The MTLs
     * @return The {@link MeshPrimitive}s
     */
    private List<MeshPrimitive> createMeshPrimitives(
        ReadableObj obj, Map<String, Mtl> mtls)
    {
        // When there are no materials, create the MeshPrimitives for the OBJ
        int numMaterialGroups = obj.getNumMaterialGroups();
        if (numMaterialGroups == 0 || mtls.isEmpty())
        {
            return createMeshPrimitives(obj);
        }
        
        // Create the MeshPrimitives for the material groups
        List<MeshPrimitive> meshPrimitives = new ArrayList<MeshPrimitive>();
        for (int i = 0; i < numMaterialGroups; i++)
        {
            ObjGroup materialGroup = obj.getMaterialGroup(i);
            String materialGroupName = materialGroup.getName();
            Obj materialObj = ObjUtils.groupToObj(obj, materialGroup, null);
            Mtl mtl = mtls.get(materialGroupName);
            
            logger.log(level, "Creating MeshPrimitive for material " + 
                materialGroupName);

            // If the material group is too large, it may have to
            // be split into multiple parts
            String name = "materialGroup_" + String.valueOf(i);
            List<MeshPrimitive> subMeshPrimitives = 
                createPartMeshPrimitives(materialObj, name);

            assignMaterial(subMeshPrimitives, obj, mtl);
            meshPrimitives.addAll(subMeshPrimitives);
            
            if (bufferStrategy == BufferStrategy.BUFFER_PER_GROUP)
            {
                int bufferCounter = bufferStructureBuilder.getNumBufferModels();
                String bufferName = "buffer" + bufferCounter;
                String uri = bufferName + ".bin";
                bufferStructureBuilder.createBufferModel(bufferName, uri);
            }
            
        }
        return meshPrimitives;
    }

    
    
    /**
     * Create simple {@link MeshPrimitive}s for the given OBJ
     * 
     * @param obj The OBJ
     * @return The {@link MeshPrimitive}s
     */
    private List<MeshPrimitive> createMeshPrimitives(ReadableObj obj)
    {
        logger.log(level, "Creating MeshPrimitives for OBJ");
        
        List<MeshPrimitive> meshPrimitives = 
            createPartMeshPrimitives(obj, "obj");
        
        boolean withNormals = obj.getNumNormals() > 0;
        if (assigningRandomColorsToParts)
        {
            assignRandomColorMaterials(meshPrimitives, withNormals);
        }
        else
        {
            assignDefaultMaterial(meshPrimitives, withNormals);
        }
        if (bufferStrategy == BufferStrategy.BUFFER_PER_GROUP)
        {
            int bufferCounter = bufferStructureBuilder.getNumBufferModels();
            String bufferName = "buffer" + bufferCounter;
            String uri = bufferName + ".bin";
            bufferStructureBuilder.createBufferModel(bufferName, uri);
        }
        return meshPrimitives;
    }

    
    /**
     * Create a {@link Material} for the given OBJ and MTL, and assign it
     * to all the given {@link MeshPrimitive} instances
     * 
     * @param meshPrimitives The {@link MeshPrimitive} instances
     * @param obj The OBJ
     * @param mtl The MTL
     */
    private void assignMaterial(
        Iterable<? extends MeshPrimitive> meshPrimitives, 
        ReadableObj obj, Mtl mtl)
    {
        Material material = mtlMaterialHandler.createMaterial(obj, mtl);
        int materialIndex = Optionals.of(gltf.getMaterials()).size();
        gltf.addMaterials(material);
        for (MeshPrimitive meshPrimitive : meshPrimitives)
        {
            meshPrimitive.setMaterial(materialIndex);
        }
    }
    
    /**
     * Create a default {@link Material}, and assign it to all the given 
     * {@link MeshPrimitive} instances
     * 
     * @param meshPrimitives The {@link MeshPrimitive} instances
     * @param withNormals Whether the {@link MeshPrimitive} instances have
     * normal information
     */
    private void assignDefaultMaterial(
        Iterable<? extends MeshPrimitive> meshPrimitives, boolean withNormals)
    {
        Material material = mtlMaterialHandler.createMaterialWithColor(
            withNormals, 0.75f, 0.75f, 0.75f);
        int materialIndex = Optionals.of(gltf.getMaterials()).size();
        gltf.addMaterials(material);
        for (MeshPrimitive meshPrimitive : meshPrimitives)
        {
            meshPrimitive.setMaterial(materialIndex);
        }
    }

    /**
     * Create {@link Material} instances with random colors, and assign 
     * them to the given {@link MeshPrimitive} instances
     * 
     * @param meshPrimitives The {@link MeshPrimitive} instances
     * @param withNormals Whether the {@link MeshPrimitive} instances have
     * normal information
     */
    private void assignRandomColorMaterials(
        Iterable<? extends MeshPrimitive> meshPrimitives, boolean withNormals)
    {
        Random random = new Random(0);
        for (MeshPrimitive meshPrimitive : meshPrimitives)
        {
            float r = random.nextFloat(); 
            float g = random.nextFloat(); 
            float b = random.nextFloat(); 
            Material material = 
                mtlMaterialHandler.createMaterialWithColor(
                    withNormals, r, g, b);
            int materialIndex = Optionals.of(gltf.getMaterials()).size();
            gltf.addMaterials(material);
            meshPrimitive.setMaterial(materialIndex);
        }
    }
    
    


    /**
     * Create the {@link MeshPrimitive}s from the given OBJ data.
     * 
     * @param obj The OBJ
     * @param name A name that will be used as a basis to create the IDs of 
     * the {@link BufferView} and {@link Accessor} instances
     * @return The {@link MeshPrimitive} list
     */
    private List<MeshPrimitive> createPartMeshPrimitives(
        ReadableObj obj, String name)
    {
        List<MeshPrimitive> meshPrimitives = new ArrayList<MeshPrimitive>();
        List<? extends ReadableObj> parts = objSplitter.apply(obj);
        for (int i = 0; i < parts.size(); i++)
        {
            ReadableObj part = parts.get(i);
            String partName = name;
            if (parts.size() > 1)
            {
                partName += "_part_" + i;
            }
            MeshPrimitive meshPrimitive =
                createMeshPrimitive(part, partName);
            meshPrimitives.add(meshPrimitive);
            if (bufferStrategy == BufferStrategy.BUFFER_PER_PART)
            {
                int bufferCounter = bufferStructureBuilder.getNumBufferModels();
                String bufferId = "buffer" + bufferCounter;
                String uri = bufferId + ".bin";
                bufferStructureBuilder.createBufferModel(bufferId, uri);
            }
        }
        return meshPrimitives;
    }
    
    /**
     * Create the {@link MeshPrimitive} from the given OBJ data.
     * 
     * @param part The OBJ
     * @param partName A name that will be used as a basis to create the IDs of 
     * the {@link BufferView} and {@link Accessor} instances
     * @return The {@link MeshPrimitive}
     */
    private MeshPrimitive createMeshPrimitive(ReadableObj part, String partName)
    {
        MeshPrimitive meshPrimitive = 
            new MeshPrimitive();
        meshPrimitive.setMode(GltfConstants.GL_TRIANGLES);

        // Add the indices data from the OBJ to the buffer structure
        int indicesAccessorIndex = 
            bufferStructureBuilder.getNumAccessorModels();
        bufferStructureBuilder.createAccessorModel(
            "indicesAccessor_" + indicesAccessorIndex,
            indicesComponentType, "SCALAR", 
            createIndicesByteBuffer(part, indicesComponentType));
        meshPrimitive.setIndices(indicesAccessorIndex);
        
        bufferStructureBuilder.createArrayElementBufferViewModel(
            partName + "_indices_bufferView");
        
        // Add the vertices (positions) from the OBJ to the buffer structure
        int positionsAccessorIndex = 
            bufferStructureBuilder.getNumAccessorModels();
        FloatBuffer objVertices = ObjData.getVertices(part);
        bufferStructureBuilder.createAccessorModel(
            "positionsAccessor_" + positionsAccessorIndex,
            GltfConstants.GL_FLOAT, "VEC3", 
            Buffers.createByteBufferFrom(objVertices));
        meshPrimitive.addAttributes("POSITION", positionsAccessorIndex);

        // Add the texture coordinates from the OBJ to the buffer structure
        boolean flipY = true;
        FloatBuffer objTexCoords = ObjData.getTexCoords(part, 2, flipY);
        if (objTexCoords.capacity() > 0)
        {
            int texCoordsAccessorIndex = 
                bufferStructureBuilder.getNumAccessorModels();
            bufferStructureBuilder.createAccessorModel(
                "texCoordsAccessor_" + texCoordsAccessorIndex, 
                GltfConstants.GL_FLOAT, "VEC2", 
                Buffers.createByteBufferFrom(objTexCoords));
            meshPrimitive.addAttributes("TEXCOORD_0", texCoordsAccessorIndex);
        }
        
        // Add the normals from the OBJ to the buffer structure
        FloatBuffer objNormals = ObjData.getNormals(part);
        if (objNormals.capacity() > 0)
        {
            ObjNormals.normalize(objNormals);
            int normalsAccessorIndex = 
                bufferStructureBuilder.getNumAccessorModels();
            bufferStructureBuilder.createAccessorModel(
                "normalsAccessor_" + normalsAccessorIndex, 
                GltfConstants.GL_FLOAT, "VEC3", 
                Buffers.createByteBufferFrom(objNormals));
            meshPrimitive.addAttributes("NORMAL", normalsAccessorIndex);
        }

        bufferStructureBuilder.createArrayBufferViewModel(
            partName + "_attributes_bufferView");
        
        return meshPrimitive;
    }
    
    /**
     * Create a byte buffer containing the data of the indices for the
     * given OBJ. The face vertex indices of the given OBJ will be 
     * extracted (assuming that it contains only triangles), converted
     * to the given indices component type (which is a GL constant like
     * GL_SHORT), and a byte buffer containing these indices will be returned.
     * 
     * @param obj The OBJ
     * @param indicesComponentType The indices component type
     * @return The byte buffer
     */
    private static ByteBuffer createIndicesByteBuffer(
        ReadableObj obj, int indicesComponentType)
    {
        int numVerticesPerFace = 3;
        IntBuffer objIndices = 
            ObjData.getFaceVertexIndices(obj, numVerticesPerFace);
        int indicesComponentSize =
            Accessors.getNumBytesForAccessorComponentType(
                indicesComponentType);
        ByteBuffer indicesByteBuffer = 
            IntBuffers.convertToByteBuffer(objIndices, indicesComponentSize);
        return indicesByteBuffer;
    }
    
    
    /**
     * Remove the extension from the given file name. That is, the part 
     * starting with the last <code>'.'</code> dot. If the given file name
     * does not contain a dot, it will be returned unmodified
     * 
     * @param fileName The file name
     * @return The file name without the extension
     */
    private static String stripFileNameExtension(String fileName)
    {
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex < 0)
        {
            return fileName;
        }
        return fileName.substring(0, lastDotIndex);
    }
}