/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */
package org.richfaces.renderkit.util;

import java.util.Map;

import javax.faces.component.UIComponent;
import javax.faces.component.behavior.ClientBehaviorContext;
import javax.faces.context.FacesContext;

import org.ajax4jsf.component.AjaxClientBehavior;
import org.ajax4jsf.javascript.JSFunctionDefinition;
import org.ajax4jsf.javascript.JSReference;
import org.richfaces.renderkit.AjaxConstants;
import org.richfaces.renderkit.AjaxFunction;
import org.richfaces.renderkit.AjaxOptions;
import org.richfaces.renderkit.HtmlConstants;

import com.google.common.base.Strings;

/**
 * @author shura
 *         <p/>
 *         Some utilites for render AJAX components.
 */
public final class AjaxRendererUtils {
    public static final String BEGIN_EVENT_NAME = "begin";
    public static final String AJAX_ABORT_ATTR = "ignoreDupResponses";
    public static final String AJAX_AREAS_RENDERED = "org.ajax4jsf.areas.rendered";
    public static final String AJAX_DELAY_ATTR = "requestDelay";
    public static final String AJAX_QUEUE_ATTR = "eventsQueue";
    public static final String AJAX_SINGLE_ATTR = "ajaxSingle";
    public static final String AJAX_SINGLE_PARAMETER_NAME = "ajaxSingle";
    public static final String ONBEGIN_ATTR_NAME = "onbegin";
    /**
     * Attribute for keep JavaScript function name for call after complete request.
     */
    public static final String ONCOMPLETE_CONTENT_ID = "org.ajax4jsf.oncomplete";
    public static final String SIMILARITY_GROUPING_ID_ATTR = "similarityGroupingId";
    /**
     * Attribute for keep clientId of status component
     */
    public static final String STATUS_ATTR_NAME = "status";
    public static final String VALUE_ATTR = "value";
    public static final String QUEUE_ID_ATTRIBUTE = "queueId";
    private static final RendererUtils RENDERER_UTILS = RendererUtils.getInstance();

    /**
     * Static class - protect constructor
     */
    private AjaxRendererUtils() {
    }

    private static enum BehaviorOptionsData {
        begin {
            @Override
            public String getAttributeValue(AjaxClientBehavior behavior) {
                return behavior.getOnbegin();
            }
        },
        error {
            @Override
            public String getAttributeValue(AjaxClientBehavior behavior) {
                return behavior.getOnerror();
            }
        },
        queueId {
            @Override
            public String getAttributeValue(AjaxClientBehavior behavior) {
                return behavior.getQueueId();
            }
        };

        public abstract String getAttributeValue(AjaxClientBehavior behavior);
    }

    /**
     * Build JavaScript onclick event for given component
     *
     * @param uiComponent - component for build event
     * @param facesContext
     * @return <code>StringBuffer</code> with Javascript code
     */
    public static StringBuffer buildOnClick(UIComponent uiComponent, FacesContext facesContext) {
        return buildOnClick(uiComponent, facesContext, false);
    }

    /**
     * Build JavaScript onclick event for given component
     *
     * @param uiComponent - component for build event
     * @param facesContext
     * @param omitDefaultActionUrl - default action URL is not encoded if parameter is true
     * @return <code>StringBuffer</code> with Javascript code
     */
    public static StringBuffer buildOnClick(UIComponent uiComponent, FacesContext facesContext, boolean omitDefaultActionUrl) {
        return buildOnEvent(uiComponent, facesContext, HtmlConstants.ONCLICK_ATTRIBUTE, omitDefaultActionUrl);
    }

    /**
     * Build JavaScript event for component
     *
     * @param uiComponent - component for build event
     * @param facesContext
     * @param eventName - name of event
     * @return <code>StringBuffer</code> with Javascript code
     */
    public static StringBuffer buildOnEvent(UIComponent uiComponent, FacesContext facesContext, String eventName) {
        return buildOnEvent(uiComponent, facesContext, eventName, false);
    }

    /**
     * Build JavaScript event for component
     *
     * @param uiComponent - component for build event
     * @param facesContext
     * @param eventName - name of event
     * @param omitDefaultActionUrl - default action URL is not encoded if parameter is true
     * @return <code>StringBuffer</code> with Javascript code
     */
    public static StringBuffer buildOnEvent(UIComponent uiComponent, FacesContext facesContext, String eventName,
        boolean omitDefaultActionUrl) {
        StringBuffer onEvent = new StringBuffer();

        // if (null != eventName) {
        // String commandOnEvent = (String) uiComponent.getAttributes().get(
        // eventName);
        // if (commandOnEvent != null) {
        // onEvent.append(commandOnEvent);
        // onEvent.append(';');
        // }
        // }
        // JSFunction ajaxFunction = buildAjaxFunction(uiComponent, facesContext);
        // // Create formal parameter for non-input elements ???
        // // Link Control pseudo-object
        // // Options map. Possible options for function call :
        // // control - name of form control for submit.
        // // name - name for link control \
        // // value - value of control. - possible replace by parameters ?
        // // single true/false - submit all form or only one control.
        // // affected - array of element's ID for update on responce.
        // // oncomplete - function for call after complete request.
        // // status - id of request status component.
        // // parameters - map of parameters name/value for append on request.
        // // ..........
        // ajaxFunction.addParameter(buildEventOptions(facesContext, uiComponent, omitDefaultActionUrl));
        //
        // // appendAjaxSubmitParameters(facesContext, uiComponent, onEvent);
        // ajaxFunction.appendScript(onEvent);
        // if (uiComponent instanceof AjaxSupport) {
        // AjaxSupport support = (AjaxSupport) uiComponent;
        // if (support.isDisableDefault()) {
        // onEvent.append("; return false;");
        // }
        // }
        // LOG.debug(Messages.getMessage(Messages.BUILD_ONCLICK_INFO, uiComponent
        // .getId(), onEvent.toString()));
        return onEvent;
    }

    // TODO make this function private
    public static AjaxOptions buildEventOptions(FacesContext facesContext, UIComponent component) {
        AjaxOptions ajaxOptions = new AjaxOptions();
        appendComponentOptions(facesContext, component, ajaxOptions);

        Map<String, Object> parametersMap = RENDERER_UTILS.createParametersMap(facesContext, component);
        ajaxOptions.addParameters(parametersMap);

        return ajaxOptions;
    }

    private static AjaxOptions buildAjaxOptions(ClientBehaviorContext behaviorContext, AjaxClientBehavior ajaxBehavior) {
        FacesContext facesContext = behaviorContext.getFacesContext();
        UIComponent component = behaviorContext.getComponent();

        AjaxOptions ajaxOptions = new AjaxOptions();

        Map<String, Object> parametersMap = RENDERER_UTILS.createParametersMap(facesContext, component);
        ajaxOptions.addParameters(parametersMap);

        String ajaxStatusName = ajaxBehavior.getStatus();
        if (Strings.isNullOrEmpty(ajaxStatusName)) {
            ajaxStatusName = getAjaxStatus(component);
        }
        if (!Strings.isNullOrEmpty(ajaxStatusName)) {
            ajaxOptions.set(STATUS_ATTR_NAME, ajaxStatusName);
        }

        appenAjaxBehaviorOptions(behaviorContext, ajaxBehavior, ajaxOptions);

        return ajaxOptions;
    }

    private static boolean isNotEmpty(String value) {
        return (value != null) && (value.length() != 0);
    }

    private static void appenAjaxBehaviorOptions(ClientBehaviorContext behaviorContext, AjaxClientBehavior behavior,
        AjaxOptions ajaxOptions) {
        ajaxOptions.setParameter(AjaxConstants.BEHAVIOR_EVENT_PARAMETER, behaviorContext.getEventName());
        ajaxOptions.setBeforesubmitHandler(behavior.getOnbeforesubmit());

        for (BehaviorOptionsData optionsData : BehaviorOptionsData.values()) {
            String optionValue = optionsData.getAttributeValue(behavior);

            if (isNotEmpty(optionValue)) {
                ajaxOptions.set(optionsData.toString(), optionValue);
            }
        }
    }

    private static String getHandlerScript(FacesContext facesContext, UIComponent component, String attributeName,
        String eventName) {
        HandlersChain handlersChain = new HandlersChain(facesContext, component);
        String inlineHandler = (String) component.getAttributes().get(attributeName);

        handlersChain.addInlineHandlerAsValue(inlineHandler);
        handlersChain.addBehaviors(eventName);

        return handlersChain.toScript();
    }

    private static void appendComponentOptions(FacesContext facesContext, UIComponent component, AjaxOptions ajaxOptions) {
        String handlerScript = getHandlerScript(facesContext, component, ONBEGIN_ATTR_NAME, BEGIN_EVENT_NAME);
        if (isNotEmpty(handlerScript)) {
            ajaxOptions.set(BEGIN_EVENT_NAME, handlerScript);
        }

        String queueId = getQueueId(component);
        if (isNotEmpty(queueId)) {
            ajaxOptions.set(QUEUE_ID_ATTRIBUTE, queueId);
        }

        ajaxOptions.set("incId", "1");

        String status = getAjaxStatus(component);
        if (!Strings.isNullOrEmpty(status)) {
            ajaxOptions.set(STATUS_ATTR_NAME, status);
        }
    }

    // public static AjaxEventOptions buildEventOptions(FacesContext facesContext,
    // UIComponent uiComponent, Map<String, Object> params) {
    //
    // return buildEventOptions(facesContext, uiComponent, params, false);
    // }

    /**
     * @param facesContext
     * @param uiComponent
     * @return
     */
    // public static Map<String, Object> buildEventOptions(FacesContext facesContext,
    // UIComponent uiComponent, Map<String, Object> params, boolean omitDefaultActionUrl) {
    // String clientId = uiComponent.getClientId(facesContext);
    // Map<String, Object> componentAttributes = uiComponent.getAttributes();
    // Map<String, Object> options = new HashMap<String, Object>();
    //
    // UIComponent nestingContainer = (UIComponent) findAjaxContainer(
    // facesContext, uiComponent);
    // String containerClientId = nestingContainer.getClientId(facesContext);
    // if (containerClientId != null && !AjaxViewRoot.ROOT_ID.equals(containerClientId)) {
    // options.put("containerId", containerClientId);
    // }
    //
    // Map<String, Object> parameters = new HashMap<String, Object>();
    // UIComponent targetComponent = (uiComponent instanceof AjaxSupport)?uiComponent.getParent():uiComponent;
    // // UIForm form = getNestingForm(uiComponent);
    // // "input" - if assigned to html input element.
    // boolean input = targetComponent instanceof EditableValueHolder;
    // // Action component - button etc.
    // // boolean action = targetComponent instanceof ActionSource;
    //
    // boolean ajaxSingle = Boolean.TRUE.equals(componentAttributes
    // .get(AJAX_SINGLE_ATTR));
    // // For input components in single mode or without form submit input
    // // control )
    // if (ajaxSingle ) {
    // parameters.put(AJAX_SINGLE_PARAMETER_NAME, targetComponent.getClientId(facesContext));
    // // options.put("single", JSReference.TRUE);
    // if (input) {
    // options.put("control", JSReference.THIS);
    // }
    // }
    // // Control value for submit
    // String controlName;
    // Object controlValue;
    // // TODO - make compatible with JSF RI/MyFaces ? use submittedValue ( if
    // // any ) for UIInput, converted value for ValueHolder.
    // controlName = clientId;
    // controlValue = clientId;
    // parameters.put(controlName, controlValue);
    // AjaxContext ajaxContext = AjaxContext.getCurrentInstance(facesContext);
    //
    // String ajaxActionURL = ajaxContext.getAjaxActionURL(facesContext);
    // if (omitDefaultActionUrl) {
    // UIComponent form = getNestingForm(uiComponent);
    // if (form != null && !RENDERER_UTILS.isBooleanAttribute(form, "ajaxSubmit")) {
    // if (RENDERER_UTILS.getActionUrl(facesContext).equals(ajaxActionURL)) {
    // ajaxActionURL = null;
    // }
    // }
    // }
    //
    // if (ajaxActionURL != null) {
    // // Setup action URL. For portlet environment, it will be different from
    // // page.
    // options.put("actionUrl", ajaxActionURL);
    // }
    //
    // // Add application-wide Ajax parameters
    // parameters.putAll(ajaxContext.getCommonAjaxParameters());
    // // add child parameters
    // appendParameters(facesContext, uiComponent, parameters);
    //
    // if (params != null) {
    // parameters.putAll(params);
    // }
    //
    // if (!parameters.isEmpty()) {
    // options.put("parameters", parameters);
    // }
    // // parameter to render only current list of areas.
    // // if (isAjaxLimitRender(uiComponent)) {
    // // Set<? extends Object> ajaxAreas = getAjaxAreas(uiComponent);
    // // Set<String> areasIds = new HashSet<String>();
    // // if (null != ajaxAreas) {
    // // for (Iterator<? extends Object> iter = ajaxAreas.iterator(); iter.hasNext();) {
    // // String id = (String) iter.next();
    // // UIComponent comp = RendererUtils.getInstance().
    // // findComponentFor(uiComponent, id);
    // // if (null != comp) {
    // // areasIds.add(comp.getClientId(facesContext));
    // // } else {
    // // areasIds.add(id);
    // // }
    // // }
    // // }
    // // options.put("affected", areasIds);
    // // }
    // String oncomplete = getAjaxOncomplete(uiComponent);
    // if (null != oncomplete) {
    // options.put(ONCOMPLETE_ATTR_NAME, buildAjaxOncomplete(oncomplete));
    // }
    //
    // String beforeupdate = getAjaxOnBeforeDomUpdate(uiComponent);
    // if (null != beforeupdate) {
    // options.put(ONBEFOREDOMUPDATE_ATTR_NAME, buildAjaxOnBeforeDomUpdate(beforeupdate));
    // }
    //
    //
    // String status = getAjaxStatus(uiComponent);
    // if (null != status) {
    // options.put("status", status);
    // }
    // String queue = (String) componentAttributes.get(AJAX_QUEUE_ATTR);
    // String implicitQueue = null;
    //
    // Integer requestDelay = (Integer) componentAttributes
    // .get(AJAX_DELAY_ATTR);
    // if (null != requestDelay && requestDelay.intValue() > 0) {
    // options.put(AJAX_DELAY_ATTR, requestDelay);
    // if (null == queue) {
    // implicitQueue = clientId;
    // }
    // }
    // Boolean ignoreDupResponses = (Boolean) componentAttributes
    // .get(AJAX_ABORT_ATTR);
    // if (null != ignoreDupResponses && ignoreDupResponses.booleanValue()) {
    // options.put(AJAX_ABORT_ATTR, JSReference.TRUE);
    // if (null == queue) {
    // implicitQueue = clientId;
    // }
    // }
    //
    // if (null != queue) {
    // options.put(AJAX_QUEUE_ATTR, queue);
    // } else if (implicitQueue != null) {
    // options.put("implicitEventsQueue", clientId);
    // }
    //
    // ExternalContext externalContext = facesContext.getExternalContext();
    // String namespace = externalContext.encodeNamespace("");
    // if (namespace != null && namespace.length() != 0) {
    // options.put("namespace", namespace);
    // }
    //
    // String similarityGroupingId = (String) componentAttributes.get(SIMILARITY_GROUPING_ID_ATTR);
    // if (similarityGroupingId == null || similarityGroupingId.length() == 0) {
    // similarityGroupingId = clientId;
    // } else {
    // similarityGroupingId = externalContext.encodeNamespace(similarityGroupingId);
    // }
    //
    // options.put(SIMILARITY_GROUPING_ID_ATTR, similarityGroupingId);
    //
    // // request timeout.
    // Integer timeout = (Integer) componentAttributes.get("timeout");
    // if (null != timeout && timeout.intValue() > 0) {
    // options.put("timeout", timeout);
    // }
    // // Encoding for requests
    // String encoding = (String) componentAttributes.get("encoding");
    // if (null != encoding) {
    // options.put("encoding", encoding);
    // }
    // return options;
    // }
    // /**
    // * Create call to Ajax Submit function with first two parameters
    // *
    // * @param uiComponent
    // * @param facesContext
    // * @param functionName
    // * @return
    // */
    // public static JSFunction buildAjaxFunction(UIComponent uiComponent,
    // FacesContext facesContext) {
    // JSFunction ajaxFunction = buildAjaxFunction(uiComponent, facesContext,
    // AJAX_FUNCTION_NAME);
    // // client-side script must have reference to event-enabled object.
    // ajaxFunction.addParameter(new JSReference("event"));
    // return ajaxFunction;
    // }

    /**
     * Create call to Ajax Submit function with first two parameters
     *
     * @param facesContext
     * @param component
     * @return
     */
    public static AjaxFunction buildAjaxFunction(FacesContext facesContext, UIComponent component) {
        return new AjaxFunction(component.getClientId(facesContext), buildEventOptions(facesContext, component));
    }

    public static AjaxFunction buildAjaxFunction(ClientBehaviorContext behaviorContext, AjaxClientBehavior behavior) {
        Object source;

        AjaxOptions options = buildAjaxOptions(behaviorContext, behavior);

        if (behaviorContext.getSourceId() != null) {
            source = behaviorContext.getSourceId();
        } else {
            source = JSReference.THIS;

            FacesContext facesContext = behaviorContext.getFacesContext();
            UIComponent component = behaviorContext.getComponent();

            options.setAjaxComponent(component.getClientId(facesContext));
            options.set("sourceId", source);
        }

        return new AjaxFunction(source, options);
    }

    /**
     * Append common parameters ( array of affected areas, status area id, on complete function ) to JavaScript event string.
     *
     * @param uiComponent
     * @param onClick - buffer with JavaScript code eg... AJAX.Submit(form,this
     */

    // public static void appendAjaxSubmitParameters(FacesContext facesContext,
    // UIComponent uiComponent, StringBuffer onClick)
    // {
    // Set ajaxAreas = getAjaxAreas(uiComponent);
    // onClick.append(',');
    // // parameter to render only current list of areas.
    // if (isAjaxLimitRender(uiComponent) && ajaxAreas != null &&
    // ajaxAreas.size() > 0)
    // {
    // onClick.append('[');
    // Iterator areas = ajaxAreas.iterator();
    // boolean first = true;
    // while (areas.hasNext())
    // {
    // String element = (String) areas.next();
    // UIComponent component = uiComponent.findComponent(element);
    // if (null != component)
    // {
    // if (!first)
    // {
    // onClick.append(',');
    // }
    // else
    // {
    // first = false;
    // }
    // onClick.append('\'');
    // onClick.append(component.getClientId(facesContext));
    // onClick.append('\'');
    // }
    // }
    // onClick.append("]");
    // }
    // else
    // {
    // onClick.append("null");
    // }
    // // insert id of request status element.
    // onClick.append(',');
    // String status = getAjaxStatus(uiComponent);
    // if (null != status)
    // {
    // onClick.append('\'').append(status).append('\'');
    // }
    // else
    // {
    // onClick.append("null");
    // }
    // // insert function name for call after completed request
    // onClick.append(',');
    // String oncomplete = getAjaxOncomplete(uiComponent);
    // if (null != oncomplete)
    // {
    // onClick.append(oncomplete);
    // }
    // else
    // {
    // onClick.append("null");
    // }
    //
    // }

    /**
     * Get status area Id for given component.
     *
     * @param component
     * @return clientId of status area, or <code>null</code>
     */
    public static String getAjaxStatus(UIComponent component) {
        return (String) component.getAttributes().get(STATUS_ATTR_NAME);
    }

    public static String getQueueId(UIComponent component) {
        return (String) component.getAttributes().get(QUEUE_ID_ATTRIBUTE);
    }

    public static JSFunctionDefinition buildAjaxOncomplete(String body) {
        JSFunctionDefinition function = new JSFunctionDefinition("request", "event", "data");

        function.addToBody(body);

        return function;
    }

    public static JSFunctionDefinition buildAjaxOnBeforeDomUpdate(String body) {
        JSFunctionDefinition function = new JSFunctionDefinition("request", "event", "data");

        function.addToBody(body);

        return function;
    }
}
