/*
 * Copyright (C) 2012 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.bus.server;

import static org.jboss.errai.bus.client.api.base.MessageBuilder.createConversation;
import static org.jboss.errai.bus.client.util.ErrorHelper.handleMessageDeliveryFailure;
import static org.jboss.errai.bus.server.io.websockets.WebSocketTokenManager.getNewOneTimeToken;
import static org.jboss.errai.bus.server.io.websockets.WebSocketTokenManager.verifyOneTimeToken;
import static org.jboss.errai.common.client.protocols.MessageParts.ConnectionSessionKey;
import static org.jboss.errai.common.client.protocols.MessageParts.RemoteServices;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.http.HttpServletRequest;

import org.jboss.errai.bus.client.api.BusMonitor;
import org.jboss.errai.bus.client.api.QueueSession;
import org.jboss.errai.bus.client.api.RoutingFlag;
import org.jboss.errai.bus.client.api.SubscribeListener;
import org.jboss.errai.bus.client.api.Subscription;
import org.jboss.errai.bus.client.api.UnsubscribeListener;
import org.jboss.errai.bus.client.api.base.Capabilities;
import org.jboss.errai.bus.client.api.base.ConversationMessage;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.base.NoSubscribersToDeliverTo;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageCallback;
import org.jboss.errai.bus.client.framework.BuiltInServices;
import org.jboss.errai.bus.client.framework.SubscriptionEvent;
import org.jboss.errai.bus.client.protocols.BusCommand;
import org.jboss.errai.bus.client.util.BusTools;
import org.jboss.errai.bus.server.api.MessageQueue;
import org.jboss.errai.bus.server.api.QueueCloseEvent;
import org.jboss.errai.bus.server.api.QueueClosedListener;
import org.jboss.errai.bus.server.api.ServerMessageBus;
import org.jboss.errai.bus.server.cluster.ClusteringProvider;
import org.jboss.errai.bus.server.io.BufferHelper;
import org.jboss.errai.bus.server.io.PageUtil;
import org.jboss.errai.bus.server.io.buffers.BufferColor;
import org.jboss.errai.bus.server.io.buffers.TransmissionBuffer;
import org.jboss.errai.bus.server.io.websockets.WebSocketServerHandler;
import org.jboss.errai.bus.server.io.websockets.WebSocketTokenManager;
import org.jboss.errai.bus.server.service.ErraiConfigAttribs;
import org.jboss.errai.bus.server.service.ErraiService;
import org.jboss.errai.bus.server.service.ErraiServiceConfigurator;
import org.jboss.errai.bus.server.util.LocalContext;
import org.jboss.errai.bus.server.util.SecureHashUtil;
import org.jboss.errai.common.client.protocols.MessageParts;
import org.jboss.errai.common.client.protocols.Resources;
import org.jboss.errai.common.server.api.ErraiBootstrapFailure;
import org.slf4j.Logger;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * The <tt>ServerMessageBusImpl</tt> implements the <tt>ServerMessageBus</tt>, making it possible for the server to
 * send and receive messages
 *
 * @author Mike Brock
 */
@Singleton
public class ServerMessageBusImpl implements ServerMessageBus {
  private final TransmissionBuffer transmissionbuffer;

  private final Map<String, DeliveryPlan> subscriptions = new ConcurrentHashMap<String, DeliveryPlan>();
  private final Set<String> globalSubscriptions = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
  private final Map<String, RemoteMessageCallback> remoteSubscriptions = new ConcurrentHashMap<String, RemoteMessageCallback>();

  private final Map<QueueSession, MessageQueue> messageQueues = new ConcurrentHashMap<QueueSession, MessageQueue>();

  private final Map<MessageQueue, List<Message>> deferredQueue = new ConcurrentHashMap<MessageQueue, List<Message>>();
  private final Map<String, QueueSession> sessionLookup = new ConcurrentHashMap<String, QueueSession>();
  private final Map<String, ClusterWaitEntry> deadLetter = new ConcurrentHashMap<String, ClusterWaitEntry>();

  private final List<SubscribeListener> subscribeListeners = new ArrayList<SubscribeListener>();
  private final List<UnsubscribeListener> unsubscribeListeners = new ArrayList<UnsubscribeListener>();
  private final List<QueueClosedListener> queueClosedListeners = new ArrayList<QueueClosedListener>();

  private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

  private static final Logger log = getLogger(ServerMessageBus.class);

  private BusMonitor busMonitor;

  private final Set<String> reservedNames = new HashSet<String>();

  private final boolean hostedModeTesting;
  private final boolean doLongPolling;
  private final int messageQueueTimeoutSecs;
  private final boolean sseEnabled;
  private final boolean webSocketServlet;
  private final boolean webSocketServer;
  private final boolean useSecureWebsocket;

  private final boolean clustering;
  private final ClusteringProvider clusteringProvider;

  /**
   * Sets up the <tt>ServerMessageBusImpl</tt> with the configuration supplied. Also, initializes the bus' callback
   * functions, scheduler, and monitor
   * <p/>
   * When deploying services on the server-side, it is possible to obtain references to the
   * <tt>ErraiServiceConfigurator</tt> by declaring it as injection dependencies
   */
  @Inject
  public ServerMessageBusImpl(final ErraiService service, final ErraiServiceConfigurator config) {
    this.hostedModeTesting = ErraiConfigAttribs.HOSTED_MODE_TESTING.getBoolean(config);
    this.doLongPolling = !hostedModeTesting && ErraiConfigAttribs.DO_LONG_POLL.getBoolean(config);
    this.messageQueueTimeoutSecs = ErraiConfigAttribs.MESSAGE_QUEUE_TIMEOUT_SECS.getInt(config);
    this.sseEnabled = ErraiConfigAttribs.ENABLE_SSE_SUPPORT.getBoolean(config);
    this.webSocketServer = ErraiConfigAttribs.ENABLE_WEB_SOCKET_SERVER.getBoolean(config);

    final int webSocketPort;
    final String webSocketPath;

    webSocketServlet = ErraiConfigAttribs.WEBSOCKET_SERVLET_ENABLED.getBoolean(config);
    useSecureWebsocket = ErraiConfigAttribs.FORCE_SECURE_WEBSOCKET.getBoolean(config) 
            || ErraiConfigAttribs.SECURE_WEB_SOCKET_SERVER.getBoolean(config);

    if (webSocketServlet) {
      webSocketPath = ErraiConfigAttribs.WEBSOCKET_SERVLET_CONTEXT_PATH.get(config);
      webSocketPort = -1;
    }
    else {
      webSocketPath = ErraiConfigAttribs.WEB_SOCKET_URL.get(config);
      webSocketPort = ErraiConfigAttribs.WEB_SOCKET_PORT.getInt(config);
    }

    final Integer bufferSize = ErraiConfigAttribs.BUS_BUFFER_SIZE.getInt(config);
    Integer segmentSize = ErraiConfigAttribs.BUS_BUFFER_SEGMENT_SIZE.getInt(config);
    Integer segmentCount = ErraiConfigAttribs.BUS_BUFFER_SEGMENT_COUNT.getInt(config);
    final String allocMode = ErraiConfigAttribs.BUS_BUFFER_ALLOCATION_MODE.get(config);

    if (segmentSize == null) {
      segmentSize = 8 * 1024;
    }
    else {
      segmentSize = segmentSize * 1024;
    }

    if (bufferSize != null) {
      segmentCount = (bufferSize * 1024 * 1024) / segmentSize;
    }
    else if (segmentCount == null) {
      segmentCount = 4096;
    }

    final boolean directAlloc;
    if (allocMode != null) {
      if ("direct".equals(allocMode)) {
        directAlloc = true;
      }
      else if ("heap".equals(allocMode)) {
        directAlloc = false;
      }
      else {
        throw new ErraiBootstrapFailure("unrecognized option for property: "
            + ErraiConfigAttribs.BUS_BUFFER_ALLOCATION_MODE.get(config));
      }
    }
    else {
      directAlloc = false;
    }

    TransmissionBuffer buffer;
    if (directAlloc) {
      try {
        buffer = TransmissionBuffer.createDirect(segmentSize, segmentCount);
      }
      catch (OutOfMemoryError e) {
        log.warn("could not allocate direct memory buffer. insufficient direct memory. increase the direct memory " +
            "buffer size with the JVM argument: -XX:MaxDirectMemorySize=<size>");
        log.warn("falling back to a heap allocated buffer.");
        buffer = TransmissionBuffer.create(segmentSize, segmentCount);
      }
    }
    else {
      buffer = TransmissionBuffer.create(segmentSize, segmentCount);
    }

    transmissionbuffer = buffer;

    /**
     * Define the default ServerBus service used for intrabus communication.
     */
    subscribe(BuiltInServices.ServerBus.name(), new ServerBusMessageCallback(webSocketPath, webSocketPort));

    addSubscribeListener(new DefaultSubscribeListener());
    addUnsubscribeListener(new DefaultUnsubscribeListener());

    scheduler.scheduleAtFixedRate(new HousekeeeperRunnable(), 8, 8, TimeUnit.SECONDS);

    try {
      clustering = ErraiConfigAttribs.ENABLE_CLUSTERING.getBoolean(config);
      final String clusteringProviderCls = ErraiConfigAttribs.CLUSTERING_PROVIDER.get(config);
      //noinspection unchecked
      clusteringProvider = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure() {
          bind(ServerMessageBus.class).toInstance(ServerMessageBusImpl.this);
          bind(ErraiServiceConfigurator.class).toInstance(config);
          bind(ErraiService.class).toInstance(service);
        }
      }).getInstance((Class<ClusteringProvider>) Class.forName(clusteringProviderCls));
    }
    catch (Exception e) {
      throw new RuntimeException("could not initialize clustering provider", e);
    }
  }

  private void addQueue(final QueueSession session, final MessageQueue queue) {
    messageQueues.put(session, queue);
    sessionLookup.put(session.getSessionId(), session);
  }

  /**
   * Configures the server message bus with the specified <tt>ErraiServiceConfigurator</tt>.
   * Presently there are no configurable parameters.
   */
  @Override
  public void configure(final ErraiServiceConfigurator config) {
    // no configuration in current implementation
  }

  /**
   * Sends a message globally to all subscriptions containing the same subject as the specified message.
   *
   * @param message
   *     - The message to be sent.
   */
  @Override
  public void sendGlobal(final Message message) {
    verifyConnectionToMessageSource(message);
    message.commit();
    final String subject = message.getSubject();

    if (!subscriptions.containsKey(subject) && !subscriptions.containsKey("local:".concat(subject))
            && !remoteSubscriptions.containsKey(subject)) {

      delayOrFail(message, new Runnable() {
        @Override
        public void run() {
          sendGlobal(message);
        }
      });

      return;
    }

    if (isMonitor()) {
      if (message.isFlagSet(RoutingFlag.FromRemote)) {
        busMonitor.notifyIncomingMessageFromRemote(
            message.getResource(QueueSession.class, Resources.Session.name()).getSessionId(), message);
      }
      else {
        if (subscriptions.containsKey(subject)) {
          busMonitor.notifyInBusMessage(message);
        }
      }
    }

    if (subscriptions.containsKey(subject)) {
      subscriptions.get(subject).deliver(message);
    }
    else if (subscriptions.containsKey("local:".concat(subject))) {
      subscriptions.get("local:".concat(subject)).deliver(message);
    }
  }

  private void verifyConnectionToMessageSource(final Message message) {
    if (message.isFlagSet(RoutingFlag.FromRemote)
            && !message.isFlagSet(RoutingFlag.FromPeer)
            && !(BuiltInServices.ServerBus.name().equals(message.getSubject())
                    && BusCommand.Associate.name().equals(message.getCommandType()))) {
      // throws an exception if no queue is found
      getQueueByMessage(message);
    }
  }

  private void delayOrFail(final Message message, final Runnable deliveryTaskRunnable) {
    if (message.isFlagSet(RoutingFlag.RetryDelivery)
        && message.getResource(Integer.class, Resources.RetryAttempts.name()) > 3) {
      final NoSubscribersToDeliverTo ntdt = new NoSubscribersToDeliverTo(message.getSubject());
      if (message.getErrorCallback() != null) {
        message.getErrorCallback().error(message, ntdt);
      }
      throw ntdt;
    }
    message.setFlag(RoutingFlag.RetryDelivery);
    if (!message.hasResource(Resources.RetryAttempts.name())) {
      message.setResource(Resources.RetryAttempts.name(), 0);
    }
    message.setResource(Resources.RetryAttempts.name(),
        message.getResource(Integer.class, Resources.RetryAttempts.name()) + 1);
    getScheduler().schedule(new Runnable() {
      @Override
      public void run() {
        deliveryTaskRunnable.run();
      }
    }, 250, TimeUnit.MILLISECONDS);
  }

  /**
   * Sends the <tt>message</tt>
   *
   * @param message
   *     - the message to send
   */
  @Override
  public void send(final Message message) {
    message.commit();
    if (message.hasResource(Resources.Session.name())) {
      message.setFlag(RoutingFlag.NonGlobalRouting);
      send(getQueueByMessage(message), message, true);
    }
    else if (message.hasPart(MessageParts.SessionID)) {
      message.setFlag(RoutingFlag.NonGlobalRouting);
      try {
        send(getQueueBySession(message.get(String.class, MessageParts.SessionID)), message, true);
      }
      catch (final QueueUnavailableException e) {
        forwardToCluster(message, new Runnable() {
          @Override
          public void run() {
            log.debug("Failed to deliver message to queue with id:" + message.get(String.class, MessageParts.SessionID), e);
          }
        });
      }
    }
    else {
      sendGlobal(message);
    }
  }

  /**
   * Parses the message appropriately and enqueues it for delivery
   *
   * @param message
   *     - the message to be sent
   * @param fireListeners
   *     - true if all listeners attached should be notified of delivery
   */
  @Override
  public void send(final Message message, final boolean fireListeners) {
    message.commit();
    if (!message.hasResource(Resources.Session.name())) {
      handleMessageDeliveryFailure(this, message,
          "cannot automatically route message. no session contained in message.", null, false);
    }

    final MessageQueue queue = getQueue(getSession(message));

    if (queue == null) {
      handleMessageDeliveryFailure(this, message,
          "cannot automatically route message. no session contained in message.", null, false);
    }

    send(message.hasPart(MessageParts.SessionID) ? getQueueBySession(message.get(String.class, MessageParts.SessionID)) :
        getQueueByMessage(message), message, fireListeners);
  }

  private void send(final MessageQueue queue, final Message message, final boolean fireListeners) {
    try {
      if (isMonitor()) {
        busMonitor.notifyOutgoingMessageToRemote(queue.getSession().getSessionId(), message);
      }

      enqueueForDelivery(queue, message);
    }
    catch (NoSubscribersToDeliverTo nstdt) {
      // catch this so we can get a full trace
      handleMessageDeliveryFailure(this, message, "No subscribers to deliver to", nstdt, false);
    }
  }

  private final Random random = new Random(System.nanoTime());

  private void forwardToCluster(final Message message, final Runnable timeoutCallback) {
    if (clustering && message.hasPart(MessageParts.SessionID)) {
      final String sessionId = message.get(String.class, MessageParts.SessionID);
      if (!sessionLookup.containsKey(sessionId) && !BusTools.isReservedName(message.getSubject())) {
        final byte[] hashBytes = new byte[16];
        random.nextBytes(hashBytes);
        final String messageId = message.getSubject() + SecureHashUtil.hashToHexString(hashBytes);

        deadLetter.put(messageId, new ClusterWaitEntry(System.currentTimeMillis(), message, timeoutCallback));

        clusteringProvider.clusterTransmit(sessionId, message.getSubject(), messageId);
        message.setFlag(RoutingFlag.ClusterWait);
      }
    }
    else if (timeoutCallback != null) {
      timeoutCallback.run();
    }
  }

  private void enqueueForDelivery(final MessageQueue queue, final Message message) {
    try {
      if (queue != null && isAnyoneListening(queue, message.getSubject())) {
        queue.offer(message);
      }
      else {
        if (queue != null && !queue.isInitialized()) {
          deferDelivery(queue, message);
        }
        else {
          delayOrFail(message, new Runnable() {
            @Override
            public void run() {
              enqueueForDelivery(queue, message);
            }
          });
        }
      }
    }
    catch (QueueUnavailableException e) {
      closeQueue(queue);
    }
    catch (IOException e) {
      throw new RuntimeException("failed to enqueue message for delivery", e);
    }
  }

  @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter"})
  private void deferDelivery(final MessageQueue queue, final Message message) {
    synchronized (queue) {
      if (!deferredQueue.containsKey(queue)) deferredQueue.put(queue, new ArrayList<Message>());
      deferredQueue.get(queue).add(message);
    }
  }

  @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter"})
  private void drainDeferredDeliveryQueue(final MessageQueue queue) {
    try {
      synchronized (queue) {
        if (deferredQueue.containsKey(queue)) {
          final List<Message> deferredMessages = deferredQueue.get(queue);
          final Iterator<Message> dmIter = deferredMessages.iterator();

          Message m;
          while (dmIter.hasNext()) {
            if ((m = dmIter.next()).hasPart(MessageParts.PriorityProcessing.toString())) {
              queue.offer(m);
              dmIter.remove();
            }
          }

          for (final Message message : deferredQueue.get(queue)) {
            queue.offer(message);
          }

          deferredQueue.remove(queue);
        }
      }
    }
    catch (IOException e) {
      throw new RuntimeException("error draining deferred delivery queue", e);
    }
  }

  /**
   * Gets the queue corresponding to the session id given
   *
   * @param session
   *     - the session id of the queue
   *
   * @return the message queue
   */
  @Override
  public MessageQueue getQueue(final QueueSession session) {
    return messageQueues.get(session);
  }

  /**
   * Closes the queue with <tt>sessionId</tt>
   *
   * @param sessionId
   *     - the session context of the queue to close
   */
  @Override
  public void closeQueue(final String sessionId) {
    closeQueue(getQueueBySession(sessionId));
  }

  /**
   * Closes the message queue
   *
   * @param queue
   *     - the message queue to close
   */
  @Override
  public void closeQueue(final MessageQueue queue) {
    messageQueues.values().remove(queue);
    sessionLookup.values().remove(queue.getSession());

    for (Iterator<RemoteMessageCallback> iterator = remoteSubscriptions.values().iterator(); iterator.hasNext(); ) {
      final RemoteMessageCallback cb = iterator.next();
      cb.removeQueue(queue);
      if (cb.getQueueCount() == 0) {
        iterator.remove();
      }
    }

    fireQueueCloseListeners(new QueueCloseEvent(queue));
  }

  /**
   * Adds a subscription
   *
   * @param subject
   *     - the subject to subscribe to
   * @param receiver
   *     - the callback function called when a message is dispatched
   */
  @Override
  public Subscription subscribe(final String subject, final MessageCallback receiver) {
    if (reservedNames.contains(subject))
      throw new IllegalArgumentException("cannot modify or subscribe to reserved service: " + subject);

    final DeliveryPlan plan = createOrAddDeliveryPlan(subject, receiver);

    globalSubscriptions.add(subject);

    fireSubscribeListeners(new SubscriptionEvent(false, null, plan.getTotalReceivers(), true, subject));

    return new SubscriptionHandle(subject, receiver, plan);
  }

  @Override
  public Subscription subscribeLocal(final String subject, final MessageCallback receiver) {
    if (reservedNames.contains(subject))
      throw new IllegalArgumentException("cannot modify or subscribe to reserved service: " + subject);

    final String toSubscribe = "local:".concat(subject);

    final DeliveryPlan plan = createOrAddDeliveryPlan(toSubscribe, receiver);

    fireSubscribeListeners(
        new SubscriptionEvent(false, false, true, true, plan.getTotalReceivers(), "InBus", toSubscribe)
    );

    return new LocalSubscriptionHandle(toSubscribe, receiver);
  }

  private DeliveryPlan createOrAddDeliveryPlan(final String subject, final MessageCallback receiver) {
    if (receiver == null) {
      throw new NullPointerException("message callback cannot but null");
    }

    DeliveryPlan plan = subscriptions.get(subject);

    if (plan == null) {
      subscriptions.put(subject, plan = DeliveryPlan.newDeliveryPlan(receiver));
    }
    else {
      subscriptions.put(subject, plan.newDeliveryPlanWith(receiver));
    }

    return plan;
  }

  private DeliveryPlan removeFromDeliveryPlan(final String subject, final MessageCallback receiver) {
    final DeliveryPlan plan = subscriptions.get(subject);

    if (plan != null) {
      subscriptions.put(subject, plan.newDeliveryPlanWithOut(receiver));
      fireUnsubscribeListeners(
          new SubscriptionEvent(false, "InBus", plan.getTotalReceivers(), false, subject));
    }

    return plan;
  }

  private static final Set<String> broadcastExclusionSet = new HashSet<String>() {
    {
      add(BuiltInServices.ClientBus.name());
      add(BuiltInServices.ClientBusErrors.name());
    }
  };

  /**
   * Adds a new remote subscription and fires subscription listeners
   *
   * @param sessionContext
   *     - session context of queue
   * @param queue
   *     - the message queue
   * @param subject
   *     - the subject to subscribe to
   */
  public void remoteSubscribe(final QueueSession sessionContext, final MessageQueue queue, final String subject) {
    if (subject == null) return;

    boolean isNew = false;

    RemoteMessageCallback rmc;
    synchronized (remoteSubscriptions) {
      rmc = remoteSubscriptions.get(subject);
      if (rmc == null) {
        rmc = new RemoteMessageCallback(!broadcastExclusionSet.contains(subject), subject);
        rmc.addQueue(queue);

        isNew = true;

        remoteSubscriptions.put(subject, rmc);
        createOrAddDeliveryPlan(subject, rmc);
      }
      else if (!rmc.contains(queue)) {
        rmc.addQueue(queue);
      }
    }

    fireSubscribeListeners(
        new SubscriptionEvent(true, sessionContext.getSessionId(), rmc.getQueueCount(), isNew, subject)
    );
  }

  public class RemoteMessageCallback implements MessageCallback {

    private final String svc;
    private final Set<MessageQueue> queues = Collections.newSetFromMap(new ConcurrentHashMap<MessageQueue, Boolean>());
    private final boolean broadcastable;

    private final AtomicInteger totalBroadcasted = new AtomicInteger();

    public RemoteMessageCallback(final boolean broadcastable, final String svc) {
      this.broadcastable = broadcastable;
      this.svc = svc;
    }

    @Override
    public void callback(final Message message) {
      // do not pipeline if this message is addressed to a specified session.
      if (broadcastable && !message.isFlagSet(RoutingFlag.NonGlobalRouting) && queues.size() == messageQueues.size()) {
        // all queues are listening to this subject. therefore we can save memory and time by
        // writing to the broadcast color on the buffer

        try {
          if (queues.isEmpty()) return;

          BufferHelper.encodeAndWrite(transmissionbuffer, BufferColor.getAllBuffersColor(), message);

          for (final MessageQueue q : queues) {
            q.wake();
          }

          if (log.isDebugEnabled() && totalBroadcasted.incrementAndGet() % 1000 == 0) {
            log.debug(totalBroadcasted.get() + " messages have been broadcasted to service: " + svc);
          }
        }
        catch (IOException e) {
          throw new RuntimeException("transmission error", e);
        }
      }
      else {
        for (final MessageQueue q : queues) {
          send(q, message, true);
        }
      }

      if (clustering &&
          !message.isFlagSet(RoutingFlag.FromPeer)
          && !message.hasPart(MessageParts.SessionID)
          && !BusTools.isReservedName(message.getSubject())) {

        clusteringProvider.clusterTransmitGlobal(message);
      }
    }

    public void addQueue(final MessageQueue queue) {
      queues.add(queue);
    }

    public void removeQueue(final MessageQueue queue) {
      queues.remove(queue);
    }

    public Collection<MessageQueue> getQueues() {
      return queues;
    }

    public int getQueueCount() {
      return queues.size();
    }

    public boolean contains(final MessageQueue queue) {
      return queues.contains(queue);
    }
  }

  /**
   * Unsubscribes a remote subscription and fires the appropriate listeners
   *
   * @param sessionContext
   *     - session context of queue
   * @param queue
   *     - the message queue
   * @param subject
   *     - the subject to unsubscribe from
   */
  public void remoteUnsubscribe(final QueueSession sessionContext, final MessageQueue queue, final String subject) {
    if (!remoteSubscriptions.containsKey(subject)) {
      return;
    }

    final RemoteMessageCallback rmc = remoteSubscriptions.get(subject);
    rmc.removeQueue(queue);

    try {
      fireUnsubscribeListeners(new SubscriptionEvent(true, rmc.getQueueCount() == 0, false, false, rmc.getQueueCount(),
          sessionContext.getSessionId(), subject));
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Unsubscribe all subscriptions attached to <tt>subject</tt>
   *
   * @param subject
   *     - the subject to unsubscribe from
   */
  @Override
  public void unsubscribeAll(final String subject) {
    if (reservedNames.contains(subject))
      throw new IllegalArgumentException("Attempt to modify lockdown service: " + subject);

    subscriptions.remove(subject);
    globalSubscriptions.remove(subject);

    fireUnsubscribeListeners(new SubscriptionEvent(false, null, 0, false, subject));
  }

  /**
   * Checks if a subscription exists for <tt>subject</tt>
   *
   * @param subject
   *     - the subject to search the subscriptions for
   *
   * @return true if a subscription exists
   */
  @Override
  public boolean isSubscribed(final String subject) {
    return subscriptions.containsKey(subject);
  }

  private boolean isAnyoneListening(final MessageQueue queue, final String subject) {
    return (subject.endsWith(":RespondTo:RPC") || subject.endsWith(":Errors:RPC")
        || subscriptions.containsKey(subject) || (remoteSubscriptions.containsKey(subject)
        && remoteSubscriptions.get(subject).contains(queue)));
  }

  @Override
  public boolean hasRemoteSubscriptions(final String subject) {
    return remoteSubscriptions.containsKey(subject);
  }

  @Override
  public boolean hasRemoteSubscription(final String sessionId, final String subject) {
    return remoteSubscriptions.containsKey(subject) && remoteSubscriptions.get(subject)
        .contains(getQueueBySession(sessionId));
  }

  private void fireSubscribeListeners(final SubscriptionEvent event) {
    if (isMonitor()) {
      busMonitor.notifyNewSubscriptionEvent(event);
    }

    synchronized (subscribeListeners) {
      event.setDisposeListener(false);

      for (Iterator<SubscribeListener> iter = subscribeListeners.iterator(); iter.hasNext(); ) {
        iter.next().onSubscribe(event);
        if (event.isDisposeListener()) {
          iter.remove();
          event.setDisposeListener(false);
        }
      }
    }
  }

  private void fireUnsubscribeListeners(final SubscriptionEvent event) {
    if (isMonitor()) {
      busMonitor.notifyUnSubcriptionEvent(event);
    }

    synchronized (unsubscribeListeners) {
      event.setDisposeListener(false);

      for (Iterator<UnsubscribeListener> iter = unsubscribeListeners.iterator(); iter.hasNext(); ) {
        iter.next().onUnsubscribe(event);
        if (event.isDisposeListener()) {
          iter.remove();
          event.setDisposeListener(false);
        }
      }
    }
  }

  private void fireQueueCloseListeners(final QueueCloseEvent event) {
    if (isMonitor()) {
      busMonitor.notifyQueueDetached(event.getQueue().getSession().getSessionId(), event.getQueue());
    }

    synchronized (queueClosedListeners) {
      event.setDisposeListener(false);

      for (Iterator<QueueClosedListener> iter = queueClosedListeners.iterator(); iter.hasNext(); ) {
        iter.next().onQueueClosed(event);
        if (event.isDisposeListener()) {
          iter.remove();
          event.setDisposeListener(false);
        }
      }
    }
  }

  /**
   * Adds subscription listener
   *
   * @param listener
   *     - subscription listener to add
   */
  @Override
  public void addSubscribeListener(final SubscribeListener listener) {
    synchronized (subscribeListeners) {
      subscribeListeners.add(listener);
    }
  }

  /**
   * Adds unsubscription listener
   *
   * @param listener
   *     - adds an unsubscription listener
   */
  @Override
  public void addUnsubscribeListener(final UnsubscribeListener listener) {
    synchronized (unsubscribeListeners) {
      unsubscribeListeners.add(listener);
    }
  }

  private static QueueSession getSession(final Message message) {
    return message.getResource(QueueSession.class, Resources.Session.name());
  }

  private MessageQueue getQueueByMessage(final Message message) {
    final MessageQueue queue = getQueue(getSession(message));
    if (queue == null) {
      throw new QueueUnavailableException("no queue available to send. (queue or session may have expired): " +
          "(session id: " + getSession(message).getSessionId() + ")");
    }
    else {
      return queue;
    }
  }

  @Override
  public void associateNewQueue(final QueueSession oldSession, final QueueSession newSession) {
    sessionLookup.put(newSession.getSessionId(), oldSession);
    messageQueues.put(newSession, getQueue(oldSession));
  }

  @Override
  public MessageQueue getQueueBySession(final String sessionId) {
    final QueueSession session = sessionLookup.get(sessionId);
    if (session == null) {
      throw new QueueUnavailableException("no queue for sessionId=" + sessionId);
    }
    return getQueue(session);
  }

  @Override
  public QueueSession getSessionBySessionId(final String id) {
    return sessionLookup.get(id);
  }

  /**
   * Gets all the message queues
   *
   * @return a map of the message queues that exist
   */
  @Override
  public Map<QueueSession, MessageQueue> getMessageQueues() {
    return messageQueues;
  }

  /**
   * Gets the scheduler being used within this message bus for housekeeping and
   * other periodic or deferred tasks.
   *
   * @return the scheduler
   */
  @Override
  public ScheduledExecutorService getScheduler() {
    return scheduler;
  }

  @Override
  public void addQueueClosedListener(final QueueClosedListener listener) {
    synchronized (queueClosedListeners) {
      queueClosedListeners.add(listener);
    }
  }

  @Override
  public Collection<MessageCallback> getReceivers(final String subject) {
    final DeliveryPlan deliveryPlan = subscriptions.get(subject);
    return (deliveryPlan != null ? Collections.unmodifiableCollection(deliveryPlan.getDeliverTo()) : Collections.emptyList());
  }

  private boolean isMonitor() {
    return this.busMonitor != null;
  }

  @Override
  public void attachMonitor(final BusMonitor monitor) {
    if (this.busMonitor != null) {
      log.warn("new monitor attached, but a monitor was already attached: old monitor has been detached.");
    }
    this.busMonitor = monitor;

    for (final Map.Entry<QueueSession, MessageQueue> entry : messageQueues.entrySet()) {
      busMonitor.notifyQueueAttached(entry.getKey().getSessionId(), entry.getValue());
    }

    for (final String subject : subscriptions.keySet()) {
      busMonitor.notifyNewSubscriptionEvent(new SubscriptionEvent(false, "None", 1, false, subject));
    }
    for (final Map.Entry<String, RemoteMessageCallback> entry : remoteSubscriptions.entrySet()) {
      for (final MessageQueue queue : entry.getValue().getQueues()) {
        busMonitor.notifyNewSubscriptionEvent(
            new SubscriptionEvent(true, queue.getSession().getSessionId(), 1, false, entry.getKey())
        );
      }
    }

    monitor.attach(this);
  }

  @Override
  public Message getDeadLetterMessage(final String messageId) {
    final ClusterWaitEntry entry = deadLetter.get(messageId);
    if (entry != null) {
      return entry.getMessage();
    }
    return null;
  }

  @Override
  public boolean removeDeadLetterMessage(final String messageId) {
    return deadLetter.remove(messageId) != null;
  }

  @Override
  public void stop() {
    for (final MessageQueue queue : messageQueues.values()) {
      queue.stopQueue();
    }

    scheduler.shutdown();

    transmissionbuffer.clear();
    subscriptions.clear();
    remoteSubscriptions.clear();
    deferredQueue.clear();
    sessionLookup.clear();
  }

  public void finishInit() {
    reservedNames.addAll(subscriptions.keySet());
  }

  private class ServerBusMessageCallback implements MessageCallback {

    private final String webSocketPath;
    private final int webSocketPort;
    public ServerBusMessageCallback(String webSocketPath, int webSocketPort) {
      this.webSocketPath = webSocketPath;
      this.webSocketPort = webSocketPort;
    }

    @Override
    @SuppressWarnings({"unchecked"})
    public void callback(final Message message) {
      try {
        final QueueSession session = getSession(message);
        MessageQueueImpl queue = (MessageQueueImpl) messageQueues.get(session);

        switch (BusCommand.valueOf(message.getCommandType())) {
          case Heartbeat:
            if (queue != null) {
              queue.heartBeat();
            }
            break;

          case RemoteSubscribe:
            if (queue == null) return;

            if (message.hasPart(MessageParts.SubjectsList)) {
              for (final String subject : (List<String>) message.get(List.class, MessageParts.SubjectsList)) {
                remoteSubscribe(session, queue, subject);
              }
            }
            else if (message.hasPart(MessageParts.RemoteServices)) {
              for (final String subject : message.get(String.class, MessageParts.RemoteServices).split(",")) {
                remoteSubscribe(session, queue, subject);
              }
            }
            else {
              remoteSubscribe(session, messageQueues.get(session),
                  message.get(String.class, MessageParts.Subject));
            }

            break;

          case RemoteUnsubscribe:
            if (queue == null) return;

            remoteUnsubscribe(session, queue,
                message.get(String.class, MessageParts.Subject));
            break;

          case Disconnect:
            if (queue == null) return;

            synchronized (messageQueues) {
              queue.stopQueue();
              closeQueue(queue);
              session.endSession();
            }
            break;

          case Resend:
            if (queue == null) return;

          case Associate: {
            List<Message> deferred = null;
            synchronized (messageQueues) {
              if (messageQueues.containsKey(session)) {
                final MessageQueue q = messageQueues.get(session);
                synchronized (q) {
                  if (deferredQueue.containsKey(q)) {
                    deferred = deferredQueue.remove(q);
                  }
                }

                messageQueues.get(session).stopQueue();
              }

              queue = new MessageQueueImpl(transmissionbuffer, session, messageQueueTimeoutSecs);

              addQueue(session, queue);

              if (deferred != null) {
                deferredQueue.put(queue, deferred);
              }

              remoteSubscribe(session, queue, BuiltInServices.ClientBus.name());
            }

            for (final String svc : message.get(String.class, MessageParts.RemoteServices).split(",")) {
              remoteSubscribe(session, queue, svc);
            }

            if (isMonitor()) {
              busMonitor.notifyQueueAttached(session.getSessionId(), queue);
            }

            final Message msg = ConversationMessage.create(message)
                .toSubject(BuiltInServices.ClientBus.name())
                .command(BusCommand.FinishAssociation);

            final StringBuilder subjects = new StringBuilder();
            for (final String s : new HashSet<String>(globalSubscriptions)) {
              if (subjects.length() != 0) {
                subjects.append(',');
              }
              subjects.append(s);
            }

            msg.set(RemoteServices, subjects.toString());

            final StringBuilder capabilitiesBuffer = new StringBuilder(25);

            if (doLongPolling) {
              capabilitiesBuffer.append(Capabilities.LongPolling.name());
            }
            else {
              capabilitiesBuffer.append(Capabilities.ShortPolling.name());
              msg.set(MessageParts.PollFrequency, hostedModeTesting ? 50 : 250);
            }

            if (webSocketServer || webSocketServlet) {
              capabilitiesBuffer.append(',').append(Capabilities.WebSockets.name());
              final String webSocketURL;
              final HttpServletRequest request = message.getResource(HttpServletRequest.class,
                      HttpServletRequest.class.getName());

              String websocketScheme = "ws";
              if (request.getScheme().equals("https") || useSecureWebsocket || "https".equalsIgnoreCase(request.getHeader("X-Forwarded-Proto"))) {
                websocketScheme = "wss";
                log.debug("use secure websocket");
              }

              if (webSocketServlet) {
                webSocketURL = websocketScheme + "://" + request.getHeader("Host") + webSocketPath;
              }
              else {
                webSocketURL = websocketScheme + "://" + request.getServerName() + ":" + webSocketPort + webSocketPath;
              }
              msg.set(MessageParts.WebSocketURL, webSocketURL);
              msg.set(MessageParts.WebSocketToken, WebSocketTokenManager.getNewOneTimeToken(session));
            }

            if (sseEnabled && !session.hasAttribute("NoSSE")) {
              capabilitiesBuffer.append(",").append(Capabilities.SSE.name());
            }

            msg.set(MessageParts.CapabilitiesFlags, capabilitiesBuffer.toString());

            msg.set(ConnectionSessionKey, queue.getSession().getSessionId());
            send(msg, false);

            queue.finishInit();
            drainDeferredDeliveryQueue(queue);
            break;
          }

          case WebsocketChannelVerify:
            if (message.hasPart(MessageParts.WebSocketToken)) {
              if (verifyOneTimeToken(session, message.get(String.class, MessageParts.WebSocketToken))) {
                final String reconnectionToken = getNewOneTimeToken(session);

                final LocalContext localContext = LocalContext.get(session);

                localContext.setAttribute(WebSocketServerHandler.SESSION_ATTR_WS_STATUS,
                    WebSocketServerHandler.WEBSOCKET_ACTIVE);

                createConversation(message)
                    .toSubject(BuiltInServices.ClientBus.name())
                    .command(BusCommand.WebsocketChannelOpen)
                    .with(MessageParts.WebSocketToken, reconnectionToken)
                    .done().sendNowWith(ServerMessageBusImpl.this, false);
              }
              else {
              }
            }
            break;
        }
      }
      catch (Throwable t) {
        t.printStackTrace();
      }
    }
  }

  private BufferStatus bufferStatus() {
    final int headBytes = transmissionbuffer.getHeadPositionBytes();
    final int bufSize = transmissionbuffer.getBufferSize();

    long lowTail = -1;
    long highTail = -1;
    int activeTails = 0;

    final int free;
    long lowSegBytes = 0;
    long highSegBytes = 0;


    for (final MessageQueue q : messageQueues.values()) {
      activeTails++;
      final long seq = q.getCurrentBufferSequenceNumber();
      if (lowTail == -1) {
        lowTail = highTail = seq;
      }
      else {
        if (seq > highTail) highTail = seq;
        if (seq < lowTail) lowTail = seq;
      }
    }

    if (activeTails > 0) {
      lowSegBytes = (lowTail % transmissionbuffer.getBufferSize()) * transmissionbuffer.getSegmentSize();
      highSegBytes = (highTail % transmissionbuffer.getBufferSize()) * transmissionbuffer.getSegmentSize();

      if (lowSegBytes < headBytes) {
        free = (int) ((bufSize - headBytes) + lowSegBytes);
      }
      else if (lowSegBytes > headBytes) {
        free = (int) (lowSegBytes - bufSize);
      }
      else {
        free = bufSize;
      }
    }
    else {
      free = bufSize;
    }

    return new BufferStatus(free, (int) (highSegBytes - lowSegBytes), activeTails, ((float) free) / bufSize);
  }

  private class DefaultSubscribeListener implements SubscribeListener {
    @Override
    public void onSubscribe(final SubscriptionEvent event) {
      if (event.isLocalOnly() || event.isRemote() || event.getSubject().startsWith("local:")) return;

      MessageBuilder.createMessage()
          .toSubject(BuiltInServices.ClientBus.name())
          .command(BusCommand.RemoteSubscribe)
          .with(MessageParts.Subject, event.getSubject())
          .noErrorHandling().sendGlobalWith(ServerMessageBusImpl.this);

    }
  }

  private class DefaultUnsubscribeListener implements UnsubscribeListener {
    @Override
    public void onUnsubscribe(final SubscriptionEvent event) {
      if (event.isLocalOnly() || event.isRemote() || event.getSubject().startsWith("local:")) return;
      if (messageQueues.isEmpty()) return;

      MessageBuilder.createMessage()
          .toSubject(BuiltInServices.ClientBus.name())
          .command(BusCommand.RemoteUnsubscribe)
          .with(MessageParts.Subject, event.getSubject())
          .noErrorHandling().sendGlobalWith(ServerMessageBusImpl.this);
    }
  }

  private class HousekeeeperRunnable implements Runnable {
    int runCount = 0;
    boolean lastWasEmpty = false;

    @Override
    public void run() {
      runCount++;
      boolean houseKeepingPerformed = false;
      final List<MessageQueue> endSessions = new LinkedList<MessageQueue>();

      int paged = 0, killed = 0;

      while (!houseKeepingPerformed) {
        try {
          final Iterator<MessageQueue> iter = ServerMessageBusImpl.this.messageQueues.values().iterator();
          MessageQueue q;
          while (iter.hasNext()) {
            if ((q = iter.next()).isStale()) {
              iter.remove();
              endSessions.add(q);
              killed++;
            }

            if (PageUtil.pageIfStraddling(q)) {
              paged++;
            }
          }

          houseKeepingPerformed = true;
        }
        catch (ConcurrentModificationException cme) {
          // fall-through and try again.
        }
      }

      if (paged > 0 || killed > 0) {
        log.debug("[bus] killed " + killed + " sessions and paged out " + paged + " queues");
      }

      for (final MessageQueue ref : endSessions) {
        for (final String subject : new HashSet<String>(ServerMessageBusImpl.this.remoteSubscriptions.keySet())) {
          ServerMessageBusImpl.this.remoteUnsubscribe(ref.getSession(), ref, subject);
        }

        ServerMessageBusImpl.this.closeQueue(ref);
        ref.getSession().endSession();
        deferredQueue.remove(ref);
        ref.discard();
      }

      final Iterator<ClusterWaitEntry> entryIterator = deadLetter.values().iterator();

      while (entryIterator.hasNext()) {
        final ClusterWaitEntry entry = entryIterator.next();
        if (entry.isStale()) {
          entryIterator.remove();
          try {
            entry.notifyTimeout();
          }
          catch (Exception e) {
            log.warn("exception occurred expunging from dead letter queue", e);
          }
        }
      }

      final BufferStatus stat = bufferStatus();
      if (stat.getFree() == 1.0f) {
        if (lastWasEmpty) {
          return;
        }
        else {
          lastWasEmpty = true;
        }
      }
      else {
        lastWasEmpty = false;
      }

      log.debug("[bus] buffer status [freebytes: " + stat.getFreeBytes()
          + " (" + (stat.getFree() * 100) + "%) tail rng: " + stat.getTailRange() + "; actv tails: "
          + stat.getActiveTails() + "]");

      if (stat.getFree() < 0.50f) {
        log.debug("[bus] high load condition detected!");
      }
    }

    @Override
    public String toString() {
      return "Bus Housekeeper";
    }
  }

  private class LocalSubscriptionHandle implements Subscription {
    private final String toSubscribe;
    private final MessageCallback receiver;

    public LocalSubscriptionHandle(String toSubscribe, MessageCallback receiver) {
      this.toSubscribe = toSubscribe;
      this.receiver = receiver;
    }

    @Override
    public void remove() {
      removeFromDeliveryPlan(toSubscribe, receiver);
    }
  }

  private class SubscriptionHandle implements Subscription {
    private final String subject;
    private final MessageCallback receiver;
    private final DeliveryPlan plan;

    public SubscriptionHandle(String subject, MessageCallback receiver, DeliveryPlan plan) {
      this.subject = subject;
      this.receiver = receiver;
      this.plan = plan;
    }

    @Override
    public void remove() {
      if (removeFromDeliveryPlan(subject, receiver).getTotalReceivers() == 0) {
        globalSubscriptions.remove(subject);
        subscriptions.remove(subject);
      }
      else {
        boolean nonRemote = true;
        for (final MessageCallback callback : plan.getDeliverTo()) {
          if (!(callback instanceof RemoteMessageCallback)) {
            nonRemote = false;
            break;
          }
        }
        if (nonRemote) {
          globalSubscriptions.remove(subject);
          subscriptions.remove(subject);
        }
      }
    }
  }
}
