package org.example.newmat;
import Graph.readFile;
import Shape.MovingObject;
import Shape.VisibleWindow;
import Shape.Obstacle;
import Shape.ObstacleField;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.geometry.Point3D;
import javafx.geometry.Pos;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.*;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.Arrays;


/**
 * Demonstrates advanced JavaFX graphics and window management by creating a 3D scene with
 * animated elements and FPS (Frames Per Second) counter.
 * <p>
 * This application creates a 3D scene containing a textured cube and a dynamically generated
 * mesh based on input parameters. It showcases the handling of 3D transformations, animation,
 * camera movement, and basic input from the keyboard to manipulate the scene. Additionally,
 * it includes a real-time FPS counter overlaid on the scene to monitor performance.
 */
public class GraphicsAndWindowsTest extends Application {
    //public static String[] parameters;
    public static MovingObject[] objects;
    /**
     * Starts the JavaFX application.
     * This method initializes the 3D scene with its objects, animation timer, and event handlers.
     *
     * @param stage The primary stage for this application, onto which the scene is set.
     * @throws IOException If there is an issue reading the input file for generating the mesh.
     */
    @Override
    public void start(Stage stage) throws IOException {
        double windowXOffset = 600;
        double windowYOffset = 200;
        VisibleWindow sceneWindow = new VisibleWindow(500, -250, -250);
        Box ourWindow = sceneWindow.makeWindow();

//        ObstacleField obs = new ObstacleField(sceneWindow.getWindowSize(),sceneWindow.getXOffset(),
//                sceneWindow.getYOffset(),350,100, 20, 50, "Box",
//                Color.ORANGERED);
        String[] ob2 = {"--image", "src/teapot.txt", "--speed", "20", "--dir", "4,", "10,", "6"};
        String[] ob3 = {"--image", "src/armadillo.txt", "--speed", "5", "--dir", "1,", "1,", "1"};
        String[] ob4 = {"--image", "src/teapot.txt", "--speed", "100", "--dir", "3,", "5,", "7"};
        MovingObject newObject1 = new MovingObject(getParameters().getRaw().toArray(new String[0]));
        MovingObject newObject2 = new MovingObject(ob2);
        MovingObject newObject3 = new MovingObject(ob3);
        MovingObject newObject4 = new MovingObject(ob4);
        objects = new MovingObject[4];
        objects[0] = newObject1;
        objects[1] = newObject2;
        objects[2] = newObject3;
        objects[3] = newObject4;


        Group group3D = new Group(); // Group of our 3D elements
        for (MovingObject o : objects ) {
            o.setTranslate(sceneWindow.getXOffset(), sceneWindow.getYOffset(),0);
            group3D.getChildren().add(o.getMesh());
        }

        group3D.setTranslateX(windowXOffset);
        group3D.setTranslateY(windowYOffset);
        group3D.setTranslateZ(250);
        group3D.getChildren().add(ourWindow);
        group3D.getChildren().add(new AmbientLight(Color.WHITE));
        //obs.addToGroup(group3D);

        // NOTE: Due to the perspectiveCamera it looks like it is rotating even though it isn't
        // If you comment out this line and the scene.setCamera(camera) line
        // You will see it move without the rotation effect
        // I think this is pretty cool
        Camera camera = new PerspectiveCamera();
        camera.setNearClip(.1);
        camera.setFarClip(2000);
        camera.setTranslateX(-125);
        camera.setTranslateY(-250);
        camera.setTranslateZ(-250);

        //https://stackoverflow.com/questions/65510979/move-camera-relative-to-view-in-javafx
        //Incorporated the code from this post
        Point3D xAxis = new Point3D(1,0,0);
        Point3D yAxis = new Point3D(0,1,0);
        Rotate groupRotX = new Rotate(0, xAxis);
        Rotate groupRotY = new Rotate(0, yAxis);


        group3D.getTransforms().addAll(groupRotY, groupRotX);

        stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
            switch(event.getCode()){
                case LEFT:
                    groupRotY.setAngle(groupRotY.getAngle() + 10);
                    break;
                case RIGHT:
                    groupRotY.setAngle(groupRotY.getAngle() - 10);
                    break;
                case UP:
                    groupRotX.setAngle(groupRotX.getAngle() + 10);
                    break;
                case DOWN:
                    groupRotX.setAngle(groupRotX.getAngle() - 10);
                    break;

            }
        });

        /*
         * SubScene to handle 3D components
         * Makes it easier to have 2D and 3D components in the same animation
         */
        SubScene subScene3D = new SubScene(group3D, 1500, 800);
        subScene3D.setFill(Color.SILVER);
        subScene3D.setCamera(camera);


        /*
         * Text label to display FPS
         */
        Label fpsLabel = new Label("FPS: 0");
        fpsLabel.setTextFill(Color.WHITE);

        //Lays out the children in Z order with the last member being front most
        //Effectively allows you to layer multiple elements
        //We are layering our 3D scene with a 2D label on top of it
        StackPane Layers = new StackPane();
        Layers.getChildren().addAll(subScene3D, fpsLabel);
        StackPane.setAlignment(fpsLabel, Pos.BOTTOM_RIGHT); //Places FPS in bottom right

        Scene scene = new Scene(Layers, 1500, 800);

        stage.setTitle("FPS test");
        stage.setScene(scene);
        stage.show();

        // Animation logic to move the Box back and forth
        AnimationTimer timer = new AnimationTimer() {
            private long lastUpdate = 0; //Movement timer
            private long fpsTimer = 0; //Frame timer
            private long frameRate = 0; //Frames per second

            /**
             * Called at every frame while the {@link AnimationTimer} is active to update the scene's elements,
             * including object movements, boundary condition checks, mesh deformation on collision, and FPS counter updates.
             * <p>
             * This method extends the scene update logic with collision detection between the mesh and a virtual bounding box,
             * causing mesh deformation upon nearing the box's edges. It calculates new positions for 3D objects based on their
             * current positions and direction vectors, simulates movement within the scene, and reverses the movement direction
             * at the scene's boundaries. Additionally, it modifies the mesh's vertices to "squish" the mesh when it comes close
             * to or collides with the bounding box, creating a more dynamic interaction between the scene's objects.
             * <p>
             * Movement, boundary checking, and mesh deformation logic are applied based on the bounds of the 3D mesh and cube within the scene.
             * The FPS counter is updated once every second, based on the number of frames rendered in that interval, to provide real-time
             * performance feedback.
             *
             * @param now The timestamp of the current frame given in nanoseconds. This value is used to ensure smooth animations and
             *            consistent updates across frames.
             */
            @Override
            public void handle(long now) {
                //Built in class that helps calculate the bounds of the mesh for collision
                //Bounds meshBounds = newObject1.getMesh().getBoundsInParent();
                Bounds[] meshBounds = new Bounds[objects.length];
                double[][] direction = new double[objects.length][3];
                for (int i = 0; i < objects.length; i++) {
                    meshBounds[i] = objects[i].getMesh().getBoundsInParent();
                    direction[i] = objects[i].getVelocityVec();
                }

                //Number is roughly 1 mill divided by 60
                if (now - lastUpdate >= 16_000_000) { // Roughly 60 frames per second

                    int lastChanged = 1;
                    double winLeftXBound = sceneWindow.getXOffset();
                    double winTopYBound = sceneWindow.getYOffset();
                    double WindowSize = sceneWindow.getWindowSize();
                    // Reverse direction at bounds
                    for (int i = 0; i < objects.length; i++) {
                        if (meshBounds[i].getMinX() + direction[i][0] < winLeftXBound || meshBounds[i].getMaxX() + direction[i][0]  > winLeftXBound + WindowSize) {

                            // load the vertices of the mesh
                            //float[] points = mesh.getPoints().toArray(null);
                        /*
                        // find the vertices which come close to the bounding box threshold and squish them into the mesh
                        for (int j = 0; j < points.length; j += 3) {
                            if (Math.abs(points[j] + translate.getX() + directionX - minX) < 10 || Math.abs(points[j] + translate.getX() + directionX - maxX) < 10) {
                                points[j] += (float) (-directionX*5);
                            }
                        }

                         */

                            //update the direction of the mesh and set the new coordinates
                            //mesh.getPoints().setAll(points);


                            direction[i][0] *= -1;
                            lastChanged = 1;

                        }
                        if (meshBounds[i].getMinY() + direction[i][1] < winTopYBound || meshBounds[i].getMaxY() + direction[i][1] > winTopYBound + WindowSize) {

                            // load the vertices of the mesh
                            //float[] points = mesh.getPoints().toArray(null);
                        /*
                        // find the vertices which come close to the bounding box threshold and squish them into the mesh
                        for (int j = 1; j < points.length; j += 3) {
                            if (Math.abs(points[j] + translate.getY() + directionY - minY) < 10 || Math.abs(points[j] + translate.getY() + directionY - maxY) < 10) {
                                points[j] += (float) (-directionY*5);
                            }
                        }
                        */
                            //update the direction of the mesh and set the new coordinates
                            direction[i][1] *= -1;
                            lastChanged = 2;
                            //mesh.getPoints().setAll(points);
                        }
                        if (meshBounds[i].getMinZ() + direction[i][2] < 0 || meshBounds[i].getMaxZ() + direction[i][2] > WindowSize) {

                            // load the vertices of the mesh
                            //float[] points = mesh.getPoints().toArray(null);
                        /*
                        // find the vertices which come close to the bounding box threshold and squish them into the mesh
                        for (int j = 2; j < points.length; j += 3) {
                            if (Math.abs(points[j] + translate.getZ() + directionZ - minZ) < 10 || Math.abs(points[j] + translate.getZ() + directionZ - maxZ) < 10) {
                                points[j] += (float) (-directionZ*5);
                            }
                        }
                        */

                            //update the direction of the mesh and set the new coordinates
                            direction[i][2] *= -1;
                            lastChanged = 3;
                            //mesh.getPoints().setAll(points);

                        }
                    /*
                    if(obs.checkCollision(meshBounds[i])){
                        switch (lastChanged){
                            case 1: direction[i][0] *= -1;

                            case 2: direction[i][1] *= -1;

                            case 3: direction[i][2] *= -1;

                            default: System.out.println();
                        }
                    }

                     */


                        objects[i].updateTranslate(objects[i].getTranslate().getX() + direction[i][0], objects[i].getTranslate().getY() + direction[i][1], objects[i].getTranslate().getZ() + direction[i][2]);
                        System.out.println(Arrays.toString(objects[i].getCentroid()));
                        objects[i].updateRays();
                    }
                    lastUpdate = now;
                }
                if (fpsTimer == 0) {
                    fpsTimer = now; // Initialize the FPS timer
                }
                frameRate++; // Increment frame count for each invocation
                //1 mill nanoseconds in a second
                if (now - fpsTimer >= 1_000_000_000) { // Every second, update the FPS display
                    fpsLabel.setText(String.format("FPS: %d", frameRate));
                    frameRate = 0; // Reset frame count
                    fpsTimer = now; // Reset the FPS timer
                }
            }
        };
        timer.start();
    }
    /**
     * The entry point of the application. This method is called when the application starts.
     *
     * @param args Command-line arguments passed to the application. Expected to contain
     *             parameters for mesh generation and other configurations.
     */
    public static void main(String[] args) {
        Application.launch(args);
    }
}
