/*
 * 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 org.apache.myfaces.custom.tree2;


import org.apache.myfaces.component.html.ext.HtmlGraphicImage;
import org.apache.myfaces.renderkit.html.util.AddResource;
import org.apache.myfaces.renderkit.html.util.AddResourceFactory;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils;
import org.apache.myfaces.shared_tomahawk.renderkit.RendererUtils;

import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UICommand;
import javax.faces.component.UIGraphic;
import javax.faces.component.UIViewRoot;
import javax.faces.component.UIParameter;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Iterator;
import java.net.URLDecoder;
import javax.servlet.http.Cookie;
import java.util.HashMap;

/**
 * @JSFRenderer
 *   renderKitId = "HTML_BASIC" 
 *   family = "org.apache.myfaces.HtmlTree2"
 *   type = "org.apache.myfaces.HtmlTree2"
 *   
 * @author Sean Schofield
 * @author Chris Barlow
 * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
 * @version $Revision: 987802 $ $Date: 2010-08-21 12:41:17 -0500 (Sat, 21 Aug 2010) $
 */
public class HtmlTreeRenderer extends Renderer
{
    protected static final String TOGGLE_SPAN = "org.apache.myfaces.tree.TOGGLE_SPAN";
    protected static final String ROOT_NODE_ID = "0";

    private static final String NAV_COMMAND = "org.apache.myfaces.tree.NAV_COMMAND";
    private static final String ENCODING = "UTF-8";
    private static final String ATTRIB_DELIM = ";";
    private static final String ATTRIB_KEYVAL = "=";
    private static final String NODE_STATE_EXPANDED = "x";
    private static final String NODE_STATE_CLOSED = "c";
    private static final String SEPARATOR = String.valueOf(NamingContainer.SEPARATOR_CHAR);
    private static final String IMAGE_PREFIX = "t2";
    private static final String TOGGLE_ID = "t2g";

    private static final int NOTHING = 0;
    private static final int CHILDREN = 1;
    private static final int EXPANDED = 2;
    private static final int LINES = 4;
    private static final int LAST = 8;
    private int counter = 0;

    // see superclass for documentation
    public boolean getRendersChildren()
    {
        return true;
    }

    private void restoreStateFromCookies(FacesContext context, UIComponent component) {
        String nodeId = null;
        HtmlTree tree = (HtmlTree)component;
        TreeState state = tree.getDataModel().getTreeState();

        Map cookieMap = context.getExternalContext().getRequestCookieMap();
        Cookie treeCookie = (Cookie)cookieMap.get(component.getId());
        if (treeCookie == null || treeCookie.getValue() == null)
        {
            return;
        }

        String nodeState = null;
        Map attrMap = getCookieAttr(treeCookie);
        Iterator i = attrMap.keySet().iterator();
        while (i.hasNext())
        {
            nodeId = (String)i.next();
            nodeState = (String)attrMap.get(nodeId);

            if (NODE_STATE_EXPANDED.equals(nodeState))
            {
                if (!state.isNodeExpanded(nodeId))
                {
                    state.toggleExpanded(nodeId);
                }
            }
            else if (NODE_STATE_CLOSED.equals(nodeState))
            {
                if (state.isNodeExpanded(nodeId))
                {
                    state.toggleExpanded(nodeId);
                }
            }
        }
    }


    public void decode(FacesContext context, UIComponent component)
    {
        super.decode(context, component);

        // see if one of the nav nodes was clicked, if so, then toggle appropriate node
        String nodeId = null;
        HtmlTree tree = (HtmlTree)component;

        if (tree.isClientSideToggle() && tree.isPreserveToggle())
        {
            restoreStateFromCookies(context, component);
        }
        else
        {
            nodeId = (String)context.getExternalContext().getRequestParameterMap().get(tree.getId() + SEPARATOR + NAV_COMMAND);

            if (nodeId == null || nodeId.equals(""))
            {
                return;
            }

            component.queueEvent(new ToggleExpandedEvent(component, nodeId));
        }
    }

    public void encodeBegin(FacesContext context, UIComponent component) throws IOException
    {
        // Fixes TOMAHAWK-437
        //HtmlTree tree = (HtmlTree)component;
        //if (!tree.getDataModel().getTreeState().isTransient()
        //        && tree.isClientSideToggle()
        //        && tree.isPreserveToggle())
        //{
        //    restoreStateFromCookies(context, component);
        //}

        // write javascript functions
        encodeJavascript(context, component);
    }

    /**
     * Renders the whole tree.  It generates a <code>&lt;span></code> element with an <code>id</code>
     * attribute if the component has been given an explicit ID.  The model nodes are rendered
     * recursively by the private <code>encodeNodes</code> method.
     *
     * @param context FacesContext
     * @param component The component whose children are to be rendered
     * @throws IOException
     */
    public void encodeChildren(FacesContext context, UIComponent component) throws IOException
    {
        HtmlTree tree = (HtmlTree)component;

        if (!component.isRendered()) return;
        if (tree.getValue() == null) return;

        ResponseWriter out = context.getResponseWriter();
        String clientId = null;

        if (component.getId() != null && !component.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX))
        {
            clientId = component.getClientId(context);
        }

        boolean isOuterSpanUsed = false;

        if (clientId != null)
        {
            isOuterSpanUsed = true;
            out.startElement("span", component);
            out.writeAttribute("id", clientId, "id");
        }

        boolean clientSideToggle = tree.isClientSideToggle();
        boolean showRootNode = tree.isShowRootNode();

        TreeState state = tree.getDataModel().getTreeState();
        TreeWalker walker = tree.getDataModel().getTreeWalker();
        walker.reset();
        walker.setTree(tree);

        walker.setCheckState(!clientSideToggle); // walk all nodes in client mode

        if (showRootNode)
        {
            // encode the tree (starting with the root node)
            if (walker.next())
            {
                encodeTree(context, out, tree, walker);
            }
        }
        else
        {
            // skip the root node
            walker.next();
            TreeNode rootNode = tree.getNode();

            // now mark the root as expanded (so we don't stop there)
            String rootNodeId = tree.getNodeId();
            if(!state.isNodeExpanded(rootNodeId))
            {
                state.toggleExpanded(rootNodeId);
            }

            // now encode each of the nodes in the level immediately below the root
            for (int i=0; i < rootNode.getChildCount(); i++)
            {
                if (walker.next())
                {
                    encodeTree(context, out, tree, walker);
                }
            }
        }

        // reset the current node id once we're done encoding everything
        tree.setNodeId(null);

        if (isOuterSpanUsed)
        {
            out.endElement("span");
        }
    }

    /**
     * Encodes the tree and its children.
     *
     * @param context FacesContext
     * @param out ResponseWriter
     * @param tree HtmlTree
     * @param walker TreeWalker
     * @throws IOException
     */
    protected void encodeTree(FacesContext context, ResponseWriter out, HtmlTree tree, TreeWalker walker)
        throws IOException
    {
        boolean clientSideToggle = tree.isClientSideToggle();

        // encode the current node
        HtmlRendererUtils.writePrettyLineSeparator(context);
        beforeNodeEncode(context, out, tree);
        encodeCurrentNode(context, out, tree);
        afterNodeEncode(context, out);

        // if client side toggling is on, add a span to be used for displaying/hiding children
        if (clientSideToggle)
        {
            String spanId = TOGGLE_SPAN + ":" + tree.getClientId(context) + ":" + tree.getNodeId();

            out.startElement(HTML.SPAN_ELEM, tree);
            out.writeAttribute(HTML.ID_ATTR, spanId, null);

            if (tree.isNodeExpanded())
            {
                out.writeAttribute(HTML.STYLE_ATTR, "display:block", null);
            }
            else
            {
                out.writeAttribute(HTML.STYLE_ATTR, "display:none", null);
            }
        }

        TreeNode node = tree.getNode();

        for (int i=0; i < node.getChildCount(); i++)
        {
            if (walker.next())
            {
                encodeTree(context, out, tree, walker);
            }
        }

        if (clientSideToggle)
        {
            out.endElement(HTML.SPAN_ELEM);
        }
    }

    /**
     * Encodes the current node.  It is protected so that custom {@link Renderer}s can extend it.  That might be useful
     * if you would like to render additional per node information besides the tree node.
     *
     * @param context FacesContext
     * @param out ResponseWriter
     * @param tree HtmlTree
     * @throws IOException
     */
    protected void encodeCurrentNode(FacesContext context, ResponseWriter out, HtmlTree tree)
        throws IOException
    {
        TreeNode node = tree.getNode();

        // set configurable values
        boolean showRootNode = tree.isShowRootNode();
        boolean showNav = tree.isShowNav();
        boolean showLines = tree.isShowLines();
        boolean clientSideToggle = tree.isClientSideToggle();

        if (clientSideToggle)
        {
            // we must show the nav icons if client side toggle is enabled (regardless of what user says)
            showNav = true;
        }

        UIComponent nodeTypeFacet = tree.getFacet(node.getType());
        UIComponent nodeImgFacet = null;

        if (nodeTypeFacet == null)
        {
            throw new IllegalArgumentException("Unable to locate facet with the name: " + node.getType());
        }

        // render node padding
        String[] pathInfo = tree.getPathInformation(tree.getNodeId());
        int paddingLevel = pathInfo.length - 1;

        for (int i = (showRootNode ? 0 : 1); i < paddingLevel; i++)
        {
            boolean lastChild = tree.isLastChild((String)pathInfo[i]);
            String lineSrc = (!lastChild && showLines)
                             ? getImageSrc(context, tree, "line-trunk.gif", true)
                             : getImageSrc(context, tree, "spacer.gif", true);
            String altString = (!lastChild && showLines)
                             ? "line trunk"
                             : "spacer";             
            

            out.startElement(HTML.TD_ELEM, tree);
            out.writeAttribute(HTML.WIDTH_ATTR, "19", null);
            out.writeAttribute(HTML.HEIGHT_ATTR, "100%", null);
            out.writeURIAttribute(HTML.STYLE_ATTR, "background-image:url('" + lineSrc + "');", null); //we use "style" because "background" is no valid xhtml attribute for td
            out.startElement(HTML.IMG_ELEM, tree);
            out.writeURIAttribute(HTML.SRC_ATTR, lineSrc, null);
            out.writeAttribute(HTML.WIDTH_ATTR, "19", null);
            out.writeAttribute(HTML.HEIGHT_ATTR, "18", null);
            out.writeAttribute(HTML.BORDER_ATTR, "0", null);
            out.writeAttribute(HTML.ALT_ATTR, altString, null); // placing a suitable description for the alt.
            out.endElement(HTML.IMG_ELEM);
            out.endElement(HTML.TD_ELEM);
        }

        if (showNav)
        {
            nodeImgFacet = encodeNavigation(context, out, tree);
        }

        // render node
        out.startElement(HTML.TD_ELEM, tree);
        if (nodeImgFacet != null)
        {
            RendererUtils.renderChild(context, nodeImgFacet);
        }
        RendererUtils.renderChild(context, nodeTypeFacet);
        out.endElement(HTML.TD_ELEM);
    }

    protected void beforeNodeEncode(FacesContext context, ResponseWriter out, HtmlTree tree)
        throws IOException
    {
        out.startElement(HTML.TABLE_ELEM, tree);
        out.writeAttribute(HTML.CELLPADDING_ATTR, "0", null);
        out.writeAttribute(HTML.CELLSPACING_ATTR, "0", null);
        out.writeAttribute(HTML.BORDER_ATTR, "0", null);
        out.startElement(HTML.TR_ELEM, tree);
    }

    protected void afterNodeEncode(FacesContext context, ResponseWriter out)
        throws IOException
    {
        out.endElement(HTML.TR_ELEM);
        out.endElement(HTML.TABLE_ELEM);
    }

    /**
     * Handles the encoding related to the navigation functionality.
     *
     * @param context FacesContext
     * @param out ResponseWriter
     * @param tree HtmlTree
     * @return The additional navigation image to display inside the node (if any).  Only used with client-side toggle.
     * @throws IOException
     */
    private UIComponent encodeNavigation(FacesContext context, ResponseWriter out, HtmlTree tree)
        throws IOException
    {
        TreeNode node = tree.getNode();
        String nodeId = tree.getNodeId();
        String spanId = TOGGLE_SPAN + ":" + tree.getClientId(context) + ":" + nodeId;//TOGGLE_SPAN + nodeId;
        boolean showLines = tree.isShowLines();
        boolean clientSideToggle = tree.isClientSideToggle();
        UIComponent nodeTypeFacet = tree.getFacet(node.getType());
        String navSrc = null;
        String altSrc = null;
        UIComponent nodeImgFacet = null;

        int bitMask = NOTHING;
        bitMask += (node.isLeaf()) ? NOTHING : CHILDREN;
        if (bitMask == CHILDREN) // if there are no children, ignore expand state -> more flexible with dynamic tree-structures
            bitMask += (tree.isNodeExpanded()) ? EXPANDED : NOTHING;
        bitMask += (tree.isLastChild(tree.getNodeId())) ? LAST : NOTHING;
        bitMask += (showLines) ? LINES : NOTHING;

        switch (bitMask)
        {
            case (NOTHING):

            case (LAST):
                navSrc = "spacer.gif";
                break;

            case (LINES):
                navSrc = "line-middle.gif";
                break;

            case (LINES + LAST):
                navSrc = "line-last.gif";
                break;

            case (CHILDREN):

            case (CHILDREN + LAST):
                navSrc = "nav-plus.gif";
                altSrc = "nav-minus.gif";
                break;

            case (CHILDREN + LINES):

                navSrc = "nav-plus-line-middle.gif";
                altSrc = "nav-minus-line-middle.gif";
                break;

            case (CHILDREN + LINES + LAST):

                navSrc = "nav-plus-line-last.gif";
                altSrc = "nav-minus-line-last.gif";
                break;

            case (CHILDREN + EXPANDED):

            case (CHILDREN + EXPANDED + LAST):
                navSrc = "nav-minus.gif";
                altSrc = "nav-plus.gif";
                break;

            case (CHILDREN + EXPANDED + LINES):
                navSrc = "nav-minus-line-middle.gif";
                altSrc = "nav-plus-line-middle.gif";
                break;

            case (CHILDREN + EXPANDED + LINES + LAST):
                navSrc = "nav-minus-line-last.gif";
                altSrc = "nav-plus-line-last.gif";
                break;

            // unacceptable bitmask combinations

            case (EXPANDED + LINES):
            case (EXPANDED + LINES + LAST):
            case (EXPANDED):
            case (EXPANDED + LAST):

                throw new IllegalStateException("Encountered a node ["+ nodeId + "] + with an illogical state.  " +
                                                "Node is expanded but it is also considered a leaf (a leaf cannot be considered expanded.");

            default:
                // catch all for any other combinations
                throw new IllegalArgumentException("Invalid bit mask of " + bitMask);
        }

        // adjust navSrc and altSrc so that the images can be retrieved using the extensions filter
        String navSrcUrl = getImageSrc(context, tree, navSrc, false);
        navSrc = getImageSrc(context, tree, navSrc, true);
        altSrc = getImageSrc(context, tree, altSrc, true);

        // render nav cell
        out.startElement(HTML.TD_ELEM, tree);
        out.writeAttribute(HTML.WIDTH_ATTR, "19", null);
        out.writeAttribute(HTML.HEIGHT_ATTR, "100%", null);
        out.writeAttribute("valign", "top", null);

        if ((bitMask & LINES)!=0 && (bitMask & LAST)==0)
        {
            //out.writeURIAttribute("background", getImageSrc(context, tree, "line-trunk.gif", true), null);
            out.writeURIAttribute(HTML.STYLE_ATTR, "background-image:url('" + getImageSrc(context, tree, "line-trunk.gif", true) + "');", null); 
        }

//      add the appropriate image for the nav control
        UIGraphic image = new HtmlGraphicImage();
        String imageId = IMAGE_PREFIX+(counter++);
        image.setId(imageId);
        image.setUrl(navSrcUrl);
        
        Map imageAttrs = image.getAttributes();
        imageAttrs.put(HTML.WIDTH_ATTR, "19");
        imageAttrs.put(HTML.HEIGHT_ATTR, "18");
        imageAttrs.put(HTML.BORDER_ATTR, "0");
        imageAttrs.put(HTML.ALT_ATTR, "Nav control image"); // placing a suitable description for the alt.

        if (clientSideToggle)
        {
            /**
             * With client side toggle, user has the option to specify open/closed images for the node (in addition to
             * the navigtion ones provided by the component.)
             */
            String expandImgSrc = "";
            String collapseImgSrc = "";
            String nodeImageId = "";

            UIComponent expandFacet = nodeTypeFacet.getFacet("expand");
            if (expandFacet != null)
            {
                UIGraphic expandImg = (UIGraphic)expandFacet;
                expandImgSrc = context.getApplication().getViewHandler().getResourceURL(context, expandImg.getUrl());
                if (expandImg.isRendered())
                {
                    expandImg.setId(imageId + NODE_STATE_EXPANDED);
                    expandImg.setParent(tree);
                    nodeImageId = expandImg.getClientId(context);
                    nodeImgFacet = expandFacet;
                }
            }

            UIComponent collapseFacet = nodeTypeFacet.getFacet("collapse");
            if (collapseFacet != null)
            {
                UIGraphic collapseImg = (UIGraphic)collapseFacet;
                collapseImgSrc = context.getApplication().getViewHandler().getResourceURL(context, collapseImg.getUrl());
                if (collapseImg.isRendered())
                {
                    collapseImg.setId(imageId + NODE_STATE_CLOSED);
                    collapseImg.setParent(tree);
                    nodeImageId = collapseImg.getClientId(context);
                    nodeImgFacet = collapseFacet;
                }
            }

            image.setParent(tree);
            if (node.getChildCount() > 0)
            {
                String onClick = new StringBuffer()
                    .append("treeNavClick('")
                    .append(spanId)
                    .append("', '")
                    .append(image.getClientId(context))
                    .append("', '")
                    .append(navSrc)
                    .append("', '")
                    .append(altSrc)
                    .append("', '")
                    .append(nodeImageId)
                    .append("', '")
                    .append(expandImgSrc)
                    .append("', '")
                    .append(collapseImgSrc)
                    .append("', '")
                    .append(tree.getId())
                    .append("', '")
                    .append(nodeId)
                    .append("');")
                    .toString();

                imageAttrs.put(HTML.ONCLICK_ATTR, onClick);
                imageAttrs.put(HTML.STYLE_ATTR, "cursor:pointer;cursor:hand");
            }
            RendererUtils.renderChild(context, image);

        }
        else
        {
            if (node.getChildCount() > 0)
            {
                // set up the expand control and remove whatever children (if any) this control had previously
                UICommand expandControl = tree.getExpandControl();
                expandControl.getChildren().clear();
                expandControl.setId(TOGGLE_ID);

                UIParameter param = new UIParameter();
                param.setName(tree.getId() + NamingContainer.SEPARATOR_CHAR + NAV_COMMAND);
                param.setValue(tree.getNodeId());
                expandControl.getChildren().add(param);
                expandControl.getChildren().add(image);

                RendererUtils.renderChild(context, expandControl);
            }
            else
            {
                RendererUtils.renderChild(context, image);
            }
        }
        out.endElement(HTML.TD_ELEM);

        return nodeImgFacet;
    }

    /**
     * Encodes any stand-alone javascript functions that are needed.  Uses either the extension filter, or a
     * user-supplied location for the javascript files.
     *
     * @param context FacesContext
     * @param component UIComponent
     * @throws IOException
     */
    private void encodeJavascript(FacesContext context, UIComponent component) throws IOException
    {
        // render javascript function for client-side toggle (it won't be used if user has opted for server-side toggle)
        String javascriptLocation = ((HtmlTree)component).getJavascriptLocation();
        AddResource addResource = AddResourceFactory.getInstance(context);
        if (javascriptLocation == null)
        {
            addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlTreeRenderer.class, "javascript/tree.js");
            addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlTreeRenderer.class, "javascript/cookielib.js");
        }
        else
        {
            addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/tree.js");
            addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/cookielib.js");
        }
    }

    /**
     * Get the appropriate image src location.  Uses the extension filter mechanism for images if no image
     * location has been specified.
     *
     * @param context The {@link FacesContext}.  NOTE: If <code>null</code> then context path information
     *    will not be used with extensions filter (assuming no imageLocation specified.)
     * @param component UIComponent
     * @param imageName The name of the image file to use.
     * @return The image src information.
     */
    private String getImageSrc(FacesContext context, UIComponent component, String imageName, boolean withContextPath)
    {
        String imageLocation = ((HtmlTree)component).getImageLocation();
        AddResource addResource = AddResourceFactory.getInstance(context);
        if (imageLocation == null)
        {
            return addResource.getResourceUri(context, HtmlTreeRenderer.class,
                                              "images/" + imageName, withContextPath);
        }
        else
        {
            return addResource.getResourceUri(context, imageLocation + "/" + imageName, withContextPath);
        }
    }

    /**
     * Helper method for getting the boolean value of an attribute.  If the attribute is not specified,
     * then return the default value.
     *
     * @param component The component for which the attributes are to be checked.
     * @param attributeName The name of the boolean attribute.
     * @param defaultValue The default value of the attribute (to be returned if no value found).
     * @return boolean
     */
    protected boolean getBoolean(UIComponent component, String attributeName, boolean defaultValue)
    {
        Boolean booleanAttr = (Boolean)component.getAttributes().get(attributeName);

        if (booleanAttr == null)
        {
            return defaultValue;
        }
        else
        {
            return booleanAttr.booleanValue();
        }
    }

    private Map getCookieAttr(Cookie cookie)
    {
        Map attribMap = new HashMap();
        try
        {
            String cookieValue = URLDecoder.decode(cookie.getValue(),ENCODING);
            String[] attribArray = cookieValue.split(ATTRIB_DELIM);
            for (int j = 0; j < attribArray.length; j++)
            {
                int index = attribArray[j].indexOf(ATTRIB_KEYVAL);
                String name = attribArray[j].substring(0, index);
                String value = attribArray[j].substring(index + 1);
                attribMap.put(name, value);
            }
        }
        catch (UnsupportedEncodingException e)
        {
            throw new RuntimeException("Error parsing tree cookies", e);
        }
        return attribMap;
    }
}
