/*
 * 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.context;

import static org.richfaces.renderkit.AjaxConstants.AJAX_COMPONENT_ID_PARAMETER;
import static org.richfaces.renderkit.AjaxConstants.ALL;
import static org.richfaces.renderkit.AjaxConstants.BEHAVIOR_EVENT_PARAMETER;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import javax.faces.FactoryFinder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitContextFactory;
import javax.faces.component.visit.VisitHint;
import javax.faces.context.FacesContext;
import javax.faces.context.PartialResponseWriter;
import javax.faces.context.PartialViewContext;
import javax.faces.context.PartialViewContextWrapper;
import javax.faces.event.PhaseId;

import org.ajax4jsf.component.AjaxOutput;
import org.ajax4jsf.javascript.ScriptUtils;
import org.richfaces.application.ServiceTracker;
import org.richfaces.javascript.JavaScriptService;
import org.richfaces.javascript.ScriptsHolder;
import org.richfaces.renderkit.AjaxDataSerializer;
import org.richfaces.renderkit.HtmlConstants;
import org.richfaces.util.FastJoiner;

import com.google.common.base.Function;
import com.google.common.collect.Maps;

/**
 * <p>
 * The RichFaces custom version of PartialViewContext
 * </p>
 * <p>
 * Important differences of RichFaces implementation:
 * </p>
 * <ul>
 * <li>Values are resolved in runtime by visiting the request activator component and evaluating attributes</li>
 * <li>Usage of extended visit contexts in order to support meta-components processing</li>
 * <li>Support for auto-updateable Ajax components</li>
 * <li>Support for Ajax extensions like passing data to the client, onbeforedomupdate and oncomplete callbacks and {@link JavaScriptService}</li>
 * </ul>
 *
 *
 * <h2>Extended Partial View Processing Scheme</h2>
 *
 * <p>On following diagram, you can see which classes contributes to RichFaces specific partial view processing (find an explanation bellow):</p>
 *
 * <pre>
 * +-------------------------+  RF: ExtendedPartialViewContextFactory
 * |PartialViewContextFactory|
 * +------------+------------+   +------------------------+
 *              |                |   PartialViewContext   | Mojarra: PartialViewContextImpl
 *              +--------------->|                        | RF: ExtendedPartialViewContext
 *                creates        |#processPartial(PhaseId)|
 *                               +--+-------------+-------+
 *                                  |             |
 * RF: ExtendedVisitContextFactory  |             |
 * +-------------------+            |             |
 * |VisitContextFactory|            |             |
 * +--------------+----+       uses |             |creates/uses
 *                |                 |             |
 *        creates |                 |             |
 *                v                 v             v
 *        +----------------------------+        +---------------------------+
 *        |        VisitContext        |        |       VisitCallback       |
 *        |                            |        |                           |
 *        |     #invokeVisitContext    |        |          #visit           |
 *        |(UIComponent, VisitCallback)|        |(VisitContext, UIComponent)|
 *        +----------------------------+        +---------------------------+
 *                                               Mojarra: PhaseAwareVisitCallback
 *         RF: ExtendedExecuteVisitContext       RF: MetaComponentProcessingVisitCallback
 *         RF: ExtendedRenderVisitContext        RF: MetaComponentEncodingVisitCallback
 *
 *                        +-----------------------------+
 *                        |          UIViewRoot         |
 *                        |                             |
 *                        |          #visitTree         |
 *                        |(VisitContext, VisitCallback)|
 *                        +-----------------------------+
 * </pre>
 *
 * <p>{@link ExtendedPartialViewContext} does (except other things) tracking of the mode ({@link ExtendedVisitContextMode}). This mode determines whether we are not inside a partial processing phase (inside {@link #processPartial(PhaseId)}) or not (determined by mode equal to <tt>null</tt>).</p>
 *
 * <p>The knowledge of current mode is leveraged by {@link ExtendedVisitContextFactory} that either use visit context created by parent factory (in a wrapper chain) in case the mode is equal to <tt>null</tt> or it creates {@link ExtendedExecuteVisitContext} (resp. {@link ExtendedRenderVisitContext}).</p>
 *
 * <p>(Note: the <tt>null</tt> mode is necessary because the tree visiting can be leveraged outside of partial tree processing/rendering, e.g. in partial state saving)
 *
 * <p>These {@link VisitContext} wrappers makes sure that when {@link VisitContext#invokeVisitCallback(UIComponent, VisitCallback)} method is called, the actual {@link VisitCallback} created by JSF implementation is wrapped in wrappers that allows to execute (resp. render) meta-components {@link MetaComponentProcessingVisitCallback} (resp. {@link MetaComponentEncodingVisitCallback}).</p>
 *
 * <p>While extended {@link VisitContext} implementations ({@link ExtendedRenderVisitContext} and {@link ExtendedExecuteVisitContext}) allows to visit subtrees that are not normally visited (meta-components and implicitly renderer areas, aka {@link AjaxOutput}s),</p>
 *
 * <p>extended implementations of {@link VisitCallback} ({@link MetaComponentProcessingVisitCallback} and {@link MetaComponentEncodingVisitCallback}) do the extended processing and rendering logic for meta-components.</p>
 *
 * <p>{@link UIViewRoot} is a place where the tree processing starts to dive into subtrees, it is called by JSF implementation of {@link PartialViewContext#processPartial(PhaseId)}.</p>
 *
 * <h2>Rendering AJAX Extensions</h2>
 *
 * <p>This context returns wrapped {@link PartialResponseWriter} in order to intercept its {@link PartialResponseWriter#endDocument()} method and write extensions before the document is actually ended. For more details see {@link ExtensionWritingPartialResponseWriter}.
 *
 * @author Lukas Fryc
 * @author Nick Belaevski
 */
public class ExtendedPartialViewContext extends PartialViewContextWrapper {

    private static final String EXTENSION_ID = "org.richfaces.extension";
    private static final String BEFOREDOMUPDATE_ELEMENT_NAME = "beforedomupdate";
    private static final String COMPLETE_ELEMENT_NAME = "complete";
    private static final String RENDER_ELEMENT_NAME = "render";
    private static final String DATA_ELEMENT_NAME = "data";
    private static final String COMPONENT_DATA_ELEMENT_NAME = "componentData";
    private static final FastJoiner SPACE_JOINER = FastJoiner.on(' ');

    private static final String ATTRIBUTE_NAME = ExtendedPartialViewContext.class.getName();

    private FacesContext facesContext;
    private PartialViewContext wrappedViewContext;
    private PartialResponseWriter partialResponseWriter;
    private boolean released = false;

    // request data
    private ContextMode contextMode = null;
    private String activatorComponentId = null;
    private String behaviorEvent = null;

    // computed properties
    private Collection<String> executeIds = null;
    private Collection<String> renderIds = null;
    private Boolean renderAll = null;

    // activator component data
    private Collection<String> componentRenderIds = null;
    private String onbeforedomupdate;
    private String oncomplete;
    private Object responseData;
    private boolean limitRender = false;

    private Map<String, Object> responseComponentDataMap = Maps.newHashMap();
    private StringBuilder beforedomupdateHandler = new StringBuilder();
    private StringBuilder completeHandler = new StringBuilder();

    // current visit mode setup during #processPartial method
    private Stack<ExtendedVisitContextMode> visitMode = new Stack<ExtendedVisitContextMode>();


    public ExtendedPartialViewContext(PartialViewContext wrappedViewContext, FacesContext facesContext) {
        this.wrappedViewContext = wrappedViewContext;
        this.facesContext = facesContext;
        setInstance(facesContext, this);
    }

    /**
     * The partial view processing mode
     */
    private enum ContextMode {
        /**
         * Use wrapped {@link PartialViewContext} implementation for partial view processing.
         */
        WRAPPED,

        /**
         * Use RichFaces-specific partial view processing processing via {@link ExtendedPartialViewContext}
         */
        EXTENDED
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.PartialViewContextWrapper#getWrapped()
     */
    @Override
    public PartialViewContext getWrapped() {
        return wrappedViewContext;
    }

    /**
     * This method is present in the JSF 2.2 PartialViewContextWrapper, but not in the JSF 2.1.  Implementing it here so we are still compatible with JSF 2.1.
     */
    @Override
    public void setPartialRequest(boolean isPartialRequest) {
        getWrapped().setPartialRequest(isPartialRequest);
    }

    /**
     * Returns {@link FacesContext} for current partial view processing context
     */
    protected FacesContext getFacesContext() {
        return facesContext;
    }

    /**
     * Return current {@link ExtendedPartialViewContext} instance for given {@link FacesContext}
     */
    public static ExtendedPartialViewContext getInstance(FacesContext facesContext) {
        return (ExtendedPartialViewContext) facesContext.getAttributes().get(ATTRIBUTE_NAME);
    }

    /**
     * Setups given {@link ExtendedPartialViewContext} instance to context
     * @param facesContext
     * @param instance
     */
    private static void setInstance(FacesContext facesContext, ExtendedPartialViewContext instance) {
        facesContext.getAttributes().put(ATTRIBUTE_NAME, instance);
    }

    /**
     * <p>
     * This method detects in which phase we are and setups {@link ExtendedVisitContextMode} attribute.
     * </p>
     *
     * <p>
     * Then it delegates to wrapped implementation of {@link PartialViewContext#processPartial(PhaseId)}.
     * </p>
     *
     * <p>
     * The {@link ExtendedVisitContextMode} attribute is used by {@link ExtendedVisitContextFactory} to create either
     * {@link ExtendedExecuteVisitContext} or {@link ExtendedRenderVisitContext}.
     * </p>
     */
    @Override
    public void processPartial(PhaseId phaseId) {
        initializeContext();
        try {
            if (isProcessedExecutePhase(phaseId)) {
                setVisitMode(ExtendedVisitContextMode.EXECUTE);
            } else {
                setVisitMode(ExtendedVisitContextMode.RENDER);
            }
            wrappedViewContext.processPartial(phaseId);
        } finally {
            resetVisitMode();
        }
    }

    private boolean isProcessedExecutePhase(PhaseId phaseId) {
        return phaseId == PhaseId.APPLY_REQUEST_VALUES || phaseId == PhaseId.PROCESS_VALIDATIONS
                || phaseId == PhaseId.UPDATE_MODEL_VALUES;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.PartialViewContextWrapper#getExecuteIds()
     */
    @Override
    public Collection<String> getExecuteIds() {
        assertNotReleased();
        if (detectContextMode() == ContextMode.EXTENDED) {
            if (executeIds == null) {
                executeIds = new LinkedHashSet<String>();
                visitActivatorAtExecute();
            }
            return executeIds;
        } else {
            return wrappedViewContext.getExecuteIds();
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.PartialViewContextWrapper#getRenderIds()
     */
    @Override
    public Collection<String> getRenderIds() {
        assertNotReleased();
        if (detectContextMode() == ContextMode.EXTENDED) {
            PhaseId currenPhaseId = facesContext.getCurrentPhaseId();
            if (renderIds == null) {
                renderIds = new LinkedHashSet<String>();
            }
            if (currenPhaseId == PhaseId.RENDER_RESPONSE) {
                visitActivatorAtRender();
            }
            return renderIds;
        } else {
            return wrappedViewContext.getRenderIds();
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.PartialViewContextWrapper#isExecuteAll()
     */
    @Override
    public boolean isExecuteAll() {
        assertNotReleased();
        if (detectContextMode() == ContextMode.EXTENDED) {
            return getExecuteIds().contains(ALL);
        } else {
            return wrappedViewContext.isExecuteAll();
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.PartialViewContextWrapper#isRenderAll()
     */
    @Override
    public boolean isRenderAll() {
        assertNotReleased();
        if (detectContextMode() == ContextMode.EXTENDED) {
            if (renderAll == null) {
                setRenderAll(detectRenderAll());
            }
            return renderAll.booleanValue();
        } else {
            return wrappedViewContext.isRenderAll();
        }
    }

    /**
     * Detects whether current context's state indicates render=@all
     */
    private boolean detectRenderAll() {
        // RF-13740, MyFaces doesn't call for renderIds in advance
        if (renderIds == null) {
            renderIds = new LinkedHashSet<String>();

            ActivatorComponentRenderCallback callback = new ActivatorComponentRenderCallback(getFacesContext(), behaviorEvent);

            if (visitActivatorComponent(activatorComponentId, callback, EnumSet.noneOf(VisitHint.class))) {
                setupRenderCallbackData(callback);
            }

            // take collection value stored during execute
            if (componentRenderIds != null) {
                renderIds.addAll(componentRenderIds);
            }

            if (!isRenderAll()) {
                addImplicitRenderIds(renderIds);
            }
        }

        return Boolean.TRUE.equals(renderAll) || renderIds.contains(ALL);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.PartialViewContextWrapper#setRenderAll(boolean)
     */
    @Override
    public void setRenderAll(final boolean renderAll) {
        assertNotReleased();
        this.renderAll = renderAll;
        visitPatentContexts(new Function<PartialViewContext, Void>() {
            public Void apply(PartialViewContext pvc) {
                if (pvc != ExtendedPartialViewContext.this) {
                    pvc.setRenderAll(renderAll);
                }
                return null;
            }
        });
    }

    /**
     * Returns user-provided data as an extension for partial-response response element
     */
    public Object getResponseData() {
        return responseData;
    }

    /**
     * Returns data provided by components as an extension for partial-response response element
     */
    public Map<String, Object> getResponseComponentDataMap() {
        return responseComponentDataMap;
    }

    /**
     * Sets user-provided data as an extension for partial-response
     */
    public void setResponseData(Object responseData) {
        this.responseData = responseData;
    }

    /**
     * Append a script to oncomplete handler
     */
    public void appendOncomplete(Object handler) {
        if (handler != null) {
            completeHandler.append(handler.toString());
            completeHandler.append(';');
        }
    }

    /**
     * Prepend an oncomplete handler with given script
     */
    public void prependOncomplete(Object handler) {
        if (handler != null) {
            completeHandler.insert(0, ';');
            completeHandler.insert(0, handler.toString());
        }
    }

    /**
     * Return oncomplete handler as an extension for partial-response
     */
    public Object getOncomplete() {
        return completeHandler.toString();
    }

    /**
     * Append a script to onbeforedomupdate handler
     */
    public void appendOnbeforedomupdate(Object handler) {
        if (handler != null) {
            beforedomupdateHandler.append(handler.toString());
            beforedomupdateHandler.append(';');
        }
    }

    /**
     * Prepend an onbeforedomupdate handler with given script
     */
    public void prependOnbeforedomupdate(Object handler) {
        if (handler != null) {
            beforedomupdateHandler.insert(0, handler.toString());
            beforedomupdateHandler.insert(0, ';');
        }
    }

    /**
     * Return onbeforedomupdate handler as an extension for partial-response
     */
    public Object getOnbeforedomupdate() {
        return beforedomupdateHandler.toString();
    }

    /**
     * Returns true if rendering in current context is limited to components listed in activator's component <em>render</em> attribute.
     */
    public boolean isLimitRender() {
        return limitRender;
    }

    /**
     * Returns in which visit mode we currently operate in
     */
    public ExtendedVisitContextMode getVisitMode() {
        if (visitMode.isEmpty()) {
            return null;
        }
        return visitMode.peek();
    }

    /**
     * Set ups current visit mode to given value.
     *
     * Works as a stack because {@link #processPartial(javax.faces.event.PhaseId)} methods that sets this flag may nest.
     *
     * @see #resetVisitMode()
     */
    private void setVisitMode(ExtendedVisitContextMode visitMode) {
        this.visitMode.add(visitMode);
    }

    /**
     * Resets current visit mode.
     *
     * Works as a stack because {@link #processPartial(javax.faces.event.PhaseId)} methods that sets this flag may nest.
     * Partial processing needs to {@link #resetVisitMode()} before returning.
     */
    private void resetVisitMode() {
        this.visitMode.pop();
    }

    /**
     * We are wrapping {@link PartialResponseWriter} obtained from wrapped implementation into {@link ExtensionWritingPartialResponseWriter}.
     *
     * The wrapper makes sure the RichFaces-specific extensions are written into partial-response before the document is ended.
     */
    @Override
    public PartialResponseWriter getPartialResponseWriter() {
        assertNotReleased();
        if (partialResponseWriter == null) {
            partialResponseWriter = new ExtensionWritingPartialResponseWriter(wrappedViewContext.getPartialResponseWriter());
        }
        return partialResponseWriter;
    }

    /**
     * Makes sure the RichFaces-specific extensions are written into partial-response before the document is ended.
     */
    private class ExtensionWritingPartialResponseWriter extends PartialResponseWriterWrapper {

        public ExtensionWritingPartialResponseWriter(PartialResponseWriter wrapped) {
            super(wrapped);
        }

        /**
         * Render RichFaces-specific extensions and then {@link #endDocument()} finally.
         */
        @Override
        public void endDocument() throws IOException {
            try {
                FacesContext facesContext = FacesContext.getCurrentInstance();
                UIViewRoot viewRoot = facesContext.getViewRoot();


                addJavaScriptServicePageScripts(facesContext);
                renderExtensions(facesContext, viewRoot);
            } finally {
                super.endDocument();
            }
        }
    }

    /**
     * Visits activator component to collect attributes needed for execute phase
     */
    private void visitActivatorAtExecute() {
        if (detectContextMode() == ContextMode.EXTENDED) {

            ActivatorComponentExecuteCallback callback = new ActivatorComponentExecuteCallback(getFacesContext(), behaviorEvent);

            if (visitActivatorComponent(activatorComponentId, callback, EnumSet.of(VisitHint.SKIP_UNRENDERED))) {
                executeIds.addAll(callback.getExecuteIds());

                setupRenderCallbackData(callback);

                if (!executeIds.contains(ALL)) {
                    addImplicitExecuteIds(executeIds);
                }
            }
        }
    }

    /**
     * Copies the data collected from activator component processing to to this context
     */
    private void setupRenderCallbackData(ActivatorComponentRenderCallback callback) {
        componentRenderIds = callback.getRenderIds();
        onbeforedomupdate = callback.getOnbeforedomupdate();
        oncomplete = callback.getOncomplete();
        responseData = callback.getData();
        limitRender = callback.isLimitRender();
    }

    /**
     * Visits activator component to collect attributes needed for render phase
     */
    private void visitActivatorAtRender() {
        if (detectContextMode() == ContextMode.EXTENDED) {
            ActivatorComponentRenderCallback callback = new ActivatorComponentRenderCallback(getFacesContext(), behaviorEvent);

            if (visitActivatorComponent(activatorComponentId, callback, EnumSet.of(VisitHint.SKIP_UNRENDERED))) {
                setupRenderCallbackData(callback);
            } else {
                // TODO - the same as for "execute"
            }

            // take collection value stored during execute
            if (componentRenderIds != null) {
                renderIds.addAll(componentRenderIds);
            }

            if (!isRenderAll()) {
                addImplicitRenderIds(renderIds);

                appendOnbeforedomupdate(onbeforedomupdate);
                appendOncomplete(oncomplete);
                setResponseData(responseData);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.context.ExtendedPartialViewContext#release()
     */
    @Override
    public void release() {
        assertNotReleased();

        super.release();

        if (facesContext != null && !facesContext.isReleased()) {
            setInstance(facesContext, null);
        }
        facesContext = null;

        released = true;

        wrappedViewContext.release();
        wrappedViewContext = null;

        renderAll = null;
        executeIds = null;
        renderIds = null;

        limitRender = false;

        activatorComponentId = null;
        behaviorEvent = null;
        contextMode = null;
    }

    /**
     * Adding implicitly executed areas to the list of component that should be executed.
     *
     * This implementation handles just {@link UIViewRoot#METADATA_FACET_NAME} execution.
     */
    protected void addImplicitExecuteIds(Collection<String> executeIds) {
        if (!executeIds.isEmpty()) {
            UIViewRoot root = getFacesContext().getViewRoot();
            if (root.getFacetCount() > 0) {
                if (root.getFacet(UIViewRoot.METADATA_FACET_NAME) != null) {
                    executeIds.add(UIViewRoot.METADATA_FACET_NAME);
                }
            }
        }
    }

    /**
     * Adding implicitly renderer areas to the list of component that should be rendered.
     *
     *
     */
    protected void addImplicitRenderIds(Collection<String> renderIds) {
        if (!limitRender) {
            final FacesContext facesContext = getFacesContext();
            Collection<UIComponent> ajaxOutputs = AjaxOutputTracker.getAjaxOutputs(facesContext, facesContext.getViewRoot());
            for (UIComponent component : ajaxOutputs) {
                if (component instanceof AjaxOutput && ((AjaxOutput)component).isAjaxRendered()) {
                    renderIds.add(component.getClientId(facesContext));
                }
            }
        }
    }

    /**
     * Add scripts collected by {@link JavaScriptService} as a partial response extension.
     */
    protected void addJavaScriptServicePageScripts(FacesContext context) {
        ScriptsHolder scriptsHolder = ServiceTracker.getService(JavaScriptService.class).getScriptsHolder(context);
        StringBuilder scripts = new StringBuilder();
        for (Object script : scriptsHolder.getScripts()) {
            scripts.append(ScriptUtils.toScript(script));
            scripts.append(";");
        }
        for (Object script : scriptsHolder.getPageReadyScripts()) {
            scripts.append(ScriptUtils.toScript(script));
            scripts.append(";");
        }
        if (scripts.length() > 0) {
            scripts.append("RichFaces.javascriptServiceComplete();");
            prependOncomplete(scripts.toString());
        }
    }

    /**
     * Render RichFaces specific extensions for partial-update such as:
     *
     * <ul>
     * <li>beforedumpdate</li>
     * <li>complete</li>
     * <li>render</li>
     * <li>data</li>
     * <li>componentData</li>
     * </ul>
     */
    protected void renderExtensions(FacesContext context, UIComponent component) throws IOException {
        Map<String, String> attributes = Collections.singletonMap(HtmlConstants.ID_ATTRIBUTE, context.getExternalContext()
                .encodeNamespace(EXTENSION_ID));
        PartialResponseWriter writer = context.getPartialViewContext().getPartialResponseWriter();
        boolean[] writingState = new boolean[] { false };

        Object onbeforedomupdate = this.getOnbeforedomupdate();
        if (onbeforedomupdate != null) {
            String string = onbeforedomupdate.toString();
            if (string.length() != 0) {
                startExtensionElementIfNecessary(writer, attributes, writingState);
                writer.startElement(BEFOREDOMUPDATE_ELEMENT_NAME, component);
                writer.writeText(onbeforedomupdate, null);
                writer.endElement(BEFOREDOMUPDATE_ELEMENT_NAME);
            }
        }

        Object oncomplete = this.getOncomplete();
        if (oncomplete != null) {
            String string = oncomplete.toString();
            if (string.length() != 0) {
                startExtensionElementIfNecessary(writer, attributes, writingState);
                writer.startElement(COMPLETE_ELEMENT_NAME, component);
                writer.writeText(oncomplete, null);
                writer.endElement(COMPLETE_ELEMENT_NAME);
            }
        }

        if (!this.getRenderIds().isEmpty()) {
            String renderIds = SPACE_JOINER.join(this.getRenderIds());
            startExtensionElementIfNecessary(writer, attributes, writingState);
            writer.startElement(RENDER_ELEMENT_NAME, component);
            writer.writeText(renderIds, null);
            writer.endElement(RENDER_ELEMENT_NAME);
        }

        Object responseData = this.getResponseData();
        if (responseData != null) {
            startExtensionElementIfNecessary(writer, attributes, writingState);
            writer.startElement(DATA_ELEMENT_NAME, component);

            AjaxDataSerializer serializer = ServiceTracker.getService(context, AjaxDataSerializer.class);
            writer.writeText(serializer.asString(responseData), null);

            writer.endElement(DATA_ELEMENT_NAME);
        }

        Map<String, Object> responseComponentDataMap = this.getResponseComponentDataMap();
        if (responseComponentDataMap != null && !responseComponentDataMap.isEmpty()) {
            startExtensionElementIfNecessary(writer, attributes, writingState);
            writer.startElement(COMPONENT_DATA_ELEMENT_NAME, component);

            AjaxDataSerializer serializer = ServiceTracker.getService(context, AjaxDataSerializer.class);
            writer.writeText(serializer.asString(responseComponentDataMap), null);

            writer.endElement(COMPONENT_DATA_ELEMENT_NAME);
        }

        endExtensionElementIfNecessary(writer, writingState);
    }

    /**
     * Asserts that this context was not released yet
     */
    private void assertNotReleased() {
        if (released) {
            throw new IllegalStateException("PartialViewContext already released!");
        }
    }

    /**
     * Visits activator component in order to collect its data
     */
    private boolean visitActivatorComponent(String componentActivatorId, VisitCallback visitCallback, Set<VisitHint> visitHints) {
        final FacesContext facesContext = getFacesContext();
        try {
            Set<String> idsToVisit = Collections.singleton(componentActivatorId);
            setVisitMode(ExtendedVisitContextMode.EXECUTE);
            VisitContextFactory visitContextFactory = (VisitContextFactory) FactoryFinder
                    .getFactory(javax.faces.FactoryFinder.VISIT_CONTEXT_FACTORY);
            VisitContext visitContext = visitContextFactory.getVisitContext(facesContext, idsToVisit, visitHints);
            return facesContext.getViewRoot().visitTree(visitContext, visitCallback);
        } finally {
            resetVisitMode();
        }
    }

    /**
     * Detects in what context mode should be this partial view request processed
     */
    protected ContextMode detectContextMode() {
        initializeContext();
        return contextMode;
    }

    /**
     * <p>Initializes context:</p>
     *
     * <ul>
     * <li>{@link #contextMode}</li>
     * <li>{@link #activatorComponentId}</li>
     * <li>{@link #behaviorEvent}</li>
     * </ul>
     */
    protected void initializeContext() {
        if (contextMode == null) {
            Map<String, String> requestParameterMap = getFacesContext().getExternalContext().getRequestParameterMap();
            activatorComponentId = requestParameterMap.get(AJAX_COMPONENT_ID_PARAMETER);

            if (activatorComponentId != null) {
                contextMode = ContextMode.EXTENDED;
                behaviorEvent = requestParameterMap.get(BEHAVIOR_EVENT_PARAMETER);
            } else {
                contextMode = ContextMode.WRAPPED;
            }
        }
    }

    private static void startExtensionElementIfNecessary(PartialResponseWriter partialResponseWriter,
            Map<String, String> attributes, boolean[] writingState) throws IOException {

        if (!writingState[0]) {
            writingState[0] = true;

            partialResponseWriter.startExtension(attributes);
        }
    }

    private static void endExtensionElementIfNecessary(PartialResponseWriter partialResponseWriter, boolean[] writingState)
            throws IOException {

        if (writingState[0]) {
            writingState[0] = false;

            partialResponseWriter.endExtension();
        }
    }

    /**
     * All the parent wrappers of this context will be traversed and given callback will be called upon them
     */
    private void visitPatentContexts(Function<PartialViewContext, Void> function) {
        PartialViewContext pvc = (PartialViewContextWrapper) this;
        do {
            pvc = ((PartialViewContextWrapper) pvc).getWrapped();
            function.apply(pvc);
        } while (pvc instanceof PartialViewContextWrapper);
    }
}
