/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.jboss.as.ee.component;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.as.ee.logging.EeLogger;
import org.jboss.as.ee.utils.DescriptorUtils;
import org.jboss.as.naming.ManagedReference;
import org.jboss.invocation.Interceptor;
import org.jboss.invocation.InterceptorContext;
import org.jboss.invocation.InterceptorFactory;
import org.jboss.invocation.Interceptors;
import org.jboss.invocation.SimpleInterceptorFactoryContext;
import org.jboss.invocation.proxy.ProxyFactory;
import org.jboss.msc.inject.Injector;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.wildfly.security.manager.WildFlySecurityManager;

import static org.jboss.as.ee.logging.EeLogger.ROOT_LOGGER;

/**
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public final class ViewService implements Service<ComponentView> {

    private final InjectedValue<Component> componentInjector = new InjectedValue<Component>();
    private final Map<Method, InterceptorFactory> viewInterceptorFactories;
    private final Map<Method, InterceptorFactory> clientInterceptorFactories;
    private final InterceptorFactory clientPostConstruct;
    private final InterceptorFactory clientPreDestroy;
    private final ProxyFactory<?> proxyFactory;
    private final Class<?> viewClass;
    private final Set<Method> asyncMethods;
    private final ViewInstanceFactory viewInstanceFactory;
    private final Map<Class<?>, Object> privateData;
    private volatile ComponentView view;

    private volatile Interceptor clientPostConstructInterceptor;
    private volatile Interceptor clientPreDestroyInterceptor;
    private volatile Map<Method, Interceptor> clientInterceptors;


    public ViewService(final ViewConfiguration viewConfiguration) {
        viewClass = viewConfiguration.getViewClass();
        final ProxyFactory<?> proxyFactory = viewConfiguration.getProxyFactory();
        this.proxyFactory = proxyFactory;
        final List<Method> methods = proxyFactory.getCachedMethods();
        final int methodCount = methods.size();
        clientPostConstruct = Interceptors.getChainedInterceptorFactory(viewConfiguration.getClientPostConstructInterceptors());
        clientPreDestroy = Interceptors.getChainedInterceptorFactory(viewConfiguration.getClientPreDestroyInterceptors());
        final IdentityHashMap<Method, InterceptorFactory> viewInterceptorFactories = new IdentityHashMap<Method, InterceptorFactory>(methodCount);
        final IdentityHashMap<Method, InterceptorFactory> clientInterceptorFactories = new IdentityHashMap<Method, InterceptorFactory>(methodCount);
        for (final Method method : methods) {
            if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) {
                viewInterceptorFactories.put(method, Interceptors.getTerminalInterceptorFactory());
            } else {
                viewInterceptorFactories.put(method, Interceptors.getChainedInterceptorFactory(viewConfiguration.getViewInterceptors(method)));
                clientInterceptorFactories.put(method, Interceptors.getChainedInterceptorFactory(viewConfiguration.getClientInterceptors(method)));
            }
        }
        this.viewInterceptorFactories = viewInterceptorFactories;
        this.clientInterceptorFactories = clientInterceptorFactories;
        this.asyncMethods = viewConfiguration.getAsyncMethods();
        if (viewConfiguration.getViewInstanceFactory() == null) {
            viewInstanceFactory = new DefaultViewInstanceFactory();
        } else {
            viewInstanceFactory = viewConfiguration.getViewInstanceFactory();
        }
        if(viewConfiguration.getPrivateData().isEmpty()) {
            privateData = Collections.emptyMap();
        } else {
            privateData = viewConfiguration.getPrivateData();
        }
    }

    public void start(final StartContext context) throws StartException {
        // Construct the view
        View view = new View(privateData);
        view.initializeInterceptors();
        this.view = view;

        final SimpleInterceptorFactoryContext factoryContext = new SimpleInterceptorFactoryContext();
        final Component component = view.getComponent();
        factoryContext.getContextData().put(Component.class, component);
        factoryContext.getContextData().put(ComponentView.class, view);

        clientPostConstructInterceptor = clientPostConstruct.create(factoryContext);
        clientPreDestroyInterceptor = clientPreDestroy.create(factoryContext);

        final Map<Method, InterceptorFactory> clientInterceptorFactories = ViewService.this.clientInterceptorFactories;
        clientInterceptors = new IdentityHashMap<Method, Interceptor>(clientInterceptorFactories.size());
        for (Method method : clientInterceptorFactories.keySet()) {
            clientInterceptors.put(method, clientInterceptorFactories.get(method).create(factoryContext));
        }


    }

    public void stop(final StopContext context) {
        view = null;
    }

    public Injector<Component> getComponentInjector() {
        return componentInjector;
    }

    public ComponentView getValue() throws IllegalStateException, IllegalArgumentException {
        return view;
    }

    class View implements ComponentView {

        private final Component component;
        private final Map<Method, Interceptor> viewInterceptors;
        private final Map<MethodDescription, Method> methods;
        private final Map<Class<?>, Object> privateData;

        View(final Map<Class<?>, Object> privateData) {
            this.privateData = privateData;
            component = componentInjector.getValue();
            //we need to build the view interceptor chain
            this.viewInterceptors = new IdentityHashMap<Method, Interceptor>();
            this.methods = new HashMap<MethodDescription, Method>();
        }

        void initializeInterceptors() {
            final SimpleInterceptorFactoryContext factoryContext = new SimpleInterceptorFactoryContext();
            final Map<Method, InterceptorFactory> viewInterceptorFactories = ViewService.this.viewInterceptorFactories;
            final Map<Method, Interceptor> viewEntryPoints = viewInterceptors;
            factoryContext.getContextData().put(Component.class, component);
            //we don't have this code in the constructor so we avoid passing around
            //a half constructed instance
            factoryContext.getContextData().put(ComponentView.class, this);

            for (Method method : viewInterceptorFactories.keySet()) {
                viewEntryPoints.put(method, viewInterceptorFactories.get(method).create(factoryContext));
                methods.put(new MethodDescription(method.getName(), DescriptorUtils.methodDescriptor(method)), method);
            }

        }

        public ManagedReference createInstance() throws Exception {
            return createInstance(Collections.<Object, Object>emptyMap());
        }

        public ManagedReference createInstance(Map<Object, Object> contextData) throws Exception {
            // view instance creation can lead to instantiating application component classes (like the MDB implementation class
            // or even the EJB implementation class of a no-interface view exposing bean). Such class initialization needs to
            // have the TCCL set to the component/application's classloader. @see https://issues.jboss.org/browse/WFLY-3989
            final ClassLoader oldTCCL = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
            try {
                WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(component.getComponentClass());
                return viewInstanceFactory.createViewInstance(this, contextData);
            } finally {
                // reset the TCCL
                WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(oldTCCL);
            }
        }

        @Override
        public Object invoke(InterceptorContext interceptorContext) throws Exception {
            final Method method = interceptorContext.getMethod();
            final Interceptor interceptor = viewInterceptors.get(method);
            return interceptor.processInvocation(interceptorContext);
        }

        public Component getComponent() {
            return component;
        }

        @Override
        public Class<?> getProxyClass() {
            return proxyFactory.defineClass();
        }

        @Override
        public Class<?> getViewClass() {
            return viewClass;
        }

        @Override
        public Set<Method> getViewMethods() {
            return viewInterceptors.keySet();
        }

        @Override
        public Method getMethod(final String name, final String descriptor) {
            Method method = this.methods.get(new MethodDescription(name, descriptor));
            if (method == null) {
                throw EeLogger.ROOT_LOGGER.viewMethodNotFound(name, descriptor, viewClass, component.getComponentClass());
            }
            return method;
        }

        @Override
        public <T> T getPrivateData(final Class<T> clazz) {
            return (T) privateData.get(clazz);
        }

        @Override
        public boolean isAsynchronous(final Method method) {
            return asyncMethods.contains(method);
        }

        @Override
        public String toString() {
            return "Component view " + viewClass + " for component "
                    + component.getComponentClass();
        }

        private final class MethodDescription {
            private final String name;
            private final String descriptor;

            public MethodDescription(final String name, final String descriptor) {
                this.name = name;
                this.descriptor = descriptor;
            }

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

                final MethodDescription that = (MethodDescription) o;

                if (!descriptor.equals(that.descriptor)) return false;
                if (!name.equals(that.name)) return false;

                return true;
            }

            @Override
            public int hashCode() {
                int result = name.hashCode();
                result = 31 * result + descriptor.hashCode();
                return result;
            }
        }
    }

    private class DefaultViewInstanceFactory implements ViewInstanceFactory {

        public ManagedReference createViewInstance(final ComponentView componentView, final Map<Object, Object> contextData) throws Exception {

            final Object proxy;
            final Component component = componentView.getComponent();
            final ComponentClientInstance instance = new ComponentClientInstance();
            try {
                proxy = proxyFactory.newInstance(new ProxyInvocationHandler(clientInterceptors, instance, componentView));
            } catch (InstantiationException e) {
                InstantiationError error = new InstantiationError(e.getMessage());
                Throwable cause = e.getCause();
                if (cause != null) error.initCause(cause);
                throw error;
            } catch (IllegalAccessException e) {
                IllegalAccessError error = new IllegalAccessError(e.getMessage());
                Throwable cause = e.getCause();
                if (cause != null) error.initCause(cause);
                throw error;
            }

            InterceptorContext context = new InterceptorContext();
            context.putPrivateData(ComponentView.class, componentView);
            context.putPrivateData(Component.class, component);
            context.putPrivateData(ComponentClientInstance.class, instance);
            context.setContextData(new HashMap<String, Object>());
            for(Map.Entry<Object, Object> entry : contextData.entrySet()) {
                context.putPrivateData(entry.getKey(), entry.getValue());
            }
            clientPostConstructInterceptor.processInvocation(context);
            instance.constructionComplete();

            return new ManagedReference() {

                @Override
                public void release() {
                    try {
                        InterceptorContext interceptorContext = new InterceptorContext();
                        interceptorContext.putPrivateData(ComponentView.class, componentView);
                        interceptorContext.putPrivateData(Component.class, component);
                        clientPreDestroyInterceptor.processInvocation(interceptorContext);
                    } catch (Exception e) {
                        ROOT_LOGGER.preDestroyInterceptorFailure(e, component.getComponentClass());
                    }
                }

                @Override
                public Object getInstance() {
                    return proxy;
                }
            };
        }
    }
}

