/*
 * Copyright (c) 2008, 2016, Oracle and/or its affiliates.
 * All rights reserved. Use is subject to license terms.
 *
 * This file is available and licensed under the following license:
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  - Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the distribution.
 *  - Neither the name of Oracle Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package ensemble.samples.media.overlaymediaplayer;

import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.ParallelTransition;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.ToolBar;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.scene.paint.Color;
import javafx.util.Duration;

public class PlayerPane extends Region {
        private MediaPlayer mp;
        private MediaView mediaView;
        private final boolean repeat = false;
        private boolean stopRequested = false;
        private boolean atEndOfMedia = false;
        private Duration duration;
        private Slider timeSlider;
        private Label playTime;
        private Slider volumeSlider;
        private ToolBar topBar = new ToolBar();
        private ToolBar bottomBar = new ToolBar();
        private HBox mediaTopBar;
        private HBox mediaBottomBar;
        private ParallelTransition transition = null;
        final double mediaWidth = 480;
        final double mediaHeight = 270;

        @Override protected void layoutChildren() {
            final double controlHeight = 40;
            final double controlOffset = 115;
            mediaView.relocate(0, 0);
            mediaView.setFitWidth(mediaWidth);
            mediaView.setFitHeight(mediaHeight);
            topBar.resizeRelocate(0, 0, mediaWidth, controlHeight);
            bottomBar.resizeRelocate(controlOffset,
                                     mediaHeight - controlHeight,
                                     mediaWidth, controlHeight);
        }

        @Override protected double computeMinWidth(double height) {
            return mediaBottomBar.prefWidth(-1);
        }

        @Override protected double computeMinHeight(double width) {
            return 200;
        }

        @Override protected double computePrefWidth(double height) {
            return Math.max(mp.getMedia().getWidth(),
                            mediaBottomBar.prefWidth(height));
        }

        @Override protected double computePrefHeight(double width) {
            return mp.getMedia().getHeight() + mediaBottomBar.prefHeight(width);
        }

        @Override protected double computeMaxWidth(double height) {
            return Double.MAX_VALUE;
        }

        @Override protected double computeMaxHeight(double width) {
            return Double.MAX_VALUE;
        }

        public PlayerPane(final MediaPlayer mp) {
            this.mp = mp;
            setId("player-pane");

            mediaView = new MediaView(mp);

            mediaTopBar = new HBox();
            mediaTopBar.setPadding(new Insets(5, 10, 5, 10));
            mediaTopBar.setAlignment(Pos.CENTER);
            mediaTopBar.setOpacity(1.0);

            topBar.setId("top-bar");
            topBar.setStyle("-fx-background-color: rgb(0,0,0.0,0.0);");
            bottomBar.setStyle("-fx-background-color: rgb(0,0,0.0,0.0);");

            setOnMouseEntered((MouseEvent t) -> {
                if (transition != null) {
                    transition.stop();
                }
                FadeTransition fade1 = new FadeTransition(Duration.millis(200),
                                                          topBar);
                fade1.setToValue(1.0);
                fade1.setInterpolator(Interpolator.EASE_OUT);

                FadeTransition fade2 = new FadeTransition(Duration.millis(200),
                                                          bottomBar);
                fade2.setToValue(1.0);
                fade2.setInterpolator(Interpolator.EASE_OUT);

                transition = new ParallelTransition(fade1, fade2);
                transition.play();
            });
            setOnMouseExited((MouseEvent t) -> {
                if (transition != null) {
                    transition.stop();
                }
                FadeTransition fadeTop = new FadeTransition(Duration.millis(800),
                                                            topBar);
                fadeTop.setToValue(0.0);
                fadeTop.setInterpolator(Interpolator.EASE_OUT);

                FadeTransition fadeBottom = new FadeTransition(Duration.millis(800),
                                                               bottomBar);
                fadeBottom.setToValue(0.0);
                fadeBottom.setInterpolator(Interpolator.EASE_OUT);
                transition = new ParallelTransition(fadeTop, fadeBottom);
                transition.play();
            });

            ReadOnlyObjectProperty<Duration> time = mp.currentTimeProperty();
            time.addListener((ObservableValue<? extends Duration> observable,
                              Duration oldValue, Duration newValue) -> {
                updateValues();
            });
            mp.setOnPlaying(() -> {
                if (stopRequested) {
                    mp.pause();
                    stopRequested = false;
                }
            });
            mp.setOnReady(() -> {
                duration = mp.getMedia().getDuration();
                updateValues();
            });
            mp.setOnEndOfMedia(() -> {
                if (!repeat) {
                    stopRequested = true;
                    atEndOfMedia = true;
                }
            });
            mp.setCycleCount(repeat ? MediaPlayer.INDEFINITE : 1);

            // Time label
            Label timeLabel = new Label("Time");
            timeLabel.setMinWidth(Control.USE_PREF_SIZE);
            timeLabel.setTextFill(Color.WHITE);

            mediaTopBar.getChildren().add(timeLabel);

            // Time slider
            timeSlider = new Slider();
            timeSlider.setId("media-slider");
            timeSlider.setMinWidth(200);
            timeSlider.setMaxWidth(Double.MAX_VALUE);
            timeSlider.valueProperty().addListener((ObservableValue<? extends Number> observable,
                                                    Number old, Number now) -> {
                if (timeSlider.isValueChanging()) {
                    // multiply duration by percentage calculated by slider position
                    if (duration != null) {
                        mp.seek(duration.multiply(timeSlider.getValue() / 100.0));
                    }
                    updateValues();
                } else if (Math.abs(now.doubleValue() - old.doubleValue()) > 1.5) {
                    // multiply duration by percentage calculated by slider position
                    if (duration != null) {
                        mp.seek(duration.multiply(timeSlider.getValue() / 100.0));
                    }
                }
            });
            mediaTopBar.getChildren().add(timeSlider);

            // Play label
            playTime = new Label();
            playTime.setPrefWidth(75);
            playTime.setMinWidth(75);
            playTime.setTextFill(Color.WHITE);
            mediaTopBar.getChildren().add(playTime);

            // Volume label
            Label volumeLabel = new Label("Vol");
            volumeLabel.setMinWidth(Control.USE_PREF_SIZE);
            volumeLabel.setTextFill(Color.WHITE);

            mediaTopBar.getChildren().add(volumeLabel);

            // Volume slider
            volumeSlider = new Slider();
            volumeSlider.setId("media-slider");
            volumeSlider.setPrefWidth(120);
            volumeSlider.setMinWidth(30);
            volumeSlider.setMaxWidth(Region.USE_PREF_SIZE);
            volumeSlider.valueProperty().addListener((Observable ov) -> {
            });
            volumeSlider.valueProperty().addListener((ObservableValue<? extends Number> observable,
                                                      Number old, Number now) -> {
                mp.setVolume(volumeSlider.getValue() / 100.0);
            });
            HBox.setHgrow(volumeSlider, Priority.ALWAYS);
            mediaTopBar.getChildren().add(volumeSlider);

            final EventHandler<ActionEvent> backAction = (ActionEvent e) -> {
                mp.seek(Duration.ZERO);
            };
            final EventHandler<ActionEvent> stopAction = (ActionEvent e) -> {
                mp.stop();
            };
            final EventHandler<ActionEvent> playAction = (ActionEvent e) -> {
                mp.play();
            };
            final EventHandler<ActionEvent> pauseAction = (ActionEvent e) -> {
                mp.pause();
            };
            final EventHandler<ActionEvent> forwardAction = (ActionEvent e) -> {
                Duration currentTime = mp.getCurrentTime();
                mp.seek(Duration.seconds(currentTime.toSeconds() + 5.0));
            };

            mediaBottomBar = new HBox();
            mediaBottomBar.setId("bottom");
            mediaBottomBar.setSpacing(0);
            mediaBottomBar.setAlignment(Pos.CENTER);

            Button backButton = new Button("Back");
            backButton.setId("back-button");
            backButton.setOnAction(backAction);

            Button stopButton = new Button("Stop");
            stopButton.setId("stop-button");
            stopButton.setOnAction(stopAction);

            Button playButton = new Button("Play");
            playButton.setId("play-button");
            playButton.setOnAction(playAction);

            Button pauseButton = new Button("Pause");
            pauseButton.setId("pause-button");
            pauseButton.setOnAction(pauseAction);

            Button forwardButton = new Button("Forward");
            forwardButton.setId("forward-button");
            forwardButton.setOnAction(forwardAction);

            mediaBottomBar.getChildren().addAll(backButton, stopButton, playButton, pauseButton, forwardButton);

            topBar.getItems().add(mediaTopBar);
            bottomBar.getItems().add(mediaBottomBar);

            getChildren().addAll(mediaView, topBar, bottomBar);

            topBar.setOpacity(0.0);
            bottomBar.setOpacity(0.0);
        }

        protected void updateValues() {
            if (playTime != null && timeSlider != null && volumeSlider != null && duration != null) {
                Platform.runLater(() -> {
                    Duration currentTime = mp.getCurrentTime();
                    playTime.setText(formatTime(currentTime, duration));
                    timeSlider.setDisable(duration.isUnknown());
                    if (!timeSlider.isDisabled() && duration.greaterThan(Duration.ZERO) && !timeSlider.isValueChanging()) {
                        timeSlider.setValue(currentTime.divide(duration).toMillis() * 100.0);
                    }
                    if (!volumeSlider.isValueChanging()) {
                        volumeSlider.setValue((int) Math.round(mp.getVolume() * 100));
                    }
                });
            }
        }

        private static String formatTime(Duration elapsed, Duration duration) {
            int intElapsed = (int)Math.floor(elapsed.toSeconds());
            int elapsedHours = intElapsed / (60 * 60);
            if (elapsedHours > 0) {
                intElapsed -= elapsedHours * 60 * 60;
            }
            int elapsedMinutes = intElapsed / 60;
            int elapsedSeconds = intElapsed - elapsedHours * 60 * 60 - elapsedMinutes * 60;

            if (duration.greaterThan(Duration.ZERO)) {
                int intDuration = (int)Math.floor(duration.toSeconds());
                int durationHours = intDuration / (60 * 60);
                if (durationHours > 0) {
                    intDuration -= durationHours * 60 * 60;
                }
                int durationMinutes = intDuration / 60;
                int durationSeconds = intDuration - durationHours * 60 * 60 - durationMinutes * 60;

                if (durationHours > 0) {
                    return String.format("%d:%02d:%02d",
                                         elapsedHours, elapsedMinutes, elapsedSeconds);
                } else {
                    return String.format("%02d:%02d",
                                         elapsedMinutes, elapsedSeconds);
                }
            } else {
                if (elapsedHours > 0) {
                    return String.format("%d:%02d:%02d",
                                         elapsedHours, elapsedMinutes, elapsedSeconds);
                } else {
                    return String.format("%02d:%02d",
                                         elapsedMinutes, elapsedSeconds);
                }
            }
        }
}
