// Licensed 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 org.apache.tapestry5.corelib.components;

import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.*;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.func.F;
import org.apache.tapestry5.func.Flow;
import org.apache.tapestry5.func.Worker;
import org.apache.tapestry5.internal.util.CaptureResultCallback;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.runtime.RenderCommand;
import org.apache.tapestry5.runtime.RenderQueue;
import org.apache.tapestry5.services.Heartbeat;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.tree.*;

import java.util.List;

/**
 * A component used to render a recursive tree structure, with expandable/collapsable/selectable nodes. The data that is displayed
 * by the component is provided as a {@link TreeModel}. A secondary model, the {@link TreeExpansionModel}, is used
 * to track which nodes have been expanded. The optional {@link TreeSelectionModel} is used to track node selections (as currently
 * implemented, only leaf nodes may be selected).
 *
 * Tree is <em>not</em> a form control component; all changes made to the tree on the client
 * (expansions, collapsing, and selections) are propagated immediately back to the server.
 *
 * The Tree component uses special tricks to support recursive rendering of the Tree as necessary.
 *
 * @tapestrydoc
 * @since 5.3
 */
@SuppressWarnings(
        {"rawtypes", "unchecked", "unused"})
@Events({EventConstants.NODE_SELECTED, EventConstants.NODE_UNSELECTED})
@Import(module = "t5/core/tree")
public class Tree
{
    /**
     * The model that drives the tree, determining top level nodes and making revealing the overall structure of the
     * tree.
     */
    @Parameter(required = true, autoconnect = true)
    private TreeModel model;

    /**
     * Allows the container to specify additional CSS class names for the outer DIV element. The outer DIV
     * always has the class name "tree-container"; the additional class names are typically used to apply
     * a specific size and width to the component.
     */
    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL)
    private String className;

    /**
     * Optional parameter used to inform the container about what TreeNode is currently rendering; this
     * is primarily used when the label parameter is bound.
     */
    @Property
    @Parameter
    private TreeNode node;

    /**
     * Used to control the Tree's expansion model. By default, a persistent field inside the Tree
     * component stores a {@link DefaultTreeExpansionModel}. This parameter may be bound when more
     * control over the implementation of the expansion model, or how it is stored, is
     * required.
     */
    @Parameter(allowNull = false, value = "defaultTreeExpansionModel")
    private TreeExpansionModel expansionModel;

    /**
     * Used to control the Tree's selections. When this parameter is bound, then the client-side Tree
     * will track what is selected or not selected, and communicate this (via Ajax requests) up to
     * the server, where it will be recorded into the model. On the client-side, the Tree component will
     * add or remove the {@code selected-leaf-node-label} CSS class from {@code span.tree-label}
     * for the node.
     */
    @Parameter
    private TreeSelectionModel selectionModel;

    /**
     * Optional parameter used to inform the container about the value of the currently rendering TreeNode; this
     * is often preferable to the TreeNode, and like the node parameter, is primarily used when the label parameter
     * is bound.
     */
    @Parameter
    private Object value;

    /**
     * A renderable (usually a {@link Block}) that can render the label for a tree node.
     * This will be invoked after the {@link #value} parameter has been updated.
     */
    @Property
    @Parameter(value = "block:defaultRenderTreeNodeLabel")
    private RenderCommand label;

    @Environmental
    private JavaScriptSupport jss;

    @Inject
    private ComponentResources resources;

    @Persist
    private TreeExpansionModel defaultTreeExpansionModel;

    private static RenderCommand RENDER_CLOSE_TAG = new RenderCommand()
    {
        public void render(MarkupWriter writer, RenderQueue queue)
        {
            writer.end();
        }
    };

    private static RenderCommand RENDER_LABEL_SPAN = new RenderCommand()
    {
        public void render(MarkupWriter writer, RenderQueue queue)
        {
            writer.element("span", "class", "tree-label");
        }
    };

    private static RenderCommand MARK_SELECTED = new RenderCommand()
    {
        public void render(MarkupWriter writer, RenderQueue queue)
        {
            writer.getElement().attribute("class", "selected-leaf-node");
        }
    };

    @Environmental
    private Heartbeat heartbeat;

    /**
     * Renders a single node (which may be the last within its containing node).
     * This is a mix of immediate rendering, and queuing up various Blocks and Render commands
     * to do the rest. May recursively render child nodes of the active node. The label part
     * of the node is rendered inside a {@linkplain org.apache.tapestry5.services.Heartbeat heartbeat}.
     *
     * @param node
     *         to render
     * @param isLast
     *         if true, add "last" attribute to the LI element
     * @return command to render the node
     */
    private RenderCommand toRenderCommand(final TreeNode node, final boolean isLast)
    {
        return new RenderCommand()
        {
            public void render(MarkupWriter writer, RenderQueue queue)
            {
                // Inform the component's container about what value is being rendered
                // (this may be necessary to generate the correct label for the node).
                Tree.this.node = node;

                value = node.getValue();

                boolean isLeaf = node.isLeaf();

                writer.element("li");

                if (isLast)
                {
                    writer.attributes("class", "last");
                }

                if (isLeaf)
                {
                    writer.getElement().attribute("class", "leaf-node");
                }

                Element e = writer.element("span", "class", "tree-icon");

                if (!isLeaf && !node.getHasChildren())
                {
                    e.addClassName("empty-node");
                }

                boolean hasChildren = !isLeaf && node.getHasChildren();
                boolean expanded = hasChildren && expansionModel.isExpanded(node);

                writer.attributes("data-node-id", node.getId());

                if (expanded)
                {
                    // Inform the client side, so it doesn't try to fetch it a second time.
                    e.addClassName("tree-expanded");
                }

                writer.end(); // span.tree-icon

                // From here on in, we're pushing things onto the queue. Remember that
                // execution order is reversed from order commands are pushed.

                queue.push(RENDER_CLOSE_TAG); // li

                if (expanded)
                {
                    queue.push(new RenderNodes(node.getChildren()));
                }

                queue.push(RENDER_CLOSE_TAG);
                final RenderCommand startHeartbeat = new RenderCommand() {

                    @Override
                    public void render(MarkupWriter writer, RenderQueue queue) {
                        heartbeat.begin();
                    }
                };

                final RenderCommand endHeartbeat = new RenderCommand() {

                    @Override
                    public void render(MarkupWriter writer, RenderQueue queue) {
                        heartbeat.end();
                    }
                };

                queue.push(endHeartbeat);

                queue.push(label);

                queue.push(startHeartbeat);

                if (isLeaf && selectionModel != null && selectionModel.isSelected(node))
                {
                    queue.push(MARK_SELECTED);
                }

                queue.push(RENDER_LABEL_SPAN);

            }
        };
    }

    /**
     * Renders an &lt;ul&gt; element and renders each node recursively inside the element.
     */
    private class RenderNodes implements RenderCommand
    {
        private final Flow<TreeNode> nodes;

        public RenderNodes(List<TreeNode> nodes)
        {
            assert !nodes.isEmpty();

            this.nodes = F.flow(nodes).reverse();
        }

        public void render(MarkupWriter writer, final RenderQueue queue)
        {
            writer.element("ul");
            queue.push(RENDER_CLOSE_TAG);

            queue.push(toRenderCommand(nodes.first(), true));

            nodes.rest().each(new Worker<TreeNode>()
            {
                public void work(TreeNode element)
                {
                    queue.push(toRenderCommand(element, false));
                }
            });
        }

    }

    public String getContainerClass()
    {
        return className == null ? "tree-container" : "tree-container " + className;
    }

    public Link getTreeActionLink()
    {
        return resources.createEventLink("treeAction");
    }

    Object onTreeAction(@RequestParameter("t:nodeid") String nodeId,
                        @RequestParameter("t:action") String action)
    {
        if (action.equalsIgnoreCase("expand"))
        {
            return doExpandChildren(nodeId);
        }

        if (action.equalsIgnoreCase("markExpanded"))
        {
            return doMarkExpanded(nodeId);
        }

        if (action.equalsIgnoreCase("markCollapsed"))
        {
            return doMarkCollapsed(nodeId);
        }

        if (action.equalsIgnoreCase("select"))
        {
            return doUpdateSelected(nodeId, true);
        }

        if (action.equalsIgnoreCase("deselect"))
        {
            return doUpdateSelected(nodeId, false);
        }

        throw new IllegalArgumentException(String.format("Unexpected action: '%s' for Tree component.", action));
    }

    Object doExpandChildren(String nodeId)
    {
        TreeNode container = model.getById(nodeId);

        expansionModel.markExpanded(container);

        return new RenderNodes(container.getChildren());
    }

    Object doMarkExpanded(String nodeId)
    {
        expansionModel.markExpanded(model.getById(nodeId));

        return new JSONObject();
    }


    Object doMarkCollapsed(String nodeId)
    {
        expansionModel.markCollapsed(model.getById(nodeId));

        return new JSONObject();
    }

    Object doUpdateSelected(String nodeId, boolean selected)
    {
        TreeNode node = model.getById(nodeId);

        String event;

        if (selected)
        {
            selectionModel.select(node);

            event = EventConstants.NODE_SELECTED;
        } else
        {
            selectionModel.unselect(node);

            event = EventConstants.NODE_UNSELECTED;
        }

        CaptureResultCallback<Object> callback = CaptureResultCallback.create();

        resources.triggerEvent(event, new Object[]{nodeId}, callback);

        final Object result = callback.getResult();

        if (result != null)
        {
            return result;
        }

        return new JSONObject();
    }

    public TreeExpansionModel getDefaultTreeExpansionModel()
    {
        if (defaultTreeExpansionModel == null)
        {
            defaultTreeExpansionModel = new DefaultTreeExpansionModel();
        }

        return defaultTreeExpansionModel;
    }

    /**
     * Returns the actual {@link TreeExpansionModel} in use for this Tree component,
     * as per the expansionModel parameter. This is often, but not always, the same
     * as {@link #getDefaultTreeExpansionModel()}.
     */
    public TreeExpansionModel getExpansionModel()
    {
        return expansionModel;
    }

    /**
     * Returns the actual {@link TreeSelectionModel} in use for this Tree component,
     * as per the {@link #selectionModel} parameter.
     */
    public TreeSelectionModel getSelectionModel()
    {
        return selectionModel;
    }

    public Object getRenderRootNodes()
    {
        return new RenderNodes(model.getRootNodes());
    }

    /**
     * Clears the tree's {@link TreeExpansionModel}.
     */
    public void clearExpansions()
    {
        expansionModel.clear();
    }

    public Boolean getSelectionEnabled()
    {
        return selectionModel != null ? true : null;
    }
}
