/*
 * Copyright (c) 2009-2021 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.terrain.geomipmap;

import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.math.FastMath;
import com.jme3.math.Triangle;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.terrain.GeoMap;
import com.jme3.util.BufferUtils;
import com.jme3.util.TempVars;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;

/**
 * Produces the mesh for the TerrainPatch.
 * This LOD algorithm generates a single triangle strip by first building the center of the
 * mesh, minus one outer edge around it. Then it builds the edges in counter-clockwise order,
 * starting at the bottom right and working up, then left across the top, then down across the
 * left, then right across the bottom.
 * It needs to know what its neighbour's LODs are, so it can stitch the edges.
 * It creates degenerate polygons in order to keep the winding order of the polygons and to move
 * the strip to a new position while still maintaining the continuity of the overall mesh. These
 * degenerates are removed quickly by the video card.
 *
 * @author Brent Owens
 */
public class LODGeomap extends GeoMap {

    public LODGeomap() {
    }
    
    public LODGeomap(int size, float[] heightMap) {
        super(heightMap, size, size, 1);
    }

    public Mesh createMesh(Vector3f scale, Vector2f tcScale, Vector2f tcOffset, float offsetAmount, int totalSize, boolean center) {
        return this.createMesh(scale, tcScale, tcOffset, offsetAmount, totalSize, center, 1, false, false, false, false);
    }

    public Mesh createMesh(Vector3f scale, Vector2f tcScale, Vector2f tcOffset, float offsetAmount, int totalSize, boolean center, int lod, boolean rightLod, boolean topLod, boolean leftLod, boolean bottomLod) {
        FloatBuffer pb = writeVertexArray(null, scale, center);
        FloatBuffer texb = writeTexCoordArray(null, tcOffset, tcScale, offsetAmount, totalSize);
        FloatBuffer nb = writeNormalArray(null, scale);
        IndexBuffer ib = writeIndexArrayLodDiff(lod, rightLod, topLod, leftLod, bottomLod, totalSize);
        FloatBuffer bb = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
        FloatBuffer tanb = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
        writeTangentArray(nb, tanb, bb, texb, scale);
        Mesh m = new Mesh();
        m.setMode(Mode.TriangleStrip);
        m.setBuffer(Type.Position, 3, pb);
        m.setBuffer(Type.Normal, 3, nb);
        m.setBuffer(Type.Tangent, 3, tanb);
        m.setBuffer(Type.Binormal, 3, bb);
        m.setBuffer(Type.TexCoord, 2, texb);
        switch (ib.getFormat()) {
            case UnsignedInt:
                m.setBuffer(Type.Index, 3, (IntBuffer) ib.getBuffer());
                break;
            case UnsignedShort:
                m.setBuffer(Type.Index, 3, (ShortBuffer) ib.getBuffer());
                break;
            case UnsignedByte:
                m.setBuffer(Type.Index, 3, (ByteBuffer) ib.getBuffer());
                break;
        }
        m.setStatic();
        m.updateBound();
        return m;
    }

    public FloatBuffer writeTexCoordArray(FloatBuffer store, Vector2f offset, Vector2f scale, float offsetAmount, int totalSize) {
        if (store != null) {
            if (store.remaining() < getWidth() * getHeight() * 2) {
                throw new BufferUnderflowException();
            }
        } else {
            store = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 2);
        }

        if (offset == null) {
            offset = new Vector2f();
        }

        Vector2f tcStore = new Vector2f();

        // work from bottom of heightmap up, so we don't flip the coords
        for (int y = getHeight() - 1; y >= 0; y--) {
            for (int x = 0; x < getWidth(); x++) {
                getUV(x, y, tcStore, offset, offsetAmount, totalSize);
                float tx = tcStore.x * scale.x;
                float ty = tcStore.y * scale.y;
                store.put(tx);
                store.put(ty);
            }
        }

        return store;
    }

    public Vector2f getUV(int x, int y, Vector2f store, Vector2f offset, float offsetAmount, int totalSize) {
        float offsetX = offset.x + (offsetAmount * 1.0f);
        float offsetY = -offset.y + (offsetAmount * 1.0f);//note the -, we flip the tex coords

        store.set((x + offsetX) / (totalSize - 1), // calculates percentage of texture here
                (y + offsetY) / (totalSize - 1));
        return store;
    }

    /**
     * Create the LOD index array that will seam its edges with its neighbour's LOD.
     * This is a scary method!!! It will break your mind.
     *
     * @param lod level of detail of the mesh
     * @param rightLod LOD of the right neighbour
     * @param topLod LOD of the top neighbour
     * @param leftLod LOD of the left neighbour
     * @param bottomLod LOD of the bottom neighbour
     * @return the LOD-ified index buffer
     */
    public IndexBuffer writeIndexArrayLodDiff(int lod, boolean rightLod, boolean topLod, boolean leftLod, boolean bottomLod, int totalSize) {
        int numVertices = getWidth() * getHeight();
        int numIndexes = calculateNumIndexesLodDiff(lod);
        IndexBuffer ib = IndexBuffer.createIndexBuffer(numVertices, numIndexes);
        VerboseBuffer buffer = new VerboseBuffer(ib);

        // generate center squares minus the edges
        //System.out.println("for (x="+lod+"; x<"+(getWidth()-(2*lod))+"; x+="+lod+")");
        //System.out.println("    for (z="+lod+"; z<"+(getWidth()-(1*lod))+"; z+="+lod+")");
        for (int r = lod; r < getWidth() - (2 * lod); r += lod) { // row
            int rowIdx = r * getWidth();
            int nextRowIdx = (r + 1 * lod) * getWidth();
            for (int c = lod; c < getWidth() - (1 * lod); c += lod) { // column
                int idx = rowIdx + c;
                buffer.put(idx);
                idx = nextRowIdx + c;
                buffer.put(idx);
            }

            // add degenerate triangles
            if (r < getWidth() - (3 * lod)) {
                int idx = nextRowIdx + getWidth() - (1 * lod) - 1;
                buffer.put(idx);
                idx = nextRowIdx + (1 * lod); // inset by 1
                buffer.put(idx);
                //System.out.println("");
            }
        }
        //System.out.println("\nright:");

        //int runningBufferCount = buffer.getCount();
        //System.out.println("buffer start: "+runningBufferCount);


        // right
        int br = getWidth() * (getWidth() - lod) - 1 - lod;
        buffer.put(br); // bottom right -1
        int corner = getWidth() * getWidth() - 1;
        buffer.put(corner);    // bottom right corner
        if (rightLod) { // if lower LOD
            for (int row = getWidth() - lod; row >= 1 + lod; row -= 2 * lod) {
                int idx = (row) * getWidth() - 1 - lod;
                buffer.put(idx);
                idx = (row - lod) * getWidth() - 1;
                buffer.put(idx);
                if (row > lod + 1) { //if not the last one
                    idx = (row - lod) * getWidth() - 1 - lod;
                    buffer.put(idx);
                    idx = (row - lod) * getWidth() - 1;
                    buffer.put(idx);
                } else {
                }
            }
        } else {
            buffer.put(corner);//br+1);//degenerate to flip winding order
            for (int row = getWidth() - lod; row > lod; row -= lod) {
                int idx = row * getWidth() - 1; // mult to get row
                buffer.put(idx);
                buffer.put(idx - lod);
            }

        }

        buffer.put(getWidth() - 1);


        //System.out.println("\nbuffer right: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();


        //System.out.println("\ntop:");

        // top (the order gets reversed here so the diagonals line up)
        if (topLod) { // if lower LOD
            if (rightLod) {
                buffer.put(getWidth() - 1);
            }
            for (int col = getWidth() - 1; col >= lod; col -= 2 * lod) {
                int idx = (lod * getWidth()) + col - lod; // next row
                buffer.put(idx);
                idx = col - 2 * lod;
                buffer.put(idx);
                if (col > lod * 2) { //if not the last one
                    idx = (lod * getWidth()) + col - 2 * lod;
                    buffer.put(idx);
                    idx = col - 2 * lod;
                    buffer.put(idx);
                } else {
                }
            }
        } else {
            if (rightLod) {
                buffer.put(getWidth() - 1);
            }
            for (int col = getWidth() - 1 - lod; col > 0; col -= lod) {
                int idx = col + (lod * getWidth());
                buffer.put(idx);
                idx = col;
                buffer.put(idx);
            }
            buffer.put(0);
        }
        buffer.put(0);

        //System.out.println("\nbuffer top: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();

        //System.out.println("\nleft:");

        // left
        if (leftLod) { // if lower LOD
            if (topLod) {
                buffer.put(0);
            }
            for (int row = 0; row < getWidth() - lod; row += 2 * lod) {
                int idx = (row + lod) * getWidth() + lod;
                buffer.put(idx);
                idx = (row + 2 * lod) * getWidth();
                buffer.put(idx);
                if (row < getWidth() - 1 - 2 * lod) { //if not the last one
                    idx = (row + 2 * lod) * getWidth() + lod;
                    buffer.put(idx);
                    idx = (row + 2 * lod) * getWidth();
                    buffer.put(idx);
                } else {
                }
            }
        } else {
            if (!topLod) {
                buffer.put(0);
            }
            //buffer.put(getWidth()+1); // degenerate
            //buffer.put(0); // degenerate winding-flip
            for (int row = lod; row < getWidth() - lod; row += lod) {
                int idx = row * getWidth();
                buffer.put(idx);
                idx = row * getWidth() + lod;
                buffer.put(idx);
            }

        }
        buffer.put(getWidth() * (getWidth() - 1));


        //System.out.println("\nbuffer left: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();

        //if (true) return buffer.delegate;
        //System.out.println("\nbottom");

        // bottom
        if (bottomLod) { // if lower LOD
            if (leftLod) {
                buffer.put(getWidth() * (getWidth() - 1));
            }
            // there was a slight bug here when really high LOD near maxLod
            // far right has extra index one row up and all the way to the right, need to skip last index entered
            // seemed to be fixed by making "getWidth()-1-2-lod" this: "getWidth()-1-2*lod", which seems more correct
            for (int col = 0; col < getWidth() - lod; col += 2 * lod) {
                int idx = getWidth() * (getWidth() - 1 - lod) + col + lod;
                buffer.put(idx);
                idx = getWidth() * (getWidth() - 1) + col + 2 * lod;
                buffer.put(idx);
                if (col < getWidth() - 1 - 2 * lod) { //if not the last one
                    idx = getWidth() * (getWidth() - 1 - lod) + col + 2 * lod;
                    buffer.put(idx);
                    idx = getWidth() * (getWidth() - 1) + col + 2 * lod;
                    buffer.put(idx);
                } else {
                }
            }
        } else {
            if (leftLod) {
                buffer.put(getWidth() * (getWidth() - 1));
            }
            for (int col = lod; col < getWidth() - lod; col += lod) {
                int idx = getWidth() * (getWidth() - 1 - lod) + col; // up
                buffer.put(idx);
                idx = getWidth() * (getWidth() - 1) + col; // down
                buffer.put(idx);
            }
            //buffer.put(getWidth()*getWidth()-1-lod); // <-- THIS caused holes at the end!
        }

        buffer.put(getWidth() * getWidth() - 1);

        //System.out.println("\nbuffer bottom: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();

        //System.out.println("\nBuffer size: "+buffer.getCount());

        // fill in the rest of the buffer with degenerates, there should only be a couple
        for (int i = buffer.getCount(); i < numIndexes; i++) {
            buffer.put(getWidth() * getWidth() - 1);
        }

        return buffer.delegate;
    }

    public IndexBuffer writeIndexArrayLodVariable(int lod, int rightLod, int topLod, int leftLod, int bottomLod, int totalSize) {
        int numVertices = getWidth() * getHeight();
        int numIndexes = calculateNumIndexesLodDiff(lod);
        IndexBuffer ib = IndexBuffer.createIndexBuffer(numVertices, numIndexes);
        VerboseBuffer buffer = new VerboseBuffer(ib);

        // generate center squares minus the edges
        //System.out.println("for (x="+lod+"; x<"+(getWidth()-(2*lod))+"; x+="+lod+")");
        //System.out.println("    for (z="+lod+"; z<"+(getWidth()-(1*lod))+"; z+="+lod+")");
        for (int r = lod; r < getWidth() - (2 * lod); r += lod) { // row
            int rowIdx = r * getWidth();
            int nextRowIdx = (r + 1 * lod) * getWidth();
            for (int c = lod; c < getWidth() - (1 * lod); c += lod) { // column
                int idx = rowIdx + c;
                buffer.put(idx);
                idx = nextRowIdx + c;
                buffer.put(idx);
            }

            // add degenerate triangles
            if (r < getWidth() - (3 * lod)) {
                int idx = nextRowIdx + getWidth() - (1 * lod) - 1;
                buffer.put(idx);
                idx = nextRowIdx + (1 * lod); // inset by 1
                buffer.put(idx);
                //System.out.println("");
            }
        }
        //System.out.println("\nright:");

        //int runningBufferCount = buffer.getCount();
        //System.out.println("buffer start: "+runningBufferCount);


        // right
        int br = getWidth() * (getWidth() - lod) - 1 - lod;
        buffer.put(br); // bottom right -1
        int corner = getWidth() * getWidth() - 1;
        buffer.put(corner);    // bottom right corner
        if (rightLod > lod) { // if lower LOD
            int idx = corner;
            int it = (getWidth() - 1) / rightLod; // iterations
            int lodDiff = rightLod / lod;
            for (int i = it; i > 0; i--) { // for each lod level of the neighbour
                idx = getWidth() * (i * rightLod + 1) - 1;
                for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
                    int idxB = idx - (getWidth() * (j * lod)) - lod;

                    if (j == lodDiff && i == 1) {// the last one
                        buffer.put(getWidth() - 1);
                    } else if (j == lodDiff) {
                        buffer.put(idxB);
                        buffer.put(idxB + lod);
                    } else {
                        buffer.put(idxB);
                        buffer.put(idx);
                    }
                }
            }
            // reset winding order
            buffer.put(getWidth() * (lod + 1) - lod - 1); // top-right +1row
            buffer.put(getWidth() - 1);// top-right

        } else {
            buffer.put(corner);//br+1);//degenerate to flip winding order
            for (int row = getWidth() - lod; row > lod; row -= lod) {
                int idx = row * getWidth() - 1; // mult to get row
                buffer.put(idx);
                buffer.put(idx - lod);
            }
            buffer.put(getWidth() - 1);
        }


        //System.out.println("\nbuffer right: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();


        //System.out.println("\ntop:");

        // top (the order gets reversed here so the diagonals line up)
        if (topLod > lod) { // if lower LOD
            if (rightLod > lod) {
                // need to flip winding order
                buffer.put(getWidth() - 1);
                buffer.put(getWidth() * lod - 1);
                buffer.put(getWidth() - 1);
            }
            int idx = getWidth() - 1;
            int it = (getWidth() - 1) / topLod; // iterations
            int lodDiff = topLod / lod;
            for (int i = it; i > 0; i--) { // for each lod level of the neighbour
                idx = (i * topLod);
                for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
                    int idxB = lod * getWidth() + (i * topLod) - (j * lod);

                    if (j == lodDiff && i == 1) {// the last one
                        buffer.put(0);
                    } else if (j == lodDiff) {
                        buffer.put(idxB);
                        buffer.put(idx - topLod);
                    } else {
                        buffer.put(idxB);
                        buffer.put(idx);
                    }
                }
            }
        } else {
            if (rightLod > lod) {
                buffer.put(getWidth() - 1);
            }
            for (int col = getWidth() - 1 - lod; col > 0; col -= lod) {
                int idx = col + (lod * getWidth());
                buffer.put(idx);
                idx = col;
                buffer.put(idx);
            }
            buffer.put(0);
        }
        buffer.put(0);

        //System.out.println("\nbuffer top: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();

        //System.out.println("\nleft:");

        // left
        if (leftLod > lod) { // if lower LOD

            int idx = 0;
            int it = (getWidth() - 1) / leftLod; // iterations
            int lodDiff = leftLod / lod;
            for (int i = 0; i < it; i++) { // for each lod level of the neighbour
                idx = getWidth() * (i * leftLod);
                for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
                    int idxB = idx + (getWidth() * (j * lod)) + lod;

                    if (j == lodDiff && i == it - 1) {// the last one
                        buffer.put(getWidth() * getWidth() - getWidth());
                    } else if (j == lodDiff) {
                        buffer.put(idxB);
                        buffer.put(idxB - lod);
                    } else {
                        buffer.put(idxB);
                        buffer.put(idx);
                    }
                }
            }

        } else {
            buffer.put(0);
            buffer.put(getWidth() * lod + lod);
            buffer.put(0);
            for (int row = lod; row < getWidth() - lod; row += lod) {
                int idx = row * getWidth();
                buffer.put(idx);
                idx = row * getWidth() + lod;
                buffer.put(idx);
            }
            buffer.put(getWidth() * (getWidth() - 1));
        }
        //buffer.put(getWidth()*(getWidth()-1));


        //System.out.println("\nbuffer left: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();

        //if (true) return buffer.delegate;
        //System.out.println("\nbottom");

        // bottom
        if (bottomLod > lod) { // if lower LOD
            if (leftLod > lod) {
                buffer.put(getWidth() * (getWidth() - 1));
                buffer.put(getWidth() * (getWidth() - lod));
                buffer.put(getWidth() * (getWidth() - 1));
            }

            int idx = getWidth() * getWidth() - getWidth();
            int it = (getWidth() - 1) / bottomLod; // iterations
            int lodDiff = bottomLod / lod;
            for (int i = 0; i < it; i++) { // for each lod level of the neighbour
                idx = getWidth() * getWidth() - getWidth() + (i * bottomLod);
                for (int j = 1; j <= lodDiff; j++) { // for each section in that lod level
                    int idxB = idx - (getWidth() * lod) + j * lod;

                    if (j == lodDiff && i == it - 1) {// the last one
                        buffer.put(getWidth() * getWidth() - 1);
                    } else if (j == lodDiff) {
                        buffer.put(idxB);
                        buffer.put(idx + bottomLod);
                    } else {
                        buffer.put(idxB);
                        buffer.put(idx);
                    }
                }
            }
        } else {
            if (leftLod > lod) {
                buffer.put(getWidth() * (getWidth() - 1));
                buffer.put(getWidth() * getWidth() - (getWidth() * lod) + lod);
                buffer.put(getWidth() * (getWidth() - 1));
            }
            for (int col = lod; col < getWidth() - lod; col += lod) {
                int idx = getWidth() * (getWidth() - 1 - lod) + col; // up
                buffer.put(idx);
                idx = getWidth() * (getWidth() - 1) + col; // down
                buffer.put(idx);
            }
            //buffer.put(getWidth()*getWidth()-1-lod); // <-- THIS caused holes at the end!
        }

        buffer.put(getWidth() * getWidth() - 1);

        //System.out.println("\nbuffer bottom: "+(buffer.getCount()-runningBufferCount));
        //runningBufferCount = buffer.getCount();

        //System.out.println("\nBuffer size: "+buffer.getCount());

        // fill in the rest of the buffer with degenerates, there should only be a couple
        for (int i = buffer.getCount(); i < numIndexes; i++) {
            buffer.put(getWidth() * getWidth() - 1);
        }

        return buffer.delegate;
    }


    /*private int calculateNumIndexesNormal(int lod) {
    int length = getWidth()-1;
    int num = ((length/lod)+1)*((length/lod)+1)*2;
    System.out.println("num: "+num);
    num -= 2*((length/lod)+1);
    System.out.println("num2: "+num);
    // now get the degenerate indexes that exist between strip rows
    num += 2*(((length/lod)+1)-2); // every row except the first and last
    System.out.println("Index buffer size: "+num);
    return num;
    }*/
    /**
     * calculate how many indexes there will be.
     * This isn't that precise and there might be a couple extra.
     */
    private int calculateNumIndexesLodDiff(int lod) {
        if (lod == 0) {
            lod = 1;
        }
        int length = getWidth() - 1; // make it even for lod calc
        int side = (length / lod) + 1 - (2);
        //System.out.println("side: "+side);
        int num = side * side * 2;
        //System.out.println("num: "+num);
        num -= 2 * side;    // remove one first row and one last row (they are only hit once each)
        //System.out.println("num2: "+num);
        // now get the degenerate indexes that exist between strip rows
        int degenerates = 2 * (side - (2)); // every row except the first and last
        num += degenerates;
        //System.out.println("degenerates: "+degenerates);

        //System.out.println("center, before edges: "+num);

        num += (getWidth() / lod) * 2 * 4;
        num++;

        num += 10;// TODO remove me: extra
        //System.out.println("Index buffer size: "+num);
        return num;
    }

    public FloatBuffer[] writeTangentArray(FloatBuffer normalBuffer, FloatBuffer tangentStore, FloatBuffer binormalStore, FloatBuffer textureBuffer, Vector3f scale) {
        if (!isLoaded()) {
            throw new IllegalStateException("The Geomap data is not loaded.");
        }

        if (tangentStore != null) {
            if (tangentStore.remaining() < getWidth() * getHeight() * 3) {
                throw new BufferUnderflowException();
            }
        } else {
            tangentStore = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
        }
        tangentStore.rewind();

        if (binormalStore != null) {
            if (binormalStore.remaining() < getWidth() * getHeight() * 3) {
                throw new BufferUnderflowException();
            }
        } else {
            binormalStore = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
        }
        binormalStore.rewind();

        Vector3f normal = new Vector3f();
        Vector3f tangent = new Vector3f();
        Vector3f binormal = new Vector3f();
        /*Vector3f v1 = new Vector3f();
        Vector3f v2 = new Vector3f();
        Vector3f v3 = new Vector3f();
        Vector2f t1 = new Vector2f();
        Vector2f t2 = new Vector2f();
        Vector2f t3 = new Vector2f();*/

        for (int r = 0; r < getHeight(); r++) {
            for (int c = 0; c < getWidth(); c++) {
                
                int idx = (r * getWidth() + c) * 3;
                normal.set(normalBuffer.get(idx), normalBuffer.get(idx+1), normalBuffer.get(idx+2));
                tangent.set(normal.cross(new Vector3f(0,0,1)));
                binormal.set(new Vector3f(1,0,0).cross(normal));
                
                BufferUtils.setInBuffer(tangent.normalizeLocal(), tangentStore, (r * getWidth() + c)); // save the tangent
                BufferUtils.setInBuffer(binormal.normalizeLocal(), binormalStore, (r * getWidth() + c)); // save the binormal
            }
        }

/*        for (int r = 0; r < getHeight(); r++) {
            for (int c = 0; c < getWidth(); c++) {

                int texIdx = ((getHeight() - 1 - r) * getWidth() + c) * 2; // pull from the end
                int texIdxAbove = ((getHeight() - 1 - (r - 1)) * getWidth() + c) * 2; // pull from the end
                int texIdxNext = ((getHeight() - 1 - (r + 1)) * getWidth() + c) * 2; // pull from the end

                v1.set(c, getValue(c, r), r);
                t1.set(textureBuffer.get(texIdx), textureBuffer.get(texIdx + 1));

                // below
                if (r == getHeight()-1) { // last row
                    v3.set(c, getValue(c, r), r + 1);
                    float u = textureBuffer.get(texIdx) - textureBuffer.get(texIdxAbove);
                    u += textureBuffer.get(texIdx);
                    float v = textureBuffer.get(texIdx + 1) - textureBuffer.get(texIdxAbove + 1);
                    v += textureBuffer.get(texIdx + 1);
                    t3.set(u, v);
                } else {
                    v3.set(c, getValue(c, r + 1), r + 1);
                    t3.set(textureBuffer.get(texIdxNext), textureBuffer.get(texIdxNext + 1));
                }
                
                //right
                if (c == getWidth()-1) { // last column
                    v2.set(c + 1, getValue(c, r), r);
                    float u = textureBuffer.get(texIdx) - textureBuffer.get(texIdx - 2);
                    u += textureBuffer.get(texIdx);
                    float v = textureBuffer.get(texIdx + 1) - textureBuffer.get(texIdx - 1);
                    v += textureBuffer.get(texIdx - 1);
                    t2.set(u, v);
                } else {
                    v2.set(c + 1, getValue(c + 1, r), r); // one to the right
                    t2.set(textureBuffer.get(texIdx + 2), textureBuffer.get(texIdx + 3));
                }

                calculateTangent(new Vector3f[]{v1.mult(scale), v2.mult(scale), v3.mult(scale)}, new Vector2f[]{t1, t2, t3}, tangent, binormal);
                BufferUtils.setInBuffer(tangent, tangentStore, (r * getWidth() + c)); // save the tangent
                BufferUtils.setInBuffer(binormal, binormalStore, (r * getWidth() + c)); // save the binormal
            }
        }
        */
        return new FloatBuffer[]{tangentStore, binormalStore};
    }

    /**
     * 
     * @param v Takes 3 vertices: root, right, bottom
     * @param t Takes 3 tex coords: root, right, bottom
     * @param tangent that will store the result
     * @return the tangent store
     */
    public static Vector3f calculateTangent(Vector3f[] v, Vector2f[] t, Vector3f tangent, Vector3f binormal) {
        Vector3f edge1 = new Vector3f(); // y=0
        Vector3f edge2 = new Vector3f(); // x=0
        Vector2f edge1uv = new Vector2f(); // y=0
        Vector2f edge2uv = new Vector2f(); // x=0

        t[2].subtract(t[0], edge2uv);
        t[1].subtract(t[0], edge1uv);

        float det = edge1uv.x * edge2uv.y;// - edge1uv.y*edge2uv.x;  = 0

        boolean normalize = true;
        if (Math.abs(det) < 0.0000001f) {
            det = 1;
            normalize = true;
        }

        v[1].subtract(v[0], edge1);
        v[2].subtract(v[0], edge2);

        tangent.set(edge1);
        tangent.normalizeLocal();
        binormal.set(edge2);
        binormal.normalizeLocal();

        float factor = 1 / det;
        tangent.x = (edge2uv.y * edge1.x) * factor;
        tangent.y = 0;
        tangent.z = (edge2uv.y * edge1.z) * factor;
        if (normalize) {
            tangent.normalizeLocal();
        }

        binormal.x = 0;
        binormal.y = (edge1uv.x * edge2.y) * factor;
        binormal.z = (edge1uv.x * edge2.z) * factor;
        if (normalize) {
            binormal.normalizeLocal();
        }

        return tangent;
    }

    @Override
    public FloatBuffer writeNormalArray(FloatBuffer store, Vector3f scale) {
        if (!isLoaded()) {
            throw new IllegalStateException("The Geomap data is not loaded.");
        }

        if (store != null) {
            if (store.remaining() < getWidth() * getHeight() * 3) {
                throw new BufferUnderflowException();
            }
        } else {
            store = BufferUtils.createFloatBuffer(getWidth() * getHeight() * 3);
        }
        store.rewind();

        TempVars vars = TempVars.get();
        
        Vector3f rootPoint = vars.vect1;
        Vector3f rightPoint = vars.vect2;
        Vector3f leftPoint = vars.vect3;
        Vector3f topPoint = vars.vect4;
        Vector3f bottomPoint = vars.vect5;
        
        Vector3f tmp1 = vars.vect6;

        // calculate normals for each polygon
        for (int r = 0; r < getHeight(); r++) {
            for (int c = 0; c < getWidth(); c++) {

                rootPoint.set(0, getValue(c, r), 0);
                Vector3f normal = vars.vect8;

                if (r == 0) { // first row
                    if (c == 0) { // first column
                        rightPoint.set(1, getValue(c + 1, r), 0);
                        bottomPoint.set(0, getValue(c, r + 1), 1);
                        getNormal(bottomPoint, rootPoint, rightPoint, scale, normal);
                    } else if (c == getWidth() - 1) { // last column
                        leftPoint.set(-1, getValue(c - 1, r), 0);
                        bottomPoint.set(0, getValue(c, r + 1), 1);
                        getNormal(leftPoint, rootPoint, bottomPoint, scale, normal);
                    } else { // all middle columns
                        leftPoint.set(-1, getValue(c - 1, r), 0);
                        rightPoint.set(1, getValue(c + 1, r), 0);
                        bottomPoint.set(0, getValue(c, r + 1), 1);
                        
                        normal.set( getNormal(leftPoint, rootPoint, bottomPoint, scale, tmp1) );
                        normal.addLocal( getNormal(bottomPoint, rootPoint, rightPoint, scale, tmp1) );
                    }
                } else if (r == getHeight() - 1) { // last row
                    if (c == 0) { // first column
                        topPoint.set(0, getValue(c, r - 1), -1);
                        rightPoint.set(1, getValue(c + 1, r), 0);
                        getNormal(rightPoint, rootPoint, topPoint, scale, normal);
                    } else if (c == getWidth() - 1) { // last column
                        topPoint.set(0, getValue(c, r - 1), -1);
                        leftPoint.set(-1, getValue(c - 1, r), 0);
                        getNormal(topPoint, rootPoint, leftPoint, scale, normal);
                    } else { // all middle columns
                        topPoint.set(0, getValue(c, r - 1), -1);
                        leftPoint.set(-1, getValue(c - 1, r), 0);
                        rightPoint.set(1, getValue(c + 1, r), 0);
                        
                        normal.set( getNormal(topPoint, rootPoint, leftPoint, scale, tmp1) );
                        normal.addLocal( getNormal(rightPoint, rootPoint, topPoint, scale, tmp1) );
                    }
                } else { // all middle rows
                    if (c == 0) { // first column
                        topPoint.set(0, getValue(c, r - 1), -1);
                        rightPoint.set(1, getValue(c + 1, r), 0);
                        bottomPoint.set(0, getValue(c, r + 1), 1);
                        
                        normal.set( getNormal(rightPoint, rootPoint, topPoint, scale, tmp1) );
                        normal.addLocal( getNormal(bottomPoint, rootPoint, rightPoint, scale, tmp1) );
                    } else if (c == getWidth() - 1) { // last column
                        topPoint.set(0, getValue(c, r - 1), -1);
                        leftPoint.set(-1, getValue(c - 1, r), 0);
                        bottomPoint.set(0, getValue(c, r + 1), 1);

                        normal.set( getNormal(topPoint, rootPoint, leftPoint, scale, tmp1) );
                        normal.addLocal( getNormal(leftPoint, rootPoint, bottomPoint, scale, tmp1) );
                    } else { // all middle columns
                        topPoint.set(0, getValue(c, r - 1), -1);
                        leftPoint.set(-1, getValue(c - 1, r), 0);
                        rightPoint.set(1, getValue(c + 1, r), 0);
                        bottomPoint.set(0, getValue(c, r + 1), 1);
                        
                        normal.set( getNormal(topPoint,  rootPoint, leftPoint, scale, tmp1 ) );
                        normal.addLocal( getNormal(leftPoint, rootPoint, bottomPoint, scale, tmp1) );
                        normal.addLocal( getNormal(bottomPoint, rootPoint, rightPoint, scale, tmp1) );
                        normal.addLocal( getNormal(rightPoint, rootPoint, topPoint, scale, tmp1) );
                    }
                }
                normal.normalizeLocal();
                BufferUtils.setInBuffer(normal, store, (r * getWidth() + c)); // save the normal
            }
        }
        vars.release();
        
        return store;
    }

    private Vector3f getNormal(Vector3f firstPoint, Vector3f rootPoint, Vector3f secondPoint, Vector3f scale, Vector3f store) {
        float x1 = firstPoint.x - rootPoint.x;
        float y1 = firstPoint.y - rootPoint.y;
        float z1 = firstPoint.z - rootPoint.z;
        x1 *= scale.x;
        y1 *= scale.y;
        z1 *= scale.z;
        float x2 = secondPoint.x - rootPoint.x;
        float y2 = secondPoint.y - rootPoint.y;
        float z2 = secondPoint.z - rootPoint.z;
        x2 *= scale.x;
        y2 *= scale.y;
        z2 *= scale.z;
        float x3 = (y1 * z2) - (z1 * y2);
        float y3 = (z1 * x2) - (x1 * z2);
        float z3 = (x1 * y2) - (y1 * x2);
        
        float inv = 1.0f / FastMath.sqrt(x3 * x3 + y3 * y3 + z3 * z3);
        store.x = x3 * inv;
        store.y = y3 * inv;
        store.z = z3 * inv;
        
        
        /*firstPoint.multLocal(scale);
        rootPoint.multLocal(scale);
        secondPoint.multLocal(scale);
        firstPoint.subtractLocal(rootPoint);
        secondPoint.subtractLocal(rootPoint);
        firstPoint.cross(secondPoint, store);*/
        return store;
    }

    /**
     * Keeps a count of the number of indexes, good for debugging
     */
    public class VerboseBuffer {

        private IndexBuffer delegate;
        //private IntBuffer delegateInt;
        //private ShortBuffer delegateShort;
        int count = 0;
        //private boolean intb = true;

        public VerboseBuffer(IndexBuffer d) {
            this.delegate = d;
        }
        
        /*public VerboseBuffer(Buffer d) {
            if (d instanceof IntBuffer)
                delegateInt = (IntBuffer)d;
            else if (d instanceof ShortBuffer) {
                delegateShort = (ShortBuffer)d;
                intb = false;
            }
        }*/

        public void put(int value) {
            delegate.put(count, value);
            count++;
        }
        /*public void put(int value) {
            try {
                count++;
                int limit = intb? delegateInt.limit() : delegateShort.limit();
                if (count > limit)
                    throw new BufferOverflowException();
                if (intb)
                    delegateInt.put(value);
                else {
                    System.out.println(Integer.toString(value)+" "+Short.toString((short)value));
                    delegateShort.put((short)value);
                }
            } catch (BufferOverflowException e) {
                Logger.getLogger(this.getClass().getName()).log(Logger.Level.ERROR, "err buffer size: "+delegateInt.capacity());
            }
        }*/

        public int getCount() {
            return count;
        }
    }

    /**
     * Get the two triangles that make up the grid section at the specified point.
     *
     * For every grid space there are two triangles oriented like this:
     *  *----*
     *  |a / |
     *  | / b|
     *  *----*
     * The corners of the mesh have differently oriented triangles. The two
     * corners that we have to special-case are the top left and bottom right
     * corners. They are oriented inversely:
     *  *----*
     *  | \ b|
     *  |a \ |
     *  *----*
     */
    protected float getHeight(int x, int z, float xm, float zm) {
        
        int index = findClosestHeightIndex(x, z);
        if (index < 0) {
            return Float.NaN;
        }
        
        float h1 = hdata[index];                // top left
        float h2 = hdata[index + 1];            // top right
        float h3 = hdata[index + width];        // bottom left
        float h4 = hdata[index + width + 1];    // bottom right

        //float dix = (x % 1f) ;
        //float diz = (z % 1f) ;
            
        if ((x == 0 && z == 0) || (x == width - 2 && z == width - 2)) {
            // top left or bottom right grid point
            /*  1----2
             *  | \ b|
             *  |a \ |
             *  3----4 */
            if (xm<zm)
                return h1 + xm*(h4-h3) + zm*(h3-h1);
            else
                return h1 + xm*(h2-h1) + zm*(h4-h2);
            
        } else {
            // all other grid points
            /*  1----2
             *  |a / |
             *  | / b|
             *  3----4 */
            if (xm<(1-zm))
                return h3 + (xm)*(h2-h1) + (1f-zm)*(h1-h3);
            else
                return h3 + (xm)*(h4-h3) + (1f-zm)*(h2-h4);
        }
    }
    
    /**
     * Get a representation of the underlying triangle at the given point,
     * translated to world coordinates.
     * 
     * @param x local x coordinate
     * @param z local z coordinate
     * @return a triangle in world space not local space
     */
    protected Triangle getTriangleAtPoint(float x, float z, Vector3f scale, Vector3f translation) {
        Triangle tri = getTriangleAtPoint(x, z);
        if (tri != null) {
            tri.get1().multLocal(scale).addLocal(translation);
            tri.get2().multLocal(scale).addLocal(translation);
            tri.get3().multLocal(scale).addLocal(translation);
        }
        return tri;
    }

    /**
     * Get the two triangles that make up the grid section at the specified point,
     * translated to world coordinates.
     *
     * @param x local x coordinate
     * @param z local z coordinate
     * @param scale
     * @param translation
     * @return two triangles in world space not local space
     */
    protected Triangle[] getGridTrianglesAtPoint(float x, float z, Vector3f scale, Vector3f translation) {
        Triangle[] tris = getGridTrianglesAtPoint(x, z);
        if (tris != null) {
            tris[0].get1().multLocal(scale).addLocal(translation);
            tris[0].get2().multLocal(scale).addLocal(translation);
            tris[0].get3().multLocal(scale).addLocal(translation);
            tris[1].get1().multLocal(scale).addLocal(translation);
            tris[1].get2().multLocal(scale).addLocal(translation);
            tris[1].get3().multLocal(scale).addLocal(translation);
        }
        return tris;
    }

    /**
     * Get the two triangles that make up the grid section at the specified point.
     *
     * For every grid space there are two triangles oriented like this:
     *  *----*
     *  |a / |
     *  | / b|
     *  *----*
     * The corners of the mesh have differently oriented triangles. The two
     * corners that we have to special-case are the top left and bottom right
     * corners. They are oriented inversely:
     *  *----*
     *  | \ b|
     *  |a \ |
     *  *----*
     *
     * @param x local x coordinate
     * @param z local z coordinate
     * @return a new array or null
     */
    protected Triangle[] getGridTrianglesAtPoint(float x, float z) {
        int gridX = (int) x;
        int gridY = (int) z;

        int index = findClosestHeightIndex(gridX, gridY);
        if (index < 0) {
            return null;
        }

        Triangle t = new Triangle(new Vector3f(), new Vector3f(), new Vector3f());
        Triangle t2 = new Triangle(new Vector3f(), new Vector3f(), new Vector3f());

        float h1 = hdata[index];                // top left
        float h2 = hdata[index + 1];            // top right
        float h3 = hdata[index + width];        // bottom left
        float h4 = hdata[index + width + 1];    // bottom right


        if ((gridX == 0 && gridY == 0) || (gridX == width - 2 && gridY == width - 2)) {
            // top left or bottom right grid point
            t.get(0).x = (gridX);
            t.get(0).y = (h1);
            t.get(0).z = (gridY);

            t.get(1).x = (gridX);
            t.get(1).y = (h3);
            t.get(1).z = (gridY + 1);

            t.get(2).x = (gridX + 1);
            t.get(2).y = (h4);
            t.get(2).z = (gridY + 1);

            t2.get(0).x = (gridX);
            t2.get(0).y = (h1);
            t2.get(0).z = (gridY);

            t2.get(1).x = (gridX + 1);
            t2.get(1).y = (h4);
            t2.get(1).z = (gridY + 1);

            t2.get(2).x = (gridX + 1);
            t2.get(2).y = (h2);
            t2.get(2).z = (gridY);
        } else {
            // all other grid points
            t.get(0).x = (gridX);
            t.get(0).y = (h1);
            t.get(0).z = (gridY);

            t.get(1).x = (gridX);
            t.get(1).y = (h3);
            t.get(1).z = (gridY + 1);

            t.get(2).x = (gridX + 1);
            t.get(2).y = (h2);
            t.get(2).z = (gridY);

            t2.get(0).x = (gridX + 1);
            t2.get(0).y = (h2);
            t2.get(0).z = (gridY);

            t2.get(1).x = (gridX);
            t2.get(1).y = (h3);
            t2.get(1).z = (gridY + 1);

            t2.get(2).x = (gridX + 1);
            t2.get(2).y = (h4);
            t2.get(2).z = (gridY + 1);
        }

        return new Triangle[]{t, t2};
    }

    /**
     * Get the triangle that the point is on.
     * 
     * @param x coordinate in local space to the geomap
     * @param z coordinate in local space to the geomap
     * @return triangle in local space to the geomap
     */
    protected Triangle getTriangleAtPoint(float x, float z) {
        Triangle[] triangles = getGridTrianglesAtPoint(x, z);
        if (triangles == null) {
            //System.out.println("x,z: " + x + "," + z);
            return null;
        }
        Vector2f point = new Vector2f(x, z);
        Vector2f t1 = new Vector2f(triangles[0].get1().x, triangles[0].get1().z);
        Vector2f t2 = new Vector2f(triangles[0].get2().x, triangles[0].get2().z);
        Vector2f t3 = new Vector2f(triangles[0].get3().x, triangles[0].get3().z);

        if (0 != FastMath.pointInsideTriangle(t1, t2, t3, point)) {
            return triangles[0];
        }

        t1.set(triangles[1].get1().x, triangles[1].get1().z);
        t1.set(triangles[1].get2().x, triangles[1].get2().z);
        t1.set(triangles[1].get3().x, triangles[1].get3().z);

        if (0 != FastMath.pointInsideTriangle(t1, t2, t3, point)) {
            return triangles[1];
        }

        return null;
    }

    protected int findClosestHeightIndex(int x, int z) {

        if (x < 0 || x >= width - 1) {
            return -1;
        }
        if (z < 0 || z >= width - 1) {
            return -1;
        }

        return z * width + x;
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        super.read(im);
    }
}
