import java.awt.*;
import java.awt.image.*;
import java.lang.*;


/*

    Orientation du repère :

    Y
    |_ X
   /
  -Z

  */


public class Voxel3 extends java.applet.Applet 
                   implements Runnable 
{    
    private MemoryImageSource backBuffer;
    private int backBufferData[];
    private Image imgBuffer;

    private ReadableImage heightMap;
    private ReadableImage texture;
    private ReadableImage ombresNuages;
    private ReadableImage nuages;

    private boolean isComputing;
    private boolean readyToRun = false;

    private int dimX, dimY;

    private Camera camera = new Camera();

    private int temps;

    private int altitudeNuages;
    private double zoomNuages;
	private int niveauMer;

    // parametres du rayon
    private boolean ombreNuages;
    private boolean ciel;
    private boolean fog;
    private boolean reflexionEau;

    // Optimisation LOD
    private int hauteursRayons[];

    /*
        Fonction modulo
     */

    private int modulo( int numero, int modulo )
    {
        while (numero>=modulo)
        {
            numero-=modulo;
        }

        while (numero<0)
        {
            numero+=modulo;
        }

        return numero;
    }
        

    /*
        Fonction d'initialisation
     */

    public void init(int X, int Y)
    {        
        this.getGraphics().drawString("Please wait ....", 50, 100);
    
        // HeightMap creation
        heightMap = new ReadableImage( "terrain.gif" );

        // Texture creation
        texture = new ReadableImage( "texture.gif" );
        ombresNuages = new ReadableImage( "ombreNuages.gif" );
        nuages = new ReadableImage( "nuages.jpg" );

        // Initialise buffer & resize applet
        dimX = X;
        dimY = Y;

        resize(X,Y);

        backBufferData = new int[dimX*dimY];
        backBuffer = new MemoryImageSource(dimX, dimY, backBufferData, 0, dimX);
        backBuffer.setAnimated(true);

        // Initialise camera
        camera.init(0, 150, 0, -5, 0, 0, 0, 0, 400, 2000, 500);

        // Parametres du rayon
        ombreNuages = true;
        ciel = true;
        fog = true;
        reflexionEau = true;

        altitudeNuages = 200;
        zoomNuages = 0.6;
		niveauMer = 93;
        temps = 0;

        // Optimisation LOD
        hauteursRayons = new int[dimX];
    }

        
    public void update(Graphics g) 
    {
        paint(g);
    }

    public void paint(Graphics g) 
    {
        if( readyToRun == false ) 
        {
            g.drawString("Please wait ....", 50, 100);
        } 
        else 
        {
            calculateImage();
            imgBuffer = createImage(backBuffer);            
            getGraphics().drawImage( imgBuffer, 0, 0, this );
        }
    }

    public void start() 
    {
        readyToRun = false;

        // L'optimisation necessite des tailles divisibles par 4 augmentées de 1
        init(321, 321);
        readyToRun = true;
    }

    synchronized public void stop() 
    {
        notify();
    }
    
	public void lancerReflet( 
        int screenX, int screenY,
        int t,
        double Xdepart,double Ydepart,double Zdepart,
        double RXdepart, double RYdepart, int step)
	{
		// Variable de parcours du rayon : t

		// Variables indiquant ou l'on est actuellement sur le rayon
        double newPosX = Xdepart;
        double newPosY = Ydepart;
        double newPosZ = Zdepart;

		int offset;
		
        int heightMapX, heightMapY;
        
        boolean hasStriken = false;

        double dx = Math.cos(RXdepart)*Math.sin(RYdepart)*step;
        double dz = Math.cos(RXdepart)*Math.cos(RYdepart)*step;
        double dy = -Math.sin(RXdepart)*step;
        
        while(!hasStriken && (t<camera.getFarDistance()))
        {
            //!calculer l'offset
            
            heightMapX = modulo((int)newPosX, heightMap.getIconWidth());
            heightMapY = modulo((int)newPosZ, heightMap.getIconHeight());

            offset = heightMap.getIconWidth()*heightMapY + heightMapX;
            
            if (newPosY < (heightMap.getBuffer()[offset] & 0x000000FF))
            {
                // On calcule la couleur du pixel
                int red = ((texture.getBuffer()[offset] >> 16)& 0x000000FF);
                int green = ((texture.getBuffer()[offset] >> 8)& 0x000000FF);
                int blue = (texture.getBuffer()[offset] & 0x000000FF);
                
                red *= 0.7;
                green *= 0.8;                
                
                hasStriken = true;
                backBufferData[screenY*dimY + screenX] = (255 << 24) | (red << 16) | (green << 8) | blue;
            }
            
            newPosX += dx;
            newPosY += dy;
            newPosZ += dz;
            
            t+=step;
            
        }
        
        if(!hasStriken)
        {
            if (ciel)
            {
                double posZ = Zdepart + altitudeNuages*Math.sin(RYdepart) / Math.tan( RXdepart);
                double posX = Xdepart + altitudeNuages*Math.cos(RYdepart) / Math.tan( RXdepart);
                
                backBufferData[screenY*dimY + screenX] = nuages.getBuffer()[modulo((int)posX, nuages.getIconHeight())*nuages.getIconWidth() + modulo((int)posZ, nuages.getIconWidth())];
            }
            else
            {
                backBufferData[screenY*dimY + screenX] = (255 << 24) | 200;
            }           
        }
    }

    public void lancerRayonLOD( 
        int screenX, int screenY,
        double angleFOVy,
        double Xdepart, double Ydepart, double Zdepart,
        double RXdepart, double RYdepart, 
        int step,
        int distanceMaximum,
        int offsetNuages )
    {
        // Trouver les increments pour se deplacer sur le rayon
        double dx = step*Math.cos( RXdepart )*Math.sin( RYdepart );
        double dy = step*Math.sin( RXdepart );
        double dz = step*Math.cos( RXdepart )*Math.cos( RYdepart );

        // Variables indiquant ou l'on est actuellement sur le rayon
        double newPosX = Xdepart;
        double newPosY = Ydepart;
        double newPosZ = Zdepart;

        // increment lors du changement de ligne dans le backbuffer
        double angleIncrement = -2*angleFOVy / dimY;
        int nombreDeLignesSautees = 0;
    
        // Coordonnees actuelles dans la heightMap
        int heightMapX, heightMapY;

        // coefficient de brouillard
        double fogCoeff = 0.0d;
        double incrementBrouillard = 1.0d/(double)(camera.getFarDistance()-camera.getFogDistance());

        boolean rayHasStriken = false;

        int offset2 = screenY*dimX + screenX;

        for (int t=0; t<distanceMaximum; t+=step)
        {
            heightMapX = modulo((int)newPosX, heightMap.getIconWidth());
            heightMapY = modulo((int)newPosZ, heightMap.getIconHeight());

            int offset = heightMapY * heightMap.getIconWidth() + heightMapX;

            // !! Prevoir un offset au cas ou la texture n'ai pas la meme taille que la
            //    heightmap						
			
			while((newPosY <= niveauMer) && (screenY >= 0))
			{
				lancerReflet( 
                    screenX, screenY, 
                    t, 
                    newPosX, newPosY, newPosZ, 
                    RXdepart+nombreDeLignesSautees*angleIncrement, RYdepart,
                    step);

				rayHasStriken = true;
				offset2-=dimX;
				screenY--;
				newPosY++;
				nombreDeLignesSautees++;
                temps++;
			}

            while ( (newPosY <= (heightMap.getBuffer()[offset] & 0x000000FF)) &&
                    (screenY >= 0) )
            {
                rayHasStriken = true;

				// On calcule la couleur du pixel
                int red = ((texture.getBuffer()[offset] >> 16)& 0x000000FF);
                int green = ((texture.getBuffer()[offset] >> 8)& 0x000000FF);
                int blue = (texture.getBuffer()[offset] & 0x000000FF);                              

                if ( (t > camera.getFogDistance()) && (fog) )
                {
                    red   += 255*fogCoeff;
                    green += 255*fogCoeff;
                    blue  += 255*fogCoeff;                    
                }
                else if (ombreNuages) // les ombres ne s'appliquent qu'en dehors du brouillard
                {
                    if ((ombresNuages.getBuffer()[modulo(offsetNuages+offset, ombresNuages.getIconWidth()*ombresNuages.getIconHeight())] & 0x000000FF) == 0)
                    {
                        red  *= 0.3;
                        green*= 0.3;
                        blue *= 0.3;
                    }                    
                }

                red = (red>255 ? 255 : red);
                green = (green>255 ? 255 : green);
                blue = (blue>255 ? 255 : blue);

                red = (red<0 ? 0 : red);
                green = (green<0 ? 0 : green);
                blue = (blue<0 ? 0 : blue);
                
                backBufferData[offset2] = (255 << 24) | (red << 16) | (green << 8) | blue;
                
                // On remonte d'un pixel sur l'ecran.
                screenY--;
                offset2 -= dimX;
                nombreDeLignesSautees++;
	            newPosY+=1;
            }
            
            // Recalculer dy 
            // ! optim : recalculer seulement si on a changé screenY
            dy = step*Math.sin( RXdepart + nombreDeLignesSautees*angleIncrement);

            // mettre a jour les positions sur le rayon
            newPosX += dx;
            newPosY += dy;
            newPosZ += dz;

            if ( (t > camera.getFogDistance()) && (fog) )
            {
                fogCoeff+=incrementBrouillard;
            }
        }

        if (!rayHasStriken)
        {
            backBufferData[offset2] = 0;
        }

        if (distanceMaximum==camera.getFarDistance())
        {
            // Grand rayon
            hauteursRayons[screenX] = screenY;
        }
        else if (distanceMaximum==camera.getFogDistance())
        {
            // petit rayon (3 .. 7 .. 11)

            // Completer le rayon si necessaire
            if (screenY>(hauteursRayons[screenX-1]+hauteursRayons[screenX+1])/2)
            {
                // premierement par rapport au rayon moyen
                for (int i=screenY; i>=(hauteursRayons[screenX-1]+hauteursRayons[screenX+1])/2; i--)
                {
                    backBufferData[offset2] = backBufferData[offset2-1];
                    offset2-=dimX;
                    screenY--;
                    nombreDeLignesSautees++;
                }
            }
        }
        else if (distanceMaximum==camera.getFogDistance()+1)
        {
            // petit rayon (1 .. 5 .. 9)

            // Completer le rayon si necessaire
            if (screenY>(hauteursRayons[screenX-1]+hauteursRayons[screenX+1])/2)
            {
                // premierement par rapport au rayon moyen
                for (int i=screenY; i>=(hauteursRayons[screenX-1]+hauteursRayons[screenX+1])/2; i--)
                {
                    backBufferData[offset2] = backBufferData[offset2+1];
                    offset2-=dimX;
                    screenY--;
                    nombreDeLignesSautees++;
                }
            }
        }
        else
        {
            // Rayon moyen
            hauteursRayons[screenX] = (hauteursRayons[screenX-2]+hauteursRayons[screenX+2])/2;

            // Completer le rayon si necessaire
            if (screenY>(hauteursRayons[screenX-2]+hauteursRayons[screenX+2])/2)
            {
                for (int i=screenY; i>=(hauteursRayons[screenX-2]+hauteursRayons[screenX+2])/2; i--)
                {
                    if (hauteursRayons[screenX-2]>hauteursRayons[screenX+2])
                    {
                        backBufferData[offset2] = backBufferData[offset2-2];
                    }
                    else
                    {
                        backBufferData[offset2] = backBufferData[offset2+2];
                    }
                    offset2-=dimX;
                    screenY--;
                    nombreDeLignesSautees++;
                }
            }
        }

		//Nuages

        fogCoeff = 0;

        if (ciel)
        {
            int oldScreenY = screenY;
            screenY = 0;          
            offset2 = screenX;

            dy = -step*Math.sin( RXdepart );

            angleIncrement = -angleIncrement;

            newPosX = Xdepart;
            newPosY = Ydepart;
            newPosZ = Zdepart;

            nombreDeLignesSautees = 0;

            for (int t=0; t<camera.getFarDistance(); t+=step)
            {
                heightMapX = modulo((int)(zoomNuages*newPosX), nuages.getIconWidth());
                heightMapY = modulo((int)(zoomNuages*newPosZ), nuages.getIconHeight());
                
                int offset = heightMapY * nuages.getIconWidth() + heightMapX;
                
                while ( (newPosY >= altitudeNuages) &&
                        (screenY <= oldScreenY) )
                {
                    rayHasStriken = true;
                    
                    // On calcule la couleur du pixel
                    int red = ((nuages.getBuffer()[offset] >> 16)& 0x000000FF);
                    int green = ((nuages.getBuffer()[offset] >> 8)& 0x000000FF);
                    int blue = (nuages.getBuffer()[offset] & 0x000000FF);                              
                    
                    if ( (t > camera.getFogDistance()) && (fog) )
                    {
                        red   += 255*fogCoeff;
                        green += 255*fogCoeff;
                        blue  += 255*fogCoeff;
                        
                        red = (red>255 ? 255 : red);
                        green = (green>255 ? 255 : green);
                        blue = (blue>255 ? 255 : blue);
                    }
                    
                    backBufferData[offset2] = (255 << 24) | (red << 16) | (green << 8) | blue;
                    
                    // On remonte d'un pixel sur l'ecran.
                    screenY++;
                    offset2+=dimX;
                    nombreDeLignesSautees++;
                    newPosY-=1;
                }
                
                // Recalculer dy 
                dy = step*Math.sin( -RXdepart + nombreDeLignesSautees*angleIncrement);
                
                // mettre a jour les positions sur le rayon
                newPosX += dx;
                newPosY += dy;
                newPosZ += dz;
                
                if ( (t > camera.getFogDistance()) && (fog) )
                {
                    fogCoeff+=incrementBrouillard;
                }
            }
        }
        else
        {
            for (int i=screenY; i>=0; i--)
            {
                backBufferData[offset2] = (255 << 24) | 200;
                offset2-=dimX;                
                nombreDeLignesSautees++;                
            }
        }
    }

    public void calculateImage()
    {
        camera.update();

        // temps+=5;
        
        int screenX = 0;
        int screenY = dimY-1;

        double angleFOVx = Math.atan( -(double)dimY / (double)(2.0d*(double)camera.getFOV()) );
        double angleFOVy = Math.atan( -(double)dimX / (double)(2.0d*(double)camera.getFOV()) );

        double angleIncrement = 4*2*angleFOVy / dimX;

        double angleActuel = angleFOVy;

        // Lancer les rayons a grande distance (jusqu'a camera.getFarDistance())
        for (screenX=0; screenX<dimX; screenX+=4)
        {
            lancerRayonLOD( 
                screenX, dimY-1,
                angleFOVy,
                camera.getX(), camera.getY(), camera.getZ(), 
                camera.getRXZ() + angleFOVx, 
                camera.getRY() + angleActuel,
                2,
                camera.getFarDistance(),
                modulo(temps, ombresNuages.getIconWidth()) );

            angleActuel += angleIncrement;
        }

        angleActuel = angleFOVy+angleIncrement/2;

        // Lancer les rayons a distance intermediaire (jusqu'a (camera.getFarDistance() + camera.getFogDistance())/2)
        for (screenX=2; screenX<dimX; screenX+=4)
        {
            lancerRayonLOD( 
                screenX, dimY-1,
                angleFOVy,
                camera.getX(), camera.getY(), camera.getZ(), 
                camera.getRXZ() + angleFOVx, 
                camera.getRY() + angleActuel,
                2,
                (camera.getFarDistance() + camera.getFogDistance())/2,
                modulo(temps, ombresNuages.getIconWidth()) );

            angleActuel += angleIncrement;
        }

        angleActuel = angleFOVy+angleIncrement/4;

        // Lancer les rayons a distance faible (jusqu'a camera.getFogDistance())
        for (screenX=1; screenX<dimX; screenX+=4)
        {
            lancerRayonLOD( 
                screenX, dimY-1,
                angleFOVy,
                camera.getX(), camera.getY(), camera.getZ(), 
                camera.getRXZ() + angleFOVx, 
                camera.getRY() + angleActuel,
                2,
                camera.getFogDistance()+1,
                modulo(temps, ombresNuages.getIconWidth()) );

            angleActuel += angleIncrement;
        }  

        angleActuel = angleFOVy+3*angleIncrement/4;

        // Lancer les rayons a distance faible (jusqu'a camera.getFogDistance())
        for (screenX=3; screenX<dimX; screenX+=4)
        {
            lancerRayonLOD( 
                screenX, dimY-1,
                angleFOVy,
                camera.getX(), camera.getY(), camera.getZ(), 
                camera.getRXZ() + angleFOVx, 
                camera.getRY() + angleActuel,
                2,
                camera.getFogDistance(),
                modulo(temps, ombresNuages.getIconWidth()) );

            angleActuel += angleIncrement;
        }
    }
    
    public void run() 
    {
    }
}