// This code example is created for educational purpose
// by Thorsten Thormaehlen (contact: www.thormae.de).
// It is distributed without any warranty.

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Random;

import javax.imageio.ImageIO;
import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLCapabilities;
import com.jogamp.opengl.GLEventListener;
import com.jogamp.opengl.GLProfile;
import com.jogamp.opengl.awt.GLCanvas;
import com.jogamp.opengl.glu.GLU;
import javax.swing.JFrame;

import com.jogamp.opengl.util.FPSAnimator;

class Renderer {

  private GLU glu = new GLU();
  
  public float t = 0.0f;
  private int texID = 0;
  private float[] terrain;
  
  public void init(GLAutoDrawable d) {
    GL2 gl = d.getGL().getGL2(); // get the OpenGL 2 graphics context
    gl.glEnable(GL2.GL_DEPTH_TEST);
    
    String[] filenames = new String[6];
    filenames[0] = "deep_water.png";
    filenames[1] = "shallow_water.png";
    filenames[2] = "shore.png";
    filenames[3] = "fields.png";
    filenames[4] = "rocks.png";
    filenames[5] = "snow.png";
    
    texID = loadTexture3D(d, filenames);
   
    rebuildTerrain();
  }
  
  public void resize(GLAutoDrawable d, int w, int h) {
    GL2 gl = d.getGL().getGL2(); // get the OpenGL 2 graphics context
    gl.glViewport(0, 0, w, h);
    gl.glMatrixMode(GL2.GL_PROJECTION);
    gl.glLoadIdentity();
    glu.gluPerspective (30.0, (float)w/(float)h, 0.1, 50.0);
  }
  
  public void display(GLAutoDrawable d) {
    GL2 gl = d.getGL().getGL2();  // get the OpenGL 2 graphics context
    gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);

    gl.glMatrixMode(GL2.GL_MODELVIEW);
    gl.glLoadIdentity();
    // set camera
    glu.gluLookAt(1.5, -1.0, 1.5, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
    // draw scene
    gl.glRotatef(t, 0.0f, 0.0f, 1.0f);
    drawTerrain(d);
  }

  public void rebuildTerrain() {

    //create random values
    int dim = 40;
    terrain = new float[dim*dim];

    Random generator = new Random();
    for(int r=0; r < dim*dim; r++) {
      terrain[r] =  generator.nextFloat();
    }

    if(true) { // generate smooth terrain values
      float[] smoothTerrain = new float[dim*dim];
      for(int k=0; k < 5; k++){
        float maxVal = 0.0f;
        float minVal = 1.0f;
        for(int x = 0; x < dim; x++) {
          for(int y = 0; y < dim; y++) {
            if(x == 0 || x == dim-1) terrain[x*dim+y] = 0.0f;
            else if (y == 0 || y == dim-1) terrain[x*dim+y] = 0.0f;
            else {
              float a = 0.0f;
              int counter = 0;
              for(int s=-1; s <= 1; s++) {
                for(int r=-1; r <= 1; r++) {
                  a += terrain[(x+s)*dim+(y+r)];
                  counter++;
                }
              }
              float val = a / (float)counter;
              smoothTerrain[x*dim+y] = val;
              if(val > maxVal) maxVal = val;
              if(val < minVal) minVal = val;
            }
          }
        }
        for(int r=0; r < dim*dim; r++) terrain[r] = (smoothTerrain[r] - minVal) / (maxVal-minVal);
      }
    }
  }
  
  private void drawTerrain(GLAutoDrawable d) {
    GL2 gl = d.getGL().getGL2();  // get the OpenGL 2 graphics context
    
    gl.glEnable(GL2.GL_TEXTURE_3D);

    gl.glBindTexture(GL2.GL_TEXTURE_3D, texID);
    gl.glColor3f(1.0f,0.0f,0.0f);
    int dim = (int)Math.sqrt(terrain.length);
    float maxHeight = 0.2f;
    float texHeight = 0.9f;
    for(int x = 1; x < dim; x++) {
      for(int y = 1; y < dim; y++) {
        gl.glBegin(GL2.GL_POLYGON);
        gl.glTexCoord3f((float)(x-1)/(float)(dim),
                        (float)(y-1)/(float)(dim),
                        terrain[(x-1)*dim+(y-1)]*texHeight);
        gl.glVertex3f((float)(x-1)/(float)(dim)-0.5f,
                      (float)(y-1)/(float)(dim)-0.5f,
                      terrain[(x-1)*dim+(y-1)]*maxHeight);
        gl.glTexCoord3f((float)(x)/(float)(dim),
                        (float)(y-1)/(float)(dim),
                        terrain[x*dim+(y-1)]*texHeight);
        gl.glVertex3f((float)(x)/(float)(dim)-0.5f,
                      (float)(y-1)/(float)(dim)-0.5f,
                      terrain[x*dim+(y-1)]*maxHeight);
        gl.glTexCoord3f((float)(x)/(float)(dim),
                        (float)(y)/(float)(dim),
                        terrain[x*dim+y]*texHeight);
        gl.glVertex3f((float)(x)/(float)(dim)-0.5f,
                      (float)(y)/(float)(dim)-0.5f,
                      terrain[x*dim+y]*maxHeight);
        gl.glTexCoord3f((float)(x-1)/(float)(dim),
                        (float)(y)/(float)(dim),
                        terrain[(x-1)*dim+y]*texHeight);
        gl.glVertex3f((float)(x-1)/(float)(dim)-0.5f,
                      (float)(y)/(float)(dim)-0.5f,
                      terrain[(x-1)*dim+y]*maxHeight);
        gl.glEnd();
      }
    }
    gl.glDisable(GL2.GL_TEXTURE_3D);
  }
  
  
  // returns a valid textureID on success, otherwise 0
  private int loadTexture3D(GLAutoDrawable d, String[] filenames) {
    GL2 gl = d.getGL().getGL2(); // get the OpenGL 2 graphics context

    int width = 0;
    int height = 0;
    int depth = filenames.length;
    int level = 0;
    int border = 0;
    ByteBuffer buffer = null;
    
  
    // pack 2D images subsequently into a large 3D buffer
    for(int i=0; i < depth; i++) {
      try{
        // open file
        FileInputStream fileInputStream = new FileInputStream(new File(filenames[i]));

        // read image
        BufferedImage bufferedImage = ImageIO.read(fileInputStream);
        fileInputStream.close();
       
        width = bufferedImage.getWidth();
        height = bufferedImage.getHeight();

        
        // convert image to ByteBuffer
        int[] pixelIntData = new int[width * height];
        bufferedImage.getRGB(0, 0, width, height, pixelIntData, 0, width);
        
        if(i==0) {
          //allocate memory
          buffer = ByteBuffer.allocateDirect(pixelIntData.length * 4 * depth);
          buffer.order(ByteOrder.nativeOrder());
        }
        for(int k=0; k < pixelIntData.length; k++) {
          buffer.put((byte)(pixelIntData[k]>>> 16));
          buffer.put((byte)(pixelIntData[k]>>> 8));
          buffer.put((byte)pixelIntData[k]);
          buffer.put((byte)(pixelIntData[k]>>> 24));
        }
        
      } catch( FileNotFoundException e) {
        System.out.println("Can not find texture data file " + filenames[i]);
      } catch(IOException e) {
        e.printStackTrace( );
      }    
    }
    
    if(buffer != null && width > 0 && height > 0) {
      buffer.rewind();

      // data is aligned in byte order
      gl.glPixelStorei(GL2.GL_UNPACK_ALIGNMENT, 1);

      //request textureID
      final int[] textureID = new int[1];
      gl.glGenTextures( 1, textureID, 0);

      // bind texture
      gl.glBindTexture(GL2.GL_TEXTURE_3D, textureID[0]);

      //parameters the define how to warp the texture
      gl.glTexParameteri(GL2.GL_TEXTURE_3D, GL2.GL_TEXTURE_WRAP_S, GL2.GL_CLAMP_TO_BORDER);
      gl.glTexParameteri(GL2.GL_TEXTURE_3D, GL2.GL_TEXTURE_WRAP_T, GL2.GL_CLAMP_TO_BORDER);
      gl.glTexParameteri(GL2.GL_TEXTURE_3D, GL2.GL_TEXTURE_WRAP_R, GL2.GL_CLAMP_TO_BORDER);
      float[] borderColor = {0.0f,0.0f,0.0f,1.0f};
      gl.glTexParameterfv(GL2.GL_TEXTURE_3D, GL2.GL_TEXTURE_BORDER_COLOR, borderColor, 0);

      //define how to filter the texture
      gl.glTexParameteri (GL2.GL_TEXTURE_3D, GL2.GL_TEXTURE_MAG_FILTER, GL2.GL_LINEAR);
      gl.glTexParameteri (GL2.GL_TEXTURE_3D,GL2. GL_TEXTURE_MIN_FILTER, GL2.GL_LINEAR);

      //texture colors should replace the original color values
      gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_REPLACE); //GL_MODULATE

      // specify the 2D texture map
      gl.glTexImage3D(GL2.GL_TEXTURE_3D, level, GL2.GL_RGB, width, height, depth, border, GL2.GL_RGBA, GL2.GL_UNSIGNED_BYTE, buffer);
 
      return textureID[0];
    }
    
    
    return 0;
  }
}

class MyGui extends JFrame implements GLEventListener {

  private Renderer renderer;
  
  public void createGUI() {
    setTitle("Press 1 to generate a new random terrain");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   
    GLProfile glp = GLProfile.getDefault();
    GLCapabilities caps = new GLCapabilities(glp);
    GLCanvas canvas = new GLCanvas(caps);
    setSize(320, 320);
    
    getContentPane().add(canvas);
    final FPSAnimator ani = new FPSAnimator(canvas, 60, true);
    canvas.addGLEventListener(this);
    setVisible(true);
    renderer = new Renderer();
    
    canvas.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent event) {
        boolean redraw = false;
        switch(event.getKeyCode()) {
          case '1':
            renderer.rebuildTerrain();
            redraw = true;
            break;
        }
        if(redraw) { // done automatically
        }
      }
    });
    
    
    ani.start();
  }

  @Override
  public void init(GLAutoDrawable d) { 
    renderer.init(d); 
  }

  @Override
  public void reshape(GLAutoDrawable d, int x, int y, int width, int height) {
    renderer.resize(d, width, height);
  }

  @Override
  public void display(GLAutoDrawable d) {
    float offset = 1.0f;
    renderer.t += offset;
    renderer.display(d);
  }

  @Override
  public void dispose(GLAutoDrawable d) { 
  }

}

public class Texture3D {
  public static void main(String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        MyGui myGUI = new MyGui();
        myGUI.createGUI();
      }
    });
  }
}
