package com.example.demo;

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
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 javafx.util.Duration;

import java.io.IOException;
import java.io.SerializablePermission;
import java.util.concurrent.ScheduledThreadPoolExecutor;

class MovableCamera extends PerspectiveCamera {

    private Translate pan;
    private Rotate rotateX;
    private Rotate rotateY;
    private Rotate yaw;
    private Rotate tilt;
    private Rotate roll;
    private double x;
    private double y;

    public MovableCamera ( double xAngle, double zDistance, double nearClip, double farClip ) {
        super ( true );

        this.setNearClip ( nearClip );
        this.setFarClip ( farClip );

        this.pan = new Translate ( 0, 0, zDistance );
        this.rotateX = new Rotate ( xAngle, Rotate.X_AXIS );
        this.rotateY = new Rotate ( 0, Rotate.Y_AXIS );

        this.yaw = new Rotate ( 0, Rotate.Y_AXIS );
        this.tilt = new Rotate ( 0, Rotate.X_AXIS );
        this.roll = new Rotate ( 0, Rotate.Z_AXIS );

        this.getTransforms ( ).addAll (
                this.rotateY,
                this.rotateX,
                this.pan,
                this.yaw,
                this.tilt,
                this.roll
        );
    }

    public void handleScrollEvent ( ScrollEvent event ) {
        double sign = event.getDeltaY ( ) > 0 ? 1 : -1;

        this.pan.setZ ( this.pan.getZ ( ) + sign * 10 );
    }

    public void handleMouseEvent ( MouseEvent event ) {
        if ( event.getEventType ( ).equals ( MouseEvent.MOUSE_PRESSED ) ) {
            this.x = event.getSceneX ( );
            this.y = event.getSceneY ( );
        } else if ( event.getEventType ( ).equals ( MouseEvent.MOUSE_DRAGGED ) ) {
            double dX = this.x - event.getSceneX ( );
            double dY = this.y - event.getSceneY ( );

            this.x = event.getSceneX ( );
            this.y = event.getSceneY ( );

            if ( event.isPrimaryButtonDown ( ) ) {
                double angleX = this.rotateX.getAngle ( );
                double angleY = this.rotateY.getAngle ( );

                this.rotateX.setAngle ( angleX + dY );
                this.rotateY.setAngle ( angleY + dX );
            } else if ( event.isSecondaryButtonDown ( ) ) {
                double panX = this.pan.getX ( );
                double panY = this.pan.getY ( );

                this.pan.setX ( panX + dX );
                this.pan.setY ( panY + dY );
            }
        }
    }

    public void handleKeyEvent ( KeyEvent event ) {
        switch ( event.getCode ( ) ) {
            case UP: {
                this.tilt.setAngle ( this.tilt.getAngle ( ) + 1 );
                break;
            }
            case DOWN: {
                this.tilt.setAngle ( this.tilt.getAngle ( ) - 1 );
                break;
            }
            case LEFT: {
                this.yaw.setAngle ( this.yaw.getAngle ( ) - 1 );
                break;
            }
            case RIGHT: {
                this.yaw.setAngle ( this.yaw.getAngle ( ) + 1 );
                break;
            }
            case PAGE_UP: {
                this.roll.setAngle ( this.roll.getAngle ( ) + 1 );
                break;
            }
            case PAGE_DOWN: {
                this.roll.setAngle ( this.roll.getAngle ( ) - 1 );
                break;
            }
        }
    }
}

public class HelloApplication extends Application {

    private static MeshView createTruncatedPyramid ( double lowerRadius, double upperRadius, double height, int divisions ) {
        float points[] = new float[( divisions * 2 + 2 ) * 3];

        points[0] = 0;
        points[1] = ( float ) height / 2;
        points[2] = 0;

        int pointsIndex = 3;

        double dAngle = Math.toRadians ( 360. / divisions );

        for ( int i = 0; i < divisions; ++i ) {
            double angle = i * dAngle;

            float x = ( float ) ( Math.cos ( angle ) * lowerRadius );
            float z = ( float ) ( Math.sin ( angle ) * lowerRadius );

            points[pointsIndex++] = x;
            points[pointsIndex++] = ( float ) height / 2;
            points[pointsIndex++] = z;
        }

        for ( int i = 0; i < divisions; ++i ) {
            double angle = i * dAngle;

            float x = ( float ) ( Math.cos ( angle ) * upperRadius );
            float z = ( float ) ( Math.sin ( angle ) * upperRadius );

            points[pointsIndex++] = x;
            points[pointsIndex++] = ( float ) - height / 2;
            points[pointsIndex++] = z;
        }

        points[pointsIndex++] = 0;
        points[pointsIndex++] = ( float ) -height / 2;
        points[pointsIndex++] = 0;

        float textureCoordinates[] = {
                0.1f, 0.5f,
                0.3f, 0.5f,
                0.5f, 0.5f,
                0.7f, 0.5f,
                0.9f, 0.5f,
        };

        int faces[] = new int[divisions * 4 * 2 * 6];

        int facesIndex = 0;
        int textureIndex = 0;

        // lower base
        for ( int i = 0; i < divisions; ++i ) {
            int v0 = 0;
            int v1 = i + 1;
            int v2 = i != ( divisions - 1 ) ? v1 + 1 : 1;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;

            textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );
        }

        // upper base
        for ( int i = 0; i < divisions; ++i ) {
            int v0 = divisions * 2 + 1;
            int v1 = i + 1 + divisions;
            int v2 = i != ( divisions - 1 ) ? v1 + 1 : divisions + 1;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;

            textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );
        }

        // sides
        for ( int i = 0; i < divisions; ++i ) {
            int v0 = i + 1;
            int v1 = i != ( divisions - 1 ) ? v0 + 1 : 1;
            int v2 = v0 + divisions;
            int v3 = v1 + divisions;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;

            textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );

            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v3;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;

            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v3;
            faces[facesIndex++] = textureIndex;

            textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );
        }

        TriangleMesh triangleMesh = new TriangleMesh ( );

        triangleMesh.getPoints ( ).addAll ( points );
        triangleMesh.getTexCoords ( ).addAll ( textureCoordinates );
        triangleMesh.getFaces ( ).addAll ( faces );

        MeshView meshView = new MeshView ( );
        meshView.setMesh ( triangleMesh );

        PhongMaterial material = new PhongMaterial ( );
        material.setDiffuseMap ( new Image ( "pallete.png" ) );

        meshView.setMaterial ( material );

        return meshView;
    }

    private static MeshView createPyramid ( double radius, double height, int divisions ) {
        float points[] = new float[( divisions + 2 ) * 3];

        points[0] = 0;
        points[1] = 0;
        points[2] = 0;

        int pointsIndex = 3;

        double dAngle = Math.toRadians ( 360. / divisions );
        for ( int i = 0; i < divisions; ++i ) {
            double angle = i * dAngle;

            float x = ( float ) ( Math.cos ( angle ) * radius );
            float z = ( float ) ( Math.sin ( angle ) * radius );

            points[pointsIndex++] = x;
            points[pointsIndex++] = 0;
            points[pointsIndex++] = z;
        }

        points[pointsIndex++] = 0;
        points[pointsIndex++] = ( float ) -height;
        points[pointsIndex++] = 0;

        float textureCoordinates[] = {
                0.1f, 0.5f,
                0.3f, 0.5f,
                0.5f, 0.5f,
                0.7f, 0.5f,
                0.9f, 0.5f,
        };

        int faces[] = new int[divisions * 2 * 2 * 6];

        int facesIndex = 0;
        int textureIndex = 0;

        // base
        for ( int i = 0; i < divisions; ++i ) {
            int v0 = 0;
            int v1 = i + 1;
            int v2 = i != ( divisions - 1 ) ? v1 + 1 : 1;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;

            textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );
        }

        // sides
        for ( int i = 0; i < divisions; ++i ) {
            int v0 = divisions + 1;
            int v1 = i + 1;
            int v2 = i != ( divisions - 1 ) ? v1 + 1 : 1;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;

            faces[facesIndex++] = v0;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v2;
            faces[facesIndex++] = textureIndex;
            faces[facesIndex++] = v1;
            faces[facesIndex++] = textureIndex;

            textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );
        }

        TriangleMesh triangleMesh = new TriangleMesh ( );

        triangleMesh.getPoints ( ).addAll ( points );
        triangleMesh.getTexCoords ( ).addAll ( textureCoordinates );
        triangleMesh.getFaces ( ).addAll ( faces );

        MeshView meshView = new MeshView ( );
        meshView.setMesh ( triangleMesh );

        PhongMaterial material = new PhongMaterial ( );
        material.setDiffuseMap ( new Image ( "pallete.png" ) );

        meshView.setMaterial ( material );

        return meshView;
    }

    private static MeshView createDonut ( double innerRadius, double outerRadius, int innerDivisions, int outerDivisions ) {
        float points[] = new float[innerDivisions * outerDivisions * 3];

        int pointsIndex = 0;

        double dInnerAngle = Math.toRadians ( 360. / innerDivisions );
        double dOuterAngle = Math.toRadians ( 360. / outerDivisions );

        for ( int i = 0; i < outerDivisions; ++i ) {
            for ( int j = 0; j < innerDivisions; ++j ) {
                double outerAngle = i * dOuterAngle;
                double innerAngle = j * dInnerAngle;

                double x = ( outerRadius + innerRadius * Math.cos ( innerAngle ) ) * Math.cos ( outerAngle );
                double y = ( outerRadius + innerRadius * Math.cos ( innerAngle ) ) * Math.sin ( outerAngle );
                double z = innerRadius * Math.sin ( innerAngle );

                points[pointsIndex++] = ( float ) x;
                points[pointsIndex++] = ( float ) y;
                points[pointsIndex++] = ( float ) z;
            }
        }

        float textureCoordinates[] = {
                0.1f, 0.5f,
                0.3f, 0.5f,
                0.5f, 0.5f,
                0.7f, 0.5f,
                0.9f, 0.5f,
        };

        int faces[] = new int[innerDivisions * outerDivisions * 2 * 2 * 6];

        int facesIndex = 0;
        int textureIndex = 0;

        for ( int i = 0; i < outerDivisions; ++i ) {
            for ( int j = 0; j < innerDivisions; ++j ) {
                int v0 = i * innerDivisions + j;
                int v1 = j != ( innerDivisions - 1 ) ? v0 + 1 : i * innerDivisions;
                int v2 = ( v0 + innerDivisions ) % ( innerDivisions * outerDivisions );
                int v3 = ( v1 + innerDivisions ) % ( innerDivisions * outerDivisions );

                faces[facesIndex++] = v0; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v1; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v3; faces[facesIndex++] = textureIndex;

                faces[facesIndex++] = v0; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v3; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v1; faces[facesIndex++] = textureIndex;

                textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );

                faces[facesIndex++] = v0; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v3; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v2; faces[facesIndex++] = textureIndex;

                faces[facesIndex++] = v0; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v2; faces[facesIndex++] = textureIndex;
                faces[facesIndex++] = v3; faces[facesIndex++] = textureIndex;

                textureIndex = ( textureIndex + 1 ) % ( textureCoordinates.length / 2 );
            }
        }

        TriangleMesh triangleMesh = new TriangleMesh ( );

        triangleMesh.getPoints ( ).addAll ( points );
        triangleMesh.getTexCoords ( ).addAll ( textureCoordinates );
        triangleMesh.getFaces ( ).addAll ( faces );

        MeshView meshView = new MeshView ( );
        meshView.setMesh ( triangleMesh );

        PhongMaterial material = new PhongMaterial ( );
        material.setDiffuseMap ( new Image ( "pallete.png" ) );

        meshView.setMaterial ( material );

        return meshView;
    }

    @Override
    public void start ( Stage stage ) throws IOException {
        Group root = new Group ( );

        root.getChildren ( ).addAll ( HelloApplication.createDonut ( 20, 100, 20, 100 ) );

        Scene scene = new Scene ( root, 600, 600, true, SceneAntialiasing.BALANCED );

        MovableCamera camera = new MovableCamera ( 0, -1000, 1, 10000 );

        scene.setCamera ( camera );

        scene.addEventHandler ( MouseEvent.ANY, event -> camera.handleMouseEvent ( event ) );
        scene.addEventHandler ( ScrollEvent.ANY, event -> camera.handleScrollEvent ( event ) );
        scene.addEventHandler ( KeyEvent.ANY, event -> camera.handleKeyEvent ( event ) );

        stage.setTitle ( "Hello!" );
        stage.setScene ( scene );
        stage.show ( );
    }

    public static void main ( String[] args ) {
        launch ( );
    }
}