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

import org.apache.deltaspike.core.api.config.view.DefaultErrorView;
import org.apache.deltaspike.core.api.config.view.ViewConfig;
import org.apache.deltaspike.core.api.config.view.ViewRef;
import org.apache.deltaspike.core.api.config.view.metadata.ViewConfigDescriptor;
import org.apache.deltaspike.core.api.config.view.metadata.ViewConfigResolver;
import org.apache.deltaspike.core.api.provider.BeanManagerProvider;
import org.apache.deltaspike.core.api.provider.BeanProvider;
import org.apache.deltaspike.core.util.ClassUtils;
import org.apache.deltaspike.core.api.config.view.navigation.NavigationParameter;
import org.apache.deltaspike.jsf.api.config.view.View;
import org.apache.deltaspike.core.api.config.view.navigation.event.PreViewConfigNavigateEvent;
import org.apache.deltaspike.core.api.config.view.navigation.NavigationParameterContext;
import org.apache.deltaspike.jsf.impl.util.JsfUtils;

import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.faces.application.NavigationHandler;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

public class ViewConfigAwareNavigationHandler extends NavigationHandler
{
    private Set<String> otherOutcomes = new CopyOnWriteArraySet<String>();
    private Map<String, ViewConfigDescriptor> viewConfigs = new ConcurrentHashMap<String, ViewConfigDescriptor>();

    private final NavigationHandler navigationHandler;

    private volatile BeanManager beanManager;

    private NavigationParameterContext navigationParameterContext;

    private ViewConfigResolver viewConfigResolver;

    /**
     * Constructor which allows to use the given {@link NavigationHandler}
     *
     * @param navigationHandler navigation-handler of jsf
     */
    public ViewConfigAwareNavigationHandler(NavigationHandler navigationHandler)
    {
        this.navigationHandler = navigationHandler;
    }

    //Security checks will be performed by the view-handler provided by ds
    @Override
    public void handleNavigation(FacesContext facesContext, String fromAction, String outcome)
    {
        lazyInit();
        if (outcome != null && outcome.contains("."))
        {
            String originalOutcome = outcome;

            if (!this.otherOutcomes.contains(outcome))
            {
                //it isn't possible to support interfaces due to cdi restrictions
                if (outcome.startsWith("class "))
                {
                    outcome = outcome.substring(6);
                }
                ViewConfigDescriptor entry = this.viewConfigs.get(outcome);

                if (entry == null)
                {
                    if (DefaultErrorView.class.getName().equals(originalOutcome))
                    {
                        entry = this.viewConfigResolver.getDefaultErrorViewConfigDescriptor();
                    }
                }

                boolean allowCaching = true;
                if (entry == null)
                {
                    Class<?> loadedClass = ClassUtils.tryToLoadClassForName(outcome);

                    if (loadedClass == null)
                    {
                        this.otherOutcomes.add(originalOutcome);
                    }
                    else if (ViewConfig.class.isAssignableFrom(loadedClass))
                    {
                        //a sub-classed page-config for annotating it with different view params
                        if (loadedClass.getAnnotation(View.class) == null &&
                                loadedClass.getSuperclass().getAnnotation(View.class) != null)
                        {
                            allowCaching = false;
                            addConfiguredViewParameters(loadedClass);

                            loadedClass = loadedClass.getSuperclass();
                        }
                        entry = this.viewConfigResolver
                                .getViewConfigDescriptor((Class<? extends ViewConfig>) loadedClass);
                    }
                }

                if (entry != null)
                {
                    //in case of false it has been added already
                    if (allowCaching)
                    {
                        this.viewConfigs.put(outcome, entry);
                        addConfiguredViewParameters(entry.getConfigClass());
                    }

                    String oldViewId = null;

                    if (facesContext.getViewRoot() != null)
                    {
                        oldViewId = facesContext.getViewRoot().getViewId();
                    }

                    PreViewConfigNavigateEvent navigateEvent = firePreViewConfigNavigateEvent(oldViewId, entry);

                    entry = tryToUpdateEntry(entry, navigateEvent);

                    if (entry != null)
                    {
                        outcome = convertEntryToOutcome(facesContext.getExternalContext(), entry);
                    }
                }
            }
        }

        this.navigationHandler.handleNavigation(facesContext, fromAction, outcome);
    }

    private void addConfiguredViewParameters(Class<?> viewConfigClass)
    {
        if (this.navigationParameterContext != null)
        {
            NavigationParameter navigationParameter = viewConfigClass.getAnnotation(NavigationParameter.class);

            if (navigationParameter != null)
            {
                addConfiguredPageParameter(navigationParameter);
            }
            else
            {
                NavigationParameter.List pageParameterList =
                        viewConfigClass.getAnnotation(NavigationParameter.List.class);

                if (pageParameterList != null)
                {
                    for (NavigationParameter currentNavigationParameter : pageParameterList.value())
                    {
                        addConfiguredPageParameter(currentNavigationParameter);
                    }
                }
            }
        }
    }

    private void addConfiguredPageParameter(NavigationParameter viewParameter)
    {
        JsfUtils.addStaticNavigationParameter(
            this.navigationParameterContext, viewParameter.key(), viewParameter.value());
    }

    private String convertEntryToOutcome(ExternalContext externalContext, ViewConfigDescriptor entry)
    {
        View viewMetaData = entry.getMetaData(View.class).iterator().next();

        boolean performRedirect = View.NavigationMode.REDIRECT.equals(viewMetaData.navigation());
        boolean includeViewParameters = View.ViewParameterMode.INCLUDE.equals(viewMetaData.viewParams());

        StringBuilder result = new StringBuilder(entry.getViewId());

        if (performRedirect)
        {
            result.append("?faces-redirect=true");
        }
        if (includeViewParameters)
        {
            if (performRedirect)
            {
                result.append("&");
            }
            else
            {
                result.append("?");
            }
            result.append("includeViewParams=true");

            return JsfUtils.addPageParameters(externalContext, result.toString(), false);
        }

        return result.toString();
    }

    private ViewConfigDescriptor tryToUpdateEntry(ViewConfigDescriptor viewConfigDescriptor,
                                                  PreViewConfigNavigateEvent navigateEvent)
    {
        if (navigateEvent.getToView() == null)
        {
            return null;
        }

        if (navigateEvent.getToView().equals(viewConfigDescriptor.getConfigClass()))
        {
            return viewConfigDescriptor;
        }

        return this.viewConfigResolver.getViewConfigDescriptor(navigateEvent.getToView());
    }

    private PreViewConfigNavigateEvent firePreViewConfigNavigateEvent(String oldViewId,
                                                                      ViewConfigDescriptor newViewConfigDescriptor)
    {
        ViewConfigDescriptor oldViewConfigDescriptor = this.viewConfigResolver.getViewConfigDescriptor(oldViewId);

        PreViewConfigNavigateEvent navigateEvent;

        if (oldViewConfigDescriptor != null)
        {
            navigateEvent = new PreViewConfigNavigateEvent(
                    oldViewConfigDescriptor.getConfigClass(), newViewConfigDescriptor.getConfigClass());
        }
        else
        {
            navigateEvent = new PreViewConfigNavigateEvent(
                    ViewRef.Manual.class, newViewConfigDescriptor.getConfigClass());
        }

        this.beanManager.getEvent().fire(navigateEvent);
        return navigateEvent;
    }

    private void lazyInit()
    {
        if (this.beanManager == null)
        {
            init();
        }
    }

    private synchronized void init()
    {
        if (this.beanManager == null)
        {
            this.navigationParameterContext =
                    BeanProvider.getContextualReference(NavigationParameterContext.class);
            this.viewConfigResolver =
                    BeanProvider.getContextualReference(ViewConfigResolver.class);

            this.beanManager = BeanManagerProvider.getInstance().getBeanManager();
        }
    }
}
