/*
 * Copyright (c) 2009-2010 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 jme3tools.optimize;

import com.jme3.bounding.BoundingBox;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.math.Matrix4f;
import com.jme3.math.Ray;
import com.jme3.math.Triangle;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.debug.WireBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public class Octnode {

    static final Vector3f[] extentMult = new Vector3f[]
    {
        new Vector3f( 1, 1, 1), // right top forw
        new Vector3f(-1, 1, 1), // left top forw
        new Vector3f( 1,-1, 1), // right bot forw
        new Vector3f(-1,-1, 1), // left bot forw
        new Vector3f( 1, 1,-1), // right top back
        new Vector3f(-1, 1,-1), // left top back
        new Vector3f( 1,-1,-1), // right bot back
        new Vector3f(-1,-1,-1)  // left bot back
    };

    final BoundingBox bbox;
    final ArrayList<OCTTriangle> tris;
    Geometry[] geoms;
    final Octnode[] children = new Octnode[8];
    boolean leaf = false;
    FastOctnode fastNode;

    public Octnode(BoundingBox bbox, ArrayList<OCTTriangle> tris){
        this.bbox = bbox;
        this.tris = tris;
    }
    
    private BoundingBox getChildBound(int side){
        float extent = bbox.getXExtent() * 0.5f;
        Vector3f center = new Vector3f(bbox.getCenter().x + extent * extentMult[side].x,
                                       bbox.getCenter().y + extent * extentMult[side].y,
                                       bbox.getCenter().z + extent * extentMult[side].z);
        return new BoundingBox(center, extent, extent, extent);
    }

    private float getAdditionCost(BoundingBox bbox, OCTTriangle t){
        if (bbox.intersects(t.get1(), t.get2(), t.get3())){
            float d1 = bbox.distanceToEdge(t.get1());
            float d2 = bbox.distanceToEdge(t.get2());
            float d3 = bbox.distanceToEdge(t.get3());
            return d1 + d2 + d3;
        }
        return Float.POSITIVE_INFINITY;
    }

    private void expandBoxToContainTri(BoundingBox bbox, OCTTriangle t){
        Vector3f min = bbox.getMin(null);
        Vector3f max = bbox.getMax(null);
        BoundingBox.checkMinMax(min, max, t.get1());
        BoundingBox.checkMinMax(min, max, t.get2());
        BoundingBox.checkMinMax(min, max, t.get3());
        bbox.setMinMax(min, max);
    }

    private boolean contains(BoundingBox bbox, OCTTriangle t){
        if (bbox.contains(t.get1()) &&
            bbox.contains(t.get2()) &&
            bbox.contains(t.get3())){
            return true;
        }
        return false;
    }

    public void subdivide(int depth, int minTrisPerNode){
        if (tris == null || depth > 50 || bbox.getVolume() < 0.01f || tris.size() < minTrisPerNode){
            // no need to subdivide anymore
            leaf = true;
            return;
        }

        ArrayList<OCTTriangle> keepTris = new ArrayList<OCTTriangle>();
        ArrayList[] trisForChild = new ArrayList[8];
        BoundingBox[] boxForChild = new BoundingBox[8];
        // create boxes for children
        for (int i = 0; i < 8; i++){
            boxForChild[i] = getChildBound(i);
            trisForChild[i] = new ArrayList<Triangle>();
        }

        for (OCTTriangle t : tris){
            float lowestCost = Float.POSITIVE_INFINITY;
            int lowestIndex = -1;
            int numIntersecting = 0;
            for (int i = 0; i < 8; i++){
                BoundingBox childBox = boxForChild[i];
                float cost = getAdditionCost(childBox, t);
                if (cost < lowestCost){
                    lowestCost = cost;
                    lowestIndex = i;
                    numIntersecting++;
                }
            }
            if (numIntersecting < 8 && lowestIndex > -1){
                trisForChild[lowestIndex].add(t);
                expandBoxToContainTri(boxForChild[lowestIndex], t);
            }else{
                keepTris.add(t);
            }
//            boolean wasAdded = false;
//            for (int i = 0; i < 8; i++){
//                BoundingBox childBox = boxForChild[i];
//                if (contains(childBox, t)){
//                    trisForChild[i].add(t);
//                    wasAdded = true;
//                    break;
//                }
//            }
//            if (!wasAdded){
//                keepTris.add(t);
//            }
        }
        tris.retainAll(keepTris);
        for (int i = 0; i < 8; i++){
            if (trisForChild[i].size() > 0){
                children[i] = new Octnode(boxForChild[i], trisForChild[i]);
                children[i].subdivide(depth + 1, minTrisPerNode);
            }
        }
    }

    public void subdivide(int minTrisPerNode){
        subdivide(0, minTrisPerNode);
    }

    public void createFastOctnode(List<Geometry> globalGeomList){
        fastNode = new FastOctnode();

        if (geoms != null){
            Collection<Geometry> geomsColl = Arrays.asList(geoms);
            List<Geometry> myOptimizedList = GeometryBatchFactory.makeBatches(geomsColl);

            int startIndex = globalGeomList.size();
            globalGeomList.addAll(myOptimizedList);

            fastNode.setOffset(startIndex);
            fastNode.length = myOptimizedList.size();
        }else{
            fastNode.setOffset(0);
            fastNode.length = 0;
        }

        for (int i = 0; i < 8; i++){
            if (children[i] != null){
                children[i].createFastOctnode(globalGeomList);
            }
        }
    }

    public void generateFastOctnodeLinks(Octnode parent, Octnode nextSibling, int side){
        fastNode.setSide(side);
        fastNode.next = nextSibling != null ? nextSibling.fastNode : null;

        // We set the next sibling property by going in reverse order
        Octnode prev = null;
        for (int i = 7; i >= 0; i--){
            if (children[i] != null){
                children[i].generateFastOctnodeLinks(this, prev, i);
                prev = children[i];
            }
        }
        fastNode.child = prev != null ? prev.fastNode : null;
    }

    private void generateRenderSetNoCheck(Set<Geometry> renderSet, Camera cam){
        if (geoms != null){
            renderSet.addAll(Arrays.asList(geoms));
        }
        for (int i = 0; i < 8; i++){
            if (children[i] != null){
                children[i].generateRenderSetNoCheck(renderSet, cam);
            }
        }
    }

    public void generateRenderSet(Set<Geometry> renderSet, Camera cam){
//        generateRenderSetNoCheck(renderSet, cam);

        bbox.setCheckPlane(0);
        cam.setPlaneState(0);
        Camera.FrustumIntersect result = cam.contains(bbox);
        if (result != Camera.FrustumIntersect.Outside){
            if (geoms != null){
                renderSet.addAll(Arrays.asList(geoms));
            }
            for (int i = 0; i < 8; i++){
                if (children[i] != null){
                    if (result == Camera.FrustumIntersect.Inside){
                        children[i].generateRenderSetNoCheck(renderSet, cam);
                    }else{
                        children[i].generateRenderSet(renderSet, cam);
                    }
                }
            }
        }
    }

    public void collectTriangles(Geometry[] inGeoms){
        if (tris.size() > 0){
            List<Geometry> geomsList = TriangleCollector.gatherTris(inGeoms, tris);
            geoms = new Geometry[geomsList.size()];
            geomsList.toArray(geoms);
        }else{
            geoms = null;
        }
        for (int i = 0; i < 8; i++){
            if (children[i] != null){
                children[i].collectTriangles(inGeoms);
            }
        }
    }

    public void renderBounds(RenderQueue rq, Matrix4f transform, WireBox box, Material mat){
        int numChilds = 0;
        for (int i = 0; i < 8; i++){
            if (children[i] != null){
                numChilds ++;
                break;
            }
        }
        if (geoms != null && numChilds == 0){
            BoundingBox bbox2 = new BoundingBox(bbox);
            bbox.transform(transform, bbox2);
//            WireBox box = new WireBox(bbox2.getXExtent(), bbox2.getYExtent(),
//                                      bbox2.getZExtent());
//            WireBox box = new WireBox(1,1,1);

            Geometry geom = new Geometry("bound", box);
            geom.setLocalTranslation(bbox2.getCenter());
            geom.setLocalScale(bbox2.getXExtent(), bbox2.getYExtent(),
                               bbox2.getZExtent());
            geom.updateGeometricState();
            geom.setMaterial(mat);
            rq.addToQueue(geom, Bucket.Opaque);
            box = null;
            geom = null;
        }
        for (int i = 0; i < 8; i++){
            if (children[i] != null){
                children[i].renderBounds(rq, transform, box, mat);
            }
        }
    }

    public final void intersectWhere(Ray r, Geometry[] geoms, float sceneMin, float sceneMax,
                                            CollisionResults results){
        for (OCTTriangle t : tris){
            float d = r.intersects(t.get1(), t.get2(), t.get3());
            if (Float.isInfinite(d))
                continue;

            Vector3f contactPoint = new Vector3f(r.getDirection()).multLocal(d).addLocal(r.getOrigin());
            CollisionResult result = new CollisionResult(geoms[t.getGeometryIndex()],
                                                         contactPoint,
                                                         d,
                                                         t.getTriangleIndex());
            results.addCollision(result);
        }
        for (int i = 0; i < 8; i++){
            Octnode child = children[i];
            if (child == null)
                continue;

            if (child.bbox.intersects(r)){
                child.intersectWhere(r, geoms, sceneMin, sceneMax, results);
            }
        }
    }

}
