/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.jboss.as.ee.component;

import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;

import org.jboss.as.ee.component.interceptors.ComponentDispatcherInterceptor;
import org.jboss.as.ee.component.interceptors.InterceptorOrder;
import org.jboss.as.server.deployment.DeploymentPhaseContext;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.reflect.ClassReflectionIndex;
import org.jboss.as.server.deployment.reflect.ClassReflectionIndexUtil;
import org.jboss.as.server.deployment.reflect.DeploymentReflectionIndex;
import org.jboss.invocation.ImmediateInterceptorFactory;
import org.jboss.invocation.Interceptor;
import org.jboss.invocation.InterceptorContext;
import org.jboss.invocation.Interceptors;
import org.jboss.invocation.proxy.MethodIdentifier;
import org.jboss.invocation.proxy.ProxyFactory;
import org.jboss.msc.service.ServiceName;

import static java.lang.reflect.Modifier.ABSTRACT;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
import static org.jboss.as.server.deployment.Attachments.REFLECTION_INDEX;

/**
 * A description of a view.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public class ViewDescription {

    //JVM bridge method flag
    public static final int BRIDGE = 0x0040;

    private final String viewClassName;
    private final ComponentDescription componentDescription;
    private final List<String> viewNameParts = new ArrayList<String>();
    private final Set<String> bindingNames = new HashSet<String>();
    private final Deque<ViewConfigurator> configurators = new ArrayDeque<ViewConfigurator>();
    private boolean serializable;
    private boolean useWriteReplace;
    private final String markupClassName;

    /**
     * Construct a new instance.
     *
     * @param componentDescription the associated component description
     * @param viewClassName        the view class name
     */
    public ViewDescription(final ComponentDescription componentDescription, final String viewClassName) {
        this(componentDescription, viewClassName, true);
    }

    /**
     * Construct a new instance.
     *
     * @param componentDescription        the associated component description
     * @param viewClassName               the view class name
     * @param defaultConfiguratorRequired
     */
    public ViewDescription(final ComponentDescription componentDescription, final String viewClassName, final boolean defaultConfiguratorRequired) {
        this(componentDescription, viewClassName, defaultConfiguratorRequired, null);
    }

    public ViewDescription(final ComponentDescription componentDescription, final String viewClassName, final boolean defaultConfiguratorRequired, final String markupClassName) {
        this.componentDescription = componentDescription;
        this.viewClassName = viewClassName;
        if (defaultConfiguratorRequired) {
            configurators.addFirst(DefaultConfigurator.INSTANCE);
        }
        configurators.addFirst(ViewBindingConfigurator.INSTANCE);
        this.markupClassName = markupClassName;
    }

    /**
     * Get the view's class name.
     *
     * @return the class name
     */
    public String getViewClassName() {
        return viewClassName;
    }

    /**
     * Get the associated component description.
     *
     * @return the component description
     */
    public ComponentDescription getComponentDescription() {
        return componentDescription;
    }

    /**
     * Get the strings to append to the view base name.  The view base name is the component base name
     * followed by {@code "VIEW"} followed by these strings.
     *
     * @return the list of strings
     */
    public List<String> getViewNameParts() {
        return viewNameParts;
    }

    /**
     * Get the service name for this view.
     *
     * @return the service name
     */
    public ServiceName getServiceName() {
        //TODO: need to set viewNameParts somewhere
        if (!viewNameParts.isEmpty()) {
            return componentDescription.getServiceName().append("VIEW").append(viewNameParts.toArray(new String[viewNameParts.size()]));
        } else {
            return componentDescription.getServiceName().append("VIEW").append(viewClassName);
        }
    }

    /**
     * Creates view configuration. Allows for extensibility in EE sub components.
     *
     * @param viewClass              view class
     * @param componentConfiguration component config
     * @param proxyFactory           proxy factory
     * @return new view configuration
     */
    public ViewConfiguration createViewConfiguration(final Class<?> viewClass, final ComponentConfiguration componentConfiguration, final ProxyFactory<?> proxyFactory) {
        return new ViewConfiguration(viewClass, componentConfiguration, getServiceName(), proxyFactory);
    }

    /**
     * Get the set of binding names for this view.
     *
     * @return the set of binding names
     */
    public Set<String> getBindingNames() {
        return bindingNames;
    }

    /**
     * Get the configurators for this view.
     *
     * @return the configurators
     */
    public Deque<ViewConfigurator> getConfigurators() {
        return configurators;
    }

    /**
     * Create the injection source
     *
     * @param serviceName     The view service name
     * @param viewClassLoader The view class loader
     * @param appclient       appclient environment
     */
    protected InjectionSource createInjectionSource(final ServiceName serviceName, Supplier<ClassLoader> viewClassLoader, boolean appclient) {
        return new ViewBindingInjectionSource(serviceName);
    }

    public static final ImmediateInterceptorFactory CLIENT_DISPATCHER_INTERCEPTOR_FACTORY = new ImmediateInterceptorFactory(new Interceptor() {
        public Object processInvocation(final InterceptorContext context) throws Exception {
            ComponentView view = context.getPrivateData(ComponentView.class);
            return view.invoke(context);
        }
    });

    private static class DefaultConfigurator implements ViewConfigurator {

        public static final DefaultConfigurator INSTANCE = new DefaultConfigurator();

        public void configure(final DeploymentPhaseContext context, final ComponentConfiguration componentConfiguration, final ViewDescription description, final ViewConfiguration configuration) throws DeploymentUnitProcessingException {
            // Create method indexes
            final DeploymentReflectionIndex reflectionIndex = context.getDeploymentUnit().getAttachment(REFLECTION_INDEX);
            final List<Method> methods = configuration.getProxyFactory().getCachedMethods();
            for (final Method method : methods) {
                MethodIdentifier methodIdentifier = MethodIdentifier.getIdentifierForMethod(method);
                Method componentMethod = ClassReflectionIndexUtil.findMethod(reflectionIndex, componentConfiguration.getComponentClass(), methodIdentifier);

                if (componentMethod == null && method.getDeclaringClass().isInterface() && (method.getModifiers() & (ABSTRACT | PUBLIC | STATIC)) == PUBLIC) {
                    // no component method and the interface method is defaulted, so we really do want to invoke on the interface method
                    componentMethod = method;
                }
                if (componentMethod != null) {

                    if ((BRIDGE & componentMethod.getModifiers()) != 0) {
                        Method other = findRealMethodForBridgeMethod(componentMethod, componentConfiguration, reflectionIndex, methodIdentifier);
                        //try and find the non-bridge method to delegate to
                        if(other != null) {
                                componentMethod = other;
                        }
                    }

                    configuration.addViewInterceptor(method, new ImmediateInterceptorFactory(new ComponentDispatcherInterceptor(componentMethod)), InterceptorOrder.View.COMPONENT_DISPATCHER);
                    configuration.addClientInterceptor(method, CLIENT_DISPATCHER_INTERCEPTOR_FACTORY, InterceptorOrder.Client.CLIENT_DISPATCHER);
                    configuration.getViewToComponentMethodMap().put(method, componentMethod);
                }
            }

            configuration.addClientPostConstructInterceptor(Interceptors.getTerminalInterceptorFactory(), InterceptorOrder.ClientPostConstruct.TERMINAL_INTERCEPTOR);
            configuration.addClientPreDestroyInterceptor(Interceptors.getTerminalInterceptorFactory(), InterceptorOrder.ClientPreDestroy.TERMINAL_INTERCEPTOR);
        }

        private Method findRealMethodForBridgeMethod(final Method componentMethod, final ComponentConfiguration componentConfiguration, final DeploymentReflectionIndex reflectionIndex, final MethodIdentifier methodIdentifier) {
            final ClassReflectionIndex classIndex = reflectionIndex.getClassIndex(componentMethod.getDeclaringClass()); //the non-bridge method will be on the same class as the bridge method
            final Collection<Method> methods = classIndex.getAllMethods(componentMethod.getName(), componentMethod.getParameterCount());
            for(final Method method : methods) {
                if (((BRIDGE & method.getModifiers()) == 0)
                        && componentMethod.getReturnType().isAssignableFrom(method.getReturnType())) {
                    boolean ok = true;
                    for (int i = 0; i < method.getParameterCount(); ++i) {
                        if (!componentMethod.getParameterTypes()[i].isAssignableFrom(method.getParameterTypes()[i])) {
                            ok = false;
                            break;
                        }
                    }
                    if (ok) {
                        return method;
                    }

                }
            }
            return null;
        }
    }


    private static class ViewBindingConfigurator implements ViewConfigurator {

        public static final ViewBindingConfigurator INSTANCE = new ViewBindingConfigurator();

        @Override
        public void configure(final DeploymentPhaseContext context, final ComponentConfiguration componentConfiguration, final ViewDescription description, final ViewConfiguration configuration) throws DeploymentUnitProcessingException {
            boolean appclient = context.getDeploymentUnit().getAttachment(Attachments.EE_MODULE_DESCRIPTION).isAppClient();
            // Create view bindings
            final List<BindingConfiguration> bindingConfigurations = configuration.getBindingConfigurations();
            for (String bindingName : description.getBindingNames()) {
                bindingConfigurations.add(new BindingConfiguration(bindingName, description.createInjectionSource(description.getServiceName(), () -> componentConfiguration.getModuleClassLoader(), appclient)));
            }
        }
    }

    public boolean isSerializable() {
        return serializable;
    }

    public void setSerializable(final boolean serializable) {
        this.serializable = serializable;
    }

    public boolean isUseWriteReplace() {
        return useWriteReplace;
    }

    public void setUseWriteReplace(final boolean useWriteReplace) {
        this.useWriteReplace = useWriteReplace;
    }

    @Override
    public String toString() {
        return "View of type " + viewClassName + " for " + componentDescription;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        final ViewDescription that = (ViewDescription) o;

        //compare the component description based on ==
        if (componentDescription != that.componentDescription)
            return false;
        if (viewClassName != null ? !viewClassName.equals(that.viewClassName) : that.viewClassName != null)
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = viewClassName != null ? viewClassName.hashCode() : 0;
        result = 31 * result + (componentDescription != null ? componentDescription.hashCode() : 0);
        return result;
    }

    public String getMarkupClassName() {
        return markupClassName;
    }

    public boolean requiresSuperclassInProxy() {
        return true;
    }
}
