// MemoryControl.java
// -------------------------------------------
// (C) 2005 by Michael Peter Christen; mc@yacy.net, Frankfurt a. M., Germany
// first published 22.09.2005 on http://yacy.net
//
// $LastChangedDate: 2011-08-18 00:24:17 +0200 (Do, 18. Aug 2011) $
// $LastChangedRevision: 7883 $
// $LastChangedBy: orbiter $
//
// LICENSE
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

package net.yacy.kelondro.util;

import java.util.concurrent.atomic.AtomicBoolean;

import net.yacy.cora.util.Memory;

/**
 * Standard implementation to get information about memory usage or try to free some memory
 */
public class StandardMemoryStrategy extends MemoryStrategy {

    private final long[] gcs = new long[5];
    private int gcs_pos = 0;
    private long properByte = 0L; // treshold
    private long prevTreshold = 0L;
    private int tresholdCount = 0;
    private boolean proper = true;

    public StandardMemoryStrategy() {
    	name = "Standard Memory Strategy";
    	error= false; //since this is the standard implementation we assume always false here
    }

    /**
     * Runs the garbage collector if last garbage collection is more than last millis ago
     * @param last time which must be passed since lased gc
     * @param info additional info for log
     */
    @Override
    protected final synchronized boolean gc(final int last, final String info) { // thq
    	assert last >= 10000; // too many forced GCs will cause bad execution performance
        final long elapsed = System.currentTimeMillis() - lastGC;
        if (elapsed > last) {
            final long before = free();
            final long start = System.currentTimeMillis();
            System.gc();
            lastGC = System.currentTimeMillis();
            final long after = free();
            this.gcs[this.gcs_pos++] = after - before;
            if (this.gcs_pos >= this.gcs.length) this.gcs_pos = 0;

            if (log.isFine()) log.info("[gc] before: " + Formatter.bytesToString(before) +
                                              ", after: " + Formatter.bytesToString(after) +
                                              ", freed: " + Formatter.bytesToString(after - before) +
                                              ", rt: " + (lastGC - start) + " ms, call: " + info);
            return true;
        }

        if (log.isFinest()) log.finest("[gc] no execute, last run: " + (elapsed / 1000) + " seconds ago, call: " + info);
        return false;
    }

    /**
     * This method calculates the average amount of bytes freed by the last GCs forced by this class
     * @return the average amount of freed bytes of the last forced GCs or <code>0</code> if no
     * GC has been run yet
     */
    protected final long getAverageGCFree() {
        long x = 0;
        int y = 0;
        for (final long gc : this.gcs)
            if (gc != 0) {
                x += gc;
                y++;
            }
        return (y == 0) ? 0 : x / y;
    }

    /**
     * memory that is free without increasing of total memory taken from os
     * @return bytes
     */
    @Override
    protected final long free() {
        return Memory.free();
    }

    /**
     * memory that is available including increasing total memory up to maximum
     * @return bytes
     */
    @Override
    protected final long available() {
        return Memory.available();
    }

    /**
	 * maximum memory the Java virtual will allocate machine; may vary over time in some cases
	 * @return bytes
	 */
    @Override
    protected final long maxMemory()
    {
    	return Memory.maxMemory();
    }

	/**
	 * currently allocated memory in the Java virtual machine; may vary over time
	 * @return bytes
	 */
    @Override
    protected final long total()
	{
		return Memory.total();
	}

	/**
     * <p>Tries to free a specified amount of bytes.</p>
     * <p>
     *   If the currently available memory is enough, the method returns <code>true</code> without
     *   performing additional steps. If not, the behaviour depends on the parameter <code>force</code>.
     *   If <code>false</code>, a Full GC is only performed if former GCs indicated that a GC should
     *   provide enough free memory. If former GCs didn't but <code>force</code> is set to <code>true</code>
     *   a Full GC is performed nevertheless.
     * </p>
     * <p>
     *   Setting the <code>force</code> parameter to false doesn't necessarily mean, that no GC may be
     *   performed by this method, if the currently available memory doesn't suffice!
     * </p>
     * <p><em>Be careful with this method as GCs should always be the last measure to take</em></p>
     *
     * @param size the requested amount of free memory in bytes
     * @param force specifies whether a GC should be run even in case former GCs didn't provide enough memory
     * @return whether enough memory could be freed (or is free) or not
     */
    @Override
    protected boolean request(final long size, final boolean force, AtomicBoolean shortStatus) {
        if (size <= 0) return true;
        final boolean r = request0(size, force);
        shortStatus.set(!r);
        return r;
    }
    private boolean request0(final long size, final boolean force) {
    	final long avg = getAverageGCFree();
    	// if (avg >= size) return true; // optimistic view, GC may just has happened
        long avail = available();
        if (avail >= size) return true;
        if (log.isFine()) {
            final String t = new Throwable("Stack trace").getStackTrace()[1].toString();
            log.fine(t + " requested " + (size >> 10) + " KB, got " + (avail >> 10) + " KB");
        }
        if (force || avg == 0 || avg + avail >= size) {
            // this is only called if we expect that an allocation of <size> bytes would cause the jvm to call the GC anyway

            final long memBefore = avail;
            final boolean performedGC = gc(10000, "serverMemory.runGC(...)");
            avail = available();
            if (performedGC) {
                final long freed = avail - memBefore;
                log.info("performed " + ((force) ? "explicit" : "necessary") + " GC, freed " + (freed >> 10)
                    + " KB (requested/available/average: "
                    + (size >> 10) + " / " + (avail >> 10) + " / " + (avg >> 10) + " KB)");
            }
            checkProper(avail);
            return this.proper && avail >= size;
        }
        if (log.isFine()) log.fine("former GCs indicate to not be able to free enough memory (requested/available/average: "
                + (size >> 10) + " / " + (avail >> 10) + " / " + (avg >> 10) + " KB)");
        return false;
    }

    /**
     * memory that is currently bound in objects
     * @return used bytes
     */
    @Override
    protected final long used() {
        return Memory.used();
    }

    @Override
    protected boolean properState() {
    	return this.proper;
    }

    @Override
    protected void resetProperState() {
    	this.proper = true;
    	this.tresholdCount = 0;
    }

    /**
     * set the memory to be available
     */
    @Override
    protected void setProperMbyte(final long mbyte) {
    	this.properByte = mbyte << 20; // convert to byte
    	this.tresholdCount = 0;
    }

    private void checkProper(final long available) {
    	// disable proper state if memory is less than treshold - 4 times, maximum 11 minutes between each detection
    	if ((available) < this.properByte) {
    		final long t = System.currentTimeMillis();
    		if(this.prevTreshold + 11L /* minutes */ * 60000L > t) {
    			this.tresholdCount++;
    			if(this.tresholdCount > 3 /* occurencies - 1 */) this.proper = false;
    		}
    		else this.tresholdCount = 1;

    		this.prevTreshold = t;

			log.info("checkProper: below treshold; tresholdCount: " + this.tresholdCount + "; proper: " + this.proper);
    	}
    	else if (!this.proper && (available) > (this.properByte * 2L)) // we were wrong!
    		resetProperState();
    }

}
