/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2000-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * @(#)Topic.java	1.57 11/16/07
 */ 

package com.sun.messaging.jmq.jmsserver.core;

import java.util.*;
import java.io.*;
import com.sun.messaging.jmq.jmsserver.util.FeatureUnavailableException;
import com.sun.messaging.jmq.jmsserver.util.BrokerException;
import com.sun.messaging.jmq.jmsserver.Globals;
import com.sun.messaging.jmq.util.lists.*;
import com.sun.messaging.jmq.jmsserver.util.lists.*;
import com.sun.messaging.jmq.io.SysMessageID;
import com.sun.messaging.jmq.util.log.*;
import com.sun.messaging.jmq.util.selector.*;
import com.sun.messaging.jmq.jmsserver.service.ConnectionUID;
import com.sun.messaging.jmq.jmsserver.service.Connection;
import com.sun.messaging.jmq.io.DestMetricsCounters;
import com.sun.messaging.jmq.jmsserver.core.cluster.*;


/**
 * This class represents a topic destination
 */
public class Topic extends Destination
{

    static final long serialVersionUID = -5748515630523651753L;

    private static boolean DEBUG = false;


    private transient Map selectorToInterest;
    private transient List selectors;

    private transient Map remoteConsumers;
    private static int TOPIC_DEFAULT_PREFETCH = Globals.getConfig().
                  getIntProperty(Globals.IMQ+
                 ".autocreate.topic.consumerFlowLimit", 1000);

    private boolean hasNoLocalConsumers = false;

    int maxSharedConsumers = 0;
    int sharedPrefetch = 0;

    public static final String MAX_SHARE_CONSUMERS = "max_shared_consumers";
    public static final String SHARED_PREFETCH = "sharedPrefetch";


    public static final int AUTO_MAX_SHARED_CONSUMER_LIMIT =
            Globals.getConfig().getIntProperty(
                Globals.IMQ + ".autocreate.topic.maxNumSharedConsumers",
                -1);

    public static final int AUTO_MAX_SHARED_FLOW_LIMIT =
            Globals.getConfig().getIntProperty(
                Globals.IMQ + ".autocreate.topic.sharedConsumerFlowLimit",
                5);
    public static final int ADMIN_MAX_SHARED_CONSUMER_LIMIT =
            Globals.getConfig().getIntProperty(
                Globals.IMQ + ".admincreate.topic.maxNumSharedConsumers",
                -1);

    public static final int ADMIN_MAX_SHARED_FLOW_LIMIT =
            Globals.getConfig().getIntProperty(
                Globals.IMQ + ".admincreate.topic.sharedConsumerFlowLimit",
                5);

    public Hashtable getDebugState() {
        Hashtable ht = super.getDebugState();
        Hashtable sel = new Hashtable();
        synchronized(selectorToInterest) {
            Iterator itr = selectorToInterest.keySet().iterator();
            while (itr.hasNext()) {
               Selector selector = (Selector)itr.next();
               Set s = (Set)selectorToInterest.get(selector);
               Vector v = new Vector();
               synchronized (s) {
                   Iterator itr1 = s.iterator();
                   while (itr1.hasNext()) {
                       Consumer c =(Consumer)itr1.next();
                       v.add(String.valueOf(c.getConsumerUID().longValue()));
                   }
               }
               sel.put((selector == null ? "no selector" : selector.toString()), v);
            }
         }
         ht.put("selectorInfo", sel);
         ht.put(MAX_SHARE_CONSUMERS, new Integer(maxSharedConsumers));
         ht.put(SHARED_PREFETCH, new Integer(sharedPrefetch));
         return ht;
    }
   
    // used only as a space holder when deleting corrupted destinations 
    protected Topic(DestinationUID uid)
    {
        super(uid);
    }           
        
    protected Topic(String destination, int type, 
           boolean store, ConnectionUID id, boolean autocreate) 
        throws FeatureUnavailableException, BrokerException, IOException
    {
        super(destination,type,store, id,autocreate);

        maxPrefetch = TOPIC_DEFAULT_PREFETCH;

        if (autocreate) {
            maxSharedConsumers=AUTO_MAX_SHARED_CONSUMER_LIMIT;
            sharedPrefetch=AUTO_MAX_SHARED_FLOW_LIMIT;
        } else {
            maxSharedConsumers=ADMIN_MAX_SHARED_CONSUMER_LIMIT;
            sharedPrefetch=ADMIN_MAX_SHARED_FLOW_LIMIT;
        }
    }

    protected void initVar() {
       selectorToInterest = new HashMap();
       selectors = new ArrayList();
       remoteConsumers = new HashMap();
    }

    public void unload(boolean refs) {
        super.unload(refs);
        if (refs) {
            // remove the refs
            Iterator i = consumers.values().iterator();
            while (i.hasNext()) {
                Consumer con = (Consumer)i.next();
                con.unloadMessages();
            }
        }
    } 

    public int getUnackSize()
    {
        Set s = null;
        synchronized (destMessages) {
            s = new HashSet(destMessages.values());
        }
        int counter = 0;
        Iterator itr = s.iterator();
        while (itr.hasNext()) {
            PacketReference ref = (PacketReference)itr.next();
            if (!ref.isInvalid() && !ref.isDestroyed() &&
                 (ref.getDeliverCnt() -
                  ref.getCompleteCnt() > 0)) {
                counter ++;
            }
        }
        return counter;
    }


    public Set routeAndMoveMessage(PacketReference oldRef, 
                  PacketReference newRef)
	throws IOException, BrokerException
    {
        throw new RuntimeException("XXX not implemented");
    }


    public boolean queueMessage(PacketReference pkt, boolean trans) 
        throws BrokerException
    {
        // if there are no consumers, throw it away
        if (!trans && consumers.size() == 0) {
            return false;
        }
        return super.queueMessage(pkt, trans);
    }


    /**
     * handles transient data when class is deserialized
     */
    private void readObject(java.io.ObjectInputStream ois)
        throws IOException, ClassNotFoundException
    {
        initVar();
        ois.defaultReadObject();
    }


    public void eventOccured(EventType type,  Reason reason,
            Object source, Object OrigValue, Object NewValue, 
            Object userdata)
    {
        super.eventOccured(type,reason, source, 
              OrigValue, NewValue, userdata);
    }

    /**
     * @deprecated
     */
    public void routeNewMessage(SysMessageID  id) 
         throws BrokerException, SelectorFormatException
    {
        PacketReference ref = (PacketReference)destMessages.get(id);
        Set s = routeNewMessage(ref);
        forwardMessage(s, ref);
    }

    public int getConsumerCount() {
        int count =  super.getConsumerCount();
        // get 3.0 remote consumer count
        synchronized(remoteConsumers) {
            Iterator itr = remoteConsumers.values().iterator();
            while (itr.hasNext()) {
                RemoteConsumer rc = (RemoteConsumer)itr.next();
                count += rc.getConsumerCount();
           }
        }
        return count;
    }

    private Set matchRemoteConsumers(PacketReference msg, Set s)
         throws BrokerException, SelectorFormatException
    {
       
        synchronized(remoteConsumers) {
            if (remoteConsumers.isEmpty()) {
                return s;
            }
            Set newset = new HashSet(s);
            Iterator itr = remoteConsumers.values().iterator();
            while (itr.hasNext()) {
                RemoteConsumer rc = (RemoteConsumer)itr.next();
                if (rc.match(msg, newset)) {
                    s.add(rc);
                }
                
            }
            return newset;
        }
    }

    protected ConsumerUID[] routeLoadedTransactionMessage(
           PacketReference msg)
         throws BrokerException, SelectorFormatException
    {
        Set matching = new HashSet();

        Map props = null;
        Map headers = null;

        synchronized(selectorToInterest) {
            Iterator itr = selectorToInterest.keySet().iterator();
            while (itr.hasNext()) {
               Selector selector = (Selector)itr.next();
            
               if (selector != null) {
                   if (props == null && selector.usesProperties()) {
                       try {
                           props = msg.getProperties();
                       } catch (ClassNotFoundException ex) {
                           logger.logStack(Logger.ERROR,"INTERNAL ERROR", ex);
                           props = new HashMap();
                       }
                   }
                   if (headers == null && selector.usesFields()) {
                       headers = msg.getHeaders();
                   }
               }
                   
               boolean matches = (selector == null) ||
                   selector.match(props, headers);
               if (matches) {
                   if (DEBUG) {
                       logger.log(Logger.INFO,"Selector " + selector 
                          + " Matches " + msg.getSysMessageID());
                   }
                   Set s = (Set)selectorToInterest.get(selector);
                   if (s == null) continue;
                   synchronized (s) {
                       matching.addAll(s);
                   }
               }
            }
        }

        // deal w/ isLocal

        // OK .. the logic is a little different than previous releases
        //
        // If we are a subscription and have a ClientID ... compare clientID's
        // otherwise
        // Compare ConsumerUIDs

        HashSet hs = new HashSet();
        Iterator nlitr = matching.iterator();
        ConnectionUID pcuid = msg.getProducingConnectionUID();
        String clientid = msg.getClientID();
        while (nlitr.hasNext()) {
            Consumer c = (Consumer)nlitr.next();
            if (c.getNoLocal()) {
                // fix for 5025241 Durable subscriber with noLocal=true 
                //         receives self-published msgs
                if (c instanceof Subscription && clientid != null &&
                    ((Subscription)c).getClientID() != null &&
                    ((Subscription)c).getClientID().equals(clientid)) {
                    // check subscription clientID case
                    nlitr.remove();
                } else if (c.getConsumerUID().getConnectionUID() ==
                       pcuid) {
                    nlitr.remove();
                } else {
                    hs.add(c.getConsumerUID());
                }
            } else {
                hs.add(c.getConsumerUID());
            }
        }
        return (ConsumerUID[])hs.toArray(new ConsumerUID[0]);
    }
    
    
    public void unrouteLoadedTransactionAckMessage(PacketReference ref,
			ConsumerUID consumerId) throws BrokerException {
		logger.log(Logger.DEBUG,
				"unrouteLoadedTransactionAckMessage num consumers = "
						+ consumers.size());
		// do nothing as consumers not attatched yet?
		// need to check this
		// what happens when consumer attatches?
		Consumer consumer = getConsumer(consumerId);
		if (consumer == null) {
			logger.log(Logger.DEBUG, "could not find consumer for "
					+ consumerId);
		} else
			consumer.unrouteMessage(ref);
	}

    public Set routeNewMessage(PacketReference  msg) 
         throws BrokerException, SelectorFormatException
    {
        Set matching = new HashSet();

        Map props = null;
        Map headers = null;

        for (int i=0; i < selectors.size(); i ++) {
            Selector selector = null;
            try {
                //LKS-XXX NOTE: don't need selectors !!!
                selector = (Selector)selectors.get(i);
            } catch (Exception ex) {
                continue; // selector was removed
            }
            if (selector == null ) {
                Set s = (Set)selectorToInterest.get(selector);
                if (s == null) continue;
                synchronized (s) {
                    matching.addAll(s);
                }
            } else {
                if (props == null && selector.usesProperties()) {
                   try {
                       props = msg.getProperties();
                   } catch (ClassNotFoundException ex) {
                       logger.logStack(Logger.ERROR,"INTERNAL ERROR", ex);
                       props = new HashMap();
                   }
                }
                if (headers == null && selector.usesFields()) {
                    headers = msg.getHeaders();
                }
                if (selector.match(props, headers)) {
                       Set s = (Set)selectorToInterest.get(selector);
                       if (s != null) {
                           synchronized(s) {
                               matching.addAll(s);
                           }
                       }
                }
           }
        }

        // deal w/ isLocal
        if (hasNoLocalConsumers) {
            Iterator nlitr = matching.iterator();
            ConnectionUID pcuid = msg.getProducingConnectionUID();
            String clientid = msg.getClientID();
            while (nlitr.hasNext()) {
                Consumer c = (Consumer)nlitr.next();
                if (c.getNoLocal()) {
                    // fix for 5025241 Durable subscriber with noLocal=true 
                    //         receives self-published msgs
                    if (c instanceof Subscription && clientid != null &&
                        ((Subscription)c).getClientID() != null &&
                        ((Subscription)c).getClientID().equals(clientid)) {
                        // check subscription clientID case
                        nlitr.remove();
                    } else if (c.getConsumerUID().getConnectionUID() ==
                       pcuid) {
                        nlitr.remove();
                    }
                }
             }
        }
        if (matching.isEmpty()) {
            removeMessage(msg.getSysMessageID(), RemoveReason.ACKNOWLEDGED); 
            return null;
        } else {
            msg.store(matching);
        }

        return matching;
   }
    
    /**
     * @param msg
     * @param forStoreOnly specifies the routing info is for storage only, 
     * so only need to apply selectors for consumers that  
     * @return
     * @throws BrokerException
     * @throws SelectorFormatException
     */
    public Set routeMessage(PacketReference msg, boolean forStoreOnly) throws BrokerException,
			SelectorFormatException {
		Set matching = new HashSet();

		Map props = null;
		Map headers = null;

		for (int i = 0; i < selectors.size(); i++) {
			Selector selector = null;
			try {
				// LKS-XXX NOTE: don't need selectors !!!
				selector = (Selector) selectors.get(i);
			} catch (Exception ex) {
				continue; // selector was removed
			}
			if (selector == null) {
				Set s = (Set) selectorToInterest.get(selector);
				if (s == null)
					continue;
				synchronized (s) {
					matching.addAll(s);
				}
			} else {
				
				
				if (props == null && selector.usesProperties()) {
					try {
						props = msg.getProperties();
					} catch (ClassNotFoundException ex) {
						logger.logStack(Logger.ERROR, "INTERNAL ERROR", ex);
						props = new HashMap();
					}
				}
				if (headers == null && selector.usesFields()) {
					headers = msg.getHeaders();
				}
				Set s = (Set) selectorToInterest.get(selector);
				if(s==null)
					continue;
				
				if (forStoreOnly) {
					// optimisation...
					// with this option, no need to match selector if no
					// consumers need storing

					boolean needsStoring = false;
					synchronized (s) {
						Iterator iter = s.iterator();
						while (iter.hasNext()) {
							Consumer consumer = (Consumer) iter.next();
							if (consumer != null
									&& consumer.getStoredConsumerUID().shouldStore) {
								// at least one of the consumers needs storing,
								// so
								// we will need to check the selector
								needsStoring = true;
								break;
							}
						}
						if (!needsStoring) {
							continue;
						}
					}
				}
				
				if (selector.match(props, headers)) {					
					synchronized (s) {
						matching.addAll(s);
					}
					
				}
			}
		}

		// deal w/ isLocal
		if (hasNoLocalConsumers) {
			Iterator nlitr = matching.iterator();
			ConnectionUID pcuid = msg.getProducingConnectionUID();
			String clientid = msg.getClientID();
			while (nlitr.hasNext()) {
				Consumer c = (Consumer) nlitr.next();
				if (c.getNoLocal()) {
					// fix for 5025241 Durable subscriber with noLocal=true
					// receives self-published msgs
					if (c instanceof Subscription
							&& clientid != null
							&& ((Subscription) c).getClientID() != null
							&& ((Subscription) c).getClientID()
									.equals(clientid)) {
						// check subscription clientID case
						nlitr.remove();
					} else if (c.getConsumerUID().getConnectionUID() == pcuid) {
						nlitr.remove();
					}
				}
			}
		}		
		return matching;
	}
    
    public ConsumerUID[] calculateStoredInterests(PacketReference ref)
			throws BrokerException, SelectorFormatException {
		// this will return null if message is not persistent

		ConsumerUID[] storedInterests = null;
		try {

			Collection allConsumers = routeMessage(ref, true);
			storedInterests = ref.getRoutingForStore(allConsumers);

		} catch (BrokerException ex) {
			throw ex;
		} catch (RuntimeException ex) {
			throw new BrokerException(ex.toString(), ex);
		}
		return storedInterests;
	}
    
    public void forwardOrphanMessages(Collection refs,
                  ConsumerUID consumer)
         throws BrokerException
    {
        BrokerException ex = new BrokerException("INTERNAL ERROR: Unexpected call");
        logger.logStack(Logger.ERROR, ex.getMessage(), ex);
        throw ex;
    }

    /* called from transaction code */
    public void forwardOrphanMessage(PacketReference ref,
                  ConsumerUID consumer)
         throws BrokerException
    {
        Consumer c = getConsumer(consumer);
        if (c == null) {
            // no more consumer -> ack us
        	 logger.log(Logger.DEBUG, "Could not find " + consumer + " in " + getName() + "size="+consumers.size());
        	 Iterator iter = consumers.keySet().iterator();
        	 while (iter.hasNext())
        	 {
        		 Object v = iter.next();
        		 logger.log(Logger.DEBUG, "consumer= " + v);
             	
        	 }
             
            logger.log(Logger.DEBUG,"Dumping orphan message " + ref);
            try {
                if (ref.acknowledged(consumer,
                        consumer, false, false)) {
                    removeMessage(ref.getSysMessageID(),
                        RemoveReason.ACKNOWLEDGED);
                }
            } catch (Exception ex) {
                logger.logStack(Logger.DEBUG,"Error forwarding orphan", ex);
            }
        }
  
        Set matches = new HashSet();
        matches.add(c);
        forwardMessage(matches, ref);      
    }


   public void forwardMessage(Set matching, PacketReference msg)
         throws BrokerException
   { 
        Set remote = null;


        if (matching == null || matching.isEmpty()) {
            removeMessage(msg.getSysMessageID(), RemoveReason.ACKNOWLEDGED); 
        } else {
            Iterator con_itr = matching.iterator();
            while (con_itr.hasNext()) {
                   Consumer c = (Consumer)con_itr.next();
                   // OK .. this is ugly
                   // I really wanted to treat the remote consumers the
                   // same as local consumers (merging topics)
                   // but I dont want to send multiple copies of the
                   // message for 3.0 clients PLUS there looks like
                   // there is a timing hole in 3.0 where a message
                   // arriving late (e.g. durable) could be removed
                   // without tracking acks IF it was received just
                   // as the last consumer was acking it
                   // when we only send over 1 interest to deal w/ the
                   // ack flood, this should improve (maybe raptor
                   // but probably 3.6)
                   //
                   // For now .. I'm handling the non-durable clients seperately
                   //
                   if (c.isFalconRemote()) { // message to a remote broker
                       if (remote == null) {
                           remote = new HashSet();
                       }
                       remote.add(c);
                   } else {
                     
                       if (!c.routeMessage(msg, false)) {
                           boolean acked = false;
                           try {
                               ConsumerUID cid = c.getConsumerUID();
                               acked = msg.acknowledged(cid, 
                                     c.getStoredConsumerUID(),
                                     !cid.isUnsafeAck(), true);
                               if (acked)
                                   removeMessage(msg.getSysMessageID(), null);
                           } catch (IOException ex) {
                              //XXX ?? 
                           }
                       }
                   }
           }
        }
        if (remote != null && !remote.isEmpty()) {
            // send the messages directly to the remote broker
            // do not pass go, do not collect $200, do not
            // deal w/ flow control
            Globals.getClusterBroadcast().forwardMessage(msg, remote);
        }
    }       

    public Consumer addConsumer(Consumer c, boolean notify, Connection conn) 
        throws BrokerException, SelectorFormatException
    {

        if (c instanceof Subscription) {
            if (consumers.get(c.getConsumerUID()) != null) {
                return null;
            }
        }
        super.addConsumer(c,notify, conn);
        hasNoLocalConsumers |= c.getNoLocal();

        Selector selector = c.getSelector();
        Set s = null;

        synchronized (selectorToInterest) {
            s = (Set)selectorToInterest.get(selector);
            if ( s == null) {
                s = new HashSet();
                selectorToInterest.put(selector,s);
                selectors.add(selector);
            }
        }
        synchronized (s) {
            s.add(c);
        }

        if (!(c instanceof Subscription)) {
            notifyConsumerAdded(c, conn);
        }

        return null; 
    }

    public void removeConsumer(ConsumerUID interest, boolean notify)
        throws BrokerException {
        removeConsumer(interest, null, false, notify);
    }

    public void removeConsumer(ConsumerUID interest, Map remotePendings, 
                               boolean remoteCleanup, boolean notify)
                               throws BrokerException 
    {
        Consumer c = (Consumer)consumers.get(interest);
        if (c == null) {
            return; 
        }

        Set s = null;
        synchronized (selectorToInterest) {
            s = (Set)selectorToInterest.get(c.getSelector());
            if (s != null) {
                synchronized (s) {
                    s.remove(c);
                    if (s.isEmpty()) {
                        selectorToInterest.remove(c.getSelector());
                        selectors.remove(c.getSelector());
                    }
                }
            }
        }
        super.removeConsumer(interest, remotePendings, remoteCleanup, notify);

        if (!(c instanceof Subscription)) {
            notifyConsumerRemoved();
        }
    }


    public void sort(Comparator c) {
    }
 

    protected void getDestinationProps(Map m) {
        super.getDestinationProps(m);
        m.put(MAX_SHARE_CONSUMERS,new Integer(maxSharedConsumers));
        m.put(SHARED_PREFETCH, new Integer(sharedPrefetch));
    }

    public void setDestinationProperties(Map m) 
        throws BrokerException
    {
        super.setDestinationProperties(m);
        if (m.get(MAX_SHARE_CONSUMERS) != null) {
           try {
               setMaxSharedConsumers(((Integer)m.get(
                     MAX_SHARE_CONSUMERS)).intValue());
           } catch (Exception ex) {
               logger.log(Logger.INFO,"Internal Error ", ex);
           }

        }
        if (m.get(SHARED_PREFETCH) != null) {
           try {
               setSharedFlowLimit(((Integer)m.get(
                     SHARED_PREFETCH)).intValue());
           } catch (Exception ex) {
               logger.log(Logger.INFO,"Internal Error ", ex);
           }

        }
     }
    public DestMetricsCounters getMetrics() {
        DestMetricsCounters dmc = super.getMetrics();
        return dmc;

        // TBD add MAX_SHARE_CONSUMERS, SHARED_PREFETCH
    }

    
    public void setMaxSharedConsumers(int max) {
       maxSharedConsumers = max;
    }
    public void setSharedFlowLimit(int prefetch) {
       sharedPrefetch = prefetch;
    }

    public int getMaxNumSharedConsumers() {
        return maxSharedConsumers;
    }
    public int getSharedConsumerFlowLimit() {
        return sharedPrefetch;
    }

}
