/*
 * Copyright (C) 2014 Red Hat, Inc. and/or its affiliates.
 *
 * 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.cdi.server.events;

import static org.jboss.errai.enterprise.client.cdi.api.CDI.getSubjectNameByType;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.EventMetadata;
import javax.enterprise.inject.spi.ObserverMethod;

import org.jboss.errai.bus.client.api.QueueSession;
import org.jboss.errai.bus.client.api.RoutingFlag;
import org.jboss.errai.bus.client.api.base.CommandMessage;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageBus;
import org.jboss.errai.bus.client.api.messaging.MessageCallback;
import org.jboss.errai.bus.server.api.RpcContext;
import org.jboss.errai.bus.server.util.LocalContext;
import org.jboss.errai.common.client.protocols.MessageParts;
import org.jboss.errai.config.rebind.EnvUtil;
import org.jboss.errai.enterprise.client.cdi.CDICommands;
import org.jboss.errai.enterprise.client.cdi.CDIProtocol;
import org.jboss.errai.enterprise.client.cdi.api.CDI;
import org.jboss.errai.enterprise.client.cdi.api.Conversational;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Acts as a bridge between Errai Bus and the CDI event system.
 *
 * @author Mike Brock
 * @author Christian Sadilek <csadilek@redhat.com>
 * @author Jonathan Fuerth <jfuerth@redhat.com>
 */
public class EventDispatcher implements MessageCallback {
  private static final Logger log = LoggerFactory.getLogger("EventDispatcher");
  private static final String CDI_EVENT_CHANNEL_OPEN = "cdi.event.channel.open";

  private final BeanManager beanManager;
  private final EventRoutingTable eventRoutingTable;
  private final MessageBus messagebus;
  private final Set<String> observedEvents;
  private final Map<String, Annotation> eventQualifiers;

  private final Set<ClientObserverMetadata> clientObservers = Collections
          .newSetFromMap(new ConcurrentHashMap<ClientObserverMetadata, Boolean>());

  public EventDispatcher(final BeanManager beanManager, final EventRoutingTable eventRoutingTable,
          final MessageBus messageBus, final Set<String> observedEvents, final Map<String, Annotation> eventQualifiers) {

    this.beanManager = beanManager;
    this.eventRoutingTable = eventRoutingTable;
    this.messagebus = messageBus;
    this.observedEvents = observedEvents;
    this.eventQualifiers = eventQualifiers;
  }

  @Override
  @SuppressWarnings("unchecked")
  public void callback(final Message message) {
    if (!message.isFlagSet(RoutingFlag.FromRemote))
      return;

    try {
      final LocalContext localContext = LocalContext.get(message);
      final String typeName = message.get(String.class, CDIProtocol.BeanType);
      final Set<String> annotationTypes = message.get(Set.class, CDIProtocol.Qualifiers);

      switch (CDICommands.valueOf(message.getCommandType())) {
      case RemoteSubscribe:
        final Class<?> type = Thread.currentThread().getContextClassLoader().loadClass(typeName);
        final ClientObserverMetadata clientObserver = new ClientObserverMetadata(type, annotationTypes);

        if (!clientObservers.contains(clientObserver)) {
          if (type == null || !EnvUtil.isPortableType(type)) {
            log.warn("client tried to register a non-portable type: " + type);
            return;
          }
          clientObservers.add(clientObserver);
        }
        eventRoutingTable.activateRoute(typeName, annotationTypes, message.getResource(QueueSession.class, "Session"));
        break;

      case RemoteUnsubscribe:
        eventRoutingTable.deactivateRoute(typeName, annotationTypes, message.getResource(QueueSession.class, "Session"));
        break;

      case CDIEvent:
        if (!isRoutable(localContext, message)) {
          return;
        }

        final Object o = message.get(Object.class, CDIProtocol.BeanReference);
        try {
          RpcContext.set(message);
          final Set<String> qualifierNames = message.get(Set.class, CDIProtocol.Qualifiers);
          final List<Annotation> qualifiers = new ArrayList<Annotation>();

          if (qualifierNames != null) {
            for (final String serializedQualifier : qualifierNames) {
              final Annotation qualifier = eventQualifiers.get(serializedQualifier);
              if (qualifier != null) {
                qualifiers.add(qualifier);
              }
            }
          }
          // Fire event to all local observers
          final Annotation[] qualArray = qualifiers.toArray(new Annotation[qualifiers.size()]);
          final Set<ObserverMethod<? super Object>> observerMethods = beanManager.resolveObserverMethods(o, qualArray);
          for (final ObserverMethod<? super Object> observer : observerMethods) {
            // Don't mirror the event back to the clients
            if (!(AnyEventObserver.class.equals(observer.getBeanClass()))) {
              observer.notify(o);
            }
          }
        } finally {
          RpcContext.remove();
        }

        break;

      case AttachRemote:
        if (observedEvents.size() > 0) {
          MessageBuilder.createConversation(message).toSubject(CDI.CLIENT_DISPATCHER_SUBJECT)
                  .command(CDICommands.AttachRemote).with(MessageParts.RemoteServices, getEventTypes()).done().reply();
        }
        else {
          MessageBuilder.createConversation(message).toSubject(CDI.CLIENT_DISPATCHER_SUBJECT)
                  .command(CDICommands.AttachRemote).with(MessageParts.RemoteServices, "").done().reply();
        }

        localContext.setAttribute(CDI_EVENT_CHANNEL_OPEN, "1");
        break;

      default:
        throw new IllegalArgumentException("Unknown command type " + message.getCommandType());
      }
    } catch (final Exception e) {
      throw new RuntimeException("Failed to dispatch CDI Event", e);
    }
  }

  private String getEventTypes() {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final String s : observedEvents) {

      if (stringBuilder.length() != 0) {
        stringBuilder.append(",");
      }
      stringBuilder.append(s);
    }
    return stringBuilder.toString();
  }

  public boolean isRoutable(final LocalContext localContext, final Message message) {
    return "1".equals(localContext.getAttribute(String.class, CDI_EVENT_CHANNEL_OPEN))
            && observedEvents.contains(message.get(String.class, CDIProtocol.BeanType));
  }

  public void sendEventToClients(Object event, EventMetadata emd) {
    for (final ClientObserverMetadata clientObserver : clientObservers) {
      if (clientObserver.matches(event, emd)) {
        sendEventToClient(event, clientObserver.getQualifiers());
      }
    }
  }

  private void sendEventToClient(Object event, Set<String> qualifierTypes) {
    final Class<? extends Object> eventType = event.getClass();
    final String sessionId = getConversationalSessionId(eventType);

    final Map<String, Object> messageParts = new HashMap<String, Object>(10);
    messageParts.put(MessageParts.ToSubject.name(), getSubjectNameByType(eventType.getName()));
    messageParts.put(MessageParts.CommandType.name(), CDICommands.CDIEvent.name());
    messageParts.put(CDIProtocol.BeanType.name(), eventType.getName());
    messageParts.put(CDIProtocol.BeanReference.name(), event);

    if (!qualifierTypes.isEmpty()) {
      messageParts.put(CDIProtocol.Qualifiers.name(), qualifierTypes);
    }

    if (sessionId != null) {
      messageParts.put(MessageParts.SessionID.name(), sessionId);
      messagebus.send(CommandMessage.createWithParts(messageParts));
    }
    else {
      for (final String id : eventRoutingTable.getQueueIdsForRoute(eventType.getName(), qualifierTypes)) {
        messagebus.send(CommandMessage.createWithParts(new RoutingMap(messageParts, id)));
      }
    }
  }
  
  static String getConversationalSessionId(Class<? extends Object> eventType) {
    final QueueSession queueSession = RpcContext.getQueueSession();
    if (eventType.isAnnotationPresent(Conversational.class) && queueSession != null && queueSession.getSessionId() != null) {
      return queueSession.getSessionId();
    }
    
    return null;
  }
}
