package net.monoid.j3d.utils.geometry;

import java.awt.Point;
import javax.media.j3d.*;
import javax.vecmath.*;

import app.Settings;

import com.sun.j3d.utils.geometry.*;

/**
 * Heightmap
 * @author Michael Nischt
 * @version 0.1
 */
public class Heightmap extends Group {
    
    // the max view range
    final static float viewRadius = 4.5f*Settings.backClipDistance;
    
    private float stride = 10.0f;     // amount to move from vert to vert
    private float scale = 1.1f;               // how to scale the heightmap data
    private int tile = 4;             // how much to tile a texture
    
    private int tileWidth;              // the width of one tile
    
    private int width = 0;            // width of terrain
    private int height = 0;           // height of terrain   
    
    // for the GeometryUpdater
    private GeometryArray[] geomArray = null;
    private int number = 0;
    
    
    // for splittingt he map
    private Point[] fieldMinima = null;
    private Point[] fieldMaxima = null;
    
    // indices
    int[] ndx = null;                   // for normal generating
    
    //Geometry By Reference
    public Point3f[] verts = null;
    public TexCoord2f[] tcoords = null;
    public Vector3f[] normals = null;
    
    // 
    private Appearance ap = null;
    Texture2D mapTexture = null;
    
      
    public Heightmap(int[] buffer, int width, int height, Appearance ap, Texture2D mapTexture)
    {
        this.width = width;
        this.height = height;
        
        int w = (width-1)/2;
        int h = (height-1)/2;
        
        this.stride = 60.0f;
        this.tile =   8;
        this.scale =  2.0f;
        
        this.tileWidth = (width-1)/tile;
        
        this.ap = ap;   
        this.mapTexture = mapTexture;
       

        buildTerrain(buffer);
        
        // GeometryInfo
        GeometryInfo geomInfo = new GeometryInfo(GeometryInfo.TRIANGLE_ARRAY);
        
        // Setup geometry
        geomInfo.setCoordinates(verts);
        geomInfo.setCoordinateIndices(ndx);
        
        // Helper for GeometryInfo
        NormalGenerator ng = new NormalGenerator();
        ng.setCreaseAngle(Math.PI);
        ng.generateNormals(geomInfo);
        
        normals = geomInfo.getNormals();

        // bounds for this-objects (extends Group)
        this.setBoundsAutoCompute(false);
        this.setBounds(new BoundingBox(new Point3d(-w * stride, 0.0, -h*stride), new Point3d(w * stride, scale*256.0, h * stride)));
  
        geomArray = new GeometryArray[tile*tile];
        
        this.split();
        
    }
    
    private void buildTerrain(int[] buffer)
    {
        // Setup-Arrays
        ndx = new int[3*2*(width-1)*(height-1)];
        verts = new Point3f[width * height];
        tcoords = new TexCoord2f[width * height];
        
        // for indexing
        int vPos, nPos;
        int tri = 0;
        int[] quadNdx = new int[4];
        
        //for texcoords
        float u = 0, v = 0;
        float hop = (float)tile / (width-1);
        
        //for vertices
        float x = 0, y = 0, z = -((height-1) / 2.0f) * stride;
        
        // build the terrain, gather the texture coords and setup the indices
        for (int h = 0; h < height; h++, z += stride, v += hop)
        {
            x = -(((width-1) / 2.0f) * stride);
            u = 0;
            
            for (int w = 0; w < width; w++, x += stride, u += hop)
            {
                vPos = ((h * width) + w);
                
                y = buffer[vPos] * scale;
                
                verts[vPos] = new Point3f(x, y, z);
                tcoords[vPos] = new TexCoord2f(u, v);
                
                if (h < height-1 && w < width-1)
                {
                    nPos = tri * 3;
                    quadNdx[0] = (h * width) + w;
                    quadNdx[1] = (h * width) + w+1;
                    quadNdx[2] = ((h+1) * width) + w+1;
                    quadNdx[3] = ((h+1) * width) + w;
                    
                    ndx[nPos  ] = quadNdx[1];
                    ndx[nPos+1] = quadNdx[0];
                    ndx[nPos+2] = quadNdx[3];
                    tri++;
                    
                    nPos = tri * 3;
                    ndx[nPos  ] = quadNdx[3];
                    ndx[nPos+1] = quadNdx[2];
                    ndx[nPos+2] = quadNdx[1];
                    tri++;
                }
            }
        }
    }
    
     
    private void split()
    {
        // INIT
        int maxFields = tile * tile;
        fieldMinima = new Point[maxFields];
        fieldMaxima = new Point[maxFields];
        
        for(int i=0; i < tile; i++)
            for(int j=0; j < tile; j++)
            {
                fieldMinima[(i* tile) + j] = new Point(j*(tileWidth), i*(tileWidth));
                fieldMaxima[(i* tile) + j] = new Point((j+1)*(tileWidth), (i+1)*(tileWidth));
            }
        
        for(int actual=0; actual < maxFields;actual++)
            addField(fieldMinima[actual], fieldMaxima[actual]);
               
    }
     
    private void addGeometry(int[] stripNdx, int[] stripCnts)
    {
        // geom will be an IndexedTriangleStripArray with BY_REFERENCE //---data from from GeometryInfo Object
        IndexedTriangleStripArray geom = new IndexedTriangleStripArray(verts.length,
        GeometryArray.COORDINATES | GeometryArray.NORMALS | GeometryArray.TEXTURE_COORDINATE_2 | GeometryArray.BY_REFERENCE,
        stripNdx.length , stripCnts);
        
        // set GeometryArray stuff BY_REFERENCE
        geom.setCoordRef3f(verts);
        geom.setNormalRef3f(normals);
        geom.setTexCoordRef2f(0, tcoords);
        
        // set Indices
        geom.setCoordinateIndices(0, stripNdx);
        geom.setNormalIndices(0, stripNdx);
        geom.setTextureCoordinateIndices(0, 0, stripNdx);
        
        geom.setCapability(GeometryArray.ALLOW_REF_DATA_READ);
        geom.setCapability(GeometryArray.ALLOW_REF_DATA_WRITE);
        
        geomArray[number++] = geom;        
        
        Shape3D shape = new Shape3D(geom);
        
        
        if(ap != null)
        {
            ap.setTexture(mapTexture);
            shape.setAppearance(ap);
        }
        
               
        shape.setBoundsAutoCompute(false);
        shape.setBounds(new BoundingBox(new Point3d(verts[stripNdx[0]].x, 0.0, verts[stripNdx[0]].z),
        new Point3d(verts[stripNdx[stripNdx.length-1]].x, 256.0 * scale, verts[stripNdx[stripNdx.length-1]].z)));
              
        addChild(shape);
    }
    
    private void addField(Point min, Point max)
    {
        int fieldWidth = max.x - min.x + 1;
        int fieldHeight = max.y - min.y;
        
        int[] stripCnts= new int[fieldHeight];
        for(int i=0; i<stripCnts.length; i++)
        {
            stripCnts[i] = (fieldWidth) * 2;
        }
        
        //modify Strip-indices
        int[] stripNdx = new int[fieldHeight * (fieldWidth) * 2];
        
        for(int j=0; j<stripCnts.length; j++)
        {
            for(int i=0; i<stripCnts[j]; i++)
            {
                if(i % 2 == 0)
                    stripNdx[stripCnts[j]*j+i] = (width)*(min.y+j)+((min.x)+i/2);
                else
                    stripNdx[stripCnts[j]*j+i] = (width)*(min.y+j+1)+((min.x)+i/2);
                
            }
        }
        addGeometry(stripNdx, stripCnts);        
    }
    
    public void updateData(GeometryUpdater updater, boolean generateNormals)
    throws CapabilityNotSetException
    {
        
        for(int i=0; i < number; i++)
            geomArray[i].updateData(updater);
        
        // very slow
        if(generateNormals)
        {
            GeometryInfo geomInfo = new GeometryInfo(GeometryInfo.TRIANGLE_ARRAY);
            
            geomInfo.setCoordinates(verts);
            geomInfo.setCoordinateIndices(ndx);
            
            NormalGenerator ng = new NormalGenerator();
            ng.setCreaseAngle(Math.PI);
            ng.generateNormals(geomInfo);
            
            normals = geomInfo.getNormals();
        }    
    }

    // not tested yet !!
    public float getHeight(float x, float z)
    {
        int _x = (int) (x / stride);
        int _z = (int) (z / stride);
        
        Point3f tempPoint = verts[_x + _z*width];
        
        return tempPoint.y;
    }
    
    public float getStride()
    {
        return stride;
    }
    
    public float getTile()
    {
        return tile;
    }
    
    public float getScale()
    {
        return scale;
    }
    
    public int getWidth()
    {
        return width;
    }
}