/*
 * 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 java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.FacesEvent;
import javax.faces.event.FacesListener;
import javax.faces.event.PhaseId;

import org.apache.myfaces.shared_tomahawk.util.MessageUtils;
import org.apache.myfaces.tomahawk.util.Constants;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * TreeData is a {@link UIComponent} that supports binding data stored in a tree represented
 * by a {@link TreeNode} instance.  During iterative processing over the tree nodes in the
 * data model, the object for the current node is exposed as a request attribute under the key
 * specified by the <code>var</code> property.  {@link javax.faces.render.Renderer}s of this
 * component should use the appropriate facet to assist in rendering.
 *
 * @JSFComponent
 * @author Sean Schofield
 * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
 * @version $Revision: 940138 $ $Date: 2010-05-01 20:34:32 -0500 (Sat, 01 May 2010) $
 */
public class UITreeData extends UIComponentBase implements NamingContainer, Tree {
    private Log log = LogFactory.getLog(UITreeData.class);

    public static final String COMPONENT_TYPE = "org.apache.myfaces.UITree2";
    public static final String COMPONENT_FAMILY = "org.apache.myfaces.HtmlTree2";
    //private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.Tree2";
    private static final String MISSING_NODE = "org.apache.myfaces.tree2.MISSING_NODE";
    private static final int PROCESS_DECODES = 1;
    private static final int PROCESS_VALIDATORS = 2;
    private static final int PROCESS_UPDATES = 3;

    private TreeModel _cachedModel;
    private String _nodeId;
    private TreeNode _node;

    private Object _value;
    private String _var;
    private Map _saved = new HashMap();

    private TreeState _restoredState = null;

    /**
     * Constructor
     */
    public UITreeData()
    {
        //setRendererType(DEFAULT_RENDERER_TYPE);
    }


    // see superclass for documentation
    public String getFamily()
    {
        return COMPONENT_FAMILY;
    }

    //  see superclass for documentation
    public Object saveState(FacesContext context)
    {
        Object values[] = new Object[3];
        values[0] = super.saveState(context);
        values[1] = _var;
        values[2] = _restoredState;
        return ((Object) (values));
    }


    // see superclass for documentation
    public void restoreState(FacesContext context, Object state)
    {
        Object values[] = (Object[]) state;
        super.restoreState(context, values[0]);

        _var = (String)values[1];
        _restoredState = (TreeState) values[2];
    }

    public void encodeEnd(FacesContext context) throws IOException {
        super.encodeEnd(context);

        // prepare to save the tree state -- fix for MYFACES-618
        // should be done in saveState() but Sun RI does not call saveState() and restoreState()
        // with javax.faces.STATE_SAVING_METHOD = server
        TreeState state = getDataModel().getTreeState();
        if ( state == null)
        {
            // the model supplier has forgotten to return a valid state manager, but we need one
            state = new TreeStateBase();
        }
        // save the state with the component, unless it should explicitly not saved eg. session-scoped model and state
        _restoredState = (state.isTransient()) ? null : state;

    }

    public void queueEvent(FacesEvent event)
    {
        super.queueEvent(new FacesEventWrapper(event, getNodeId(), this));
    }


    public void broadcast(FacesEvent event) throws AbortProcessingException
    {
        if (event instanceof FacesEventWrapper)
        {
            FacesEventWrapper childEvent = (FacesEventWrapper) event;
            String currNodeId = getNodeId();
            setNodeId(childEvent.getNodeId());
            FacesEvent nodeEvent = childEvent.getFacesEvent();
            nodeEvent.getComponent().broadcast(nodeEvent);
            setNodeId(currNodeId);
            return;
        }
        else if(event instanceof ToggleExpandedEvent)
        {
            ToggleExpandedEvent toggleEvent = (ToggleExpandedEvent) event;
            String currentNodeId = getNodeId();
            setNodeId(toggleEvent.getNodeId());
            toggleExpanded();
            setNodeId(currentNodeId);
        }
        else
        {
            super.broadcast(event);
            return;
        }
    }


    // see superclass for documentation
    public void processDecodes(FacesContext context)
    {
        if (context == null) throw new NullPointerException("context");
        if (!isRendered()) return;

        _cachedModel = null;
        _saved = new HashMap();

        setNodeId(null);
        decode(context);

        processNodes(context, PROCESS_DECODES, getDataModel().getTreeWalker());
        // After processNodes is executed, the node active is the last one
        // we have to set it to null again to avoid inconsistency on outsider
        // code (just like UIData components does)
        setNodeId(null);

    }

    // see superclass for documentation
    public void processValidators(FacesContext context)
    {
        if (context == null) throw new NullPointerException("context");
        if (!isRendered()) return;

        processNodes(context, PROCESS_VALIDATORS, getDataModel().getTreeWalker());

        setNodeId(null);
    }


    // see superclass for documentation
    public void processUpdates(FacesContext context)
    {
        if (context == null) throw new NullPointerException("context");
        if (!isRendered()) return;

        processNodes(context, PROCESS_UPDATES, getDataModel().getTreeWalker());

        setNodeId(null);
    }

    // see superclass for documentation
    public String getClientId(FacesContext context)
    {
        String ownClientId = super.getClientId(context);
        if (_nodeId != null)
        {
            return ownClientId + NamingContainer.SEPARATOR_CHAR + _nodeId;
        } else
        {
            return ownClientId;
        }
    }

    // see superclass for documentation
    public void setValueBinding(String name, ValueBinding binding)
    {
        if ("value".equals(name))
        {
            _cachedModel = null;
        } else if ("nodeVar".equals(name) || "nodeId".equals(name) || "treeVar".equals(name))
        {
            throw new IllegalArgumentException("name " + name);
        }
        super.setValueBinding(name, binding);
    }

    // see superclass for documentation
    public void encodeBegin(FacesContext context) throws IOException
    {
        /**
         * The renderer will handle most of the encoding, but if there are any
         * error messages queued for the components (validation errors), we
         * do want to keep the saved state so that we can render the node with
         * the invalid value.
         */

        if (!keepSaved(context))
        {
            _saved = new HashMap();
        }

        // FIX for MYFACES-404
        // do not use the cached model the render phase
        _cachedModel = null;

        super.encodeBegin(context);
    }

    /**
     * Sets the value of the TreeData.
     *
     * @param value The new value
     *
     * @deprecated
     */
    public void setValue(Object value)
    {
        _cachedModel = null;
        _value = value;
    }


    /**
     * Gets the model of the TreeData -
     *  due to backwards-compatibility, this can also be retrieved by getValue.
     *
     * @return The value
     */
    public Object getModel()
    {
        return getValue();
    }

    /**
     * Sets the model of the TreeData -
     *  due to backwards-compatibility, this can also be set by calling setValue.
     *
     * @param model The new model
     */
    public void setModel(Object model)
    {
        setValue(model);
    }


    /**
     * Gets the value of the TreeData.
     *
     * @JSFProperty
     *   required="true"
     * @return The value
     *
     * @deprecated
     */
    public Object getValue()
    {
        if (_value != null) return _value;
        ValueBinding vb = getValueBinding("value");
        return vb != null ? vb.getValue(getFacesContext()) : null;
    }

    /**
     * Set the request-scope attribute under which the data object for the current node wil be exposed
     * when iterating.
     *
     * @param var The new request-scope attribute name
     */
    public void setVar(String var)
    {
        _var = var;
    }


    /**
     * Return the request-scope attribute under which the data object for the current node will be exposed
     * when iterating. This property is not enabled for value binding expressions.
     * 
     * @JSFProperty
     * @return The iterator attribute
     */
    public String getVar()
    {
        return _var;
    }

    /**
     * Calls through to the {@link TreeModel} and returns the current {@link TreeNode} or <code>null</code>.
     *
     * @return The current node
     */
    public TreeNode getNode()
    {
        return _node;
    }


    public String getNodeId()
    {
        return _nodeId;
    }


    public void setNodeId(String nodeId)
    {
        saveDescendantState();

        _nodeId = nodeId;

        TreeModel model = getDataModel();
        if (model == null)
        {
            return;
        }

        try
        {
            _node = model.getNodeById(nodeId);
        }
        //TODO: change to an own exception
        catch (IndexOutOfBoundsException aob)
        {
            /**
             * This might happen if we are trying to process a commandLink for a node that node that no longer
             * exists.  Instead of allowing a RuntimeException to crash the application, we will add a warning
             * message so the user can optionally display the warning.  Also, we will allow the user to provide
             * their own value binding method to be called so they can handle it how they see fit.
             */
            FacesMessage message = MessageUtils.getMessageFromBundle(Constants.TOMAHAWK_DEFAULT_BUNDLE, MISSING_NODE, new String[] {nodeId});
            message.setSeverity(FacesMessage.SEVERITY_WARN);
            FacesContext.getCurrentInstance().addMessage(getId(), message);

            /** @todo call hook */
            /** @todo figure out whether or not to abort this method gracefully */
        }

        restoreDescendantState();

        if (_var != null)
        {
            Map requestMap = getFacesContext().getExternalContext().getRequestMap();

            if (nodeId == null)
            {
                requestMap.remove(_var);
            } else
            {
                requestMap.put(_var, getNode());
            }
        }
    }

    /**
     * Gets an array of String containing the ID's of all of the {@link TreeNode}s in the path to
     * the specified node.  The path information will be an array of <code>String</code> objects
     * representing node ID's. The array will starting with the ID of the root node and end with
     * the ID of the specified node.
     *
     * @param nodeId The id of the node for whom the path information is needed.
     * @return String[]
     */
    public String[] getPathInformation(String nodeId)
    {
        return getDataModel().getPathInformation(nodeId);
    }

    /**
     * Indicates whether or not the specified {@link TreeNode} is the last child in the <code>List</code>
     * of children.  If the node id provided corresponds to the root node, this returns <code>true</code>.
     *
     * @param nodeId The ID of the node to check
     * @return boolean
     */
    public boolean isLastChild(String nodeId)
    {
        return getDataModel().isLastChild(nodeId);
    }

    /**
     * Returns a previously cached {@link TreeModel}, if any, or sets the cache variable to either the
     * current value (if its a {@link TreeModel}) or to a new instance of {@link TreeModel} (if it's a
     * {@link TreeNode}) with the provided value object as the root node.
     *
     * @return TreeModel
     */
    public TreeModel getDataModel()
    {
        if (_cachedModel != null)
        {
            return _cachedModel;
        }

        Object value = getValue();
        if (value != null)
        {
            if (value instanceof TreeModel)
            {
                _cachedModel = (TreeModel) value;
            }
            else if (value instanceof TreeNode)
            {
                _cachedModel = new TreeModelBase((TreeNode) value);
            } else
            {
                throw new IllegalArgumentException("Value must be a TreeModel or TreeNode");
            }
        }

        if (_restoredState != null)
            _cachedModel.setTreeState(_restoredState); // set the restored state (if there is one) on the model

        return _cachedModel;
    }

    /**
     * Epands all nodes by default.
     */
    public void expandAll()
    {
        toggleAll(true);
    }

    /**
     * Collapse all nodes by default.
     */
    public void collapseAll()
    {
        toggleAll(false);
    }

    /**
     * Toggles all of the nodes to either expanded or collapsed depending on the
     * parameter supplied.
     *
     * @param expanded Expand all of the nodes (a value of false indicates collapse
     * all nodes)
     */
    private void toggleAll(boolean expanded)
    {
        TreeWalker walker = getDataModel().getTreeWalker();
        walker.reset();

        TreeState state =  getDataModel().getTreeState();
        walker.setCheckState(false);
        walker.setTree(this);

        while(walker.next())
        {
            String id = getNodeId();
            if ((expanded && !state.isNodeExpanded(id)) || (!expanded && state.isNodeExpanded(id)))
            {
                state.toggleExpanded(id);
            }
        }
    }

    /**
     * Expands all of the nodes in the specfied path.
     * @param nodePath The path to expand.
     */
    public void expandPath(String[] nodePath)
    {
        getDataModel().getTreeState().expandPath(nodePath);
    }

    /**
     * Expands all of the nodes in the specfied path.
     * @param nodePath The path to expand.
     */
    public void collapsePath(String[] nodePath)
    {
        getDataModel().getTreeState().collapsePath(nodePath);
    }


    protected void processNodes(FacesContext context, int processAction, TreeWalker walker)
    {
        UIComponent facet = null;
        walker.reset();
        walker.setTree(this);

        while(walker.next())
        {
            TreeNode node = getNode();
            facet = getFacet(node.getType());

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

            switch (processAction)
            {
                case PROCESS_DECODES:

                    facet.processDecodes(context);
                    break;

                case PROCESS_VALIDATORS:

                    facet.processValidators(context);
                    break;

                case PROCESS_UPDATES:

                    facet.processUpdates(context);
                    break;
            }
        }

    }

    /**
     * To support using input components for the nodes (e.g., input fields, checkboxes, and selection
     * lists) while still only using one set of components for all nodes, the state held by the components
     * for the current node must be saved for a new node is selected.
     */
    private void saveDescendantState()
    {
        FacesContext context = getFacesContext();
        Iterator i = getFacets().values().iterator();
        while (i.hasNext())
        {
            UIComponent facet = (UIComponent) i.next();
            saveDescendantState(facet, context);
        }
    }

    /**
     * Overloaded helper method for the no argument version of this method.
     *
     * @param component The component whose state needs to be saved
     * @param context   FacesContext
     */
    private void saveDescendantState(UIComponent component, FacesContext context)
    {
        if (component instanceof EditableValueHolder)
        {
            EditableValueHolder input = (EditableValueHolder) component;
            String clientId = component.getClientId(context);
            SavedState state = (SavedState) _saved.get(clientId);
            if (state == null)
            {
                state = new SavedState();
                _saved.put(clientId, state);
            }
            state.setValue(input.getLocalValue());
            state.setValid(input.isValid());
            state.setSubmittedValue(input.getSubmittedValue());
            state.setLocalValueSet(input.isLocalValueSet());
        }

        List kids = component.getChildren();
        for (int i = 0; i < kids.size(); i++)
        {
            saveDescendantState((UIComponent) kids.get(i), context);
        }
    }


    /**
     * Used to configure a new node with the state stored previously.
     */
    private void restoreDescendantState()
    {
        FacesContext context = getFacesContext();
        Iterator i = getFacets().values().iterator();
        while (i.hasNext())
        {
            UIComponent facet = (UIComponent) i.next();
            restoreDescendantState(facet, context);
        }
    }

    /**
     * Overloaded helper method for the no argument version of this method.
     *
     * @param component The component whose state needs to be restored
     * @param context   FacesContext
     */
    private void restoreDescendantState(UIComponent component, FacesContext context)
    {
        String id = component.getId();
        component.setId(id); // forces the cilent id to be reset

        if (component instanceof EditableValueHolder)
        {
            EditableValueHolder input = (EditableValueHolder) component;
            String clientId = component.getClientId(context);
            SavedState state = (SavedState) _saved.get(clientId);
            if (state == null)
            {
                state = new SavedState();
            }
            input.setValue(state.getValue());
            input.setValid(state.isValid());
            input.setSubmittedValue(state.getSubmittedValue());
            input.setLocalValueSet(state.isLocalValueSet());
        }

        List kids = component.getChildren();
        for (int i = 0; i < kids.size(); i++)
        {
            restoreDescendantState((UIComponent)kids.get(i), context);
        }
        Map facets = component.getFacets();
        for(Iterator i = facets.values().iterator(); i.hasNext();)
        {
            restoreDescendantState((UIComponent)i.next(), context);
        }
    }

    /**
     * A regular bean with accessor methods for all state variables.
     *
     * @author Sean Schofield
     * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
     * @version $Revision: 940138 $ $Date: 2010-05-01 20:34:32 -0500 (Sat, 01 May 2010) $
     */
    private static class SavedState implements Serializable
    {
        private static final long serialVersionUID = 273343276957070557L;
        private Object submittedValue;
        private boolean valid = true;
        private Object value;
        private boolean localValueSet;

        Object getSubmittedValue()
        {
            return submittedValue;
        }

        void setSubmittedValue(Object submittedValue)
        {
            this.submittedValue = submittedValue;
        }

        boolean isValid()
        {
            return valid;
        }

        void setValid(boolean valid)
        {
            this.valid = valid;
        }

        Object getValue()
        {
            return value;
        }

        void setValue(Object value)
        {
            this.value = value;
        }

        boolean isLocalValueSet()
        {
            return localValueSet;
        }

        void setLocalValueSet(boolean localValueSet)
        {
            this.localValueSet = localValueSet;
        }
    }

    /**
     * Inner class used to wrap the original events produced by child components in the tree.
     * This will allow the tree to find the appropriate component later when its time to
     * broadcast the events to registered listeners.  Code is based on a similar private
     * class for UIData.
     */
    private static class FacesEventWrapper extends FacesEvent
    {
        private static final long serialVersionUID = -3056153249469828447L;
        private FacesEvent _wrappedFacesEvent;
        private String _nodeId;


        public FacesEventWrapper(FacesEvent facesEvent, String nodeId, UIComponent component)
        {
            super(component);
            _wrappedFacesEvent = facesEvent;
            _nodeId = nodeId;
        }


        public PhaseId getPhaseId()
        {
            return _wrappedFacesEvent.getPhaseId();
        }


        public void setPhaseId(PhaseId phaseId)
        {
            _wrappedFacesEvent.setPhaseId(phaseId);
        }


        public void queue()
        {
            _wrappedFacesEvent.queue();
        }


        public String toString()
        {
            return _wrappedFacesEvent.toString();
        }


        public boolean isAppropriateListener(FacesListener faceslistener)
        {
            // this event type is only intended for wrapping a real event
            return false;
        }


        public void processListener(FacesListener faceslistener)
        {
            throw new UnsupportedOperationException("This event type is only intended for wrapping a real event");
        }


        public FacesEvent getFacesEvent()
        {
            return _wrappedFacesEvent;
        }


        public String getNodeId()
        {
            return _nodeId;
        }
    }

    /**
     * Returns true if there is an error message queued for at least one of the nodes.
     *
     * @param context FacesContext
     * @return whether an error message is present
     */
    private boolean keepSaved(FacesContext context)
    {
        Iterator clientIds = _saved.keySet().iterator();
        while (clientIds.hasNext())
        {
            String clientId = (String) clientIds.next();
            Iterator messages = context.getMessages(clientId);
            while (messages.hasNext())
            {
                FacesMessage message = (FacesMessage) messages.next();
                if (message.getSeverity().compareTo(FacesMessage.SEVERITY_ERROR) >= 0)
                {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Toggle the expanded state of the current node.
     */
    public void toggleExpanded()
    {
        getDataModel().getTreeState().toggleExpanded(getNodeId());
    }

    /**
     * Indicates whether or not the current {@link TreeNode} is expanded.
     * @return boolean
     */
    public boolean isNodeExpanded()
    {
        return getDataModel().getTreeState().isNodeExpanded(getNodeId());
    }

    /**
     * Implements the {@link javax.faces.event.ActionListener} interface.  Basically, this
     * method is used to listen for node selection events (when a user has clicked on a
     * leaf node.)
     *
     * @param event ActionEvent
     */
    public void setNodeSelected(ActionEvent event)
    {
        getDataModel().getTreeState().setSelected(getNodeId());
    }

    /**
     * Indicates whether or not the current {@link TreeNode} is selected.
     * @return boolean
     */
    public boolean isNodeSelected()
    {
        return (getNodeId() != null) ? getDataModel().getTreeState().isSelected(getNodeId()) : false;
    }
}
