 /*
  * RHQ Management Platform
  * Copyright (C) 2005-2008 Red Hat, Inc.
  * All rights reserved.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License, version 2, as
  * published by the Free Software Foundation, and/or the GNU Lesser
  * General Public License, version 2.1, also as published by the Free
  * Software Foundation.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  * GNU General Public License and the GNU Lesser General Public License
  * for more details.
  *
  * You should have received a copy of the GNU General Public License
  * and the GNU Lesser General Public License along with this program;
  * if not, write to the Free Software Foundation, Inc.,
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
package org.rhq.core.gui.util;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.el.ValueExpression;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIOutput;
import javax.faces.component.UIParameter;
import javax.faces.component.UIForm;
import javax.faces.component.UIViewRoot;
import javax.faces.component.html.HtmlColumn;
import javax.faces.component.html.HtmlCommandButton;
import javax.faces.component.html.HtmlCommandLink;
import javax.faces.component.html.HtmlDataTable;
import javax.faces.component.html.HtmlForm;
import javax.faces.component.html.HtmlGraphicImage;
import javax.faces.component.html.HtmlMessage;
import javax.faces.component.html.HtmlOutputLabel;
import javax.faces.component.html.HtmlOutputLink;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.component.html.HtmlPanelGrid;
import javax.faces.component.html.HtmlPanelGroup;
import javax.faces.context.FacesContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.richfaces.component.html.HtmlSeparator;
import org.richfaces.component.html.HtmlSimpleTogglePanel;

 /**
 * A set of utility methods for working with JSF {@link UIComponent}s.
 *
 * @author Ian Springer
 */
public abstract class FacesComponentUtility {
    public static final String NO_STYLE_CLASS = null;

    private static final String DISABLED_ATTRIBUTE_NAME = "disabled";
    private static final String READONLY_ATTRIBUTE = "readonly";
    private static final String UNSET_ATTRIBUTE = "unset";
    private static final String OVERRIDE_ATTRIBUTE = "override";

    @NotNull
    public static HtmlPanelGroup createBlockPanel(FacesComponentIdFactory idFactory, String styleClass) {
        HtmlPanelGroup panel = createComponent(HtmlPanelGroup.class, idFactory);
        panel.setLayout("block");
        panel.setStyleClass(styleClass);
        return panel;
    }

    @NotNull
    public static HtmlPanelGroup addBlockPanel(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String styleClass) {
        HtmlPanelGroup panel = createBlockPanel(idFactory, styleClass);
        parent.getChildren().add(panel);
        return panel;
    }

    @NotNull
    public static HtmlPanelGroup addInlinePanel(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String styleClass) {
        HtmlPanelGroup panel = createComponent(HtmlPanelGroup.class, idFactory);
        panel.setStyleClass(styleClass);
        parent.getChildren().add(panel);
        return panel;
    }

    @NotNull
    public static HtmlSimpleTogglePanel addSimpleTogglePanel(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String label) {
        HtmlSimpleTogglePanel panel = createComponent(HtmlSimpleTogglePanel.class, idFactory);
        panel.setLabel(label);

        //panel.setAjaxSingle(true);
        panel.setSwitchType("client");
        parent.getChildren().add(panel);
        return panel;
    }

    @NotNull
    public static HtmlSeparator addSeparator(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory) {
        HtmlSeparator separator = createComponent(HtmlSeparator.class, idFactory);
        parent.getChildren().add(separator);
        return separator;
    }

    @NotNull
    public static HtmlOutputText addOutputText(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, CharSequence value, String styleClass) {
        HtmlOutputText text = createComponent(HtmlOutputText.class, idFactory);
        text.setValue(value);
        text.setStyleClass(styleClass);
        parent.getChildren().add(text);
        return text;
    }

    @NotNull
    public static HtmlOutputText createOutputText(FacesComponentIdFactory idFactory, CharSequence value,
        String styleClass) {
        HtmlOutputText text = createComponent(HtmlOutputText.class, idFactory);
        if (value != null)
            value = value.toString().trim();
        text.setValue(value);
        text.setStyleClass(styleClass);
        return text;
    }

    @NotNull
    public static HtmlColumn addColumn(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, CharSequence headerText, String headerStyle) {
        HtmlColumn column = createComponent(HtmlColumn.class, idFactory);
        HtmlOutputText header = new HtmlOutputText();
        header.setValue(headerText);
        header.setStyleClass(headerStyle);
        column.setHeader(header);
        parent.getChildren().add(column);
        return column;
    }

    @NotNull
    public static HtmlOutputText addVerbatimText(@NotNull
    UIComponent parent, CharSequence html) {
        HtmlOutputText outputText = createComponent(HtmlOutputText.class, null);
        outputText.setEscape(false);
        outputText.setValue(html);
        parent.getChildren().add(outputText);
        return outputText;
    }

    @NotNull
    public static UIOutput addButton(UIComponent parent, String label, String styleClass) {
        return FacesComponentUtility.addVerbatimText(parent, "<button class=\"" + styleClass + "\">" + label
            + "</button>");
    }

    @NotNull
    public static UIOutput addJavaScript(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, @Nullable
    CharSequence src, @Nullable
    CharSequence script) {
        UIOutput output = createComponent(UIOutput.class, idFactory);
        output.getAttributes().put("escape", Boolean.FALSE);
        StringBuilder value = new StringBuilder();
        value.append("\n<script language=\"JavaScript\" type=\"text/javascript\"");
        if (src != null) {
            value.append(" src=\"").append(src).append("\"");
        }

        value.append(">");
        if (script != null) {
            value.append("\n").append(script).append("\n");
        }

        value.append("</script>\n");
        output.setValue(value);
        parent.getChildren().add(output);
        return output;
    }

    @NotNull
    public static <T extends UIComponent> List<T> getDescendantsOfType(UIComponent component, Class<T> componentClass) {
        List<T> results = new ArrayList<T>();
        getDescendantsOfType(component, componentClass, results);
        return results;
    }

    @SuppressWarnings("unchecked")
    private static <T extends UIComponent> void getDescendantsOfType(UIComponent component, Class<T> componentClass,
        List<T> results) {
        for (int i = 0; i < component.getChildren().size(); i++) {
            UIComponent child = component.getChildren().get(i);
            if (componentClass.isAssignableFrom(child.getClass())) {
                results.add((T) child);
            }

            getDescendantsOfType(child, componentClass, results);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T extends UIComponent> T getAncestorOfType(UIComponent component, Class<T> componentClass) {
        if (component == null) {
            return null;
        }

        if (componentClass.isAssignableFrom(component.getClass())) {
            return (T) component;
        } else {
            return getAncestorOfType(component.getParent(), componentClass);
        }
    }

    @NotNull
    public static HtmlOutputLabel addLabel(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, UIInput associatedInput, String value, String styleClass) {
        HtmlOutputLabel label = createComponent(HtmlOutputLabel.class, idFactory);
        label.setFor(associatedInput.getId());
        label.setValue(value);
        label.setStyleClass(styleClass);
        parent.getChildren().add(label);
        return label;
    }

    @NotNull
    public static HtmlGraphicImage addGraphicImage(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String url, String alt) {
        HtmlGraphicImage image = FacesComponentUtility.createComponent(HtmlGraphicImage.class, idFactory);
        image.setUrl(url);
        image.setAlt(alt);
        image.setTitle(alt);
        parent.getChildren().add(image);
        return image;
    }

    @NotNull
    public static HtmlOutputLink addOutputLink(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String value) {
        HtmlOutputLink link = FacesComponentUtility.createComponent(HtmlOutputLink.class, idFactory);
        link.setValue(value);
        parent.getChildren().add(link);
        return link;
    }

    @NotNull
    public static UIParameter addParameter(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String name, String value) {
        UIParameter parameter = createComponent(UIParameter.class, idFactory);
        parameter.setName(name);
        parameter.setValue(value);
        parent.getChildren().add(parameter);
        return parameter;
    }

    @NotNull
    public static HtmlForm addForm(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory) {
        HtmlForm form = createComponent(HtmlForm.class, idFactory);
        form.setPrependId(false);
        parent.getChildren().add(form);
        return form;
    }

    @NotNull
    public static HtmlCommandButton addCommandButton(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String value, String styleClass) {
        HtmlCommandButton button = createComponent(HtmlCommandButton.class, idFactory);
        button.setValue(value);
        button.setStyleClass(styleClass);
        parent.getChildren().add(button);
        return button;
    }

    @NotNull
    public static HtmlCommandLink addCommandLink(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory) {
        HtmlCommandLink link = FacesComponentUtility.createComponent(HtmlCommandLink.class, idFactory);
        parent.getChildren().add(link);
        return link;
    }

    @NotNull
    public static HtmlMessage addMessage(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String associatedComponentId, String styleClass) {
        HtmlMessage message = FacesComponentUtility.createComponent(HtmlMessage.class, idFactory);
        message.setFor(associatedComponentId);
        message.setShowDetail(true);
        message.setErrorClass(styleClass);
        message.setWarnClass(styleClass);
        message.setFatalClass(styleClass);
        message.setInfoClass(styleClass);
        parent.getChildren().add(message);
        return message;
    }

    @NotNull
    public static HtmlPanelGrid addPanelGrid(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, int columns, String styleClass) {
        HtmlPanelGrid panelGrid = FacesComponentUtility.createComponent(HtmlPanelGrid.class, idFactory);
        panelGrid.setColumns(columns);
        panelGrid.setStyleClass(styleClass);
        parent.getChildren().add(panelGrid);
        return panelGrid;
    }

    @NotNull
    public static HtmlDataTable addDataTable(@NotNull
    UIComponent parent, FacesComponentIdFactory idFactory, String styleClass) {
        HtmlDataTable dataTable = FacesComponentUtility.createComponent(HtmlDataTable.class, idFactory);
        dataTable.setStyleClass(styleClass);
        parent.getChildren().add(dataTable);
        return dataTable;
    }

    @Nullable
    public static String getExpressionAttribute(@NotNull
    UIComponent component, @NotNull
    String attribName) {
        return getExpressionAttribute(component, attribName, String.class);
    }

    @Nullable
    public static <T> T getExpressionAttribute(@NotNull
    UIComponent component, @NotNull
    String attribName, @NotNull
    Class<T> expectedType) {
        ValueExpression valueExpression = component.getValueExpression(attribName);
        T attribValue = (valueExpression != null) ? FacesExpressionUtility.getValue(valueExpression, expectedType)
            : null;
        return attribValue;
    }

    @NotNull
    public static Map<String, String> getParameters(@NotNull
    UIComponent component) {
        // Use a LinkedHashMap, so the order of the parameters is maintained.
        Map<String, String> params = new LinkedHashMap<String, String>();
        List<UIComponent> children = component.getChildren();
        for (UIComponent child : children) {
            if (child instanceof UIParameter) {
                UIParameter param = (UIParameter) child;
                Object paramValue = param.getValue();
                params.put(param.getName(), (paramValue != null) ? paramValue.toString() : null);
            }
        }

        return params;
    }

    /**
     * Creates a {@link UIComponent} of the specified type.
     *
     * @param  componentClass the type of {@link UIComponent} to create
     *
     * @return a {@link UIComponent} of the specified type
     */
    public static <T extends UIComponent> T createComponent(Class<T> componentClass) {
        return createComponent(componentClass, null);
    }

    /**
     * Creates a {@link UIComponent} of the specified type.
     *
     * @param  componentClass the type of {@link UIComponent} to create
     * @param  idFactory      a factory to use to create a unique id for the new component
     *
     * @return a {@link UIComponent} of the specified type
     */
    @SuppressWarnings("unchecked")
    public static <T extends UIComponent> T createComponent(Class<T> componentClass, @Nullable
    FacesComponentIdFactory idFactory) {
        Application application = FacesContext.getCurrentInstance().getApplication();
        String componentType = getComponentType(componentClass);
        T component = (T) application.createComponent(componentType);
        if (idFactory == null) {
            idFactory = new DefaultFacesComponentIdFactory();
        }
        component.setId(idFactory.createUniqueId());
        return component;
    }

    /**
     * Sets the "disabled" attribute on the specified component to the provided value. NOTE: Not all components will
     * render this attribute (e.g. HtmlInputHidden does not render it).
     *
     * @param component a component
     * @param disabled  the new value for the "disabled" attribute
     */
    public static void setDisabled(UIComponent component, boolean disabled) {
        component.getAttributes().put(DISABLED_ATTRIBUTE_NAME, disabled);
    }

    /**
     * Gets the value of the "disabled" attribute on the specified component, or <code>false</code> if the component does
     * not expose the attribute.
     *
     * @param  component a component
     *
     * @return the value of the "disabled" attribute on the specified component, or <code>false</code> if the component
     *         does not expose the attribute
     */
    public static boolean isDisabled(UIComponent component) {
        Boolean disabled = (Boolean) component.getAttributes().get(DISABLED_ATTRIBUTE_NAME);
        return (disabled != null) && disabled;
    }

    /**
     * Sets the "readonly" attribute on the specified component to the provided value. NOTE: Only certain components
     * will render this attribute (e.g. HtmlInputText, HtmlInputSecret, and HtmlInputTextarea render it).
     *
     * @param input    an input component
     * @param readonly the new value for the "readonly" attribute
     */
    public static void setReadonly(UIInput input, boolean readonly) {
        input.getAttributes().put(READONLY_ATTRIBUTE, readonly);
    }

    /**
     * Gets the value of the "readonly" attribute on the specified component, or <code>false</code> if the component does
     * not expose the attribute.
     *
     * @param  component a component
     *
     * @return the value of the "readonly" attribute on the specified component, or <code>false</code> if the component
     *         does not expose the attribute
     */
    public static boolean isReadonly(UIComponent component) {
        Boolean readonly = (Boolean) component.getAttributes().get(READONLY_ATTRIBUTE);
        return (readonly != null) && readonly;
    }

    /**
     * Sets the "unset" attribute on the specified component to the provided value. NOTE: This is a RHQ-specific
     * attribute, which is only rendered for the Config component.
     *
     * @param input an input component
     * @param unset the new value for the "unset" attribute
     */
    public static void setUnset(UIInput input, boolean unset) {
        input.getAttributes().put(UNSET_ATTRIBUTE, unset);
    }

    /**
     * Gets the value of the "unset" attribute on the specified component, or <code>false</code> if the component does
     * not expose the attribute.
     *
     * @param  component a component
     *
     * @return the value of the "unset" attribute on the specified component, or <code>false</code> if the component does
     *         not expose the attribute
     */
    public static boolean isUnset(UIComponent component) {
        Boolean unset = (Boolean) component.getAttributes().get(UNSET_ATTRIBUTE);
        return (unset != null) && unset;
    }

    public static void setOverride(UIInput input, boolean override) {
        input.getAttributes().put(OVERRIDE_ATTRIBUTE, override);
    }

    public static boolean isOverride(UIComponent component) {
        Boolean override = (Boolean) component.getAttributes().get(OVERRIDE_ATTRIBUTE);
        return (override == null || override);
    }

    /**
     * Removes the specified component from its parent if it has one.
     *
     * @param component a JSF component
     */
    public static void detachComponent(UIComponent component) {
        UIComponent parent = component.getParent();
        if (parent != null) {
            Iterator<UIComponent> children = parent.getChildren().iterator();
            while (children.hasNext()) {
                UIComponent child = children.next();
                if ((child.getId() != null) && child.getId().equals(component.getId())) {
                    children.remove(); // remove the last child returned by next()
                    break;
                }
            }
        }
    }

    /**
     * Returns the enclosing UIForm component, if any, from the specified component's ancestry.
     *
     * @param component the component
     *
     * @return the enclosing UIForm component, if any, from the specified component's ancestry
     *
     * @throws IllegalArgumentException if more than one UIForm is found in the component's ancestry
     */
    @Nullable
     public static UIForm getEnclosingForm(UIComponent component)
         throws IllegalArgumentException
     {
         UIForm ret = null;
         while (component != null)
         {
             if (component instanceof UIForm)
             {
                 if (ret != null)
                 {
                     // Cannot have a doubly-nested form!
                     throw new IllegalArgumentException();
                 }
                 ret = (UIForm) component;
             }
             component = component.getParent();
         }
         return ret;
     }

    private static <T extends UIComponent> String getComponentType(Class<T> componentClass) {
        String componentType;
        try {
            componentType = (String) componentClass.getDeclaredField("COMPONENT_TYPE").get(null);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        return componentType;
    }

     private static class DefaultFacesComponentIdFactory implements FacesComponentIdFactory {
        public String createUniqueId()
        {
            // NOTE: Our id's *must* begin with UIViewRoot.UNIQUE_ID_PREFIX ("j_id") to prevent HtmlOutputText
            //       components from being rendered as SPAN elements
            //       (see com.sun.faces.renderkit.html_basic.TextRenderer#getEndTextToRender()).
            return UIViewRoot.UNIQUE_ID_PREFIX + "_RHQ_" + UUID.randomUUID();
        }
    }
}