content: /*
   * Copyright 2014-2016 Arnaud Nouard. All rights reserved.
   *
   * All rights reserved.
   *
   * 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.
   *
   * 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 HOLDER 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 insidefx.undecorator;
  
  import java.util.logging.Level;
  import javafx.application.Platform;
  import javafx.collections.ObservableList;
  import javafx.geometry.BoundingBox;
  import javafx.geometry.Bounds;
  import javafx.geometry.Rectangle2D;
  import javafx.scene.Cursor;
  import javafx.scene.Node;
  import javafx.scene.input.MouseEvent;
  import javafx.stage.Screen;
  import javafx.stage.Stage;
  import javafx.stage.StageStyle;
  import javafx.stage.WindowEvent;
  
  /**
   *
   * @author in-sideFX
   */
  public class UndecoratorController {
  
      static final int DOCK_NONE = 0x0;
      static final int DOCK_LEFT = 0x1;
      static final int DOCK_RIGHT = 0x2;
      static final int DOCK_TOP = 0x4;
      int lastDocked = DOCK_NONE;
      private static double initX = -1;
      private static double initY = -1;
      private static double newX;
      private static double newY;
      private static int RESIZE_PADDING;
      private static int SHADOW_WIDTH;
      Undecorator undecorator;
      BoundingBox savedBounds, savedFullScreenBounds;
      boolean maximized = false;
      static boolean isMacOS = false;
      static final int MAXIMIZE_BORDER = 20;  // Allow double click to maximize on top of the Scene
  
      static {
          String os = System.getProperty("os.name").toLowerCase();
          if (os.contains("mac")) {
              isMacOS = true;
          }
      }
  
      public UndecoratorController(Undecorator ud) {
          undecorator = ud;
      }
  
  
      /*
       * Actions
       */
      protected void maximizeOrRestore() {
          Stage stage = undecorator.getStage();
          if (maximized) {
              restoreSavedBounds(stage, false);
              undecorator.setShadow(true);
              savedBounds = null;
              maximized = false;
          } else {
              ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
              Screen screen = screensForRectangle.get(0);
              Rectangle2D visualBounds = screen.getVisualBounds();
  
              savedBounds = new BoundingBox(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
              undecorator.setShadow(false);
  
              stage.setX(visualBounds.getMinX());
              stage.setY(visualBounds.getMinY());
              stage.setWidth(visualBounds.getWidth());
              stage.setHeight(visualBounds.getHeight());
              maximized = true;
          }
      }
  
      public void saveBounds() {
          Stage stage = undecorator.getStage();
          savedBounds = new BoundingBox(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
      }
  
      public void saveFullScreenBounds() {
          Stage stage = undecorator.getStage();
          savedFullScreenBounds = new BoundingBox(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
      }
  
      public void restoreSavedBounds(Stage stage, boolean fullscreen) {
          stage.setX(savedBounds.getMinX());
          stage.setY(savedBounds.getMinY());
          stage.setWidth(savedBounds.getWidth());
          stage.setHeight(savedBounds.getHeight());
          savedBounds = null;
      }
  
      public void restoreFullScreenSavedBounds(Stage stage) {
          stage.setX(savedFullScreenBounds.getMinX());
          stage.setY(savedFullScreenBounds.getMinY());
          stage.setWidth(savedFullScreenBounds.getWidth());
          stage.setHeight(savedFullScreenBounds.getHeight());
          savedFullScreenBounds = null;
      }
  
      protected void setFullScreen(boolean value) {
          Stage stage = undecorator.getStage();
          stage.setFullScreen(value);
      }
  
      public void close() {
          final Stage stage = undecorator.getStage();
          Platform.runLater(() -> {
              stage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
          });
  
      }
  
      public void minimize() {
          if (!Platform.isFxApplicationThread()) // Ensure on correct thread else hangs X under Unbuntu
          {
              Platform.runLater(() -> {
                  _minimize();
              });
          } else {
              _minimize();
          }
      }
  
      private void _minimize() {
          Stage stage = undecorator.getStage();
          stage.setIconified(true);
      }
  
      /**
       * Stage resize management
       *
       * @param stage
       * @param node
       * @param PADDING
       * @param SHADOW
       */
      public void setStageResizableWith(final Stage stage, final Node node, int PADDING, int SHADOW) {
          RESIZE_PADDING = PADDING;
          SHADOW_WIDTH = SHADOW;
          node.setOnMouseClicked((MouseEvent mouseEvent) -> {
              if (undecorator.getStageStyle() != StageStyle.UTILITY && !stage.isFullScreen() && mouseEvent.getClickCount() > 1) {
                  if (mouseEvent.getSceneY() - SHADOW_WIDTH < MAXIMIZE_BORDER) {
                      undecorator.maximizeProperty().set(!undecorator.maximizeProperty().get());
                      mouseEvent.consume();
                  }
              }
          });
  
          node.setOnMousePressed((MouseEvent mouseEvent) -> {
              if (mouseEvent.isPrimaryButtonDown()) {
                  initX = mouseEvent.getScreenX();
                  initY = mouseEvent.getScreenY();
                  mouseEvent.consume();
              }
          });
          node.setOnMouseDragged((MouseEvent mouseEvent) -> {
              if (!mouseEvent.isPrimaryButtonDown() || (initX == -1 && initY == -1)) {
                  return;
              }
              if (stage.isFullScreen()) {
                  return;
              }
              if (mouseEvent.isStillSincePress()) {
                  return;
              }
              if (maximized) {
                  // Remove maximized state
                  undecorator.maximizeProperty.set(false);
                  return;
              } // Docked then moved, so restore state
              else if (savedBounds != null) {
                  undecorator.setShadow(true);
              }
  
              newX = mouseEvent.getScreenX();
              newY = mouseEvent.getScreenY();
              double deltax = newX - initX;
              double deltay = newY - initY;
  
              Cursor cursor = node.getCursor();
              if (Cursor.E_RESIZE.equals(cursor)) {
                  setStageWidth(stage, stage.getWidth() + deltax);
                  mouseEvent.consume();
              } else if (Cursor.NE_RESIZE.equals(cursor)) {
                  if (setStageHeight(stage, stage.getHeight() - deltay)) {
                      setStageY(stage, stage.getY() + deltay);
                  }
                  setStageWidth(stage, stage.getWidth() + deltax);
                  mouseEvent.consume();
              } else if (Cursor.SE_RESIZE.equals(cursor)) {
                  setStageWidth(stage, stage.getWidth() + deltax);
                  setStageHeight(stage, stage.getHeight() + deltay);
                  mouseEvent.consume();
              } else if (Cursor.S_RESIZE.equals(cursor)) {
                  setStageHeight(stage, stage.getHeight() + deltay);
                  mouseEvent.consume();
              } else if (Cursor.W_RESIZE.equals(cursor)) {
                  if (setStageWidth(stage, stage.getWidth() - deltax)) {
                      stage.setX(stage.getX() + deltax);
                  }
                  mouseEvent.consume();
              } else if (Cursor.SW_RESIZE.equals(cursor)) {
                  if (setStageWidth(stage, stage.getWidth() - deltax)) {
                      stage.setX(stage.getX() + deltax);
                  }
                  setStageHeight(stage, stage.getHeight() + deltay);
                  mouseEvent.consume();
              } else if (Cursor.NW_RESIZE.equals(cursor)) {
                  if (setStageWidth(stage, stage.getWidth() - deltax)) {
                      stage.setX(stage.getX() + deltax);
                  }
                  if (setStageHeight(stage, stage.getHeight() - deltay)) {
                      setStageY(stage, stage.getY() + deltay);
                  }
                  mouseEvent.consume();
              } else if (Cursor.N_RESIZE.equals(cursor)) {
                  if (setStageHeight(stage, stage.getHeight() - deltay)) {
                      setStageY(stage, stage.getY() + deltay);
                  }
                  mouseEvent.consume();
              }
          });
          node.setOnMouseMoved((MouseEvent mouseEvent) -> {
              if (maximized) {
                  setCursor(node, Cursor.DEFAULT);
                  return; // maximized mode does not support resize
              }
              if (stage.isFullScreen()) {
                  return;
              }
              if (!stage.isResizable()) {
                  return;
              }
              double x = mouseEvent.getX();
              double y = mouseEvent.getY();
              Bounds boundsInParent = node.getBoundsInParent();
              if (isRightEdge(x, y, boundsInParent)) {
                  if (y < RESIZE_PADDING + SHADOW_WIDTH) {
                      setCursor(node, Cursor.NE_RESIZE);
                  } else if (y > boundsInParent.getHeight() - (double) (RESIZE_PADDING + SHADOW_WIDTH)) {
                      setCursor(node, Cursor.SE_RESIZE);
                  } else {
                      setCursor(node, Cursor.E_RESIZE);
                  }
  
              } else if (isLeftEdge(x, y, boundsInParent)) {
                  if (y < RESIZE_PADDING + SHADOW_WIDTH) {
                      setCursor(node, Cursor.NW_RESIZE);
                  } else if (y > boundsInParent.getHeight() - (double) (RESIZE_PADDING + SHADOW_WIDTH)) {
                      setCursor(node, Cursor.SW_RESIZE);
                  } else {
                      setCursor(node, Cursor.W_RESIZE);
                  }
              } else if (isTopEdge(x, y, boundsInParent)) {
                  setCursor(node, Cursor.N_RESIZE);
              } else if (isBottomEdge(x, y, boundsInParent)) {
                  setCursor(node, Cursor.S_RESIZE);
              } else {
                  setCursor(node, Cursor.DEFAULT);
              }
          });
      }
  
      /**
       * Under Windows, the undecorator Stage could be been dragged below the Task
       * bar and then no way to grab it again... On Mac, do not drag above the
       * menu bar
       *
       * @param y
       */
      void setStageY(Stage stage, double y) {
          try {
              ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
              if (screensForRectangle.size() > 0) {
                  Screen screen = screensForRectangle.get(0);
                  Rectangle2D visualBounds = screen.getVisualBounds();
                  if (y < visualBounds.getHeight() - 30 && y + SHADOW_WIDTH >= visualBounds.getMinY()) {
                      stage.setY(y);
                  }
              }
          } catch (Exception e) {
              Undecorator.LOGGER.log(Level.SEVERE, "setStageY issue", e);
          }
      }
  
      boolean setStageWidth(Stage stage, double width) {
          if (width >= stage.getMinWidth()) {
              stage.setWidth(width);
              initX = newX;
              return true;
          }
          return false;
      }
  
      boolean setStageHeight(Stage stage, double height) {
          if (height >= stage.getMinHeight()) {
              stage.setHeight(height);
              initY = newY;
              return true;
          }
          return false;
      }
  
      /**
       * Allow this node to drag the Stage
       *
       * @param stage
       * @param node
       */
      public void setAsStageDraggable(final Stage stage, final Node node) {
          node.setOnMouseClicked((MouseEvent mouseEvent) -> {
              if (undecorator.getStageStyle() != StageStyle.UTILITY && !stage.isFullScreen() && stage.isResizable() && mouseEvent.getClickCount() > 1) {
                  if (mouseEvent.getSceneY() - SHADOW_WIDTH < MAXIMIZE_BORDER) {
                      undecorator.maximizeProperty().set(!undecorator.maximizeProperty().get());
                      mouseEvent.consume();
                  }
              }
          });
          node.setOnMousePressed((MouseEvent mouseEvent) -> {
              if (mouseEvent.isPrimaryButtonDown()) {
                  initX = mouseEvent.getScreenX();
                  initY = mouseEvent.getScreenY();
                  mouseEvent.consume();
              } else {
                  initX = -1;
                  initY = -1;
              }
          });
          node.setOnMouseDragged((MouseEvent mouseEvent) -> {
              if (!mouseEvent.isPrimaryButtonDown() || initX == -1) {
                  return;
              }
              if (stage.isFullScreen()) {
                  return;
              }
              /*
              * Long press generates drag event!
               */
              if (mouseEvent.isStillSincePress()) {
                  return;
              }
              if (maximized) {
                  // Remove Maximized state
                  undecorator.maximizeProperty.set(false);
                  // Center
                  stage.setX(mouseEvent.getScreenX() - stage.getWidth() / 2);
                  stage.setY(mouseEvent.getScreenY() - SHADOW_WIDTH);
              } // Docked then moved, so restore state
              else if (savedBounds != null) {
                  restoreSavedBounds(stage, false);
                  undecorator.setShadow(true);
                  // Center
                  stage.setX(mouseEvent.getScreenX() - stage.getWidth() / 2);
                  stage.setY(mouseEvent.getScreenY() - SHADOW_WIDTH);
              }
              double newX1 = mouseEvent.getScreenX();
              double newY1 = mouseEvent.getScreenY();
              double deltax = newX1 - initX;
              double deltay = newY1 - initY;
              initX = newX1;
              initY = newY1;
              setCursor(node, Cursor.HAND);
              stage.setX(stage.getX() + deltax);
              setStageY(stage, stage.getY() + deltay);
              testDock(stage, mouseEvent);
              mouseEvent.consume();
          });
          node.setOnMouseReleased((MouseEvent t) -> {
              if (stage.isResizable()) {
                  undecorator.setDockFeedbackInvisible();
                  setCursor(node, Cursor.DEFAULT);
                  initX = -1;
                  initY = -1;
                  dockActions(stage, t);
              }
          });
      }
  
      /**
       * (Humble) Simulation of Windows behavior on screen's edges Feedbacks
       */
      void testDock(Stage stage, MouseEvent mouseEvent) {
          if (!stage.isResizable()) {
              return;
          }
          int dockSide = getDockSide(mouseEvent);
          // Dock Left
          switch (dockSide) {
              case DOCK_LEFT: {
                  if (lastDocked == DOCK_LEFT) {
                      return;
                  }
                  ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
                  Screen screen = screensForRectangle.get(0);
                  Rectangle2D visualBounds = screen.getVisualBounds();
                  // Dock Left
                  double x = visualBounds.getMinX();
                  double y = visualBounds.getMinY();
                  double width = visualBounds.getWidth() / 2;
                  double height = visualBounds.getHeight();
                  undecorator.setDockFeedbackVisible(x, y, width, height);
                  lastDocked = DOCK_LEFT;
                  break;
              } // Dock Right
              case DOCK_RIGHT: {
                  if (lastDocked == DOCK_RIGHT) {
                      return;
                  }
                  ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
                  Screen screen = screensForRectangle.get(0);
                  Rectangle2D visualBounds = screen.getVisualBounds();
                  // Dock Right (visualBounds = (javafx.geometry.Rectangle2D) Rectangle2D [minX = 1440.0, minY=300.0, maxX=3360.0, maxY=1500.0, width=1920.0, height=1200.0])
                  double x = visualBounds.getMinX() + visualBounds.getWidth() / 2;
                  double y = visualBounds.getMinY();
                  double width = visualBounds.getWidth() / 2;
                  double height = visualBounds.getHeight();
                  undecorator.setDockFeedbackVisible(x, y, width, height);
                  lastDocked = DOCK_RIGHT;
                  break;
              } // Dock top
              case DOCK_TOP: {
                  if (lastDocked == DOCK_TOP) {
                      return;
                  }
                  ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
                  Screen screen = screensForRectangle.get(0);
                  Rectangle2D visualBounds = screen.getVisualBounds();
                  // Dock Left
                  double x = visualBounds.getMinX();
                  double y = visualBounds.getMinY();
                  double width = visualBounds.getWidth();
                  double height = visualBounds.getHeight();
                  undecorator.setDockFeedbackVisible(x, y, width, height);
                  lastDocked = DOCK_TOP;
                  break;
              }
              default:
                  undecorator.setDockFeedbackInvisible();
                  lastDocked = DOCK_NONE;
                  break;
          }
      }
  
      /**
       * Based on mouse position returns dock side
       *
       * @param mouseEvent
       * @return DOCK_LEFT,DOCK_RIGHT,DOCK_TOP
       */
      int getDockSide(MouseEvent mouseEvent) {
          double minX = Double.POSITIVE_INFINITY;
          double minY = Double.POSITIVE_INFINITY;
          double maxX = 0;
          double maxY = 0;
          // Get "big" screen bounds
          ObservableList<Screen> screens = Screen.getScreens();
          for (Screen screen : screens) {
              Rectangle2D visualBounds = screen.getVisualBounds();
              minX = Math.min(minX, visualBounds.getMinX());
              minY = Math.min(minY, visualBounds.getMinY());
              maxX = Math.max(maxX, visualBounds.getMaxX());
              maxY = Math.max(maxY, visualBounds.getMaxY());
          }
          // Dock Left
          if (mouseEvent.getScreenX() == minX) {
              return DOCK_LEFT;
          } else if (mouseEvent.getScreenX() >= maxX - 1) { // MaxX returns the width? Not width -1 ?!
              return DOCK_RIGHT;
          } else if (mouseEvent.getScreenY() <= minY) {   // Mac menu bar
              return DOCK_TOP;
          }
          return 0;
      }
  
      /**
       * (Humble) Simulation of Windows behavior on screen's edges Actions
       */
      void dockActions(Stage stage, MouseEvent mouseEvent) {
          ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
          Screen screen = screensForRectangle.get(0);
          Rectangle2D visualBounds = screen.getVisualBounds();
          // Dock Left
          if (mouseEvent.getScreenX() == visualBounds.getMinX()) {
              savedBounds = new BoundingBox(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
              stage.setX(visualBounds.getMinX());
              stage.setY(visualBounds.getMinY());
              // Respect Stage Max size
              double width = visualBounds.getWidth() / 2;
              if (stage.getMaxWidth() < width) {
                  width = stage.getMaxWidth();
              }
              stage.setWidth(width);
              double height = visualBounds.getHeight();
              if (stage.getMaxHeight() < height) {
                  height = stage.getMaxHeight();
              }
              stage.setHeight(height);
              undecorator.setShadow(false);
          } // Dock Right (visualBounds = [minX = 1440.0, minY=300.0, maxX=3360.0, maxY=1500.0, width=1920.0, height=1200.0])
          else if (mouseEvent.getScreenX() >= visualBounds.getMaxX() - 1) { // MaxX returns the width? Not width -1 ?!
              savedBounds = new BoundingBox(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
              stage.setX(visualBounds.getWidth() / 2 + visualBounds.getMinX());
              stage.setY(visualBounds.getMinY());
              // Respect Stage Max size
              double width = visualBounds.getWidth() / 2;
              if (stage.getMaxWidth() < width) {
                  width = stage.getMaxWidth();
              }
              stage.setWidth(width);
              double height = visualBounds.getHeight();
              if (stage.getMaxHeight() < height) {
                  height = stage.getMaxHeight();
              }
              stage.setHeight(height);
              undecorator.setShadow(false);
          } else if (mouseEvent.getScreenY() <= visualBounds.getMinY()) { // Mac menu bar
              undecorator.maximizeProperty.set(true);
          }
      }
  
      public boolean isRightEdge(double x, double y, Bounds boundsInParent) {
          return x < boundsInParent.getWidth() && x > boundsInParent.getWidth() - RESIZE_PADDING;
      }
  
      public boolean isTopEdge(double x, double y, Bounds boundsInParent) {
          return y >= 0 && y < RESIZE_PADDING;
      }
  
      public boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
          return y < boundsInParent.getHeight() && y > boundsInParent.getHeight() - RESIZE_PADDING;
      }
  
      public boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
          return x >= 0 && x < RESIZE_PADDING;
      }
  
      public void setCursor(Node n, Cursor c) {
          n.setCursor(c);
      }
  }
  