/*
 * Scenic View, 
 * Copyright (C) 2014 Jonathan Giles, Ander Ruiz, Amy Fowler, Arnaud Nouard
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.scenicview.view.threedom;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javafx.animation.Interpolator;
import javafx.animation.ParallelTransition;
import javafx.animation.RotateTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Point3D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Accordion;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TitledPane;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.util.Duration;
import org.fxconnector.ConnectorUtils;
import org.fxconnector.node.SVNode;
import org.scenicview.utils.PropertiesUtils;

/**
 * Main class for 3D display TODO: 2D to 3D Miss: a snapshot parameter to only capture container and not descendants
 * (RFE for JDK9). Replace Tile3D by a Mesh with only one textured face.
 */
public class ThreeDOM implements ITile3DListener {

    private static final String THREEDOM_BACKGROUNDCOLOR = "threedom.backgroundcolor";

    @FXML
    Slider slider;
    @FXML
    CheckBox checkBoxAxes;
    @FXML
    Label overTileText;
    @FXML
    Button reload;
    @FXML
    TitledPane controls;
    @FXML
    Accordion accordion;
    @FXML
    BorderPane subSceneContainer;
    @FXML
    ColorPicker colorPicker;
    @FXML
    Button defaultBackgroundColor;
    @FXML
    Slider spaceSlider;

    private static final String STYLESHEETS = ThreeDOM.class.getResource("threedom.css").toExternalForm();

    static final double FACTOR2D3D = 1;
    static final double AXES_SIZE = 400;

    public boolean onlyOnce = false;

    double translateRootX;
    double translateRootY;
    double maxDepth = 0;
    double THICKNESS = 10;
    Group root3D;
    SVNode currentRoot2D;
    IThreeDOM iThreeDOM;
    Tile3D currentSelectedNode;
    Translate translateCamera;
    RotateTransition rotateTransition;
    ParallelTransition initialParallelTransition;

    public void setHolder(IThreeDOM h) {
        iThreeDOM = h;
    }

    public Parent createContent(SVNode root2D) throws Exception {
        currentRoot2D = root2D;

        // Build the Scene Graph
        Pane root = null;
        // UI part of the decoration
        try {
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("threedom.fxml"));
            fxmlLoader.setController(this);
            root = (BorderPane) fxmlLoader.load();
            root.getStylesheets().addAll(STYLESHEETS);
        } catch (Exception ex) {
            System.err.println(ex);
        }

        Group world = new Group();
        // Create and position camera
        PerspectiveCamera camera = new PerspectiveCamera(true);
        camera.setFieldOfView(70);
        camera.setNearClip(0.1);
        camera.setFarClip(10000);
        Rotate rotateX = new Rotate(20, Rotate.X_AXIS);
        Rotate rotateY = new Rotate(20, Rotate.Y_AXIS);
        translateCamera = new Translate(0, 0, -600);
        camera.getTransforms().addAll(
                rotateY,
                rotateX,
                translateCamera);

        world.getChildren().add(camera);
        rotateTransition = new RotateTransition(Duration.seconds(2));
        rotateTransition.setNode(camera);
        rotateTransition.setAxis(new Point3D(0, 1, 0));
        rotateTransition.setByAngle(30);

        Group axes3DRoot = new Group();

        root3D = new Group() {

            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                if (initialParallelTransition != null & !onlyOnce) {
                    initialParallelTransition.play();
                    onlyOnce = true;
                }
            }

        };
        world.getChildren().addAll(axes3DRoot, root3D);

        // Use a SubScene       
        SubScene subScene = new SubScene(world, 1024, 768, true, SceneAntialiasing.BALANCED);
        subScene.setFill(Color.TRANSPARENT);
        subScene.setCamera(camera);

        // Mouse
        DragSupport dragSupport = new DragSupport(subScene, null, Orientation.HORIZONTAL, rotateY.angleProperty());
        DragSupport dragSupport2 = new DragSupport(subScene, null, Orientation.VERTICAL, rotateX.angleProperty());
        ZoomSupport zoomSupport = new ZoomSupport(subScene, null, MouseButton.NONE, Orientation.VERTICAL, translateCamera.zProperty(), translateCamera.xProperty(), 1);

        controls.setExpanded(true);
        accordion.setExpandedPane(controls);
        //
        spaceSlider.valueProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            ObservableList<Node> childrenUnmodifiable = root3D.getChildrenUnmodifiable();
            childrenUnmodifiable.stream().forEach((n) -> {
                double z = ((Tile3D) n).getTranslateZ();
                ((Tile3D) n).setTranslateZ(((Tile3D) n).get3Depth() * -newValue.doubleValue());
            });
        });
        //
        slider.setShowTickMarks(true);
        slider.setShowTickLabels(true);
        slider.setBlockIncrement(2);
        slider.setOrientation(Orientation.HORIZONTAL);
        slider.setPrefWidth(200);
        slider.setPrefHeight(30);
        slider.setValue(maxDepth);
        slider.setShowTickMarks(true);
        slider.setShowTickLabels(true);
        slider.valueProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            setDepth(slider.getValue(), root3D);
        });
        //
        reload.setOnAction((ActionEvent event) -> {
            _reload();
        });
        subScene.setCursor(Cursor.HAND);
        subSceneContainer.setPrefSize(600, 500);
        subSceneContainer.setMinSize(60, 50);
        subSceneContainer.setCenter(subScene);
        subScene.heightProperty().bind(subSceneContainer.heightProperty());
        subScene.widthProperty().bind(subSceneContainer.widthProperty());

        buildAxes(axes3DRoot);        // Axes

        init3D(true);
        Bounds layoutBounds = root3D.getLayoutBounds();
        translateRootX = -layoutBounds.getWidth() / 2;
        translateRootY = -layoutBounds.getHeight() / 2;
        root3D.getTransforms().add(new Translate(translateRootX, translateRootY));
        // Scale to Scene's size
        double zoom = 600 * (layoutBounds.getWidth() / 600);
        translateCamera.setZ(-zoom);

        // Prefs
        final Properties properties = PropertiesUtils.getProperties();
        String color = properties.getProperty(THREEDOM_BACKGROUNDCOLOR);
        if (color != null) {
            Color c = Color.web(color);
            colorPicker.setValue(c);
            applyColor(c);
        }
        return root;
    }

    void init3D(boolean animate) {
        maxDepth = 0;
        if (animate) {
            initialParallelTransition = new ParallelTransition();
            initialParallelTransition.setDelay(Duration.seconds(1));
            initialParallelTransition.setInterpolator(Interpolator.EASE_OUT);
        } else {
            initialParallelTransition = null;
        }

        from2Dto3D(currentRoot2D, root3D, 0);

        slider.setMax(maxDepth);
        slider.setValue(maxDepth);
        if (animate) {
            if (rotateTransition != null) {
                initialParallelTransition.getChildren().add(rotateTransition);
            }
        }
    }

    private Tile3D from2Dto3D(SVNode root2D, Group root3D, double depth) {
        Tile3D childNode3D = null;
        // Currently, only work with internal usage. Remote is not yet possible, mainly due to the node.snapshot() calls
        if (ConnectorUtils.nodeClass(root2D).contains("SVRemoteNodeAdapter")) {
            Label label = new Label("The 3D view of ScenicView is only accessible when you invoke the tool from your source code. Remote access is not yet available.");
            label.setStyle("-fx-text-fill: #ff0000; -fx-font-size: 16pt;");
            label.setWrapText(true);
            label.setPadding(new Insets(10,10,10,10));
            controls.setExpanded(false);
            accordion.setExpandedPane(null);
            subSceneContainer.setCenter(label);
            return null;
        }
        Tile3D node3D = nodeToTile3D(root2D, FACTOR2D3D, depth);
        root3D.getChildren().add(node3D);
        depth += 3;
        List<SVNode> childrenUnmodifiable = root2D.getChildren();
        for (SVNode svnode : childrenUnmodifiable) {

            if (!ConnectorUtils.isNormalNode(svnode)) {
                continue;
            }
            Node node = svnode.getImpl();
            if (node.isVisible() && node instanceof Parent) {
                childNode3D = from2Dto3D(svnode, root3D, depth);
            } else if (node.isVisible()) {
                childNode3D = nodeToTile3D(svnode, FACTOR2D3D, depth);
                root3D.getChildren().add(childNode3D);

            }
            // Since 3D model is flat, keep "hierarchy" to child nodes
            if (childNode3D != null) {
                node3D.addChildrenTile(childNode3D);
            }
        }
        if (depth > maxDepth) {
            maxDepth = depth;
        }
        return node3D;
    }

    private Tile3D nodeToTile3D(SVNode node2D, double factor2d3d, double depth) {

        Tile3D tile = new Tile3D(currentRoot2D, factor2d3d, node2D, depth, THICKNESS, this, iThreeDOM);

        if (initialParallelTransition != null && depth > 1) {
            TranslateTransition translateTransition = new TranslateTransition(Duration.seconds(2));
            translateTransition.setInterpolator(Interpolator.EASE_OUT);
            translateTransition.setNode(tile);
            // Take into account slider's value
            translateTransition.setToZ(-depth * spaceSlider.getValue());
            initialParallelTransition.getChildren().add(translateTransition);
        } else {
            tile.setTranslateZ(-depth * spaceSlider.getValue());
        }

        return tile;
    }

    /**
     * Remove a node and its children . Issue: Do not update the depth slider
     *
     * @param node
     */
    public void removeNode(SVNode node) {
        Tile3D found = find(node);
        if (found != null) {
            ArrayList<Tile3D> childrenTile = found.getChildrenTile();
            hideChildren(childrenTile);
            root3D.getChildren().remove(found);
        }
        reload(currentRoot2D);
    }

    private void hideChildren(ArrayList<Tile3D> childrenTile) {
        if (childrenTile == null || childrenTile.isEmpty()) {
            return;
        }
        childrenTile.stream().forEach((tile) -> {
            tile.setVisible(false);
            hideChildren(tile.getChildrenTile());
        });
    }

    public void reload(SVNode root2D) {
        currentRoot2D = root2D;
        _reload();
    }

    void _reload() {
        if (root3D != null) {
            root3D.getChildren().clear();
        }
        init3D(false);
    }

    private void setDepth(double depth, Group world) {
        ObservableList<Node> childrenUnmodifiable = world.getChildrenUnmodifiable();
        childrenUnmodifiable.stream().filter((child) -> (child instanceof Tile3D)).forEach((Node child) -> {
            double cDepth = ((Tile3D) child).get3Depth();
            if (cDepth > depth) {
                child.setVisible(false);
            } else {
                child.setVisible(true);
            }
        });
    }

    @Override
    public void onMouseMovedOnTile(String s) {
        overTileText.setText(s);
    }

    @Override
    public void onMouseClickedOnTile(Tile3D tile) {
        iThreeDOM.clickOnTile(tile.getSVNode());
    }

    @Override
    public void onMouseRightClickedOnTile(MouseEvent evt) {
        iThreeDOM.rightClickOnTile(evt);
    }

    public void clearSelection() {
        if (currentSelectedNode != null) {
            ((PhongMaterial) currentSelectedNode.getMaterial()).setDiffuseColor(Color.WHITE);
            ((Tile3D) currentSelectedNode).snapshot();   // In case of changes
        }
    }

    public void setSelectedTile(Tile3D tile) {
        // Simulate SV behavior
        if (currentSelectedNode != null) {
            ((PhongMaterial) currentSelectedNode.getMaterial()).setDiffuseColor(Color.WHITE);
            final Node currentSelectedNodeToUnselect = currentSelectedNode;
            // Wait for principal UI to be updated (the selection)
            Platform.runLater(() -> {
                ((Tile3D) currentSelectedNodeToUnselect).snapshot();   // In case of changes on 2D node after edit
            });

        }
        currentSelectedNode = tile;
        ((PhongMaterial) tile.getMaterial()).setDiffuseColor(Color.YELLOW); // Simulate SV selection color
    }

    private void buildAxes(Group world) {
        PhongMaterial redMaterial = new PhongMaterial();
        redMaterial.setDiffuseColor(Color.DARKRED);
        redMaterial.setSpecularColor(Color.RED);
        PhongMaterial greenMaterial = new PhongMaterial();
        greenMaterial.setDiffuseColor(Color.DARKGREEN);
        greenMaterial.setSpecularColor(Color.GREEN);
        PhongMaterial blueMaterial = new PhongMaterial();
        blueMaterial.setDiffuseColor(Color.DARKBLUE);
        blueMaterial.setSpecularColor(Color.BLUE);

        Box xAxis = new Box(AXES_SIZE, 1, 1);
        xAxis.setMaterial(redMaterial);
        Box yAxis = new Box(1, AXES_SIZE, 1);
        yAxis.setMaterial(greenMaterial);
        Box zAxis = new Box(1, 1, AXES_SIZE);
        zAxis.setMaterial(blueMaterial);

        Group group = new Group();
        group.getChildren().addAll(xAxis, yAxis, zAxis);
        group.setVisible(true);
        world.getChildren().addAll(group);
        group.visibleProperty().bind(checkBoxAxes.selectedProperty());
    }

    static public ThreeDOM getInstance() {
        return new ThreeDOM();
    }

    public Tile3D find(SVNode node) {
        ObservableList<Node> childrenUnmodifiable = root3D.getChildrenUnmodifiable();
        for (Node n : childrenUnmodifiable) {
            if (n instanceof Tile3D) {
                if (((Tile3D) n).getSVNode().equals(node)) {
                    return (Tile3D) n;
                }
            }
        }
        return null;
    }

    @FXML
    public void onDefaultBackgroundColor(ActionEvent ae) {
        subSceneContainer.getStyleClass().add("subSceneBackground");
        // Prefs 
        final Properties properties = PropertiesUtils.getProperties();
        properties.remove(THREEDOM_BACKGROUNDCOLOR);
    }

    @FXML
    public void onColorPicker(ActionEvent ae) {
        Color newColor = colorPicker.getValue();
        applyColor(newColor);
        // Prefs 
        final Properties properties = PropertiesUtils.getProperties();
        properties.put(THREEDOM_BACKGROUNDCOLOR, newColor.toString());
    }

    void applyColor(Color color) {
        subSceneContainer.getStyleClass().remove("subSceneBackground");
        subSceneContainer.setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
    }
}