import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.AmbientLight;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

public class Hello3d extends Application {

    private static final double EARTH_RADIUS = 6378.137 / 2; // WGS84長半径[km]

    //private final Image earthImage = new Image(getClass().getResourceAsStream("physical-free-world-map-b1.jpg"));
    private final Image earthImage = new Image(getClass().getResourceAsStream("bluemarble_cyrindricalEqualArea_2048.png"));
    private double azimuth;
    private long previousHandledTime;

    private final DoubleProperty azimuthRateProperty = new SimpleDoubleProperty(10d);
    private final DoubleProperty elevationProperty = new SimpleDoubleProperty(0d);
    private final DoubleProperty orbitalAltitudeProperty = new SimpleDoubleProperty(4_000d);

    private final Rotate cameraRotateX = new Rotate(0d, Rotate.X_AXIS);
    private final Rotate cameraRotateY = new Rotate(0d, Rotate.Y_AXIS);
    private final Rotate cameraRotateZ = new Rotate(0d, Rotate.Z_AXIS);
    private final Translate cameraTranslate = new Translate(0d, 0d, 0d);

    @Override
    public void start(final Stage stage) {
        final SubScene earthScene = createEarthScene();
        final GridPane panel = createUIPanel();

        // レイアウト
	final AnchorPane root = new AnchorPane();
        root.setTopAnchor(earthScene, 0d);
        root.setLeftAnchor(earthScene, 0d);
        root.setTopAnchor(panel, 0d);
        root.setRightAnchor(panel, 0d);
        root.getChildren().addAll(earthScene, panel);

	final Scene scene = new Scene(root, 1100, 600, Color.BLACK);

	stage.setScene(scene);
	stage.setTitle("Hello JavaFX 3D World");
	stage.show();

        AnimationTimer timer = new AnimationTimer() {
                @Override
                public void handle(long now) {
                    update(now);
                }
            };
        timer.start();
    }

    /**
     * カメラ位置を更新する。
     * カメラは原点を中心に (地球半径 + 高度)を半径とし、赤道面(y=0)から
     * 指定した仰角の円軌道を移動する。
     */
    private void update(long now) {
        if (previousHandledTime == 0) {
            previousHandledTime = now;
            return;
        }
        
        azimuth += azimuthRateProperty.get() * (now - previousHandledTime) / 1_000_000_000;
        previousHandledTime = now;
        double orbitalRadius = orbitalAltitudeProperty.get() + EARTH_RADIUS;

        cameraTranslate.setZ(-orbitalRadius);
	cameraRotateX.setAngle(elevationProperty.get());
        cameraRotateY.setAngle(-1 * azimuth);
    }

    /**
     * 地球系のモデルを作成
     */
    private SubScene createEarthScene() {
        final Group earthGroup = new Group();

	// 球体の定義
	final Sphere earth = new Sphere(EARTH_RADIUS);
	earthGroup.getChildren().add(earth);

	// 材質の定義
	final PhongMaterial material = new PhongMaterial();
	material.setDiffuseMap(earthImage);
	earth.setMaterial(material);

	// カメラの定義
	final PerspectiveCamera camera = new PerspectiveCamera(true);
	camera.setFieldOfView(45d);
	camera.setFarClip(100_000d);
	camera.getTransforms().addAll(
            cameraRotateX,
            cameraRotateY,
            cameraRotateZ,
	    cameraTranslate
	);

	// 点光源の定義
	final PointLight pointLight = new PointLight(Color.WHITE);
	pointLight.setTranslateX(-2_000d);
	pointLight.setTranslateY(0d);
	pointLight.setTranslateZ(100_000d);
	earthGroup.getChildren().add(pointLight);

	// 環境光の定義
	final AmbientLight ambientLight = new AmbientLight(Color.rgb(80, 80, 80, 0.5));
	earthGroup.getChildren().add(ambientLight);

        // サブシーンの定義
        final SubScene earthScene = new SubScene(earthGroup, 800d, 600d);
        earthScene.setFill(Color.BLACK);
        earthScene.setCamera(camera);
        return earthScene;
    }

    /**
     * 制御UIパネルを作成する。
     */
    private GridPane createUIPanel() {
        final GridPane grid = new GridPane();
        grid.setHgap(8d);
        grid.setVgap(16d);
        grid.setPadding(new Insets(32d, 10d, 10d, 16d));
        grid.setPrefSize(300d, 600d);
        grid.setStyle("-fx-background-color: midnightblue;");

        final Label azimuthRateLabel = createSliderLabel("回転速度[deg/s]");
        grid.add(azimuthRateLabel, 0, 0);
        grid.setValignment(azimuthRateLabel, VPos.TOP);
        grid.add(createSlider(-30d, 30d, 1d, 10d, 2d, azimuthRateProperty), 1, 0);
        final Label elevationLabel = createSliderLabel("仰角[deg]");
        grid.add(elevationLabel, 0, 1);
        grid.setValignment(elevationLabel, VPos.TOP);
        grid.add(createSlider(-90d, 90d, 0d, 30d, 6d, elevationProperty), 1, 1);
        final Label altitudeLabel = createSliderLabel("高度[km]");
        grid.add(altitudeLabel, 0, 2);
        grid.setValignment(altitudeLabel, VPos.TOP);
        grid.add(createSlider(0d, 30_000d, 200d, 5_000d, 1_000d, orbitalAltitudeProperty), 1, 2);
        return grid;
    }

    /**
     * ラベルを作成する。
     */
    private Label createSliderLabel(String title) {
        final Label label = new Label(title);
        label.setStyle("-fx-text-fill: aliceblue;");
        return label;
    }

    /**
     * スライダーを生成する。
     */
    private Slider createSlider(double min, double max, double initial, double major, double minor, DoubleProperty prop) {
        final Slider slider = new Slider(min, max, initial);
        slider.setShowTickMarks(true);
        slider.setShowTickLabels(true);
        slider.setMajorTickUnit(major);
        slider.setBlockIncrement(minor);
        slider.valueProperty().bindBidirectional(prop);
        return slider;
    }

    public static void main(final String... args) {
	launch(args);
    }
}
