/*
 * Copyright 2011 JBoss, by Red Hat, Inc
 *
 * Licensed 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.jboss.errai.bus.server.service;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.jboss.errai.bus.client.api.Local;
import org.jboss.errai.bus.client.api.Message;
import org.jboss.errai.bus.client.api.MessageCallback;
import org.jboss.errai.bus.client.api.builder.DefaultRemoteCallBuilder;
import org.jboss.errai.bus.client.framework.MessageBus;
import org.jboss.errai.bus.client.framework.ProxyFactory;
import org.jboss.errai.bus.client.framework.RequestDispatcher;
import org.jboss.errai.config.rebind.RebindUtils;
import org.jboss.errai.bus.server.annotations.Command;
import org.jboss.errai.bus.server.annotations.Remote;
import org.jboss.errai.bus.server.annotations.Service;
import org.jboss.errai.bus.server.annotations.security.RequireAuthentication;
import org.jboss.errai.bus.server.annotations.security.RequireRoles;
import org.jboss.errai.bus.server.io.CommandBindingsCallback;
import org.jboss.errai.bus.server.io.ConversationalEndpointCallback;
import org.jboss.errai.bus.server.io.RemoteServiceCallback;
import org.jboss.errai.bus.server.io.ServiceInstanceProvider;
import org.jboss.errai.bus.server.security.auth.rules.RolesRequiredRule;
import org.jboss.errai.bus.server.service.bootstrap.BootstrapContext;
import org.jboss.errai.bus.server.service.bootstrap.GuiceProviderProxy;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.common.client.api.ResourceProvider;
import org.jboss.errai.common.client.api.tasks.TaskManager;
import org.jboss.errai.common.client.api.tasks.TaskManagerFactory;
import org.jboss.errai.common.metadata.MetaDataProcessor;
import org.jboss.errai.common.metadata.MetaDataScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

/**
 * @author Heiko Braun <hbraun@redhat.com>
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public class ServiceProcessor implements MetaDataProcessor<BootstrapContext> {
  private Logger log = LoggerFactory.getLogger(ServiceProcessor.class);

  // TODO need to exclude client classes based on GWT module definition
  private static final String CLIENT_PKG_REGEX = ".*(\\.client\\.).*";

  @Override
  public void process(final BootstrapContext context, MetaDataScanner reflections) {
    final ErraiServiceConfiguratorImpl config = (ErraiServiceConfiguratorImpl) context.getConfig();
    final Set<Class<?>> services = reflections.getTypesAnnotatedWithExcluding(Service.class, CLIENT_PKG_REGEX);

    for (Class<?> loadClass : services) {
      Object svc = null;

      Service svcAnnotation = loadClass.getAnnotation(Service.class);
      if (null == svcAnnotation) {
        // Diagnose Errai-111
        StringBuilder sb = new StringBuilder();
        sb.append("Service annotation cannot be loaded. (See https://jira.jboss.org/browse/ERRAI-111)\n");
        sb.append(loadClass.getSimpleName()).append(" loader: ").append(loadClass.getClassLoader()).append("\n");
        sb.append("@Service loader:").append(Service.class.getClassLoader()).append("\n");
        log.warn(sb.toString());
        continue;
      }

      boolean local = loadClass.isAnnotationPresent(Local.class);

      String svcName = svcAnnotation.value();

      // If no name is specified, just use the class name as the service by default.
      if ("".equals(svcName)) {
        svcName = loadClass.getSimpleName();
      }

      Map<String, Method> commandPoints = new HashMap<String, Method>();
      for (final Method method : loadClass.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Command.class)) {
          Command command = method.getAnnotation(Command.class);
          for (String cmdName : command.value()) {
            if (cmdName.equals("")) cmdName = method.getName();
            commandPoints.put(cmdName, method);
          }
        }
      }

      Class remoteImpl = getRemoteImplementation(loadClass);
      if (remoteImpl != null) {
        svc = createRPCScaffolding(remoteImpl, loadClass, context);
      }

      if (MessageCallback.class.isAssignableFrom(loadClass)) {
        final Class<? extends MessageCallback> clazz = loadClass.asSubclass(MessageCallback.class);
        log.debug("discovered service: " + clazz.getName());
        try {
          svc = Guice.createInjector(new AbstractModule() {
            @Override
            protected void configure() {
              bind(MessageCallback.class).to(clazz);
              bind(MessageBus.class).toInstance(context.getBus());
              bind(RequestDispatcher.class).toInstance(context.getService().getDispatcher());
              bind(TaskManager.class).toInstance(TaskManagerFactory.get());

              // Add any extension bindings.
              for (Map.Entry<Class<?>, ResourceProvider> entry : config.getExtensionBindings().entrySet()) {
                bind(entry.getKey()).toProvider(new GuiceProviderProxy(entry.getValue()));
              }
            }
          }).getInstance(MessageCallback.class);
        }
        catch (Throwable t) {
          t.printStackTrace();
        }

        if (commandPoints.isEmpty()) {
          // Subscribe the service to the bus.
          if (local) {
            context.getBus().subscribeLocal(svcName, (MessageCallback) svc);
          }
          else {
            context.getBus().subscribe(svcName, (MessageCallback) svc);
          }
        }
      }

      if (svc == null) {
        svc = Guice.createInjector(new AbstractModule() {
          @Override
          protected void configure() {
            bind(MessageBus.class).toInstance(context.getBus());
            bind(RequestDispatcher.class).toInstance(context.getService().getDispatcher());
            bind(TaskManager.class).toInstance(TaskManagerFactory.get());

            // Add any extension bindings.
            for (Map.Entry<Class<?>, ResourceProvider> entry : config.getExtensionBindings().entrySet()) {
              bind(entry.getKey()).toProvider(new GuiceProviderProxy(entry.getValue()));
            }
          }
        }).getInstance(loadClass);
      }

      RolesRequiredRule rule = null;
      if (loadClass.isAnnotationPresent(RequireRoles.class)) {
        rule = new RolesRequiredRule(loadClass.getAnnotation(RequireRoles.class).value(), context.getBus());
      }
      else if (loadClass.isAnnotationPresent(RequireAuthentication.class)) {
        rule = new RolesRequiredRule(new HashSet<Object>(), context.getBus());
      }

      if (!commandPoints.isEmpty()) {
        if (local) {
          context.getBus().subscribeLocal(svcName, new CommandBindingsCallback(commandPoints, svc, context.getBus()));
        }
        else {
          context.getBus().subscribe(svcName, new CommandBindingsCallback(commandPoints, svc, context.getBus()));
        }
      }

      if (rule != null) {
        context.getBus().addRule(svcName, rule);
      }
    }
  }

  private static Class getRemoteImplementation(Class type) {
    for (Class iface : type.getInterfaces()) {
      if (iface.isAnnotationPresent(Remote.class)) {
        return iface;
      }
      else if (iface.getInterfaces().length != 0 && ((iface = getRemoteImplementation(iface)) != null)) {
        return iface;
      }
    }
    return null;
  }

  private static Object createRPCScaffolding(final Class remoteIface, final Class<?> type, final BootstrapContext context) {

    final ErraiServiceConfiguratorImpl config = (ErraiServiceConfiguratorImpl) context.getConfig();
    final Injector injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(MessageBus.class).toInstance(context.getBus());
        bind(RequestDispatcher.class).toInstance(context.getService().getDispatcher());
        bind(TaskManager.class).toInstance(TaskManagerFactory.get());

        // Add any extension bindings.
        for (Map.Entry<Class<?>, ResourceProvider> entry : config.getExtensionBindings().entrySet()) {
          bind(entry.getKey()).toProvider(new GuiceProviderProxy(entry.getValue()));
        }
      }
    });

    final Object svc = injector.getInstance(type);

    final Map<String, MessageCallback> epts = new HashMap<String, MessageCallback>();

    // beware of classloading issues. better reflect on the actual instance
    for (Class<?> intf : svc.getClass().getInterfaces()) {
      for (final Method method : intf.getMethods()) {
        if (RebindUtils.isMethodInInterface(remoteIface, method)) {
          epts.put(RebindUtils.createCallSignature(intf, method), new ConversationalEndpointCallback(new ServiceInstanceProvider() {
            @Override
            public Object get(Message message) {
              return svc;
            }
          }, method, context.getBus()));
        }
      }
    }

    context.getBus().subscribe(remoteIface.getName() + ":RPC", new RemoteServiceCallback(epts));

    // note: this method just exists because we want AbstractRemoteCallBuilder to be package private.
    DefaultRemoteCallBuilder.setProxyFactory(Assert.notNull(new ProxyFactory() {
      @Override
      public <T> T getRemoteProxy(Class<T> proxyType) {
        throw new RuntimeException("There is not yet an available Errai RPC implementation for the server-side environment.");
      }
    }));

    return svc;
  }
}
