/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.jfoenix.controls;

import javafx.animation.*;
import javafx.animation.Animation.Status;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiFunction;

/**
 * list of nodes that are toggled On/Off by clicking on the 1st node
 *
 * @author Shadi Shaheen
 * @version 1.0
 * @since 2016-03-09
 */
public class JFXNodesList extends VBox {

    private static void setConstraint(Node node, Object key, Object value) {
        if (value == null) {
            node.getProperties().remove(key);
        } else {
            node.getProperties().put(key, value);
        }
        if (node.getParent() != null) {
            node.getParent().requestLayout();
        }
    }

    private static Object getConstraint(Node node, Object key) {
        if (node.hasProperties()) {
            Object value = node.getProperties().get(key);
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private static final String ALIGN_NODE_CONSTRAINT = "align-node";

    /**
     * set a child node as the alignment controller when applying alignments on
     * the nodes list.
     * @param node
     * @param child
     */
    public static void alignNodeToChild(Node node, Node child) {
        setConstraint(node, ALIGN_NODE_CONSTRAINT, child);
    }

    public static Node getAlignNodeToChild(Node node) {
        return (Node)getConstraint(node, ALIGN_NODE_CONSTRAINT);
    }


    private final HashMap<Node, BiFunction<Boolean, Duration, Collection<KeyFrame>>> animationsMap = new HashMap<>();
    private boolean expanded = false;
    private final Timeline animateTimeline = new Timeline();

    /**
     * Creates empty nodes list.
     */
    public JFXNodesList() {
        setPickOnBounds(false);
        getStyleClass().add("jfx-nodes-list");
        setAlignment(Pos.TOP_CENTER);
    }

    /**
     * Adds node to list.
     * Note: this method must be called instead of getChildren().add().
     *
     * @param node {@link Region} to add
     */
    public void addAnimatedNode(Region node) {
        addAnimatedNode(node, null, true);
    }

    /**
     * Adds node to list.
     * Note: this method must be called instead of getChildren().add().
     *
     * @param node {@link Region} to add
     */
    public void addAnimatedNode(Region node, boolean addTriggerListener) {
        addAnimatedNode(node, null, addTriggerListener);
    }

    public void addAnimatedNode(Region node, BiFunction<Boolean, Duration, Collection<KeyFrame>> animationFramesFunction){
        addAnimatedNode(node, animationFramesFunction, true);
    }
    /**
     * add node to list with a specified callback that is triggered after the node animation is finished.
     * Note: this method must be called instead of getChildren().add().
     *
     * @param node {@link Region} to add
     */
    public void addAnimatedNode(Region node, BiFunction<Boolean, Duration, Collection<KeyFrame>> animationFramesFunction, boolean addTriggerListener) {
        // create container for the node if it's a sub nodes list
        if (node instanceof JFXNodesList) {
            StackPane container = new StackPane(node);
            container.setPickOnBounds(false);
            addAnimatedNode(container, animationFramesFunction, addTriggerListener);
            return;
        }
        // init node property and its listeners
        initChild(node, getChildren().size(), animationFramesFunction, addTriggerListener);
        // add the node
        getChildren().add(node);
    }

    private void initChild(Node node, int index, BiFunction<Boolean, Duration, Collection<KeyFrame>> animationFramesFunction, boolean addTriggerListener) {
        if (index > 0) {
            initNode(node);
            node.setVisible(false);
        } else {
            if (addTriggerListener) {
                if (node instanceof Button) {
                    node.addEventHandler(ActionEvent.ACTION, event -> animateList());
                } else {
                    node.addEventHandler(MouseEvent.MOUSE_CLICKED, event-> animateList());
                }
            }
            node.getStyleClass().add("trigger-node");
            node.setVisible(true);
        }

        if (animationFramesFunction == null && index != 0) {
            animationFramesFunction = initDefaultAnimation(node);
        } else if (animationFramesFunction == null && index == 0) {
            animationFramesFunction = (aBoolean, duration) -> new ArrayList<>();
        }

        animationsMap.put(node, animationFramesFunction);
    }

    @Override
    protected double computePrefWidth(double height) {
        if (!getChildren().isEmpty()) {
            return getChildren().get(0).prefWidth(height);
        }
        return super.computePrefWidth(height);
    }

    @Override
    protected double computePrefHeight(double width) {
        if (!getChildren().isEmpty()) {
            return getChildren().get(0).prefHeight(width);
        }
        return super.computePrefHeight(width);
    }

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

    @Override
    protected double computeMinWidth(double height) {
        return computePrefWidth(height);
    }

    @Override
    protected double computeMaxHeight(double width) {
        return computePrefHeight(width);
    }

    @Override
    protected double computeMaxWidth(double height) {
        return computePrefWidth(height);
    }

    private boolean performingLayout = false;
    @Override public void requestLayout() {
        if (performingLayout) {
            return;
        }
        super.requestLayout();
    }

    @Override
    protected void layoutChildren() {
        performingLayout = true;

        List<Node> children = getChildren();

        Insets insets = getInsets();
        double width = getWidth();
        double rotate = getRotate();
        double height = getHeight();
        double left = snapSpace(insets.getLeft());
        double right = snapSpace(insets.getRight());
        double space = snapSpace(getSpacing());
        boolean isFillWidth = isFillWidth();
        double contentWidth = width - left - right;


        Pos alignment = getAlignment();
        alignment = alignment == null ? Pos.TOP_CENTER : alignment;
        final HPos hpos = alignment.getHpos();
        final VPos vpos = alignment.getVpos();

        double y = 0;

        for (int i = 0, size = children.size(); i < size; i++) {
            Node child = children.get(i);
            child.autosize();
            child.setRotate(rotate % 180 == 0 ? rotate : -rotate);

            // init child node if not added using addAnimatedChild method
            if (!animationsMap.containsKey(child)) {
                if (child instanceof JFXNodesList) {
                    StackPane container = new StackPane(child);
                    container.setPickOnBounds(false);
                    getChildren().set(i, container);
                }
                initChild(child, i, null, true);
            }

            double x = 0;
            double childWidth = child.getLayoutBounds().getWidth();
            double childHeight = child.getLayoutBounds().getHeight();


            if(childWidth > width){
                switch (hpos) {
                    case CENTER:
                        x = snapPosition(contentWidth - childWidth) / 2;
                        break;
                }
                Node alignToChild = getAlignNodeToChild(child);
                if (alignToChild != null && child instanceof Parent) {
                    ((Parent) child).layout();
                    double alignedWidth = alignToChild.getLayoutBounds().getWidth();
                    double alignedX = alignToChild.getLayoutX();
                    if(childWidth / 2 > alignedX + alignedWidth){
                        alignedWidth = -(childWidth / 2 - (alignedWidth/2 + alignedX));
                    }else{
                        alignedWidth = alignedWidth/2 + alignedX - childWidth / 2;
                    }
                    child.setTranslateX(-alignedWidth * Math.cos(Math.toRadians(rotate)));
                    child.setTranslateY(alignedWidth * Math.cos(Math.toRadians(90 - rotate)));
                }
            }else{
                childWidth = contentWidth;
            }

            final Insets margin = getMargin(child);
            if (margin != null) {
                childWidth += margin.getLeft() + margin.getRight();
                childHeight += margin.getTop() + margin.getRight();
            }

            layoutInArea(child, x, y, childWidth, childHeight,
                /* baseline shouldn't matter */0,
                margin, isFillWidth, true, hpos, vpos);

            y += child.getLayoutBounds().getHeight() + space;
            if (margin != null) {
                y += margin.getTop() + margin.getBottom();
            }
            y = snapPosition(y);
        }

        performingLayout = false;
    }

    /**
     * Animates the list to show/hide the nodes.
     */
    public void animateList() {
        expanded = !expanded;
        if (animateTimeline.getStatus() == Status.RUNNING) {
            animateTimeline.stop();
        }
        animateTimeline.getKeyFrames().clear();
        createAnimation(expanded, animateTimeline);
        animateTimeline.play();
    }

    public void animateList(boolean expand){
        if ((expanded && !expand) || (!expanded && expand)) {
            animateList();
        }
    }

    public boolean isExpanded(){
        return expanded;
    }

    public Animation getListAnimation(boolean expanded){
        Timeline animation = new Timeline();
        createAnimation(expanded, animation);
        return animation;
    }

    private void createAnimation(boolean expanded, Timeline animation) {
        final ObservableList<Node> children = getChildren();
        double duration = 160 / (double) children.size();
        // show child nodes
        if (expanded) {
            for (Node child : children) {
                child.setVisible(true);
            }
        }

        // add child nodes animation
        for (int i = 1; i < children.size(); i++) {
            Node child = children.get(i);
            Collection<KeyFrame> frames = animationsMap.get(child).apply(expanded, Duration.millis(i * duration));
            animation.getKeyFrames().addAll(frames);
        }
        // add 1st element animation
        Collection<KeyFrame> frames = animationsMap.get(children.get(0)).apply(expanded, Duration.millis(160));
        animation.getKeyFrames().addAll(frames);

        // hide child nodes to allow mouse events on the nodes behind them
        if (!expanded) {
            animation.setOnFinished((finish) -> {
                for (int i = 1; i < children.size(); i++) {
                    children.get(i).setVisible(false);
                }
            });
        } else {
            animation.setOnFinished(null);
        }
    }

    private BiFunction<Boolean, Duration, Collection<KeyFrame>> initDefaultAnimation(Node child) {
        return (expanded, duration) -> {
            ArrayList<KeyFrame> frames = new ArrayList<>();
            frames.add(new KeyFrame(duration, event -> {
                child.setScaleX(expanded ? 1 : 0);
                child.setScaleY(expanded ? 1 : 0);
            },
                new KeyValue(child.scaleXProperty(), expanded ? 1 : 0, Interpolator.EASE_BOTH),
                new KeyValue(child.scaleYProperty(), expanded ? 1 : 0, Interpolator.EASE_BOTH)
            ));
            return frames;
        };
    }

    protected void initNode(Node node) {
        node.setScaleX(0);
        node.setScaleY(0);
        node.getStyleClass().add("sub-node");
    }
}