import java.awt.*;
import java.awt.image.*;
import java.lang.*;


/*

    Orientation du repère :

    Y
    |_ X
   /
  -Z

  */


public class Voxel 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 readyToRun = false;

    private int dimX, dimY;

    private Camera camera = new Camera();

    private int temps;

    //pour les nuages
    private int altitudeNuages;
    private double zoomNuages;

    //pour la mer
    private int niveauMer;
    private double vaguesX[],vaguesZ[];
    private double houle;

    // parametres du rayon
    private boolean ombreNuages;
    private boolean ciel;
    private boolean fog;
    private boolean reflexionEau;
    private boolean flares;

    //Pour le Lens Flare
    private ReadableImage shine1, flare1, flare5;
    private double soleilRy, soleilRxz;


    /*
        Fonction modulo
     ---------------------*/

    private int modulo( int numero, int modulo )
    {
        while (numero>=modulo)
        {
            numero-=modulo;
        }

        while (numero<0)
        {
            numero+=modulo;
        }

        return numero;
    }
    






    /*
	Fonction hypot (pour le calcul des vagues)
    -------------------------------------------------*/

    public double hypot(double x,double y)
    {
	return Math.sqrt(x*x + y*y);
    }






    /*
	Fonction booleenne soleilDansLeChampDeVision
    ---------------------------------------------------*/

    public boolean soleilDansLeChampDeVision()
    {
	double angleFOVx = Math.atan( dimY*3 / (2*camera.getFOV()) );
        double angleFOVy = Math.atan( dimX*3 / (2*camera.getFOV()) );

	if	(
			(
				(camera.getRY() < soleilRy + angleFOVx)
				&& 
				(camera.getRY() > soleilRy - angleFOVx)
			)
		&&
			(
				(camera.getRXZ() < soleilRxz + angleFOVy)
				&&
				(camera.getRXZ() > soleilRxz - angleFOVy)
			)
	   )
	{
		return true;
	}
	else return false;
    }








    /*
        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.02, 0,
		    0, 0, 
		    400, 1500, 500);

        // Parametres du rayon
        ombreNuages = true;
        ciel = true;
        fog = true;
        reflexionEau = true;
        flares = true;

        altitudeNuages = 200;
        zoomNuages = 0.6;


	//Paramètres de l'eau
	niveauMer = 40;
	houle = 0.02;

	vaguesX = new double[dimX*dimY];
	vaguesZ = new double[dimX*dimY];
	int off=0; 
	  for (int j=0;j<dimX;j++) { 
	    for (int i=0;i<dimY;i++) { 
	      double x = (double)i; 
		  double y = (double)j; 
	      vaguesX[off]=		2*(Math.sin(x/20) + Math.sin(x*y/2000) 
	                       + Math.sin((x+y)/100) + Math.sin((y-x)/70) + Math.sin((x+4*y)/70) 
	                       + 2*Math.sin(hypot(256-x,(150-y/8))/40)) ; 
	      vaguesZ[off]=		Math.cos(x/31) + Math.cos(x*y/1783) + 
	                       + 2*Math.cos((x+y)/137) + Math.cos((y-x)/55) + 2*Math.cos((x+8*y)/57) 
	                       + Math.cos(hypot(384-x,(274-y/9))/51) ; 
	      off++; 
	    } 
	  } 

	// creation des Flares
	shine1 = new ReadableImage("Shine1.gif");
	flare1 = new ReadableImage("Flare1.gif");
	flare5 = new ReadableImage("Flare5.gif");

	soleilRy = 0;
	soleilRxz = 0.2;

        temps = 0;
    }

        






    /*
	Fonction d'affichage du buffer
    -------------------------------------*/

    public void paint(Graphics g) 
    {
        if( readyToRun == false ) 
        {
            g.drawString("Please wait ....", 50, 100);
        } 
        else 
        {
            calculateImage();   
            getGraphics().drawImage( createImage(backBuffer), 0, 0, this );
        }
    }





   /*
	Fonction d'affichage d'un halo lumineux
   -------------------------------------------------*/

   public void paintFlare(int x, int y, int seuil, int lum, double coefJaune, ReadableImage flare)
   {
	int flareWidth = flare.getIconWidth();
	int flareHeight = flare.getIconHeight();
	int xDepart = x - flareWidth/2;
	int yDepart = y - flareHeight/2;
	int offset = yDepart*dimX+xDepart;

	for (int j=0; j<flareHeight ; j+=1)
	{	
		for (int i=0; i<flareWidth ;i+=1)
		{	
			if(	(xDepart+i >= 0)
				&&(xDepart+i<dimX)
				&&(yDepart+i >= 0)
				&&(yDepart+i<dimY)
				)
			{
				int red = ((flare.getBuffer()[i + flareWidth*j] >> 16)& 0x000000FF);
				int green = ((flare.getBuffer()[i + flareWidth*j] >> 8)& 0x000000FF);
				int blue = (flare.getBuffer()[i + flareWidth*j] & 0x000000FF);
				int intensite = (red+green+blue)/3;

				if (intensite > seuil)
				{
					offset = (yDepart+j)*dimX+xDepart+i;
					
					int redBuff = ((backBufferData[offset] >> 16)& 0x000000FF);
					int greenBuff = ((backBufferData[offset] >> 8)& 0x000000FF);
					int blueBuff = (backBufferData[offset] & 0x000000FF);
					
					//on éclaircit les pixels en fonction de la marge intensite - seuil
					int k = 0;
					while((k<(intensite-seuil))&&(redBuff<255)&&(greenBuff<255)&&(blueBuff<255))
					{
						redBuff  = Math.min(redBuff+lum,255);
						greenBuff = Math.min(greenBuff+lum,255);
						blueBuff = Math.min(blueBuff+lum,255);
						k++;	
					}
					
					//on donne une teinte jaune au flare
					blueBuff *=(1-coefJaune);

					backBufferData[offset] = (255 << 24) | (redBuff << 16) | (greenBuff << 8) | (blueBuff);
				}
			}			
		}
	}
   }





    /*
	Gestion de l'applet
    --------------------------*/


    public void start() 
    {
        readyToRun = false;
        init(320, 320);
        readyToRun = true;
    }

    synchronized public void stop() 
    {
        notify();
    }


    public void update(Graphics g) 
    {
        paint(g);
    }






    /*
	Lancement d'un rayon
    ----------------------------
	(un "rayon" calcul les pixels sur toute une colonne)*/
    

    public void lancerRayon( 
        int screenX, int screenY,
        double angleFOVy,
        double Xdepart, double Ydepart, double Zdepart,
        double RXdepart, double RYdepart, int step,
        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<camera.getFarDistance(); 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,
				    offsetNuages);

				rayHasStriken = true;
				offset2-=dimX;
				screenY--;
				newPosY++;
				nombreDeLignesSautees++;
			}

            while ( (newPosY <= (heightMap.getBuffer()[offset] & 0x000000FF)/2) &&
                    (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;
                    
                    red = (red>255 ? 255 : red);
                    green = (green>255 ? 255 : green);
                    blue = (blue>255 ? 255 : blue);
                }
                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<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 
            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] = 0xFFFFFFFF;
        }

	//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;
                }
            }

	    for (int i=screenY; i<=oldScreenY; i++)
		{
		backBufferData[offset2] = 0xFFFFFFFF;
		offset2+=dimX;
		}
        }
        else
        {
            for (int i=screenY; i>=0; i--)
            {
                backBufferData[offset2] = (255 << 24) | 200;
                offset2-=dimX;                
                nombreDeLignesSautees++;                
            }
        }        
    }






    /*
	Lancement du reflet d'un rayon qui arrive sur l'eau
    ----------------------------------------------------------*/


    public void lancerReflet(	
	int screenX, int screenY,
	int t,
	double Xdepart,double Ydepart,double Zdepart,
	double RXZdepart, double RYdepart, int step,
	int offsetNuages)
    {
	// 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 angleVaguesXZ =	houle*
				  (vaguesX[Math.abs(((int)(Xdepart*50+temps)) % (dimX*dimY-1))]
				  +vaguesZ[Math.abs(((int)(Zdepart*50+3*temps)) % (dimX*dimY-1))]);

	RXZdepart = -RXZdepart + angleVaguesXZ;

	double dx = Math.cos(RXZdepart)*Math.sin(RYdepart)*step;
	double dz = Math.cos(RXZdepart)*Math.cos(RYdepart)*step;
	double dy = Math.sin(RXZdepart)*step;

		

	while(!hasStriken && ((int)newPosY < altitudeNuages) && (t<camera.getFarDistance()))
	{
		//!calculer l'offset

		heightMapX = (int)(newPosX) & (heightMap.getIconWidth()-1);
		heightMapY = (int)(newPosZ) & (heightMap.getIconHeight()-1);
		offset = heightMap.getIconWidth()*heightMapY + heightMapX;

		if (newPosY < (heightMap.getBuffer()[offset] & 0x000000FF)/2)
		{
				
			// 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.4;
			green *= 0.6;

			hasStriken = true;
			backBufferData[screenY*dimY + screenX] = (255 << 24) | (red << 16) | (green << 8) | blue;

		}
		
		// On avance sur le rayon-reflet
		newPosX += dx;
		newPosY += dy;
		newPosZ += dz;

		t+=step;

	}

	if(!hasStriken)
	{
		double posZ = Zdepart + 0.1*(altitudeNuages - Ydepart)*Math.sin(RYdepart) / Math.tan( RXZdepart);
	        double posX = Xdepart + 0.1*(altitudeNuages - Ydepart)*Math.cos(RYdepart) / Math.tan( RXZdepart);

	        posZ = (int)(posZ) & (nuages.getIconWidth()-1);
		posX = (int)(posX) & (nuages.getIconHeight()-1);

		backBufferData[screenY*dimY + screenX] = nuages.getBuffer()[(int)posX*nuages.getIconWidth()+(int)posZ];

	}
    }






    /*
	Calcul de tous les pixels de l'image
    -------------------------------------------
	lance tous les rayon nécessaires pour remplir l'écran
	et affiche tous les halos du lens flare*/


    public void calculateImage()
    {
        camera.update();

        temps+=5;
        


	//lancement de tous les rayons
        int screenX = 0;
        int screenY = dimY-1;

        double angleFOVy = Math.atan( -(double)dimY / (double)(2.0d*(double)camera.getFOV()) );
        double angleFOVx = Math.atan( -(double)dimX / (double)(2.0d*(double)camera.getFOV()) );

        double angleIncrement = 2*angleFOVx / dimX;

        double angleActuel = angleFOVx;

        for (screenX=0; screenX<dimX; screenX++)
        {
            lancerRayon( 
                screenX, dimY-1,
                angleFOVx,
                camera.getX(), camera.getY(), camera.getZ(), 
                camera.getRXZ() + angleFOVy, 
                camera.getRY() + angleActuel,
                2,
                modulo(temps, ombresNuages.getIconWidth()) );

            angleActuel += angleIncrement;
        }

        if (flares)
	{
		//Affichage du lens flare
		if (soleilDansLeChampDeVision())
		{	
			int soleilScreenX,soleilScreenY;
			soleilScreenX = (int)(dimX/2 + Math.tan(camera.getRY() - soleilRy)*300);
			soleilScreenY = (int)(dimY/2 + Math.tan(camera.getRXZ() - soleilRxz)*300);
			paintFlare(soleilScreenX, soleilScreenY, 190, 255, 0.2, flare1);
			
			double coefPos;
			int flareX, flareY;
	
			coefPos = 2;
			flareX = (int)(dimX/2 + (soleilScreenX-dimX/2)/coefPos);
			flareY = (int)(dimY/2 + (soleilScreenY-dimY/2)/coefPos);
			paintFlare(flareX, flareY, 200, 2, 0.05, flare1);
	
			coefPos = -8;
			flareX = (int)(dimX/2 + (soleilScreenX-dimX/2)/coefPos);
			flareY = (int)(dimY/2 + (soleilScreenY-dimY/2)/coefPos);
			paintFlare(flareX, flareY, 230, 2, 0.05, flare5);
	
			coefPos = -4;
			flareX = (int)(dimX/2 + (soleilScreenX-dimX/2)/coefPos);
			flareY = (int)(dimY/2 + (soleilScreenY-dimY/2)/coefPos);
			paintFlare(flareX, flareY, 170, 2, 0.07, flare1);
	
			coefPos = -1.5;
			flareX = (int)(dimX/2 + (soleilScreenX-dimX/2)/coefPos);
			flareY = (int)(dimY/2 + (soleilScreenY-dimY/2)/coefPos);
			paintFlare(flareX, flareY, 200, 2, 0.05, flare1);	
		}
	}
    }
    






    public void run() 
    {
    }
}