///usr/bin/env jbang "$0" "$@" ; exit $?

//DEPS org.openjfx:javafx-controls:22.0.2
//DEPS org.openjfx:javafx-graphics:22.0.2:${os.detected.jfxname}

import javafx.animation.AnimationTimer;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.Random;

/**
 * Execute with a Java runtime with JavaFX included, and JBang:
 * jbang FxDemo.java
 *
 * A demo is available in this movie:
 * https://www.youtube.com/watch?v=XhDQvkcYJ88
 */
public class FxDemo extends Application {

    private static int NUMBER_OF_BALLS = 10_000;
    private Random r = new Random();
    private Scene scene;
    private long lastUpdate = 0;
    
    @Override
    public void start(Stage stage) {
        var javaVersion = System.getProperty("java.version");
        var javaVendor = System.getProperty("java.vendor");
        var javaVendorVersion = System.getProperty("java.vm.version");
        var javafxVersion = System.getProperty("javafx.version");

        VBox holder = new VBox();
        holder.setFillWidth(true);
        holder.setAlignment(Pos.TOP_CENTER);
        holder.setSpacing(5);
        holder.getChildren().addAll(
            new Label("Java: " + javaVersion + ", " + javaVendor + ", " + javaVendorVersion),
            new Label("JavaFX: " + javafxVersion),
            getFramerateLabel(),
            getBouncingBallPane()
        );

        scene = new Scene(holder, 400, 700);
        stage.setTitle("JavaFX Demo");
        stage.setScene(scene);
        stage.show();
    }

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

    private Label getFramerateLabel() {
        Label frameRateLabel = new Label();

        AnimationTimer frameRateMeter = new AnimationTimer() {
            @Override
            public void handle(long now) {
                if (lastUpdate > 0) {
                    long nanosElapsed = now - lastUpdate;
                    double frameRate = 1_000_000_000.0 / nanosElapsed;
                    frameRateLabel.setText(String.format("Current frame rate: %.3f", frameRate));
                }
                lastUpdate = now;
            }
        };

        frameRateMeter.start();

        return frameRateLabel;
    }

    private Pane getBouncingBallPane() {
        Pane pane = new Pane();
        pane.setPrefWidth(Double.MAX_VALUE);
        pane.setPrefHeight(Double.MAX_VALUE);
        pane.setMinHeight(100.0);

        for (var i = 0; i < NUMBER_OF_BALLS; i++) {
            pane.getChildren().add(new Ball());
        }
        
        return pane;
    }

    class Ball extends Circle {
        private double dx = r.nextInt(1, 5);
        private double dy = r.nextInt(1, 5);
        private int size = r.nextInt(1, 15);
        private Color randomColor = Color.color(Math.random(), Math.random(), Math.random());

        public Ball() {
            this.setRadius(size);
            this.setFill(randomColor);
            relocate(r.nextInt(380), r.nextInt(620));
            Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -> {
                if (hitRightOrLeftEdge()) {
                    dx *= -1; // Ball hit right or left wall, so reverse direction
                }
                if (hitTopOrBottom()) {
                    dy *= -1; // Ball hit top or bottom, so reverse direction
                }
                setLayoutX(getLayoutX() + dx);
                setLayoutY(getLayoutY() + dy);
            }));
            timeline.setCycleCount(Timeline.INDEFINITE);
            timeline.play();
        }

        private boolean hitRightOrLeftEdge() {
            return (getLayoutX() < (scene.getX() + getRadius())) ||
                (getLayoutX() > (scene.getWidth() - getRadius()));
        }

        private boolean hitTopOrBottom() {
            return (getLayoutY() < (scene.getY() - getRadius())) ||
                (getLayoutY() > (scene.getHeight() - getRadius() - 60));
        }
    }
}