/**
 * CubMesh.java
 *
 * LibGDX represents vertex meshes as arrays of floats. This is extremely
 * efficient, but makes them difficult to use. This class provides an overlay
 * on top of the float arrays, making them easier for students to use.
 *
 * @author Walker M. White
 * @date   3/1/2025
 */
package edu.cornell.cis3152.cube;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.GL30;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.NumberUtils;

import com.badlogic.gdx.math.*;
import com.badlogic.gdx.utils.ShortArray;
import edu.cornell.gdiac.math.*;

/**
 * This class represents a flexible mesh data type for textured 3d objects.
 *
 * A mesh is a collection of vertices and indices to pass to an OpenGL/ES 3.0
 * shader. It does not include a drawing command, as the command for this mesh
 * is always GL_TRIANGLES.
 *
 * This mesh is designed to support a vertex type with only two attributes:
 * position and texture coordinate. Position is three floats while texture is
 * two floats.
 */
public class CubeMesh {
	/** The vertex stride of this mesh */
	public static final int STRIDE = 5;
    /** The vertices for this mesh */
    public FloatArray vertices;
    /** The indices for this mesh */
    public ShortArray indices;

    /**
     * Creates an empty cube mesh.
     *
     * The created mesh has no vertices and no triangulation.
     */
    public CubeMesh() {
        vertices = new FloatArray();
        indices = new ShortArray();
    }

    /**
     * Sets the sube mesh to have the given vertices
     *
     * The resulting mesh has no indices triangulating the vertices.
     *
     * @param vertices  The vector of vertices (as float in triples) in this mesh.
     */
    public CubeMesh(float[] vertices) {
        this.vertices = new  FloatArray(vertices);
        indices = new ShortArray();
    }

    /**
     * Sets the cube mesh to have the given vertices
     *
     * The resulting mesh has no indices triangulating the vertices.
     *
     * @param vertices  The vector of vertices (as float in pairs) in this polygon
     * @param voffset   The offset of the first array vertex
     * @param vlength   The array size to use
     */
    public CubeMesh(float[] vertices, int voffset, int vlength) {
        this.vertices = new FloatArray(vlength);
        this.vertices.addAll( vertices, voffset, vlength );
        indices = new ShortArray();
    }

    /**
     * Sets the cube mesh to have the given vertices and indices.
     *
     * The size of the indices should be a multiple of 3.
     *
     * @param vertices  The float array of vertices (as float in pairs) in this mesh
     * @param indices  The array of indices in this polygon
     *
     */
    public CubeMesh(float[] vertices, short[] indices) {
        this.vertices = new  FloatArray(vertices);
        this.indices = new ShortArray(indices);
    }

    /**
     * Creates a copy of the given cube mesh.
     *
     * All of the contents are copied, so that this mesh does not hold any
     * references to elements of the other mesh.
     *
     * @param mesh	The mesh to copy
     */
    public CubeMesh(CubeMesh mesh) {
        vertices = new FloatArray();
        indices = new ShortArray();
        this.vertices.addAll(mesh.vertices);
        this.indices.addAll(mesh.indices);
    }

    /**
     * Sets the cube mesh to have the given vertices
     *
     * The method will invalid the indices. The resulting mesh has no indices
     * triangulating the vertices.
     *
     * This method returns a reference to this mesh for chaining.
     *
     * @param vertices  The vertices in this mesh
     *
     * @return This mesh, returned for chaining
     */
    public CubeMesh set(float[] vertices) {
        this.vertices.clear();
        this.vertices.addAll( vertices );
        this.indices.clear();
        return this;
    }

    /**
     * Sets the cube mesh to have the given vertices.
     *
     * The method will invalid the indices. The resulting mesh has no indices
     * triangulating the vertices.
     *
     * This method returns a reference to this mesh for chaining.
     *
     * @param vertices  The array of vertices in this mesh
     * @param voffset   The offset of the first array vertex
     * @param vlength   The array size to use
     *
     * @return This mesh, returned for chaining
     */
    public CubeMesh set(float[] vertices, int voffset, int vlength) {
        this.vertices.clear();
        this.vertices.addAll( vertices, voffset, vlength );
        this.indices.clear();
        return this;
    }

    /**
     * Sets this cube mesh to be a copy of the given one.
     *
     * All of the contents are copied, so that this mesh does not hold any
     * references to elements of the other mesh.
     *
     * This method returns a reference to this mesh for chaining.
     *
     * @param mesh  The mesh to copy
     *
     * @return This mesh, returned for chaining
     */
    public CubeMesh set(CubeMesh mesh) {
        this.vertices.clear();
        this.vertices.addAll( mesh.vertices );
        this.indices.clear();
        this.indices.addAll( mesh.indices );
        return this;
    }

    /**
     * Sets the indices for this cube mesh to the ones given.
     *
     * A valid list of indices must only refer to vertices in the vertex array.
     * That is, the indices should all be non-negative, and each value should be
     * less than the number of vertices. In addition, the number of indices
     * should be a multiple of 3. However, this method does not currently
     * enforce either of these preconditions.
     *
     * The provided indices are copied. The mesh does not retain a reference.
     *
     * @param indices   The vector of indices for the shape
     *
     * @return This mesh, returned for chaining
     */
    public CubeMesh setIndices(short[] indices) {
        this.indices.clear();
        this.indices.addAll(indices);
        return this;
    }

    /**
     * Sets the indices for this cube mesh to the ones given.
     *
     * A valid list of indices must only refer to vertices in the vertex array.
     * That is, the indices should all be non-negative, and each value should be
     * less than the number of vertices. In addition, the number of indices
     * should be a multiple of 3. However, this method does not currently
     * enforce either of these preconditions.
     *
     * The provided indices are copied. The mesh does not retain a reference.
     *
     * @param indices   The array of indices for the rendering
     * @param ioffset   The offset of the first array vertex
     * @param ilength   The array size to use
     *
     * @return This mesh, returned for chaining
     */
    public CubeMesh setIndices(short[] indices, int ioffset, int ilength) {
        this.indices.clear();
        this.indices.addAll(indices, ioffset, ilength);
        return this;
    }

    /**
     * Clears the contents of this cube mesh (both vertices and indices)
     */
    public void clear() {
        vertices.clear();
        indices.clear();
    }

    /**
     * Returns the byte stride of this mesh
     *
     * @return the byte stride of this mesh
     */
    public static int byteStride() {
        return STRIDE*4;
    }

    /**
     * Returns the offset of the position attribute
     *
     * @return the offset of the position attribute
     */
    public static int positionOffset() {
        return 0;
    }

    /**
     * Returns the offset of the tex coord attribute
     *
     * @return the offset of the tex coord attribute
     */
    public static int texCoordOffset() {
        return 3*4;
    }

    /**
     * Returns the number of vertices in this mesh
     *
     * @return the number of vertices in this mesh
     */
    public int vertexCount() {
        return vertices.size/STRIDE;
    }

    /**
     * Returns the number of indices in this mesh
     *
     * @return the number of indices in this mesh
     */
    public int indexCount() {
        return indices.size;
    }

    /**
     * Returns the x-coordinate of the position for index idx
     *
     * @param idx	The vertex index
     *
     * @return the x-coordinate of the position for index idx
     */
     public float getPositionX(int idx) {
     	return vertices.items[idx*STRIDE];
     }

    /**
     * Returns the y-coordinate of the position for index idx
     *
     * @param idx	The vertex index
     *
     * @return the y-coordinate of the position for index idx
     */
     public float getPositionY(int idx) {
     	return vertices.items[idx*STRIDE+1];
     }

    /**
     * Returns the z-coordinate of the position for index idx
     *
     * @param idx	The vertex index
     *
     * @return the z-coordinate of the position for index idx
     */
     public float getPositionZ(int idx) {
     	return vertices.items[idx*STRIDE+2];
     }

    /**
     * Returns the vertex position for index idx
     *
     * Note that this method creates a new Vector3 object. You can prevent this
     * memory allocation by using the alternate getter.
     *
     * @param idx	The vertex index
     *
     * @return the vertex position for index idx
     */
     public Vector3 getPosition(int idx) {
     	int pos = idx*STRIDE;
     	return new Vector3(vertices.items[pos],vertices.items[pos+1],vertices.items[pos+2]);
     }

    /**
     * Returns the vertex position for index idx
     *
     * Note that this method places the data into the provided Vector3 object,
     * preventing an object allocation. It will return this vector for chaining.
     *
     * @param idx	The vertex index
     * @param v		The vector to store the results;
     *
     * @return the vertex position for index idx
     */
     public Vector3 getPosition(int idx, Vector3 v) {
     	int pos = idx*STRIDE;
     	if (v == null) {
	     	return new Vector3(vertices.items[pos],vertices.items[pos+1],vertices.items[pos+2]);
	     } else {
	     	v.set(vertices.items[pos],vertices.items[pos+1],vertices.items[pos+2]);
	     	return v;
	     }
     }

    /**
     * Sets the vertex position for index idx
     *
     * @param idx	The vertex index
     * @param v		The vertex position
     */
	public void setPosition(int idx, Vector3 v) {
        setPosition(idx,v.x,v.y,v.z);
    }

    /**
     * Sets the vertex position for index idx
     *
     * @param idx	The vertex index
     * @param x		The vertex x-coordinate
     * @param y		The vertex y-coordinate
     * @param z		The vertex z-coordinate
     */
	public void setPosition(int idx, float x, float y, float z) {
     	int pos = idx*STRIDE;
     	vertices.items[pos  ] = x;
     	vertices.items[pos+1] = y;
     	vertices.items[pos+2] = z;
    }

    /**
     * Returns the x-coordinate of the texture for index idx
     *
     * @param idx	The vertex index
     *
     * @return the x-coordinate of the texture for index idx
     */
     public float getTextureX(int idx) {
     	return vertices.items[idx*STRIDE+3];
     }

    /**
     * Returns the y-coordinate of the texture for index idx
     *
     * @param idx	The vertex index
     *
     * @return the y-coordinate of the texture for index idx
     */
     public float getTextureY(int idx) {
     	return vertices.items[idx*STRIDE+4];
     }

    /**
     * Returns the vertex texture coordinate for index idx
     *
     * Note that this method creates a new Vector2 object. You can prevent this
     * memory allocation by using the alternate getter.
     *
     * @param idx	The vertex index
     *
     * @return the vertex texture coordinate for index idx
     */
     public Vector2 getTextureCoord(int idx) {
     	int pos = idx*STRIDE;
     	return new Vector2(vertices.items[pos+3],vertices.items[pos+4]);
     }

    /**
     * Returns the vertex texture coordinate for index idx
     *
     * Note that this method places the data into the provided Vector2 object,
     * preventing an object allocation. It will return this vector for chaining.
     *
     * @param idx	The vertex index
     * @param v		The vector to store the results;
     *
     * @return the vertex texture coordinate for index idx
     */
     public Vector2 getTextureCoord(int idx, Vector2 v) {
     	int pos = idx*STRIDE;
     	if (v == null) {
	     	return new Vector2(vertices.items[pos+3],vertices.items[pos+4]);
	     } else {
	     	v.set(vertices.items[pos+3],vertices.items[pos+4]);
	     	return v;
	     }
     }

    /**
     * Sets the vertex texture coordinate for index idx
     *
     * @param idx	The vertex index
     * @param v		The vertex texture coordinate
     */
	public void setTextureCoord(int idx, Vector2 v) {
        setTextureCoord(idx,v.x,v.y);
    }

    /**
     * Sets the vertex texture coordinate for index idx
     *
     * @param idx	The vertex index
     * @param x		The texture x-coordinate
     * @param y		The texture y-coordinate
     */
	public void setTextureCoord(int idx, float x, float y) {
     	int pos = idx*STRIDE;
     	vertices.items[pos+3] = x;
     	vertices.items[pos+4] = y;
    }

    // Internal cache to reduce object allocation
    private Vector3 vectorCache = new Vector3();

	/**
     * Uniformly scales all of the vertices of this mesh.
     *
     * The vertices are scaled from the origin of the coordinate space.  This
     * means that if the origin is not in the interior of this mesh, the
     * mesh will be effectively translated by the scaling.
     *
     * @param scale The uniform scaling factor
     *
     * @return This mesh, scaled uniformly.
     */
    public CubeMesh scl(float scale) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] *= scale;
            vertices.items[ii+1] *= scale;
            vertices.items[ii+2] *= scale;
        }
        return this;
    }

    /**
     * Nonuniformly scales all of the vertices of this mesh.
     *
     * The vertices are scaled from the origin of the coordinate space.  This
     * means that if the origin is not in the interior of this mesh, the
     * mesh will be effectively translated by the scaling.
     *
     * @param scale The non-uniform scaling factor
     *
     * @return This mesh, scaled non-uniformly.
     */
    public CubeMesh scl(Vector3 scale) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] *= scale.x;
            vertices.items[ii+1] *= scale.y;
            vertices.items[ii+2] *= scale.z;
        }
        return this;
    }

    /**
     * Nonuniformly scales all of the vertices of this mesh.
     *
     * The vertices are scaled from the origin of the coordinate space.  This
     * means that if the origin is not in the interior of this mesh, the
     * mesh will be effectively translated by the scaling.
     *
     * @param sx	The x-axis scaling factor
     * @param sy	The y-axis scaling factor
     * @param sz	The z-axis scaling factor
     *
     * @return This mesh, scaled non-uniformly.
     */
    public CubeMesh scl(float x, float y, float z) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] *= x;
            vertices.items[ii+1] *= y;
            vertices.items[ii+2] *= z;
        }
        return this;
    }

    /**
     * Uniformly scales all of the vertices of this mesh.
     *
     * The vertices are scaled from the origin of the coordinate space.  This
     * means that if the origin is not in the interior of this mesh, the
     * mesh will be effectively translated by the scaling.
     *
     * @param scale The inverse of the uniform scaling factor
     *
     * @return This mesh, scaled uniformly.
     */
    public CubeMesh div(float scale) {
        if (scale == 0) throw new ArithmeticException("Divided by zero operation cannot possible");
        return scl(1/scale);
    }

    /**
     * Nonuniformly scales all of the vertices of this mesh.
     *
     * The vertices are scaled from the origin of the coordinate space. This
     * means that if the origin is not in the interior of this mesh, the
     * mesh will be effectively translated by the scaling.
     *
     * @param scale The inverse of the non-uniform scaling factor
     *
     * @return This mesh, scaled non-uniformly.
     */
    public CubeMesh div(Vector3 scale) {
        if (scale.x == 0 || scale.y == 0 || scale.z == 0) {
        	throw new ArithmeticException("Divided by zero operation cannot possible");
        }
        return scl(1/scale.x,1/scale.y,1/scale.z);
    }

    /**
     * Nonuniformly scales all of the vertices of this mesh.
     *
     * The vertices are scaled from the origin of the coordinate space. This
     * means that if the origin is not in the interior of this mesh, the
     * mesh will be effectively translated by the scaling.
     *
     * @param sx	The inverse of the x-axis scaling factor
     * @param sy	The inverse of the y-axis scaling factor
     * @param sz	The inverse of the z-axis scaling factor
     *
     * @return This mesh, scaled non-uniformly.
     */
    public CubeMesh div(float sx, float sy, float sz) {
        if (sx == 0 || sy == 0 || sz == 0) {
        	throw new ArithmeticException("Divided by zero operation cannot possible");
        }
        return scl(1/sx,1/sy,1/sz);
    }

    /**
     * Uniformly translates all of the vertices of this mesh.
     *
     * @param offset The uniform translation amount
     *
     * @return This mesh, translated uniformly.
     */
    public CubeMesh add(float offset) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] += offset;
            vertices.items[ii+1] += offset;
            vertices.items[ii+2] += offset;
        }
        return this;
    }

    /**
     * Non-uniformly translates all of the vertices of this mesh.
     *
     * @param offset The non-uniform translation amount
     *
     * @return This mesh, translated non-uniformly.
     */
    public CubeMesh add(Vector3 offset) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] += offset.x;
            vertices.items[ii+1] += offset.y;
            vertices.items[ii+2] += offset.z;
        }
        return this;
    }

    /**
     * Non-uniformly translates all of the vertices of this mesh.
     *
     * @param x     The x-axis translation amount
     * @param y     The y-axis translation amount
     * @param z     The z-axis translation amount
     *
     * @return This mesh, translated non-uniformly.
     */
    public CubeMesh add(float x, float y, float z) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] += x;
            vertices.items[ii+1] += y;
            vertices.items[ii+2] += z;
        }
        return this;
    }

    /**
     * Uniformly translates all of the vertices of this mesh.
     *
     * @param offset The inverse of the uniform translation amount
     *
     * @return This mesh, translated uniformly.
     */
    public CubeMesh sub(float offset) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] -= offset;
            vertices.items[ii+1] -= offset;
            vertices.items[ii+2] -= offset;
        }
        return this;
    }

    /**
     * Non-uniformly translates all of the vertices of this mesh.
     *
     * @param offset The inverse of the non-uniform translation amount
     *
     * @return This mesh, translated non-uniformly.
     */
    public CubeMesh sub(Vector3 offset) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] -= offset.x;
            vertices.items[ii+1] -= offset.y;
            vertices.items[ii+2] -= offset.z;
        }
        return this;
    }

	/**
     * Non-uniformly translates all of the vertices of this mesh.
     *
     * @param x     The inverse of the x-axis translation amount
     * @param y     The inverse of the y-axis translation amount
     * @param z     The inverse of the z-axis translation amount
     *
     * @return This mesh, translated non-uniformly.
     */
    public CubeMesh sub(float x, float y, float z) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
            vertices.items[ii  ] -= x;
            vertices.items[ii+1] -= y;
            vertices.items[ii+2] -= z;
        }
        return this;
    }

    /**
     * Transforms all of the positional data in this mesh.
     *
     * @param matrix    The transform matrix
     *
     * @return This mesh with the vertices transformed
     */
    public CubeMesh transform(Matrix4 matrix) {
        for (int ii=0; ii < vertices.size; ii += STRIDE) {
        	vectorCache.set(vertices.items[ii],vertices.items[ii+1],vertices.items[ii+2]);
            vectorCache.mul( matrix );
			vertices.items[ii  ] = vectorCache.x;
			vertices.items[ii+1] = vectorCache.y;
			vertices.items[ii+2] = vectorCache.z;
        }
        return this;
    }

    /**
     * Transforms all of the positional data in this mesh.
     *
     * The value offset is a vertex position, not a float array position.
     *
     * @param matrix    The transform matrix
     * @param offset    The first vertex to transform
     * @param count     The number of vertices to transform
     *
     * @return This mesh with the vertices transformed
     */
    public CubeMesh transform(Matrix4 matrix, int offset, int count) {
        if (offset+count > vertexCount()) {
            throw new IndexOutOfBoundsException( "offset+count exceeds number of vertices" );
        }
        for(int ii = offset*STRIDE; ii < count*STRIDE; ii+= STRIDE) {
        	vectorCache.set(vertices.items[ii],vertices.items[ii+1],vertices.items[ii+2]);
            vectorCache.mul( matrix );
			vertices.items[ii  ] = vectorCache.x;
			vertices.items[ii+1] = vectorCache.y;
			vertices.items[ii+2] = vectorCache.z;
        }
        return this;
    }

	/**
     * Adds a vertex to the end of this mesh
     *
     * This method does not update the indices. It is simply a convenience
     * method for adding vertices.  The colors and texture/gradient coordinates
     * will be set to the defaults.
     *
     * @param position	The vertex position
     */
    public void push(Vector3 position) {
        push(position.x, position.y, position.z);
    }

    /**
     * Adds a vertex to the end of this mesh
     *
     * This method does not update the indices. It is simply a convenience
     * method for adding vertices.  The texture coordinates are set to zero.
     *
     * @param x         The vertex x-coordinate
     * @param y         The vertex y-coordinate
     * @param z         The vertex z-coordinate
     */
    public void push(float x, float y, float z) {
        vertices.ensureCapacity( STRIDE );
        int idx = vertices.size;
        vertices.items[idx++] = x;
        vertices.items[idx++] = y;
        vertices.items[idx++] = z;
        vertices.items[idx++] = 0;
        vertices.items[idx++] = 0;
        vertices.size += STRIDE;
    }

	/**
     * Adds a vertex to the end of this mesh
     *
     * This method does not update the indices. It is simply a convenience
     * method for adding vertices.
     *
     * @param position     	The vertex position
     * @param texcoord		The vertex texture coordinates
     */
    public void push(Vector3 position, Vector2 texcoord) {
        push(position.x, position.y, position.z, texcoord.x, texcoord.y);
    }

	/**
     * Adds a vertex to the end of this mesh
     *
     * This method does not update the indices. It is simply a convenience
     * method for adding vertices.
     *
     * @param x         The vertex x-coordinate
     * @param y         The vertex y-coordinate
     * @param z         The vertex z-coordinate
     * @param tx        The texture x-coordinate
     * @param ty        The texture y-coordinate
     */
    public void push(float x, float y, float z, float tx, float ty) {
        vertices.ensureCapacity( STRIDE );
        int idx = vertices.size;
        vertices.items[idx++] = x;
        vertices.items[idx++] = y;
        vertices.items[idx++] = z;
        vertices.items[idx++] = tx;
        vertices.items[idx++] = ty;
        vertices.size += STRIDE;
    }

}
