/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.ui.ajax.ajax;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

import javax.el.ExpressionFactory;
import javax.el.MethodExpression;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.component.behavior.ClientBehaviorHint;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.AjaxBehaviorListener;
import javax.faces.event.BehaviorEvent;

import org.richfaces.cdk.annotations.Attribute;
import org.richfaces.cdk.annotations.JsfBehavior;
import org.richfaces.cdk.annotations.JsfBehaviorRenderer;
import org.richfaces.cdk.annotations.Signature;
import org.richfaces.cdk.annotations.Tag;
import org.richfaces.cdk.annotations.TagType;
import org.richfaces.ui.attribute.AjaxProps;
import org.richfaces.ui.behavior.ClientBehavior;
import org.richfaces.util.Sets;

/**
 * <p>
 * The &lt;r:ajax&gt; behavior allows Ajax capability to be added to a non-Ajax component. The non-Ajax component must
 * implement the ClientBehaviorHolder interface for all the event attributes that support behavior rendering.
 * </p>
 * @author Anton Belevich
 */
@JsfBehavior(id = AjaxBehavior.BEHAVIOR_ID, tag = @Tag(name = "ajax", handlerClass = AjaxHandler.class, type = TagType.Facelets), renderer = @JsfBehaviorRenderer(type = AjaxBehaviorRenderer.RENDERER_TYPE))
public class AjaxBehavior extends ClientBehavior implements AjaxClientBehavior, AjaxProps {

    public static final String BEHAVIOR_ID = "org.richfaces.ui.behavior.Ajax";

    private static final Set<ClientBehaviorHint> HINTS = Collections.unmodifiableSet(EnumSet.of(ClientBehaviorHint.SUBMITTING));

    enum PropertyKeys {
        data,
        event,
        execute,
        onbeforedomupdate,
        onbegin,
        oncomplete,
        onerror,
        queueId,
        render,
        status,
        disabled,
        limitRender,
        listener,
        immediate,
        bypassUpdates,
        onbeforesubmit
    }

    private Set<String> execute;
    private Set<String> render;
    @SuppressWarnings("unused")
    @Attribute(generate = false, signature = @Signature(returnType = Void.class, parameters = AjaxBehaviorEvent.class))
    private MethodExpression listener;

    @Override
    public void setLiteralAttribute(String name, Object value) {

        ExpressionFactory expFactory = getFacesContext().getApplication().getExpressionFactory();

        if (compare(PropertyKeys.data, name)) {
            setData(value);
        } else if (compare(PropertyKeys.execute, name)) {
            setExecute(toSet(PropertyKeys.execute, value));
        } else if (compare(PropertyKeys.render, name)) {
            setRender(toSet(PropertyKeys.render, value));
        } else if (compare(PropertyKeys.onbeforedomupdate, name)) {
            setOnbeforedomupdate((String) value);
        } else if (compare(PropertyKeys.onbegin, name)) {
            setOnbegin((String) value);
        } else if (compare(PropertyKeys.oncomplete, name)) {
            setOncomplete((String) value);
        } else if (compare(PropertyKeys.onerror, name)) {
            setOnerror((String) value);
        } else if (compare(PropertyKeys.queueId, name)) {
            setQueueId((String) value);
        } else if (compare(PropertyKeys.status, name)) {
            setStatus((String) value);
        } else if (compare(PropertyKeys.disabled, name)) {
            value = expFactory.coerceToType(value, Boolean.class);
            setDisabled((Boolean) value);
        } else if (compare(PropertyKeys.limitRender, name)) {
            value = expFactory.coerceToType(value, Boolean.class);
            setLimitRender((Boolean) value);
        } else if (compare(PropertyKeys.immediate, name)) {
            value = expFactory.coerceToType(value, Boolean.class);
            setImmediate((Boolean) value);
        } else if (compare(PropertyKeys.bypassUpdates, name)) {
            value = expFactory.coerceToType(value, Boolean.class);
            setBypassUpdates((Boolean) value);
        } else if (compare(PropertyKeys.onbeforesubmit, name)) {
            setOnbeforesubmit((String) value);
        }
    }

    private Set<String> toSet(Serializable propertyName, Object value) {

        Set<String> result = null;

        result = Sets.asSet(value);

        if (result == null) {
            throw new FacesException(
                propertyName.toString()
                    + "' attribute value must be Collection, List, array, String, comma-separated String, whitespace-separate String'");
        }

        return result;
    }

    /**
     * Serialized (on default with JSON) data passed to the client by a developer on an AJAX request. It's accessible
     * via "event.data" syntax. Both primitive types and complex types such as arrays and collections can be serialized
     * and used with data
     */
    @Attribute
    public Object getData() {
        return getStateHelper().eval(PropertyKeys.data);
    }

    public void setData(Object data) {
        getStateHelper().put(PropertyKeys.data, data);
    }

    /**
     * Name of JavaScript event property (click, change, etc.) of parent component that triggers the behavior.
     * If the event attribute is not defined, the behavior is triggered on the event that normally provides
     * interaction behavior for the parent component
     */
    @Attribute
    public String getEvent() {
        return (String) getStateHelper().eval(PropertyKeys.event);
    }

    public void setEvent(String event) {
        getStateHelper().put(PropertyKeys.event, event);
    }

    /**
     * Method expression referencing a method that will be called when an AjaxBehaviorEvent has been broadcast for the listener.
     */
    @Attribute
    public MethodExpression getListener() {
        return (MethodExpression) getStateHelper().eval(PropertyKeys.listener);
    }

    public void setListener(MethodExpression listener) {
        getStateHelper().put(PropertyKeys.listener, listener);
    }

    /**
     * Ids of components that will participate in the "execute" portion of the Request Processing Lifecycle.
     * Can be a single id, a space or comma separated list of Id's, or an EL Expression evaluating to an array or Collection.
     * Any of the keywords "@this", "@form", "@all", "@none", "@region" may be specified in the identifier list.
     * Some components make use of additional keywords
     */
    @Attribute
    public Collection<String> getExecute() {
        return getCollectionValue(PropertyKeys.execute, execute);
    }

    public void setExecute(Collection<String> execute) {
        this.execute = copyToSet(execute);
        clearInitialState();
    }

    /**
     * The client-side script method to be called after the ajax response comes back, but before the DOM is updated
     */
    @Attribute
    public String getOnbeforedomupdate() {
        return (String) getStateHelper().eval(PropertyKeys.onbeforedomupdate);
    }

    public void setOnbeforedomupdate(String onbeforedomupdate) {
        getStateHelper().put(PropertyKeys.onbeforedomupdate, onbeforedomupdate);
    }

    /**
     * The client-side script method to be called before an ajax request.
     */
    @Attribute
    public String getOnbegin() {
        return (String) getStateHelper().eval(PropertyKeys.onbegin);
    }

    public void setOnbegin(String onbegin) {
        getStateHelper().put(PropertyKeys.onbegin, onbegin);
    }

    /**
     * The client-side script method to be called before the AJAX request is submitted
     */
    @Attribute
    public String getOnbeforesubmit() {
        return (String) getStateHelper().eval(PropertyKeys.onbeforesubmit);
    }

    public void setOnbeforesubmit(String onbeforesubmit) {
        getStateHelper().put(PropertyKeys.onbeforesubmit, onbeforesubmit);
    }

    /**
     * The client-side script method to be called after the DOM is updated
     */
    @Attribute
    public String getOncomplete() {
        return (String) getStateHelper().eval(PropertyKeys.oncomplete);
    }

    public void setOncomplete(String oncomplete) {
        getStateHelper().put(PropertyKeys.oncomplete, oncomplete);
    }

    /**
     * The client-side script method to be called when an error has occurred during Ajax communications
     */
    @Attribute
    public String getOnerror() {
        return (String) getStateHelper().eval(PropertyKeys.onerror);
    }

    public void setOnerror(String onerror) {
        getStateHelper().put(PropertyKeys.onerror, onerror);
    }

    /**
     * Identify the name of the destination queue
     */
    @Attribute
    public String getQueueId() {
        return (String) getStateHelper().eval(PropertyKeys.queueId);
    }

    public void setQueueId(String queueId) {
        getStateHelper().put(PropertyKeys.queueId, queueId);
    }

    /**
     * Ids of components that will participate in the "render" portion of the Request Processing Lifecycle.
     * Can be a single id, a space or comma separated list of Id's, or an EL Expression evaluating to an array or Collection.
     * Any of the keywords "@this", "@form", "@all", "@none", "@region" may be specified in the identifier list.
     * Some components make use of additional keywords
     */
    @Attribute
    public Collection<String> getRender() {
        return getCollectionValue(PropertyKeys.render, render);
    }

    public void setRender(Collection<String> render) {
        this.render = copyToSet(render);
        clearInitialState();
    }

    /**
     * ID of the request status component
     */
    @Attribute
    public String getStatus() {
        return (String) getStateHelper().eval(PropertyKeys.status);
    }

    public void setStatus(String status) {
        getStateHelper().put(PropertyKeys.status, status);
    }

    /**
     * If "true", do not initiate an ajax request when the associated event is observed
     */
    @Attribute
    public boolean isDisabled() {
        return (Boolean) getStateHelper().eval(PropertyKeys.disabled, false);
    }

    public void setDisabled(boolean disabled) {
        getStateHelper().put(PropertyKeys.disabled, disabled);
    }

    /**
     * If "true", render only those ids specified in the "render" attribute, forgoing the render of the auto-rendered panels
     */
    @Attribute
    public boolean isLimitRender() {
        return (Boolean) getStateHelper().eval(PropertyKeys.limitRender, false);
    }

    public void setLimitRender(boolean limitRender) {
        getStateHelper().put(PropertyKeys.limitRender, limitRender);
    }

    /**
     * Flag indicating that, if this component is activated by the user, notifications should be delivered to interested
     * listeners and actions immediately (that is, during Apply Request Values phase) rather than waiting until Invoke
     * Application phase.
     */
    @Attribute
    public boolean isImmediate() {
        return (Boolean) getStateHelper().eval(PropertyKeys.immediate, false);
    }

    public void setImmediate(boolean immediate) {
        getStateHelper().put(PropertyKeys.immediate, immediate);
    }

    /**
     * If "true", after process validations phase it skips updates of model beans on a force render response.
     * It can be used for validating components input
     */
    @Attribute
    public boolean isBypassUpdates() {
        return (Boolean) getStateHelper().eval(PropertyKeys.bypassUpdates, false);
    }

    public void setBypassUpdates(boolean bypassUpdates) {
        getStateHelper().put(PropertyKeys.bypassUpdates, bypassUpdates);
    }

    @Override
    public String getRendererType() {
        return BEHAVIOR_ID;
    }

    @Override
    public Set<ClientBehaviorHint> getHints() {
        return HINTS;
    }

    public void addAjaxBehaviorListener(AjaxBehaviorListener listener) {
        addBehaviorListener(listener);
    }

    public void removeAjaxBehaviorListener(AjaxBehaviorListener listener) {
        removeBehaviorListener(listener);
    }

    @Override
    public void broadcast(BehaviorEvent event) throws AbortProcessingException {
        if (this.equals(event.getBehavior()) && event instanceof BypassUpdatesAjaxBehaviorEvent) {
            FacesContext.getCurrentInstance().renderResponse();
        }

        super.broadcast(event);
    }

    private Object saveSet(Serializable propertyName, Set<String> set) {
        if ((set == null) || set.isEmpty()) {
            return null;
        }

        int size = set.size();

        if (size == 1) {
            return set.toArray(new String[1])[0];
        }

        return set.toArray(new String[size]);
    }

    private Set<String> restoreSet(Serializable propertyName, Object state) {
        if (state == null) {
            return null;
        }

        Set<String> set = toSet(propertyName, state);
        return set;
    }

    private Set<String> copyToSet(Collection<String> collection) {
        return Collections.unmodifiableSet(new HashSet<String>(collection));
    }

    private Collection<String> getCollectionValue(Serializable propertyName, Collection<String> collection) {
        if (collection != null) {
            return collection;
        }

        Collection<String> result = null;

        ValueExpression expression = getValueExpression(propertyName.toString());
        if (expression != null) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            Object value = expression.getValue(facesContext.getELContext());

            if (value != null) {

                if (value instanceof Collection) {
                    return (Collection<String>) value;
                }

                result = toSet(propertyName, value);
            }
        }
        return result == null ? Collections.<String>emptyList() : result;
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        if (state != null) {
            Object[] values = (Object[]) state;
            super.restoreState(context, values[0]);

            if (values.length != 1) {
                execute = restoreSet(PropertyKeys.execute, values[1]);
                render = restoreSet(PropertyKeys.render, values[2]);

                clearInitialState();
            }
        }
    }

    @Override
    public Object saveState(FacesContext context) {
        Object parentState = super.saveState(context);
        Object state = null;

        if (initialStateMarked()) {

            if (parentState != null) {
                state = new Object[] { parentState };
            }
        } else {
            Object[] values = new Object[3];
            values[0] = parentState;
            values[1] = saveSet(PropertyKeys.execute, execute);
            values[2] = saveSet(PropertyKeys.render, render);

            state = values;
        }

        return state;
    }
}
