/*
 * 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.deltaspike.jsf.impl.config.view;

import org.apache.deltaspike.core.api.config.view.DefaultErrorView;
import org.apache.deltaspike.core.api.config.view.metadata.ViewConfigResolver;
import org.apache.deltaspike.core.api.config.view.navigation.ViewNavigationHandler;
import org.apache.deltaspike.core.api.projectstage.ProjectStage;
import org.apache.deltaspike.core.api.projectstage.TestStage;
import org.apache.deltaspike.core.api.provider.BeanProvider;
import org.apache.deltaspike.core.spi.activation.Deactivatable;
import org.apache.deltaspike.core.util.ProjectStageProducer;

import jakarta.enterprise.context.ContextNotActiveException;
import jakarta.faces.FacesException;
import jakarta.faces.application.ViewExpiredException;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.ExceptionHandler;
import jakarta.faces.context.ExceptionHandlerWrapper;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.Flash;
import jakarta.faces.event.ExceptionQueuedEvent;
import jakarta.faces.event.ExceptionQueuedEventContext;
import java.util.Iterator;

public class DefaultErrorViewAwareExceptionHandlerWrapper extends ExceptionHandlerWrapper implements Deactivatable
{
    private ExceptionHandler wrapped;

    private volatile ViewNavigationHandler viewNavigationHandler;

    /**
     * Constructor used by proxy libs
     */
    protected DefaultErrorViewAwareExceptionHandlerWrapper()
    {
    }

    public DefaultErrorViewAwareExceptionHandlerWrapper(ExceptionHandler wrapped)
    {
        this.wrapped = wrapped;
    }

    @Override
    public void handle() throws FacesException
    {
        lazyInit();
        Iterator<ExceptionQueuedEvent> exceptionQueuedEventIterator = getUnhandledExceptionQueuedEvents().iterator();

        while (exceptionQueuedEventIterator.hasNext())
        {
            ExceptionQueuedEventContext exceptionQueuedEventContext =
                    (ExceptionQueuedEventContext) exceptionQueuedEventIterator.next().getSource();

            @SuppressWarnings({ "ThrowableResultOfMethodCallIgnored" })
            Throwable throwable = exceptionQueuedEventContext.getException();

            String viewId = null;

            if (!isExceptionToHandle(throwable))
            {
                continue;
            }

            FacesContext facesContext = exceptionQueuedEventContext.getContext();
            Flash flash = facesContext.getExternalContext().getFlash();

            if (throwable instanceof ViewExpiredException)
            {
                viewId = ((ViewExpiredException) throwable).getViewId();
            }
            else if (throwable instanceof ContextNotActiveException)
            {
                //the error page uses a cdi scope which isn't active as well
                //(it's recorded below - see flash.put(throwable.getClass().getName(), throwable);)
                if (flash.containsKey(ContextNotActiveException.class.getName()))
                {
                    //TODO show it in case of project-stage development
                    break;
                }

                if (facesContext.getViewRoot() != null)
                {
                    viewId = facesContext.getViewRoot().getViewId();
                }
                else
                {
                    viewId = BeanProvider.getContextualReference(ViewConfigResolver.class)
                            //has to return a value otherwise this handler wouldn't be active
                            .getDefaultErrorViewConfigDescriptor().getViewId();
                }
            }

            if (viewId != null)
            {
                UIViewRoot uiViewRoot = facesContext.getApplication().getViewHandler().createView(facesContext, viewId);

                if (uiViewRoot == null)
                {
                    continue;
                }

                if (facesContext.isProjectStage(jakarta.faces.application.ProjectStage.Development) ||
                        ProjectStageProducer.getInstance().getProjectStage() == ProjectStage.Development ||
                        ProjectStageProducer.getInstance().getProjectStage() instanceof TestStage)
                {
                    throwable.printStackTrace();
                }

                facesContext.setViewRoot(uiViewRoot);
                exceptionQueuedEventIterator.remove();

                //record the current exception -> to check it at the next call or to use it on the error-page
                flash.put(throwable.getClass().getName(), throwable);
                flash.keep(throwable.getClass().getName());

                this.viewNavigationHandler.navigateTo(DefaultErrorView.class);

                break;
            }
        }

        this.wrapped.handle();
    }

    //TODO it should be possible to configure the exceptions we handle
    //e.g. @View could specify which exception/s a page
    // - can recover from (-> redirect in case of a POST to refresh the state + optional message)
    // - can't recover from (-> redirect to the default-error page)
    protected boolean isExceptionToHandle(Throwable throwable)
    {
        return throwable instanceof ViewExpiredException ||
                throwable instanceof ContextNotActiveException;
    }

    private void lazyInit()
    {
        if (this.viewNavigationHandler == null)
        {
            this.viewNavigationHandler = BeanProvider.getContextualReference(ViewNavigationHandler.class);
        }
    }

    @Override
    public ExceptionHandler getWrapped()
    {
        lazyInit();
        return wrapped;
    }
}
