/*
 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed 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.
 *
 * Contributors:
 *     Anahide Tchertchian
 */
package org.nuxeo.ecm.platform.actions.facelets;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.el.ELException;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.FaceletHandler;
import javax.faces.view.facelets.MetaRuleset;
import javax.faces.view.facelets.MetaTagHandler;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributes;
import javax.faces.view.facelets.TagConfig;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.platform.actions.Action;
import org.nuxeo.ecm.platform.forms.layout.api.BuiltinWidgetModes;
import org.nuxeo.ecm.platform.forms.layout.api.Widget;
import org.nuxeo.ecm.platform.forms.layout.api.impl.WidgetDefinitionImpl;
import org.nuxeo.ecm.platform.forms.layout.facelets.FaceletHandlerHelper;
import org.nuxeo.ecm.platform.forms.layout.facelets.RenderVariables;
import org.nuxeo.ecm.platform.forms.layout.facelets.WidgetTagHandler;
import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager;
import org.nuxeo.ecm.platform.ui.web.binding.BlockingVariableMapper;
import org.nuxeo.ecm.platform.ui.web.tag.handler.FormTagHandler;
import org.nuxeo.ecm.platform.ui.web.tag.handler.TagConfigFactory;
import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
import org.nuxeo.ecm.platform.ui.web.util.FaceletDebugTracer;
import org.nuxeo.runtime.api.Framework;

import com.sun.faces.facelets.tag.TagAttributesImpl;

/**
 * Tag handler rendering an action given its type, applying corresponding widget tag handler, axposing additional
 * variables for action templates usage.
 *
 * @since 8.2
 */
public class ActionTagHandler extends MetaTagHandler {

    private static final Log log = LogFactory.getLog(ActionTagHandler.class);

    protected final TagConfig config;

    protected final TagAttribute action;

    protected final TagAttribute widgetName;

    protected final TagAttribute value;

    protected final TagAttribute mode;

    protected final TagAttribute addForm;

    protected final TagAttribute useAjaxForm;

    protected final TagAttribute formStyleClass;

    protected final TagAttribute postFilterMethod;

    protected final TagAttribute[] vars;

    protected final String[] reservedVarsArray = { "action", "widgetName", "value", "mode", "addForm", "useAjaxForm",
            "formStyleClass", "postFilterMethod" };

    public ActionTagHandler(TagConfig config) {
        super(config);
        this.config = config;

        action = getRequiredAttribute("action");
        widgetName = getAttribute("widgetName");
        value = getRequiredAttribute("value");
        mode = getAttribute("mode");
        addForm = getAttribute("addForm");
        useAjaxForm = getAttribute("useAjaxForm");
        formStyleClass = getAttribute("formStyleClass");
        postFilterMethod = getAttribute("postFilterMethod");

        vars = tag.getAttributes().getAll();
    }

    /**
     * Renders given widget resolving its {@link FaceletHandler} from {@link WebLayoutManager} configuration.
     * <p>
     * Variables exposed: {@link RenderVariables.globalVariables#value}, same variable suffixed with "_n" where n is the
     * widget level, and {@link RenderVariables.globalVariables#document}.
     */
    public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, ELException {
        long start = FaceletDebugTracer.start();
        Action actionInstance = null;

        try {
            if (action != null) {
                actionInstance = (Action) action.getObject(ctx, Action.class);
            }
            if (actionInstance == null) {
                return;
            }

            VariableMapper orig = ctx.getVariableMapper();
            try {
                BlockingVariableMapper vm = new BlockingVariableMapper(orig);
                ctx.setVariableMapper(vm);

                // build corresponding widget and adjust properties
                String wtype = actionInstance.getType();
                if (StringUtils.isBlank(wtype)) {
                    wtype = "link";
                }
                String wcat = "jsfAction";

                String modeValue = null;
                if (mode != null) {
                    modeValue = mode.getValue(ctx);
                }
                if (StringUtils.isBlank(modeValue)) {
                    modeValue = BuiltinWidgetModes.VIEW;
                }

                Map<String, Serializable> props = new HashMap<>();
                // put all action properties
                props.putAll(actionInstance.getProperties());
                if ("template".equals(wtype)) {
                    // avoid erasing template value from widget type configuration, and match template
                    String templateName = "template";
                    String modeTemplateName = "template" + "_" + modeValue;
                    if (BuiltinWidgetModes.VIEW.equals(modeValue) && props.containsKey(templateName)) {
                        props.put("action_template", props.get(templateName));
                    } else if (props.containsKey(modeTemplateName)) {
                        props.put("action_template", props.get(modeTemplateName));
                        props.remove(modeTemplateName);
                    }
                    props.remove(templateName);
                }
                // handle onclick
                StringBuilder fullOnclick = new StringBuilder();
                if (BuiltinWidgetModes.VIEW.equals(modeValue) && props.containsKey("confirmMessage")) {
                    String confirmMessage = (String) props.get("confirmMessage");
                    if (!StringUtils.isEmpty(confirmMessage)) {
                        fullOnclick.append(
                                "var message = \"#{nxu:translate(widgetProperty_confirmMessage, widgetProperty_confirmMessageArgs)}\";if (message != \"\" && !confirm(message)) {return false;};");
                    }
                }
                String confirm = actionInstance.getConfirm();
                if (!StringUtils.isEmpty(confirm)) {
                    fullOnclick.append(confirm).append(";");
                }
                String onclick = (String) actionInstance.getProperties().get("onclick");
                if (!StringUtils.isEmpty(onclick)) {
                    fullOnclick.append(onclick).append(";");
                }
                props.put("immediate", actionInstance.isImmediate());
                props.put("icon", actionInstance.getIcon());
                props.put("onclick", actionInstance.getConfirm());
                props.put("accessKey", actionInstance.getAccessKey());
                props.put("link", actionInstance.getLink());
                props.put("actionId", actionInstance.getId());
                props.put("action", actionInstance);
                if (useAjaxForm != null && !props.containsKey("useAjaxForm")) {
                    props.put("useAjaxForm", useAjaxForm.getValue());
                }

                String valueName = value.getValue();
                String bareValueName = valueName;
                if (ComponentTagUtils.isStrictValueReference(valueName)) {
                    bareValueName = ComponentTagUtils.getBareValueName(valueName);
                }

                // add filtering method if needed
                if (!actionInstance.isFiltered()) {
                    // make sure variables are in the context for this filter resolution
                    ExpressionFactory eFactory = ctx.getExpressionFactory();
                    ValueExpression actionVe = eFactory.createValueExpression(actionInstance, Action.class);
                    vm.setVariable("action", actionVe);
                    vm.addBlockedPattern("action");

                    String bindingValue = bareValueName;
                    boolean bindingDone = false;
                    if (props.containsKey("actionContextDocument")) {
                        Object val = props.get("actionContextDocument");
                        if (val instanceof String && ComponentTagUtils.isStrictValueReference((String) val)) {
                            bindingValue = ComponentTagUtils.getBareValueName((String) val);
                            ValueExpression bindingVe = eFactory.createValueExpression(ctx, (String) val, Object.class);
                            vm.setVariable("actionContextDocument", bindingVe);
                            vm.addBlockedPattern("actionContextDocument");
                            bindingDone = true;
                        }
                    }
                    if (!bindingDone) {
                        // just bound current value to make expressions consistent
                        vm.setVariable("actionContextDocument", value.getValueExpression(ctx, DocumentModel.class));
                        vm.addBlockedPattern("actionContextDocument");
                    }

                    String method = null;
                    if (postFilterMethod != null) {
                        method = postFilterMethod.getValue(ctx);
                    }
                    if (StringUtils.isBlank(method)) {
                        method = "webActions.isAvailableForDocument";
                    }
                    String filterExpr = "#{" + method + "(" + bindingValue + ", action)}";
                    props.put("available", filterExpr);
                    props.put("enabled", filterExpr);
                } else {
                    props.put("available", actionInstance.getAvailable());
                    props.put("enabled", "true");
                }

                // add all extra props passed to the tag
                String widgetPropertyMarker = RenderVariables.widgetVariables.widgetProperty.name() + "_";
                List<String> reservedVars = Arrays.asList(reservedVarsArray);
                for (TagAttribute var : vars) {
                    String localName = var.getLocalName();
                    if (!reservedVars.contains(localName)) {
                        if (localName != null && localName.startsWith(widgetPropertyMarker)) {
                            localName = localName.substring(widgetPropertyMarker.length());
                        }
                        props.put(localName, var.getValue());
                    }
                }

                String widgetNameValue = null;
                if (widgetName != null) {
                    widgetNameValue = widgetName.getValue(ctx);
                }
                if (StringUtils.isBlank(widgetNameValue)) {
                    widgetNameValue = actionInstance.getId();
                }
                // avoid double markers
                if (widgetNameValue != null && widgetNameValue.startsWith(FaceletHandlerHelper.WIDGET_ID_PREFIX)) {
                    widgetNameValue = widgetNameValue.substring(FaceletHandlerHelper.WIDGET_ID_PREFIX.length());
                }

                WidgetDefinitionImpl wDef = new WidgetDefinitionImpl(widgetNameValue, wtype, actionInstance.getLabel(),
                        actionInstance.getHelp(), true, null, null, props, null);
                wDef.setTypeCategory(wcat);
                wDef.setDynamic(true);
                WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class);
                Widget widgetInstance = layoutService.createWidget(ctx, wDef, modeValue, bareValueName, null);
                if (widgetInstance == null) {
                    return;
                }
                // set unique id on widget before exposing it to the context
                FaceletHandlerHelper helper = new FaceletHandlerHelper(config);
                WidgetTagHandler.generateWidgetId(ctx, helper, widgetInstance, false);

                // expose widget variables
                WidgetTagHandler.exposeWidgetVariables(ctx, vm, widgetInstance, null, false);

                // create widget handler
                TagAttributes wattrs = FaceletHandlerHelper.getTagAttributes();
                wattrs = FaceletHandlerHelper.addTagAttribute(wattrs,
                        helper.createAttribute(RenderVariables.widgetVariables.widget.name(),
                                "#{" + RenderVariables.widgetVariables.widget.name() + "}"));
                wattrs = FaceletHandlerHelper.addTagAttribute(wattrs,
                        helper.createAttribute("value", value.getValue()));
                TagConfig wconfig = TagConfigFactory.createTagConfig(config, config.getTagId(), wattrs, nextHandler);
                FaceletHandler handler = new WidgetTagHandler(wconfig);

                // expose ajax render props to the context
                String reRender = (String) props.get("ajaxReRender");
                if (!StringUtils.isEmpty(reRender)) {
                    ExpressionFactory eFactory = ctx.getExpressionFactory();
                    ValueExpression ve = eFactory.createValueExpression(ctx,
                            "#{nxu:joinRender(ajaxReRender, '" + reRender + "')}", String.class);
                    vm.setVariable("ajaxReRender", ve);
                }

                // create form handler if needed
                boolean doAddForm = false;
                if (addForm != null) {
                    doAddForm = addForm.getBoolean(ctx);
                }
                if (!doAddForm) {
                    // check if addForm information held by the action configuration
                    doAddForm = helper.createAttribute("addForm", String.valueOf(widgetInstance.getProperty("addForm")))
                                      .getBoolean(ctx);
                }
                if (doAddForm) {
                    // resolve form related attributes early
                    boolean discard = helper.createAttribute("discardSurroundingForm",
                            String.valueOf(widgetInstance.getProperty("discardSurroundingForm"))).getBoolean(ctx);
                    boolean doUseAjaxForm = helper.createAttribute("useAjaxForm",
                            String.valueOf(widgetInstance.getProperty("useAjaxForm"))).getBoolean(ctx);
                    if (!discard || doUseAjaxForm) {
                        List<TagAttribute> fattrs = new ArrayList<>();
                        if (doUseAjaxForm) {
                            Object ajaxProp = widgetInstance.getProperty("ajaxSupport");
                            if (ajaxProp == null) {
                                ajaxProp = widgetInstance.getProperty("supportAjax");
                            }
                            fattrs.add(helper.createAttribute("useAjaxForm", String.valueOf(ajaxProp)));
                        }
                        fattrs.add(helper.createAttribute("disableMultipartForm",
                                String.valueOf(widgetInstance.getProperty("disableMultipartForm"))));
                        fattrs.add(helper.createAttribute("disableDoubleClickShield",
                                String.valueOf(widgetInstance.getProperty("disableDoubleClickShield"))));
                        fattrs.add(helper.createAttribute("styleClass",
                                formStyleClass != null ? formStyleClass.getValue() : null));
                        fattrs.add(helper.createAttribute("id", widgetInstance.getId() + "_form"));

                        TagConfig fconfig = TagConfigFactory.createTagConfig(config, config.getTagId(),
                                new TagAttributesImpl(fattrs.toArray(new TagAttribute[] {})), handler);
                        handler = new FormTagHandler(fconfig);
                    }
                }

                handler.apply(ctx, parent);

            } finally {
                ctx.setVariableMapper(orig);
            }

        } finally {
            FaceletDebugTracer.trace(start, config.getTag(), actionInstance == null ? null : actionInstance.getId());
        }
    }

    @Override
    @SuppressWarnings("rawtypes")
    protected MetaRuleset createMetaRuleset(Class type) {
        return null;
    }

}