/*
 * 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);
    }
}