/*
 * 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.renderer;

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.material.TechniqueDef;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.queue.GeometryList;
import com.jme3.renderer.queue.OpaqueComparator;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.shape.Box;
import com.jme3.system.TestUtil;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

public class OpaqueComparatorTest {
    
    private final Mesh mesh = new Box(1,1,1);
    private Camera cam = new Camera(1, 1);
    private RenderManager renderManager;
    private AssetManager assetManager;
    private OpaqueComparator comparator = new OpaqueComparator();
    
    @Before
    public void setUp() {
        assetManager = TestUtil.createAssetManager();
        renderManager = TestUtil.createRenderManager();
        comparator.setCamera(cam);
    }
    
    /**
     * Given a correctly sorted list of materials, check if the 
     * opaque comparator can sort a reversed list of them.
     * 
     * Each material will be cloned so that none of them will be equal to each other
     * in reference, forcing the comparator to compare the material sort ID.
     * 
     * E.g. for a list of materials A, B, C, the following list will be generated:
     * <pre>C, B, A, C, B, A, C, B, A</pre>, it should result in
     * <pre>A, A, A, B, B, B, C, C, C</pre>.
     * 
     * @param materials The pre-sorted list of materials to check sorting for.
     */
    private void testSort(Material ... materials) {
        GeometryList gl = new GeometryList(comparator);
        for (int g = 0; g < 5; g++) {
            for (int i = materials.length - 1; i >= 0; i--) {
                Geometry geom = new Geometry("geom", mesh);
                Material clonedMaterial = materials[i].clone();
                
                if (materials[i].getActiveTechnique() != null) {
                    String techniqueName = materials[i].getActiveTechnique().getDef().getName();
                    clonedMaterial.selectTechnique(techniqueName, renderManager);
                } else {
                    clonedMaterial.selectTechnique(TechniqueDef.DEFAULT_TECHNIQUE_NAME, renderManager);
                }
                
                geom.setMaterial(clonedMaterial);
                gl.add(geom);
            }
        }
        gl.sort();
        
        for (int i = 0; i < gl.size(); i++) {
            Material mat = gl.get(i).getMaterial();
            String sortId = Integer.toHexString(mat.getSortId()).toUpperCase();
            System.out.print(sortId + "\t");
            System.out.println(mat);
        }
        
        Set<String> alreadySeen = new HashSet<String>();
        Material current = null;
        for (int i = 0; i < gl.size(); i++) {
            Material mat = gl.get(i).getMaterial();
            if (current == null) {
                current = mat;
            } else if (!current.getName().equals(mat.getName())) {
                assert !alreadySeen.contains(mat.getName());
                alreadySeen.add(current.getName());
                current = mat;
            }
        }
        
        for (int i = 0; i < materials.length; i++) {
            for (int g = 0; g < 5; g++) {
                int index = i * 5 + g;
                Material mat1 = gl.get(index).getMaterial();
                Material mat2 = materials[i];
                assert mat1.getName().equals(mat2.getName()) : 
                       mat1.getName() + " != " + mat2.getName() + " for index " + index;
            }
        }
    }
    
    @Test
    public void testSortByMaterialDef() {
        Material lightingMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material particleMat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
        Material unshadedMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        Material skyMat      = new Material(assetManager, "Common/MatDefs/Misc/Sky.j3md");
        
        lightingMat.setName("MatLight");
        particleMat.setName("MatParticle");
        unshadedMat.setName("MatUnshaded");
        skyMat.setName("MatSky");
        testSort(skyMat, lightingMat, particleMat, unshadedMat);
    }
    
    @Test
    public void testSortByTechnique() {
        Material lightingMatDefault = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingPreShadow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingPostShadow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatPreNormalPass = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatGBuf = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatGlow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        
        lightingMatDefault.setName("TechDefault");
        lightingMatDefault.selectTechnique(TechniqueDef.DEFAULT_TECHNIQUE_NAME, renderManager);
        
        lightingPostShadow.setName("TechPostShad");
        lightingPostShadow.selectTechnique("PostShadow", renderManager);
        
        lightingPreShadow.setName("TechPreShad");
        lightingPreShadow.selectTechnique("PreShadow", renderManager);
        
        lightingMatPreNormalPass.setName("TechNorm");
        lightingMatPreNormalPass.selectTechnique("PreNormalPass", renderManager);
        
        lightingMatGBuf.setName("TechGBuf");
        lightingMatGBuf.selectTechnique("GBuf", renderManager);
        
        lightingMatGlow.setName("TechGlow");
        lightingMatGlow.selectTechnique("Glow", renderManager);
        
        testSort(lightingMatGlow, lightingPreShadow, lightingMatPreNormalPass,
                 lightingMatDefault, lightingPostShadow, lightingMatGBuf);
    }
    
    @Test(expected = AssertionError.class)
    public void testNoSortByParam() {
        Material sameMat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        Material sameMat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        
        sameMat1.setName("MatRed");
        sameMat1.setColor("Color", ColorRGBA.Red);
        
        sameMat2.setName("MatBlue");
        sameMat2.setColor("Color", ColorRGBA.Blue);
        
        testSort(sameMat1, sameMat2);
    }
    
    private Texture createTexture(String name) {
        ByteBuffer bb = BufferUtils.createByteBuffer(3);
        Image image = new Image(Format.RGB8, 1, 1, bb, ColorSpace.sRGB);
        Texture2D texture = new Texture2D(image);
        texture.setName(name);
        return texture;
    }
    
    @Test
    public void testSortByTexture() {
        Material texture1Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        Material texture2Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        Material texture3Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        
        Texture tex1 = createTexture("A");
        tex1.getImage().setId(1);
        
        Texture tex2 = createTexture("B");
        tex2.getImage().setId(2);
        
        Texture tex3 = createTexture("C");
        tex3.getImage().setId(3);
        
        texture1Mat.setName("TexA");
        texture1Mat.setTexture("ColorMap", tex1);
        
        texture2Mat.setName("TexB");
        texture2Mat.setTexture("ColorMap", tex2);
        
        texture3Mat.setName("TexC");
        texture3Mat.setTexture("ColorMap", tex3);
        
        testSort(texture1Mat, texture2Mat, texture3Mat);
    }
    
    @Test
    public void testSortByShaderDefines() {
        Material lightingMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatVColor = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatVLight = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatTC = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatVColorLight = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material lightingMatTCVColorLight = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        
        lightingMat.setName("DefNone");
        
        lightingMatVColor.setName("DefVC");
        lightingMatVColor.setBoolean("UseVertexColor", true);
        
        lightingMatVLight.setName("DefVL");
        lightingMatVLight.setBoolean("VertexLighting", true);
        
        lightingMatTC.setName("DefTC");
        lightingMatTC.setBoolean("SeparateTexCoord", true);
        
        lightingMatVColorLight.setName("DefVCVL");
        lightingMatVColorLight.setBoolean("UseVertexColor", true);
        lightingMatVColorLight.setBoolean("VertexLighting", true);
        
        lightingMatTCVColorLight.setName("DefVCVLTC");
        lightingMatTCVColorLight.setBoolean("UseVertexColor", true);
        lightingMatTCVColorLight.setBoolean("VertexLighting", true);
        lightingMatTCVColorLight.setBoolean("SeparateTexCoord", true);
        
        testSort(lightingMat, lightingMatVColor, lightingMatVLight,
                 lightingMatVColorLight, lightingMatTC, lightingMatTCVColorLight);
    }
    
    @Test
    public void testSortByAll() {
        Material matBase1 = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        Material matBase2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        
        Texture texBase = createTexture("BASE");
        texBase.getImage().setId(1);
        Texture tex1 = createTexture("1");
        tex1.getImage().setId(2);
        Texture tex2 = createTexture("2");
        tex2.getImage().setId(3);
        
        matBase1.setName("BASE");
        matBase1.selectTechnique(TechniqueDef.DEFAULT_TECHNIQUE_NAME, renderManager);
        matBase1.setBoolean("UseVertexColor", true);
        matBase1.setTexture("DiffuseMap", texBase);
        
        Material mat1100 = matBase1.clone();
        mat1100.setName("1100");
        mat1100.selectTechnique("PreShadow", renderManager);
        
        Material mat1101 = matBase1.clone();
        mat1101.setName("1101");
        mat1101.selectTechnique("PreShadow", renderManager);
        mat1101.setTexture("DiffuseMap", tex1);
        
        Material mat1102 = matBase1.clone();
        mat1102.setName("1102");
        mat1102.selectTechnique("PreShadow", renderManager);
        mat1102.setTexture("DiffuseMap", tex2);
        
        Material mat1110 = matBase1.clone();
        mat1110.setName("1110");
        mat1110.selectTechnique("PreShadow", renderManager);
        mat1110.setFloat("AlphaDiscardThreshold", 2f);
        
        Material mat1120 = matBase1.clone();
        mat1120.setName("1120");
        mat1120.selectTechnique("PreShadow", renderManager);
        mat1120.setBoolean("UseInstancing", true);
        
        Material mat1121 = matBase1.clone();
        mat1121.setName("1121");
        mat1121.selectTechnique("PreShadow", renderManager);
        mat1121.setBoolean("UseInstancing", true);
        mat1121.setTexture("DiffuseMap", tex1);
        
        Material mat1122 = matBase1.clone();
        mat1122.setName("1122");
        mat1122.selectTechnique("PreShadow", renderManager);
        mat1122.setBoolean("UseInstancing", true);
        mat1122.setTexture("DiffuseMap", tex2);
        
        Material mat1140 = matBase1.clone();
        mat1140.setName("1140");
        mat1140.selectTechnique("PreShadow", renderManager);
        mat1140.setFloat("AlphaDiscardThreshold", 2f);
        mat1140.setBoolean("UseInstancing", true);
        
        Material mat1200 = matBase1.clone();
        mat1200.setName("1200");
        mat1200.selectTechnique("PostShadow", renderManager);
        
        Material mat1210 = matBase1.clone();
        mat1210.setName("1210");
        mat1210.selectTechnique("PostShadow", renderManager);
        mat1210.setFloat("AlphaDiscardThreshold", 2f);
        
        Material mat1220 = matBase1.clone();
        mat1220.setName("1220");
        mat1220.selectTechnique("PostShadow", renderManager);
        mat1220.setBoolean("UseInstancing", true);
        
        Material mat2000 = matBase2.clone();
        mat2000.setName("2000");
        
        testSort(mat1100, mat1101, mat1102, mat1110, 
                 mat1120, mat1121, mat1122, mat1140, 
                 mat1200, mat1210, mat1220, mat2000);
    }
}
