/*
 * CubeScene.java
 *
 * This demo is actually four different BoxScenes run at the same time. This
 * class is the parent scene that runs the individual scenes and displays them
 * on the side of a cube.
 *
 * Based on the original PhysicsDemo Lab by Don Holden, 2007
 *
 * Author:  Walker M. White
 * Version: 3/1/2025
 */
 package edu.cornell.cis3152.cube;

import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.BufferUtils;
import com.badlogic.gdx.utils.JsonValue;
import com.badlogic.gdx.utils.ScreenUtils;
import edu.cornell.gdiac.assets.AssetDirectory;
import edu.cornell.gdiac.audio.SoundEffect;
import edu.cornell.gdiac.graphics.*;

import java.nio.IntBuffer;

/**
 * A scene for displaying box2d demos on the side of a 3d cube.
 *
 * This class has a custom OpenGL pipeline for rendering the sides of the
 * cube. When we only need one side of a cube, we just use the sprite batch
 * to draw directly to the screen.  But if we need more than one side, we draw
 * each side to a texture (using render targets) and then apply those textures
 * to cube.
 *
 * This class creates a lot of OpenGL objects. Such objects MUST be disposed
 * when they are no longer needed. If you let the Java garbage collector hit
 * these objects before they are disposed, you may incur a memory leak.
 */
public class CubeScene implements Screen {
    /** The OpenGL context */
    GL20 gl;

    /** The asset directory for retrieving textures, atlases */
    private AssetDirectory directory;
    /** The sprite batch for rendering the individual scenes */
    private SpriteBatch batch;
    /** The drawing camera for this scene */
    private PerspectiveCamera camera;
    /** The shader for drawing the cube */
    private Shader shader;
    /** The vertex buffer for receiving our cube data */
    private VertexBuffer vertbuff;
    /** Our cube geometry */
    private CubeMesh mesh;

    /** The individual scenes to draw */
    private BoxScene[] scenes;
    /** The two render targets for drawing */
    private RenderTarget[] targets;
    /** The textures to display each scene */
    private Texture[] textures;


    /** The width of this scene */
    private int width;
    /** The height of this scene */
    private int height;

    /** The current side */
    int side;
    /** The goal side */
    int goal;
    /** Whether to animate left */
    boolean left;
    /** Whether or not we are currently animating */
    boolean animate;
    /** The animation step */
    float step;
    /** The zoom out factor */
    float zoom;

    /** Whether or not this scene is still active */
    private boolean active;

    /**
     * Creates a new rotating cube.
     *
     * The faces of the cube will be active physics simulations that will
     * run while the cube rotates. Use the arrow keys to rotate the cube.
     */
    protected CubeScene(AssetDirectory directory, SpriteBatch batch) {
        this.directory = directory;
        this.batch = batch;

        resize( Gdx.graphics.getWidth(), Gdx.graphics.getHeight() );

        gl = Gdx.gl30;
        if (gl == null) {
            gl = Gdx.gl20;
        }

        textures = new Texture[4];
        scenes = new BoxScene[4];
        scenes[0] = new BoxScene("scene1", directory);
        scenes[1] = new BoxScene("scene2", directory);
        scenes[2] = new BoxScene("scene3", directory);
        scenes[3] = new BoxScene("scene4", directory);

        targets = new RenderTarget[2];


        shader = directory.getEntry( "cubeface", Shader.class );
        shader.bind();
        shader.setUniformMatrix( "u_projTrans", camera.combined );

        vertbuff = new VertexBuffer( CubeMesh.byteStride(), 40, 60 );
        vertbuff.setupAttribute( Shader.POSITION_ATTRIBUTE, 3, GL20.GL_FLOAT, false, CubeMesh.positionOffset() );
        vertbuff.setupAttribute( Shader.TEXCOORD_ATTRIBUTE, 2, GL20.GL_FLOAT, false, CubeMesh.texCoordOffset() );

        // Attach the shader
        vertbuff.attach( shader );

        // Let's make a box
        mesh = new CubeMesh();
        mesh.vertices.ensureCapacity( 6 * 4 );
        mesh.indices.ensureCapacity( 6 * 6 );

        float[] points = new float[3];
        points[0] = width / (float) height;
        points[1] = -width / (float) height;
        points[2] = -1.0f;

        // Put texture coords updside down for render target
        float[] texcoord = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};

        int anchor = 0;
        int prev, next;
        int offt;
        for (int ii = 0; ii < 4; ii++) {
            next = (ii < 2 ? (anchor + 1) % 3 : (anchor + 2) % 3);
            prev = (ii < 2 ? (anchor + 2) % 3 : (anchor + 1) % 3);
            offt = (next == 2 ? 2 : 0);
            int tx = offt;
            mesh.push( points[0], points[1], points[2], texcoord[tx], texcoord[tx + 1] );

            tx = offt + 2;
            points[next] = -points[next];
            mesh.push( points[0], points[1], points[2], texcoord[tx], texcoord[tx + 1] );

            tx = offt + 4;
            points[prev] = -points[prev];
            mesh.push( points[0], points[1], points[2], texcoord[tx], texcoord[tx + 1] );

            tx = ((offt + 6) % 8);
            points[next] = -points[next];
            mesh.push( points[0], points[1], points[2], texcoord[tx], texcoord[tx + 1] );

            // Restore everything
            points[prev] = -points[prev];
            if (ii < 2) {
                points[anchor] = -points[anchor];
            }
            if (ii < 1) {
                points[next] = -points[next];
            }
            anchor = (anchor + 1) % 2;
        }


        for (int ii = 0; ii < mesh.vertexCount(); ii += 4) {
            int off = mesh.indices.size;
            mesh.indices.ensureCapacity( 6 );
            mesh.indices.items[off] = (short) (ii + 0);
            mesh.indices.items[off + 1] = (short) (ii + 1);
            mesh.indices.items[off + 2] = (short) (ii + 2);
            mesh.indices.items[off + 3] = (short) (ii + 2);
            mesh.indices.items[off + 4] = (short) (ii + 3);
            mesh.indices.items[off + 5] = (short) (ii + 0);
            mesh.indices.size += 6;
        }

        // Load the mesh into the vertex buffer
        // We only need to reload it if the vertex data changes (which is never)
        vertbuff.loadVertexData( mesh.vertices.items, mesh.vertices.size );
        vertbuff.loadIndexData( mesh.indices.items, mesh.indices.size );

        // Create the render targets
        // These are used to draw the scenes to the side of the cube
        // Only need two at any given time.

        // How big a texture does this platform support?
        IntBuffer query = BufferUtils.newByteBuffer( 4 ).asIntBuffer();
        gl.glGetIntegerv( gl.GL_ACTIVE_TEXTURE, query );
        int maxt = query.get();
        query.clear();

        int w = width;
        int h = height;
        if (width*2 <= maxt && height*2 <= maxt) {
            w = 2*width;
            h = 2*height;
        } else if (width < maxt && height < maxt) {
            float ratio = width/(float)height;
            if (ratio < 1) {
                h = maxt;
                w = (int)Math.ceil(ratio*maxt);
            } else {
                w = maxt;
                h = (int)Math.ceil(maxt/ratio);
            }
        }
        targets[0] = new RenderTarget(w,h);
        targets[1] = new RenderTarget(w,h);
    }

    /**
     * Disposes all of the OpenGL resources (IMPORTANT)
     */
    @Override
    public void dispose() {
        if (targets != null) {
            for(int ii = 0; ii < targets.length; ii++) {
                targets[ii].dispose();
            }
            targets = null;
        }
        if (vertbuff != null) {
            vertbuff.dispose();
            vertbuff = null;
        }
        if (shader != null) {
            shader.dispose();
            shader = null;
        }

    }

    /**
     * Called when the Screen should render itself.
     *
     * We defer to the other methods update() and draw().  However, it is VERY
     * important that we only quit AFTER a draw.
     *
     * @param delta Number of seconds since last animation frame
     */
    @Override
    public void render(float dt) {
        update( dt );
        draw();
    }

    /**
	 * Updates the core gameplay loop of this scene.
	 *
	 * This method updates the physics simulations of the four box scenes. It
	 * also processed input commands to transition between box scenes on the
	 * display.
	 *
	 * @param dt	Number of seconds since last animation frame
	 */
    public void update(float dt) {
        // Update the individual scenes
        scenes[0].update(dt);
        scenes[1].update(dt);
        scenes[2].update(dt);
        scenes[3].update(dt);

        InputController input = InputController.getInstance();
        input.sync();
        if (input.exit()) {
            Gdx.app.exit( );
        } else if (!animate) {
            // Only accept input if animating
            if (input.left() && !input.right()) {
                animate = true;
                left = true;
                goal = (side+1) % 4;
            } else if (!input.left() && input.right()) {
                animate = true;
                left = false;
                goal = (side+3) % 4;
            }
        } else {
            // Let's go around the world
            step += dt / 5;
            float ang1, ang2;
            ang1 = (float) (side * 90);
            if (left) {
                ang2 = (float) ((side + 1) * 90);
            } else {
                ang2 = (float) ((side - 1) * 90);
            }
            Vector3 axis = new Vector3( 0, 0, 1 );
            Quaternion quat1 = new Quaternion( axis, ang1 );
            Quaternion quat2 = new Quaternion( axis, ang2 );
            Matrix4 transform = new Matrix4();
            if (step > 1) {
                side = goal;
                step = 0;
                animate = false;
                transform.set( quat2 );
            } else {
                quat1.slerp( quat2, step );
                transform.set( quat1 );
            }
            axis.set( zoom, 0, 0 );
            axis.mul( transform );
            camera.position.set( axis );
            camera.lookAt( 0, 0, 0 );
            camera.update();
        }
    }

    /**
     * Draws this scene.
     *
     * When we only need one side of a cube, this method will just use the
     * sprite batch to draw directly to the screen. But if we need more than one
     * side, it will draw each side to a texture (using render targets) and then
     * apply those textures to cube.
     */
    public void draw() {
        if (!animate) {
            // Lock the scene in place for now
            scenes[side].draw(batch);
        } else {
            // Reset the side textures
            textures[0] = null;
            textures[1] = null;
            textures[2] = null;
            textures[3] = null;

            // Render the two visible cube sides
            targets[0].begin();
            scenes[side].draw(batch);
            targets[0].end();
            textures[side] = targets[0].getTexture();

            targets[1].begin();
            scenes[goal].draw(batch);
            targets[1].end();
            textures[goal] = targets[1].getTexture();

            // OpenGL commands to set up everything
            gl.glEnable( GL20.GL_CULL_FACE );
            gl.glEnable( GL20.GL_BLEND );
            gl.glEnable( GL30.GL_DEPTH );
            gl.glBlendEquation( GL20.GL_FUNC_ADD );
            gl.glBlendFunc( GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA );

            // Clear the screen (needed because of intermediates)
            ScreenUtils.clear( 0.75f, 0.75f, 0.75f, 1.0f );

            // Draw the cube
            shader.bind();
            vertbuff.bind();
            shader.setUniformMatrix( "u_projTrans", camera.combined );

            for (int face = 0; face < 4; face++) {
                if (textures[face] != null) {
                    textures[face].bind();
                    vertbuff.draw( GL20.GL_TRIANGLES, 6, face * 6 );
                    gl.glBindTexture( gl.GL_TEXTURE_2D, 0 );
                }
            }
            vertbuff.unbind();

            // Swallow an error we cannot find right now
            gl.glGetError();
        }
    }


    /**
     * Called when the screen is resized
     *
     * @param width     The new screen width
     * @param height    The new screen height
     */
    @Override
    public void resize(int width, int height) {
        this.width = width;
        this.height = height;

        if (camera == null) {
            zoom = width / (float) height + (float) (1 / Math.tan( 15 * Math.PI / 180 ));
            camera = new PerspectiveCamera( 30, width, height );
            camera.up.set( 0, 0, 1 );
            camera.position.set( zoom, 0, 0 );
            camera.lookAt( 0, 0, 0 );
            camera.near = 0.1f;
        } else {
            camera.viewportWidth = width;
            camera.viewportHeight = height;
        }
        camera.update();

    }

    /**
     * Called when the Screen is paused.
     * <p>
     * This is usually when it's not active or visible on screen. An Application is
     * also paused before it is destroyed.
     */
    public void pause() {
        // TODO Auto-generated method stub

    }

    /**
     * Called when the Screen is resumed from a paused state.
     * <p>
     * This is usually when it regains focus.
     */
    public void resume() {
        // TODO Auto-generated method stub

    }

    /**
     * Called when this screen becomes the current screen for a Game.
     */
    public void show() {
        // Useless if called in outside animation loop
        active = true;
    }

    /**
     * Called when this screen is no longer the current screen for a Game.
     */
    public void hide() {
        // Useless if called in outside animation loop
        active = false;
    }

}
