/*
 * Copyright (C) 2015 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 java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.jboss.errai.bus.client.api.QueueSession;
import org.jboss.errai.bus.client.api.laundry.Laundry;
import org.jboss.errai.bus.client.api.laundry.LaundryList;
import org.jboss.errai.bus.client.api.laundry.LaundryListProviderFactory;

/**
 * <a href="http://www.youtube.com/watch?v=qBXn9PptgN8">Architectural Overview</a>
 *
 * @author Mike Brock
 */
public class EventRoutingTable {
  private static final String CDI_EVENT_ROUTES_ACTIVE = "cdi.event.routesActive";

  // type to (set<annotations> to set<session ids>)
  private final Map<String, Map<Set<String>, Set<String>>> activeRoutes
      = new ConcurrentHashMap<String, Map<Set<String>, Set<String>>>();

  private final Object routeChangeLock = new Object();
  private final Object sessionChangeLock = new Object();

  public void activateRoute(final String eventType,
                            final Set<String> annotations,
                            final QueueSession queueSession) {


    Map<Set<String>, Set<String>> route = activeRoutes.get(eventType);
    if (route == null) {
      synchronized (routeChangeLock) {
        route = activeRoutes.get(eventType);
        if (route == null) {
          activeRoutes.put(eventType, route = new ConcurrentHashMap<Set<String>, Set<String>>());
        }
      }
    }

    Set<String> sessions = route.get(annotations);

    if (sessions == null) {
      synchronized (sessionChangeLock) {
        sessions = route.get(annotations);
        if (sessions == null) {
          route.put(annotations, sessions =
              Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()));
        }
      }
    }

    sessions.add(queueSession.getSessionId());

    updateLaundry(queueSession);
  }

  public void deactivateRoute(final String eventType,
                              final Set<String> annotations,
                              final QueueSession queueSession) {

    final Map<Set<String>, Set<String>> route = activeRoutes.get(eventType);
    if (route == null) {
      return;
    }

    final Set<String> sessions = route.get(annotations);
    if (sessions == null) {
      for (final Map.Entry<Set<String>, Set<String>> entry : activeRoutes.get(eventType).entrySet()) {
        if (annotations.containsAll(entry.getKey())) {
          entry.getValue().remove(queueSession.getSessionId());
        }
      }
    }
    else {
      sessions.remove(queueSession.getSessionId());
    }
  }

  public boolean isRouteActive(final String eventType,
                               final Set<String> annotations,
                               final QueueSession queueSession) {
    final Map<Set<String>, Set<String>> route = activeRoutes.get(eventType);
    if (route == null) {
      return false;
    }
    final Set<String> sessions = route.get(annotations);
    final boolean active = sessions != null && sessions.contains(queueSession.getSessionId());
    return active;
  }

  public Collection<String> getQueueIdsForRoute(final String eventType,
                                                final Set<String> annotations) {

    final Map<Set<String>, Set<String>> route = activeRoutes.get(eventType);
    if (route == null) {
      return Collections.emptySet();
    }

    final Set<String> sessions = route.get(annotations);
    if (sessions == null) {
      final Set<String> ids = new HashSet<String>();
      for (final Map.Entry<Set<String>, Set<String>> entry : activeRoutes.get(eventType).entrySet()) {
        if (annotations.containsAll(entry.getKey())) {
          ids.addAll(entry.getValue());
        }
      }

      synchronized (sessionChangeLock) {
        final Map<Set<String>, Set<String>> routeMap = new ConcurrentHashMap<Set<String>, Set<String>>();
        routeMap.put(annotations, ids);
        activeRoutes.put(eventType, routeMap);
      }

      return ids;
    }

    return sessions;
  }

  private void removeAllForId(final String id) {
    synchronized (sessionChangeLock) {
      for (final Map<Set<String>, Set<String>> routeMaps : activeRoutes.values()) {
        for (final Map.Entry<Set<String>, Set<String>> entry : routeMaps.entrySet()) {
          entry.getValue().remove(id);
        }
      }
    }
  }

  private void updateLaundry(final QueueSession queueSession) {
    Boolean routesActive = queueSession.getAttribute(Boolean.class, CDI_EVENT_ROUTES_ACTIVE);
    if (routesActive == null) {
      synchronized (queueSession) {
        routesActive = queueSession.getAttribute(Boolean.class, CDI_EVENT_ROUTES_ACTIVE);
        if (routesActive == null) {
          final LaundryList laundryList = LaundryListProviderFactory.get().getLaundryList(queueSession);
          laundryList.add(new Laundry() {
            @Override
            public void clean() throws Exception {
              removeAllForId(queueSession.getSessionId());
            }
          });
        }
      }
    }
  }
}
