/*
 * 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.
 */

/*
 * @(#)MemoryManager.java	1.29 06/29/07
 */ 

package com.sun.messaging.jmq.jmsserver.util.memory;

import com.sun.messaging.jmq.util.DiagManager;
import com.sun.messaging.jmq.util.DiagManager.Data;
import com.sun.messaging.jmq.util.DiagDictionaryEntry;
import com.sun.messaging.jmq.jmsserver.Globals;
import com.sun.messaging.jmq.jmsserver.config.*;
import com.sun.messaging.jmq.jmsserver.resources.*;
import com.sun.messaging.jmq.util.log.*;
import java.util.*;
import java.lang.reflect.Constructor;
import com.sun.messaging.jmq.util.timer.*;
import com.sun.messaging.jmq.Version;
import com.sun.messaging.jmq.jmsserver.management.agent.Agent;


/**
 * class which handles the broker memory management
 */

public class MemoryManager implements DiagManager.Data
{
    private static boolean NO_GC_DEFAULT = false;

    private static final String GC_JAVA_VERSION="1.4.2";

    private static final String PACKAGE =
         "com.sun.messaging.jmq.jmsserver.util.memory.levels.";



    protected static final Logger logger = Globals.getLogger();

    static {
        NO_GC_DEFAULT=getNoGCDefault();
    }

    private static boolean getNoGCDefault() {
        boolean NoGCDefault = false;
        Version v = Globals.getVersion();
        int cmp = v.compareVersions((String)System.getProperties()
                  .get("java.version"), GC_JAVA_VERSION, true) ;
        if (cmp < 0) {
            // version is before 1.4.2
            NoGCDefault = false;
        } else {  // 1.4.2 or later
            NoGCDefault = true;
        }

        logger.log(Logger.DEBUGHIGH,"NoGC commuted from JDK:  " + NoGCDefault);
        return NoGCDefault;
    }

    public boolean NO_GC=Globals.getConfig().getBooleanProperty(
              "imq.memory_management.nogc", NO_GC_DEFAULT);

    private static boolean DEBUG = false;

    ArrayList diagDictionary = null;

    /**
     * set of memory handlers
     */
    MemoryLevelHandler[] levelHandlers = null;

    /**
     * level to enter a specific memory handler
     */
    long[] byteLevels = null;

    /**
     * memory state the broker is currently in
     */
    protected int currentLevel = 0;


    /**
     * memory state string [Displayed in diag]
     */
    protected String currentLevelString = "";

    /**
     * minimum amount of memory needed for the broker
     * runtime [Displayed in diag]
     */
    protected long baseMemory;


    /**
     * maximum size of a message allowed on the system
     * (-1 indicates no limit)
     * [displayed in diag]
     */
    protected long maxMessageSize;


    /**
     * variable which stores the value of Max memory on the
     * broker [ displayed in diag]
     */
    protected long maxAvailableMemory;

    /**
     * max memory as retrived from Runtime.maxMemory()
     */
    protected long maxSizeOfVM;

    /**
     * variable which stores the current Heap Size of the
     * broker [ displayed in diag]
     */
    protected long totalMemory;

    /**
     * variable which contains the currently available free
     * memory (based on total Memory .. not max memory)
     */
    protected long freeMemory;

    /**
     * variable which contains the currently used
     * memory (totalMemory - freeMemory)
     */
    protected long allocatedMemory;


    /**
     * variable which contains the true available memory
     * (maxAvailableMemory - (usedMemory)
     */
    protected long availMemory;



    /**
     * producer count
     */
    protected int producerCount = 0;


    /**
     * JMQSize value
     */
    protected int JMQSizeValue = 0;

    /**
     * JMQBytes value
     */
    protected long JMQBytesValue = 0;

    /**
     * JMQMaxMessageSize value
     */
    protected long JMQMaxMessageSize = 0;


    /*
     * Average Memory Usage
     */
    protected long averageMemUsage = 0;


    /**
     * High Memory Usage
     */
    protected long highestMemUsage = 0;

    /**
     * number of memory checks
     */
    protected long memoryCheckCount = 0; 
    
    /**
     * time in level
     */
    protected long timeInLevel = 0;

    /**
     * cumulative time in level
     */
    protected long cumulativeTimeInLevel = 0;
    

    /**
     * time Memory mgt was started
     */
    protected long startTime = 0;


    /**
     * amount of memory which must be freed in
     * a gc() to preform another iteration
     */
    private static final int GC_DELTA_DEFAULT = 1024; // 1K
    private static int GC_DELTA = Globals.getConfig().getIntProperty(
         Globals.IMQ + ".memory.gcdelta", GC_DELTA_DEFAULT);

    /**
     * amount of buffer memory in the system
     * (used when calculating memory limits to
     * add in a little extra room
     */
    private static long OVERHEAD_MEMORY_DEFAULT = 1024*10;

    private static long OVERHEAD_MEMORY = Globals.getConfig().getLongProperty(
                    Globals.IMQ+".memory.overhead", OVERHEAD_MEMORY_DEFAULT);


    private boolean turnOffMemory=
         !Globals.getConfig().getBooleanProperty(
         Globals.IMQ + ".memory_management.enabled", true);

    /**
     * the amount the system must be BELOW the
     * threshold of a level to re-enter that
     * level
     */
    private static int THRESHOLD_DELTA_DEFAULT = 1024;
    private static int THRESHOLD_DELTA = 
                 Globals.getConfig().getIntProperty(
                 Globals.IMQ+".memory.hysteresis", 
                 THRESHOLD_DELTA_DEFAULT);


    private HashMap callbacklist = new HashMap();
    private List pausedList = new ArrayList();

    private boolean active = false;

    /**
     * thread which wakes up and periodically checks memory
     */

    private class MyTimerTask extends TimerTask 
    {
           public void run() {
              checkMemoryState();
           }
    }

    public Hashtable getDebugState() {
        Hashtable ht = new Hashtable();
        ht.put("turnOffMemory", Boolean.valueOf(turnOffMemory));
        ht.put("active", Boolean.valueOf(active));
        ht.put("noForcedGC", Boolean.valueOf(NO_GC));
        ht.put("baseMemory", new Long(baseMemory));
        ht.put("maxMessageSize", new Long(maxMessageSize));
        ht.put("maxAvailableMemory", new Long(maxAvailableMemory));
        ht.put("maxSizeOfVM", new Long(maxSizeOfVM));
        ht.put("totalMemory", new Long(totalMemory));
        ht.put("freeMemory", new Long(freeMemory));
        ht.put("allocatedMemory", new Long(allocatedMemory));
        ht.put("availMemory", new Long(availMemory));
        ht.put("allocatedMemory", new Long(allocatedMemory));
        ht.put("JMQBytesValue", new Long(JMQBytesValue));
        ht.put("JMQMaxMessageSize", new Long(JMQMaxMessageSize));
        ht.put("averageMemUsage", new Long(averageMemUsage));
        ht.put("highestMemUsage", new Long(highestMemUsage));
        ht.put("memoryCheckCount", new Long(memoryCheckCount));
        ht.put("timeInLevel", new Long(timeInLevel));
        ht.put("cumulativeTimeInLevel", new Long(cumulativeTimeInLevel));
        ht.put("startTime", new Long(startTime));
        ht.put("OVERHEAD_MEMORY", new Long(OVERHEAD_MEMORY));
        ht.put("currentLevel", new Integer(currentLevel));
        ht.put("producerCount", new Integer(producerCount));
        ht.put("JMQSizeValue", new Integer(JMQSizeValue));
        ht.put("GC_DELTA", new Integer(GC_DELTA));
        ht.put("THRESHOLD_DELTA", new Integer(THRESHOLD_DELTA));
        ht.put("currentLevelString", currentLevelString);
        if (byteLevels != null) {
            ht.put("byteLevels#", new Integer(byteLevels.length));
            Vector v = new Vector();
            for (int i=0; i < byteLevels.length; i ++) {
                v.add(new Long(byteLevels[i]));
            }
            ht.put("byteLevels", v);
        }
        if (levelHandlers != null) {
            Vector v = new Vector();
            ht.put("levelHandlers#", new Integer(levelHandlers.length));
            for (int i=0; i < levelHandlers.length; i ++) {
                v.add(levelHandlers[i].getDebugState());
            }
            ht.put("levelHandlers", v);
        }
        ht.put("pausedList#", new Integer(pausedList.size()));
        ht.put("callbacklist#", new Integer(callbacklist.size()));
        if (pausedList.size() > 0) {
            Vector v = new Vector();
            for (int i=0; i < pausedList.size(); i ++) {
                v.add(pausedList.get(i).toString());
            }
            ht.put("pausedList", v);
         }
         if (callbacklist.size() > 0) {
            Vector v = new Vector();
            Iterator itr = callbacklist.values().iterator();
            while (itr.hasNext()) {
                v.add(itr.next().toString());
            }
            ht.put("callbacklist", v);
         }
           
        return ht;
    }

    private Object stateChangeLock = new Object();
    private Object valuesObjectLock = new Object();
    private Object timerObjectLock = new Object();

    private MyTimerTask mytimer = null;

    public MemoryManager() {
        if (turnOffMemory) {
            JMQSizeValue = -1;
            JMQBytesValue = -1;
            JMQMaxMessageSize = -1;
            return;
        }

        maxSizeOfVM = Runtime.getRuntime().maxMemory()
               - (64*1024*1024*1024);
        maxAvailableMemory = maxSizeOfVM
                          - OVERHEAD_MEMORY_DEFAULT;
        String[] levels = Globals.getConfig().getArray(
                Globals.IMQ + ".memory.levels");
        if (levels == null) {
            levels = new String[0];
        }
            
        try {
            levelHandlers = new MemoryLevelHandler[levels.length];
            byteLevels = new long[levels.length];
            for (int i = 0; i < levels.length; i ++) {
                 String fullclassname = Globals.getConfig().getProperty(
                     Globals.IMQ + "." + levels[i] + ".classname");
                 if (fullclassname == null) {
                     StringBuffer classname = new StringBuffer(levels[i]);
                     // capitalize first letter
                     char first = classname.charAt(0);
                     char upper = Character.toUpperCase(first);
                     classname.setCharAt(0, upper);
    
                     fullclassname = PACKAGE + classname;
                  }
                  if (DEBUG)
                      logger.log(Logger.DEBUG,
                          "Loading level " + levels[i] 
                          + " as " + fullclassname);
                  Class myclass = Class.forName(fullclassname);
                  Class cons_args[] = {String.class};
                  Constructor constructor = myclass.getConstructor(cons_args);
                  Object args[] = { levels[i]};
                  levelHandlers[i] = (MemoryLevelHandler)
                                constructor.newInstance(args);
                  byteLevels[i] = levelHandlers[i].getThresholdPercent() 
                                 * maxAvailableMemory / 100;
            }
        } catch (Exception ex) {
            logger.logStack(Logger.WARNING,
                  BrokerResources.E_INTERNAL_BROKER_ERROR,
                  "loading memory manager", ex);
        }
    }

    public void stopManagement() {
        active = false;
        synchronized (timerObjectLock) {
            if (mytimer != null) {
                mytimer.cancel();
                mytimer = null;
            }
        }
    }

    public void startManagement() {
        if (turnOffMemory) {
              logger.log(Logger.DEBUG,
                 "Memory Management turned off");
              return;
        }
        if (active) {
              logger.log(Logger.DEBUG, "Memory Management already active");
              return;
        }
        active = true;

        // always display some basic startup information in DEBUG
        // (even if DEBUG flag is false)
        logger.log(Logger.DEBUG,
                "Starting Memory Management: adjusted available"
                + " memory is " + (maxAvailableMemory/1024) + "K");
        logger.log(Logger.DEBUG,"Explicitly GC : " + (!NO_GC));
        for (int i = 0; i < levelHandlers.length; i ++) 
            logger.log(Logger.DEBUG, "LEVEL:" + levelHandlers[i].levelName()
                    + "[percent = " 
                    + levelHandlers[i].getThresholdPercent() 
                    + "%"
                    + ", bytes = " 
                    + (byteLevels[i]/(long)1024) 
                    + "K]");
 
        
        DiagManager.register(this);
        startTime = System.currentTimeMillis();
        baseMemory = Runtime.getRuntime().totalMemory() - 
                     Runtime.getRuntime().freeMemory();
        // update state and memory variables
        currentLevel = calculateState();
        MemoryLevelHandler currentHandler = levelHandlers[currentLevel];
        currentLevelString = currentHandler.levelName();

        currentHandler.enter(false);
        // get initial Size/Bytes values
        JMQSizeValue = currentHandler.getMessageCount(
                      availMemory, producerCount);
        JMQBytesValue = currentHandler.getMemory(
                            availMemory, producerCount);
        JMQMaxMessageSize = maxAvailableMemory/2;
        updateMaxMessageSize(-2);

        mytimer = new MyTimerTask();
        long time = currentHandler.getTimeBetweenChecks();

        try {
            Globals.getTimer().schedule(mytimer, time, time);
        } catch (IllegalStateException ex) {
            logger.log(Logger.DEBUG,"Timer canceled " , ex);
        }

    }


    public int getJMQSize() {
        synchronized (valuesObjectLock) {
            return JMQSizeValue;
        }
    }

    public long getJMQBytes() {
        synchronized (valuesObjectLock) {
            return JMQBytesValue;
        }
    }

    public long getJMQMaxMsgBytes() {
        synchronized (valuesObjectLock) {
            return JMQMaxMessageSize;
        }
    }


    /**
     * request notification when the caller can resume.
     * specifying an optional # of bytes which may be available
     *
     * @param cb class requesting notification
     * @param bytes bytes of memory which must be available
     *              before the client can resume (or 0
     *              if free memory is not important)
     */



    public synchronized void notifyWhenAvailable(MemoryCallback cb, long bytes)
    {
        if (DEBUG) {
            logger.log(Logger.DEBUG,
                "Registering notifyWhenAvailable at " 
                + bytes + " for " + cb);
        }
        if (turnOffMemory|| !active) {
             cb.resumeMemory(JMQSizeValue, JMQBytesValue, JMQMaxMessageSize);
             return;
        }
        checkMemoryState();
        synchronized (valuesObjectLock) {
              // force slowing using callback in orange
             if ((bytes == 0 || bytes < JMQBytesValue) && JMQSizeValue > 1) {
               cb.resumeMemory(JMQSizeValue, JMQBytesValue, JMQMaxMessageSize);
               return;
             }
         }
        // wait for notification
        MemoryCallbackEntry mce = (MemoryCallbackEntry)callbacklist.get(cb);
        if (cb == null) {
            mce = new MemoryCallbackEntry();
            callbacklist.put(cb, mce);
            mce.cb = cb;
            mce.keepAfterNotify = false;
        }
        mce.paused = true;
        mce.bytes = bytes;
        synchronized (pausedList) {
            pausedList.add(mce);
        }
        return;
    }



    /**
     * request notification when memory levels have changed
     * because the system has changed memory levels
     *
     * @param cb class requesting notification
     */


    public  void registerMemoryCallback(MemoryCallback cb)
    {
        if (DEBUG) {
            logger.log(Logger.DEBUG,
                "Registering registerMemoryCallback  for " + cb);
        }
        MemoryCallbackEntry mce = new MemoryCallbackEntry();
        mce.cb = cb;
        mce.keepAfterNotify = true;
        mce.paused = false;
        mce.bytes = 0;
        synchronized (callbacklist) {
            callbacklist.put(cb, mce);
        }
    }

    public  void removeMemoryCallback(MemoryCallback cb) {
        synchronized (callbacklist) {
            callbacklist.remove(cb);
        }
    }


    public int getCurrentLevel() {
        synchronized (stateChangeLock) {
            return currentLevel;
         }
    }

    public String getCurrentLevelName() {
        synchronized (stateChangeLock) {
	    return (levelHandlers[currentLevel].localizedLevelName());
         }
    }

    /**
     * notifies all MemoryCallbackEntry objects that the bytes, count, etc
     * have changed.
     * If override is false, it will only notify non-paused clients
     *
     * @param override if true, notify even if paused
     */

    public  void notifyAllOfStateChange(boolean override) {

        if (turnOffMemory|| !active) {
             return;
        }

// GENERAL notes:

//    resumeMemory is called when a pauses state can be resumed
//    updateMemory is called for any other state change

        List l = new ArrayList();

        synchronized (callbacklist) {
            l.addAll(callbacklist.values());
        }

        long BytesValue = 0;
        int SizeValue = 0;
        long MaxMessageSize = 0;
        synchronized (valuesObjectLock) {
            BytesValue = JMQBytesValue;
            SizeValue = JMQSizeValue;
            MaxMessageSize = JMQMaxMessageSize;
        }

        if (DEBUG) {
            logger.log(Logger.DEBUGMED,
                    "notifyAllOfStateChange [size,bytes,max] = ["
                    +JMQSizeValue +","+ JMQBytesValue+","
                    + JMQMaxMessageSize + "]");
        }
        Iterator waiting = l.iterator();
        while (waiting.hasNext()) {
            MemoryCallbackEntry mce = (MemoryCallbackEntry)waiting.next();
            if (!override && mce.paused) {
                continue; // ignore it, will be cause when pause is handled
            } else if (mce.paused) { // we are paused but getting a notification
                                     // decide if we should be removed from
                                     // the pausedList
                if (mce.bytes == 0 && mce.bytes < BytesValue) {
                     // heck .. send a resume
                     mce.paused = false;
                     synchronized (pausedList) {
                         pausedList.remove(mce);
                     }
                     mce.cb.resumeMemory(SizeValue, BytesValue, MaxMessageSize);
                     if (DEBUG) {
                         logger.log(Logger.DEBUGMED, 
                             "\tresumeMemory for  "  +mce.bytes 
                             +" bytes on "+ mce.cb);
                     }
                } else { // not resuming, just notifying
                     if (DEBUG) {
                         logger.log(Logger.DEBUGMED,
                             "\tupdateMemory for  " + mce.cb);
                     }
                     mce.cb.updateMemory(SizeValue, BytesValue, MaxMessageSize);
                }
            } else { 
                if (DEBUG) {
                    logger.log(Logger.DEBUGMED, 
                        "\tupdateMemory for  " + mce.cb);
                }
                mce.cb.updateMemory(SizeValue, BytesValue, MaxMessageSize);
            }
        }
    }

    public  void checkAndNotifyPaused()
    {
        if (DEBUG) {
            logger.log(Logger.DEBUGMED,
                    "checkAndNotifyPaused [size,bytes,max] = ["
                    +JMQSizeValue +","+ JMQBytesValue
                    +","+ JMQMaxMessageSize + "]");
        }
        List l = null;
        synchronized (pausedList) {
            if (pausedList.isEmpty()) {
                return; // nothing to do
            }
            l = new ArrayList();
            l.addAll(pausedList);
        }
        long BytesValue = 0;
        int SizeValue = 0;
        long MaxMessageSize = 0;
        synchronized (valuesObjectLock) {
            BytesValue = JMQBytesValue;
            SizeValue = JMQSizeValue;
            MaxMessageSize = JMQMaxMessageSize;
        }

        Iterator waiting = l.iterator();
        while (waiting.hasNext()) {
            MemoryCallbackEntry mce = (MemoryCallbackEntry)waiting.next();
            if (mce.bytes == 0 && mce.bytes < BytesValue) {
                     // heck .. send a resume
                 mce.paused = false;
                 waiting.remove();
                 synchronized (pausedList) {
                     pausedList.remove(mce);
                 }
                 if (DEBUG) {
                     logger.log(Logger.DEBUGMED, "\tresumeMemory for  " +
                         mce.bytes +" bytes on " + mce.cb);
                 }
                 mce.cb.resumeMemory(SizeValue, BytesValue, MaxMessageSize);
            }
         
        }
    }


    public String toString() {
        return "MemoryManager";
    }


    public synchronized void addProducer() {
        producerCount++;
        if (DEBUG) {
            logger.log(Logger.DEBUG, "addProducer " +producerCount);
        }
    }

    public synchronized void removeProducer() {
        producerCount--;
        if (DEBUG) {
            logger.log(Logger.DEBUG, "removeProducer " + producerCount);
        }
    }

    public synchronized void removeProducer(int cnt) {
        producerCount -= cnt;
        if (DEBUG) {
            logger.log(Logger.DEBUG,
                "removeProducer(" + cnt + ") " + producerCount);
        }
    }

    private boolean quickState(int state) {
        if (turnOffMemory|| !active) {
             return false;
        }
        freeMemory = Runtime.getRuntime().freeMemory();
        totalMemory = Runtime.getRuntime().totalMemory();
        allocatedMemory = totalMemory - freeMemory;
        if (state < (byteLevels.length -1) && 
            allocatedMemory > byteLevels[state +1]) {
            // increased
            return true;
        }
        return false;
    }

    /**
     * @param size bytes addition to check
     * @return false if allocate size of mem causes level change
     *               or currentLevel not in lowest level
     */
    public boolean allocateMemCheck(long size) { 
        if (DEBUG) {
            int level = currentLevel;
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < byteLevels.length; i++) {
                sb.append("byteLevel["+i+"]="+byteLevels[i]+", ");
            }
            long freem = Runtime.getRuntime().freeMemory();
            long totalm = Runtime.getRuntime().totalMemory();
            long allocatedm = totalm - freem;
           
            logger.log(logger.INFO, 
            "MemoryManager: turnOffMemory="+turnOffMemory+", active="+active+
            ", check size="+size+", currentLevel="+level+"("+byteLevels.length+"), "+sb.toString()+
            ", freemem="+freem+", totalmem="+totalm+", allocatedmem="+allocatedm);
        }

        if (turnOffMemory|| !active) {
             return true;
        }

        int currentl = currentLevel;
        if (currentl >= (byteLevels.length -1)) return false; 

        long freem = Runtime.getRuntime().freeMemory();
        long totalm = Runtime.getRuntime().totalMemory();
        long allocatedm = totalm - freem + size;
        if (allocatedm > byteLevels[byteLevels.length-1]) {
            return false;
        }
        return true;
    }

    private int calculateState() {
        // update all of the various memory values which
        // are required for determining the level
        if (turnOffMemory|| !active) {
             return 0;
        }

        freeMemory = Runtime.getRuntime().freeMemory();
        totalMemory = Runtime.getRuntime().totalMemory();
        allocatedMemory = totalMemory - freeMemory;
        long foo = Runtime.getRuntime().maxMemory();

        if (allocatedMemory > maxAvailableMemory)
            recalcMemory();

        availMemory = maxAvailableMemory - allocatedMemory;

        int targetstate = 0;
        for (int i = byteLevels.length-1; i >= 0; i --) {
            if (allocatedMemory > byteLevels[i]) {
                targetstate = i;
                break;
            }
        }

        int returnstate = targetstate;
        if (targetstate < (byteLevels.length -1)
           && allocatedMemory > byteLevels[targetstate+1]- THRESHOLD_DELTA) {
            if (DEBUG) {
                logger.log(Logger.DEBUGMED,
                    "calculateState:didnt meet delta requirements");
            }
            returnstate = targetstate + 1;
        }

        if (DEBUG) {
            logger.log(Logger.DEBUGMED,
                "calculateState [oldstate, calcstate, returnstate] = [" 
                + currentLevel + "," + targetstate + "," + returnstate + "]");
        }

        return returnstate;

    }


    /**
     * Update the MaxMessageSize:
     * @param size value of MAX_MESSAGE_SIZE property (or -2 if
     *        being called because maxAvailableMemory was updated)
     */
    public void updateMaxMessageSize(long size) {
        synchronized (valuesObjectLock) {
            if (size != -2)
                maxMessageSize = size;

            JMQMaxMessageSize = maxAvailableMemory/2;
            if (maxMessageSize > -1 && maxMessageSize < JMQMaxMessageSize) {
                 JMQMaxMessageSize = maxMessageSize;
            }
            if (DEBUG) {
                logger.log(Logger.DEBUG, 
                    "updateMaxMessageSize [size, JMQMaxMessageSize] = [" 
                    + size + "," + JMQMaxMessageSize + "]");
            }
        }
    }


    /*
     * main processing for the memory manager:
     *
     * Logic:
     *
     * First the new state (based on free memory) is
     * calculated.
     *
     * Next the system tries to determine if we have
     * entered a new "higher" (tighter) memory state
     *     - system performs gc and cleanup tasks
     *          until all tasks are done OR we have
     *          moved back to our old memory state
     *     - if its still in the same (new) state, 
     *          the system enters the new state
     *
     * If we haven't entered a higher memory state, 
     * the system tries to determine if we have entered
     * a "lower" memory state.
     *     - if we are in a lower memory state the system
     *       calls leave() on the last state
     *     -  sets boolean to update waiting clients (if necessary)
     *
     * If we are still in the same state as last iteration
     * the system:
     *     may try to gc (if we periodically gc in the
     *     existing level) -> determined by gcIterations
     *
     *
     *     - calculate the current state
     *     - is the new state > the old state
     *         yes - entering lower memory state
     *            - garbage collect -> still in state
     *                  yes -> try and clean up memory
     *                     after cleanup-> still in state
     *                          yes -> Move to new state
     * Finally, the memory count and memory size values for
     * clients are updated AND any resume messages are
     * sent
     */
    public void quickMemoryCheck() {
        if (quickState(currentLevel)) {
            // we are SLOW
            checkMemoryState();
        }
    }

    boolean completedRunningCleanup = true;
    int cleanupCnt = 0;

    public  void checkMemoryState() {
        if (turnOffMemory|| !active) {
             return;
        }
        // OK -> first check memory
        // calculate new state

        if (DEBUG) {
            logger.log(Logger.DEBUG, "checkMemoryState  " + memoryCheckCount);
        }


        boolean notify = false;

        int oldLevel = 0;
        int newState = 0;
        synchronized (stateChangeLock) {
            newState = calculateState();
            oldLevel = currentLevel;
            currentLevel = newState;    
        }
        MemoryLevelHandler currentHandler = levelHandlers[oldLevel];

        if (newState != oldLevel) {

            MemoryLevelHandler newHandler = levelHandlers[newState];

            if (newState > oldLevel) { // entered new state
                gc(newHandler.gcCount());
                newState = calculateState();
            }
            for (int i = oldLevel; i < newState; i ++) { // higher
                notify = levelHandlers[i+1].enter(false);
                notify |= levelHandlers[i].leave(true);
            }
            for (int i = oldLevel; i > newState; i --) { // lower
                notify |= levelHandlers[i-1].enter(true);
                notify |= levelHandlers[i].leave(false);
            }
            newHandler = levelHandlers[newState];

            // update variables 
            synchronized (valuesObjectLock) {
                JMQSizeValue = newHandler.getMessageCount(
                        availMemory, producerCount);
                JMQBytesValue = newHandler.getMemory(
                        availMemory, producerCount);
            }

            if (notify) {
                notifyAllOfStateChange(true);
            }
            currentLevel = newState;
            if (newState > oldLevel) { // cleanup
                completedRunningCleanup = false;
                cleanupCnt = 0;
                completedRunningCleanup = 
                    levelHandlers[newState].cleanup(cleanupCnt++);
            } else if (newState < oldLevel) {
                completedRunningCleanup = true;
                cleanupCnt = 0;
            }
            if (newState != oldLevel) {
                String args[] = {levelHandlers[newState].localizedLevelName(), 
                        levelHandlers[oldLevel].localizedLevelName(), 
                        String.valueOf((allocatedMemory/1024)), 
                        String.valueOf((
                          allocatedMemory*100/maxAvailableMemory))};

                logger.log(Logger.INFO, 
                           BrokerResources.I_CHANGE_OF_MEMORY_STATE,
                           args);

                Agent agent = Globals.getAgent();
                if (agent != null)  {
                    agent.notifyResourceStateChange(levelHandlers[oldLevel].localizedLevelName(),
					levelHandlers[newState].localizedLevelName(),
					null);
                }

                currentLevel = newState;
                currentHandler = newHandler;    
                currentLevelString = currentHandler.levelName(); // diags

                synchronized (timerObjectLock) {

                    // set new timer
                    if (mytimer != null) {
                        mytimer.cancel();
                        mytimer = null;
                    }
                    mytimer = new MyTimerTask();
                    long time = currentHandler.getTimeBetweenChecks();
                    try {
                        Globals.getTimer(true).schedule(mytimer, time, time);
                    } catch (IllegalStateException ex) {
                        logger.log(Logger.DEBUG,"Timer canceled " , ex);
                    }
                }
            }

        } else {
            if (!completedRunningCleanup) {
                completedRunningCleanup = levelHandlers[oldLevel].cleanup(
                         cleanupCnt++);
            }
            // periodically perform gcs on some iterations
            if (currentHandler.gcIteration() != 0 &&
                memoryCheckCount % currentHandler.gcIteration() == 0) {
                 gc(); 
            }
            // update variables 
            synchronized (valuesObjectLock) {
                JMQSizeValue = currentHandler.getMessageCount(
                        availMemory, producerCount);
                JMQBytesValue = currentHandler.getMemory(
                        availMemory, producerCount);
            }


            if (JMQSizeValue > 0)
                checkAndNotifyPaused();
        }

        if (allocatedMemory > highestMemUsage)
             highestMemUsage = allocatedMemory;

        // XXX racer revisit
        // we may not want to calculate this on each call
        averageMemUsage = ((averageMemUsage*memoryCheckCount) 
            + allocatedMemory)/ (memoryCheckCount + 1);

        memoryCheckCount++;



    }

    public void forceRedState() {
        long size =   Runtime.getRuntime().maxMemory() -
                      Runtime.getRuntime().freeMemory();
        logger.log(Logger.WARNING, BrokerResources.W_EARLY_OUT_OF_MEMORY,
                String.valueOf(size), 
                String.valueOf(Runtime.getRuntime().maxMemory()));
        recalcMemory();
        checkMemoryState();
    }

    public void recalcMemory() {
        // update memory values
        maxAvailableMemory = Runtime.getRuntime().totalMemory();
        updateMaxMessageSize(-2);

        for (int i = 0; i < byteLevels.length; i ++) {
            byteLevels[i] = levelHandlers[i].getThresholdPercent() 
                           * maxAvailableMemory / 100;
        }
    }


    protected void gc() {
        gc(1, GC_DELTA);
    }
    protected void gc(int count) {
        gc(count, GC_DELTA);
    }

    protected void gc(int count, long delta) {
        if (!NO_GC) {
        	 logger.log(Logger.DEBUG,"calling Runtime.freeMemory()");
            long free = Runtime.getRuntime().freeMemory(); 
            int i = 0;
            for (i = 0; i < count; i ++) {
                Runtime.getRuntime().gc();
                long newfree = Runtime.getRuntime().freeMemory();
                if (free - newfree > delta) {
                     // we freed enough memory
                     break;
                }

            }
        }
        else{
        	// do nothing
        	
        }
    }



    /* 
     * ---------------------------------------------------
     *             DIAG SUPPORT
     * ---------------------------------------------------
     */
    public synchronized List getDictionary() {
        if (diagDictionary == null) {
            diagDictionary = new ArrayList();
            diagDictionary.add(new DiagDictionaryEntry("currentLevelString", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("allocatedMemory", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("timeInLevel", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("cumulativeTimeInLevel", 
                                 DiagManager.COUNTER));
            diagDictionary.add(new DiagDictionaryEntry("JMQSizeValue", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("JMQBytesValue", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("JMQMaxMessageSize", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("totalMemory", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("freeMemory", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("availMemory", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("maxAvailableMemory", 
                                 DiagManager.CONSTANT));
            diagDictionary.add(new DiagDictionaryEntry("producerCount", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("averageMemUsage", 
                                 DiagManager.VARIABLE));
            diagDictionary.add(new DiagDictionaryEntry("memoryCheckCount", 
                                 DiagManager.COUNTER));
            diagDictionary.add(new DiagDictionaryEntry("highestMemUsage", 
                                 DiagManager.COUNTER));
            diagDictionary.add(new DiagDictionaryEntry("maxSizeOfVM", 
                                 DiagManager.CONSTANT));

        }
        return diagDictionary;
    }




    public  void update() {
        calculateState(); // update memory, ignore new state
        MemoryLevelHandler currentHandler = null;
        synchronized (stateChangeLock) {
            currentHandler = levelHandlers[currentLevel];
        }
        timeInLevel = currentHandler.getCurrentTimeInLevel();
        cumulativeTimeInLevel = currentHandler.getTotalTimeInLevel();
    }

    public String getPrefix() {
        return "mem_mgr";
    }

    public String getTitle() {
        return "MemoryManager";
    }


    public String toDebugString() {
        String retstr = "MemoryManager: [" + currentLevel + "]"+"\n";
        for (int i = 0; i < levelHandlers.length; i ++) {
            retstr += "\t" + i + "\t" + levelHandlers[i].levelName()
                      +"\t" + levelHandlers[i].getThresholdPercent()
                      + "\t" + byteLevels[i] + "\n";
        }
        for (int i = 0; i < levelHandlers.length; i ++) {
               retstr += "-------------------------------\n";
               retstr += levelHandlers[i].toDebugString()+" \n\n";
        }
        return retstr;
        
    }



}

class MemoryCallbackEntry
{
    MemoryCallback cb = null;
    long bytes = 0;
    boolean paused = false;
    boolean keepAfterNotify = false;

    public String toString() {
        return "MCE[ bytes=" + bytes +", paused=" + paused
            + ", keepAfterNotify="+keepAfterNotify
            + ", object = " + cb.toString() + "]";
    }
}
