package com.srbenoit.render;

import java.util.BitSet;
import com.srbenoit.geom.BasedVector3;
import com.srbenoit.log.LoggedObject;
import com.srbenoit.sparsearray.SparseArrayException;

/**
 * A scene, which behaves much like a <code>SparseArray</code>, but for which four lists are
 * maintained in parallel. A list of "world coordinate" vertices and "world coordinate" faces
 * represents the live scene content, and parallel lists of vertices and faces are maintained.
 *
 * <p>The rendering pipeline, then, consists of locking the scene and transforming all
 * world-coordinate vertices and faces into view coordinates in the parallel arrays, and unlocking
 * the scene so the live content can continue to evolve. Then the view-coordinate objects, in their
 * self-consistent state, are further processed through culling, lighting, projecting into
 * canonical screen space, clipping, and rasterization.
 */
public class Scene extends LoggedObject {

    /** an object on which to synchronize access to scene structures */
    private final Object synch;

    /** filled status of each object in the <code>WorldVertex</code> array */
    private transient BitSet worldVertexStatus;

    /** filled status of each object in the <code>ViewVertex</code> array */
    private transient BitSet viewVertexStatus;

    /** total number of objects in the <code>WorldVertex</code> array */
    private transient int numWorldVertices;

    /** index of first available position in <code>WorldVertex</code> array */
    private transient int firstOpenWorldVertex;

    /** array of <code>WorldVertex</code> objects */
    private transient WorldVertex[] worldVertices;

    /** array of <code>ViewVertex</code> objects */
    private transient ViewVertex[] viewVertices;

    /** filled status of each object in the <code>WorldFace</code> array */
    private transient BitSet worldFaceStatus;

    /** filled status of each object in the <code>ViewFace</code> array */
    private transient BitSet viewFaceStatus;

    /** total number of objects in the <code>WorldFace</code> array */
    private transient int numWorldFaces;

    /** index of first available position in <code>WorldFace</code> array */
    private transient int firstOpenWorldFace;

    /** array of <code>WorldFace</code> objects */
    private transient WorldFace[] worldFaces;

    /** array of <code>ViewFace</code> objects */
    private transient ViewFace[] viewFaces;

    /** the lights in world-space */
    private transient Light[] worldLights;

    /** the lights in view-space */
    private transient Light[] viewLights;

    /** the based vectors in world-space */
    private transient BasedVector3[] worldBasedVectors;

    /** the based vectors in view-space */
    private transient BasedVector3[] viewBasedVectors;

    /**
     * Constructs a new <code>Scene</code>.
     *
     * @param  expectedFaceCount  the estimated number of faces in the scene
     */
    public Scene(final int expectedFaceCount) {

        int numFaces;
        int numVert;

        this.synch = new Object();

        // For (Nf) faces, assuming triangular faces only, there are three edges per face, each
        // edge shared by 2 faces, so number of edges (Ne) is (3/2)Nf.  Assuming Euler
        // characteristic 2, so number of vertices (Nv) is 2 + Ne - Nf = 2 + (Nf/2).

        // Round face counts to the next multiple of 128, vertex count will be multiple of 64
        numFaces = ((expectedFaceCount + 127) / 128) * 128;
        numVert = numFaces / 2;

        this.worldVertexStatus = new BitSet(numVert);
        this.viewVertexStatus = new BitSet(numVert);
        this.numWorldVertices = 0;
        this.firstOpenWorldVertex = 0;
        this.worldVertices = new WorldVertex[numVert];
        this.viewVertices = new ViewVertex[numVert];

        this.worldFaceStatus = new BitSet(numFaces);
        this.viewFaceStatus = new BitSet(numFaces);
        this.numWorldFaces = 0;
        this.firstOpenWorldFace = 0;
        this.worldFaces = new WorldFace[numFaces];
        this.viewFaces = new ViewFace[numFaces];

        this.worldLights = new Light[0];
        this.worldBasedVectors = new BasedVector3[0];
    }

    /**
     * Adds a vertex to the world-coordinate vertex array, increasing allocated storage if needed.
     *
     * @param   vertex  the vertex to add
     * @return  the index of the newly added vertex in the vertex array
     */
    public int addVertex(final WorldVertex vertex) {

        int len;
        int index;

        // If we need to allocate more space, do it
        synchronized (this.synch) {
            len = this.worldVertices.length;

            if (this.numWorldVertices == len) {
                setVertexCapacity(len + 128);
                index = len;
                this.firstOpenWorldVertex = len + 1;
            } else {
                index = this.firstOpenWorldVertex;

                for (int i = index + 1; i < len; i++) {

                    if (!this.worldVertexStatus.get(i)) {
                        this.firstOpenWorldVertex = i;

                        break;
                    }
                }
            }

            this.worldVertices[index] = vertex;
            this.viewVertices[index] = new ViewVertex();
            this.worldVertexStatus.set(index);
            this.numWorldVertices++;
        }

        vertex.setIndexInScene(index);

        return index;
    }

    /**
     * Attempts to add a world-coordinate vertex at a particular index.
     *
     * @param   vertex  the vertex to add
     * @param   index   the index at which to add the vertex
     * @throws  SparseArrayException  if the supplied index is not valid (either the array does not
     *                                contain the index, or there is already a vertex at the index)
     */
    public void addVertex(final WorldVertex vertex, final int index) throws SparseArrayException {

        synchronized (this.synch) {

            if (this.worldVertices.length > index) {

                if (this.worldVertexStatus.get(index)) {
                    throw new SparseArrayException("Index " + index + " already occupied");
                }

                this.worldVertices[index] = vertex;
                this.worldVertexStatus.set(index);
                this.numWorldVertices++;

                vertex.setIndexInScene(index);
            } else {
                throw new SparseArrayException("Index " + index + " out of bounds");
            }
        }
    }

    /**
     * Adds a vertex to the surface. The newly added vertex is not referenced by any faces.
     *
     * @param   xCoord  the X coordinate
     * @param   yCoord  the Y coordinate
     * @param   zCoord  the Z coordinate
     * @return  the immutable index of the vertex
     */
    public WorldVertex addVertex(final double xCoord, final double yCoord, final double zCoord) {

        int index;
        WorldVertex vertex;

        vertex = new WorldVertex(xCoord, yCoord, zCoord);
        index = addVertex(vertex);
        vertex.setIndexInScene(index);

        return vertex;
    }

    /**
     * Removes a world-coordinate vertex from the array.
     *
     * @param   index  the index of the vertex to remove
     * @return  <code>true</code> if a vertex was present at the requested index and was removed;
     *          <code>false</code> otherwise
     */
    public WorldVertex removeVertex(final int index) {

        WorldVertex obj;

        synchronized (this.synch) {

            if (this.worldVertexStatus.get(index)) {
                obj = this.worldVertices[index];
                this.worldVertexStatus.clear(index);
                this.numWorldVertices--;

                if (index < this.firstOpenWorldVertex) {
                    this.firstOpenWorldVertex = index;
                }

                obj.setIndexInScene(-1);
            } else {
                obj = null;
            }
        }

        return obj;
    }

    /**
     * Gets the index of the next filled index in the world vertex array after (or including) a
     * given starting index.
     *
     * @param   index  the starting index
     * @return  the next filled index, or -1 if no indices are filled from the start index on
     */
    public int nextWorldVertexFilled(final int index) {

        return this.worldVertexStatus.nextSetBit(index);
    }

    /**
     * Gets the index of the next filled index in the view vertex array after (or including) a
     * given starting index.
     *
     * @param   index  the starting index
     * @return  the next filled index, or -1 if no indices are filled from the start index on
     */
    public int nextViewVertexFilled(final int index) {

        return this.viewVertexStatus.nextSetBit(index);
    }

    /**
     * Gets a world-coordinate vertex from the array.
     *
     * @param   index  the index of the vertex to get
     * @return  the vertex
     */
    public WorldVertex getWorldVertex(final int index) {

        synchronized (this.synch) {
            return this.worldVertices[index];
        }
    }

    /**
     * Gets a view-coordinate vertex from the array.
     *
     * @param   index  the index of the vertex to get
     * @return  the vertex
     */
    public ViewVertex getViewVertex(final int index) {

        synchronized (this.synch) {
            return this.viewVertices[index];
        }
    }

    /**
     * Gets the length of the allocated array of world-coordinate vertices.
     *
     * @return  the length of the array (includes empty positions)
     */
    public int vertexCapacity() {

        synchronized (this.synch) {
            return this.worldVertices.length;
        }
    }

    /**
     * Sets the capacity of the world-coordinate vertex array, allocating new objects if needed. If
     * the current array is already large enough to accommodate the request, no changes are made
     * (that is, the capacity after this request may be larger than the requested capacity).
     *
     * @param  newCap  the new capacity
     */
    public void setVertexCapacity(final int newCap) {

        int len;
        WorldVertex[] newWorld;
        ViewVertex[] newView;

        synchronized (this.synch) {
            len = this.worldVertices.length;

            if (newCap > len) {
                newWorld = new WorldVertex[newCap];
                newView = new ViewVertex[newCap];
                System.arraycopy(this.worldVertices, 0, newWorld, 0, len);
                System.arraycopy(this.viewVertices, 0, newView, 0, len);
                this.worldVertices = newWorld;
                this.viewVertices = newView;
            }
        }
    }

    /**
     * Adds a face to the world-coordinate face array, increasing allocated storage if needed.
     *
     * @param   face  the face to add
     * @return  the index of the newly added object in the face array
     */
    public int addFace(final WorldFace face) {

        int len;
        int index;

        // If we need to allocate more space, do it
        synchronized (this.synch) {
            len = this.worldFaces.length;

            if (this.numWorldFaces == len) {
                setFaceCapacity(len + 128);
                index = len;
                this.firstOpenWorldFace = len + 1;
            } else {
                index = this.firstOpenWorldFace;

                for (int i = index + 1; i < len; i++) {

                    if (!this.worldFaceStatus.get(i)) {
                        this.firstOpenWorldFace = i;

                        break;
                    }
                }
            }

            this.worldFaces[index] = face;
            this.worldFaceStatus.set(index);
            this.viewFaces[index] = new ViewFace();
            this.numWorldFaces++;
        }

        face.setIndexInScene(index);

        return index;
    }

    /**
     * Attempts to add a world-coordinate face at a particular index.
     *
     * @param   face   the face to add
     * @param   index  the index at which to add the face
     * @throws  SparseArrayException  if the supplied index is not valid (either the array does not
     *                                contain the index, or there is already a face at the index)
     */
    public void addFace(final WorldFace face, final int index) throws SparseArrayException {

        synchronized (this.synch) {

            if (this.worldFaces.length > index) {

                if (this.worldFaceStatus.get(index)) {
                    throw new SparseArrayException("Index " + index + " already occupied");
                }

                this.worldFaces[index] = face;
                this.worldFaceStatus.set(index);
                this.numWorldFaces++;
            } else {
                throw new SparseArrayException("Index " + index + " out of bounds");
            }
        }
    }

    /**
     * Adds a vertex to the surface. The newly added vertex is not referenced by any faces.
     *
     * @param   vert0  the X coordinate
     * @param   vert1  the Y coordinate
     * @param   vert2  the Z coordinate
     * @return  the immutable index of the vertex
     */
    public WorldFace addFace(final WorldVertex vert0, final WorldVertex vert1,
        final WorldVertex vert2) {

        int index;
        WorldFace face;

        face = new WorldFace(vert0, vert1, vert2);
        index = addFace(face);
        face.setIndexInScene(index);

        return face;
    }

    /**
     * Removes a world-coordinate face from the array.
     *
     * @param   index  the index of the face to remove
     * @return  <code>true</code> if a face was present at the requested index and was removed;
     *          <code>false</code> otherwise
     */
    public WorldFace removeFace(final int index) {

        WorldFace obj;

        synchronized (this.synch) {

            if (this.worldFaceStatus.get(index)) {
                obj = this.worldFaces[index];
                this.worldFaceStatus.clear(index);
                this.numWorldFaces--;

                if (index < this.firstOpenWorldFace) {
                    this.firstOpenWorldFace = index;
                }
            } else {
                obj = null;
            }
        }

        return obj;
    }

    /**
     * Gets the index of the next filled index in the world face array after (or including) a given
     * starting index.
     *
     * @param   index  the starting index
     * @return  the next filled index, or -1 if no indices are filled from the start index on
     */
    public int nextWorldFaceFilled(final int index) {

        return this.worldFaceStatus.nextSetBit(index);
    }

    /**
     * Gets the index of the next filled index in the view face array after (or including) a given
     * starting index.
     *
     * @param   index  the starting index
     * @return  the next filled index, or -1 if no indices are filled from the start index on
     */
    public int nextViewFaceFilled(final int index) {

        return this.viewFaceStatus.nextSetBit(index);
    }

    /**
     * Gets a world-coordinate face from the array.
     *
     * @param   index  the index of the face to get
     * @return  the face
     */
    public WorldFace getWorldFace(final int index) {

        synchronized (this.synch) {
            return this.worldFaces[index];
        }
    }

    /**
     * Gets a view-coordinate face from the array.
     *
     * @param   index  the index of the face to get
     * @return  the face
     */
    public ViewFace getViewFace(final int index) {

        synchronized (this.synch) {
            return this.viewFaces[index];
        }
    }

    /**
     * Gets the length of the allocated array of world-coordinate faces.
     *
     * @return  the length of the array (includes empty positions)
     */
    public int faceCapacity() {

        synchronized (this.synch) {
            return this.worldFaces.length;
        }
    }

    /**
     * Sets the capacity of the world-coordinate face array, allocating new objects if needed. If
     * the current array is already large enough to accommodate the request, no changes are made
     * (that is, the capacity after this request may be larger than the requested capacity).
     *
     * @param  newCap  the new capacity
     */
    public void setFaceCapacity(final int newCap) {

        int len;
        WorldFace[] newWorld;
        ViewFace[] newView;

        synchronized (this.synch) {
            len = this.worldVertices.length;

            if (newCap > len) {
                newWorld = new WorldFace[newCap];
                newView = new ViewFace[newCap];
                System.arraycopy(this.worldFaces, 0, newWorld, 0, len);
                System.arraycopy(this.viewFaces, 0, newView, 0, len);
                this.worldFaces = newWorld;
                this.viewFaces = newView;
            }
        }
    }

    /**
     * Adds a light to the scene.
     *
     * @param  light  the light to add
     */
    public void addLight(final Light light) {

        int len;
        Light[] newArray;

        synchronized (this.synch) {
            len = this.worldLights.length;
            newArray = new Light[len + 1];

            if (len > 0) {
                System.arraycopy(this.worldLights, 0, newArray, 0, len);
            }

            newArray[len] = light;
            this.worldLights = newArray;

            newArray = new Light[len + 1];

            if (len > 0) {
                System.arraycopy(this.viewLights, 0, newArray, 0, len);
            }

            newArray[len] = new Light(0, 0, 0, light.getColor());
            this.viewLights = newArray;
        }
    }

    /**
     * Gets the number of lights. NOTE: Lights cannot be added once rendering starts, or the light
     * count will be off and we may try to use a view-space light that has not been transformed. In
     * the future, we should treat lights like vertices and faces.
     *
     * @return  the number of lights
     */
    public int numLights() {

        return this.viewLights.length;
    }

    /**
     * Gets a particular light in view coordinates.
     *
     * @param   index  the index of the light to get
     * @return  the light
     */
    public Light getViewLight(final int index) {

        return this.viewLights[index];
    }

    /**
     * Adds a based vector to the scene.
     *
     * @param  vec  the based vector to add
     */
    public void addBasedVector(final BasedVector3 vec) {

        int len;
        BasedVector3[] newArray;

        synchronized (this.synch) {
            len = this.worldBasedVectors.length;
            newArray = new BasedVector3[len + 1];

            if (len > 0) {
                System.arraycopy(this.worldBasedVectors, 0, newArray, 0, len);
            }

            newArray[len] = vec;
            this.worldBasedVectors = newArray;

            newArray = new BasedVector3[len + 1];

            if (len > 0) {
                System.arraycopy(this.viewBasedVectors, 0, newArray, 0, len);
            }

            newArray[len] = new BasedVector3();
            this.viewBasedVectors = newArray;
        }
    }

    /**
     * Gets the number of based vectors. NOTE: Based vectors cannot be added once rendering starts,
     * or the vector count will be off and we may try to use a view-space vector that has not been
     * transformed. In the future, we should treat based vectors like vertices and faces.
     *
     * @return  the number of based vectors
     */
    public int numBasedVectors() {

        return this.worldBasedVectors.length;
    }

    /**
     * Gets a particular based vector in view coordinates.
     *
     * @param   index  the index of the based vector to get
     * @return  the based vector
     */
    public BasedVector3 getBasedVector(final int index) {

        return this.viewBasedVectors[index];
    }

    /**
     * Transforms all vertices and faces from world to view coordinates using a specified camera.
     * This is an atomic operation that locks the vertex and face arrays for the duration so we get
     * a consistent state in the transformed object set.
     *
     * @param  camera  the camera that holds the transformation we will use
     */
    public void worldToView(final Camera camera) {

        int len;

        synchronized (this.synch) {

            // Transform all vertices
            this.viewVertexStatus.clear();
            this.viewVertexStatus.or(this.worldVertexStatus);

            len = this.worldVertices.length;

            for (int i = 0; i < len; i++) {

                if (this.worldVertexStatus.get(i)) {
                    this.viewVertices[i].transformFrom(camera, this.worldVertices[i]);
                }
            }

            // Transform all faces (normal vectors)
            this.viewFaceStatus.clear();
            this.viewFaceStatus.or(this.worldFaceStatus);

            len = this.worldFaces.length;

            for (int i = 0; i < len; i++) {

                if (this.worldFaceStatus.get(i)) {
                    this.viewFaces[i].transformFrom(camera, this.viewVertices, this.worldFaces[i]);
                }
            }

            // Transform lights
            len = this.worldLights.length;

            for (int i = 0; i < len; i++) {
                camera.transformPoint(this.worldLights[i], this.viewLights[i]);
            }

            // Transform based vectors
            len = this.worldBasedVectors.length;

            for (int i = 0; i < len; i++) {
                camera.transformPoint(this.worldBasedVectors[i], this.viewBasedVectors[i]);
                camera.transformVec(this.worldBasedVectors[i], this.viewBasedVectors[i]);
            }
        }
    }

    /**
     * Applies the perspective transformation to vertices to map the view frustum to the X and Y
     * range -1 \le X \le 1 and -1 \le Y \le 1, and where Z values are now positive (a left-handed
     * frame).
     *
     * @param  camera  the camera that holds the transformation we will use
     */
    public void viewToNormalized(final Camera camera) {

        int len;

        // Transform vertices
        len = this.viewVertices.length;

        for (int i = 0; i < len; i++) {

            if (this.viewVertexStatus.get(i)) {
                camera.toNormalizedDeviceCoordinates(this.viewVertices[i]);
            }
        }

        // Transform based vectors
        len = this.worldBasedVectors.length;

        for (int i = 0; i < len; i++) {
            camera.toNormalizedDeviceCoordinates(this.viewBasedVectors[i]);
        }
    }

    /**
     * Applies the perspective transformation to vertices to map the view frustum to the X and Y
     * range -1 \le X \le 1 and -1 \le Y \le 1, and where Z values are now positive (a left-handed
     * frame).
     *
     * @param  camera  the camera that holds the transformation we will use
     * @param  width   the width of the image being rendered
     * @param  height  the height of the image being rendered
     */
    public void normalizedToScreen(final Camera camera, final int width, final int height) {

        int len;

        // Transform vertices
        len = this.viewVertices.length;

        for (int i = 0; i < len; i++) {

            if (this.viewVertexStatus.get(i)) {
                camera.pointToScreen(width, height, this.viewVertices[i]);
            }
        }

        // Transform based vectors
        len = this.worldBasedVectors.length;

        for (int i = 0; i < len; i++) {
            camera.vecToScreen(width, height, this.viewBasedVectors[i]);
            camera.pointToScreen(width, height, this.viewBasedVectors[i]);
        }
    }
}
