/*
 * Copyright (c) 2009-2010 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 jme3tools.converters.model;

import com.jme3.bounding.BoundingBox;
import com.jme3.math.Transform;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.util.BufferUtils;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;

public class FloatToFixed {

    private static final float shortSize = Short.MAX_VALUE - Short.MIN_VALUE;
    private static final float shortOff  = (Short.MAX_VALUE + Short.MIN_VALUE) * 0.5f;

    private static final float byteSize = Byte.MAX_VALUE - Byte.MIN_VALUE;
    private static final float byteOff  = (Byte.MAX_VALUE + Byte.MIN_VALUE) * 0.5f;

    public static void convertToFixed(Geometry geom, Format posFmt, Format nmFmt, Format tcFmt){
        geom.updateModelBound();
        BoundingBox bbox = (BoundingBox) geom.getModelBound();
        Mesh mesh = geom.getMesh();

        VertexBuffer positions = mesh.getBuffer(Type.Position);
        VertexBuffer normals   = mesh.getBuffer(Type.Normal);
        VertexBuffer texcoords = mesh.getBuffer(Type.TexCoord);
        VertexBuffer indices   = mesh.getBuffer(Type.Index);

        // positions
        FloatBuffer fb = (FloatBuffer) positions.getData();
        if (posFmt != Format.Float){
            Buffer newBuf = VertexBuffer.createBuffer(posFmt, positions.getNumComponents(),
                                                      mesh.getVertexCount());
            Transform t = convertPositions(fb, bbox, newBuf);
            t.combineWithParent(geom.getLocalTransform());
            geom.setLocalTransform(t);

            VertexBuffer newPosVb = new VertexBuffer(Type.Position);
            newPosVb.setupData(positions.getUsage(),
                               positions.getNumComponents(),
                               posFmt,
                               newBuf);
            mesh.clearBuffer(Type.Position);
            mesh.setBuffer(newPosVb);
        }

        // normals, automatically convert to signed byte
        fb = (FloatBuffer) normals.getData();

        ByteBuffer bb = BufferUtils.createByteBuffer(fb.capacity());
        convertNormals(fb, bb);

        normals = new VertexBuffer(Type.Normal);
        normals.setupData(Usage.Static, 3, Format.Byte, bb);
        normals.setNormalized(true);
        mesh.clearBuffer(Type.Normal);
        mesh.setBuffer(normals);

        // texcoords
        fb = (FloatBuffer) texcoords.getData();
        if (tcFmt != Format.Float){
            Buffer newBuf = VertexBuffer.createBuffer(tcFmt,
                                                      texcoords.getNumComponents(),
                                                      mesh.getVertexCount());
            convertTexCoords2D(fb, newBuf);

            VertexBuffer newTcVb = new VertexBuffer(Type.TexCoord);
            newTcVb.setupData(texcoords.getUsage(),
                              texcoords.getNumComponents(),
                              tcFmt,
                              newBuf);
            mesh.clearBuffer(Type.TexCoord);
            mesh.setBuffer(newTcVb);
        }
    }

    public static void compressIndexBuffer(Mesh mesh){
        int vertCount = mesh.getVertexCount();
        VertexBuffer vb = mesh.getBuffer(Type.Index);
        Format targetFmt;
        if (vb.getFormat() == Format.UnsignedInt && vertCount <= 0xffff){
            if (vertCount <= 256)
                targetFmt = Format.UnsignedByte;
            else
                targetFmt = Format.UnsignedShort;
        }else if (vb.getFormat() == Format.UnsignedShort && vertCount <= 0xff){
            targetFmt = Format.UnsignedByte;
        }else{
            return;
        }

        IndexBuffer src = mesh.getIndexBuffer();
        Buffer newBuf = VertexBuffer.createBuffer(targetFmt, vb.getNumComponents(), src.size());

        VertexBuffer newVb = new VertexBuffer(Type.Index);
        newVb.setupData(vb.getUsage(), vb.getNumComponents(), targetFmt, newBuf);
        mesh.clearBuffer(Type.Index);
        mesh.setBuffer(newVb);

        IndexBuffer dst = mesh.getIndexBuffer();
        for (int i = 0; i < src.size(); i++){
            dst.put(i, src.get(i));
        }
    }

    private static void convertToFixed(FloatBuffer input, IntBuffer output){
        if (output.capacity() < input.capacity())
            throw new RuntimeException("Output must be at least as large as input!");

        input.clear();
        output.clear();
        for (int i = 0; i < input.capacity(); i++){
            output.put( (int) (input.get() * (float)(1<<16)) );
        }
        output.flip();
    }

    private static void convertToFloat(IntBuffer input, FloatBuffer output){
        if (output.capacity() < input.capacity())
            throw new RuntimeException("Output must be at least as large as input!");

        input.clear();
        output.clear();
        for (int i = 0; i < input.capacity(); i++){
            output.put( ((float)input.get() / (float)(1<<16)) );
        }
        output.flip();
    }

    private static void convertToUByte(FloatBuffer input, ByteBuffer output){
        if (output.capacity() < input.capacity())
            throw new RuntimeException("Output must be at least as large as input!");

        input.clear();
        output.clear();
        for (int i = 0; i < input.capacity(); i++){
            output.put( (byte) (input.get() * 255f) );
        }
        output.flip();
    }


    public static VertexBuffer convertToUByte(VertexBuffer vb){
        FloatBuffer fb = (FloatBuffer) vb.getData();
        ByteBuffer bb = BufferUtils.createByteBuffer(fb.capacity());
        convertToUByte(fb, bb);

        VertexBuffer newVb = new VertexBuffer(vb.getBufferType());
        newVb.setupData(vb.getUsage(),
                        vb.getNumComponents(),
                        Format.UnsignedByte,
                        bb);
        newVb.setNormalized(true);
        return newVb;
    }

    public static VertexBuffer convertToFixed(VertexBuffer vb){
        if (vb.getFormat() == Format.Int)
            return vb;

        FloatBuffer fb = (FloatBuffer) vb.getData();
        IntBuffer ib = BufferUtils.createIntBuffer(fb.capacity());
        convertToFixed(fb, ib);

        VertexBuffer newVb = new VertexBuffer(vb.getBufferType());
        newVb.setupData(vb.getUsage(),
                        vb.getNumComponents(),
                        Format.Int,
                        ib);
        return newVb;
    }

    public static VertexBuffer convertToFloat(VertexBuffer vb){
        if (vb.getFormat() == Format.Float)
            return vb;

        IntBuffer ib = (IntBuffer) vb.getData();
        FloatBuffer fb = BufferUtils.createFloatBuffer(ib.capacity());
        convertToFloat(ib, fb);

        VertexBuffer newVb = new VertexBuffer(vb.getBufferType());
        newVb.setupData(vb.getUsage(),
                        vb.getNumComponents(),
                        Format.Float,
                        fb);
        return newVb;
    }

    private static void convertNormals(FloatBuffer input, ByteBuffer output){
        if (output.capacity() < input.capacity())
            throw new RuntimeException("Output must be at least as large as input!");

        input.clear();
        output.clear();
        Vector3f temp = new Vector3f();
        int vertexCount = input.capacity() / 3;
        for (int i = 0; i < vertexCount; i++){
            BufferUtils.populateFromBuffer(temp, input, i);

            // offset and scale vector into -128 ... 127
            temp.multLocal(127).addLocal(0.5f, 0.5f, 0.5f);

            // quantize
            byte v1 = (byte) temp.getX();
            byte v2 = (byte) temp.getY();
            byte v3 = (byte) temp.getZ();

            // store
            output.put(v1).put(v2).put(v3);
        }
    }

    private static void convertTexCoords2D(FloatBuffer input, Buffer output){
        if (output.capacity() < input.capacity())
            throw new RuntimeException("Output must be at least as large as input!");

        input.clear();
        output.clear();
        Vector2f temp = new Vector2f();
        int vertexCount = input.capacity() / 2;

        ShortBuffer sb = null;
        IntBuffer ib = null;

        if (output instanceof ShortBuffer)
            sb = (ShortBuffer) output;
        else if (output instanceof IntBuffer)
            ib = (IntBuffer) output;
        else
            throw new UnsupportedOperationException();

        for (int i = 0; i < vertexCount; i++){
            BufferUtils.populateFromBuffer(temp, input, i);

            if (sb != null){
                sb.put( (short) (temp.getX()*Short.MAX_VALUE) );
                sb.put( (short) (temp.getY()*Short.MAX_VALUE) );
            }else{
                int v1 = (int) (temp.getX() * ((float)(1 << 16)));
                int v2 = (int) (temp.getY() * ((float)(1 << 16)));
                ib.put(v1).put(v2);
            }
        }
    }

    private static Transform convertPositions(FloatBuffer input, BoundingBox bbox, Buffer output){
        if (output.capacity() < input.capacity())
            throw new RuntimeException("Output must be at least as large as input!");

        Vector3f offset = bbox.getCenter().negate();
        Vector3f size = new Vector3f(bbox.getXExtent(), bbox.getYExtent(), bbox.getZExtent());
        size.multLocal(2);

        ShortBuffer sb = null;
        ByteBuffer bb = null;
        float dataTypeSize;
        float dataTypeOffset;
        if (output instanceof ShortBuffer){
            sb = (ShortBuffer) output;
            dataTypeOffset = shortOff;
            dataTypeSize = shortSize;
        }else{
            bb = (ByteBuffer) output;
            dataTypeOffset = byteOff;
            dataTypeSize = byteSize;
        }
        Vector3f scale = new Vector3f();
        scale.set(dataTypeSize, dataTypeSize, dataTypeSize).divideLocal(size);

        Vector3f invScale = new Vector3f();
        invScale.set(size).divideLocal(dataTypeSize);

        offset.multLocal(scale);
        offset.addLocal(dataTypeOffset, dataTypeOffset, dataTypeOffset);

        // offset = (-modelOffset * shortSize)/modelSize + shortOff
        // scale = shortSize / modelSize

        input.clear();
        output.clear();
        Vector3f temp = new Vector3f();
        int vertexCount = input.capacity() / 3;
        for (int i = 0; i < vertexCount; i++){
            BufferUtils.populateFromBuffer(temp, input, i);

            // offset and scale vector into -32768 ... 32767
            // or into -128 ... 127 if using bytes
            temp.multLocal(scale);
            temp.addLocal(offset);

            // quantize and store
            if (sb != null){
                short v1 = (short) temp.getX();
                short v2 = (short) temp.getY();
                short v3 = (short) temp.getZ();
                sb.put(v1).put(v2).put(v3);
            }else{
                byte v1 = (byte) temp.getX();
                byte v2 = (byte) temp.getY();
                byte v3 = (byte) temp.getZ();
                bb.put(v1).put(v2).put(v3);
            }
        }

        Transform transform = new Transform();
        transform.setTranslation(offset.negate().multLocal(invScale));
        transform.setScale(invScale);
        return transform;
    }

}
