package electrosphere.util.ds;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import org.joml.Vector3d;

import annotations.FastTest;
import electrosphere.util.ds.Octree.OctreeNode;

/**
 * Unit testing for the octree implementation
 */
public class OctreeTests {
    
    /**
     * Creates an octree
     */
    @FastTest
    public void testCreateOctree(){
        new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
    }

    /**
     * Add a single leaf
     */
    @FastTest
    public void testAddNode(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
        octree.addLeaf(new Vector3d(1,1,1), 1);
        //
        //validate the tree
        //
        List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
        openSet.add(octree.getRoot());
        while(openSet.size() > 0){
            OctreeNode<Integer> current = openSet.remove(0);
            if(current.isLeaf()){
                assertNotNull(current.getData());
            } else {
                assertNull(current.getData());
                for(OctreeNode<Integer> child : current.getChildren()){
                    if(child != null && child.getParent() != current){
                        fail("Child not attached to parent!");
                    }
                    if(child != null){
                        openSet.add(child);
                    }
                }
            }
        }
    }

    /**
     * Add two leaves in a line
     */
    @FastTest
    public void testAddTwo(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
        octree.addLeaf(new Vector3d(1,1,1), 1);
        octree.addLeaf(new Vector3d(2,1,1), 2);
        //
        //validate the tree
        //
        List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
        openSet.add(octree.getRoot());
        while(openSet.size() > 0){
            OctreeNode<Integer> current = openSet.remove(0);
            if(current.isLeaf()){
                assertNotNull(current.getData());
            } else {
                assertNull(current.getData());
                for(OctreeNode<Integer> child : current.getChildren()){
                    if(child != null && child.getParent() != current){
                        fail("Child not attached to parent!");
                    }
                    if(child != null){
                        openSet.add(child);
                    }
                }
            }
        }
        //
        //Specific, expected values to check. Verifies that the octree construct itself correctly
        //
        OctreeNode<Integer> current = octree.getRoot().getChildren().get(0);
        assertEquals(8, current.getLocation().x);
        assertEquals(8, current.getLocation().y);
        assertEquals(8, current.getLocation().z);
        assertTrue(!current.isLeaf());
        assertEquals(1, current.getNumChildren());

        current = current.getChildren().get(0);
        assertEquals(4, current.getLocation().x);
        assertEquals(4, current.getLocation().y);
        assertEquals(4, current.getLocation().z);
        assertTrue(!current.isLeaf());
        assertEquals(1, current.getNumChildren());

        current = current.getChildren().get(0);
        assertEquals(2, current.getLocation().x);
        assertEquals(2, current.getLocation().y);
        assertEquals(2, current.getLocation().z);
        assertTrue(!current.isLeaf());
        assertEquals(2, current.getNumChildren());

        OctreeNode<Integer> leaf1 = current.getChildren().get(0);
        OctreeNode<Integer> leaf2 = current.getChildren().get(1);
        assertTrue(leaf1.isLeaf());
        assertTrue(leaf2.isLeaf());
    }

    /**
     * Adds a whole bunch of nodes in a line
     */
    @FastTest
    public void testAddLine(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
        for(int i = 0; i < 31; i++){
            octree.addLeaf(new Vector3d(i,1,0), i);
        }
        //
        //validate the tree
        //
        List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
        openSet.add(octree.getRoot());
        while(openSet.size() > 0){
            OctreeNode<Integer> current = openSet.remove(0);
            if(current.isLeaf()){
                assertNotNull(current.getData());
            } else {
                assertNull(current.getData());
                for(OctreeNode<Integer> child : current.getChildren()){
                    if(child != null && child.getParent() != current){
                        fail("Child not attached to parent!");
                    }
                    if(child != null){
                        openSet.add(child);
                    }
                }
            }
        }
    }

    /**
     * Get a single leaf
     */
    @FastTest
    public void testGetLeaf(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
        octree.addLeaf(new Vector3d(1,1,1), 1);
        OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
        assertNotNull(leaf);
        //
        //validate the tree
        //
        List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
        openSet.add(octree.getRoot());
        while(openSet.size() > 0){
            OctreeNode<Integer> current = openSet.remove(0);
            if(current.isLeaf()){
                assertNotNull(current.getData());
            } else {
                assertNull(current.getData());
                for(OctreeNode<Integer> child : current.getChildren()){
                    if(child != null && child.getParent() != current){
                        fail("Child not attached to parent!");
                    }
                    if(child != null){
                        openSet.add(child);
                    }
                }
            }
        }
    }

    /**
     * Remove a single leaf
     */
    @FastTest
    public void testRemoveLeaf(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
        octree.addLeaf(new Vector3d(1,1,1), 1);
        OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
        assertNotNull(leaf);
        assertNotNull(leaf.getParent());
        octree.removeNode(leaf);
        //
        //validate the tree
        //
        List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
        openSet.add(octree.getRoot());
        while(openSet.size() > 0){
            OctreeNode<Integer> current = openSet.remove(0);
            if(current.isLeaf()){
                assertNotNull(current.getData());
            } else {
                assertNull(current.getData());
                for(OctreeNode<Integer> child : current.getChildren()){
                    if(child != null && child.getParent() != current){
                        fail("Child not attached to parent!");
                    }
                    if(child != null){
                        openSet.add(child);
                    }
                }
            }
        }
    }

    /**
     * Remove a leaf from a more complex tree
     */
    @FastTest
    public void testRemoveLeaf2(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
        octree.addLeaf(new Vector3d(1,1,1), 1);
        octree.addLeaf(new Vector3d(2,1,1), 2);
        octree.addLeaf(new Vector3d(3,1,1), 3);
        OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(2,1,1));
        assertNotNull(leaf);
        assertNotNull(leaf.getParent());

        //removes 2 & 3, but not 1
        octree.removeNode(leaf.getParent());
        assertEquals(1, octree.getNumLeaves());

        //remove 1
        leaf = octree.getLeaf(new Vector3d(1,1,1));
        assertNotNull(leaf);
        assertNotNull(leaf.getParent());
        octree.removeNode(leaf.getParent());
        assertEquals(0, octree.getNumLeaves());

        //
        //validate the tree
        //
        List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
        openSet.add(octree.getRoot());
        while(openSet.size() > 0){
            OctreeNode<Integer> current = openSet.remove(0);
            if(current.isLeaf()){
                assertNotNull(current.getData());
            } else {
                assertNull(current.getData());
                for(OctreeNode<Integer> child : current.getChildren()){
                    if(child != null && child.getParent() != current){
                        fail("Child not attached to parent!");
                    }
                    if(child != null){
                        openSet.add(child);
                    }
                }
            }
        }
    }
    
    /**
     * Adds lots of (random) points
     */
    @FastTest
    public void testAddManyPoints(){
        Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(256,256,256));


        //
        //add points
        Random rand = new Random();
        for(int i = 0; i < 1000; i++){
            Vector3d loc = new Vector3d(
                rand.nextInt(0, 256),
                rand.nextInt(0, 256),
                rand.nextInt(0, 256)
            );
            octree.addLeaf(loc, i);
        }

        //
        //verify at least one point was added
        assertEquals(true, octree.getNumLeaves() > 0);
    }

    

}
