/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

package org.apache.myfaces.orchestra.viewController.jsf;

import java.util.Set;
import java.util.TreeSet;

import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.orchestra.viewController.ViewControllerManager;
import org.apache.myfaces.orchestra.viewController.ViewControllerManagerFactory;

/**
 * Causes lifecycle methods to be invoked on backing beans that are associated with
 * the current view.
 * <p>
 * For details about when initView callbacks occur, see the documentation for
 * method initView on class ViewController.
 * <p>
 * See the javadoc for class ViewControllerManager on how to configure the ViewController
 * behaviour.
 * <p>
 * Note that at the moment this does not support a ViewController bean for subviews
 * (ie f:subView tags), which the Shale ViewController framework does provide. ViewController
 * beans can only be associated with the main viewId, ie the "top level" view template file.
 * <p>
 * Note that some callbacks might not be invoked if exceptions are thrown by actionlisteners,
 * etc (which again Shale guarantees). This is particularly important for an "endView"
 * callback, where resources allocated on initView (such as database connections) might
 * be released. Hopefully this will be implemented in some later Orchestra release.
 */
public class ViewControllerPhaseListener implements PhaseListener
{
    private static final long serialVersionUID = -3975277433747722402L;
    private final Log log = LogFactory.getLog(ViewControllerPhaseListener.class);

    /**
     * @since 1.1
     */
    public static class ViewControllerPhaseListenerState
    {
        private Set initedViews = new TreeSet();

        protected ViewControllerPhaseListenerState()
        {
        }
    }

    public void beforePhase(PhaseEvent event)
    {
        if (PhaseId.RESTORE_VIEW.equals(event.getPhaseId()) ||
            PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()))
        {
            assertConversationState(event.getFacesContext());
            if (event.getFacesContext().getResponseComplete())
            {
                // we have a redirect ... stop now
                return;
            }
        }

        // Try to init the view in every phase, just so we are sure to never miss it.
        // This skips the actual call if init has already happened for the current
        // view root instance.
        executeInitView(event.getFacesContext());

        if (PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()))
        {
            preRenderResponse(event.getFacesContext());
        }

        if (PhaseId.INVOKE_APPLICATION.equals(event.getPhaseId()))
        {
            preInvokeApplication(event.getFacesContext());
        }
    }

    public void afterPhase(PhaseEvent event)
    {
        if (PhaseId.RESTORE_VIEW.equals(event.getPhaseId()))
        {
            assertConversationState(event.getFacesContext());
            if (event.getFacesContext().getResponseComplete())
            {
                // we have a redirect ... stop now
                return;
            }
        }

        executeInitView(event.getFacesContext());
    }

    public PhaseId getPhaseId()
    {
        return PhaseId.ANY_PHASE;
    }

    protected String getViewId(FacesContext facesContext)
    {
        UIViewRoot viewRoot = facesContext.getViewRoot();
        if (viewRoot == null)
        {
            return null;
        }
        return viewRoot.getViewId();
    }

    /**
     * invoked multiple times during the lifecycle to ensure the conversation(s)
     * to the associated viewController are running.
     *
     * @param facesContext
     */
    protected void assertConversationState(FacesContext facesContext)
    {
        ViewControllerManager manager = ViewControllerManagerFactory.getInstance();
        if (manager == null)
        {
            return;
        }

        String viewId = getViewId(facesContext);
        if (viewId == null)
        {
            return;
        }

        manager.assertConversationState(viewId);
    }

    /**
     * invokes the preRenderView method on your view controller
     */
    protected void preRenderResponse(FacesContext facesContext)
    {
        ViewControllerManager manager = ViewControllerManagerFactory.getInstance();
        if (manager == null)
        {
            return;
        }

        String viewId = getViewId(facesContext);
        if (viewId == null)
        {
            return;
        }

        manager.executePreRenderView(viewId);
    }

    /**
     * invokes the initView method on your view controller
     * @since 1.1
     */
    protected void executeInitView(FacesContext facesContext)
    {
        postRestoreView(facesContext);
    }

    /**
     * @deprecated overload/use {@link #executeInitView(javax.faces.context.FacesContext)} instead
     */
    protected void postRestoreView(FacesContext facesContext)
    {
        ViewControllerManager manager = ViewControllerManagerFactory.getInstance();
        if (manager == null)
        {
            return;
        }

        UIViewRoot viewRoot = facesContext.getViewRoot();
        if (viewRoot == null)
        {
            return;
        }

        String viewId = viewRoot.getViewId();
        if (viewId == null)
        {
            return;
        }

        // Here we keep track of the ViewRoot instances that we have already called initView for,
        // and if it changes then we call initView again.
        //
        // An alternative would be to keep track of the ViewController instance, and call initView
        // if that instance changes. But this is tricky as this object is often a proxy for the
        // real object, and may not change even when the target is invalidated and recreated.

        String viewKey = String.valueOf(System.identityHashCode(viewRoot));
        ViewControllerPhaseListenerState state = getState(facesContext);
        if (state.initedViews.contains(viewKey))
        {
            // this view instance is already initialized
            if (log.isDebugEnabled())
            {
                log.debug("Skipping already-initialized viewcontroller bean " + viewKey + " for view " + viewId);
            }
            return;
        }

        if (log.isDebugEnabled())
        {
            log.debug("Initializing viewcontroller bean " + viewKey + " for view " + viewId);
        }

        state.initedViews.add(viewKey);
        manager.executeInitView(viewId);
    }

    protected ViewControllerPhaseListenerState getState(FacesContext facesContext)
    {
        ViewControllerPhaseListenerState state = (ViewControllerPhaseListenerState)
            facesContext.getExternalContext().getRequestMap()
                .get(ViewControllerPhaseListenerState.class.getName());
        if (state == null)
        {
            state = new ViewControllerPhaseListenerState();
            facesContext.getExternalContext().getRequestMap().put(
                ViewControllerPhaseListenerState.class.getName(), state);
        }
        return state;
    }

    /**
     * invokes the preProcess method on your view controller
     */
    protected void preInvokeApplication(FacesContext facesContext)
    {
        ViewControllerManager manager = ViewControllerManagerFactory.getInstance();
        if (manager == null)
        {
            return;
        }

        String viewId = getViewId(facesContext);
        if (viewId == null)
        {
            return;
        }

        manager.executePreProcess(viewId);
    }
}
