/*
 * Copyright (c) 2009-2015 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.light;

import com.jme3.bounding.BoundingSphere;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.util.TempVars;
import org.junit.Before;
import org.junit.Test;

/**
 * Test light filtering for various light types.
 * 
 * @author Kirill Vainer
 */
public class LightFilterTest {
    
    private DefaultLightFilter filter;
    private Camera cam;
    private Geometry geom;
    private LightList list;
    
    private void checkFilteredLights(int expected) {
        geom.updateGeometricState();
        filter.setCamera(cam); // setCamera resets the intersection cache
        list.clear();
        filter.filterLights(geom, list);
        assert list.size() == expected;
    }
    
    @Before
    public void setUp() {
        filter = new DefaultLightFilter();
        
        cam = new Camera(512, 512);
        cam.setFrustumPerspective(45, 1, 1, 1000);
        cam.setLocation(Vector3f.ZERO);
        cam.lookAtDirection(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
        filter.setCamera(cam);
        
        Box box = new Box(1, 1, 1);
        geom = new Geometry("geom", box);
        geom.setLocalTranslation(0, 0, 10);
        geom.updateGeometricState();
        list = new LightList(geom);
    }
    
    @Test
    public void testAmbientFiltering() {
        geom.addLight(new AmbientLight());
        checkFilteredLights(1); // Ambient lights must never be filtered
        
        // Test for bounding Sphere
        geom.setModelBound(new BoundingSphere(0.5f, Vector3f.ZERO));
        checkFilteredLights(1); // Ambient lights must never be filtered
    }
    
    @Test
    public void testDirectionalFiltering() {
        geom.addLight(new DirectionalLight(Vector3f.UNIT_Y));
        checkFilteredLights(1); // Directional lights must never be filtered
        
        // Test for bounding Sphere
        geom.setModelBound(new BoundingSphere(0.5f, Vector3f.ZERO));
        checkFilteredLights(1); // Directional lights must never be filtered
    }
    
    @Test
    public void testPointFiltering() {
        PointLight pl = new PointLight(Vector3f.ZERO);
        geom.addLight(pl);
        checkFilteredLights(1); // Infinite point lights must never be filtered
        
        // Light at origin does not intersect geom which is at Z=10
        pl.setRadius(1);
        checkFilteredLights(0);
        
        // Put it closer to geom, the very edge of the sphere touches the box.
        // Still not considered an intersection though.
        pl.setPosition(new Vector3f(0, 0, 8f));
        checkFilteredLights(0);
        
        // And more close - now its an intersection.
        pl.setPosition(new Vector3f(0, 0, 8f + FastMath.ZERO_TOLERANCE));
        checkFilteredLights(1);
        
        // Move the geometry away
        geom.move(0, 0, FastMath.ZERO_TOLERANCE);
        checkFilteredLights(0);
        
        // Test if the algorithm converts the sphere 
        // to a box before testing the collision (incorrect)
        float sqrt3 = FastMath.sqrt(3);
        
        pl.setPosition(new Vector3f(2, 2, 8));
        pl.setRadius(sqrt3);
        checkFilteredLights(0);
        
        // Make it a wee bit larger.
        pl.setRadius(sqrt3 + FastMath.ZERO_TOLERANCE);
        checkFilteredLights(1);
        
        // Rotate the camera so it is up, light is outside frustum.
        cam.lookAtDirection(Vector3f.UNIT_Y, Vector3f.UNIT_Y);
        checkFilteredLights(0);
        
        // ==================================
        // Tests for bounding Sphere
        geom.setModelBound(new BoundingSphere(1f, Vector3f.ZERO));
        geom.setLocalTranslation(0, 0, 2);
        pl.setPosition(new Vector3f(0, 0, 2f));

        // Infinite point lights must never be filtered
        pl.setRadius(0);
        checkFilteredLights(1);
     
        pl.setRadius(1f);
        // Put the light at the very close to the geom,
        // the very edge of the sphere touches the other bounding sphere
        // Still not considered an intersection though.
        pl.setPosition(new Vector3f(0, 0, 0));
        checkFilteredLights(0);

        // And more close - now its an intersection.
        pl.setPosition(new Vector3f(0, 0, 0f + FastMath.ZERO_TOLERANCE));
        checkFilteredLights(1);
               
        geom.setLocalTranslation(0, 0, 0);
        // In this case its an intersection for pointLight v. box
        // But not for pointLight v. sphere
        // Vector3f(0, 0.5f, 0.5f).normalize().mult(2) ~ >= (0.0, 1.4142135, 1.4142135)
        //pl.setPosition(new Vector3f(0, 0.5f, 0.5f).normalizeLocal().multLocal(2 + FastMath.ZERO_TOLERANCE));
        pl.setPosition(new Vector3f(0f, 1.4142135f, 1.4142135f).multLocal(1+FastMath.ZERO_TOLERANCE));
        checkFilteredLights(0);
        
        // Make the distance a wee bit closer, now its an intersection
        //pl.setPosition(new Vector3f(0, 0.5f, 0.5f).normalizeLocal().multLocal(2 - FastMath.ZERO_TOLERANCE));
        pl.setPosition(new Vector3f(0f, 1.4142135f, 1.4142135f).multLocal(1-FastMath.ZERO_TOLERANCE));
        checkFilteredLights(1);
        
        // it's a point light, also test for the other corner
        pl.setPosition(new Vector3f(0f, -1.4142135f, -1.4142135f).multLocal(1-FastMath.ZERO_TOLERANCE));
        checkFilteredLights(0);

    }
    
    @Test
    public void testSpotFiltering() {
        SpotLight sl = new SpotLight(Vector3f.ZERO, Vector3f.UNIT_Z);
        sl.setSpotRange(0);
        geom.addLight(sl);
        checkFilteredLights(1); // Infinite spot lights are only filtered
                                // if the geometry is outside the infinite cone.
        
        TempVars vars = TempVars.get();
        try {
            // The spot is not touching the near plane of the camera yet, 
            // should still be culled.
            sl.setSpotRange(1f - FastMath.ZERO_TOLERANCE);
            assert !sl.intersectsFrustum(cam, vars);
            // should be culled from the geometry's PoV
            checkFilteredLights(0);
            
            // Now it touches the near plane.
            sl.setSpotRange(1f);
            // still culled from the geometry's PoV
            checkFilteredLights(0);
            assert sl.intersectsFrustum(cam, vars);
        } finally {
            vars.release();
        }
        
        // make it barely reach the geometry
        sl.setSpotRange(9f);
        checkFilteredLights(0);
        
        // make it reach the geometry (touching its bound)
        sl.setSpotRange(9f + FastMath.ZERO_TOLERANCE);
        checkFilteredLights(1);
        
        // rotate the cone a bit so it no longer faces the geom
        sl.setDirection(new Vector3f(0.316f, 0, 0.948f).normalizeLocal());
        checkFilteredLights(0);
        
        // extent the range much farther
        sl.setSpotRange(20);
        checkFilteredLights(0);
        
        // Create box of size X=10 (double the extent)
        // now, the spot will touch the box.
        geom.setMesh(new Box(5, 1, 1));
        checkFilteredLights(1);
        
        // ==================================
        // Tests for bounding sphere, with a radius of 1f (in the box geom)
        sl.setPosition(Vector3f.ZERO);
        sl.setDirection(Vector3f.UNIT_Z);
        geom.setLocalTranslation(Vector3f.ZERO);
        geom.setModelBound(new BoundingSphere(1f, Vector3f.ZERO));
       
        // Infinit spot lights are only filtered
        // if the geometry is outside the infinite cone.
        sl.setSpotRange(0);
        checkFilteredLights(1);
       
        //the geommetry is outside the infinit cone (cone direction going away from the geom)
        sl.setPosition(Vector3f.UNIT_Z.mult(1+FastMath.ZERO_TOLERANCE));
        checkFilteredLights(0);
       
        //place the spote ligth in the corner of the box geom, (in order to test bounding sphere)
        sl.setDirection(new Vector3f(1, 1, 0).normalizeLocal());
        geom.setLocalTranslation(0, 0, 10);
        sl.setPosition(sl.getDirection().mult(-2f).add(geom.getLocalTranslation()));

        // make it barely reach the sphere, incorect with a box
        sl.setSpotRange(1f - FastMath.ZERO_TOLERANCE);
        checkFilteredLights(0);
       
        // make it reach the sphere
        sl.setSpotRange(1f + FastMath.ZERO_TOLERANCE);
        checkFilteredLights(1);
       
        // extent the range
        sl.setPosition(Vector3f.ZERO);
        sl.setDirection(Vector3f.UNIT_Z);
        sl.setSpotRange(20);
        checkFilteredLights(1);
               
        // rotate the cone a bit so it no longer faces the geom
        sl.setDirection(new Vector3f(0, 0.3f, 0.7f).normalizeLocal());
        checkFilteredLights(0);
       
        // Create sphere of size X=10 (double the radius)
        // now, the spot will touch the sphere.
        geom.setModelBound(new BoundingSphere(5f, Vector3f.ZERO));
        checkFilteredLights(1);
    }
}
